fix: stand down with exit 0 on non-mint issue arrivals

reconcile_opened_issue's two early exits were bare returns, which carry
the failed guard's status into the executed script's set -e — every
triage-authored mint killed the labels run before one issue was
reconciled (#91, 4/4 observed). The stand-downs now say return 0; a
genuine failure on the arrival path still aborts loudly.

The suite sources the script and takes the set -u-only branch, so it
was blind to this by construction. The new arrival section executes the
script as a subprocess behind a fixture-serving gh stub (the house
pattern from test/release-chain.test.sh) and covers all three arrival
outcomes plus the preserved loud-failure path; it fails against
bb37c15 with the production signature — exit 1, empty output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-23 20:16:20 +00:00
parent bb37c1567c
commit c39b78959d
3 changed files with 95 additions and 2 deletions

View file

@ -6,6 +6,7 @@ so entries say what changed, cite the issue, and stop.
## Unreleased ## Unreleased
- `issueflow-reconcile` — a triage-authored issue arrival stands down with exit 0 instead of killing the run before the sweep (#91).
- FLEET.md — the assignee's `attention` wake: one role-independent trigger ahead of every per-role list, one acked session per demand; a spec on paper until `duty.sh` polls it (#86). - FLEET.md — the assignee's `attention` wake: one role-independent trigger ahead of every per-role list, one acked session per demand; a spec on paper until `duty.sh` polls it (#86).
- `attention` doctrine — define its assignee-owned pickup, ack, queue and clock semantics across labels, triage, and builder roles (#85). - `attention` doctrine — define its assignee-owned pickup, ack, queue and clock semantics across labels, triage, and builder roles (#85).
- `attention` — add the issue-only, hand-set assignee-demand flag to the core label taxonomy (#84). - `attention` — add the issue-only, hand-set assignee-demand flag to the core label taxonomy (#84).

View file

@ -366,11 +366,14 @@ reconcile_issue() {
reconcile_opened_issue() { reconcile_opened_issue() {
local n="$1" author triage=false labels remove="" label local n="$1" author triage=false labels remove="" label
ISSUE_JSON="$(gh api "repos/$REPO/issues/$n")" ISSUE_JSON="$(gh api "repos/$REPO/issues/$n")"
jq -e 'has("pull_request") | not' <<<"$ISSUE_JSON" >/dev/null || return # The stand-downs return 0 explicitly: a bare return carries the failed
# test's status, which under execution is live `set -e` — and it killed the
# run on every triage-authored mint, before one issue was reconciled (#91).
jq -e 'has("pull_request") | not' <<<"$ISSUE_JSON" >/dev/null || return 0
author="$(jq -r '.user.login' <<<"$ISSUE_JSON")" author="$(jq -r '.user.login' <<<"$ISSUE_JSON")"
is_triage_actor "$author" && triage=true is_triage_actor "$author" && triage=true
labels="$(jq -r '.labels[].name' <<<"$ISSUE_JSON")" labels="$(jq -r '.labels[].name' <<<"$ISSUE_JSON")"
[ "$(author_decision "$triage" <<<"$labels")" = ADD_NEEDS_TRIAGE ] || return [ "$(author_decision "$triage" <<<"$labels")" = ADD_NEEDS_TRIAGE ] || return 0
for label in epic "${QUEUE_LABELS[@]}"; do for label in epic "${QUEUE_LABELS[@]}"; do
grep -qxF "$label" <<<"$labels" && remove="$remove,$label" grep -qxF "$label" <<<"$labels" && remove="$remove,$label"
done done

View file

@ -396,4 +396,93 @@ churned="$(issue_probe 24 $'claimed\nneeds-ruling')"
check "8 real-quiet days nudge through a 2-day-old label churn" 0 "" \ check "8 real-quiet days nudge through a 2-day-old label churn" 0 "" \
grep -q 'ruling nudge' <<<"$churned" grep -q 'ruling nudge' <<<"$churned"
# ---------------------------------------------------------------------------
# The arrival path, executed the way the action executes it (#91): four
# triage-authored mints died silently because the stand-down `return`s in
# reconcile_opened_issue carried the failed test's status into `set -e`. A
# sourced test takes the `set -u`-only branch and is blind to that class of
# bug by construction, so these run the script as a subprocess behind a
# PATH-stubbed gh — the house pattern from test/release-chain.test.sh.
# ---------------------------------------------------------------------------
ARRIVAL="$TMP/arrival"
mkdir -p "$ARRIVAL/stub" "$ARRIVAL/fixtures"
printf 'triage-actors=triage-one triage-two\n' >"$ARRIVAL/labels.conf"
cat >"$ARRIVAL/stub/gh" <<'EOF'
#!/usr/bin/env bash
# Endpoints map to files under $GH_FIXTURES ('/?&=' -> '_'); an absent file
# answers an empty list, a .error sentinel fails the call like a dead API.
if [ "$1" = api ]; then
shift
endpoint="" jqexpr=""
while [ $# -gt 0 ]; do
case "$1" in
--jq) jqexpr="$2"; shift ;;
-f|-F) shift ;;
-*) ;;
*) [ -n "$endpoint" ] || endpoint="$1" ;;
esac
shift
done
file="$GH_FIXTURES/$(printf '%s' "$endpoint" | tr '/?&=' '____').json"
[ ! -f "$file.error" ] || exit 1
if [ -f "$file" ]; then payload="$(cat "$file")"; else payload='[]'; fi
if [ -n "$jqexpr" ]; then jq -r "$jqexpr" <<<"$payload"; else printf '%s\n' "$payload"; fi
exit 0
fi
if [ "$1" = issue ]; then printf '%s\n' "$*" >>"$GH_FIXTURES/edits"; exit 0; fi
echo "gh stub: unexpected call: gh $*" >&2
exit 97
EOF
chmod +x "$ARRIVAL/stub/gh"
printf '%s\n' \
'{"data":{"repository":{"pullRequests":{"nodes":[],"pageInfo":{"hasNextPage":false,"endCursor":null}}}}}' \
>"$ARRIVAL/fixtures/graphql.json"
arrival_fixture() { printf '%s\n' "$1" >"$ARRIVAL/fixtures/repos_owner_repo_issues_91.json"; }
arrival_run() {
: >"$ARRIVAL/fixtures/edits"
env PATH="$ARRIVAL/stub:$PATH" GH_FIXTURES="$ARRIVAL/fixtures" \
REPO=owner/repo LABELS_CONF="$ARRIVAL/labels.conf" \
EVENT_NAME=issues EVENT_ACTION=opened EVENT_ISSUE=91 \
bash "$ROOT/actions/issueflow-reconcile/issueflow-reconcile.sh"
}
arrival_fixture '{"user":{"login":"triage-one"},"labels":[{"name":"ready"}]}'
triage_out="$(arrival_run 2>&1)"
triage_rc=$?
check "a triage-authored arrival exits 0 (#91's four dead mints)" 0 "" \
test "$triage_rc" -eq 0
check "...and its output reaches the sweep" 0 "" \
grep -qF 'issueflow: reconciled.' <<<"$triage_out"
check "...and mints nothing" 1 "" test -s "$ARRIVAL/fixtures/edits"
arrival_fixture '{"user":{"login":"outsider"},"labels":[{"name":"ready"}]}'
outside_out="$(arrival_run 2>&1)"
outside_rc=$?
check "an outside-authored arrival exits 0" 0 "" test "$outside_rc" -eq 0
check "...still mints needs-triage" 0 "" \
grep -qF 'needs-triage (opened by outsider)' <<<"$outside_out"
check "...still strips the smuggled queue label" 0 "" \
grep -qxF 'issue edit 91 -R owner/repo --add-label needs-triage --remove-label ready' \
"$ARRIVAL/fixtures/edits"
check "...and the sweep still runs after the mint" 0 "" \
grep -qF 'issueflow: reconciled.' <<<"$outside_out"
arrival_fixture '{"user":{"login":"outsider"},"labels":[],"pull_request":{"url":"x"}}'
pr_out="$(arrival_run 2>&1)"
pr_rc=$?
check "a PR arrival exits 0" 0 "" test "$pr_rc" -eq 0
check "...stands down without minting" 1 "" test -s "$ARRIVAL/fixtures/edits"
check "...and the sweep still runs" 0 "" \
grep -qF 'issueflow: reconciled.' <<<"$pr_out"
# D2 preserved: only the deliberate stand-downs changed; a genuine failure on
# the arrival path still kills the run loudly.
: >"$ARRIVAL/fixtures/repos_owner_repo_issues_91.json.error"
err_out="$(arrival_run 2>&1)"
err_rc=$?
check "a dead API on the arrival path still fails the run (D2)" 0 "" \
test "$err_rc" -eq 1
check "...and the sweep does not run over a lying arrival" 1 "" \
grep -qF 'issueflow: reconciled.' <<<"$err_out"
summary summary