ci: make CI executable + close the drift loops it never caught #44

Merged
triform-admin merged 17 commits from agent/oss-ci-verification-loops into main 2026-07-30 03:19:28 +00:00

Why

This repo's CI has never been green. 171 of 175 runs failed — zero successes across June–July 2026. It was executing the whole time; it just failed on every trigger, so nothing ever caught drift. That single fact explains most of what this PR fixes: the dead infra/Dockerfile, the never-read config.js, the unpullable Helm sidecar, two live silent-failure bugs in Cb.startNativeSession, and an entirely unimplemented security control.

Fixing the code without fixing the loop just resets the clock, so this does both.

What

1. CI can actually pass. Forgejo git-clones every uses: in a job before running any step, so one unresolvable action kills the job regardless of its if: (logs show skipping post step for 'actions/checkout@v4'; step was not executed). Seven actions 404 on data.forgejo.org; all replaced with portable run: steps or gated. github-script alone caused 56 failures, codeql-action another 38.

2. Three host-runnable checks — no Chromium tree, no Docker, ~13s via make verify:

  • workflow_actions_lint.py — every uses: must resolve on the mirror
  • cxx_include_lint.py — Chromium symbols used without their header (capture/ only compiles inside a 4–8h build, so this class otherwise surfaces hours later)
  • test_cxx_include_lint.py — 9 self-tests so a broken linter fails loudly

CI now also runs the client's 207 vitest assertions, which were never gated.

3. Region scoping (aud) enforced. processRegion was set at startup and never read; Claims had no Aud field. A token minted for eu-west-1 was accepted by a us-east-1 server. The test asserting this has been failing silently the entire time.

