Skip to content

PR lifecycle: merge-forward avoids the rebase/force-push PR churn, and statusCheckRollup reports running checks as conclusion "" #1436

Description

@kyle-sexton

Two PR-lifecycle facts learned the expensive way while driving #1393/#1316/#1318/#1322 to merge. Both are cheap to act on and both currently cost real work.

1. A stale PR branch does not need a rebase, and rebasing it costs a whole PR

A prior session's handoff recorded this as a hard constraint:

Force-pushing a rebased PR branch. git push --force-with-lease is denied by the auto-mode classifier, and so is the git reset --hard + merge-forward workaround. Consequence: every rebase of a PR branch costs a fresh branch + fresh PR + close the old one. This burned #1315#1377#1393.

The constraint is real but the consequence does not follow. git merge origin/main into the PR branch resolves the conflict and pushes fast-forward — no force-push, no classifier denial. main's ruleset is squash-only (allowed_merge_methods: ["squash"]), so the merge commits inside the PR branch collapse to one commit on merge and required_linear_history is satisfied.

Verified repeatedly this session: #1393 landed that way, and #1318 was merge-forwarded five times as main moved under it, never once needing a new branch.

The cost of not knowing this was #1315#1377#1393: two closed PRs, two abandoned branches, and every review thread on them re-opened from scratch on the successor.

Suggested: state it wherever the force-push block is documented, so the next reader gets the workaround with the constraint. The source-control plugin's PR-lifecycle guidance is the likely home.

2. gh pr view --json statusCheckRollup reports a running check with conclusion: "", not null

The obvious filter for "did anything fail" —

[.statusCheckRollup[] | select(.conclusion != null and .conclusion != "SUCCESS" ...)]

— matches every in-progress check, because an unfinished check has conclusion: "" (empty string), not null. I reported #1318 as having two failing checks when both were simply still running.

Correct forms:

# failures only
[.statusCheckRollup[] | select(.conclusion=="FAILURE" or .conclusion=="TIMED_OUT" or .conclusion=="CANCELLED")]
# still running
[.statusCheckRollup[] | select(.status!="COMPLETED")]

Related trap in the same area, worth pairing: a gate is not proven green by an empty failure list. On a freshly pushed head the rollup can be empty or partial, so absence of failures and absence of checks look identical. Assert presence of each required context — pr-title / pr-title, do-not-merge / do-not-merge, ci-status, security-review / security-review — with conclusion == SUCCESS, rather than inferring from an empty filter.

Suggested: fold both into whatever helper or documented recipe reads check state — babysit-prs' readiness gate is the obvious consumer.


Filed to make a gitignored .work/ handoff discardable; the findings are the durable part.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: mediumReal value, no hard deadline; normal backlog flow.status: needs-decisionAwaiting a human or maintainer judgment call.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions