From 1d5d9ccaec72ff1e2d0cfc4a8ab9428d877cd397 Mon Sep 17 00:00:00 2001 From: Blake Gentry Date: Sun, 1 Nov 2026 15:55:29 -0600 Subject: [PATCH] keep Go CI build caches warm Go test, lint, and release jobs share an immutable cache keyed by dependencies. A smaller workload can populate it without race or cross-compilation artifacts, leaving later jobs to rebuild without saving their results. Shared cached test results can also bypass the selected Postgres version. Separate shared module downloads from build archives scoped by runner, Go version, workload, database version, and race mode. Let one full-suite job populate the module archive and refresh build archives when Go, SQL, CI, or embedded asset inputs change. Restore compatible earlier builds so changed jobs reuse previously compiled dependencies. Include local composite actions in dependency update scans. --- .github/actions/setup-go-ci/action.yml | 110 +++++++++++++++++++++++++ .github/dependabot.yml | 4 +- .github/workflows/ci.yaml | 18 ++-- 3 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 .github/actions/setup-go-ci/action.yml diff --git a/.github/actions/setup-go-ci/action.yml b/.github/actions/setup-go-ci/action.yml new file mode 100644 index 00000000..53c9fe18 --- /dev/null +++ b/.github/actions/setup-go-ci/action.yml @@ -0,0 +1,110 @@ +# Share module downloads, but keep each workload's compiled code and test results +# in a cache that can refresh after source changes. Dependency-only setup-go +# caches are immutable, so a lint or release job can otherwise freeze an archive +# without the race builds needed by tests. +name: Set up Go for CI +description: Install Go with shared module downloads and workload-specific build caches + +# Composite action inputs are strings. Quote literal boolean-like flags below. +inputs: + build-cache-scope: + description: >- + Workload identifier; defaults to the workflow name and job ID. Test scopes + must include the database version and race mode to isolate test results. + required: false + build-cache-extra-hash: + description: Additional input hash for generated or embedded assets + required: false + check-latest: + description: 'String flag ("true" or "false"): check for the latest matching Go version' + default: "false" + required: false + go-version: + description: Go version or selector; takes precedence over go-version-file + required: false + go-version-file: + description: Repository-relative go.mod or go.work file + required: false + save-module-cache: + description: >- + String flag ("true" or "false"): allow a full-suite job to save module + downloads. Other workloads only restore, so they cannot seed a partial cache. + default: "false" + required: false + +runs: + using: composite + steps: + - name: Install Go + id: go + uses: actions/setup-go@v7 + with: + cache: false + check-latest: ${{ inputs.check-latest }} + go-version: ${{ inputs.go-version }} + go-version-file: ${{ inputs.go-version-file }} + + # Keep keys in outputs. Only paths use GITHUB_ENV because nested composite + # outputs are unavailable when cache post-job inputs are resolved. Invoke + # this action once per job so these job-wide path variables stay consistent. + - name: Prepare cache paths and keys + id: cache-config + shell: bash + env: + BUILD_CACHE_SCOPE: ${{ inputs.build-cache-scope || format('{0}-{1}', github.workflow, github.job) }} + DEPENDENCY_HASH: ${{ hashFiles('**/go.mod', '**/go.sum', 'go.work', 'go.work.sum') }} + EXTRA_INPUT_HASH: ${{ inputs.build-cache-extra-hash }} + GO_VERSION: ${{ steps.go.outputs.go-version }} + SOURCE_HASH: ${{ hashFiles('**/*.go', '**/*.sql', '**/testdata/**', 'public/**', 'Makefile', '.golangci.yaml', '.github/actions/setup-go-ci/**', '.github/workflows/*') }} + run: | + module_cache_prefix="go-mod-v1-${RUNNER_OS}-${RUNNER_ARCH}" + build_cache_prefix="go-build-v1-${RUNNER_OS}-${RUNNER_ARCH}-${ImageOS:-$RUNNER_OS}-${GO_VERSION}-${BUILD_CACHE_SCOPE}" + + { + echo "RIVERUI_GO_BUILD_CACHE=$(go env GOCACHE)" + echo "RIVERUI_GO_MOD_CACHE=$(go env GOMODCACHE)" + } >> "$GITHUB_ENV" + + { + echo "build-prefix=$build_cache_prefix" + echo "dependency-hash=$DEPENDENCY_HASH" + echo "module-prefix=$module_cache_prefix" + echo "source-hash=$SOURCE_HASH${EXTRA_INPUT_HASH:+-$EXTRA_INPUT_HASH}" + } >> "$GITHUB_OUTPUT" + + # Downloads are independent of the Go version and workload. Hash every + # module's manifests and checksums, plus workspace metadata, to refresh the + # immutable archive when version selection changes. Restore older downloads + # as a fallback; Go verifies checksums and fetches missing versions normally. + # Only a full-suite job saves, after downloading the complete module graph. + - name: Restore and save module cache + if: inputs.save-module-cache == 'true' + uses: actions/cache@v6 + with: + path: ${{ env.RIVERUI_GO_MOD_CACHE }} + key: ${{ steps.cache-config.outputs.module-prefix }}-${{ steps.cache-config.outputs.dependency-hash }} + restore-keys: | + ${{ steps.cache-config.outputs.module-prefix }}- + + - name: Restore module cache + if: inputs.save-module-cache != 'true' + uses: actions/cache/restore@v6 + with: + path: ${{ env.RIVERUI_GO_MOD_CACHE }} + key: ${{ steps.cache-config.outputs.module-prefix }}-${{ steps.cache-config.outputs.dependency-hash }} + restore-keys: | + ${{ steps.cache-config.outputs.module-prefix }}- + + # Go also caches successful test results and cannot detect changes to an + # external database. Keep the exact toolchain, workload, database version, + # and race mode in every restore prefix, including dependency fallbacks. + # Source hashes let changed jobs save their newly compiled artifacts rather + # than repeatedly rebuilding against an immutable dependency-only archive. + - name: Restore and save build cache + uses: actions/cache@v6 + with: + path: ${{ env.RIVERUI_GO_BUILD_CACHE }} + key: ${{ steps.cache-config.outputs.build-prefix }}-${{ steps.cache-config.outputs.dependency-hash }}-${{ steps.cache-config.outputs.source-hash }} + restore-keys: | + ${{ steps.cache-config.outputs.build-prefix }}-${{ steps.cache-config.outputs.dependency-hash }}- + ${{ steps.cache-config.outputs.build-prefix }}- diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 75242992..7ec4957f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -104,7 +104,9 @@ updates: - "@tanstack/*" - package-ecosystem: "github-actions" - directory: "/" + directories: + - "/" + - "/.github/actions/**" schedule: interval: "monthly" cooldown: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 76ceb429..dac4149a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,9 +43,13 @@ jobs: uses: actions/checkout@v7.0.1 - name: Setup Go ${{ matrix.go-version }} - uses: actions/setup-go@v7 + uses: ./.github/actions/setup-go-ci with: + build-cache-scope: postgres-${{ matrix.postgres-version }}-race go-version: ${{ matrix.go-version }} + # One full-suite job seeds the shared downloads; lint and release + # workloads must not freeze an archive with only their dependencies. + save-module-cache: ${{ matrix.go-version == '1.27' && matrix.postgres-version == 18 && 'true' || 'false' }} - name: Display Go version run: go version @@ -101,9 +105,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v7.0.1 - - uses: actions/setup-go@v7 + - name: Set up Go for lint + uses: ./.github/actions/setup-go-ci with: - check-latest: true + check-latest: "true" go-version-file: "./go.mod" # ensure that there is a file in `dist` to prevent a lint error about @@ -240,9 +245,12 @@ jobs: uses: actions/checkout@v7.0.1 - name: Setup Go - uses: actions/setup-go@v7 + uses: ./.github/actions/setup-go-ci with: - check-latest: true + # The binaries embed the production frontend. Refresh their build + # archive when its sources or build configuration change as well. + build-cache-extra-hash: ${{ hashFiles('src/**', 'package.json', 'package-lock.json', '*.config.*', 'tsconfig*.json', '.npmrc') }} + check-latest: "true" go-version-file: "go.mod" - name: Install Node.js