Skip to content

Explain clean merges and admin pushes in workflow-audit - #769

Merged
nedtwigg merged 4 commits into
mainfrom
workflow-audit-push-actor
Sep 23, 2026
Merged

nedtwigg merged 4 commits into
mainfrom
workflow-audit-push-actor

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

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 equal git merge-tree --write-tree of 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 in GET /repos/{repo}/activity (last quarter, server-set timestamps) whose before..after contains the commit is a push/force_push/branch_creation by a collaborator with admin, and no author/committer field names a bot. The pusher is GitHub's record, which TEND_BOT_TOKEN can'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. actionlint is clean now.

Specs: security-ci.md → "Audit visibility" (new classifiers, reworded identity rule, residual), rationale entry, security-audit.md window-consumer FAIL IF. The budget for security-ci.md is ratcheted.

Tests: scripts/workflow-audit.test.mjs goes 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

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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 19c3453
Status: ✅  Deploy successful!
Preview URL: https://983f0cd5.mouseterm.pages.dev
Branch Preview URL: https://workflow-audit-push-actor.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread .github/workflows/workflow-audit.yaml Outdated
…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>
@nedtwigg

Copy link
Copy Markdown
Member Author

Addressed the draft review in 98eb8ab by closing the gap rather than accepting it:

  • Admin rebase of forged-author bot commits: an admin force_push now explains a commit only if every window commit it replaced (before --not after) was itself admin-introduced, recursively. GitHub serves replaced tips by SHA, so missing ones are fetched. A tip that still can't be resolved ends the walk unexplained unless an admin made that update. Pinned by reports an admin rebase of commits a non-admin pushed / explains an admin rebase of the admin's own commits and the two cannot be resolved tests.
  • Noise cost: none over the last week. The live replay still explains 32 of 45 and reports the same 13.
  • Residual now named in security-ci.md: a forged-author bot commit brought into an admin push that replaces nothing (a cherry-pick, or a rebase pushed to a new branch), or first pushed more than a quarter ago.

Each new guard was mutation-checked.

@dormouse-bot

Copy link
Copy Markdown
Collaborator

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 force_push by the admin. If the replaced tip can't be fetched, the admin's row is skipped, so the new SHA has no explaining row and is reported, which is the safe outcome. The remaining residual (a cherry-pick, or a rebase pushed to a new branch) is now named in security-ci.md, so nothing is left from the draft review.

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_push whose 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 admin push to main that fast-forwards over it, or an admin branch_creation from 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.

Comment thread .github/workflows/workflow-audit.yaml Outdated
nedtwigg and others added 2 commits September 23, 2026 12:13
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
@nedtwigg
nedtwigg marked this pull request as ready for review September 23, 2026 19:42
@nedtwigg
nedtwigg merged commit 1e75cdf into main Sep 23, 2026
11 checks passed
@nedtwigg
nedtwigg deleted the workflow-audit-push-actor branch September 23, 2026 19:42

This branch is waiting to be deployed

1 waiting deployment
hosted-preview — 19c34531 Waiting Sep 23, 2026 by nedtwigg via cleanup #325
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants