actions/issueflow-reconcile — a triage-authored issue arrival must not abort the sweep #91

Closed
opened 2026-07-23 20:05:50 +00:00 by dan-claude-bot · 2 comments
dan-claude-bot commented 2026-07-23 20:05:50 +00:00 (Migrated from github.com)

Context

Every issue this repo has minted today failed its labels run, silently. Found in triage hygiene against the live board.

reconcile_opened_issue runs first in main, before the sweep body. Its two "nothing to do here" early exits are bare returns, and a bare return carries the exit status of the test that just failed — 1. The script is executed by the action, so set -euo pipefail is live (L3-L7), and that 1 kills the run before a single issue is reconciled.

The common case is the one that breaks. An issue opened by a triage actor needs no needs-triageauthor_decision correctly says KEEP — and that correct answer is what aborts the run (L373):

[ "$(author_decision "$triage" <<<"$labels")" = ADD_NEEDS_TRIAGE ] || return

L369 has the same shape: when the arriving item is a PR, not an issue, the guard aborts instead of standing down.

Evidence

Three issues runs failed in the observable window (run history reaches back to 16:12Z today); all three are triage-authored opened events, all three failed the same step, reconcile issue flow (dogfood — the workspace IS ceremony), each in ~400ms with zero stdout:

run time issue opened
30028965385 17:20:09Z #83 (17:20:05Z)
30029052316 17:21:22Z #85 (17:21:17Z)
30036698358 19:07:53Z #90 (19:07:49Z)

The #84 and #86 opens produced runs cancelled by concurrency, so they never reported. Of the triage-authored arrivals that ran to completion, the failure rate is 3/3.

Reproduced against the real repo at bb37c15, exactly matching CI — exit 1, no output:

$ DRY_RUN=1 REPO=heavy-duty/ceremony LABELS_CONF=.github/labels.conf \
    EVENT_NAME=issues EVENT_ACTION=opened EVENT_ISSUE=90 \
    bash actions/issueflow-reconcile/issueflow-reconcile.sh
EXIT=1

Control — the same script with no opened event takes the schedule path and completes: issueflow: reconciled., exit 0.

Blast radius

Degraded, not dead: the */15 schedule still sweeps, so a mint's reconciliation is deferred to the next tick rather than lost. What is lost is the arrival path itself and the run's honesty — a red X on every mint, and any conflict the sweep would have flagged goes unreported for that event.

Why the suite is green

author_decision is tested and correct — test/issueflow-reconcile.test.sh:157 asserts a triage-authored arrival yields KEEP. The bug is in the wrapper's exit status, not the decision, and the suite sources the script, which takes the set -u-only branch of L3-L7. Under sourcing the bare return 1 is inert. The suite cannot catch this class of bug by construction: it never runs the script the way the action runs it.

Shipped 2026-07-22 in a811da8 ("fix: enforce triage author only on issue arrival"), part of #18 / PR #32.

Spec

Decisions, not options. Do not reopen these in the PR.

  • D1 — both early exits become return 0. L369 and L373 mean "no arrival work to do, carry on", and must say so. This is the whole defect.
  • D2 — main's call at L392 stays bare. A genuine failure on the arrival path — the API down, malformed JSON — must still fail the run loudly, matching this file's stated doctrine. Do not soften it with || log. Only the two deliberate stand-downs change; real errors keep aborting.
  • D3 — the per-issue loop's || log ... continuing at L415 is unchanged. That guard is for one issue failing inside a batch; the arrival path is not a batch.
  • D4 — add a test that executes the script, not one that sources it. A pure-function test cannot see this bug. Stub gh on PATH and run the script as a subprocess, following the house pattern already in test/release-chain.test.sh:21-30. Without this the fix is unguarded and regresses the next time someone writes || return.

Tasks

  • L369: || return|| return 0
  • L373: || return|| return 0
  • Add a gh stub and an executed-subprocess test covering the three arrival outcomes: triage author (stand down, sweep still runs), outside author (labels needs-triage, sweep still runs), arrival is a PR (stand down, sweep still runs)
  • Grep the file for any other || return whose intent is "stand down" and give it an explicit status

Acceptance criteria

  • A triage-authored issues:opened run exits 0 and its output reaches issueflow: reconciled.
  • An outside-authored issues:opened run still adds needs-triage and still reaches issueflow: reconciled.
  • An issues:opened event whose subject is a PR exits 0 and reaches issueflow: reconciled.
  • A genuine error inside reconcile_opened_issue still fails the run non-zero (D2 is preserved, not accidentally fixed away)
  • The new test fails against bb37c15 and passes against the fix
  • Whole suite green

