Skip to content

fix(standards): scope from HEAD^1 so private repos stop sweeping whole-repo - #171

Merged
twistedmelonman merged 1 commit into
mainfrom
claude/fix-private-repo-scoping-dc0d321a
Sep 18, 2026
Merged

twistedmelonman merged 1 commit into
mainfrom
claude/fix-private-repo-scoping-dc0d321a

Conversation

@twistedmelonman

Copy link
Copy Markdown
Member

Problem

Changed-files scoping has never worked on a private repo. Every PR in one is linted whole-repo and fails on debt its author never touched — precisely what the scoping exists to prevent (standards-check.yml:25-31).

Cause

"Resolve scope" narrowed the run by fetching the PR base:

git -C repo fetch --depth=1 origin "${BASE_SHA}"

The checkout immediately above sets persist-credentials: false, so no credential remains. A public repo serves that fetch unauthenticated and scopes correctly. A private repo cannot:

fatal: could not read Username for 'https://github.com': No such device or address

and the step falls back to a whole-repo sweep — correctly and loudly, but on every PR, forever.

Repo Private Result
nightowlstudiollc/kebab-tax yes fetch failed → whole-repo
nightowlstudiollc/financial-agent yes fetch failed → whole-repo
nightowlstudiollc/amelia-boone no scoped ✅
smartwatermelon/archive-resolver no scoped ✅
twistedmelonman/dotfiles no scoped ✅

9 private repos carry a standards-check caller and are affected.

Fix

Take the base from the local commit graph instead of the network.

For a pull_request event HEAD is refs/pull/N/merge, whose first parent is the base. fetch-depth: 2 makes HEAD^1 the diff target — no fetch, no credential, and persist-credentials: false stays exactly as it is. That setting is deliberate hardening against zizmor's artipacked, not an oversight to route around.

HEAD^1 is also strictly more accurate than the payload's base.sha, which goes stale if the base branch moves after the event fires. HEAD^1 is the base the merge ref was actually computed against; when they differ the run now says so.

The subtle part

On a non-merge HEAD, HEAD^1 still resolves — to the previous commit in linear history. Scoping to that would lint one commit's files instead of the PR's: a false-narrow, and a green check over too few files is worse than a noisy sweep. So the merge shape is now asserted, not assumed via parent count, and anything else falls back to the sweep.

Verification

Three fixture repos, exercising the real logic:

Case Expected Result
Merge ref at depth 2 scope to base, exclude pre-existing debt PASS — scoped, debt.md excluded
Non-merge HEAD refuse to scope PASS — sweeps (1 parent)
Root commit refuse to scope PASS — sweeps (0 parents)

actionlint, yamllint (canonical config) and zizmor all clean. Local code-reviewer, adversarial-reviewer and codebase reviewer all PASS.

This PR cannot prove itself. github-workflows is public, so its own run scopes either way. The fix is only proven by a private repo logging scoping to files changed sincenightowlstudiollc/kebab-tax#1270 is the intended first check, after the standards-check-v1 tag moves.

Closes #170

https://claude.ai/code/session_017s2qrmkQaV54fbcMzB2KRm

…e-repo

Changed-files scoping has never worked on a private repo. "Resolve scope"
narrowed the run by fetching the PR base:

    git -C repo fetch --depth=1 origin "${BASE_SHA}"

but the checkout above sets persist-credentials: false, so no credential
remains. A public repo serves that fetch unauthenticated and scopes
correctly; a private repo cannot, and the step falls back to a whole-repo
sweep -- correctly and loudly, but on every PR, forever. Measured: kebab-tax
and financial-agent (private) fall back; amelia-boone, archive-resolver and
dotfiles (public) scope. Clean split on visibility, 9 repos affected.

Take the base from the local commit graph instead. For a pull_request event
HEAD is refs/pull/N/merge, whose first parent IS the base, so fetch-depth: 2
makes HEAD^1 the diff target -- no network call, no credential, and
persist-credentials: false stays as-is (it is deliberate hardening against
zizmor's artipacked, not an oversight to route around).

HEAD^1 is also strictly more accurate than the event payload's base.sha,
which goes stale when the base branch moves after the event fires. HEAD^1 is
the base the merge ref was actually computed against.

Assert the merge shape rather than assume it. On a NON-merge HEAD, HEAD^1
still resolves -- to the previous commit in linear history -- which would
scope the run to one commit's files instead of the PR's. That false-narrow is
worse than sweeping, because it is a green check over too few files. The
parent count is now checked explicitly and anything that is not a merge ref
falls back to the sweep.

Verified against three fixture repos: a real merge ref at depth 2 scopes to
the base and excludes pre-existing debt; a non-merge HEAD sweeps; a root
commit sweeps. actionlint, yamllint and zizmor clean.

Closes #170

Claude-Session: https://claude.ai/code/session_017s2qrmkQaV54fbcMzB2KRm
@twistedmelonman
twistedmelonman merged commit e09b2a3 into main Sep 18, 2026
3 checks passed
@twistedmelonman
twistedmelonman deleted the claude/fix-private-repo-scoping-dc0d321a branch September 18, 2026 01:45
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.

standards-check scoping never narrows on private repos: base fetch has no credentials

1 participant