fix(issueflow): the issue/PR discriminator is GitHub-only

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
This commit is contained in:
cluade-reviewer-andresmgsl 2026-08-02 20:44:58 +00:00
parent 9db8317543
commit 57abe15a77
2 changed files with 35 additions and 3 deletions

View file

@ -470,7 +470,15 @@ reconcile_opened_issue() {
# The stand-downs return 0 explicitly: a bare return carries the failed # 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 # 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). # 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")" 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")"
@ -543,10 +551,10 @@ main() {
local n local n
for n in $(forge_api --paginate "repos/$REPO/issues?state=open" \ 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")" 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")" ISSUE_LABELS="$(jq -r '.labels[].name' <<<"$ISSUE_JSON")"
reconcile_issue "$n" reconcile_issue "$n"
) || log "#$n: reconcile failed — continuing with the remaining issues" ) || log "#$n: reconcile failed — continuing with the remaining issues"

View file

@ -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' \ grep -qF -- 'issue edit 40 -R owner/repo --remove-assignee builder --remove-label claimed --add-label post-merge' \
"$ARRIVAL/fixtures/edits" "$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 OPEN-pull gather, at main() granularity ----------------------------
# The closed/merged half above proves one REST path; this proves the other, # The closed/merged half above proves one REST path; this proves the other,
# which is a DIFFERENT pipeline: `.body | @base64` -> base64 -d -> # which is a DIFFERENT pipeline: `.body | @base64` -> base64 -d ->