Skip to content

ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) - #1794

Merged
thymikee merged 3 commits into
mainfrom
ci/1781-a1-park-full-tier
Aug 18, 2026
Merged

ci: move parked device replay suites to a dispatch-only workflow (#1781 A1)#1794
thymikee merged 3 commits into
mainfrom
ci/1781-a1-park-full-tier

Conversation

@thymikee

@thymikee thymikee commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

#1781 A1 fallback. Replay Nightly has failed every scheduled run since 2026-07-24. Two jobs are responsible and neither has an owner:

  • Android Full Emulator Suite — dies inside full:lifecycle-system, a scenario that had never executed end to end. Diagnosis and a validated fix for it (and for full:observability-artifacts) are in test(android): restore full-tier lifecycle and observability scenarios (#1781 A1) #1793; even with that, the last scenario full:fixture-replays still fails on a replay fixture whose scroll canary is authored for a taller device than the pixel_7 profile the lane pins.
  • iOS Replay Suite — fails on varying steps; not diagnosed here.

Both jobs move, byte-for-byte, into .github/workflows/replays-manual.yml, which has no schedule:. The parser fuzz lane keeps its nightly schedule in replays-nightly.yml and is untouched; the macOS job is already gone (#1787). Nothing about how the suites run changes — dispatch them with:

gh workflow run replays-manual.yml --ref <branch>

Why a separate file rather than if: github.event_name == 'workflow_dispatch'

The first revision of this PR used a job-level if:. It would have parked the jobs and lied about it: workflowLanes() (scripts/gate/workflows.ts) computes qualifying = 'pull_request' in on || 'schedule' in on once per workflow file and never reads job-level if:. So pnpm check:gate-manifest kept printing

gate manifest: ok — 48 checks wired across 32 lanes, 1 declared unprovable.

while replay-android, replay-ios, and replay-ios-device — declared only in those two jobs — no longer ran on any schedule. That is exactly the failure the manifest exists to catch: "a check that silently loses its owner looks exactly like a green build" (docs/agents/testing.md). A separate workflow_dispatch-only file is what the existing file-level model already reads correctly.

What that exposed, and how it is declared

With the jobs moved, the audit reports the truth:

check "replay-ios" is not declared by any pull_request/schedule lane: script:test:replay:ios.
check "replay-ios-device" is not declared by any pull_request/schedule lane: script:test:replay:ios-device.

These are declared in scripts/gate/declarations.ts as a new MANUAL_ONLY_OWNERS record rather than being folded into UNPROVABLE_OWNERS, because the two facts are opposites:

  • UNPROVABLE_OWNERS claims "the suite runs; this loader cannot see it" — which stopped being true for replay-android the moment its lane stopped running automatically, so its stale prose moved with it. UNPROVABLE_OWNERS is now empty, with a comment saying why.
  • MANUAL_ONLY_OWNERS says "nothing runs this until someone dispatches it", with the reason and the exit condition per entry.

check:gate-manifest now reports:

gate manifest: ok — 46 checks wired across 32 lanes, manual-only: replay-android, replay-ios, replay-ios-device.

Named, not counted — the ids read like a list of things nothing runs, where "3 manual-only" would read like a tally.

Each entry names the dispatch lane that still runs it, and a manual-only audit assertion resolves that name against the derived model: the lane must exist, must still be dispatch-only, and must still declare the gate. Without that the record would be a plain allowlist — deleting a parked job would leave the manifest green and still printing the check as "manual-only", turning parked coverage into deleted coverage. replay-android carries an explicit opaque flag, because its gate sits inside the third-party emulator action's script: (#1429): the job's existence is the whole attestation the model can make, and the flag says so instead of letting an unreadable lane look like a declaring one.

Six regressions pin the path (pnpm check:gate-manifest:test, 34/34):

  • a workflow_dispatch-only lane reads its gate but is non-qualifying;
  • every manual-only entry names a registered check no qualifying lane owns;
  • deleting a declaration reports its check as unowned;
  • deleting the parked job fails with no workflow defines;
  • putting the lane back on a schedule fails until the entry is dropped;
  • a parked lane that loses its run-gate step fails — unless the entry is opaque, which is asserted too, so the flag cannot wave a readable lane through.

Both ratchets were checked for vacuity by planting layering (a genuinely owned check) in the record and watching the suite go red.

docs/agents/testing.md records the attestation and why replay-android is opaque.

Validation

pnpm check:tooling passes (format, lint, typecheck, layering, depgraph, gate-manifest + its tests, production-exports, tmpdir-leaks, mcp-metadata, build, bundle-owner-files, package). The moved job bodies are byte-identical to the originals — only the job ids changed (nightly-android/nightly-iosmanual-android/manual-ios); job names, steps, pins, env, and artifact names are unchanged.

Notes for whoever picks these up

Exit condition

Move the two jobs back into replays-nightly.yml and delete their MANUAL_ONLY_OWNERS entries once a dispatch run is green. The audit test fails if the entries outlive the parking, so this cannot quietly become permanent.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.26 MB 2.26 MB 0 B
JS gzip 744.4 kB 744.4 kB 0 B
npm tarball 863.7 kB 863.7 kB 0 B
npm unpacked 3.01 MB 3.01 MB 0 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 26.6 ms 27.0 ms +0.4 ms
CLI --help 64.3 ms 65.7 ms +1.4 ms

Top changed chunks: no changes in the largest emitted chunks.

… A1)

Both full-tier device jobs have failed every scheduled run since 2026-07-24: the
Android suite inside full-tier scenarios that had never executed end to end, the
iOS suite on varying steps. They move to .github/workflows/replays-manual.yml,
which has no `schedule:`, so the schedule stops emitting a guaranteed failure while
the suites stay runnable on demand.

A job-level `if: github.event_name == 'workflow_dispatch'` would have looked the
same and lied: `workflowLanes()` decides `qualifying` per workflow FILE and never
reads job-level `if:`, so the manifest kept reporting replay-android, replay-ios,
and replay-ios-device as scheduled-lane owners — the silent-owner-loss failure the
manifest exists to catch. A separate file is what the file-level model already
reads correctly.

Those three checks now have no pull_request/schedule owner, so they are declared as
MANUAL_ONLY_OWNERS rather than folded into UNPROVABLE_OWNERS, whose claim ("it runs,
this loader cannot see it") is no longer true for replay-android. check:gate-manifest
drops from 48 to 46 wired checks and names the three on every run. Two tests pin it:
a dispatch-only lane is non-qualifying however many gates it declares, and every
manual-only declaration must name a registered check that no qualifying lane owns, so
a re-scheduled lane cannot keep a stale exemption.
@thymikee
thymikee force-pushed the ci/1781-a1-park-full-tier branch from a6e8c6c to 2198ff5 Compare August 17, 2026 17:59
@thymikee thymikee changed the title ci: park full-tier nightly device jobs on manual dispatch (#1781 A1) ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) Aug 17, 2026
@thymikee

Copy link
Copy Markdown
Member Author

P1: MANUAL_ONLY_OWNERS is currently only a negative allowlist. The new test proves each entry is registered and has no qualifying owner, but it never proves a workflow_dispatch lane still declares and can run that check. Deleting a parked manual job, or its run-gate step, would therefore leave check:gate-manifest green and continue to print the check as manual-only: parked coverage has silently become deleted coverage.

Please derive/attest each manual-only declaration from a dispatch-only lane and add a regression that deleting its declaration fails. Android needs an explicit attestation too: replay-android remains inside the opaque third-party action, so the existing loader cannot supply it by itself.

The loss of scheduled replay coverage itself matches #1781 A1 (manual means off). The current Bundle Size and CodeQL failures are GitHub 503 infrastructure failures; the wording/count issue in the manifest output is non-blocking.

@thymikee
thymikee marked this pull request as ready for review August 17, 2026 18:50
@thymikee

Copy link
Copy Markdown
Member Author

CI status on this head: 25 pass, 2 fail. Both failures are Analyze (java-kotlin) and Analyze (javascript-typescript) from the GitHub outage this PR was opened during — the logs contain only Encountered an error while trying to determine feature enablement: HttpError: No server is currently available…, never a finding, and that CodeQL run reports cannot be rerun; This workflow run cannot be retried. Bundle Size failed the same way (Failed to update PR comment: 503) and passes on re-run, as does every other job. The next push, or a maintainer re-run once GitHub finishes recovering, will clear the two Analyze rows.

Review P1: MANUAL_ONLY_OWNERS was a negative allowlist — it proved each entry named a
registered check no qualifying lane owned, but nothing tied the entry to a lane that can
still run it. Deleting a parked job, or its run-gate step, would have left the manifest
green and still printing the check as manual-only: parked coverage silently becoming
deleted coverage.

Each entry now names its dispatch lane, and a new 'manual-only' audit assertion resolves
that name against the derived model: the lane must exist, must still be dispatch-only, and
must still declare the gate. replay-android carries an explicit `opaque` flag because its
gate sits inside the third-party emulator action's `script:` (#1429), so the job's
existence is the whole attestation the model can make — and the flag says so rather than
letting an unreadable lane look like a declaring one.

Four regressions pin both directions: deleting a declaration reports the check as unowned;
deleting the parked job fails with 'no workflow defines'; re-scheduling the lane fails
until the entry is dropped; and a parked lane that loses its run-gate step fails unless the
entry is opaque.
@thymikee

Copy link
Copy Markdown
Member Author

P1 addressed in 643bc9c: the declaration is now an attestation the audit resolves, not an allowlist.

MANUAL_ONLY_OWNERS entries carry the lane that still runs them (Replay Manual / Android Full Emulator Suite, Replay Manual / iOS Replay Suite), and a new manual-only audit assertion resolves that name against the derived model. It fails when the lane is gone, when the lane has become a pull_request/schedule lane again, or when the lane no longer declares the gate — so deleting a parked job or its run-gate step turns the manifest red instead of leaving the check printed as merely parked.

Android gets the explicit attestation you asked for. Its entry is marked opaque: true, which is the record saying out loud that the lane's own steps cannot show the gate — it sits in the third-party emulator action's script: (#1429) — so the job's existence is the whole attestation the model can make, and the audit checks exactly that rather than pretending to check a step it never reads. The derived lanes make the difference visible:

replays-manual.yml | Replay Manual / Android Full Emulator Suite | qualifying=false | gates=["android-helpers"]
replays-manual.yml | Replay Manual / iOS Replay Suite            | qualifying=false | gates=["swift-runner-ios","replay-ios","replay-ios-device"]

Four regressions pin both directions (pnpm check:gate-manifest:test, 34/34):

  • deleting a declaration reports its check as unowned (audit(model, { manualOnly: {}, unprovable: {} }));
  • deleting the parked job fails with no workflow defines;
  • putting the lane back on a schedule fails until the entry is dropped;
  • a parked lane that loses its run-gate step fails — unless the entry is opaque, which is asserted too, so the flag cannot be used to wave a readable lane through.

I checked the new ratchet is not vacuous by planting layering (a genuinely owned check) in the record and watching the suite go red.

docs/agents/testing.md now states that each entry names its dispatch lane, that the audit resolves it, and why replay-android is opaque. pnpm check:tooling passes; the manifest still prints 46 checks wired across 32 lanes, manual-only: replay-android, replay-ios, replay-ios-device.

On sequencing: #1793 now has a green end-to-end full-tier run, including full:fixture-replays, so parking is no longer the only way to close A1 — this PR stands as the fallback if you would rather park the lane than carry those fixture repairs.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 643bc9c. P1 remains: the new audit proves only that a declared owner has no PR/schedule-qualified lane; it does not prove the workflow is still workflow_dispatch-only. Replacing workflow_dispatch in replays-manual.yml with push (or another non-qualifying trigger) while retaining the jobs/gates leaves the audit green and still reports these checks as manual-only. Preserve trigger kinds in the workflow/lane model (or add an explicit dispatch-only predicate) and add a planted regression that replaces/removes workflow_dispatch. Exact-head CI is otherwise green and merge state is clean.

…1781 A1)

Review follow-up: the attestation checked `qualifying === false`, which is true of any lane
that is not pull_request/schedule. Swapping `workflow_dispatch` for `push` in
replays-manual.yml would have kept the audit green and the checks printed as manual-only,
while the runs nobody starts by hand quietly started themselves on every push.

The lane model now keeps the trigger names instead of collapsing them into that one bit, and
the manual-only assertion requires `workflow_dispatch` and nothing else. Three planted
regressions cover the gap the review named: a parked lane re-triggered by `push` fails, a
parked lane with no trigger at all fails, and the loader test pins that trigger kinds survive
into the model (a push lane reads `[push]`, the nightly reads `[schedule, workflow_dispatch]`).
@thymikee

Copy link
Copy Markdown
Member Author

Fixed in b94f93d. You are right that qualifying === false was the wrong predicate: push, release and friends sit on the same side of that bit, so swapping the trigger would have kept the audit green while the parked lane started itself again.

The lane model now preserves the trigger names instead of collapsing them (Lane.triggers), and the manual-only assertion requires workflow_dispatch and nothing else:

"Replay Manual / iOS Replay Suite" is triggered by push, so "replay-ios" is not manual-only —
"manual" is a claim about who starts the run, and every trigger other than workflow_dispatch
starts it without them. Restore a dispatch-only workflow, or declare what actually runs the check.

Three regressions cover exactly the gap you named (pnpm check:gate-manifest:test, 37/37):

  • a parked lane re-triggered by push fails even though push does not qualify — the planted case from your comment;
  • a parked lane that loses workflow_dispatch entirely fails — removal, not just replacement;
  • trigger kinds survive the model, not just whether they qualify — a loader-level pin that a push workflow reads ["push"] and replays-nightly.yml reads ["schedule","workflow_dispatch"], so a future refactor cannot quietly collapse the field the assertion depends on.

pnpm check:tooling passes and the manifest still reports 46 checks wired across 32 lanes, manual-only: replay-android, replay-ios, replay-ios-device.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 18, 2026
@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head b94f93d. Clean readiness verdict: the lane model now preserves trigger kinds, the manual-only audit requires workflow_dispatch and nothing else, and planted push/no-trigger regressions close the prior bypass. Exact-head CI is fully green. Ready for human review.

@thymikee
thymikee merged commit ccf64f6 into main Aug 18, 2026
28 checks passed
@thymikee
thymikee deleted the ci/1781-a1-park-full-tier branch August 18, 2026 07:59
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-08-18 07:59 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant