8.5 KiB
Manual CI dispatch — a contemporaneous run on any ref
Status: revision 3 — AWAITING APPROVAL. Nothing implemented.
Revision 3 answers review of 2: D2 could pass without the two runs ever overlapping, D3 accepted an aborted job as evidence the matrix ran, the merge-base instruction named the wrong commit and the wrong command, and "when it runs automatically" was still too strong.
Revision 2 answered review of 1, whose first change was a large reduction in what this claims to be worth.
1. What this fixes
ci.yml triggers on push to main and on pull_request. There is no
way to run it on demand, so a CI run on main can only be obtained
from whenever main last happened to be pushed — or by re-running a
job from that stale run.
2. Why, and exactly which row needs it
Revision 1 claimed "four registry rows name a control they cannot
obtain". That was wrong, and the correction is worth stating plainly:
CI never invokes scripts/gate — zero occurrences in ci.yml. It
runs cargo test directly.
So every local-gate red in the registry has a merge-base control that
needs no CI at all. Stated precisely, because the sloppy version of
this sentence is itself a trap: run the exact failing command, with
its flags, at the recorded merge base — not at whatever main
happens to be now — in a separate worktree, which this repository's
shared-checkout discipline requires anyway. git checkout main && ./scripts/gate is none of those three things.
Measured against the registry:
| row | where it reds | needs this? |
|---|---|---|
| U11 | Test (macos-latest / lua54) |
YES |
| R7 | local — originally cargo test --workspace --features crdt under sweep load, and its later occurrences at scripts/gate's gpu step, including the two on the parse-budget lane |
no |
| U6 | local, scripts/gate step 04-lib-crdt |
no |
| U9 | local, scripts/gate step 11-sweep |
no |
| U10 | local, scripts/gate steps 13-sweep / 15-sweep-crdt |
no |
| U12 | local, scripts/gate step 04-lib-crdt |
no |
| U13 | local, scripts/gate step 07-sweep |
no |
One row, not four. U11 needs it because it is macOS-specific and
this project has no Mac: its control cannot be run locally at all. When
U11 recurred on PR #243, the only way to get a contemporaneous
main-side macOS run without touching main or opening a PR was to
re-run a job from a run eight days old. That is the whole of the
demonstrated need.
3. What this is, and is NOT, for the other experiments
A no-input dispatch of the unchanged workflow runs exactly what a push runs. It therefore:
- directly enables a contemporaneous
main-side run of the existing fourteen checks — U11's case, and any future CI-side, platform-specific red; - does NOT run
scripts/gate, which CI does not invoke, so it does nothing for R7's in-gate/out-of-gate question; - does NOT vary R7's gate conditions, which would need the gate in CI plus a way to change one condition per run;
- does NOT select U9's alternate test commands, which would need workflow inputs or a branch carrying different commands.
Those remain separate work. This key is infrastructure they would build on, not a substitute for them, and revision 1 blurred that.
4. What lands
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
One key. No job, matrix, step, permission or timeout changes.
5. The two interactions, both checked
5a. Concurrency — the guarantee, stated at its real strength
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
cancel-in-progress is evaluated for the INCOMING run.
The defensible guarantee is narrow: once a dispatched run is
in_progress, a second same-SHA dispatch will not cancel it.
Revision 1 said "two dispatches cannot cancel each other", which is too
broad. cancel-in-progress: false protects a running run; it does not
protect a PENDING one — GitHub still replaces an existing pending run
in the same concurrency group. And a main push and a dispatch at the
same SHA do share ci-<sha>, so the two are not isolated from each
other the way revision 1 implied.
A PR run and a dispatch never share a group — PR runs key on
pull_request.number — so no test involving a PR exercises the
predicate at all.
5b. No job is event-conditional
Measured: ci.yml contains exactly one if:, and it is
runner.os == 'Linux' (line 171). github.event_name appears only
inside the concurrency predicate (line 28). A dispatched run therefore
expands to the same fourteen jobs a push does.
6. Acceptance
The witnesses split across the merge, and they must. GitHub offers
workflow_dispatch only for a workflow already on the default
branch, so no dispatch can be demonstrated before this lands. gh workflow view ci.yml shows a summary and --yaml is needed for the
file — but even that reads the default branch's copy, which is why D1
parses the working tree instead.
| # | when | contract | witness | mutation |
|---|---|---|---|---|
| D1 | pre-merge | ci.yml is valid YAML and declares the trigger |
parse the working-tree file; assert workflow_dispatch is a key under on |
drop the key → the assertion fails |
| D2 | post-merge | a second same-SHA dispatch does not cancel a first that is already running | see below | cancel-in-progress: true unconditionally → the first ends cancelled |
| D3 | post-merge | a dispatched run executes the full matrix for real | see below | make any job event-conditional → that job is skipped |
D2's procedure, because the obvious version passes its own mutant.
"Same ref" does not mean same SHA — main can move between the two
dispatches, giving them different groups and letting the mutant
survive. And a cancelled run still reaches status completed, so
status is not the discriminator. So:
- dispatch
main, record run id A and itsheadSha; - wait until A is
in_progress, not merely queued — the guarantee is about a running run, and a pending one is genuinely replaceable; - dispatch
mainagain, record run id B and itsheadSha; - observe a state in which A is
in_progressAND B isqueued. Without this the witness passes vacuously: A can finish naturally between steps 2 and 3, and the unconditional-cancellation mutant then has nothing to cancel. If that state is never observed, the attempt is VOID — retry it; do not report it as a pass; - assert
headSha(A) == headSha(B), or the run proves nothing —maincan move between dispatches and hand the two different groups; - assert A's final
conclusionis notcancelled.
D3's procedure, for the same reason. A job suppressed by a
job-level if: still appears as a check and reports skipped, which
rolls up as success — so "all fourteen checks are listed" does not
detect the mutant. So:
- assert every one of the fourteen expected jobs started
(
started_atpresent) and concludedsuccessorfailure. "Notskipped" is not enough — it still admitscancelledandtimed_out, neither of which is a job that ran the matrix; - keep a structural check for step-level conditionals: the only
if:in the file isrunner.os == 'Linux', and no expression referencesgithub.event_nameorgithub.eventoutside the concurrency block.
D2 and D3 are recorded as OWED at merge. This framing does not pretend a pre-merge check can stand in for them.
7. Coherence impact (COHERENCE.md §20)
- Journey steps touched: NONE. No product behaviour changes.
- Interaction islands: none added.
- Config registry: no entry.
- Background work: none started. A dispatched run is human-initiated by definition.
8. What this does NOT do
- It does not make R7, U6, U9, U10, U12 or U13 more answerable. Those are local-gate reds; CI does not run the gate.
- It does not run any control. It makes one class of control — CI-side, on an arbitrary ref — obtainable without a stale rerun.
- It does not change what CI runs, or the automatic trigger
conditions. Note the narrower wording: SCHEDULING can change, because
a dispatch sharing
ci-<sha>may replace an existing pendingmain-push run. What is unchanged is when CI fires by itself. - It does not weaken the PR-superseding saving, which is keyed on
pull_request.numberand gated on the event.