ci: registry login before pulling CHROMELESS_IMAGE (first real-image run found the gap) #50

Merged
triform-admin merged 9 commits from fix/ci-registry-login into main 2026-07-30 13:59:13 +00:00

Setting CHROMELESS_IMAGE flipped smoke/e2e/native-peer-gate from skip-green to real runs — and all four image consumers immediately failed no basic auth credentials (registry.triform.cloud requires auth; job-container docker daemons don't inherit the runner pod's imagePullSecrets).

Login step before each pull site (ci smoke, both gate jobs, e2e before compose-up). Conditional on the REGISTRY_PASSWORD secret existing → forks/GitHub with a public image keep working. --password-stdin, registry host derived from the image ref.

Secrets are set on the repo. This PR's own CI is the verification: smoke/gates/e2e should now pull and RUN the cr7727-0a0d28c67858 image — the first-ever real-image CI execution.

🤖 Generated with Claude Code

Setting `CHROMELESS_IMAGE` flipped smoke/e2e/native-peer-gate from skip-green to real runs — and all four image consumers immediately failed `no basic auth credentials` (registry.triform.cloud requires auth; job-container docker daemons don't inherit the runner pod's imagePullSecrets). Login step before each pull site (ci smoke, both gate jobs, e2e before compose-up). Conditional on the `REGISTRY_PASSWORD` secret existing → forks/GitHub with a public image keep working. `--password-stdin`, registry host derived from the image ref. Secrets are set on the repo. This PR's own CI is the verification: smoke/gates/e2e should now pull and RUN the cr7727-0a0d28c67858 image — the first-ever real-image CI execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ci: registry login before pulling CHROMELESS_IMAGE (first real-image run found the gap)
Some checks failed
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 42s
CI / Docs link check (pull_request) Successful in 1m44s
CI / Container smoke test (pull_request) Failing after 3m22s
CI / Lint (pull_request) Successful in 3m58s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 12m4s
E2E / docker-compose + Playwright (pull_request) Failing after 16m22s
49c08b079b
Setting CHROMELESS_IMAGE to the in-cluster registry flipped smoke/e2e/
native-peer-gate from skip-green to real runs — and all four image
consumers immediately failed 'no basic auth credentials':
registry.triform.cloud requires auth and the runners' dind daemons had
none (runner pods pull via imagePullSecrets; job-container docker
daemons do not inherit that).

Add a login step before each pull site (ci smoke, both native-peer-gate
jobs, e2e before compose-up — compose pulls the image itself). Login is
conditional on the REGISTRY_PASSWORD secret existing, so forks and
GitHub — where the secrets are absent — keep working with a public
image. Password via --password-stdin, never argv. Registry host derived
from the image ref, not hardcoded.

