Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 73 additions & 13 deletions bin/fm-pr-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
# A GitHub merge is refused unless every pre-merge condition holds, each read
# live at merge time rather than taken from recorded metadata: the pull request
# is open, not a draft, mergeable, free of conflicts, and every unwaived check
# is green at the exact current head commit. Every failing condition is reported, not
# is green at the exact current head commit, where github_checks_not_green below
# owns what makes a check green and judges each one by its current run.
# Every failing condition is reported, not
# just the first. The verified head is then passed to gh as
# --match-head-commit, so a push that lands between that read and the merge
# fails the merge instead of landing commits nothing verified. Reading that
Expand Down Expand Up @@ -443,22 +445,80 @@ FIELDS
}

# Every GitHub check that is not green in the given live pull-request JSON, one
# name per line: a status context whose state is not SUCCESS, or a check run
# that has not completed with SUCCESS, NEUTRAL, or SKIPPED (so a pending
# check is not green either). Exits nonzero when the rollup cannot be read, so
# a malformed answer is a failed read and never an empty red set.
# name per line. An entry is green when it is a status context whose state is
# SUCCESS, or a check run that completed with SUCCESS, NEUTRAL, or SKIPPED (so
# a pending check is not green either). Exits nonzero when the rollup cannot be
# read, so a malformed answer is a failed read and never an empty red set.
#
# The rollup can hold several runs of one check name at the same head, because
# GitHub cancels a pull request's in-flight run when the base branch advances
# and re-triggers it; the cancelled run stays in the rollup beside the passing
# re-run. A check is therefore judged by its current run rather than by any run
# that a later one superseded, which is what makes this agree with GitHub's own
# CLEAN mergeStateStatus instead of refusing a pull request GitHub considers
# mergeable.
#
# Supersession applies only among check runs with the same reported name. A
# name is dropped from the red set only when every non-green run is COMPLETED,
# has a whole-second UTC startedAt, and started strictly before a green run.
# Status contexts are never grouped or superseded, and every non-green one is
# reported independently. A still-running, queued, undated, or tied check run
# stays red. A name whose runs are all green needs no timestamp, while a name
# with no green run stays red.
#
# The reported name is also what --allow-red matches. An unnamed check run is
# grouped alone and can neither supersede nor be superseded, because unrelated
# unnamed checks must not be treated as one.
github_checks_not_green() {
local json=$1
printf '%s' "$json" | jq -r '
def settled_at:
if type == "string" and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
then . else null end;
if (.statusCheckRollup | type) != "array" then error("no check rollup") else . end
| .statusCheckRollup[]
| if .__typename == "CheckRun" then
{name: (.name // ""), ok: (.status == "COMPLETED" and (.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED"))}
else
{name: (.context // ""), ok: (.state == "SUCCESS")}
end
| select(.ok | not)
| if .name == "" then "(unnamed check)" else .name end
| [ .statusCheckRollup
| to_entries[]
| .key as $i
| .value
| if .__typename == "CheckRun" then
{
kind: "check_run",
name: (.name // ""),
completed: (.status == "COMPLETED"),
ok: (.status == "COMPLETED" and (.conclusion == "SUCCESS" or .conclusion == "NEUTRAL" or .conclusion == "SKIPPED")),
at: (.startedAt | settled_at)
}
| . + {group: (if .name == "" then ["", $i] else [.name, -1] end)}
else
{kind: "status_context", name: (.context // ""), ok: (.state == "SUCCESS")}
end
]
| . as $entries
| (
($entries[]
| select(.kind == "status_context" and (.ok | not))
| .name
),
($entries
| [.[] | select(.kind == "check_run")]
| group_by(.group)[]
| {
name: .[0].name,
reds: [.[] | select(.ok | not)],
newest_green: ([.[] | select(.ok) | .at | select(. != null)] | max)
}
| select(
(.reds | length) > 0
and (
.newest_green == null
or any(.reds[]; (.completed | not) or .at == null)
or ([.reds[] | .at] | max) >= .newest_green
)
)
| .name
)
)
| if . == "" then "(unnamed check)" else . end
' 2>/dev/null || return 1
}

Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ This repo uses that setting, and its own `.no-mistakes/` directory remains local
PR-based task merges go through `bin/fm-pr-merge.sh`, which records `pr=` and any available `pr_head=` through `bin/fm-pr-check.sh` before calling the forge CLI.
The helper requires a full canonical URL and rejects malformed URLs or repo override flags before recording merge state.
A `https://github.com/<owner>/<repo>/pull/<n>` URL requires `gh` and `jq`, is merged only after one live read confirms the pull request is open, not a draft, mergeable, conflict-free, and every unwaived check is green at the current head, then `gh pr merge` binds that verified head with `--match-head-commit`.
A check run is green when its current run is green, because GitHub leaves a cancelled run in the rollup beside the passing re-run it triggered when the base branch advanced; `bin/fm-pr-merge.sh`'s `github_checks_not_green` owns the rule, which uses `startedAt` to clear only an older completed check run that a passing run with the same name provably replaced, while unfinished check runs and non-green status contexts stay red.
`--auto`, `--admin`, and branch-deletion flags are refused unless `--attended-override` is passed for an explicit captain instruction; that override never skips the live green check, the away-grant check, or a captain hold.
An attended `--allow-red <check-name>` may appear once, waives only GitHub checks with that exact name, and is refused while the away-posture record exists.
A `https://<host>/<path>/-/merge_requests/<n>` URL (see [docs/gitlab-merge-watch.md](gitlab-merge-watch.md)) invokes `glab mr merge <n> -R https://<host>/<path>`, so the instance comes from the URL, and adds no merge-method flag because the project's own merge method applies.
Expand Down
Loading
Loading