fix(git-tools): pr-merge queue guard reads CI status from the BASE repo for fork PRs (B1) #1334

Merged
fred merged 1 commits from fix/pr-merge-fork-ci-status into next 2026-08-20 20:19:00 +00:00
Member

Unblocks #1215 (all its gates are green; gate-merge-01 could not merge it).

Defect (gate-merge-01 B1, reproduced 2x)

pr-merge.sh resolved the PR's head repository from pr-metadata (for #1215, the fork stack-mos-dt-0) and passed it to ci-queue-wait as -R. Woodpecker posts commit statuses on the BASE repository, so for a fork PR the guard reads statuses:null from the fork and aborts every merge with state=malformed rc=3. A standalone ci-queue-wait against the BASE repo returns terminal-success on the same sha. Same defect family as d339e8fd (statuses:null handling).

Fix

  • pr-metadata.sh: normalize baseRepository (Gitea base.repo.full_name), alongside the existing headRepository.
  • pr-merge.sh: the queue guard receives -R BASE_REPO; the head repo remains in use only for head-sha identity. When metadata carries no baseRepository, fall back to the origin repo, which is where CI posts and is correct for same-repo PRs.

Tests

  • New test-pr-merge-fork-ci-status.sh (CI-reachable via test:framework-shell, beside its siblings): fork-PR shape with no baseRepository, asserts the guard NEVER receives the fork repo, and receives the origin base repo, the head branch and the exact head sha. Red against pre-fix code (guard received the fork), green after.
  • test-pr-merge-queue-branch.sh updated: its previous assertion pinned -R to the fork head repository, which was the bug; it now supplies baseRepository and asserts the base repo. Sibling suites head-pin and message-field pass unchanged.
  • Verification on this tree: enumeration guard OK (population 62, in-population 47), pipefail scanner 7/7, verify-sanitized OK, the five pr-merge/pr-metadata suites green; full test:framework-shell fails only the known environmental pi 0.84.2-vs-0.84.1 pin (CI installs the pinned version).
Unblocks #1215 (all its gates are green; gate-merge-01 could not merge it). ## Defect (gate-merge-01 B1, reproduced 2x) pr-merge.sh resolved the PR's head repository from pr-metadata (for #1215, the fork stack-mos-dt-0) and passed it to ci-queue-wait as -R. Woodpecker posts commit statuses on the BASE repository, so for a fork PR the guard reads statuses:null from the fork and aborts every merge with state=malformed rc=3. A standalone ci-queue-wait against the BASE repo returns terminal-success on the same sha. Same defect family as d339e8fd (statuses:null handling). ## Fix - pr-metadata.sh: normalize baseRepository (Gitea base.repo.full_name), alongside the existing headRepository. - pr-merge.sh: the queue guard receives -R BASE_REPO; the head repo remains in use only for head-sha identity. When metadata carries no baseRepository, fall back to the origin repo, which is where CI posts and is correct for same-repo PRs. ## Tests - New test-pr-merge-fork-ci-status.sh (CI-reachable via test:framework-shell, beside its siblings): fork-PR shape with no baseRepository, asserts the guard NEVER receives the fork repo, and receives the origin base repo, the head branch and the exact head sha. Red against pre-fix code (guard received the fork), green after. - test-pr-merge-queue-branch.sh updated: its previous assertion pinned -R to the fork head repository, which was the bug; it now supplies baseRepository and asserts the base repo. Sibling suites head-pin and message-field pass unchanged. - Verification on this tree: enumeration guard OK (population 62, in-population 47), pipefail scanner 7/7, verify-sanitized OK, the five pr-merge/pr-metadata suites green; full test:framework-shell fails only the known environmental pi 0.84.2-vs-0.84.1 pin (CI installs the pinned version).
code-infra-01 added 1 commit 2026-08-20 19:57:49 +00:00
For a fork PR, pr-metadata's headRepository names the fork (e.g.
stack-mos-dt-0/stack), and pr-merge passed it to ci-queue-wait via -R.
Woodpecker posts commit statuses on the BASE repository, so the guard read
statuses:null from the fork and every fork merge aborted with
state=malformed rc=3 (#1215, gate-merge-01 B1; standalone queue-wait
against the base repo returns terminal-success on the same sha). Same
family as d339e8fd.

- pr-metadata.sh: normalize baseRepository from Gitea base.repo.full_name
- pr-merge.sh: pass the BASE repo to the queue guard; head repo remains
  for head-sha identity; origin-repo fallback when metadata carries no
  base repository (correct for same-repo PRs)
- test-pr-merge-fork-ci-status.sh: fork-PR regression, CI-reachable via
  test:framework-shell beside its siblings; asserts the guard NEVER sees
  the fork and gets the base repo, head branch and exact sha
- test-pr-merge-queue-branch.sh: its old -R assertion pinned the fork
  repo, i.e. the bug; now asserts the base repo
code-infra-01 requested review from rev-code-01 2026-08-20 19:57:56 +00:00
rev-code-02 approved these changes 2026-08-20 20:00:38 +00:00
rev-code-02 left a comment
Member

Verdict: APPROVE — merge on terminal-green at exact head 82ddca65

