make the local venv match CI - #24
Merged
Merged
Conversation
CI and a workstation ran different software, and the difference decided lint results: requirements.txt pinned the batteries-included `ansible` package, so locally every collection in it resolved and ansible/requirements.yml was decorative, while CI installed only the two pins into a pipx-based environment. A task using an unlisted community.* module passed here and failed there - which is exactly how PR #22 went red. Both sides now build the same environment from the same two files: - requirements.txt pins ansible-core instead of ansible (regenerated with uv pip freeze; platformdirs moved 4.11.4 -> 4.11.5 in the fresh resolve). - CI drops pipx for `uv venv --python 3.14 --managed-python` plus `uv pip install -r requirements.txt`, the same commands README.md gives an operator, and puts .venv/bin on $GITHUB_PATH. - scripts/install-collections.sh is the one way collections are installed, used by both, into the ANSIBLE_COLLECTIONS_PATH its caller names: tofu.env sets .venv/share/ansible/collections locally, the workflow's job env: block the same path under the workspace. Recreating the venv now takes the collections with it. - check-ansible.sh verifies the installed collections against the pins before anything else, so a missing one says so instead of surfacing as an opaque ansible-lint "couldn't resolve module/action". Verified: check-ansible.sh reports ok on all four checks, tofu validate and fmt -check pass, tofu plan is unchanged, and pointing a task at an unpinned collection now fails ansible-lint locally the way it always did in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzjHsAQ3HkQjLdgv5PVi55
astral-sh/setup-uv publishes no moving major alias past v8: v9 and v10 exist only as full version tags, so @v10 failed the run at "Prepare all required actions" before any step ran. Pinning the exact version is what the rest of this repo does anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzjHsAQ3HkQjLdgv5PVi55
vpzed
approved these changes
Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
CI and a workstation ran different software, and the difference decided lint results:
ansible==14.3.1, 244 MB of collections insite-packagespipx install ansible-core, unpinned==26.8.0pipx install ansible-lint, unpinned, with its own secondansible-coreansible14.3.1 shipped (45 namespaces)ansible/requirements.yml, in$RUNNER_TEMPSo
ansible/requirements.ymlwas authoritative in CI and decorative locally: a task using an unlistedcommunity.*module passed on a workstation and failed in CI — which is how PR #22 went red.Both sides now build the same environment from the same two files:
requirements.txtfor Python,ansible/requirements.ymlfor collections.Not done, deliberately: giving CI the full
ansiblepackage. That would hand both sides collections nobody pinned and makeansible/requirements.ymlinert — the guarantee worth having is that a missing pin fails everywhere.What changed
requirements.txt—ansibleout,ansible-corestays; regenerated withuv pip freezethe way the file was originally made. Only other movement isplatformdirs4.11.4 → 4.11.5 from the fresh resolve.blackandyamllintareansible-lintdependencies, so they survive.scripts/install-collections.sh(new) — the one way collections get installed, run by both environments. Installs into theANSIBLE_COLLECTIONS_PATHits caller names,--forceso a copy elsewhere in galaxy's search path cannot turn it back into a "Nothing to do" no-op.tofu.env— exportsANSIBLE_COLLECTIONS_PATH=$PWD/.venv/share/ansible/collections. Under.venv/so recreating the venv takes the collections with it; setting it also replaces ansible's defaults, which is the point..github/workflows/validate.yaml— drops pipx foruv venv --python 3.14 --managed-python+uv pip install -r requirements.txt(the README's own commands),.venv/binonto$GITHUB_PATH, and the same script. The path is a job-levelenv:entry built fromgithub.workspace;runneris unavailable in that block, which is what produced the invalid-workflow run on golden image cutover #22.scripts/check-ansible.sh— new first check: every pin inansible/requirements.ymlinstalled at exactly the pinned version, naminginstall-collections.shwhen not. Without it a stale collection shows up only as an opaqueansible-lintfailure three checks later.README.md(setup steps, tree, the "Collections are pinned" bullet),CLAUDE.md(whattofu.envexports, the venv/collections relationship), and the header ofansible/requirements.yml, which previously described this change as the fix to make.Verification
tofu validateandtofu fmt -check -recursivepass;tofu planreports No changes — no VM is touched by this.ansible-doc community.general.timezoneresolves out of.venv/share/ansible/collections, so the module is found at execution time, not just by lint.basetask atcommunity.docker.docker_host_infonow fails locally withsyntax-check[unknown-module], which is the whole claim — before this, it would have linted clean here and failed in CI. Reverted.