diff --git a/.github/actions/ci-toolchain/action.yml b/.github/actions/ci-toolchain/action.yml new file mode 100644 index 0000000000..351b1710dd --- /dev/null +++ b/.github/actions/ci-toolchain/action.yml @@ -0,0 +1,48 @@ +name: 'Set up CI toolchain' +description: > + Puts the tools `.tool-versions` pins (Terraform, OpenTofu, Packer, Helm, + Helmfile, ...) on PATH the way a developer does: `atmos toolchain install`, + which reads `.tool-versions` and skips every tool already on disk, then + `atmos toolchain env` to export their directories to PATH. Versions live in + `.tool-versions` only. + + Persistence across jobs and runs is atmos's own `ci.cache` (the Atmos Cache + action, ./actions/cache): the one producer job per OS saves the toolchain + directory at job end (`cache: restore-and-save`); every consumer restores it + and never saves (`cache: restore-only`), so ten shards do not race to write one key. + On a miss, `atmos toolchain install` installs from the network as it always + did. + + Requires the `atmos` binary to be on PATH (install it before this step). + +inputs: + github-token: + description: 'GitHub token for the toolchain registry and release-asset lookups (raises the unauthenticated API rate limit).' + required: true + cache: + description: > + Passed through to the Atmos Cache action's `mode`: `restore-and-save` + (the one producer job per OS - restore, and save at job end), + `restore-only` (consumers, the default), or `none` to skip the cache. + required: false + default: 'restore-only' + +runs: + using: composite + steps: + - name: Restore the Atmos toolchain cache + if: inputs.cache != 'none' + continue-on-error: true + uses: ./actions/cache + with: + mode: ${{ inputs.cache }} + + - name: Install the tools in .tool-versions that are not already on disk + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + run: atmos toolchain install + + - name: Export the toolchain directories to PATH + shell: bash + run: atmos toolchain env --format=github diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1233ccbbf2..4f58182e18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -138,6 +138,7 @@ jobs: # other Windows/macOS legs in this file. allowed-endpoints: > api.github.com:443 + get.helm.sh:443 github.com:443 raw.githubusercontent.com:443 rekor.sigstore.dev:443 @@ -252,6 +253,24 @@ jobs: shell: bash run: go tool mage acceptance:verify "${{ matrix.target }}" "$TEST_SHARD_COUNT" + # Install the toolchain once per OS with the atmos just built (so the + # on-disk layout is the one the consumers' binary expects) and save it + # to atmos's ci.cache; the acceptance shards, terraform-registry-cache + # and mock jobs restore it (never save) and run the same + # `atmos toolchain install`, which then skips every tool. macos-intel is + # skipped: its only consumer (the k3s macOS job) installs no toolchain. + - name: Put the freshly built atmos on PATH for the toolchain install + if: ${{ matrix.target != 'macos-intel' && ! ( matrix.target == 'windows' && github.event.pull_request.draft ) }} + shell: bash + run: echo "${GITHUB_WORKSPACE}/build" >> "$GITHUB_PATH" + + - name: Install the CI toolchain (atmos toolchain install from .tool-versions; saves the cache) + if: ${{ matrix.target != 'macos-intel' && ! ( matrix.target == 'windows' && github.event.pull_request.draft ) }} + uses: ./.github/actions/ci-toolchain + with: + cache: restore-and-save + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload build artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: ${{ ! ( matrix.target == 'windows' && github.event.pull_request.draft ) }} @@ -425,22 +444,17 @@ jobs: path: ${{ github.workspace }} add-to-path: 'true' - - name: Cache Atmos toolchain - if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} - continue-on-error: true - uses: ./actions/cache - - - name: Install Terraform, OpenTofu, Packer, Helm, and Helmfile + # `atmos toolchain install` from .tool-versions, with the toolchain + # directory restored from atmos's own ci.cache (saved by the build job + # for this OS; consumers never save, so shards do not race for the key). + # Do not export ATMOS_XDG_CACHE_HOME or TF_PLUGIN_CACHE_DIR here: many + # tests assert XDG defaults and Terraform's plugin cache is not safe for + # shared concurrent use. + - name: Set up the CI toolchain if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - atmos toolchain install hashicorp/terraform - atmos toolchain install opentofu/opentofu - atmos toolchain install hashicorp/packer - atmos toolchain install helm/helm - atmos toolchain install helmfile/helmfile - atmos toolchain env --format=github + uses: ./.github/actions/ci-toolchain + with: + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Verify Terraform, OpenTofu, Packer, Helm, and Helmfile if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} @@ -662,12 +676,12 @@ jobs: path: ${{ github.workspace }} add-to-path: 'true' - # Dogfood `atmos ci cache`: cache the configured Atmos cache root via the - # recommended composite action. Do not export ATMOS_XDG_CACHE_HOME or - # TF_PLUGIN_CACHE_DIR for the full acceptance job: many tests assert XDG - # defaults and Terraform's plugin cache is not safe for shared concurrent - # use. This cache step restores/saves toolchain bits only; it must stay a - # pure accelerator. + # `atmos toolchain install` from .tool-versions, with the toolchain + # directory restored from atmos's own ci.cache (saved by the build job + # for this OS; consumers never save, so shards do not race for the key). + # Do not export ATMOS_XDG_CACHE_HOME or TF_PLUGIN_CACHE_DIR here: many + # tests assert XDG defaults and Terraform's plugin cache is not safe for + # shared concurrent use. # # Restore-only on the shards. The repo's Actions cache holds 18.9 GB in # 17 entries against a 10 GB LRU quota, every entry scoped to a @@ -679,24 +693,11 @@ jobs: # costing 43 s avg / 3 min max per shard for nothing. The # terraform-registry-cache job keeps the plain (restore+save) step and is # the single writer of this key per OS. - - name: Cache Atmos toolchain + - name: Set up the CI toolchain if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} - continue-on-error: true - uses: ./actions/cache + uses: ./.github/actions/ci-toolchain with: - mode: restore-only - - - name: Install Terraform, OpenTofu, Packer, Helm, and Helmfile - if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - atmos toolchain install hashicorp/terraform - atmos toolchain install opentofu/opentofu - atmos toolchain install hashicorp/packer - atmos toolchain install helm/helm - atmos toolchain install helmfile/helmfile - atmos toolchain env --format=github + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Verify Terraform, OpenTofu, Packer, Helm, and Helmfile if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} @@ -1762,6 +1763,10 @@ jobs: ocsp.usertrust.com:80 ocsp.digicert.com:80 *.pool.ntp.org:123 + get.helm.sh:443 + proxy.golang.org:443 + sum.golang.org:443 + google.golang.org:443 - name: Check out code into the Go module directory if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} @@ -1787,14 +1792,11 @@ jobs: path: ${{ github.workspace }} add-to-path: 'true' - - name: Install Terraform and OpenTofu with Atmos toolchain + - name: Set up the CI toolchain if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - atmos toolchain install hashicorp/terraform - atmos toolchain install opentofu/opentofu - atmos toolchain env --format=github + uses: ./.github/actions/ci-toolchain + with: + github-token: ${{ secrets.GITHUB_TOKEN }} - name: Verify OpenTofu if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }} diff --git a/docs/fixes/2026-09-04-ci-toolchain-one-step.md b/docs/fixes/2026-09-04-ci-toolchain-one-step.md new file mode 100644 index 0000000000..eb04109ce3 --- /dev/null +++ b/docs/fixes/2026-09-04-ci-toolchain-one-step.md @@ -0,0 +1,56 @@ +# Fix: the CI toolchain is one step - `atmos toolchain install` from `.tool-versions` + +**Date:** 2026-09-04 (supersedes the 2026-09-03 "ship the toolchain as a build artifact" design) + +## Summary + +Every acceptance shard, the `terraform-registry-cache` legs and the `mock` jobs need the external +tools the repository pins in `.tool-versions` (Terraform, OpenTofu, Packer, Helm, Helmfile, ...). +Two things had grown around that over time: + +1. **Versions duplicated in the workflow.** `test.yml` carried `OPEN_TOFU_VERSION`, + `PACKER_VERSION`, `HELM_VERSION`, `HELMFILE_VERSION` next to the pins in `.tool-versions`, and the + two drifted (a stale `OPEN_TOFU_VERSION` of 1.12.2 behind the file's 1.12.5 went undetected; + #3022 removed the env vars). The first version of this PR still fed those variables into its + action, so after merging main it would have written empty pins. +2. **Hand-rolled caching.** Because the "Cache Atmos toolchain" step never hit (the ~5 GB of Go caches + written per run churned the 10 GB Actions cache before a toolchain entry survived), the first + version of this PR built its own mechanism: the build job tarred the installed tree with + `cygpath`-aware shell, uploaded it as a `toolchain-` artifact, and every consumer downloaded and + unpacked it before installing. ~150 lines of shell doing what atmos already does. + +## Fix + +`.github/actions/ci-toolchain` is now exactly what a developer runs, plus the cache atmos already +knows how to describe: + +1. `./actions/cache` (the Atmos Cache action, driven by `ci.cache` in `atmos.yaml`): `cache: save` on + the one producer job per OS (the `build` job, with the atmos it just built), `restore-only` (the + default) on every consumer, so ten shards restore one key and none of them races to write it + (`restore-only` comes from #3038, merged into this branch). +2. `atmos toolchain install`: reads `.tool-versions`, installs what is not on disk, skips the rest. + This is the from-file form, the only one that takes the "already installed" path; the per-tool + `--default owner/repo@version` form always re-resolves and re-verifies. +3. `atmos toolchain env --format=github`: exports the tool directories to `PATH`. + +Every job that needs the toolchain is one step: `uses: ./.github/actions/ci-toolchain` with the +token (and `cache: save` on the build job). Versions are pinned in `.tool-versions` only. No tar, no +artifact, no `cygpath`, no per-job tool list. + +Why atmos's cache is enough now: the repository's Actions cache limit was raised from 10 GB to 50 GB +(org setting, 2026-09-04) after measuring that main's entries never survived a run at 10 GB, and +`restore-only` on consumers removes the ten-way save race. A toolchain entry per OS is ~400-600 MB. + +## Validation + +- `actionlint .github/workflows/test.yml`, pre-commit hooks clean. +- The PR's own `Tests` run: the build job saves `atmos-toolchain---v2`; each shard's + `Set up the CI toolchain` step restores it and `atmos toolchain install` logs every tool as already + installed. + +## Follow-ups + +- `HELM_DIFF_VERSION` (a Helm plugin, not a toolchain tool) is still a workflow env var; the + `helm plugin install` step stays as is. +- Jobs that install a single tool inline (`atmos toolchain install opentofu/opentofu` in the floci, + kubernetes-e2e and container-step jobs) could use the same action; left as they are here.