Skip to content

ci: cache Galaxy collections + pip, and bound every job (#53) - #56

Merged
thiras merged 3 commits into
mainfrom
ci/cache-galaxy-pip-and-job-timeouts
Sep 16, 2026
Merged

thiras merged 3 commits into
mainfrom
ci/cache-galaxy-pip-and-job-timeouts

Conversation

@thiras

@thiras thiras commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Closes #53.

1. Galaxy collections cache

make deps resolved against galaxy.ansible.com on every run of ansible-lint, galaxy-build and molecule, with no cache and no retry — the galaxy-build job on #50 died on a transient Connection reset by peer while resolving the transitive community.crypto, then passed unchanged on re-run.

ansible/collections is now cached across all three jobs under one shared key.

This removes the flake class rather than shortening it. With the requirements already satisfied on disk, ansible-galaxy collection install makes zero network calls — verified by pointing it at an unreachable Galaxy server:

$ ansible-galaxy collection install -r requirements.yml -p collections   # server = http://127.0.0.1:1/api/
Starting galaxy collection install process
Nothing to do. All requested collections are already installed.
$ echo $?
0

Three deliberate choices:

  • Split actions/cache/restore + /save, not the all-in-one. The failure being defended against is Galaxy dying part way through an install; the combined action's post step would happily persist that half-written tree. save is gated on the restore having missed and on the deps step succeeding.
  • No restore-keys. An older cache still satisfies the >= ranges in requirements.yml, so make deps would find nothing to do and a GALAXY_CACHE_EPOCH bump would be a silent no-op.
  • GALAXY_CACHE_EPOCH as the refresh lever. Because those are ranges and not pins, a warm cache would otherwise freeze the resolved set (transitive collections included) until requirements.yml changes, drifting CI away from what a local make lint-ansible resolves. The epoch makes that refresh explicit and visible — the same manual posture .github/dependabot.yml already declares for Galaxy. It is declared in both workflows and must match; CONTRIBUTING.md lists it alongside the other bump-in-two-places values.

2. pip cache

Via setup-python, keyed on the workflow file — this repo ships no pip manifest, and the workflow is literally where the package list lives. Two limits, recorded in the comments rather than glossed:

  • 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.
  • setup-python derives its own key and takes no custom suffix, so ansible-lint and galaxy-build share one pip cache. Their package sets overlap heavily, so whichever populates it first covers most of both.

3. timeout-minutes on every job

The issue named molecule; the ci.yml jobs had the same gap, so all seven are bounded. Values are bounds sized off observed runtimes, not targets:

Job Observed Bound
molecule 4m41s @ JOBS=3 45
galaxy-build 1m45s 20
ansible-lint 1m15s 20
helm / kics pull images 20
actionlint 12s 10
changes 5s 5

4. Path filters made self-testing

The ansible jobs' cache wiring now lives in ci.yml, and the molecule job's in molecule.yml — but neither file was in its own trigger, so this very PR would not have exercised the jobs it changes. Each workflow now lists itself, the same reason the helm filter already claimed ci.yml.

Verification

  • actionlint (the blocking CI gate for workflow syntax) — clean, exit 0.
  • make lint (all pre-commit hooks) — clean.
  • Structural check over both parsed workflows: every job bounded, the epoch identical in both files, and one single cache key used by exactly 3 restore + 3 save steps (a divergence would silently split the cache).
  • actions/cache pinned to caa29612…, confirmed to be exactly tag v5.1.0 and to carry both sub-actions at that commit.

What CI here proves and doesn't: this first run is a cold cache by definition, so it should log Cache not found for input keys: galaxy-… and save at the end. The payoff is visible on the second run — Cache restored from key: galaxy-… plus Nothing to do. All requested collections are already installed. with no download lines. Worth an empty follow-up commit to confirm before merging.

🤖 Generated with Claude Code

`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) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 16, 2026 01:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Prevent the duplicated Galaxy cache epochs from diverging and splitting the shared cache.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR improves CI reliability and speed through dependency caching, job timeouts, and self-testing workflow filters.

Changes:

  • Added Galaxy collection and pip caching.
  • Bounded all CI jobs with timeouts.
  • Updated workflow path filters and contributor documentation.
File summaries
File Summary
CONTRIBUTING.md Documents CI caching, timeouts, and Galaxy cache refreshes.
.github/workflows/molecule.yml Adds caching, timeout, and self-triggering filters; duplicated cache epochs may diverge.
.github/workflows/ci.yml Adds caching, timeouts, and CI workflow path coverage.
Review details

Suppressed comments (1)

.github/workflows/molecule.yml:14

  • Because GALAXY_CACHE_EPOCH is part of the shared key, changing only ci.yml does not run this workflow's molecule job (and changing only this file does not enable ci.yml's Ansible jobs). The two values can therefore diverge without a failing check, silently splitting the cache; derive the epoch from one tracked value or add a cross-workflow equality check.
    paths: ['ansible/**', '.github/workflows/molecule.yml']
  push:
    branches: [main]
    paths: ['ansible/**', '.github/workflows/molecule.yml']
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CONTRIBUTING.md Outdated
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) <noreply@anthropic.com>
@thiras

thiras commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Re: the duplicated GALAXY_CACHE_EPOCH (review item 2)

Verified, and the reasoning holds exactly as stated: molecule.yml's paths don't include ci.yml, and ci.yml's ansible filter doesn't include molecule.yml, so an epoch edit in one file alone produces no failing check while silently splitting the shared key. My "bump BOTH" comment was documentation, not a guard — and this repo's own ansible/Makefile is explicit that a harness which passes without having run is worse than a slow one.

Took the first of the two suggested fixes (derive from one tracked value) rather than the equality check, since it removes the duplication instead of policing it. The counter now lives in ansible/requirements.yml — the file the key already hashes:

#   cache-epoch: 1
-          key: galaxy-${{ runner.os }}-${{ env.GALAXY_CACHE_EPOCH }}-${{ hashFiles('ansible/requirements.yml') }}
+          key: galaxy-${{ runner.os }}-${{ hashFiles('ansible/requirements.yml') }}

Both env: blocks are gone. One file, one hash — divergence is now structurally impossible rather than merely discouraged, and a bump matches both workflows' ansible/** trigger so it is exercised immediately instead of on the next unrelated PR. The trade is that the lever is a load-bearing comment, which the file says in as many words.

Verified: actionlint clean, make lint clean, no GALAXY_CACHE_EPOCH left anywhere, requirements.yml still parses to the same two collections, one single key across all 6 cache steps, and bumping the counter does move the hash (bed1293a… → a84c87d8…).

Note on what the first run actually proved

All checks were green on 738afb4, but worth being precise about what that did and didn't establish.

The cold run saved a Galaxy cache of only 145 KB, because in CI the pip-installed ansible package already bundles most of the tree:

Downloading https://galaxy.ansible.com/.../devsec-hardening-10.6.0.tar.gz
Installing 'devsec.hardening:10.6.0' to '.../ansible/collections/...'
'ansible.posix:2.2.2' is already installed, skipping.
'community.mysql:5.0.2' is already installed, skipping.

So make deps only ever writes devsec.hardening into ansible/collections — pre-existing behaviour, not something the cache changed. The speed win is therefore negligible; the win that matters is still suppressing that one Downloading https://galaxy.ansible.com/… call, which is what flaked on #50.

The documented race showed up too, exactly as the comments predict, and is non-fatal:

[galaxy-build] Failed to save: Unable to reserve cache with key galaxy-Linux-… ,
               another job may be creating this cache.

Still unproven: that a warm tree yields Nothing to do with zero network in CI's layout. My local check had the full transitive set on disk; CI's tree holds only devsec.hardening. This commit changes requirements.yml, so it necessarily starts cold again — the warm-run evidence has to come from the run after this one.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
@thiras

thiras commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Warm-cache run: the claim is now verified in CI, not just locally

0fec3aa is an empty commit, so it re-ran everything against the cache f4606e3 populated. All checks green. The evidence I said was still missing:

ansible-lint

Cache restored from key: galaxy-Linux-d0010b806cd27f1c76a665daf25aebd807a15836f1a31917ecc69c546fb50ea4
...
ansible-galaxy collection install -r requirements.yml -p collections
Starting galaxy collection install process
Nothing to do. All requested collections are already installed.

molecule (same key, cache shared across workflows as designed)

Cache restored from key: galaxy-Linux-d0010b806cd27f1c76a665daf25aebd807a15836f1a31917ecc69c546fb50ea4
Nothing to do. All requested collections are already installed.

Occurrences of galaxy.ansible.com in either job's full log: 0. Compare the cold run, which had:

Downloading https://galaxy.ansible.com/.../devsec-hardening-10.6.0.tar.gz

That download is the exact call that failed #50 with Connection reset by peer. On a cache hit it is not retried, not made more reliable — it does not happen. This was the one thing my earlier local test couldn't establish, since CI's tree holds only devsec.hardening while my local tree had the full transitive set.

Save gating confirmed: neither actions/cache/save step emitted a log group on this run, i.e. both were correctly skipped by cache-hit == 'true'.

Pip cache restored too — and notably across a changed workflow-file hash (97170916… → 30ac3e35…), because setup-python applies its own internal restore-key prefix fallback. Better than I'd assumed when I wrote that comment.

Summary of the three runs

Commit Galaxy cache make deps behaviour
738afb4 miss, saved 145 KB downloads devsec.hardening from Galaxy
f4606e3 miss (key moved: requirements.yml edited), saved downloads from Galaxy
0fec3aa hit Nothing to do, zero network

Ready for review.

🤖 Generated with Claude Code

@thiras
thiras merged commit fb905fa into main Sep 16, 2026
11 checks passed
@thiras
thiras deleted the ci/cache-galaxy-pip-and-job-timeouts branch September 16, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: cache Galaxy collections + pip, and bound the molecule job

2 participants