diff --git a/.github/workflows/goCI.yml b/.github/workflows/goCI.yml index f15695a..e073060 100644 --- a/.github/workflows/goCI.yml +++ b/.github/workflows/goCI.yml @@ -40,6 +40,10 @@ on: required: false type: string default: "" + test-cache-key-suffix: + required: false + type: string + default: "" # set when a repository calls goCI more than once, so each test job keeps its own Go cache golangci-lint-version: required: false type: string @@ -196,6 +200,7 @@ jobs: if: inputs.run-test with: runs-on: ${{ inputs.test-runs-on || inputs.runs-on || 'ubuntu-latest' }} + cache-key-suffix: ${{ inputs.test-cache-key-suffix }} goprivate: ${{ inputs.goprivate }} only-latest-golang: ${{ inputs.only-latest-golang }} os-dependencies: ${{ inputs.test-os-dependencies || inputs.os-dependencies || '' }} diff --git a/.github/workflows/goTest.yml b/.github/workflows/goTest.yml index a216951..226f489 100644 --- a/.github/workflows/goTest.yml +++ b/.github/workflows/goTest.yml @@ -9,6 +9,10 @@ on: required: false type: string default: go.step.sm/,github.com/smallstep/ + cache-key-suffix: + required: false + type: string + default: "" # set when a repository runs more than one test job, so each keeps its own Go cache gotestsum-version: required: false type: string @@ -116,8 +120,51 @@ jobs: with: go-version: ${{ matrix.go }} check-latest: true - cache: true - cache-dependency-path: "**/go.sum" + # NOTE: setup-go's cache key is the same in every goCI job, so whichever + # job finishes first (usually govulncheck) saves its build cache and the + # test job's -race/coverage build output is never stored. The test job + # keeps its own cache below instead. + cache: false + + - name: Locate Go caches + id: go-env + shell: bash + env: + CACHE_KEY_SUFFIX: ${{ inputs.cache-key-suffix }} + run: | + set -euo pipefail + + case "${CACHE_KEY_SUFFIX}" in + *,* | *$'\n'*) + echo "::error::cache-key-suffix must not contain commas or newlines" + exit 1 + ;; + esac + + gocache="$(go env GOCACHE)" + gomodcache="$(go env GOMODCACHE)" + goversion="$(go env GOVERSION)" + + # The suffix comes first so that the restore-keys prefix of a job + # without one never matches another job's entries. ImageOS keeps cgo + # objects from outliving the system headers they were built against. + prefix="go-test-${CACHE_KEY_SUFFIX:+${CACHE_KEY_SUFFIX}-}${RUNNER_OS}-${ImageOS:-unknown}-${RUNNER_ARCH}-${goversion}-" + + { + echo "gocache=${gocache}" + echo "gomodcache=${gomodcache}" + echo "key-prefix=${prefix}" + } >> "${GITHUB_OUTPUT}" + + - name: Restore Go caches + id: go-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ${{ steps.go-env.outputs.gocache }} + ${{ steps.go-env.outputs.gomodcache }} + key: ${{ steps.go-env.outputs.key-prefix }}${{ hashFiles('**/go.sum', '**/go.work.sum') }} + restore-keys: ${{ steps.go-env.outputs.key-prefix }} - name: Setup SSH key for private dependencies uses: webfactory/ssh-agent@e83874834305fe9a4a2997156cb26c5de65a8555 # v0.10.0 @@ -181,6 +228,26 @@ jobs: GOTESTSUM_JSONFILE: gotestsum.json run: eval "${TEST_CMD}" + # Go keeps test results in GOCACHE too. Expire them before saving, so + # tests whose outcome depends on things Go can't track (Docker, the + # network, the clock) keep running for real instead of replaying a pass. + - name: Expire cached test results + if: steps.go-cache.outputs.cache-hit != 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + shell: bash + run: go clean -testcache + + # Only the default branch saves, so feature branches don't fill the + # repository's cache quota with near-duplicate entries; they restore the + # default branch's entry instead. + - name: Save Go caches + if: steps.go-cache.outputs.cache-hit != 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ${{ steps.go-env.outputs.gocache }} + ${{ steps.go-env.outputs.gomodcache }} + key: ${{ steps.go-cache.outputs.cache-primary-key }} + - name: Annotate Test Suite Results if: ${{ (success() || failure()) && hashFiles('gotestsum.json') != '' }} uses: guyarb/golang-test-annotations@96fc379b171c49932041d6c789e73331a7bdeec1 # v0.9.0 diff --git a/AGENTS.md b/AGENTS.md index a209b1e..4e152f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,9 +72,9 @@ jobs: secrets: inherit ``` -- **goCI.yml** - inputs: `run-lint`, `run-test`, `run-build`, `run-govulncheck`, `run-codeql` (default false), `run-codecov`, `only-latest-golang` (true: `stable` only; false: adds `oldstable`), `build-command` (`V=1 make build`), `test-command` (gotestsum with coverage), `golangci-lint-args` (`--timeout=30m`), `golangci-lint-version` / `gotestsum-version` / `govulncheck-version` (empty means take it from `.versions`), `goprivate`, `os-dependencies` and per-job `-os-dependencies`, `runs-on` and per-job `-runs-on`, `setup-bats`, `lint-skip-go-generate` (default false), `lint-skip-go-mod-tidy` (default true), `codeql-build-cmd`, `codeql-build-mode`, `codeql-make-bootstrap`. Secrets: `SSH_PRIVATE_KEY`, `PAT`, `CODECOV_TOKEN`. +- **goCI.yml** - inputs: `run-lint`, `run-test`, `run-build`, `run-govulncheck`, `run-codeql` (default false), `run-codecov`, `only-latest-golang` (true: `stable` only; false: adds `oldstable`), `build-command` (`V=1 make build`), `test-command` (gotestsum with coverage), `golangci-lint-args` (`--timeout=30m`), `golangci-lint-version` / `gotestsum-version` / `govulncheck-version` (empty means take it from `.versions`), `goprivate`, `os-dependencies` and per-job `-os-dependencies`, `runs-on` and per-job `-runs-on`, `test-cache-key-suffix` (set it when a repository calls goCI more than once, so each test job keeps its own Go cache), `setup-bats`, `lint-skip-go-generate` (default false), `lint-skip-go-mod-tidy` (default true), `codeql-build-cmd`, `codeql-build-mode`, `codeql-make-bootstrap`. Secrets: `SSH_PRIVATE_KEY`, `PAT`, `CODECOV_TOKEN`. - **goLint.yml** - if the caller has no `.golangci.*` file, it downloads this repo's `.golangci.yml` at the calling SHA and passes `--config`. With `skip-go-generate: false` (the default) it deletes every `// Code generated ... DO NOT EDIT.` file except `*.pb.go`, runs `go generate ./...`, and fails on any diff. `skip-go-mod-tidy: false` adds `go mod tidy -diff`. -- **goTest.yml** / **goBuild.yml** - same Go matrix logic; both `eval` the caller's command string. `goTest` sets `GOTESTSUM_JSONFILE` and annotates failures. +- **goTest.yml** / **goBuild.yml** - same Go matrix logic; both `eval` the caller's command string. `goTest` sets `GOTESTSUM_JSONFILE` and annotates failures. `goTest` turns off setup-go's cache (its key is shared by every goCI job, so the test job's build output was never the one saved) and keeps its own `GOCACHE`/`GOMODCACHE` cache, keyed on the optional `cache-key-suffix`, OS, `ImageOS`, arch, Go version and the `go.sum`/`go.work.sum` hash. Only the default branch saves it, after `go clean -testcache` so restored copies never replay test results; other branches restore it. - **actionci.yml** - inputs: `run-actionlint`, `run-zizmor`, `run-frizbee` (all default true), `zizmor-advanced-security` (string; empty auto-enables on public repos). The calling job must grant `actions: read` and `security-events: write` or the zizmor job fails at startup. - **code-scan.yml** - inputs: `run-codeql`, `runs-on`, `codeql-build-cmd`, `codeql-build-mode`, `codeql-os-dependencies`, `os-dependencies`. Secrets: `PAT`, `SSH_PRIVATE_KEY`. - **goreleaser.yml** - inputs: `go-version` (`stable`), `cosign-version`, `is-prerelease` (default true), `enable-gpg-sign`, `enable-packages-upload`, `os-dependencies`, `runs-on`, `goprivate`. Required secrets: `GORELEASER_PAT`, `GORELEASER_KEY`, `AWS_S3_REGION`; the rest are optional. Runs GoReleaser Pro `release --clean`.