From d9cc0fa58a0ef968ad63af11b6e7fec320c5ee1f Mon Sep 17 00:00:00 2001 From: Levi Neuwirth Date: Sun, 30 Aug 2026 10:53:04 +0200 Subject: [PATCH] ci: run the workflow on demand, on any ref Adds workflow_dispatch: to ci.yml. One key, no inputs, and no job, matrix, step, permission or timeout changes --- a dispatched run does exactly what a push run does. THE SCOPE IS ONE REGISTRY ROW, and the framing took three revisions to get that right. CI never invokes scripts/gate --- zero occurrences --- so every local-gate red already has a merge-base control that needs no CI at all: the exact failing command, with its flags, at the recorded merge base, in a separate worktree. U11 alone needs this trigger, because it is macOS-specific and this project has no Mac. When it recurred on #243 the only contemporaneous main-side control available was a re-run of a run eight days old. What this is NOT: it does not run scripts/gate, does not vary R7's gate conditions, and does not select U9's alternate test commands. Those need their own changes. This is infrastructure they would build on. The concurrency interaction is stated at its real strength: once a dispatched run is in_progress, a second same-SHA dispatch will not cancel it. cancel-in-progress: false protects a RUNNING run and not a pending one, and a main push and a dispatch at the same SHA do share ci-. A PR run never shares a dispatch's group at all, so no PR-involving test exercises the predicate. D1 passes and bites pre-merge: the working-tree file parses and workflow_dispatch is a key under `on`; dropping it fails the assertion. The witness looks up both 'on' and the boolean True, because YAML 1.1 parses `on:` as a boolean and a naive d['on'] raises before checking anything. D2 and D3 are OWED POST-MERGE and recorded as such --- GitHub offers workflow_dispatch only for a workflow already on the default branch, so neither can run before this lands. D2 carries a void-and-retry rule so it cannot pass when the two runs never actually overlap, and D3 requires each job to conclude success or failure rather than merely not-skipped. Framing docs/ci-manual-dispatch-framing.md revision 3, approved. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai --- .github/workflows/ci.yml | 13 ++ docs/active-work.md | 34 ++++++ docs/ci-manual-dispatch-framing.md | 187 +++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 docs/ci-manual-dispatch-framing.md 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.