Files
config-public/ci/toolchain/action.yml
T
Lucas WintherandClaude Opus 5.5 be0b90b879 Cache the CI toolchain in a shared action
ci/toolchain installs the pinned tools, collections and rsync from cache for
both GitHub and Gitea; GitLab caches pip downloads. shellcheck now finds
scripts rather than globbing, which broke when a glob matched nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:16:58 +02:00

133 lines
5.2 KiB
YAML

---
# The pinned toolchain, cached. Shared by the deploy, lint and connectivity
# workflows of both GitHub Actions and Gitea Actions, so that the six cannot
# drift and so that the cache logic exists once. It lives outside .github/ and
# .gitea/ for that reason: deleting the platform you do not use must not delete
# it. Call it after actions/checkout, as `uses: ./ci/toolchain` — it reads the
# pins from scripts/check.sh and is itself read from the checkout.
#
# A fresh install can take a couple of minutes against a deploy of seconds. On a
# cache hit nothing here touches the network.
#
# The toolchain key covers which pins were asked for, the pins themselves
# (check.sh, requirements.yml), the exact interpreter the venv is bound to, and
# the month: the pins are ranges so that patch releases arrive, and a cache that
# never expired would stop them. A new month, a raised pin or a new Python patch
# builds it afresh. The hash is taken in shell, not with hashFiles(), because
# Gitea does not implement it — see "Expressions are barely supported" in
# docs/ci-gitea.md.
#
# A cache server the job cannot reach is a miss, not a failure: the steps fall
# back to installing, exactly as they would with no cache at all.
name: Pinned toolchain
description: Python, the pinned Ansible tools and optionally rsync, from cache when possible
inputs:
specs:
description: >-
core installs ansible-core only; all adds ansible-lint and yamllint
default: core
collections:
description: Install the Galaxy collections in ansible/requirements.yml
default: "false"
rsync:
description: Install rsync, which ansible.posix.synchronize needs locally
default: "false"
runs:
using: composite
steps:
# Kept even though runner images ship a Python: Ubuntu 24.04 images are PEP
# 668 "externally managed", where a plain `pip install` refuses to touch the
# system interpreter. See "actions/setup-python stays" in docs/ci-gitea.md.
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Compute the cache keys
id: keys
shell: bash
env:
SPECS: ${{ inputs.specs }}
run: |
set -eu
case "$SPECS" in
core|all) ;;
*) echo "toolchain: specs must be core or all, not '$SPECS'"; exit 1 ;;
esac
. /etc/os-release
py="$(python -c 'import platform; print(platform.python_version())')"
pins="$(cat scripts/check.sh ansible/requirements.yml | sha256sum | cut -c1-16)"
{
echo "os=${ID}-${VERSION_ID}"
echo "toolchain=toolchain-${SPECS}-${ID}-${VERSION_ID}-py${py}-$(date -u +%Y-%m)-${pins}"
echo "venv=$HOME/.venvs/toolchain"
} >> "$GITHUB_OUTPUT"
# ansible.posix.synchronize shells out to rsync on the machine Ansible runs
# on, not only on the target. GitHub's hosted images ship it; Gitea's
# act_runner images do not. The .debs are cached rather than the apt lists,
# which are what the slow `apt-get update` fetches. Jobs run as root in a
# container runner, where sudo is often not installed at all, so it is
# asked for only off uid 0.
- name: Restore the rsync packages
if: inputs.rsync == 'true'
uses: actions/cache@v4
with:
path: ~/.cache/toolchain-debs
key: rsync-debs-${{ steps.keys.outputs.os }}
- name: Install rsync
if: inputs.rsync == 'true'
shell: bash
run: |
set -eu
if ! command -v rsync >/dev/null 2>&1; then
sudo=""
[ "$(id -u)" -eq 0 ] || sudo=sudo
debs="$HOME/.cache/toolchain-debs"
if ! ls "$debs"/rsync_*.deb >/dev/null 2>&1; then
mkdir -p "$debs/partial"
$sudo apt-get update -qq
$sudo apt-get install -y --no-install-recommends --download-only \
-o Dir::Cache::archives="$debs" rsync
fi
$sudo dpkg -i "$debs"/*.deb
fi
- name: Restore the pinned toolchain
id: toolchain
uses: actions/cache@v4
with:
path: |
${{ steps.keys.outputs.venv }}
~/.ansible/collections
key: ${{ steps.keys.outputs.toolchain }}-collections-${{ inputs.collections }}
# A venv rather than the setup-python interpreter itself, so that the cache
# holds exactly what this step installed and nothing else. check.sh prints
# the core pin first, which is what `specs: core` takes.
- name: Install the pinned toolchain
if: steps.toolchain.outputs.cache-hit != 'true'
shell: bash
env:
VENV: ${{ steps.keys.outputs.venv }}
SPECS: ${{ inputs.specs }}
COLLECTIONS: ${{ inputs.collections }}
run: |
set -eu
python -m venv "$VENV"
filter=(cat)
[ "$SPECS" = "all" ] || filter=(head -n 1)
scripts/check.sh --print-specs | "${filter[@]}" | tee /dev/stderr \
| xargs -d '\n' "$VENV/bin/pip" install --quiet
if [ "$COLLECTIONS" = "true" ]; then
"$VENV/bin/ansible-galaxy" install -r ansible/requirements.yml
fi
- name: Put the toolchain on PATH
shell: bash
env:
VENV: ${{ steps.keys.outputs.venv }}
run: echo "$VENV/bin" >> "$GITHUB_PATH"