Secrets REGISTRY_USERNAME/REGISTRY_PASSWORD are set on the repo (from
the cluster's registry-pull dockerconfig).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(runtime): chromium never starts in a plain container — /dev/console sink is firecracker-only
Some checks failed
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CI / Docs link check (pull_request) Successful in 37s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 23s
CI / Lint (pull_request) Successful in 1m20s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 1m2s
CI / Container smoke test (pull_request) Failing after 1m50s
E2E / docker-compose + Playwright (pull_request) Failing after 6m33s
e27423f996
The first REAL container runs of CI (PR #50: registry login fixed,
smoke/e2e actually booted the image) failed with supervisord retrying
  spawnerr: unknown error making dispatchers for 'chromium': EACCES
forever. Root cause: [program:chromium] stderr_logfile=/dev/console
(9cb4d01, CV2-ICE observability — firecracker captures the guest
console to serial.log). A k8s pod or docker container HAS no
/dev/console, supervisord can't open its dispatcher, and chromium
never spawns. Every container-runtime consumer of this image was
broken; only firecracker guests worked. (Verified pre-existing: the
old cr7727-78e5bb4 image reproduces identically — this was never a
regression of the promotion, it was latent since 2026-06-24.)

Fix in entrypoint.sh, the seam that knows the substrate: if
/dev/console is not writable, symlink it to the chromium.err.log file
sink; if /dev is read-only (runAsNonRoot), exec supervisord on a
sed-rewritten runtime copy of the conf in /run instead. Firecracker
guests keep their serial stream untouched; containers get a file.

Requires an image rebuild to take effect (context stages
infra/lifecycle/ verbatim, STEP 9).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: re-run against cr7727-e27423f99694 (image now carries the console fix)
Some checks failed
CI / Lint (pull_request) Has started running
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 46s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 46s
E2E / docker-compose + Playwright (pull_request) Failing after 2m4s
CI / Docs link check (pull_request) Successful in 3m23s
CI / Container smoke test (pull_request) Failing after 7m43s
d97d914c2a
ci: re-run — forge 503'd during the t5 node failure (postgres failed over to t8 in ~15min)
Some checks failed
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CodeQL / Analyze go (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 2m46s
CI / Docs link check (pull_request) Successful in 3m10s
CI / Container smoke test (pull_request) Failing after 4m51s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 4m59s
E2E / docker-compose + Playwright (pull_request) Failing after 11m30s
CI / Lint (pull_request) Successful in 17m16s
39db4cc261
fix(runtime): conf-rewrite fallback must write /run/supervisor, not /run
Some checks failed
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CI / Docs link check (pull_request) Successful in 11s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 29s
CI / Container smoke test (pull_request) Failing after 42s
CI / Lint (pull_request) Successful in 1m38s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 7m6s
E2E / docker-compose + Playwright (pull_request) Failing after 11m16s
f9e46a1434
First deploy of the console fallback died at the fallback itself:
/run is root:755 in this image — only /run/user/1000, /run/supervisor
and /run/chromeless-session are chowned to cbuser (Dockerfile.runtime:
84-91). The symlink attempt correctly failed (read-only /dev for
non-root), then the sed-rewrite hit 'line 45: /run/supervisord.
runtime.conf: Permission denied' and the container exited before
supervisord ever started — smoke saw 'container exited before
DevTools came up'. Write the runtime conf into /run/supervisor/,
which the image already owns to us.

Reproduced + located in-cluster with a bare pod of the failing image
(exact same two-line failure), not guessed from CI output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: re-run against cr7727-f9e46a1434b5 (boot-tested in-cluster: all 4 programs RUNNING)
Some checks failed
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CodeQL / Analyze go (pull_request) Has been skipped
CI / Docs link check (pull_request) Successful in 52s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 1m22s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 2m53s
CI / Container smoke test (pull_request) Failing after 4m8s
CI / Lint (pull_request) Successful in 4m30s
E2E / docker-compose + Playwright (pull_request) Failing after 4m53s
8edacce316
The previous image cr7727-e27423f99694 carried only the symlink-fallback
half of the console fix; its conf-rewrite path wrote a root-owned /run
and died 'Permission denied'. f9e46a1 writes /run/supervisor instead and
was verified in a bare k8s pod before this re-run: xvfb, pulseaudio,
chromium, devtools-proxy all RUNNING with 0 restarts, DevTools
/json/version answering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ci: fix the four defects the first real-image run exposed
Some checks failed
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CodeQL / Analyze go (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 34s
CI / Docs link check (pull_request) Successful in 3m22s
CI / Lint (pull_request) Failing after 3m33s
CI / Container smoke test (pull_request) Successful in 3m43s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 3m54s
E2E / docker-compose + Playwright (pull_request) Failing after 4m23s
b20b92ab49
Setting CHROMELESS_IMAGE flipped smoke/e2e/both gates from skip-green to
actually running the worker, and every one of them failed. Four independent
causes, each pre-existing and invisible while the jobs skipped. All four were
reproduced in-cluster inside the real ci-tools job container before fixing.

1. setup-node installs a glibc node onto a musl runner (killed 4 jobs)
   ubuntu-latest maps to ci-tools:latest = Alpine/musl. actions/setup-node@v4
   downloads the official glibc node tarball; it extracts fine, so setup-node
   reports success ("Adding to the cache", "npm: 10.8.2"), and then nothing
   can exec it -- /lib64/ld-linux-x86-64.so.2 does not exist:
     .../node: cannot execute: required file not found
     exec .../node: no such file or directory      <- post-steps, exit 255
   The poisoned PATH persists, so EVERY post step of EVERY JS action 255s and
   the job fails even when its work passed: Lint's tsc + all 217 vitest tests
   PASSED, then Post-Node/Post-Go/Post-checkout each 255'd -> job failed.
   Both native-peer gates died with nothing of theirs having run. setup-go is
   unaffected only because Go ships static binaries.
   Fix: drop setup-node; use the node ci-tools already ships (v22, musl) and
   assert it exists. Verified in-container: npm ci + tsc + 15 files/217 tests
   green on the image's own node.

2. smoke asserted an idle-watchdog the M7 image doesn't have
   Step 3 polled 20s for /var/log/supervisor/idle-watchdog.log to prove
   IDLE_TIMEOUT_S propagated. supervisord.phase2.conf deliberately has no
   idle-watchdog program -- the native peer owns lifecycle -- so the log can
   never appear and the smoke could never pass on any cr7727-* image.
   Fix: probe the baked conf for the program; skip honestly when absent.

3. smoke's image fallback built a Dockerfile deleted in OSS-W1
   Every resolve branch fell back to a docker build of infra/Dockerfile,
   removed when M7 deleted capture/streamer-page/. So "image absent" surfaced
   as "lstat infra: no such file or directory" / "docker build failed". That
   is exactly what a pruned image looks like here: dind-gc's high-water
   image-prune with until=30m evicts the image mid-job (observed live -- it
   ate the image between two assertions of one gate run).
   Fix: pull when a tag is named, and say so when that fails.

4. e2e ran compose subcommands without CHROMELESS_IMAGE
   compose.yaml declares it REQUIRED via the interpolation-error form, and
   every subcommand interpolates the whole file. status/log-capture/teardown
   each lacked it in their own env -> "required variable CHROMELESS_IMAGE is
   missing a value", and the always() teardown failed the job by itself.
   Fix: pass it to every compose step; make teardown non-fatal + add an
   explicit rm -f sweep (a real cbwrtc-chromium was found still running 40min
   after its job died, respawning chromium every 30s).

Also e2e: probe whether the docker daemon shares the job's filesystem before
bringing the stack up. It does not here (job container -> sibling dind over
tcp), so compose's bind-mounts cannot resolve: the prometheus.yml file-mount
failed dir-onto-file, and the ../client dir-mounts would have silently given
an EMPTY tree -- specs failing against a stack that never contained the app.
Now it skips with a notice rather than a red that says nothing.

Verified: fixed smoke run end-to-end against
registry.triform.cloud/chromeless/chromeless:cr7727-f9e46a1434b5 inside
ci-tools on forgejo-runner-7 -- all 7 steps, real navigation to example.com,
17121-byte valid PNG, "container-boot: PASS".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ci: three more real-image defects, incl. one the lint should have caught
Some checks failed
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Successful in 43s
CI / Container smoke test (pull_request) Successful in 2m10s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 1m30s
CI / Lint (pull_request) Failing after 5m49s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 5m2s
CI / Docs link check (pull_request) Has been cancelled
c81cc4b119
Round 2 of the same exercise. The node fix worked -- both gates now actually
RUN (node: v22.15.1) and assertions #1/#2 PASS against the real image with
`mode: docker` evidence for the first time, and Container smoke test went
green. What remained were three further defects, each with a different cause.

1. upload-artifact@v4 cannot work on this host (failed 3 jobs)
   It RESOLVES on the mirror -- so it passed the existing action lint, which
   only checks resolvability -- and then aborts at runtime:
     GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
     and download-artifact@v4+ are not currently supported on GHES.
   v4 talks to GitHub's artifact backend service, which Forgejo does not
   implement. Because the steps carried `if: always()`, they failed jobs whose
   own work had PASSED: the scaffold gate printed `verdict=PASS exitCode=0`
   and was then reported red by its own artifact upload.
   Fix: print the payload into the run log (the log IS the artifact here), and
   teach tools/lint/workflow_actions_lint.py this second failure class --
   resolvable is not the same as runnable. Checked tree-wide per the repo
   convention before landing: it found two MORE latent instances
   (harness-loopback.yml, release.yml) that had never been exercised. Both
   fixed rather than exempted; release.yml's SBOM now goes to the registry via
   cosign, which this host can actually do.

2. hadolint was unpinned and unthresholded
   Lint passed at 09:10Z and failed at 13:06Z with an unchanged Dockerfile.
   `hadolint/hadolint:latest` was rebuilt at 12:53Z -- 13 min before the
   failing run -- and the new version reports two more findings. No
   --failure-threshold was set either, so hadolint's default (info) applied
   and ANY info-level finding failed the job.
   Both findings are deliberate: DL3025 (HEALTHCHECK must be shell form -- the
   check is `curl … || exit 1`, which exec form cannot express) and DL3066
   (`USER cbuser` is a named user on purpose).
   Fix: pin v2.12.0 + --failure-threshold warning. Verified on a runner: rc=0
   at BOTH thresholds, confirming the pin alone is the fix and the threshold
   is defence in depth.

3. the strict gate is unsatisfiable by construction
   Strict requires every assertion PASS, and counts NOT_YET_IMPLEMENTED as
   FAIL. But #3 codec-cap and #4 native-peer are still M0 shims that return
   NYI unconditionally, and #3's real probe needs `ctx.client` from the R7
   boot harness -- which runGate never constructs. So no --position and no
   image can make strict green until M1/M3 wire those probes. It only ever
   "passed" by skipping, before CHROMELESS_IMAGE was set; running it for the
   first time exposed that.
   Fix: continue-on-error, so it still reports the honest status of the M7
   surface without failing PRs on its own unimplemented probes. The scaffold
   job remains the required check per verification/README.md; strict becomes
   required when M1+M3 land and the flag comes off.

Also: e2e still carried its own setup-node -- same glibc-on-musl trap fixed in
the other two workflows last commit. Removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ci: ci-tools ships node but not npm — install it from the distro
All checks were successful
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CI / Docs link check (pull_request) Successful in 18s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 49s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 57s
E2E / docker-compose + Playwright (pull_request) Successful in 1m5s
CI / Container smoke test (pull_request) Successful in 2m40s
CI / Lint (pull_request) Successful in 5m4s
4e16ad4206
Round 2 left one gap. Dropping setup-node was right (it installs a glibc node
onto this musl job container and poisons PATH), but I checked only that
ci-tools provides `node`. It does not provide `npm`:

  $ docker run --rm registry.triform.cloud/ci-tools:latest sh -c 'npm --version'
  sh: npm: not found            # and no npx either

My earlier in-container check passed because that container had had `apk add
npm` run in it while probing something else — the classic contaminated-probe
mistake. Lint on c81cc4b then failed exactly where the new step asserts:

  node: v22.15.1  npm: MISSING
  ::error::node present but npm missing in the job image

That assertion did its job: the failure is one clear line naming the cause,
rather than an inscrutable 255 three steps later. Everything else in the round-2
batch is confirmed working by the same run -- hadolint v2.12.0 passed (the pin
WAS the fix), and the smoke, scaffold gate, strict gate and e2e jobs all went
green.

Fix: install npm from the distro when absent, so it matches the image's own
node ABI -- which is the entire point of not using setup-node. Guarded on
apk/apt-get so it stays portable to a GitHub-hosted runner, where npm is
already present and neither branch runs. e2e needs the same (it uses npm +
npx for Playwright).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
triform/chromeless!50
No description provided.