Repository navigation
tools/pr-land.py: gate, wait and merge a PR only on a fully green CI - #1053
Merged
Merged
Conversation
…o not allow gate is the read-only verdict: the PR open, not a draft, on main or release/v*, at the asserted head, every required context of the base's rules reported and passing, nothing pending, failed or cancelled, mergeStateStatus CLEAN (UNKNOWN and an all-green BLOCKED are re-polled; BEHIND and DIRTY say to merge the base into the branch). wait polls it with a 30-60 s backoff, never gh run list. merge is wait, then gh pr merge --squash --match-head-commit (never --auto) with a required body, retrying only the head-oid race, then a verify against origin/<base>. The subject and body go through outbound-guard in-process before any gh call. Exit codes: 0 green, 1 red, 2 usage, 3 refused, 4 timeout, 5 head moved. tools/test_pr_land.py runs it against a fake gh on PATH with an injected sleeper, real gh JSON as fixtures, and a temporary git origin for the verify. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bHMduhNfrC8w3qjNhEWdB
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bHMduhNfrC8w3qjNhEWdB
…s the merge went through UNSTABLE is GitHub's "mergeable with a non-passing commit status": the required checks pass while a non-required one (CodeQL) still runs or failed. It was refused by name as an unknown state, so a merge started in that window exited 3 at once and never waited, and a failing non-required check gave exit 3 instead of 1 naming the check. It now goes to the rows like BLOCKED: a failed row is exit 1 naming it, a running row is exit 4, all rows passing is lag (exit 4 after the bounded re-poll), and green still needs CLEAN. The verify step after a successful merge: every VERIFY FAILED line starts "gh pr merge succeeded;" (exit 1 also means a red check with nothing merged); a MERGED state with no merge commit is worded on its own; and odd JSON (a check-run without a name, a non-object mergeCommit) is noted, never a traceback. A required_status_checks entry without a context is refused instead of a KeyError. A head race that never clears while the head stays at S is exit 3, not 5. The module docstring states the order judge() actually applies. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bHMduhNfrC8w3qjNhEWdB
…the credential scan passes ci/source_bundle.py refuses any file that holds the Plex token key next to a 20+ character value. The squash-message shape-rule test deliberately writes such a value, so the literal tripped the repository tree scan (PR #1053). The value is now a module constant formatted into the body, as .claude/hooks/outbound-guard-test.py does; the runtime string is unchanged, so the shape rule is still what refuses it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014bHMduhNfrC8w3qjNhEWdB
This was referenced Oct 26, 2026
GLinnik21
added a commit
that referenced
this pull request
Oct 26, 2026
GLinnik21
added a commit
that referenced
this pull request
Oct 26, 2026
GLinnik21
added a commit
that referenced
this pull request
Oct 26, 2026
GLinnik21
added a commit
that referenced
this pull request
Oct 26, 2026
…merges (#1055) `tools/pr-land.py restack --worktree W --upper BRANCH --lower PR [--dry-run] [--push]` After the PR below a stacked PR squash-merges as commit S, the upper branch is brought current by merges, never a rebase: 1. merge the lower PR's final head 2. merge `S^` 3. `merge --no-ff -s ours S` (marks S merged without changing the upper tree, which already has the lower PR's content) 4. merge `origin/<base>` Result: `git diff origin/<base>..BRANCH` is only the upper PR's own change. The manual procedure lived in AGENTS.md prose and was done by hand; step 3 alone (`-s ours` on the squash) drops whatever `main` gained before it, so the four-step order is the part that is easy to get wrong. Safety, all before any fetch or merge: the lower PR must be MERGED with a merge commit; S must exist locally, be an ancestor of `origin/<base>` and have exactly one parent; W must be clean, on BRANCH, and have no merge, cherry-pick or revert in progress; `--upper` must be a plain branch name and not the base branch. `--dry-run` fetches nothing and changes nothing. On a conflict it stops at the failing step and leaves the merge in progress for the human; it never aborts, resets, rebases or forces. `--push` runs only after all four merges, is a plain push, follows the branch's upstream (`origin/<name>` only; any other remote is refused before the merges), and reports the git rejection reason. `python3 tools/test_pr_land.py`: 88 tests OK (hermetic: temp dirs, a local bare origin, a fake `gh` on PATH, no network); the core steps were mutation-checked (drop step 3, swap steps 1 and 2, a fetching dry run and a `--force` push each turn the suite red). One independent review found no blocking findings. This PR was itself restacked onto the #1053 squash with the tool it adds.
Dr-Amp
pushed a commit
to Dr-Amp/plx-native
that referenced
this pull request
Oct 26, 2026
Brings in main's GLinnik21#1054 (one capability value per playback request), GLinnik21#1060 (OpenRecovery state machine for refusals before the first picture) and GLinnik21#1053 (tools/pr-land.py). Live TV adapts to the capability value: request_live takes the Arc<Caps> the way request_play does, and the app passes it at play start. ResolveEnv no longer derives Default, so the channel tests build their environment from ResolveEnv::snapshot and set the live channel on it. The LiveTune type moves above the ResolveEnv doc comment, which it had split. Signed-off-by: Dr-Amp <dr_amp@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Merging has been done with ad-hoc
ghcalls, and twice it went wrong: #387 merged RED becausegh pr mergewas chained after a watcher that read "run completed" mid-run, and #924 readgh run listas success while checks were still running.tools/pr-land.pyis the one place that judges a PR's checks and merges it.pr-land.py gate N [--sha S]: one verdict, one exit code.0green,1red,3refused (not OPEN, wrong base, BEHIND/DIRTY/HAS_HOOKS, a required check skipping or unknown, an unlisted merge state),4not yet (running, missing required check, empty or unstable list),5the head moved off S. Required checks come from the base branch's ruleset (gh api .../rules/branches/<base>); a base that requires nothing needs a stable check list over two polls. Green requiresmergeStateStatus == CLEAN.pr-land.py wait N --sha S [--timeout]: the same judgment, polled.pr-land.py merge N --sha S --subject T (--body-file F | --body-from-pr): waits, thengh pr merge N --squash --match-head-commit S --subject T --body-file -, with the SHA that was gated (never re-read), no--admin,--autoor branch deletion; a head race re-gates and retries only with the same SHA, otherwise exit 5. The squash message is scanned against the repo's private-value list throughoutbound-guard.py(fails closed) and what was scanned is byte-identical to what is sent. Afterwards it fetchesmainand checks that the squash commit is an ancestor, and a verify failure says the merge went through.UNSTABLE(required checks pass, a non-required one such as CodeQL is still running or failed) is judged by its rows: a failed row is red and named, a pending row is "not yet", never green until CLEAN.--body-fileand--body-from-prare mutually exclusive and exactly one is required; the plan's--body-file F [--body-from-pr]cannot be meant literally (it would require a file that the second flag then ignores), and its "a body is required;--body-from-pris opt-in" is what this implements.Scope
Stage 1a of the automation series. NOT in this PR:
restack, theland-prskill, and the outbound-guard refusal of a rawgh pr merge(and its learning this script's text flags, plan line 25); those land after this one has merged something for real.Review
One independent Opus review of the combined diff. The safety trace found no path to
gh pr mergewithout green checks and the gated head. It found 1 MAJOR (UNSTABLE refused by name, which would have blocked the first live use for about ten minutes after the required checks pass and mislabelled a red non-required check as unknown) and 4 MINOR (verify failure wording, a head-race exit code, the documented evaluation order, a traceback after a merge); all five are fixed in the last commit, the new cases red first.Verification
python3 tools/test_pr_land.py:Ran 69 tests ... OK(fakegh, injected clock, temp dirs; the worker also broke the script 12 ways and each break turned the suite red)python3 ci/test_ci_split.py:Ran 16 tests ... OKgh: green, red, timeout and refused shapes all parsed (Transcode: audio above 48 kHz is converted, and the local gate agrees with the profile #1049, Add Polish (pl) localization #1039, Library: keep the A-Z rail under Unwatched and genre filters (#869) #1030, FLAC PCM: declare sampleRate in kHz #1036, UI text: Arabic and Hebrew with RTL paragraphs #1007);gate 1036printsthe pull request is MERGED, not OPEN, exit 3.Not verified:
mergeand its verify step against the real repo (no PR has been merged through it yet; its first live use is the docs-onlyfleet-planPR), the real--match-head-commitrace wording, a real UNSTABLE PR, and thatgh pr merge --body-file -reads stdin (help text only). Needs a checkout to run (gh apifills in the repo; verify fetches origin).No tracking issue (owner: none), so no
Closes #N.🤖 Generated with Claude Code
https://claude.ai/code/session_014bHMduhNfrC8w3qjNhEWdB