diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 695be549..34953e5b 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -32,6 +32,11 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" + # Cache the npm download cache keyed on the frontend lockfile. + # Saves ~30-40s per run vs an uncached `npm ci`. Same pattern + # already used in frontend-build-sentinel.yml. + cache: "npm" + cache-dependency-path: frontend/package-lock.json - name: Set up Terraform # Required by the terraform_fmt + terraform_validate pre-commit hooks. @@ -63,13 +68,30 @@ jobs: "https://raw.githubusercontent.com/terraform-linters/tflint/${TFLINT_VERSION}/install_linux.sh" bash /tmp/tflint-install.sh + # Cache the installed tool binaries (gosec, gocyclo). Keyed on the + # pinned version strings so a tool-version bump still triggers a + # fresh install. Both binaries land in ~/go/bin which setup-go@v6 + # already adds to PATH. Restored BEFORE the install steps so the + # `if: cache-hit != 'true'` guards below can short-circuit them on + # cache-hit runs (the `go install` invocations cost ~3-5s each + # even when the module cache is warm; skipping them on cache-hit + # is worth the extra `if`). + - name: Cache Go-installed tools (gosec, gocyclo) + id: cache-go-tools + uses: actions/cache@v4 + with: + path: ~/go/bin + key: go-tools-${{ runner.os }}-gosec-v2.22.4-gocyclo-v0.6.0 + - name: Install gosec + if: steps.cache-go-tools.outputs.cache-hit != 'true' # Pinned to the same version ci.yml's `securego/gosec` Action uses, # so an upstream gosec release with rule changes can't silently # downgrade the gate between the two workflows. run: go install github.com/securego/gosec/v2/cmd/gosec@v2.22.4 - name: Install gocyclo + if: steps.cache-go-tools.outputs.cache-hit != 'true' # Pinned to match ci.yml — security tool installs must not use # @latest; that's exactly the supply-chain weakness this PR is # closing for Dockerfile FROMs. @@ -115,6 +137,33 @@ jobs: - name: Install pre-commit run: pip install 'pre-commit==4.0.1' + # Cache pre-commit's per-hook environments (Go, Python, Node, etc. + # virtualenvs it builds on first run). Keyed on the hook config + # because pre-commit will rebuild any env whose pinned rev changes. + # Saves ~30-60s per cache-hit run; safe because pre-commit verifies + # env integrity on use. + - name: Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + pre-commit-${{ runner.os }}- + + # Cache the Go build cache so `go vet`, `gosec`, and any other + # Go-compiling hooks reuse compiled object files instead of + # rebuilding from source. setup-go@v6 caches ~/go/pkg/mod + # (modules) but NOT ~/.cache/go-build (compiled output) — this + # step covers the latter. Keyed on go.sum so a dep upgrade still + # invalidates the cache and gets clean builds. + - name: Cache Go build cache + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: go-build-${{ runner.os }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-build-${{ runner.os }}- + - name: Install frontend deps run: | if [ -f frontend/package-lock.json ]; then diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b2363f98..58eab491 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -152,29 +152,57 @@ repos: pass_filenames: false files: ^internal/database/postgres/migrations/ - # Test execution + # Heavy test execution: pre-push stage only. + # + # These three hooks rebuild + run the full Go and frontend test suites, + # which is ~6-7 min of work and the bulk of the CI pre-commit job's + # runtime. They are *redundant in CI* — the same suites are run by + # dedicated workflows that PRs and pushes already trigger: + # + # - go-test (-short -race ./...) : ci.yml `unit-tests` runs the same + # suite with -race AND an integration + # pass with -tags=integration. + # - frontend-build (npm run build): frontend-build.yml runs npm run + # typecheck + npm run build on PRs; + # frontend-build-sentinel.yml runs + # the build on every push to main / + # feat/**. + # - frontend-test (jest) : frontend-build-sentinel.yml runs + # `npx jest --no-coverage --silent` + # on every push to feat/** (which + # fires on every PR-branch update). + # + # Moving them to the pre-push stage keeps the local safety net (devs + # who run `pre-commit install --hook-type pre-push` still get these + # tests on `git push`) while letting the CI pre-commit workflow stay + # focused on style/security/syntax. Pre-commit's default stage filter + # is `pre-commit`, so the CI workflow's `pre-commit run --all-files` + # skips these hooks automatically. - repo: local hooks: - id: go-test - name: Run Go tests + name: Run Go tests (pre-push only; CI covers via ci.yml) entry: bash -c 'go test -short -race ./...' language: system pass_filenames: false files: \.go$ + stages: [pre-push] - id: frontend-build - name: Build frontend + name: Build frontend (pre-push only; CI covers via frontend-build.yml) entry: bash -c 'cd frontend && npm run build' language: system pass_filenames: false files: ^frontend/src/ + stages: [pre-push] - id: frontend-test - name: Run frontend tests + name: Run frontend tests (pre-push only; CI covers via frontend-build-sentinel.yml) entry: bash -c 'cd frontend && npx jest --no-coverage --silent' language: system pass_filenames: false files: ^frontend/src/ + stages: [pre-push] # Global configuration default_stages: [pre-commit, pre-push]