Explain clean merges and admin pushes in workflow-audit - #769
Conversation
Most nightly issues were the admin's own commits, re-reported on every rebase and merge from main. Two new classifiers: - is_clean_merge: a two-parent merge whose window paths equal `git merge-tree --write-tree` of its parents. - is_admin_first_push: the earliest ref update in the repository activity log containing the commit is a push by an admin, and no author/committer field names a bot. The pusher is GitHub's record, which TEND_BOT_TOKEN cannot forge; PR merges never count as an introduction. Unexplained entries now name who first pushed them. A week's replay explains 32 of 45 window commits; the rest are dormouse-bot pushes. Also reword a comment line that shellcheck parsed as a directive, which had silently disabled shellcheck for the whole audit script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
19c3453
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://983f0cd5.mouseterm.pages.dev |
| Branch Preview URL: | https://workflow-audit-push-actor.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Draft feedback, not a merge verdict. Mark the PR ready when you want the full review.
A rebase by an admin turns a bot commit into an "admin push" if the bot forged a human author. The bot-name refusal in is_admin_first_push is the only thing covering the rebase case (the rationale says so). But it reads the commit's self-declared author and committer, which the spec itself calls non-evidence against TEND_BOT_TOKEN. Here is the path: the bot pushes a commit to its PR branch with GIT_AUTHOR_NAME/EMAIL set to a maintainer. An admin then rebases that branch (locally, or with "Update branch → rebase") and force-pushes before the nightly run. The old SHA is no longer reachable, so git log --all never lists it. The new SHA's earliest activity event is the admin's force_push, and its author fields name a human, so it is classified "first pushed by an admin" and is never reported. The residual in security-ci.md → "Audit visibility" only names the more-than-a-quarter-old case. Either close this gap, or name it there as a residual that was weighed and accepted. One way to close it: when the introducing event is a force_push, don't explain the commit if before..after rewrote commits that a non-admin first pushed. Whether that brings back too much of the rebase noise this PR is meant to cut is your call.
…gned Review on #769: - A force-push by an admin now explains a commit only if every window commit it replaced was admin-introduced too; a forged human author on a bot commit could otherwise ride an admin rebase. GitHub serves replaced tips by SHA, so missing ones are fetched; an unfetchable tip ends the walk unexplained unless an admin made that update. - Null activity fields become `-`, not empty: tab is IFS whitespace, so an empty column shifted every later one left. Lookups now index the activity log once (tips pinned as refs/audit/*, one for-each-ref --contains per commit, one admin query per actor); the week's replay runs in ~60s with the same 13 unexplained commits. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Addressed the draft review in 98eb8ab by closing the gap rather than accepting it:
Each new guard was mutation-checked. |
|
Checked 98eb8ab: this closes the rebase gap I raised. The case I described (a bot commit with a forged human author, then rebased by an admin) now recurses into the replaced commits. Their first introduction is the bot's push, so the new SHA is reported. That holds whether the admin rebases locally or clicks "Update branch → rebase", because both show up as a |
dormouse-bot
left a comment
There was a problem hiding this comment.
This is feedback on a draft, not a merge verdict. Mark the PR ready when you want the full review.
Both earlier concerns are closed. One new issue: a single SHA that can't be fetched leaves its whole 100-SHA fetch batch unresolved. git fetch origin <sha>... is all-or-nothing. If any requested object is gone (for example, a force-pushed-away bot tip that GitHub has since garbage-collected), the fetch exits 128 with not our ref and fetches none of the other SHAs. I reproduced this locally against a file:// remote with allowAnySHA1InWant. The || true hides the failure, so every row whose tip was in that batch becomes unresolved, with two effects:
- A non-admin unresolved row ends the walk for every later commit. The report fills up with noise until that row ages out of the quarter.
- An admin
force_pushwhose replaced tip was in the failed batch is skipped instead of checked. The earlier reply says the rebased SHA is then reported, but that holds only while no other row contains it. A later adminpushtomainthat fast-forwards over it, or an adminbranch_creationfrom it, becomes its first introduction and explains it. That reopens the rebase gap this commit closes.
Falling back to one fetch per SHA when a batch fails keeps a bad SHA from affecting its neighbours. The suggestion below behaves correctly in the same local repro: the good SHA arrives, and the non-zero exit is still absorbed by || true.
Review on #769: `git fetch` is all-or-nothing, so one garbage-collected tip cost its whole batch. A failed batch now retries one SHA at a time. The skip rule for unresolvable rows was also too broad: an admin force-push whose tips are lost may be the rewrite that introduced a bot commit under a new SHA, so it now ends the walk unexplained. Only an admin's plain push or branch creation with a lost new tip is skipped. A lost replaced tip needs no state of its own: it gets no ref, so the replaced-commit check fails closed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ctor # Conflicts: # scripts/spec-word-budgets.json
Cuts workflow-audit noise without weakening what it catches. The six open
[workflow-audit]issues held 24 commits; 15 were the admin's own work, re-reported after rebases and merges from main.Two new classifiers (
.github/workflows/workflow-audit.yaml):is_clean_merge: a two-parent merge whose window paths equalgit merge-tree --write-treeof its parents. A conflict outside the window doesn't disqualify it; a hand-resolved conflict inside it does.is_admin_first_push: the earliest ref update inGET /repos/{repo}/activity(last quarter, server-set timestamps) whosebefore..aftercontains the commit is apush/force_push/branch_creationby a collaborator withadmin, and no author/committer field names a bot. The pusher is GitHub's record, whichTEND_BOT_TOKENcan't forge. A PR merge never counts as an introduction.Each unexplained entry now names who first pushed it.
Replay of the real script against the live repo since 2026-09-16: 45 window commits, 32 explained. The 13 still reported are 11 dormouse-bot pushes plus 2 admin squash merges of bot PRs, which is what this audit exists to surface.
Also fixed: a comment line starting with
# shellcheck exemption.was parsed as a directive, which aborted shellcheck for the whole run block.actionlintis clean now.Specs:
security-ci.md→ "Audit visibility" (new classifiers, reworded identity rule, residual), rationale entry,security-audit.mdwindow-consumer FAIL IF. The budget forsecurity-ci.mdis ratcheted.Tests:
scripts/workflow-audit.test.mjsgoes from 6 to 17 cases. Each new guard was mutation-checked: deleting it turns a test red.Unverified: whether
GITHUB_TOKEN(contents: read) can read/activity. If it can't, the classifier explains nothing, which is the safe side, and the run summary says so.🤖 Generated with Claude Code