feat(ci): advisory pod e2e gate for the stable desktop release#2697
feat(ci): advisory pod e2e gate for the stable desktop release#2697Pritom14 wants to merge 5 commits into
Conversation
Adds a pod-based e2e gate to frontend-release.yml. After the release build publishes, an ephemeral Daytona pod installs the Linux .deb and runs the real-app smoke suite (launch + bundled-daemon-ready via Playwright's _electron.launch), posting an 'ao-stable-gate' commit status. ADVISORY for now: continue-on-error + publish-feed does NOT depend on it, so it proves itself on live releases without ever blocking a good build. Once trusted, add e2e-gate to publish-feed 'needs' to make it required. Manual override: workflow_dispatch skip_e2e_gate input. Hardening (CodeRabbit RCE lessons): the .deb is fetched on the runner and uploaded into the pod, so the pod needs no egress and holds no secret; DAYTONA_API_KEY only creates the pod, never enters it; the pod is ephemeral (deleted after each run). Verdict->exit-code->status contract is pure + unit-tested (deriveGateOutcome, 11 tests). Verified end-to-end against a published nightly: downloaded the .deb, spun a pod, ran the real-app suite (2 passed), AO_VERDICT passed. Part of AgentWrapper#2483.
whoisasx
left a comment
There was a problem hiding this comment.
Requesting changes on the release gate before this ships. The overall idea is good, but the current wiring has a couple of correctness gaps: the commit status likely cannot be posted with the granted token scopes, skipped setup failures can be reported as green, and the pod harness still requires egress for apt/npm even though the rollout/security model says it does not.
Verified locally: git diff --check origin/main...origin/pr-2697 passed, and node --test scripts/*.test.mjs passed 11/11. I could not run actionlint locally because it is not installed in this workspace.
…ess claim Addresses review on AgentWrapper#2697: - Token scopes: the e2e-gate job posts a commit status, which needs 'statuses: write' (a separate GITHUB_TOKEN scope); switch from 'contents: write' to 'contents: read' + 'statuses: write' so the status publish can't fail on missing scope. - Skip handling: a 'skipped' gate step now maps to green ONLY when skip_e2e_gate=true. If an earlier step (npm ci) fails, the gate is 'skipped' with the input unset -> now reported as failure, not a silent green. - Egress: boot-real.sh installs xvfb/tmux/@playwright/test only if ABSENT, so a prebuilt sandbox image runs egress-free; corrected the overclaim (the pod holds no secret + fetches no app code; toolchain fetch is the one remaining egress on a stock image, acceptable for the trusted stable gate, to be baked into the image for the untrusted per-PR gate). Verdict tests 11/11, node --check, bash -n, yaml, prettier all green. Part of AgentWrapper#2483.
whoisasx
left a comment
There was a problem hiding this comment.
Requesting one fix before this can be trusted as a release gate. The previous status-permission and skipped-gate issues are addressed, and the local pure checks pass, but the launch/paint smoke currently has a tautological assertion that can pass without proving the renderer painted. Please make that assertion prove an actual rendered UI signal. I also noted the pod still uses apt/npm egress on stock daytona-small; the updated boot-script wording makes that explicit, so I am not treating that as a separate blocker for this advisory stable gate.
| const win = await app.firstWindow(); | ||
| expect(win).toBeTruthy(); | ||
| // window has a title (renderer mounted) | ||
| await expect.poll(async () => (await win.title()) ?? "", { timeout: 30_000 }).not.toBeNull(); |
There was a problem hiding this comment.
This assertion does not prove the renderer mounted or painted. win.title() returns a string, and the ?? "" fallback means the polled value is never null, so .not.toBeNull() can pass even for an empty title or blank window. Please assert a real paint signal here, such as a non-empty title with not.toBe(""), visible renderer text/test id, or a screenshot/pixel check.
neversettle17-101
left a comment
There was a problem hiding this comment.
One release-signal risk to consider before merging:
- Low/Medium: the new
e2e-gateis advisory and does not blockpublish-feed, which is good, but it can still post a redao-stable-gatestatus for infrastructure/setup failures unrelated to the release build: missingDAYTONA_API_KEY, Daytona availability,npm --prefix scripts cifailure, apt/npm registry issues inside the pod, or a stock pod image missing expected tools. That could make otherwise-good stable releases look failed/noisy while the gate is proving itself.
Suggestion: make the status description distinguish app-test failures from infra/setup failures, or consider a neutral/non-blocking status for missing secret / setup unavailable cases while keeping real smoke-test failures red. That would preserve the advisory signal without training maintainers to ignore ao-stable-gate.
The advisory ao-stable-gate could post a red status for infra/setup problems (missing DAYTONA_API_KEY, Daytona unavailable, install error), crying wolf on otherwise-good releases. - deriveGateOutcome now classifies each run: passed | app_failed | infra, with conclusion success | failure | neutral. Only a real app smoke failure is red (failure, exit 1); every infra/setup/timeout case is neutral (exit 0) so a Daytona hiccup never blocks or reddens a release. - runner writes classification/conclusion to $GITHUB_OUTPUT. - release workflow publishes a check-run (supports neutral) instead of a commit status (does not); install step is continue-on-error so a failed dep install classifies as infra -> neutral. skip_e2e_gate -> skipped. - tests rewritten for the new contract; core property asserted: only app_failed is red, infra/crash/timeout never are. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@neversettle17-101 - addressed |
neversettle17-101
left a comment
There was a problem hiding this comment.
Requesting changes for the P0 gate-signal correctness issues:
-
P0:
REAL-001can pass without proving the renderer painted. The test comment says it verifies the window paints, but the assertion only checks(await win.title()) ?? ""is not null. An empty string still passes, so a blank/unmounted renderer can be reported green. Please assert a real visible UI signal, a non-empty title, or a nonblank screenshot/DOM condition that proves the packaged renderer actually mounted. -
P0: daemon readiness can be attributed to the wrong process. The suite launches the app in separate tests and then polls the fixed
http://127.0.0.1:3001/readyz. If a daemon from a previous test/run is still alive, or if port 3001 is already occupied by another AO daemon, the gate can pass against a stale daemon instead of the app instance under test. Please isolate the daemon port/data dir per test run and assert the ready daemon belongs to the launched packaged app. -
P0: fixed port
3001makes the gate fragile and can create false red/green results. The packaged app/daemon may use a different port when configured differently or when 3001 is unavailable, but the test is hardcoded to 3001. Please launch the app with an explicit uniqueAO_PORT(and isolated AO data/run file paths) and poll that exact port, or discover the daemon endpoint from the app/run file in the same way production clients do.
These need to be addressed before the status is useful as a release gate, even in advisory mode, because they affect whether ao-stable-gate means the packaged app actually launched and its own daemon reached ready.
win.title() returns a string and the `?? ""` fallback is never null, so .not.toBeNull() passed even for a blank/unpainted window. Poll inside the page instead: document load complete AND non-empty visible innerText, which only appears once the renderer actually mounts and paints (a bare `<div id="root">` from index.html would otherwise pass a DOM-count check). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The suite hardcoded 127.0.0.1:3001 and launched the app without isolating its daemon, so the gate could (a) report a blank/unmounted renderer green, (b) attribute readiness to a stale daemon from a prior run or another AO instance already on 3001, and (c) go false-red/green when the app used a different port. - launchIsolated(): every test launches the packaged app with a unique ephemeral AO_PORT and fresh AO_DATA_DIR/AO_RUN_FILE under a temp dir. The Electron main spreads process.env into the daemon child, so the daemon binds our port and writes our run file — nothing stale can be on it. - REAL-002 discovers the endpoint the way production clients do: reads THIS launch's running.json for the port (no hardcoded 3001), asserts owner=app and a startedAt after launch, then polls /readyz on that exact port. - Attribution: asserts /readyz pid === run-file pid, proving the ready daemon is the exact process this launch spawned. - Per-test cleanup removes the temp dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds a pod-based e2e gate to
frontend-release.yml. After the release build publishes, an ephemeral Daytona pod installs the Linux.deband runs the real-app smoke suite (app launches + bundled daemon reaches ready, via Playwright_electron.launchunder xvfb), then posts anao-stable-gatecommit status.Advisory first (safe rollout)
continue-on-error+publish-feeddoes NOT depend one2e-gate→ it runs on every release and reports status, but cannot block thelatestauto-update feed while it's proving itself.e2e-gatetopublish-feed'sneeds.workflow_dispatchinputskip_e2e_gate.Security hardening
.debis fetched on the runner and uploaded into the pod → the pod holds no secret and fetches no application code. Toolchain (xvfb/tmux/playwright) installs only if absent, so a prebuilt image runs egress-free; on a stock image it is fetched at boot — the one remaining egress, acceptable for this trusted stable gate (baked into the image for the untrusted per-PR gate).DAYTONA_API_KEY(repo secret) is used only to create the pod, never passed into it.Correctness
deriveGateOutcome, 11 tests): pass→green, fail/crash/timeout→red, crash never a silent pass..deb, spun a pod, ran the real-app suite (2 passed),AO_VERDICT passed.Notes
DAYTONA_API_KEYrepo Actions secret (absent → the advisory gate posts a redao-stable-gatestatus but doesn't block).Part of #2483.