4. Two live Cb.startNativeSession bugs. signalingUseTls was silently dropped (→ wss:// at a plaintext broker); array-form iceServers fell through to Google public STUN, so the relay harness believed it was on provisioned TURN and wasn't. Neither path fails quietly now.

5. Runnable for people who aren't triform. SIGNALING_URL is finally translated into the WEBRTC_SIGNALING_* vars the peer reads — previously only the k8s controller could start a session, so docker run booted a peerless worker. Plus bring-your-own-image compose, a Helm default that made every install fail, and a README that described an abandoned architecture.

Verification

make verify green: 207 client + all signaling/integration tests, all linters, all workflow YAML, every action resolving.

⚠️ The C++ is not compiled. No Chromium tree was available. Every API was checked against in-tree precedent on branch-heads/7727, which caught a real defect (BindOnce/FROM_HERE without their headers) — now enforced by the include linter. Treat capture/ changes as unverified until the build lane runs.

build/chromeless-build.sh Step 7 is now fatal by default; it previously logged WARN: continuing and shipped anyway, and the opt-in flag was set by nothing, anywhere.

## Why This repo's CI has never been green. **171 of 175 runs failed — zero successes across June–July 2026.** It was executing the whole time; it just failed on every trigger, so nothing ever caught drift. That single fact explains most of what this PR fixes: the dead `infra/Dockerfile`, the never-read `config.js`, the unpullable Helm sidecar, two live silent-failure bugs in `Cb.startNativeSession`, and an entirely unimplemented security control. Fixing the code without fixing the loop just resets the clock, so this does both. ## What **1. CI can actually pass.** Forgejo git-clones *every* `uses:` in a job before running *any* step, so one unresolvable action kills the job regardless of its `if:` (logs show `skipping post step for 'actions/checkout@v4'; step was not executed`). Seven actions 404 on `data.forgejo.org`; all replaced with portable `run:` steps or gated. `github-script` alone caused 56 failures, `codeql-action` another 38. **2. Three host-runnable checks** — no Chromium tree, no Docker, ~13s via `make verify`: - `workflow_actions_lint.py` — every `uses:` must resolve on the mirror - `cxx_include_lint.py` — Chromium symbols used without their header (`capture/` only compiles inside a 4–8h build, so this class otherwise surfaces hours later) - `test_cxx_include_lint.py` — 9 self-tests so a broken linter fails loudly CI now also runs the client's 207 vitest assertions, which were never gated. **3. Region scoping (`aud`) enforced.** `processRegion` was set at startup and **never read**; `Claims` had no `Aud` field. A token minted for `eu-west-1` was accepted by a `us-east-1` server. The test asserting this has been failing silently the entire time. **4. Two live `Cb.startNativeSession` bugs.** `signalingUseTls` was silently dropped (→ `wss://` at a plaintext broker); array-form `iceServers` fell through to **Google public STUN**, so the relay harness believed it was on provisioned TURN and wasn't. Neither path fails quietly now. **5. Runnable for people who aren't triform.** `SIGNALING_URL` is finally translated into the `WEBRTC_SIGNALING_*` vars the peer reads — previously only the k8s controller could start a session, so `docker run` booted a peerless worker. Plus bring-your-own-image compose, a Helm default that made every install fail, and a README that described an abandoned architecture. ## Verification `make verify` green: 207 client + all signaling/integration tests, all linters, all workflow YAML, every action resolving. ⚠️ **The C++ is not compiled.** No Chromium tree was available. Every API was checked against in-tree precedent on `branch-heads/7727`, which caught a real defect (`BindOnce`/`FROM_HERE` without their headers) — now enforced by the include linter. **Treat `capture/` changes as unverified until the build lane runs.** `build/chromeless-build.sh` Step 7 is now fatal by default; it previously logged `WARN: continuing` and shipped anyway, and the opt-in flag was set by nothing, anywhere.
Adds a read-only Cb.getCaptureStats method exposing the FrameSink
frame-production counter (frames_received_from_capturer) so the isolator
can verify a renderer is actually PRODUCING before freezing it into a
warm-snapshot golden.

ROOT CAUSE this enables fixing (poisoned warm-golden): the isolator's
golden-capture warmup starts FrameSink capture then blindly sleeps 3s and
assumes production. When the ~8% per-boot GPU wedge (run11: BeginFrame
ack-loop healthy but renderer produces ZERO frames) strikes during
capture, startFrameSinkCapture still SUCCEEDS (peer-independent pipe) so a
wedged, zero-producing renderer gets frozen into the golden. Every restore
is born-frozen; the poison persists on the per-worker hostPath cache. One
node (triform-6) took out ~25% of prod video this way.

The counter is the SAME frames_received the guest's cb_begin_frame_driver
uses to decide VERDICT=PRODUCING vs RENDERER-STARVED. Growing => healthy;
flat 0 => wedged (do not publish that golden).

Reuses the already-plumbed track_source_getter_ (lazy dispatch-time
resolve, CV2-69 order-safe); zero main_parts/content_browser_client
changes. Lock-free GetStats() copy, safe off-sequence. int32-saturated
CBOR (crdtp has no uint64 encoder; gate only reads 0-vs-growing).

Marker: CV2-CAPTURE-STATS (grep -a on the linked binary).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This repo's CI has never been green. Across June-July 2026: 171 of 175
runs failed, zero successes. It was executing the whole time -- Forgejo
does run .github/workflows -- and failing on every trigger.

Two root causes, both confirmed from production logs.

1. Unresolvable actions. Forgejo resolves actions from data.forgejo.org
   and git-clones EVERY `uses:` in a job before executing ANY step, so a
   single missing action fails the whole job regardless of its `if:`.
   The logs show "skipping post step for 'actions/checkout@v4'; step was
   not executed" -- the job died before checkout. Seven actions 404 there:

     actions/github-script            -> tools/ci/file_regression_issue.py
                                         (56 failures, largest bucket)
     github/codeql-action             -> whole job gated to github.com
                                         (38 failures)
     hadolint/hadolint-action         -> container `run:`
     lycheeverse/lychee-action        -> container `run:`
     anchore/sbom-action              -> upstream syft installer
     softprops/action-gh-release      -> tools/ci/publish_release.py
     actions/attest-build-provenance  -> removed (GitHub-only API)

   A job-level `if:` IS evaluated before resolution (verified against this
   Forgejo instance: 5229 jobs record status=skipped), which is what makes
   the CodeQL gate work where a step-level `if:` would not.

2. The image-building jobs built infra/Dockerfile, unbuildable since the
   M7 native-peer migration deleted capture/streamer-page/. Those jobs are
   now opt-in on a supplied CHROMELESS_IMAGE: a from-source Chromium build
   (4-8h) cannot run on a CI runner, and this repo publishes no images, so
   they report a notice and pass rather than failing forever.

Adds three host-runnable checks, none needing a Chromium tree or Docker:

  tools/lint/workflow_actions_lint.py  every `uses:` must exist on the
      mirror. Calibrated twice against reality: the mirror answers 403 for
      actions that EXIST and 404 for missing ones (so `>=400` was wrong),
      and the first regex missed subpath actions like
      github/codeql-action/init@v3 -- which is how those 38 failures hid
      from the linter itself.

  tools/lint/cxx_include_lint.py  catches Chromium symbols used without
      their header. capture/ only compiles inside a full Chromium checkout,
      so this class of defect otherwise surfaces hours later in the build
      lane. Motivating case: Cb.shutdown shipped using base::BindOnce and
      FROM_HERE with neither header included. The first draft's 20 rules
      produced 46 findings on code that builds, so the table was cut to 13
      high-confidence rules plus measured umbrella relationships.

  tools/lint/test_cxx_include_lint.py  9 self-tests pinning both
      directions -- catches the real defect, stays silent on transitive
      includes, comments, string literals, and reports true line numbers.

`make verify` runs all of it plus every unit and integration suite in ~13s.
CI also now runs the client's 14 vitest suites (207 assertions), which were
never gated -- only `npm run typecheck` was wired.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
initRegion() set processRegion at startup and NOTHING ever read it, and
Claims had no Aud field at all. A token minted for eu-west-1 was accepted
by a us-east-1 server. Region scoping was documented, tested, and entirely
unimplemented.

TestRegionAware_AudExcludesRegion has been asserting the 1008 close for
this the whole time and failing silently -- it was one of five failures
sitting on clean HEAD, invisible because CI never passed.

  * Claims.Aud, with an UnmarshalJSON that accepts the RFC 7519 4.1.3
    bare-string form as well as an array, so a spec-legal third-party
    token isn't reported as malformed claims.
  * regionPermitted(), opt-in by design: no aud claim -> permitted;
    server region unspecified -> permitted (a server that doesn't know
    its own region can't meaningfully enforce, and failing closed would
    break every single-region deployment the moment someone minted a
    scoped token); otherwise the server's region must appear in aud.
  * enforcement in verifyToken with a region_mismatch auth-failure metric.
  * dev issuer honours ?aud=a,b so the integration test can mint one.

Also deletes tests/integration/input_loop_test.go: it builds a binary from
capture/input-bridge/, a sidecar retired in M7 R4 whose source has zero
tracked files. Four more of the five failures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both bugs were LIVE and both failed silently, which is why they survived.

BUG-1: the handler read `useTls`, but callers write `signalingUseTls` --
every other signaling param carries that prefix, and cv2-fcdirect.mjs:113
sends it under that name with its own comment documenting it. The unknown
key was dropped, use_tls defaulted true, and the guest dialled wss:// at a
plaintext broker, surfacing minutes later as an opaque TLS error. Both
spellings are now accepted.

BUG-2: iceServers was read with FindString(), which returns nullptr on a
JSON list. An array-form caller therefore fell through to
BuildDefaultIceServers() = stun:stun.l.google.com:19302. The fcdirect relay
harness believed it was running on provisioned TURN and was running on
Google public STUN. Now accepts list, dict, and the legacy JSON-string form
(the isolator stores ice_servers_json as a string and forwards it verbatim).

The wider fix is that this path no longer fails quietly: an iceServers
param that was SUPPLIED but yields no usable servers is a hard ServerError
rather than a silent substitution; absence logs a warning; and
iceTransportPolicy=relay with zero TURN servers logs an error, because that
combination can never connect.

Cb.shutdown: wires the quit closure that WillRunMainMessageLoop has been
parking and nothing has ever run (the TODO at main_parts.cc:927 names this
exact method). It posts rather than runs inline so HandleCommand's response
lands before the loop quits -- otherwise the caller races a socket close
instead of seeing {"shuttingDown":true}. Quitting unwinds into
PostMainMessageLoopRun's LIFO teardown, so the peer gets a `bye` and the WS
closes 1000; SIGTERM skips all of that.

signaling: iceReplayMaxAge 60s -> 300s, configurable via
CHROMELESS_ICE_REPLAY_MAX_AGE_S and clamped to a 600s TURN-allocation
ceiling. The old value was justified as "longer than any realistic Phase-1
demo session-start latency", an assumption microVM cold-boot invalidates:
past 60s every buffered guest candidate ages out, the peer sees
remoteCandidates=0, and it sits in checking forever as 0x0 video. Physics
independently converged on 300s for exactly this reason.

NOT COMPILED. There is no Chromium tree on the machine this was written on;
capture/ needs the 4-8h build lane. Every API used was checked against
in-tree precedent on branch-heads/7727, which caught a real defect
(base::BindOnce + FROM_HERE without base/functional/bind.h or
base/location.h) -- now enforced by tools/lint/cxx_include_lint.py. Treat
the C++ as unverified until the build lane runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: make the repo runnable and honest for people who aren't triform
Some checks are pending
CI / Lint (pull_request) Waiting to run
CI / Container smoke test (pull_request) Waiting to run
CI / Docs link check (pull_request) Waiting to run
CodeQL / Analyze go (pull_request) Waiting to run
CodeQL / Analyze javascript-typescript (pull_request) Waiting to run
E2E / docker-compose + Playwright (pull_request) Waiting to run
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Waiting to run
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Waiting to run
95a6650a81
The documented quickstart could not work for anyone. Fixing that, plus the
front door that described an architecture this project abandoned.

infra/launch-chromeless.sh: translate SIGNALING_URL into the
WEBRTC_SIGNALING_* / WEBRTC_ICE_* vars the native peer actually reads
(cb_signaling_ws_client.cc:35-38). The script only ever knew SIGNALING_URL
and never translated it, so the ONLY thing in the repo that could start a
session was the Kubernetes controller -- a plain `docker run` booted a
CDP-only worker with no peer at all. An explicitly-set WEBRTC_* always
wins, so the controller path is byte-identical. Covered by 15 cases under
/bin/sh, including that precedence rule.

infra/Dockerfile + infra/supervisord.conf: deleted. The legacy
stock-Chromium + getDisplayMedia image contained none of the product (no
native peer, no FrameSink capture, no encoder factory, no Cb.* surface),
defaulted CHROMELESS_USE_FAKE_MEDIA=1 -- a synthetic green square -- and had
not been buildable since M7 deleted capture/streamer-page/ while the
Dockerfile still COPYd it. Keeping it would mean two runtimes and two
supervisord configs forever, to serve a demo that misrepresents the product.

infra/compose.yaml: bring-your-own-image via CHROMELESS_IMAGE, required
with no default. The browser is a 4-8h from-source Chromium build and this
repo publishes nothing, so there is no honest default -- compose fails fast
with a message instead of pulling a tag that doesn't exist.

client: main.ts hardcoded ws://localhost:8080/ws and never read the
__CBWRTC_CONFIG__ global that compose has always generated -- index.html
didn't even load config.js. So the documented "override SIGNALING_URL at
compose-up time" knob silently did nothing. New client/src/config.ts
resolves ?signaling= -> window.__CHROMELESS_CONFIG__ -> fallback, with 12
tests. Kept out of main.ts because that file resolves DOM nodes at import
time and cannot be unit-tested.

helm: inputBridge.enabled true -> false. It referenced
chromeless-input-bridge, an image with no Dockerfile anywhere in the repo
(sidecar retired in M7 R4), so every chart install failed on an unpullable
image. Template blocks stay, still gated on the flag.

build/chromeless-build.sh: Step 7 unit tests are now fatal by default. They
logged "WARN: continuing" and shipped the artifact anyway; the opt-in
CHROMELESS_TESTS_FATAL was set by nothing, anywhere. Renamed the escape
hatch to CHROMELESS_TESTS_NONFATAL so it names its actual purpose. A test
suite whose failures never block is not a test suite -- and this is the only
place C++ tests run at all.

README: rewritten. It claimed Phase 0, "don't fork Chromium until you have
to" (now inverted), a getDisplayMedia architecture diagram, a quickstart
marked "Coming Phase 0 exit", a helm command for an unpublished chart, and
linked to a deleted file. It now states the build cost up front rather than
burying it.

CLAUDE.md: new. Records what cost real time to rediscover -- no local
Chromium tree so verify C++ against in-tree precedent; .claude/worktrees/
poisoning every repo-wide grep; the local:true CDP requirement; that Forgejo
resolves all actions before any step; how to read CI logs out of postgres;
and which docs are history.

Also removes tests/webrtc/scenarios/portal-test-client/ and the two
portal scenarios: they test triform's input encoder, not chromeless, and
tracked a 55 KB triform wasm blob in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: re-trigger after cross-repo coalescer bug cancelled the first run
Some checks are pending
CI / Lint (pull_request) Waiting to run
CI / Container smoke test (pull_request) Waiting to run
CI / Docs link check (pull_request) Waiting to run
CodeQL / Analyze go (pull_request) Waiting to run
CodeQL / Analyze javascript-typescript (pull_request) Waiting to run
E2E / docker-compose + Playwright (pull_request) Waiting to run
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Waiting to run
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Waiting to run
4aab374b17
The superseded-run-coalescer cronjob cancelled all 8 jobs of the previous
run. Its dead_pr arm matches PRs by index alone:

  WHERE ('refs/pull/' || pr.index || '/head') = r.ref

with no pr.base_repo_id = r.repo_id predicate, so chromeless PR #44 (open)
matched cargoless #44 and tf-multiverse #44 (both closed/merged) and was
treated as dead. 11 open PRs instance-wide are currently exposed to this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: re-trigger to A/B the coalescer fix against live queued runs
Some checks are pending
CI / Lint (pull_request) Waiting to run
CI / Container smoke test (pull_request) Waiting to run
CI / Docs link check (pull_request) Waiting to run
CodeQL / Analyze go (pull_request) Waiting to run
CodeQL / Analyze javascript-typescript (pull_request) Waiting to run
E2E / docker-compose + Playwright (pull_request) Waiting to run
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Waiting to run
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Waiting to run
a7e4a8cbe4
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: TEMPORARILY run on branch push to dodge the coalescer bug
Some checks failed
CI / Lint (pull_request) Waiting to run
CI / Container smoke test (pull_request) Waiting to run
CI / Docs link check (pull_request) Waiting to run
CodeQL / Analyze go (pull_request) Waiting to run
CodeQL / Analyze javascript-typescript (pull_request) Waiting to run
E2E / docker-compose + Playwright (pull_request) Waiting to run
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Waiting to run
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Waiting to run
CI / Docs link check (push) Failing after 17m1s
CI / Container smoke test (push) Failing after 17m1s
CI / Lint (push) Failing after 17m1s
ccc6804d30
The superseded-run-coalescer cancels every refs/pull/ run for this PR
(cross-repo dead-PR match; one-line fix in tf-multiverse#9910, not yet
merged, and Flux reverts any live patch within 10m).

Its dead_pr arm is gated on `r.ref LIKE 'refs/pull/%'`, so a refs/heads/
run cannot be matched by it. Adding this branch to the push trigger is the
only way to get CI evidence for this PR before #9910 lands.

REVERT THIS COMMIT BEFORE MERGE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: re-run after the triform-5 disk-pressure outage
Some checks failed
CI / Docs link check (push) Successful in 1m33s
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CI / Container smoke test (push) Successful in 3m11s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 13m26s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Failing after 13m26s
E2E / docker-compose + Playwright (pull_request) Failing after 13m52s
CI / Docs link check (pull_request) Failing after 13m57s
CI / Container smoke test (pull_request) Failing after 15m14s
CI / Lint (pull_request) Failing after 15m16s
CI / Lint (push) Failing after 16m3s
8cd1e88576
The previous branch run started 12:12:48Z, three minutes into the
triform-5 eviction cycle that took out ingress-nginx-controller. All
three jobs hung 1021s on 'docker pull registry.triform.cloud/ci-tools'
and were killed. Infrastructure casualty, not a code failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: re-run Lint after the second triform-5 outage cancelled it
Some checks failed
CI / Container smoke test (push) Successful in 1m3s
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 34s
CI / Docs link check (push) Successful in 3m56s
CI / Lint (pull_request) Successful in 4m1s
E2E / docker-compose + Playwright (pull_request) Failing after 3m7s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 5m30s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 5m34s
CI / Container smoke test (pull_request) Successful in 6m41s
CI / Lint (push) Successful in 23m40s
47a4803b7f
Lint task 201377 was CANCELLED (status 3), not failed: its log ends
cleanly at 12:42:49 with 'all self-tests passed' and carries zero error
markers across all 90 lines. The runner then went silent for 12 minutes
through the 12:44-12:51 forge outage and the stale-job reaper killed it.

The parts that DID run are green: hadolint via container (replacing the
404ing action) and all 9 cxx-include-lint self-tests, including
'catches the Cb.shutdown defect' and 'capture/ is clean'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ci: drop the branch-push workaround; the coalescer bug is fixed
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 1m53s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 43s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 52s
E2E / docker-compose + Playwright (pull_request) Failing after 4m51s
CI / Lint (pull_request) Has been cancelled
CI / Container smoke test (pull_request) Has been cancelled
9fa4598de0
tf-multiverse#9910 merged and Flux has reconciled it — the live cronjob
now carries `AND pr.base_repo_id = r.repo_id`, so refs/pull/ runs are no
longer cancelled by a same-index closed PR in another repo.

The temporary push trigger existed only to dodge that. Reverting it
restores the intended trigger set (pull_request + push to main).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(ci): gate the compose teardown on the image probe too
Some checks failed
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CI / Container smoke test (pull_request) Successful in 41s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 46s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 44s
CI / Docs link check (pull_request) Successful in 2m49s
E2E / docker-compose + Playwright (pull_request) Failing after 11m16s
CI / Lint (pull_request) Successful in 16m39s
7ed2b2e828
The e2e job reported 'CHROMELESS_IMAGE not set - e2e skipped' correctly,
then failed anyway: the 'docker compose down' teardown ran unconditionally
under `if: always()`.

infra/compose.yaml declares CHROMELESS_IMAGE as REQUIRED (${VAR:?msg}), so
EVERY compose subcommand fails interpolation when it is unset -- including
`down`. The teardown therefore exited 1 and failed a job that had already
decided to skip.

Caught by the first e2e run that ever reached this code: the workflow had
been dying at action-resolution before today.

Swept the other workflows for the same class (a risky step ungated inside a
probe-guarded job); e2e.yml was the only instance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
THE root cause of cold-boot frozen video. A cold-boot cb-chromium guest
starts on about:blank; capture is armed on that page's FrameSinkId; then
the navigation to real content swaps the RenderViewHost (and its
FrameSinkId), but nothing re-points the capturer at the new sink. It
stays bound to the dead pre-nav sink -> VERDICT=RENDERER-STARVED,
frames_encoded=0, no video. Warm-restored golden sessions dodge this
(they resume already-on-content with no about:blank->nav transition),
which is why cold-boot was the sole frozen path -- and why the isolator
cold-boot fallback, though correct at the allocation layer, produced a
connected-but-frozen stream.

Fix: make capture self-healing at the layer that owns both the observer
and the capturer. CbActiveWebContentsResolver already observes the
captured WebContents and receives RenderViewHostChanged, but is
content-layer glue with no capturer handle (the FrameSink capturer is
deliberately content-agnostic -- FrameSinkIds only). So the resolver
gains an optional re-arm closure (SetRecaptureOnRvhSwapCallback) that
CloudBrowserBrowserMainParts supplies; on an RVH swap of the active
capture the resolver POSTs it (the new RWH's view/FSID may not be the
WC's live primary synchronously at RenderViewHostChanged -- a same-turn
UI PostTask runs after the swap settles). The owner
(RearmCaptureAfterRvhSwap) re-resolves WC->RWHV->RWH->GetFrameSinkId,
forces the fresh view SHOWING (the post-nav RWH boots HIDDEN -> renderer
throttled, same gating Cb.startFrameSinkCapture applies), calls
cb_track_source_->StartCapture on the new sink, and refreshes the
resolver's active target.

capturer.cc:138-164 confirms StartCapture on a NEW FrameSinkId re-targets
(ChangeTarget), not a no-op -- so the re-arm is effective. For a terminal
nav whose fresh RWHV isn't attached by the task turn (no later swap to
retrigger), the owner re-posts itself up to 5x/50ms (bounded) rather than
strand the capturer. Stale "Start is a no-op" comments in
cb_devtools_agent.cc + cb_framesink_video_track_source.{h,cc} that
predated the retarget path are corrected so this fix can't be mistakenly
reverted as dead code.

