Skip to content

feat(ci): advisory pod e2e gate for the stable desktop release#2697

Open
Pritom14 wants to merge 5 commits into
AgentWrapper:mainfrom
Pritom14:feat/stable-e2e-gate
Open

feat(ci): advisory pod e2e gate for the stable desktop release#2697
Pritom14 wants to merge 5 commits into
AgentWrapper:mainfrom
Pritom14:feat/stable-e2e-gate

Conversation

@Pritom14

@Pritom14 Pritom14 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

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 (app launches + bundled daemon reaches ready, via Playwright _electron.launch under xvfb), then posts an ao-stable-gate commit status.

Advisory first (safe rollout)

  • continue-on-error + publish-feed does NOT depend on e2e-gate → it runs on every release and reports status, but cannot block the latest auto-update feed while it's proving itself.
  • Once it's green across a few weekly releases, flip it to blocking by adding e2e-gate to publish-feed's needs.
  • Manual override: workflow_dispatch input skip_e2e_gate.

Security hardening

  • The .deb is 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.
  • Pod is ephemeral (deleted after each run). (Applies the CodeRabbit PR→RCE lessons: a compromised pod finds nothing to pivot to.)

Correctness

  • Verdict→exit-code→status is a pure, unit-tested function (deriveGateOutcome, 11 tests): pass→green, fail/crash/timeout→red, crash never a silent pass.
  • Verified end-to-end against a published nightly: downloaded the .deb, spun a pod, ran the real-app suite (2 passed), AO_VERDICT passed.

Notes

Part of #2483.

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 whoisasx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/frontend-release.yml Outdated
Comment thread .github/workflows/frontend-release.yml Outdated
Comment thread test/e2e-pod/boot-real.sh Outdated
…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.
@Pritom14 Pritom14 requested a review from whoisasx July 15, 2026 17:21

@whoisasx whoisasx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread test/e2e-pod/real-app.spec.ts Outdated
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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 neversettle17-101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One release-signal risk to consider before merging:

  • Low/Medium: the new e2e-gate is advisory and does not block publish-feed, which is good, but it can still post a red ao-stable-gate status for infrastructure/setup failures unrelated to the release build: missing DAYTONA_API_KEY, Daytona availability, npm --prefix scripts ci failure, 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>
@Pritom14

Copy link
Copy Markdown
Contributor Author

One release-signal risk to consider before merging:

  • Low/Medium: the new e2e-gate is advisory and does not block publish-feed, which is good, but it can still post a red ao-stable-gate status for infrastructure/setup failures unrelated to the release build: missing DAYTONA_API_KEY, Daytona availability, npm --prefix scripts ci failure, 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.

@neversettle17-101 - addressed

@neversettle17-101 neversettle17-101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the P0 gate-signal correctness issues:

  • P0: REAL-001 can 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 3001 makes 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 unique AO_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.

Pritom14 and others added 2 commits July 16, 2026 15:37
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants