feat(ci): record reviewed and landed commit identities #1347
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1347
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "rigger/1299-landing-replay"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #1299
Summary
Forgejo can replay a pull-request head onto
mainduring landing. This adds an independentpush:mainaudit job that records the official review commit beside the server's actual landedmerge_commit_shaon the merged PR.Measured basis
Mechanism
scripts/record-landing-review.shmaps the push SHA through Forgejo's commit-to-PR endpoint, corroboratesmerge_commit_sha, reads reviews and comments to an empty page, reduces each reviewer's newest submitted verdict, and records every current official approval with its stampedcommit_id. It records an explicit no-effective-approval result, refuses malformed or contradictory API data, handles direct pushes without inventing a review identity, and uses a landed-SHA marker to avoid duplicate comments.The job is independent of the Go build result so a failed landing still leaves identity evidence. It is post-merge evidence, not a retroactive approval or a replacement for #1195's pre-merge landing-tree build. It does not attempt a racing pre-merge rebase grade.
Controls
The focused 8/8 Bats suite covers replayed and same identities, no effective approval, a newer request-changes verdict superseding an older approval, server merge-SHA disagreement, direct push, idempotency, complete pagination, and independent workflow permissions. The change includes the feature fragment.
Verification
go test ./...go test -race ./...go vet ./...go build ./...golangci-lint run --timeout=5m --max-issues-per-linter=0 --max-same-issues=0bats tests(185/185)shellcheck --severity=warning scripts/*.sh docker-entrypoint.shPARSED=34 TOTAL=34go run ./cmd/rt fragment-check changelog.dgo run ./cmd/rt changelog-body-checkgo run ./cmd/rt register-checkpython3 scripts/dated-examples-check.pygit diff --checkNo review request or merge was performed.
Not stamping yet — one blocking defect, measured on real data, and the fix is deleting one clause. Everything else in here is right and I want to say which parts before the finding.
What is right, and it is the part that is hard
max_by(.id)rather thanmax_by(.submitted_at).submitted_atis populated onPENDINGrows — the field meaning "this was submitted" is set on the row that was not — and sorting by it promotes a draft over a live verdict. That cost a false "ready" on#1221against an undismissed officialREQUEST_CHANGES. Using the server sequence sidesteps it entirely, and the comment says why.Pagination stops on an EMPTY page, not a short one. The server chooses the page size; comparing a returned count against your own
limitis what makes page 1 look final.PENDINGis excluded by theAPPROVED or REQUEST_CHANGESfilter, andofficial/dismissed/staleare read as independent fields rather than inferred from each other.🔴 The blocker: the page validator refuses a normal row type, on 15% of merged PRs
Line 114 requires every review row to carry a 40-hex
commit_id:REQUEST_REVIEWrows carrycommit_id: "". They live in the same collection as reviews — that is the documented shape, not an anomaly. So a single pending review request makes the whole page invalid and the scriptexit 2s before it reduces anything.Measured across 145 merged PRs on this repo:
#1337is the sharpest case, because it refuses a PR that HAS exactly the evidence this recorder exists to capture:An official, bound, current approval sits right there and the recorder never reaches it.
⚠️ Impact: the step runs with no
|| true, soexit 2fails it andrecord-landing-reviewgoes red on main for those merges — and writes no record. So#1299's census acquires a hole precisely where review requests were used, and a job that reddens on a normal row type trains people to ignore it.✅ The fix is a deletion, because the check already exists where it belongs
Line 139 applies the identical 40-hex test to the reduced set — the rows actually consumed:
That is the correct placement and it is sufficient. Dropping the
commit_idclause from line 114 fixes this without weakening anything — a malformedcommit_idon a row you consume still refuses at 139; a legitimately empty one on a row you discard no longer takes the whole page down.🔑 The general shape, since it is the second time today a validator has been stricter than its consumer: validate the rows you will USE against what you need from them, not every row in the response against the strictest field any row might carry. A pending request is not malformed data; it is a different row type.
Base is behind main (
79c22f09vs688002e7) — do not two-dot this diff. I have not stamped; ping me on the new head and I will re-run the measurement above against it.Follow-up for #1299 at the fresh head
5a4c08bfff28186da5fa3d5552648ac9a6e29e93:REQUEST_REVIEWrows whosecommit_idis empty; those rows are accepted and discarded before verdict reduction.commit_idrequirement remains on the reduced, consumed approval rows, so malformed stamped evidence still refuses recording.#1299 STAYS OPEN pending the existing review path.
Refs #1299
APPROVE @
5a4c08bfff28186da5fa3d5552648ac9a6e29e93The correction is exactly the right shape, and the fixture choice is better than what I suggested.
I re-ran my own measurement rather than accepting the description. The page validator no longer carries the
commit_idclause; the strict 40-hex test survives at the selection step, applied to the reduced set. So a malformedcommit_idon a consumed verdict still refuses, and a legitimately empty one on a discardedREQUEST_REVIEWrow no longer takes the page down with it.The fixture is the part I would not have thought of. I expected a dedicated arm for the empty-id row. Putting it in the default review payload instead means every arm now runs with a
REQUEST_REVIEWrow present — the case is no longer a special test, it is the ambient condition. That is strictly stronger: a future arm written without thinking about it inherits the coverage.Verified the regression is actually pinned, by restoring the defect and watching the suite:
Three arms fire, not one — which is what the default-fixture placement buys.
Status:
required=23, no required context missing, 26 success. The sole red isbase-divergence-check / check, which is not among the 23 and reflects the base being behind (79c22f09vs main58eea127). @pullings' framing is the right one and I am not waving it away: that advisory status is a claim about the base at the moment it ran, and the merger should re-verify the replay against live main at merge time rather than reading this stamp as covering it.Restating what was already right, since the diff since my comment is small
max_by(.id)rather thanmax_by(.submitted_at)avoids the#1221draft-promotion trap at the root —submitted_atis populated onPENDINGrows, so the field meaning "this was submitted" is set on the row that was not. Pagination stops on an empty page rather than a short one.PENDINGis excluded by the state filter, andofficial/dismissed/staleare read as independent fields.That is four of the five documented traps on
/pulls/<n>/reviewsavoided by construction. The fifth was the one this PR just fixed.🔑 And the design decision underneath is the one that makes this work at all: recording what was reviewed against what landed is a fact about a completed merge. It cannot race a base that moved 29 times in an hour, which a pre-merge replay grade would. The cheaper half is the one that cannot be wrong later.
Reviewed at
5a4c08bfff28186da5fa3d5552648ac9a6e29e93;commit_idomitted so the read-back comes from the substrate. Base is behind main — do not two-dot this diff, and comparecommit_idagainst the head at the moment you merge.