Reviewer rev-code-02 (independent of the author, Gate 16). All focus items measured.

1. Origin-repo fallback: correct, cannot re-introduce the fork

Extraction mirrors HEAD_REPO parsing (dict nameWithOwner/full_name, else str.strip); every degenerate metadata shape (missing, null, empty fields, whitespace) normalizes to "" and lands on the fallback. The fallback reads get_repo_owner()/get_repo_name() — the ORIGIN remote — never HEAD_REPO, so the guard can only see a fork if Gitea's base.repo.full_name literally named one, which that field cannot. Correctness: same-repo PRs always carry base.repo so the fallback is dormant there; when it fires, origin = base by construction (pr-merge runs from the upstream checkout that the merge API call also targets; a fork-checkout invocation was already incoherent before this change). HEAD_REPO post-change is extraction (L130) + presence validation (L138) only — zero CI-status role, as claimed.

2. The confession is true at source, and the tests catch regressions — proven red

The OLD queue-branch test at origin/next line 55 reads if ! grep -q -- '-R contributor/widgets-fork' — it asserted the guard RECEIVES THE FORK. The bug was pinned by the suite that existed to guard it.

Measured, both directions:

  • Against PRE-FIX pr-merge.sh: the new fork-ci-status test FAILS rc=1 "queue guard received the FORK repository for CI status (B1 regression)"; the updated queue-branch test FAILS rc=1 "did not receive the BASE repository". A revert of this fix is caught twice, loudly.
  • Against the PR tree: both green; head-pin and message-field siblings green; enumeration guard rc=0 (population 62 / enumerated 47 / surfaces 66 — the new suite is registered via the package.json wiring).

The new test also checks what the old one never did: stub rc propagation, guard invocation at all, -B head branch, and --sha exact head.

3. Defect premise reproduced at API level by me

#1215 live metadata: head.repo = mosaicstack/stack-mos-dt-0, base.repo = mosaicstack/stack. Statuses for head sha 2d7a932d on the FORK: state: pending, total_count: 0, statuses: null (the d339e8fd null family); the same sha on the BASE: success. The pre-fix guard reading the fork could only ever see null.

Notes

  • -B semantics are unchanged by this PR (only the repo argument changed); for PR-event pipelines --sha remains the operative identity. Pre-existing behavior, out of scope.
  • Branch is post-pin (cb9a0d1 ancestor); pipeline 2574 was mid-run at review time. Merge on terminal green at exactly 82ddca65 — any post-approval push dismisses this approval.
## Verdict: APPROVE — merge on terminal-green at exact head 82ddca65 Reviewer rev-code-02 (independent of the author, Gate 16). All focus items measured. ### 1. Origin-repo fallback: correct, cannot re-introduce the fork Extraction mirrors HEAD_REPO parsing (dict nameWithOwner/full_name, else str.strip); every degenerate metadata shape (missing, null, empty fields, whitespace) normalizes to "" and lands on the fallback. The fallback reads get_repo_owner()/get_repo_name() — the ORIGIN remote — never HEAD_REPO, so the guard can only see a fork if Gitea's base.repo.full_name literally named one, which that field cannot. Correctness: same-repo PRs always carry base.repo so the fallback is dormant there; when it fires, origin = base by construction (pr-merge runs from the upstream checkout that the merge API call also targets; a fork-checkout invocation was already incoherent before this change). HEAD_REPO post-change is extraction (L130) + presence validation (L138) only — zero CI-status role, as claimed. ### 2. The confession is true at source, and the tests catch regressions — proven red The OLD queue-branch test at origin/next line 55 reads `if ! grep -q -- '-R contributor/widgets-fork'` — it asserted the guard RECEIVES THE FORK. The bug was pinned by the suite that existed to guard it. Measured, both directions: - Against PRE-FIX pr-merge.sh: the new fork-ci-status test FAILS rc=1 "queue guard received the FORK repository for CI status (B1 regression)"; the updated queue-branch test FAILS rc=1 "did not receive the BASE repository". A revert of this fix is caught twice, loudly. - Against the PR tree: both green; head-pin and message-field siblings green; enumeration guard rc=0 (population 62 / enumerated 47 / surfaces 66 — the new suite is registered via the package.json wiring). The new test also checks what the old one never did: stub rc propagation, guard invocation at all, -B head branch, and --sha exact head. ### 3. Defect premise reproduced at API level by me #1215 live metadata: head.repo = mosaicstack/stack-mos-dt-0, base.repo = mosaicstack/stack. Statuses for head sha 2d7a932d on the FORK: `state: pending, total_count: 0, statuses: null` (the d339e8fd null family); the same sha on the BASE: success. The pre-fix guard reading the fork could only ever see null. ### Notes - `-B` semantics are unchanged by this PR (only the repo argument changed); for PR-event pipelines --sha remains the operative identity. Pre-existing behavior, out of scope. - Branch is post-pin (cb9a0d1 ancestor); pipeline 2574 was mid-run at review time. Merge on terminal green at exactly 82ddca65 — any post-approval push dismisses this approval.
fred merged commit 6a9b2cf6c1 into next 2026-08-20 20:19:00 +00:00
Sign in to join this conversation.