Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,60 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
# `--new-from-merge-base` needs the base branch's history to find the
# common ancestor; a shallow clone has none of it.
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
cache: true # go-mod-tidy-repo resolves the whole graph; uncached it is slow and flaky
- name: Resolve the diff this change introduces
# Everything below runs against this range and nothing else — no
# `--all-files` anywhere in CI. A branch create or a force-push sends an
# all-zero `before`, and an unresolvable ref makes both pre-commit and
# golangci-lint report nothing while exiting 0, which is the silent pass
# this whole job exists to prevent. So: fall back to the merge base with
# main, then verify every ref actually resolves and fail loudly if not.
id: range
env:
BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
base="$BASE"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] \
|| ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
echo "No usable base ref ('${base:-empty}') — falling back to the merge base with main"
base="$(git merge-base origin/main "$HEAD_SHA")"
fi
git rev-parse --verify "${base}^{commit}" >/dev/null
git rev-parse --verify "${HEAD_SHA}^{commit}" >/dev/null
# The manual hook below diffs against origin/main; if that is missing
# it reports `0 issues` and exits 0 rather than erroring.
git rev-parse --verify origin/main >/dev/null
echo "base=$base" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "Linting ${base}..${HEAD_SHA}"
- name: Run pre-commit hooks
# Nothing is skipped here, unlike every other repo, which skips the
# HEAD-relative `golangci-lint`. Building a `language: golang` hook is
# this project's product, and the hook reporting nothing against a clean
# checkout is beside the point — that it *builds and runs* is the thing
# under test, and this is the one job that runs the action from the
# checkout. The Lint job still gates the change itself.
# checkout. The step below is what gates the change itself.
uses: ./
with:
extra_args: --from-ref ${{ steps.range.outputs.base }} --to-ref ${{ steps.range.outputs.head }}
- name: Lint what this change introduces
# Same config, same pin, same hook — asked the narrower question, and
# told not to fix (`--fix=false` in the hook's args): a linter that
# repairs its own findings exits 0 and reports nothing, which is a green
# check over unchanged, still-broken committed code.
uses: ./
with:
extra_args: --from-ref ${{ steps.range.outputs.base }} --to-ref ${{ steps.range.outputs.head }} --hook-stage manual golangci-lint-new

# Gating happens inside the jobs, never with a workflow-level `paths:`
# filter: a required check that never runs reports as *pending forever*
Expand Down
46 changes: 38 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,54 @@ repos:
rev: v2.12.2
hooks:
- id: golangci-lint-fmt

# The diff-scoped hook (`--new-from-rev HEAD`), deliberately: this runs on

# every commit and should report what you just wrote, not the

# repository's whole backlog. CI lints the change against its base.

# repository's whole backlog.
#

# Keep `rev` in lockstep with go.mod's tool pin.
- id: golangci-lint

# The same upstream hook, told which base to diff against and told not
# to fix. Both flags are appended to the entry, and both override it
# (measured):
#
# `--new-from-merge-base` beats the entry's `--new-from-rev HEAD`,
# which on a clean checkout compares the tree against an identical
# HEAD and so finds nothing.
#
# `--fix=false` beats the entry's `--fix`. This one is not a
# preference. Measured on a committed misspelling: with `--fix`, the
# hook reported **Passed** while rewriting the runner's working copy,
# because a linter that repairs its own findings has nothing left to
# report. Green check, no output, committed code still wrong.
#
# And pre-commit's usual safety net does not cover it. A fixing hook
# normally fails with "files were modified by this hook" — that is why
# end-of-file-fixer and friends stay honest in CI. But that check works
# from the filenames pre-commit handed the hook, and this one is
# `pass_filenames: false`, so there are none to compare. Measured both
# ways: end-of-file-fixer failed on a file it fixed; golangci-lint
# passed on one it fixed. That is what makes this hook the exception.
#
# Fixing belongs to the hook above, which runs against your working
# tree where the repair is the point. CI only reports.
#
# `stages: [manual]` keeps it out of every local commit. CI asks for
# this one by name.
- id: golangci-lint
alias: golangci-lint-new
name: golangci-lint (issues this change introduces)
stages: [manual]
args: [--new-from-merge-base=origin/main, --fix=false]

# Secret scanning
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
exclude: ^build/

# Note: the whole-repo golangci-lint run stays in CI — the hook above only
# lints what changed since HEAD, which is fast enough for every commit.
# Note: CI runs the `golangci-lint-new` alias above at the manual stage, so a
# pull request is gated on the issues it introduces. Nothing runs the whole
# repository; whole-module linters like `unused` are reported only for code
# the change touched.
Loading