Layering preserved (capturer stays content-agnostic; re-arm lives in
build-integration where content deps already exist). Optional callback:
unset -> legacy invalidate-only behaviour, so tests/no-capturer builds
are unaffected. Adversarially reviewed (lifetime/UAF, re-entrancy,
threading, StartCapture-idempotency, includes, Show() walk) -- no
correctness bugs. Composes with the isolator cold-boot fallback (now a
COMPLETE fix, not frozen) and the per-language warm-slot fix.

Guest change -- needs a cb-chromium rootfs bake to deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge branch 'main' into agent/oss-ci-verification-loops
Some checks are pending
CI / Lint (pull_request) Waiting to run
CI / Container smoke test (pull_request) Waiting to run
CI / Docs link check (pull_request) Waiting to run
CodeQL / Analyze go (pull_request) Waiting to run
CodeQL / Analyze javascript-typescript (pull_request) Waiting to run
E2E / docker-compose + Playwright (pull_request) Waiting to run
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Waiting to run
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Waiting to run
8786bf36a9
fix(ci): gate the whole e2e job on CHROMELESS_IMAGE, not just its steps
All checks were successful
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Has been skipped
CI / Docs link check (pull_request) Successful in 1m53s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 3m50s
CI / Container smoke test (pull_request) Successful in 7m44s
CI / Lint (pull_request) Successful in 11m41s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 12m58s
19931987da
The step-level probe was correct but insufficient on Forgejo: a
step-level if: prevents only the step's execution — the job still
clones every action and runs the pre-probe setup steps. Observed on
PR #44's last run: the probe correctly reported 'e2e skipped' and the
job STILL failed, because setup-node's hostedtoolcache was broken on
forgejo-runner-6 (exec .../node: no such file or directory, exit 255
in the post-steps) — infrastructure the skipped job never needed.

