From 738afb451ddd0f1dc6b5eb7e236caf497039a43f Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Wed, 16 Sep 2026 04:54:45 +0300 Subject: [PATCH 1/3] ci: cache Galaxy collections + pip, bound every job (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make deps` resolved against galaxy.ansible.com on every run of `ansible-lint`, `galaxy-build` and `molecule`, uncached and without retry. The galaxy-build job on #50 died on a transient `Connection reset by peer` while resolving the transitive community.crypto and passed unchanged on re-run. Cache `ansible/collections` across all three jobs under one shared key. This is not just a speed-up: given requirements already satisfied on disk, `ansible-galaxy collection install` prints "Nothing to do" and never contacts a server — verified by pointing it at an unreachable Galaxy URL — so a hit is immune to that error class rather than merely faster than it. - Split `restore`/`save` rather than the all-in-one action: the failure being defended against is Galaxy dying part way through an install, and the combined action's post step would persist that half-written tree. `save` is gated on success. - No `restore-keys`: an older cache still satisfies the `>=` ranges in requirements.yml, which would make a `GALAXY_CACHE_EPOCH` bump a silent no-op. - `GALAXY_CACHE_EPOCH` is the manual lever to force a re-resolve, since those `>=` ranges mean a warm cache otherwise freezes the resolved set until requirements.yml changes. Declared in both workflows; they must match. Cache pip via setup-python, keyed on the workflow file — no pip manifest exists and the workflow is where the package list lives. The installs stay unpinned, so this saves the download, not the PyPI round trip; PyPI is not the flaky dependency here. Add `timeout-minutes` to every job, sized off observed runtimes (molecule 45, the ci.yml jobs 5-20). Without one a hung job burns the 360-minute default, and with `cancel-in-progress` some branch-protection setups read the resulting cancelled check as "not failed". Also add each workflow to its own path filter, so a change to this cache wiring is exercised by the PR that makes it — the helm filter already claimed ci.yml for the same reason. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 75 ++++++++++++++++++++++++++++++++++ .github/workflows/molecule.yml | 42 +++++++++++++++++-- CONTRIBUTING.md | 19 ++++++++- 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b08a194..444c130 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,16 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Bumping GALAXY_CACHE_EPOCH forces CI to re-resolve the Galaxy collections from +# scratch. ansible/requirements.yml carries `>=` ranges rather than pins, so a warm +# cache otherwise freezes the resolved set — transitive collections like +# community.crypto included — until that file itself changes, and CI quietly drifts +# from whatever a local `make lint-ansible` resolves. Dependabot does not cover +# Galaxy (see .github/dependabot.yml), so this is the same manual lever as the +# collection versions themselves. Declared in molecule.yml too: bump BOTH. +env: + GALAXY_CACHE_EPOCH: '1' + # Third-party actions are pinned to a full commit SHA (a re-pointed tag can ship # malicious code — cf. the March 2026 KICS action compromise). The trailing # comment records the human-readable version; .github/dependabot.yml bumps them. @@ -21,6 +31,7 @@ jobs: # Detect whether ansible/ or the Helm chart changed so heavy jobs skip unrelated PRs. changes: runs-on: ubuntu-latest + timeout-minutes: 5 # ~5s in practice permissions: contents: read pull-requests: read @@ -33,8 +44,11 @@ jobs: id: filter with: filters: | + # ci.yml is listed because the ansible jobs' cache wiring lives here, so a + # change to it must re-run them — same reason the helm filter claims it. ansible: - 'ansible/**' + - '.github/workflows/ci.yml' # The chart shares the schema-key inventory and checker with molecule. helm: - 'charts/**' @@ -48,18 +62,60 @@ jobs: needs: changes if: needs.changes.outputs.ansible == 'true' runs-on: ubuntu-latest + timeout-minutes: 20 # ~1m15s in practice defaults: run: working-directory: ansible steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # `cache: pip` needs a file to key on and this repo ships no pip manifest, so + # the workflow is the dependency declaration — it is where the package list + # literally lives. The installs stay unpinned `--upgrade`, so pip still queries + # the PyPI index to resolve; the cache saves the download, not the round trip. + # That is the right trade: PyPI is not the flaky dependency here, Galaxy is + # (see the collections cache below). setup-python derives its own key and takes + # no custom suffix, so this job and galaxy-build share one pip cache; their + # package sets overlap heavily (ansible-core, ansible-lint) so whichever + # populates it first covers most of both. To split them, drop `cache:` and use + # actions/cache on ~/.cache/pip with explicit per-job keys. - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' + cache: pip + cache-dependency-path: .github/workflows/ci.yml - name: Install Ansible tooling run: python -m pip install --upgrade ansible ansible-lint yamllint + # A warm collections tree removes the Galaxy round trip outright: with the + # requirements already satisfied on disk, `ansible-galaxy collection install` + # prints "Nothing to do" and never contacts a server. So a hit is IMMUNE to the + # transient `Connection reset by peer` that failed #50 mid-resolve on the + # transitive community.crypto — not merely faster than it. + # + # restore/save are split rather than the all-in-one action on purpose: the + # failure being defended against is Galaxy dying part way through an install, + # and the combined action's post step would persist that half-written tree. + # Saving only after `make deps` returns 0 keeps a poisoned cache out. + # + # No restore-keys — an older cache still satisfies the `>=` ranges, so `make + # deps` would find nothing to do and a GALAXY_CACHE_EPOCH bump would silently + # be a no-op. + # + # `path:` and `hashFiles()` resolve against the workspace root, NOT this job's + # `working-directory: ansible`. The key is shared with galaxy-build and with + # molecule.yml; when they miss together they race to save it and the losers log + # a harmless "Cache already exists". + - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + id: galaxy-cache + with: + path: ansible/collections + key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} - name: Install Galaxy collections run: make deps # must precede lint/syntax-check + - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + if: steps.galaxy-cache.outputs.cache-hit != 'true' + with: + path: ansible/collections + key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} - name: Lint (yamllint + ansible-lint) run: make lint - name: Syntax-check playbook @@ -75,22 +131,38 @@ jobs: needs: changes if: needs.changes.outputs.ansible == 'true' runs-on: ubuntu-latest + timeout-minutes: 20 # ~1m45s in practice defaults: run: working-directory: ansible steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # Shares the pip cache with ansible-lint — see that job for why the key is + # the workflow file. - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' + cache: pip + cache-dependency-path: .github/workflows/ci.yml - name: Install build + import tooling run: python -m pip install --upgrade ansible-core ansible-lint galaxy-importer + # Same cache as ansible-lint (same key, same rationale — see that job). + - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + id: galaxy-cache + with: + path: ansible/collections + key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} - name: Vendor collection dependencies # So galaxy-importer's embedded ansible-lint can resolve the roles' FQCNs # (devsec.hardening, ansible.posix). Installed under ansible/collections. run: make deps env: ANSIBLE_COLLECTIONS_PATH: collections + - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + if: steps.galaxy-cache.outputs.cache-hit != 'true' + with: + path: ansible/collections + key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} - name: Build + validate the decdn.node collection run: make galaxy-check env: @@ -110,6 +182,7 @@ jobs: needs: changes if: needs.changes.outputs.helm == 'true' runs-on: ubuntu-latest + timeout-minutes: 20 # pulls the kubeconform image; ~1m in practice steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Helm version is pinned here AND in the kics job below; bump both. @@ -146,6 +219,7 @@ jobs: needs: changes if: needs.changes.outputs.ansible == 'true' || needs.changes.outputs.helm == 'true' runs-on: ubuntu-latest + timeout-minutes: 20 # pulls the KICS engine image; ~30s in practice steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 @@ -198,6 +272,7 @@ jobs: # Lint the workflow files themselves. actionlint: runs-on: ubuntu-latest + timeout-minutes: 10 # ~15s in practice permissions: contents: read checks: write diff --git a/.github/workflows/molecule.yml b/.github/workflows/molecule.yml index 031f1ec..5bc17ea 100644 --- a/.github/workflows/molecule.yml +++ b/.github/workflows/molecule.yml @@ -4,12 +4,14 @@ name: Molecule # Containerised converge + idempotence + verify for the decdn_node role. # Heavy (privileged systemd Docker container) — scoped to ansible/ changes and # blocking. Mark it a required status check in branch protection once proven. +# This file is in `paths` alongside ansible/ so a change to the job itself (its cache +# wiring, JOBS, the timeout) is exercised by the PR that makes it. on: pull_request: - paths: ['ansible/**'] + paths: ['ansible/**', '.github/workflows/molecule.yml'] push: branches: [main] - paths: ['ansible/**'] + paths: ['ansible/**', '.github/workflows/molecule.yml'] permissions: contents: read @@ -18,21 +20,45 @@ concurrency: group: molecule-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Must match ci.yml's value — same cache key, so a mismatch would split the cache in +# two and half the bump would do nothing. ci.yml carries the full rationale. +env: + GALAXY_CACHE_EPOCH: '1' + jobs: molecule: runs-on: ubuntu-latest # Docker is preinstalled + # ~4m41s at JOBS=3. A bound, not a target: without one a wedged privileged + # systemd container burns the 360-minute default, and with cancel-in-progress + # above, some branch-protection setups read the resulting cancelled check as + # "not failed" rather than as a failure. + timeout-minutes: 45 defaults: run: working-directory: ansible steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + # Keyed on this workflow file: no pip manifest exists, and the workflow is + # where the package list lives. ci.yml's ansible-lint job has the full note. - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12' + cache: pip + cache-dependency-path: .github/workflows/molecule.yml - name: Install molecule + Ansible run: | python -m pip install --upgrade \ molecule "molecule-plugins[docker]" ansible ansible-lint docker + # Same key and same rationale as ci.yml's jobs (see ansible-lint there): a warm + # tree makes the install a no-op that never contacts galaxy.ansible.com, and the + # save is gated so a part-way Galaxy failure cannot poison the cache. It wraps + # `make molecule` because the deps install is owned by the Make target here, not + # by a step of its own. + - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + id: galaxy-cache + with: + path: ansible/collections + key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} - name: molecule test # `make molecule` runs every scenario under ansible/molecule/ and fans them out # in parallel; ansible/Makefile documents the set and the fail-loud guards. @@ -40,10 +66,20 @@ jobs: # There is deliberately no separate `make deps` step: the molecule targets take # `deps` as a prerequisite, so a standalone one would resolve and install the # Galaxy requirements a second time every run — a second chance to trip over a - # flaky galaxy.ansible.com, for no added coverage. + # flaky galaxy.ansible.com, for no added coverage. The cache above wraps that + # Make-owned invocation instead. # # JOBS is capped at 3 rather than the default (one job per scenario, currently # 6): every scenario is a privileged systemd container, and they share this # runner's cores and cgroup hierarchy. If this job turns flaky, drop to JOBS=1 # or swap in `make molecule-serial` — the latter also serialises the output. run: make molecule JOBS=3 + # A step `if:` without a status-check function implies success(), so this is + # skipped when anything above failed — including a genuinely failing scenario, + # which leaves the cache cold for that run. Conservative on purpose: it is the + # same guard that keeps a part-way Galaxy install out of the cache. + - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + if: steps.galaxy-cache.outputs.cache-hit != 'true' + with: + path: ansible/collections + key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a0c897..be9d6c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,18 @@ Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hoo role (privileged systemd Docker container; scoped to `ansible/**`). Run locally with `make molecule` (needs Docker) — it runs all six scenarios in parallel, so reach for `make molecule-serial` when you need to read a failure in order. +- **Every job is bounded** by `timeout-minutes`. The values are bounds sized off + observed runtimes, not targets — without one a hung job burns the 360-minute + default, and combined with `cancel-in-progress` some branch-protection setups read + the resulting *cancelled* check as "not failed" rather than as a failure. +- **Two caches.** `ansible/collections` is cached across `ansible-lint`, + `galaxy-build` and `molecule` under one shared key; a hit makes `make deps` a no-op + that never contacts `galaxy.ansible.com`, which is what keeps a transient Galaxy + error from failing an unrelated PR. pip is cached via `setup-python`, keyed on the + workflow file (the repo has no pip manifest, so the workflow *is* the package list); + the installs stay unpinned, so that saves the download but not the PyPI round trip. + Both use `actions/cache`'s split `restore`/`save`, with `save` gated on success so a + part-way Galaxy failure can't poison the cache. ## Supply-chain / pinning rules @@ -57,8 +69,13 @@ Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hoo - **Dependabot** (`.github/dependabot.yml`) bumps the other action SHAs weekly. - **Bump manually** (Dependabot can't): the `KICS_IMAGE` and `KUBECONFORM_IMAGE` digests in the `Makefile`, both `setup-helm` `version:` inputs in `ci.yml` (`helm` and `kics` - jobs), + jobs), `GALAXY_CACHE_EPOCH` in **both** `ci.yml` and `molecule.yml` (they must match — + it is one shared cache key), the collection versions in `ansible/requirements.yml`, and the pre-commit hook revs via `pre-commit autoupdate`. +- **Bump `GALAXY_CACHE_EPOCH` whenever you want CI to re-resolve the collections.** + `requirements.yml` uses `>=` ranges, so a warm cache pins the resolved set — + transitive collections like `community.crypto` included — until that file changes; + the epoch is the lever that forces a fresh resolve without editing requirements. ## Solidity From f4606e3a3f4381d88f80977cd216cdd3b471de7f Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Wed, 16 Sep 2026 05:03:24 +0300 Subject: [PATCH 2/3] ci: derive the Galaxy cache epoch from requirements.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review catch: GALAXY_CACHE_EPOCH was duplicated as an env var in ci.yml and molecule.yml, and nothing could catch them diverging — molecule.yml's `paths` do not include ci.yml, and ci.yml's `ansible` filter does not include molecule.yml, so editing the epoch in one file alone produces no failing check while silently splitting the shared cache key in two. A comment saying "bump BOTH" is documentation, not a guard. Fold the counter into ansible/requirements.yml, which the key already hashes. One file, one hash: divergence is now structurally impossible rather than merely discouraged. A bump also matches both workflows' `ansible/**` trigger, so it is exercised immediately instead of on the next unrelated PR. Also correct CONTRIBUTING.md: it claimed both caches use actions/cache's split restore/save. Only the Galaxy one does — pip uses setup-python's built-in cache and has no restore/save pair. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 24 +++++++----------------- .github/workflows/molecule.yml | 13 +++++-------- CONTRIBUTING.md | 21 ++++++++++++--------- ansible/requirements.yml | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 444c130..c28f320 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,16 +14,6 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# Bumping GALAXY_CACHE_EPOCH forces CI to re-resolve the Galaxy collections from -# scratch. ansible/requirements.yml carries `>=` ranges rather than pins, so a warm -# cache otherwise freezes the resolved set — transitive collections like -# community.crypto included — until that file itself changes, and CI quietly drifts -# from whatever a local `make lint-ansible` resolves. Dependabot does not cover -# Galaxy (see .github/dependabot.yml), so this is the same manual lever as the -# collection versions themselves. Declared in molecule.yml too: bump BOTH. -env: - GALAXY_CACHE_EPOCH: '1' - # Third-party actions are pinned to a full commit SHA (a re-pointed tag can ship # malicious code — cf. the March 2026 KICS action compromise). The trailing # comment records the human-readable version; .github/dependabot.yml bumps them. @@ -96,9 +86,9 @@ jobs: # and the combined action's post step would persist that half-written tree. # Saving only after `make deps` returns 0 keeps a poisoned cache out. # - # No restore-keys — an older cache still satisfies the `>=` ranges, so `make - # deps` would find nothing to do and a GALAXY_CACHE_EPOCH bump would silently - # be a no-op. + # No restore-keys — an older cache still satisfies the `>=` ranges in + # requirements.yml, so `make deps` would find nothing to do and the cache-epoch + # bump documented in that file would silently be a no-op. # # `path:` and `hashFiles()` resolve against the workspace root, NOT this job's # `working-directory: ansible`. The key is shared with galaxy-build and with @@ -108,14 +98,14 @@ jobs: id: galaxy-cache with: path: ansible/collections - key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} + key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} - name: Install Galaxy collections run: make deps # must precede lint/syntax-check - uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 if: steps.galaxy-cache.outputs.cache-hit != 'true' with: path: ansible/collections - key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} + key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} - name: Lint (yamllint + ansible-lint) run: make lint - name: Syntax-check playbook @@ -151,7 +141,7 @@ jobs: id: galaxy-cache with: path: ansible/collections - key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} + key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} - name: Vendor collection dependencies # So galaxy-importer's embedded ansible-lint can resolve the roles' FQCNs # (devsec.hardening, ansible.posix). Installed under ansible/collections. @@ -162,7 +152,7 @@ jobs: if: steps.galaxy-cache.outputs.cache-hit != 'true' with: path: ansible/collections - key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} + key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} - name: Build + validate the decdn.node collection run: make galaxy-check env: diff --git a/.github/workflows/molecule.yml b/.github/workflows/molecule.yml index 5bc17ea..471cf45 100644 --- a/.github/workflows/molecule.yml +++ b/.github/workflows/molecule.yml @@ -20,11 +20,6 @@ concurrency: group: molecule-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# Must match ci.yml's value — same cache key, so a mismatch would split the cache in -# two and half the bump would do nothing. ci.yml carries the full rationale. -env: - GALAXY_CACHE_EPOCH: '1' - jobs: molecule: runs-on: ubuntu-latest # Docker is preinstalled @@ -51,14 +46,16 @@ jobs: molecule "molecule-plugins[docker]" ansible ansible-lint docker # Same key and same rationale as ci.yml's jobs (see ansible-lint there): a warm # tree makes the install a no-op that never contacts galaxy.ansible.com, and the - # save is gated so a part-way Galaxy failure cannot poison the cache. It wraps + # save is gated so a part-way Galaxy failure cannot poison the cache. The key is + # derived wholly from ansible/requirements.yml, so it cannot drift from ci.yml's + # copy the way a duplicated env var would. It wraps # `make molecule` because the deps install is owned by the Make target here, not # by a step of its own. - uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 id: galaxy-cache with: path: ansible/collections - key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} + key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} - name: molecule test # `make molecule` runs every scenario under ansible/molecule/ and fans them out # in parallel; ansible/Makefile documents the set and the fail-loud guards. @@ -82,4 +79,4 @@ jobs: if: steps.galaxy-cache.outputs.cache-hit != 'true' with: path: ansible/collections - key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }} + key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be9d6c3..36cf6be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,9 @@ Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hoo error from failing an unrelated PR. pip is cached via `setup-python`, keyed on the workflow file (the repo has no pip manifest, so the workflow *is* the package list); the installs stay unpinned, so that saves the download but not the PyPI round trip. - Both use `actions/cache`'s split `restore`/`save`, with `save` gated on success so a - part-way Galaxy failure can't poison the cache. + Only the Galaxy cache is wired by hand — it uses `actions/cache`'s split + `restore`/`save` with `save` gated on success, so a part-way Galaxy failure can't + poison it. The pip cache is `setup-python`'s built-in one and manages itself. ## Supply-chain / pinning rules @@ -69,13 +70,15 @@ Run it on demand with `make lint-ansible`, or `pre-commit run ansible-lint --hoo - **Dependabot** (`.github/dependabot.yml`) bumps the other action SHAs weekly. - **Bump manually** (Dependabot can't): the `KICS_IMAGE` and `KUBECONFORM_IMAGE` digests in the `Makefile`, both `setup-helm` `version:` inputs in `ci.yml` (`helm` and `kics` - jobs), `GALAXY_CACHE_EPOCH` in **both** `ci.yml` and `molecule.yml` (they must match — - it is one shared cache key), the collection versions in `ansible/requirements.yml`, - and the pre-commit hook revs via `pre-commit autoupdate`. -- **Bump `GALAXY_CACHE_EPOCH` whenever you want CI to re-resolve the collections.** - `requirements.yml` uses `>=` ranges, so a warm cache pins the resolved set — - transitive collections like `community.crypto` included — until that file changes; - the epoch is the lever that forces a fresh resolve without editing requirements. + jobs), the collection versions in `ansible/requirements.yml`, and the pre-commit hook + revs via `pre-commit autoupdate`. +- **Bump the `cache-epoch:` counter in `ansible/requirements.yml` to make CI + re-resolve the collections.** Those are `>=` ranges, so a warm cache pins the + resolved set — transitive collections like `community.crypto` included — until the + file changes; the counter forces a fresh resolve without editing the requirements + themselves. It is a comment, but a load-bearing one: the cache key is that file's + hash. Keeping it *in* the hashed file is deliberate — an epoch duplicated across + both workflows could drift, since neither workflow runs on a change to the other. ## Solidity diff --git a/ansible/requirements.yml b/ansible/requirements.yml index 6ac1ba5..25efccb 100644 --- a/ansible/requirements.yml +++ b/ansible/requirements.yml @@ -1,6 +1,20 @@ --- # Galaxy collections this project composes. Install with `make deps` # (ansible-galaxy collection install -r requirements.yml -p collections). +# +# CI caches the resolved tree under a key derived from THIS FILE's hash +# (.github/workflows/{ci,molecule}.yml). The versions below are `>=` ranges, not +# pins, so a warm cache freezes whatever was resolved — transitive collections like +# community.crypto included — until this file changes. +# +# The counter below is the lever to force a fresh resolve without otherwise editing +# the requirements. It is LOAD-BEARING despite being a comment: bumping it changes +# this file's hash, and therefore the cache key. It lives here, rather than as an env +# var in each workflow, so the key has exactly one source — two copies could drift +# apart silently, since neither workflow runs on a change to the other. Editing it +# also matches both workflows' `ansible/**` trigger, so a bump is exercised at once. +# +# cache-epoch: 1 collections: - name: devsec.hardening # os_hardening + ssh_hardening (CIS-style baseline) version: ">=10.0.0" From 0fec3aa763d95990289a70533bb43291047ebf77 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Wed, 16 Sep 2026 05:11:59 +0300 Subject: [PATCH 3/3] ci: empty commit to exercise the warm Galaxy cache No-op. The two prior runs each started cold (the first had no cache, the second moved the key by editing requirements.yml). This run is the first with a populated cache, and is what actually demonstrates the claim the change rests on: `make deps` reporting "Nothing to do" with no galaxy.ansible.com request. Co-Authored-By: Claude Opus 5 (1M context)