Test plan

The case that must fail first: run the new triage-authored-arrival test against unfixed bb37c15 and confirm it reports exit 1 with empty output. A fix whose test never failed has proven nothing here — the suite was already green through three production failures.

Then, with the fix:

  • the three arrival outcomes above, asserting both exit 0 and the presence of issueflow: reconciled. — exit status alone would pass on a script that stood down and skipped the sweep
  • a forced error inside the arrival path (stub gh returning non-zero for the issue fetch) still exits non-zero
  • the existing author_decision cases at L157-L159 unchanged and passing
  • end-to-end against this repo: DRY_RUN=1 REPO=heavy-duty/ceremony EVENT_NAME=issues EVENT_ACTION=opened EVENT_ISSUE=90 exits 0 and reconciles

Dependencies

Part of #1 (the sweep's line, alongside #18 and #61). Blocked by nothing.

## Context Every issue this repo has minted today failed its `labels` run, silently. Found in triage hygiene against the live board. `reconcile_opened_issue` runs first in `main`, before the sweep body. Its two "nothing to do here" early exits are bare `return`s, and a bare `return` carries the exit status of the test that just failed — `1`. The script is **executed** by the action, so `set -euo pipefail` is live ([L3-L7](https://github.com/heavy-duty/ceremony/blob/bb37c1567c3625312d511fef0c62cee4d7c0c5ac/actions/issueflow-reconcile/issueflow-reconcile.sh#L3-L7)), and that `1` kills the run before a single issue is reconciled. The common case is the one that breaks. An issue opened by a triage actor needs no `needs-triage` — `author_decision` correctly says `KEEP` — and that correct answer is what aborts the run ([L373](https://github.com/heavy-duty/ceremony/blob/bb37c1567c3625312d511fef0c62cee4d7c0c5ac/actions/issueflow-reconcile/issueflow-reconcile.sh#L373)): ```sh [ "$(author_decision "$triage" <<<"$labels")" = ADD_NEEDS_TRIAGE ] || return ``` `L369` has the same shape: when the arriving item is a PR, not an issue, the guard aborts instead of standing down. ### Evidence Three `issues` runs failed in the observable window (run history reaches back to 16:12Z today); all three are triage-authored `opened` events, all three failed the same step, `reconcile issue flow (dogfood — the workspace IS ceremony)`, each in ~400ms with **zero stdout**: | run | time | issue opened | |---|---|---| | [30028965385](https://github.com/heavy-duty/ceremony/actions/runs/30028965385) | 17:20:09Z | #83 (17:20:05Z) | | [30029052316](https://github.com/heavy-duty/ceremony/actions/runs/30029052316) | 17:21:22Z | #85 (17:21:17Z) | | [30036698358](https://github.com/heavy-duty/ceremony/actions/runs/30036698358) | 19:07:53Z | #90 (19:07:49Z) | The #84 and #86 opens produced runs cancelled by concurrency, so they never reported. Of the triage-authored arrivals that ran to completion, the failure rate is 3/3. Reproduced against the real repo at `bb37c15`, exactly matching CI — exit 1, no output: ``` $ DRY_RUN=1 REPO=heavy-duty/ceremony LABELS_CONF=.github/labels.conf \ EVENT_NAME=issues EVENT_ACTION=opened EVENT_ISSUE=90 \ bash actions/issueflow-reconcile/issueflow-reconcile.sh EXIT=1 ``` Control — the same script with no `opened` event takes the schedule path and completes: `issueflow: reconciled.`, exit 0. ### Blast radius Degraded, not dead: the `*/15` schedule still sweeps, so a mint's reconciliation is deferred to the next tick rather than lost. What is lost is the arrival path itself and the run's honesty — a red X on every mint, and any conflict the sweep would have flagged goes unreported for that event. ### Why the suite is green `author_decision` is tested and correct — [`test/issueflow-reconcile.test.sh:157`](https://github.com/heavy-duty/ceremony/blob/bb37c1567c3625312d511fef0c62cee4d7c0c5ac/test/issueflow-reconcile.test.sh#L157) asserts a triage-authored arrival yields `KEEP`. The bug is in the wrapper's exit status, not the decision, and the suite **sources** the script, which takes the `set -u`-only branch of L3-L7. Under sourcing the bare `return 1` is inert. The suite cannot catch this class of bug by construction: it never runs the script the way the action runs it. Shipped 2026-07-22 in `a811da8` ("fix: enforce triage author only on issue arrival"), part of #18 / PR #32. ## Spec Decisions, not options. Do not reopen these in the PR. - **D1 — both early exits become `return 0`.** L369 and L373 mean "no arrival work to do, carry on", and must say so. This is the whole defect. - **D2 — `main`'s call at L392 stays bare.** A genuine failure on the arrival path — the API down, malformed JSON — must still fail the run loudly, matching this file's stated doctrine. Do **not** soften it with `|| log`. Only the two deliberate stand-downs change; real errors keep aborting. - **D3 — the per-issue loop's `|| log ... continuing` at L415 is unchanged.** That guard is for one issue failing inside a batch; the arrival path is not a batch. - **D4 — add a test that executes the script, not one that sources it.** A pure-function test cannot see this bug. Stub `gh` on `PATH` and run the script as a subprocess, following the house pattern already in [`test/release-chain.test.sh:21-30`](https://github.com/heavy-duty/ceremony/blob/bb37c1567c3625312d511fef0c62cee4d7c0c5ac/test/release-chain.test.sh#L21-L30). Without this the fix is unguarded and regresses the next time someone writes `|| return`. ## Tasks - [ ] `L369`: `|| return` → `|| return 0` - [ ] `L373`: `|| return` → `|| return 0` - [ ] Add a `gh` stub and an executed-subprocess test covering the three arrival outcomes: triage author (stand down, sweep still runs), outside author (labels `needs-triage`, sweep still runs), arrival is a PR (stand down, sweep still runs) - [ ] Grep the file for any other `|| return` whose intent is "stand down" and give it an explicit status ## Acceptance criteria - [ ] A triage-authored `issues:opened` run exits 0 and its output reaches `issueflow: reconciled.` - [ ] An outside-authored `issues:opened` run still adds `needs-triage` and still reaches `issueflow: reconciled.` - [ ] An `issues:opened` event whose subject is a PR exits 0 and reaches `issueflow: reconciled.` - [ ] A genuine error inside `reconcile_opened_issue` still fails the run non-zero (D2 is preserved, not accidentally fixed away) - [ ] The new test fails against `bb37c15` and passes against the fix - [ ] Whole suite green ## Test plan The case that must fail first: run the new triage-authored-arrival test against unfixed `bb37c15` and confirm it reports exit 1 with empty output. A fix whose test never failed has proven nothing here — the suite was already green through three production failures. Then, with the fix: - the three arrival outcomes above, asserting **both** exit 0 and the presence of `issueflow: reconciled.` — exit status alone would pass on a script that stood down and skipped the sweep - a forced error inside the arrival path (stub `gh` returning non-zero for the issue fetch) still exits non-zero - the existing `author_decision` cases at L157-L159 unchanged and passing - end-to-end against this repo: `DRY_RUN=1 REPO=heavy-duty/ceremony EVENT_NAME=issues EVENT_ACTION=opened EVENT_ISSUE=90` exits 0 and reconciles ## Dependencies Part of #1 (the sweep's line, alongside #18 and #61). Blocked by nothing.
dan-claude-bot commented 2026-07-23 20:06:56 +00:00 (Migrated from github.com)

Minting this issue reproduced it a fourth time. The issues: opened event for #91 itself — run 30040681213, 20:05:54Z — failed the same step, reconcile issue flow (dogfood — the workspace IS ceremony), with the same silent exit 1. The evidence table above is 4/4 now.

Tracked on #1's sweep line beside #18 and #61 — triage 2026-07-23.

Minting this issue reproduced it a fourth time. The `issues: opened` event for #91 itself — [run 30040681213](https://github.com/heavy-duty/ceremony/actions/runs/30040681213), 20:05:54Z — failed the same step, `reconcile issue flow (dogfood — the workspace IS ceremony)`, with the same silent exit 1. The evidence table above is 4/4 now. Tracked on #1's sweep line beside #18 and #61 — triage 2026-07-23.
claude-bot-andresmgsl commented 2026-07-23 20:11:00 +00:00 (Migrated from github.com)

Claiming — starting now. Draft PR shortly.

Claiming — starting now. Draft PR shortly.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#91
No description provided.