A job-level if: is evaluated before action resolution and before any
step (codeql.yml already proves the pattern on this host), so an unset
CHROMELESS_IMAGE now skips the whole job green instead of gambling on
runner state.

The step-level probe stays: it guards the var-set-but-empty edge and
keeps every compose step independently gated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into agent/oss-ci-verification-loops
Some checks failed
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
CI / Container smoke test (pull_request) Successful in 25s
E2E / docker-compose + Playwright (pull_request) Has been skipped
CI / Docs link check (pull_request) Successful in 42s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 35s
CI / Lint (pull_request) Successful in 1m37s
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Failing after 4m30s
2e123b575f
# Conflicts:
#	capture/build-integration/cb_devtools_agent.cc
ci: re-run after forgejo server restart 503'd the strict gate's checkout
All checks were successful
CodeQL / Analyze go (pull_request) Has been skipped
CodeQL / Analyze javascript-typescript (pull_request) Has been skipped
E2E / docker-compose + Playwright (pull_request) Has been skipped
native-peer-gate / native-peer-gate-strict (M7 gate) (pull_request) Successful in 22s
CI / Container smoke test (pull_request) Successful in 2m12s
native-peer-gate / native-peer-gate-scaffold (permissive) (pull_request) Successful in 4m33s
CI / Docs link check (pull_request) Successful in 5m46s
CI / Lint (pull_request) Successful in 19m41s
920389e45e
The native-peer-gate-strict job's actions/checkout hit three 503s at
02:50:3xZ — exactly the window the forgejo server pod restarted
(old pod gone, new pod h6v4k started 02:50:34Z). Every other check on
2e123b5 was already green. No code change.
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!44
No description provided.