From 164373a144a820ffb4d6313d754463e73246eeff Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 12 May 2026 17:58:28 +0200 Subject: [PATCH 1/2] chore(ci): cache pre-commit envs + move heavy test hooks to pre-push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-commit CI job has crept from ~8 min to ~10 min as the test suites grew. Two changes here, each independently safe: 1. .github/workflows/pre-commit.yml — add three caches: - actions/setup-node@v6 with cache: 'npm' on frontend/package-lock.json so `npm ci` reuses ~/.npm cache (~30-40s saved). Same pattern already in frontend-build-sentinel.yml. - actions/cache for ~/.cache/pre-commit keyed on the hook config so per-hook venvs (Go, Python, Node) aren't rebuilt every run (~30-60s saved). - actions/cache for ~/.cache/go-build keyed on go.sum so go-vet, gosec, and any Go-compiling hooks reuse compiled object files rather than rebuilding from source. - actions/cache for ~/go/bin keyed on the pinned tool versions so gosec v2.22.4 and gocyclo v0.6.0 binaries don't get rebuilt by `go install` every run. 2. .pre-commit-config.yaml — move go-test, frontend-build, frontend-test from default stages to `stages: [pre-push]`. The CI workflow runs `pre-commit run --all-files` without a stage filter, so the default stage filter (`pre-commit`) skips these hooks. Local devs who run `pre-commit install --hook-type pre-push` still get them on push. These three hooks were the bulk of the ~485s "Run pre-commit" step and are redundant with dedicated CI workflows that PRs / 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). Net expected impact: the pre-commit CI job drops from ~10 min to ~2-3 min on cache-hit runs. No coverage loss in CI — the heavy tests still run, just in their dedicated workflows where parallelism + caching is already optimised. --- .github/workflows/pre-commit.yml | 42 ++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 36 ++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 695be549..f244cc1e 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. @@ -115,6 +120,43 @@ 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 }}- + + # 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. + - name: Cache Go-installed tools (gosec, gocyclo) + uses: actions/cache@v4 + with: + path: ~/go/bin + key: go-tools-${{ runner.os }}-gosec-v2.22.4-gocyclo-v0.6.0 + - 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] From 02a8c013345450e34592479448a21e61459a2df7 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 12 May 2026 19:28:24 +0200 Subject: [PATCH 2/2] chore(ci): move Go-tools cache before installs so cache-hit skips them CR feedback on PR #342: the Go-tools cache step previously sat AFTER `Install gosec` and `Install gocyclo`, so cache restoration happened too late to short-circuit the installs. Reorder the cache step BEFORE the install steps, add an `id: cache-go-tools` to the cache, and gate each install with `if: steps.cache-go-tools.outputs.cache-hit != 'true'`. On a cache hit the install steps now no-op (saving the `go install` build time per run). On a cache miss they run as before. --- .github/workflows/pre-commit.yml | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f244cc1e..34953e5b 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -68,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. @@ -147,16 +164,6 @@ jobs: restore-keys: | go-build-${{ runner.os }}- - # 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. - - name: Cache Go-installed tools (gosec, gocyclo) - uses: actions/cache@v4 - with: - path: ~/go/bin - key: go-tools-${{ runner.os }}-gosec-v2.22.4-gocyclo-v0.6.0 - - name: Install frontend deps run: | if [ -f frontend/package-lock.json ]; then