diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95d7455..e193d0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,19 @@ on: push: branches: [main] pull_request: + # Run CI on demand, on any ref. + # + # A CI-side red on a platform this project has no machine for --- U11 + # is macOS/`lua54` --- cannot have its merge-base control run locally. + # Without this trigger the only way to get a contemporaneous `main` + # run is to re-run a job from whenever `main` last happened to be + # pushed, which on PR #243 meant a run EIGHT DAYS OLD. + # + # No inputs, deliberately. This runs exactly what a push runs; it is + # not a hook for varying commands. Experiments that need that --- + # U9's test-binary concurrency control, R7's gate conditions --- need + # their own changes, and CI does not invoke `scripts/gate` at all. + workflow_dispatch: env: CARGO_TERM_COLOR: always diff --git a/docs/active-work.md b/docs/active-work.md index b3a9c11..533c04e 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -270,6 +270,40 @@ hazard in a shape that looks committed. **A documented error message that never appears is worse than no documentation**, because the reader waits for a signal that is not coming. +## Manual CI dispatch — ACTIVE + +**Written with the branch's FIRST commit**, per the standing correction +from #171 and #215. + +- **Branch `ci-manual-dispatch`**, base `githubsucks/main` @ + **`2e9f62b`** exactly, in worktree + `/home/jeans/Repos/personal/pmacs-ci-dispatch`. + **`githubsucks/ci-manual-dispatch` is the authoritative tip** (the + ref, not a SHA). +- **Framing `docs/ci-manual-dispatch-framing.md`, revision 3 — + APPROVED.** It took three revisions and every one was a correction: + revision 1 claimed four registry rows needed this (**one does**), + revision 2's D2 could pass without the two runs ever overlapping, and + its D3 accepted an aborted job as proof the matrix ran. +- **What it does:** adds `workflow_dispatch:` to `ci.yml`. One key. No + job, matrix, step, permission or timeout changes, and **no inputs**. +- **Why, and the scope is ONE ROW.** **CI never invokes `scripts/gate` + — zero occurrences.** Every local-gate red therefore has a merge-base + control needing no CI at all. **U11 alone needs this**, because it is + macOS-specific and this project has no Mac: when it recurred on #243 + the only contemporaneous `main` control available was a re-run of a + run **eight days old**. +- **D1 PASSES pre-merge and BITES**: the working-tree file parses, and + `workflow_dispatch` is a key under `on` — dropping it fails the + assertion. Note `on:` parses as the **boolean `True`** under YAML 1.1, + so the witness looks up both keys; a naive `d['on']` would raise + before it ever checked anything. +- **D2 AND D3 ARE OWED POST-MERGE**, not skipped. GitHub offers + `workflow_dispatch` only for a workflow already on the default + branch, so neither can run before this lands. Their exact procedures + are in the framing §6, including the void-and-retry rule that stops + D2 passing when the two runs never overlap. + ## Parse-budget diagnosability — MERGED as #244 (`a85205a`) - **MERGED 2026-08-29T22:04:17Z** at approved head `7b50682`, merge diff --git a/docs/ci-manual-dispatch-framing.md b/docs/ci-manual-dispatch-framing.md new file mode 100644 index 0000000..4d70eb5 --- /dev/null +++ b/docs/ci-manual-dispatch-framing.md @@ -0,0 +1,187 @@ +# 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 + +```yaml +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 + +```yaml +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-`, 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: + +1. dispatch `main`, record **run id A** and its `headSha`; +2. wait until A is **`in_progress`**, not merely queued — the guarantee + is about a running run, and a pending one is genuinely replaceable; +3. dispatch `main` again, record **run id B** and its `headSha`; +4. **observe a state in which A is `in_progress` AND B is `queued`.** + 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;** +5. **assert `headSha(A) == headSha(B)`**, or the run proves nothing — + `main` can move between dispatches and hand the two different + groups; +6. assert **A's final `conclusion` is not `cancelled`**. + +**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: + +1. assert every one of the fourteen expected jobs **started** + (`started_at` present) **and concluded `success` or `failure`**. + "Not `skipped`" is not enough — it still admits `cancelled` and + `timed_out`, neither of which is a job that ran the matrix; +2. keep a **structural** check for step-level conditionals: the only + `if:` in the file is `runner.os == 'Linux'`, and no expression + references `github.event_name` or `github.event` outside 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-` may replace an existing **pending** + `main`-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.number` and gated on the event.