Governed Delivery Fails with delivery-preflight:pull-request-count When a Reused Branch Has Historical Merged PRs
A governed delivery pipeline that queries GitHub for all pull requests on a branch using --state all exits with delivery-preflight:pull-request-count whenever a previously merged PR shares the branch name with…
At a Glance
A governed delivery pipeline that queries GitHub for all pull requests on a branch using --state all exits with delivery-preflight:pull-request-count whenever a previously merged PR shares the branch name with the current open PR.
Summary
A governed delivery pipeline that queries GitHub for all pull requests on a branch using --state all exits with delivery-preflight:pull-request-count whenever a previously merged PR shares the branch name with the current open PR. The root cause is that the pipeline compares the total number of fetched PR snapshots—not the subset whose headRefOid matches the local HEAD SHA—against the required count of one. The practical impact is a complete delivery block: no intent is written and no provider action is emitted, even though exactly one PR points to the current exact head.
Root-Cause Analysis
Confirmed Evidence
- Branch
codex/939-full-auto-mergehas two associated pull requests returned by GitHub when queried with--state all: - PR #1385: merged,
headRefOid=ac36528f7cc526f81e34da1350f62e6e7f6a7c34(historical head) - PR #1391: open,
headRefOid=8e738e8decb3baedd278b172592265800cfe2b54(current local HEAD) - Running
/task deliver #1390exits with error codedelivery-preflight:pull-request-countbefore any preflight check that inspectsheadRefOid. - The delivery pipeline treats the raw length of the fetched PR array (
2) as the identity assertion. It requires that length to equal1, so it fails immediately.
Root Cause
The identity check is applied too early and against the wrong cardinality. The pipeline performs the following steps in order:
- Fetch all PRs for the branch with
--state all(required so that a post-merge delivery rerun can locate the merged PR and write its receipt). - Assert
fetchedArray.length === 1— this is where the failure occurs. - (Never reached) Filter or inspect
headRefOid.
GitHub branch names are mutable and reusable across time. A branch that previously delivered PR #1385 and was kept open now also backs PR #1391. --state all correctly returns both. The pipeline, however, was written with the assumption that a branch maps to exactly one PR at all times, which is false for reused governed delivery branches.
The immutable delivery identity is the local HEAD commit SHA, not the branch name. Every other authority in the system—required checks, review gates, provider action authorization—already uses the exact HEAD SHA as the binding key.
Alternative Causes Ruled Out
- Network or API pagination issue: Ruled out. GitHub deterministically returns both PRs; the count of
2is correct, not a side effect of a partial response. - Incorrect
--stateflag: Ruling outmergedstate would fix the immediate symptom but break post-merge receipt recovery, which is a documented requirement. This is not the fix. - Duplicate open PRs at the same head: Not the case here. Only PR #1391 points to
8e738e8decb3baedd278b172592265800cfe2b54.
Resolution Steps
- Locate the PR snapshot array construction in the delivery pipeline, immediately after the GitHub branch PR fetch returns all results.
- Insert a
headRefOidfilter before any cardinality assertion:
const currentHeadPrs = allBranchPrs.filter(
(pr) => pr.headRefOid === localHeadSha
);- Replace the raw array length check with a check against
currentHeadPrs.length:
currentHeadPrs.length === 0→ fail closed withdelivery-preflight:pull-request-count(no PR for this exact head)currentHeadPrs.length > 1→ fail closed withdelivery-preflight:pull-request-count(ambiguous exact-head match)currentHeadPrs.length === 1→ proceed; passcurrentHeadPrs[0]to all downstream preflight, checks, and provider action logic
- Retain merged-PR recovery: After the filter, if
currentHeadPrs[0].state === 'MERGED', continue the existing merged-receipt recovery path. The filter does not break this path because a merged PR at the exact current head is a valid recovery target.
- Preserve the
delivery-preflight:head-mismatcherror path: If the sole fetched PR exists but itsheadRefOiddoes not matchlocalHeadSha(i.e.,allBranchPrs.length === 1andcurrentHeadPrs.length === 0), the error must remaindelivery-preflight:head-mismatch, notdelivery-preflight:pull-request-count. Ensure your filter logic does not collapse these two distinct failure modes.
- Add unit tests covering the three new cases via
runDeliver:
- One historical merged PR + one current-head open PR → selects the current PR, delivery proceeds.
- Multiple PRs associated with the branch, zero matching local HEAD → fails with
delivery-preflight:pull-request-count. - Multiple PRs associated with the branch, two matching local HEAD → fails with
delivery-preflight:pull-request-count.
Configuration Snippets
No configuration file changes are required. The fix is entirely in delivery pipeline logic.
CLI Commands
Run the targeted delivery test suite after applying the fix:
node --test scripts/tests/unit/task-tracker/verbs/deliver.test.mjsRun the full test suite:
npm test
npm run test:slowValidate lint and formatting:
npm run lint
npm run format:checkConfirm the fix commit:
git log --oneline -1Verification
Expected Behavior After Fix
- Delivery proceeds for PR #1391 when branch
codex/939-full-auto-mergehas both PR #1385 (merged, historical head) and PR #1391 (open, current head8e738e8decb3baedd278b172592265800cfe2b54). The emitted provider action must name PR #1391 and reference its exact head SHA.
- Unit test suite exits 0:
node --test scripts/tests/unit/task-tracker/verbs/deliver.test.mjs
# Expected: all tests pass, exit code 0- Full suite exits 0:
npm test && npm run test:slow
# Expected: exit code 0 across all lanes- Zero exact-head matches still blocks delivery with
delivery-preflight:pull-request-count; verify via the new unit test case.
- Two exact-head matches still block delivery with
delivery-preflight:pull-request-count; verify via the new unit test case.
- Single wrong-head PR still fails with
delivery-preflight:head-mismatch, notpull-request-count; verify existing test coverage remains green.
Rollback Indicator
If after the fix delivery-preflight:pull-request-count still fires when exactly one PR matches the local HEAD, the filter is either not applied or is comparing the wrong field. Confirm that headRefOid is populated in the PR snapshot object returned by the GitHub API client—some GraphQL projections omit it unless explicitly requested in the query fragment.
Prevention
Code Guardrails
- Require
headRefOidin the PR snapshot type/schema: Add a runtime assertion or TypeScript type guard thatheadRefOidis a non-empty string before the filter runs. A missing field silently produces zero matches, which would look identical to a legitimate no-PR state. - Encapsulate the filter as a named function (
selectCurrentHeadPror similar) so future callers cannot accidentally bypass it and access the raw branch PR array directly.
Testing
- Maintain the three new exact-head test cases permanently as regression tests. They directly encode the contract: branch name is not delivery identity; exact HEAD SHA is.
- Add a test for the merged-recovery path where the merged PR is at the current exact head, confirming that merged state does not disqualify a match.
Monitoring and Alerting
- Alert on
delivery-preflight:pull-request-counterrors in CI/CD telemetry. Because this error is fail-closed, a spike indicates either a real ambiguity (two engineers targeting the same head) or a regression in the filter logic. - Log the full list of branch-associated PR numbers and their head SHAs at DEBUG level during delivery preflight. This makes future ambiguity diagnoses instantaneous without requiring a re-run.
Operational Procedures
- Do not delete or force-push governed delivery branches between deliveries unless the branch PR history is explicitly reset. Keeping the branch intact is valid, but operators should expect
--state allto return the full history. - Document the exact-head identity contract in the governed delivery specification: the local HEAD commit SHA is the sole binding delivery identity; branch name is a lookup key only.