Skip to content

fix(hybrid-gate): diff the docs-only changeset from the merge-base, not the base tip - #40

Merged
forkwright merged 1 commit into
mainfrom
fix/docs-only-merge-base
Aug 15, 2026
Merged

fix(hybrid-gate): diff the docs-only changeset from the merge-base, not the base tip#40
forkwright merged 1 commit into
mainfrom
fix/docs-only-merge-base

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Finding

hybrid-gate.yml computes its docs-only verdict with git diff origin/${BASE_REF}..HEAD — two dots.
Despite looking like range notation, git diff A..B is a plain comparison of two endpoints, so
against a moving base it reports every file the BASE changed since the branch forked, not the
changeset the PR proposes.

A PR touching only README.md is therefore classified not docs-only the moment main lands an
unrelated Rust commit — which on an active repo is more or less immediately.

Evidence

  • .github/workflows/hybrid-gate.yml — the pull_request arm builds range="origin/${BASE_REF}..HEAD"
    and the next line does changed=$(git diff --name-only "$range").

  • Reproduced in an isolated repo: branch feature changes only README.md; base then gains an
    unrelated lib.rs commit.

    form files reported
    main..feature (today) README.md, lib.rs
    main...feature (this PR) README.md

    lib.rs is a file the PR never touched. Two-dot attributes it to the PR anyway.

Why this matters

This is a recovered abandoned fix, not new work. The commit sat on fix/hybrid-gate-docs-only-diff-range
with no PR ever opened — one of three such branches in this repo found during a sweep. The reasoning
in its commit message was already correct; it simply never landed.

It matters more now than when it was written, because docs_only was exposed as a workflow_call
output in #35 and has since been recommended to an adopting repo as the way to skip expensive jobs.
A verdict that silently flips to false whenever the base moves makes that capability unreliable in
exactly the busy-repo case it exists for — and the failure is invisible: the expensive job simply
runs, which looks like normal behaviour rather than a misclassification.

The direction of the error is at least the safe one — it under-claims docs-only and never skips a
build it should have run.

Desired correction

Three-dot on the pull_request arm, so the diff is taken from the merge-base — the changeset the PR
actually proposes, and what the PR's own "Files changed" tab shows.

The push arms stay two-dot and that is deliberate: before..after is exactly what a push changed,
and there is no merge-base to speak of. The recovered commit already carried that distinction in its
WHY comment; it is preserved verbatim.

Done when: a PR touching only markdown is classified docs-only regardless of what has landed on the
base since it forked.

…ot the base tip

`git diff A..B` is not range notation — it is a plain comparison of two
endpoints, identical to `git diff A B`. The docs-only step fed it
`origin/${BASE_REF}..HEAD`, so against a base that keeps moving it reported
every file main changed since the branch forked, not the files the PR
proposes. A PR touching only README.md is classified NOT docs-only the moment
an unrelated .rs commit lands on main.

Measured on forkwright/kanon's `docs/2254-entry-point-truth`, which changes 2
files: the two-dot range yields 39 files, 37 of them non-doc. Three-dot yields
2. The exemption can therefore only ever fire on a branch that is exactly
up to date with its base, which is not a property any PR holds for long.

The consequence differs by gate shape. Here the PR falls through to
full-gate-build, so the cost is a wasted build. In a trailer-only gate with no
build fallback the same expression hard-blocks the PR: kanon's standalone
gate-attestation.yml carries the identical two-dot expression at line 52 and
exits 1 with "No Gate-Passed trailer found" for a docs-only PR that its own
exemption was written to let through.

Only the pull_request arm changes. The push arms are correct as two-dot:
before..after is precisely what that push changed, and all three were
re-checked against a fixture (event.before present, all-zeros, and absent) and
resolve unchanged.

Verified by extracting this workflow's own range-selection block and running it
against a fixture PR whose branch touches only README.md while the base moved
ahead with a code change:

  pre-fix  (origin/main):  range=origin/main..HEAD   files=[README.md code.rs]  docs_only=false  rc=1
  post-fix (this branch):  range=origin/main...HEAD  files=[README.md]          docs_only=true   rc=0

The permanent assertion belongs in scripts/check_event_shape_guards.py, which
scans for pull_request-only values reaching git and would cover this class
directly. That script is still unmerged on fix/gate-push-event-trailer-sha, and
wiring it from here would collide with that branch in actionlint.yml, so this
change is scoped to the defect.

Follows up #25, which introduced the event-derived range.
@forkwright
forkwright merged commit df92942 into main Aug 15, 2026
1 check passed
@forkwright
forkwright deleted the fix/docs-only-merge-base branch August 15, 2026 16:47
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.

1 participant