From 57abe15a77210913f690f8cf5406a4d048183874 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Sun, 2 Aug 2026 20:44:58 +0000 Subject: [PATCH] fix(issueflow): the issue/PR discriminator is GitHub-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by rehearsing DRY_RUN against heavy-duty/rig's live board, not by any test. issueflow swept ZERO issues on Forgejo and printed "reconciled." — the blind sweep again, one layer in, and invisible because the log is identical to a legitimately empty queue. Measured on the two list endpoints, 2026-08-02: GitHub plain issues OMIT pull_request -> 0 of 9 carried the key Forgejo every entry HAS it, null on issues -> 10 of 10 carried it So `select(has("pull_request") | not)` matched nothing here. Replaced with `.pull_request == null`, which is true for an issue on both forges (an absent key reads as null) and false for a PR on both. Verified against both live list endpoints: Forgejo 10 open issues, GitHub 9 — each matching its API. With the fix the sweep produces real decisions against rig rather than silence: needs-triage on six issues with no queue state, the merged-Refs post-merge transition on #133, and the conflicting-queue-labels flag on #129. The regression test keeps the old expression as a must-fail: it disagrees with the new one on exactly the Forgejo shape. Refs #188 --- .../issueflow-reconcile.sh | 14 ++++++++--- test/issueflow-reconcile.test.sh | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/actions/issueflow-reconcile/issueflow-reconcile.sh b/actions/issueflow-reconcile/issueflow-reconcile.sh index d6004b8..54c2c23 100644 --- a/actions/issueflow-reconcile/issueflow-reconcile.sh +++ b/actions/issueflow-reconcile/issueflow-reconcile.sh @@ -470,7 +470,15 @@ reconcile_opened_issue() { # 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 + # `.pull_request == null`, NOT `has("pull_request") | not` (#188). The key's + # PRESENCE is a GitHub-only discriminator: GitHub omits it on a plain issue, + # Forgejo emits it on every entry and sets it to null. Measured on the list + # endpoints, 2026-08-02 — GitHub 0 of 9 issues carried the key, Forgejo 10 of + # 10 did. So the old test selected ZERO issues here and the sweep printed + # "reconciled." over an untouched board: the blind sweep again, one layer in. + # The null test is true for an issue on both forges (absent key reads as + # null) and false for a PR on both. + jq -e '.pull_request == null' <<<"$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")" @@ -543,10 +551,10 @@ main() { local n for n in $(forge_api --paginate "repos/$REPO/issues?state=open" \ - --jq '.[] | select(has("pull_request") | not) | .number'); do + --jq '.[] | select(.pull_request == null) | .number'); do ( ISSUE_JSON="$(forge_api "repos/$REPO/issues/$n")" - jq -e 'has("pull_request") | not' <<<"$ISSUE_JSON" >/dev/null || exit 0 + jq -e '.pull_request == null' <<<"$ISSUE_JSON" >/dev/null || exit 0 ISSUE_LABELS="$(jq -r '.labels[].name' <<<"$ISSUE_JSON")" reconcile_issue "$n" ) || log "#$n: reconcile failed — continuing with the remaining issues" diff --git a/test/issueflow-reconcile.test.sh b/test/issueflow-reconcile.test.sh index 83eee06..858d1cd 100644 --- a/test/issueflow-reconcile.test.sh +++ b/test/issueflow-reconcile.test.sh @@ -674,6 +674,30 @@ check "...and performs the release edit from the executable path" 0 "" \ grep -qF -- 'issue edit 40 -R owner/repo --remove-assignee builder --remove-label claimed --add-label post-merge' \ "$ARRIVAL/fixtures/edits" +# -- the issue/PR discriminator is not `has("pull_request")` --------------- +# Measured on the two list endpoints, 2026-08-02 (#188): +# +# GitHub plain issues OMIT the key -> 0 of 9 carried it +# Forgejo every entry HAS the key -> 10 of 10, valued null on issues +# +# So `select(has("pull_request") | not)` selected ZERO issues on Forgejo and +# the sweep printed "reconciled." over an untouched board — the blind sweep +# again, one layer in, and invisible because the log looks identical to a +# legitimately empty queue. Caught by rehearsing DRY_RUN against rig's live +# board, not by any unit test. `.pull_request == null` is true for an issue on +# both forges (an absent key reads as null) and false for a PR on both. +disc() { jq -e "$1" >/dev/null 2>&1 && echo issue || echo pr; } +check "github-shaped issue (key absent) reads as an issue" 0 "issue" \ + bash -c 'echo "{\"number\":1}" | jq -e ".pull_request == null" >/dev/null && echo issue || echo pr' +check "forgejo-shaped issue (key present, null) reads as an issue" 0 "issue" \ + bash -c 'echo "{\"number\":1,\"pull_request\":null}" | jq -e ".pull_request == null" >/dev/null && echo issue || echo pr' +check "a PR reads as a PR on either shape" 0 "pr" \ + bash -c 'echo "{\"number\":1,\"pull_request\":{\"url\":\"x\"}}" | jq -e ".pull_request == null" >/dev/null && echo issue || echo pr' +# The old test, kept as the must-fail: it disagrees with the new one on the +# forgejo shape, which is exactly the bug. +check "the old has() test misreads a forgejo issue as a PR" 0 "pr" \ + bash -c 'echo "{\"number\":1,\"pull_request\":null}" | jq -e "has(\"pull_request\") | not" >/dev/null && echo issue || echo pr' + # -- the OPEN-pull gather, at main() granularity ---------------------------- # The closed/merged half above proves one REST path; this proves the other, # which is a DIFFERENT pipeline: `.body | @base64` -> base64 -d ->