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
54 changes: 51 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,64 @@ 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: pre-commit is the linter. `golangci-lint` reports
# what the change introduced, which is the whole point of running it
# here rather than in a job of its own.
# Scoped to this change's diff, never `--all-files`: CI's job is to
# report what this change did, not to replay the repository's backlog.
#
# `golangci-lint` is skipped because it is `--new-from-rev HEAD`, which
# compares the working tree against HEAD and so reports nothing at all on
# a clean checkout. Diff-scoping this run cannot rescue it — the hook is
# `pass_filenames: false`, so it never sees the file list. The step below
# asks the same hook the question it can answer on a runner.
env:
SKIP: golangci-lint
uses: blairham/go-pre-commit@v4.6.6
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: blairham/go-pre-commit@v4.6.6
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
42 changes: 39 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,51 @@ repos:
- 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]

- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
exclude: ^dist/

# 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