Merge pull request #245 from levineuwirth/ci-manual-dispatch
Run CI on demand: workflow_dispatch
This commit is contained in:
commit
7b82e145d2
|
|
@ -4,6 +4,19 @@ on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
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:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,57 @@ hazard in a shape that looks committed. **A documented error message
|
||||||
that never appears is worse than no documentation**, because the reader
|
that never appears is worse than no documentation**, because the reader
|
||||||
waits for a signal that is not coming.
|
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.
|
||||||
|
- **PR #245 CI: 13/14, red on `Test (macos-latest / lua54)`.** The
|
||||||
|
failure is **R1** — `supersede_cancels_in_flight_job_within_50ms`,
|
||||||
|
fragment `supersede did not cancel within 50ms` — an existing row,
|
||||||
|
matched by selector and fragment. Its recorded flavor is `luajit` and
|
||||||
|
this is `lua54`; **flavor is not part of signature matching**.
|
||||||
|
- **The merge-base control was run rather than the PR job.** The same
|
||||||
|
job at the branch's exact base `2e9f62b`, rerun the same hour: green,
|
||||||
|
`1985 passed; 0 failed`, both logs preserved. **A green control does
|
||||||
|
not establish environmental cause and does not retire R1**; a red one
|
||||||
|
would have shown the branch did not introduce the occurrence, and
|
||||||
|
that inference is unavailable. R1 stays live under its
|
||||||
|
measurement-design disposition.
|
||||||
|
- **R1's assertion also omits its measurement**, which surfaced in the
|
||||||
|
#244 sweep I discarded as noise. **It is deliberately NOT fixed
|
||||||
|
here**: it would sharpen the next failure and repair nothing about
|
||||||
|
the measurement design R1 is actually about. That is the
|
||||||
|
async-runtime lane's, with Q#MCI3.
|
||||||
|
- **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`)
|
## Parse-budget diagnosability — MERGED as #244 (`a85205a`)
|
||||||
|
|
||||||
- **MERGED 2026-08-29T22:04:17Z** at approved head `7b50682`, merge
|
- **MERGED 2026-08-29T22:04:17Z** at approved head `7b50682`, merge
|
||||||
|
|
|
||||||
|
|
@ -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-<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:
|
||||||
|
|
||||||
|
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-<sha>` 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.
|
||||||
|
|
@ -119,6 +119,45 @@ runs, and only reds that were still readable.
|
||||||
|
|
||||||
### R1 — supersede cancellation budget
|
### R1 — supersede cancellation budget
|
||||||
|
|
||||||
|
**Occurrence on PR #245, 2026-08-30, with a merge-base control run the
|
||||||
|
same hour.** Recorded together because the control is what the
|
||||||
|
occurrence owed.
|
||||||
|
|
||||||
|
| field | value |
|
||||||
|
| --- | --- |
|
||||||
|
| **occurrence** | `Test (macos-latest / lua54)`, run `33303043179`, job `99234587352`, head `d9cc0fa`. `1984 passed; 1 failed`, panic at `src/async_runtime.rs:2444` with the row's required fragment `supersede did not cancel within 50ms` |
|
||||||
|
| **merge-base control** | the SAME job at the branch's **exact** merge base `2e9f62b` — run `33277675008`, attempt 2, job `99239538960` — **GREEN**, `1985 passed; 0 failed`, the selector reading `... ok` in its log |
|
||||||
|
| **both logs preserved** | before the rerun, so neither is reconstructed from a conclusion |
|
||||||
|
|
||||||
|
**THIS IS R1, and the new Lua flavor does not change that.** The row
|
||||||
|
records `macOS / luajit`; this occurrence is `macOS / lua54`. **Flavor
|
||||||
|
is not part of signature matching** — the selector and the required
|
||||||
|
fragment are, and both match exactly.
|
||||||
|
|
||||||
|
**What the GREEN control establishes, and what it does not:**
|
||||||
|
|
||||||
|
* **It does NOT establish environmental cause**, and it does **NOT**
|
||||||
|
retire R1. A green control shows only that the merge base can pass
|
||||||
|
the same job in the same hour.
|
||||||
|
* **A RED control would have established something specific** — that
|
||||||
|
the branch did not introduce the occurrence. This control was green,
|
||||||
|
so that inference is unavailable and is not claimed.
|
||||||
|
* **R1 remains LIVE either way**, under its existing
|
||||||
|
**measurement-design** disposition. Its retirement condition is
|
||||||
|
unchanged: the async-runtime lane replaces or justifies the
|
||||||
|
measurement (Q#MCI3).
|
||||||
|
|
||||||
|
**The assertion still omits its measurement.** `started.elapsed()` is
|
||||||
|
in hand at the panic and the message reports none of it, so this
|
||||||
|
occurrence's margin is as unrecoverable as every prior one's.
|
||||||
|
**Recording that is not the same as proposing to fix it here.** Adding
|
||||||
|
the elapsed value would improve the NEXT failure's evidence and would
|
||||||
|
repair nothing about the measurement design this row is actually about
|
||||||
|
— the premise that a `thread::sleep(15ms)` means the worker picked the
|
||||||
|
job up. That belongs to the async-runtime lane, with the rest of
|
||||||
|
Q#MCI3.
|
||||||
|
|
||||||
|
|
||||||
| field | value |
|
| field | value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **selector** | `--lib async_runtime::tests::supersede_cancels_in_flight_job_within_50ms` |
|
| **selector** | `--lib async_runtime::tests::supersede_cancels_in_flight_job_within_50ms` |
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue