diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd0855..a20404f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ so entries say what changed, cite the issue, and stop. ## 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). - `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). diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index 31b12c0..fa30c6b 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -366,11 +366,14 @@ reconcile_issue() { reconcile_opened_issue() { local n="$1" author triage=false labels remove="" label 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")" is_triage_actor "$author" && triage=true 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 grep -qxF "$label" <<<"$labels" && remove="$remove,$label" done diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 28cc14f..8d867c6 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -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 "" \ 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