From b047fc296f318291651e076226a892b985af7d94 Mon Sep 17 00:00:00 2001 From: Mike Riddle Date: Mon, 2 Nov 2026 16:50:37 -0500 Subject: [PATCH 1/2] Add the setup-vagrant-libvirt composite action Install Vagrant, the vagrant-libvirt provider and libvirt on an Ubuntu runner for Beaker acceptance tests, without failing the job when Launchpad is down. Ubuntu dropped vagrant and vagrant-libvirt after 23.10; ppa:evgeni/vagrant carries vagrant 2.3.7, the last MPL-licensed release, and a matching vagrant-libvirt. The action tries, in order: 1. The Actions cache: the PPA's packages, restored from the newest key for the runner's Ubuntu series and architecture, installed with no request to Launchpad. 2. The PPA, with each step retried three times, 30s and then 60s apart. Only the PPA's own index refresh uses --error-on=any, so a flaky third-party source on the runner image doesn't fail the job. 3. HashiCorp's apt repository, pinned to vagrant-version, with `vagrant plugin install vagrant-libvirt` pinned to vagrant-libvirt-version, and a warning that this is BSL-licensed Vagrant. On a series the PPA doesn't publish for (ppa-series), the action goes straight here with a notice. A pull request can only restore caches saved from its own ref, its base branch or the default branch, so `cache-only: 'true'` saves the packages from a default-branch workflow instead of installing. It caches every package the PPA would provide, unpinned, under a key derived from their file names: a new PPA build gets a new key, and restoring an existing key keeps it from being evicted. Downloads go through apt-get download, which checks them against the PPA's signed index. The action lives here rather than in a simp/github-action-* repo of its own, so there is one repo to manage; the README now says composite actions belong here. CI shellchecks the actions' scripts and runs this one on real runners: cache-only on ubuntu-24.04 and ubuntu-latest, and installs from the cache, the PPA and HashiCorp, with Launchpad reachable and blocked. See simp/puppetsync#122. Co-Authored-By: Claude Opus 5.5 --- .../actions/setup-vagrant-libvirt/README.md | 86 +++++++++ .../actions/setup-vagrant-libvirt/action.yml | 126 +++++++++++++ .../setup-vagrant-libvirt.bash | 169 ++++++++++++++++++ .github/workflows/ci.yml | 78 +++++++- README.md | 26 ++- 5 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 .github/actions/setup-vagrant-libvirt/README.md create mode 100644 .github/actions/setup-vagrant-libvirt/action.yml create mode 100755 .github/actions/setup-vagrant-libvirt/setup-vagrant-libvirt.bash diff --git a/.github/actions/setup-vagrant-libvirt/README.md b/.github/actions/setup-vagrant-libvirt/README.md new file mode 100644 index 0000000..f63eb39 --- /dev/null +++ b/.github/actions/setup-vagrant-libvirt/README.md @@ -0,0 +1,86 @@ +# setup-vagrant-libvirt + +Install Vagrant, the vagrant-libvirt provider and libvirt on an Ubuntu +GitHub Actions runner, for Beaker acceptance tests with +`BEAKER_HYPERVISOR=vagrant_libvirt`. + +Ubuntu stopped shipping `vagrant` and `vagrant-libvirt` after 23.10. +[`ppa:evgeni/vagrant`](https://launchpad.net/~evgeni/+archive/ubuntu/vagrant) +carries matching builds of both, with Vagrant 2.3.7, the last MPL-licensed +release. Installing from the PPA alone fails the job whenever Launchpad is +down, so this action tries, in order: + +1. **The Actions cache**: the PPA's packages, saved by a `cache-only` run on + the default branch. No request goes to Launchpad. Their dependencies come + from the Ubuntu archive. +2. **The PPA**: `add-apt-repository`, then a refresh of the PPA's index alone + and the install. Each step is tried three times, 30s and then 60s apart. +3. **HashiCorp**: `vagrant` from , pinned + to `vagrant-version`, and `vagrant plugin install vagrant-libvirt`, pinned + to `vagrant-libvirt-version` and built against `libvirt-dev`. This is + BSL-licensed Vagrant, so the job gets a warning. + +On an Ubuntu series the PPA doesn't publish for (`ppa-series`), the action +goes straight to HashiCorp with a notice. + +## Usage + +In the acceptance job: + +```yaml + - name: Setup libvirt for Vagrant + uses: simp/gha-workflows/.github/actions/setup-vagrant-libvirt@v1.1.0 + - name: beaker + env: + BEAKER_HYPERVISOR: 'vagrant_libvirt' + VAGRANT_DEFAULT_PROVIDER: 'libvirt' + run: bundle exec rake beaker:suites[default,almalinux9] +``` + +GitHub only lets a pull request restore caches saved from its own ref, its +base branch or the default branch, so a cache saved by one pull request is +no use to the next. Save the packages from the default branch with a +separate workflow, using a runner on a series the PPA publishes for: + +```yaml +name: Vagrant package cache + +'on': + push: + branches: [main] + schedule: + - cron: '17 6 * * 1,4' + workflow_dispatch: {} + +jobs: + cache: + runs-on: ubuntu-24.04 + steps: + - uses: simp/gha-workflows/.github/actions/setup-vagrant-libvirt@v1.1.0 + with: + cache-only: 'true' +``` + +A `cache-only` run looks up the packages the PPA currently provides and +derives the cache key from their file names. If that key is already cached, +it restores it, which also keeps GitHub from evicting it after seven days +without use. Otherwise it downloads the packages and saves them. A new PPA +build gets a new key, and installs restore the newest key for their Ubuntu +series and architecture. The run fails if the PPA can't be reached after +the retries. + +## Inputs + +| Input | Default | Description | +|---|---|---| +| `cache-only` | `'false'` | `'true'` saves the PPA's packages to the Actions cache instead of installing anything | +| `cache` | `'true'` | `'false'` ignores the Actions cache when installing | +| `ppa-series` | `'noble'` | Space-separated Ubuntu codenames the PPA publishes for | +| `vagrant-version` | `'2.4.9-1'` | HashiCorp's apt version of `vagrant`, for the fallback | +| `vagrant-libvirt-version` | `'0.12.2'` | The `vagrant-libvirt` plugin version for the fallback | + +## Outputs + +| Output | Description | +|---|---| +| `source` | Install: `cache`, `ppa` or `hashicorp`. Cache-only: `cached` (already cached), `saved`, or `none` (the PPA doesn't publish for this series) | diff --git a/.github/actions/setup-vagrant-libvirt/action.yml b/.github/actions/setup-vagrant-libvirt/action.yml new file mode 100644 index 0000000..48442fc --- /dev/null +++ b/.github/actions/setup-vagrant-libvirt/action.yml @@ -0,0 +1,126 @@ +--- +name: 'Setup Vagrant with libvirt' +description: >- + Install Vagrant, vagrant-libvirt and libvirt on an Ubuntu runner from + ppa:evgeni/vagrant, the Actions cache, or HashiCorp's apt repository, + so a Launchpad outage doesn't fail the job. +branding: + icon: 'box' + color: 'blue' + +inputs: + cache-only: + description: >- + 'true' to save the PPA's packages to the Actions cache instead of + installing anything. Run it from the default branch (push, schedule) + so pull requests can restore the cache. + required: false + default: 'false' + cache: + description: "'false' to ignore the Actions cache when installing" + required: false + default: 'true' + ppa-series: + description: >- + Space-separated Ubuntu codenames that ppa:evgeni/vagrant publishes + for. On any other series the action goes straight to HashiCorp. + required: false + default: 'noble' + vagrant-version: + description: "HashiCorp's apt version of vagrant, used when the PPA can't be" + required: false + default: '2.4.9-1' + vagrant-libvirt-version: + description: 'The vagrant-libvirt plugin version installed with HashiCorp vagrant' + required: false + default: '0.12.2' + +outputs: + source: + description: >- + Where Vagrant came from: 'cache', 'ppa' or 'hashicorp' (install), or + 'cached', 'saved' or 'none' (cache-only) + value: ${{ steps.install.outputs.source || steps.result.outputs.source }} + +runs: + using: composite + steps: + - name: Check the Ubuntu series + id: series + shell: bash + env: + PPA_SERIES: ${{ inputs.ppa-series }} + run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" series' + + # Install: restore the newest cached package set, if any + - name: Restore the cached PPA packages + if: >- + inputs.cache-only != 'true' && inputs.cache != 'false' && + steps.series.outputs.ppa == 'true' + uses: actions/cache/restore@v6 + with: + path: ${{ steps.series.outputs.dir }} + key: ${{ steps.series.outputs.prefix }} + restore-keys: ${{ steps.series.outputs.prefix }} + + - name: Install Vagrant and libvirt + id: install + if: inputs.cache-only != 'true' + shell: bash + env: + DEBS: ${{ steps.series.outputs.dir }} + PPA: ${{ steps.series.outputs.ppa }} + CODENAME: ${{ steps.series.outputs.codename }} + VAGRANT_VERSION: ${{ inputs.vagrant-version }} + VAGRANT_LIBVIRT_VERSION: ${{ inputs.vagrant-libvirt-version }} + run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" install' + + # Cache-only: name the PPA's current packages, then save them once + - name: Resolve the PPA packages + id: resolve + if: inputs.cache-only == 'true' && steps.series.outputs.ppa == 'true' + shell: bash + env: + PREFIX: ${{ steps.series.outputs.prefix }} + run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" resolve' + + # A restore of an existing key also keeps it from being evicted + - name: Check the cache + id: cached + if: steps.resolve.outcome == 'success' + uses: actions/cache/restore@v6 + with: + path: ${{ steps.series.outputs.dir }} + key: ${{ steps.resolve.outputs.key }} + + - name: Download the PPA packages + if: steps.resolve.outcome == 'success' && steps.cached.outputs.cache-hit != 'true' + shell: bash + env: + DEBS: ${{ steps.series.outputs.dir }} + run: '"$GITHUB_ACTION_PATH/setup-vagrant-libvirt.bash" download' + + - name: Save the PPA packages + if: steps.resolve.outcome == 'success' && steps.cached.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: ${{ steps.series.outputs.dir }} + key: ${{ steps.resolve.outputs.key }} + + - name: Report the cache result + id: result + if: inputs.cache-only == 'true' + shell: bash + env: + PPA: ${{ steps.series.outputs.ppa }} + CODENAME: ${{ steps.series.outputs.codename }} + HIT: ${{ steps.cached.outputs.cache-hit }} + run: | + if [ "$PPA" != 'true' ]; then + echo "::notice::ppa:evgeni/vagrant has no packages for Ubuntu ${CODENAME}; nothing to cache" + echo 'source=none' >> "$GITHUB_OUTPUT" + elif [ "$HIT" = 'true' ]; then + echo 'source=cached' >> "$GITHUB_OUTPUT" + else + echo 'source=saved' >> "$GITHUB_OUTPUT" + fi diff --git a/.github/actions/setup-vagrant-libvirt/setup-vagrant-libvirt.bash b/.github/actions/setup-vagrant-libvirt/setup-vagrant-libvirt.bash new file mode 100755 index 0000000..13a8808 --- /dev/null +++ b/.github/actions/setup-vagrant-libvirt/setup-vagrant-libvirt.bash @@ -0,0 +1,169 @@ +#!/bin/bash +# +# The steps of action.yml. Usage: setup-vagrant-libvirt.bash COMMAND +# +# series report the Ubuntu codename, whether the PPA publishes for it, +# and the cache directory and key prefix +# install install Vagrant, vagrant-libvirt and libvirt from the cached +# PPA packages, else the PPA, else HashiCorp and rubygems.org +# resolve add the PPA and name the packages it would install, with a +# cache key derived from their file names +# download download the resolved packages into the cache directory +# (run in the same job as resolve, which adds the PPA) +# +# ppa:evgeni/vagrant has vagrant 2.3.7, the last MPL-licensed release, and +# a matching vagrant-libvirt. Ubuntu dropped both after 23.10. + +set -euo pipefail + +PPA_NAME='ppa:evgeni/vagrant' +PPA_URL='https://ppa.launchpadcontent.net/evgeni/vagrant/' +PPA_LIST="${RUNNER_TEMP:-/tmp}/vagrant-ppa-debs.list" +LIBVIRT_PACKAGES=(libvirt-daemon-system libvirt-daemon qemu-system-x86 qemu-utils dnsmasq) + +# Up to three attempts, 30s and then 60s apart +retry() { + local delay + for delay in 30 60 0; do + "$@" && return 0 + if [ "$delay" -eq 0 ]; then + return 1 + fi + echo "::notice::Retrying in ${delay}s: $*" + sleep "$delay" + done +} + +ppa_sources() { + compgen -G '/etc/apt/sources.list.d/evgeni-ubuntu-vagrant-*' || true +} + +# Refresh the PPA's index alone, failing on any error. A plain +# `apt-get update` only warns when a source is unreachable. +update_ppa() { + local sources + sources=$(ppa_sources) + [ -n "$sources" ] || return 1 + sudo apt-get update --error-on=any \ + -o Dir::Etc::sourcelist="$sources" \ + -o Dir::Etc::sourceparts=- \ + -o APT::Get::List-Cleanup=0 +} + +add_ppa() { + retry sudo add-apt-repository -y -n "$PPA_NAME" && retry update_ppa +} + +# Removing the PPA with add-apt-repository can itself need Launchpad +remove_ppa() { + sudo rm -f /etc/apt/sources.list.d/evgeni-ubuntu-vagrant-* +} + +series() { + local ppa=false arch s + # shellcheck source=/dev/null + . /etc/os-release + arch=$(dpkg --print-architecture) + for s in ${PPA_SERIES:-}; do + if [ "$s" = "$VERSION_CODENAME" ]; then + ppa=true + fi + done + { + echo "codename=${VERSION_CODENAME}" + echo "ppa=${ppa}" + echo "dir=${RUNNER_TEMP}/vagrant-ppa-debs" + echo "prefix=vagrant-ppa-debs-${VERSION_CODENAME}-${arch}-" + } >> "$GITHUB_OUTPUT" +} + +from_cache() { + if ! compgen -G "${DEBS}/*.deb" > /dev/null; then + return 1 + fi + echo "Installing the cached PPA packages: $(cd "$DEBS" && echo *.deb)" + if retry sudo apt-get update && + retry sudo apt-get install -y --no-install-recommends "$DEBS"/*.deb "${LIBVIRT_PACKAGES[@]}"; then + return 0 + fi + echo '::warning::Installing the cached PPA packages failed; trying the PPA' + return 1 +} + +from_ppa() { + if [ "${PPA:-}" != 'true' ]; then + return 1 + fi + retry sudo apt-get update || true + if add_ppa && retry sudo apt-get install -y --no-install-recommends vagrant vagrant-libvirt "${LIBVIRT_PACKAGES[@]}"; then + return 0 + fi + remove_ppa + return 1 +} + +from_hashicorp() { + local fallback="BSL-licensed Vagrant ${VAGRANT_VERSION} from HashiCorp and vagrant-libvirt ${VAGRANT_LIBVIRT_VERSION} from rubygems.org" + if [ "${PPA:-}" = 'true' ]; then + echo "::warning::ppa:evgeni/vagrant is unavailable, so this job uses ${fallback}" + else + echo "::notice::ppa:evgeni/vagrant has no packages for Ubuntu ${CODENAME}, so this job uses ${fallback}" + fi + retry curl -fsSL -o "${RUNNER_TEMP}/hashicorp.asc" https://apt.releases.hashicorp.com/gpg + sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg "${RUNNER_TEMP}/hashicorp.asc" + echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com ${CODENAME} main" | + sudo tee /etc/apt/sources.list.d/hashicorp.list + retry sudo apt-get update + retry sudo apt-get install -y --no-install-recommends "vagrant=${VAGRANT_VERSION}" \ + libvirt-dev build-essential pkg-config "${LIBVIRT_PACKAGES[@]}" + retry vagrant plugin install vagrant-libvirt --plugin-version "$VAGRANT_LIBVIRT_VERSION" +} + +install() { + local source + if from_cache; then + source=cache + elif from_ppa; then + source=ppa + else + from_hashicorp + source=hashicorp + fi + sudo chmod 666 /var/run/libvirt/libvirt-sock + echo "Installed $(vagrant --version) (source: ${source})" + echo "source=${source}" >> "$GITHUB_OUTPUT" +} + +# The PPA's packages that `apt-get install vagrant vagrant-libvirt` would +# fetch, as apt's "'URL' FILE SIZE HASH" lines. Archive packages are left +# out: the install step gets those from the archive either way. +resolve() { + local key + retry sudo apt-get update || true + add_ppa + apt-get install --print-uris -qq vagrant vagrant-libvirt | grep -F "'${PPA_URL}" > "$PPA_LIST" || true + if [ ! -s "$PPA_LIST" ]; then + echo '::error::ppa:evgeni/vagrant provides none of the packages for vagrant and vagrant-libvirt' + return 1 + fi + cat "$PPA_LIST" + key="${PREFIX}$(awk '{ print $2 }' "$PPA_LIST" | sort | sha256sum | cut -c1-16)" + echo "key=${key}" >> "$GITHUB_OUTPUT" +} + +# apt-get download checks each file against the PPA's signed index +download() { + local packages + mapfile -t packages < <(awk '{ split($2, file, "_"); print file[1] }' "$PPA_LIST") + mkdir -p "$DEBS" + (cd "$DEBS" && retry apt-get download "${packages[@]}") + ls -l "$DEBS" +} + +case "${1:-}" in + series | install | resolve | download) "$1" ;; + *) + echo "Usage: $0 series|install|resolve|download" >&2 + exit 2 + ;; +esac diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index feeab3e..ab748fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,5 @@ -# Lint this repo's workflows (including the reusable ones) on every PR +# Lint this repo's workflows (including the reusable ones) and test its +# actions on every PR --- name: CI @@ -16,6 +17,79 @@ jobs: run: | bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) ./actionlint -color + - name: Run shellcheck on the actions' scripts + run: shellcheck .github/actions/*/*.bash + + # The setup-vagrant-libvirt action, run on real runners in each mode: + # cache-only first, so the install jobs that follow can restore its packages + vagrant-cache: + name: 'setup-vagrant-libvirt: cache-only (${{ matrix.runner }})' + strategy: + matrix: + runner: [ubuntu-24.04, ubuntu-latest] + fail-fast: false + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + - id: vagrant + uses: ./.github/actions/setup-vagrant-libvirt + with: + cache-only: 'true' + - name: Check the result + env: + SOURCE: ${{ steps.vagrant.outputs.source }} + run: | + echo "source: ${SOURCE}" + . /etc/os-release + if [ "$VERSION_CODENAME" = 'noble' ]; then + [[ "$SOURCE" =~ ^(cached|saved)$ ]] + else + [ "$SOURCE" = 'none' ] + fi + ! command -v vagrant + + # `noble` is the source expected where the runner is on noble, which the + # PPA publishes for; on any other series it is always hashicorp + vagrant-install: + name: 'setup-vagrant-libvirt: install (${{ matrix.runner }}, cache ${{ matrix.cache }}, Launchpad ${{ matrix.launchpad }})' + needs: vagrant-cache + strategy: + matrix: + include: + - {runner: ubuntu-24.04, cache: 'true', launchpad: reachable, noble: cache} + - {runner: ubuntu-24.04, cache: 'true', launchpad: blocked, noble: cache} + - {runner: ubuntu-24.04, cache: 'false', launchpad: reachable, noble: ppa} + - {runner: ubuntu-24.04, cache: 'false', launchpad: blocked, noble: hashicorp} + - {runner: ubuntu-latest, cache: 'true', launchpad: reachable, noble: cache} + fail-fast: false + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v7 + - name: Block Launchpad + if: matrix.launchpad == 'blocked' + run: | + for host in ppa.launchpadcontent.net ppa.launchpad.net api.launchpad.net launchpad.net keyserver.ubuntu.com; do + echo "0.0.0.0 ${host}" | sudo tee -a /etc/hosts + done + - id: vagrant + uses: ./.github/actions/setup-vagrant-libvirt + with: + cache: ${{ matrix.cache }} + - name: Check the result + env: + SOURCE: ${{ steps.vagrant.outputs.source }} + NOBLE: ${{ matrix.noble }} + run: | + . /etc/os-release + expected=hashicorp + if [ "$VERSION_CODENAME" = 'noble' ]; then + expected=$NOBLE + fi + echo "source: ${SOURCE} (expected ${expected})" + [ "$SOURCE" = "$expected" ] + vagrant --version + vagrant plugin list | grep vagrant-libvirt + virsh --connect qemu:///system list --all # The one check for branch protection to require: GitHub only blocks a # merge on checks listed by name, and this job fails unless every job it @@ -25,6 +99,8 @@ jobs: if: always() needs: - actionlint + - vagrant-cache + - vagrant-install runs-on: ubuntu-latest steps: - name: 'Fail unless every needed job succeeded' diff --git a/README.md b/README.md index b77c400..f70e947 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ a fix is a tag bump here instead of a fleet-wide sync — and half-rolled-out changes (like [simp/puppetsync#84][84]) become structurally impossible. See [simp/puppetsync#85][85] for the background and migration plan. -This repo holds two kinds of workflow: +This repo holds two kinds of workflow, and composite actions: - **Reusable workflows** (`on: workflow_call`) — called by a thin shim in each consuming repo. The original purpose of this repo. @@ -19,6 +19,10 @@ This repo holds two kinds of workflow: nothing in the other repos to trigger them. They are the right shape whenever a task is fleet-wide and needs no per-repo configuration: one file to change, idempotent, and it covers repos outside puppetsync's sync set for free. +- **Composite actions** (`.github/actions//action.yml`) — steps shared + across repos that have to run inside a job the repo owns, such as the + setup for an acceptance job with a repo-specific matrix. Kept here rather + than in a repo of their own, so there is one repo to manage. [reusing]: https://docs.github.com/en/actions/using-workflows/reusing-workflows [84]: https://github.com/simp/puppetsync/issues/84 @@ -42,6 +46,19 @@ namespaced by the kind of repo they serve: | [`org_sync_forks.yml`](.github/workflows/org_sync_forks.yml) | Weekly: fast-forward the org's pure-mirror forks from their upstreams. Skips any fork carrying SIMP commits | | [`org_reconcile_board_status.yml`](.github/workflows/org_reconcile_board_status.yml) | Hourly: reconcile the [Org Triage board](https://github.com/orgs/simp/projects/11) with live state — Status from member PRs (draft → In Progress, ready → In Review, …), `community` on outside contributions, `needs-attention` on items idle past their column's threshold. Never moves items out of Blocked/Parked/Done, never closes anything | +## Actions + +| Action | Purpose | +|---|---| +| [`setup-vagrant-libvirt`](.github/actions/setup-vagrant-libvirt) | Install Vagrant, vagrant-libvirt and libvirt for Beaker acceptance tests from the Actions cache, `ppa:evgeni/vagrant`, or HashiCorp, so a Launchpad outage doesn't fail the job | + +Call one as a step, pinned to a release tag like the workflows: + +```yaml + - name: Setup libvirt for Vagrant + uses: simp/gha-workflows/.github/actions/setup-vagrant-libvirt@v1.1.0 +``` + ## Calling a workflow Each consuming repo carries a thin shim (maintained by puppetsync) that @@ -77,15 +94,16 @@ secrets, permissions, and observable behavior). Callers pin an exact tag and keeping rollouts deliberate and reviewable. - **Major**: a caller must change (input/secret/permission contract) -- **Minor**: new workflows or new optional inputs +- **Minor**: new workflows or actions, or new optional inputs - **Patch**: behavior fixes within the existing contract ## What belongs here (and what doesn't) - **Here**: workflow *logic* shared across repos (`on: workflow_call`), and org-level scheduled jobs that operate on the fleet through the API (`org_*`). -- **`simp/github-action-*` repos**: single composite/Docker **actions** - (things with an `action.yml`), e.g. +- **Here, too**: composite actions shared across repos, under + `.github/actions/`. Older actions live in their own `simp/github-action-*` + repos, e.g. [github-action-build-and-sign-pkg-single-rpm](https://github.com/simp/github-action-build-and-sign-pkg-single-rpm). - **The consuming repo**: triggers, per-repo configuration (matrices, nodesets), and anything repo-specific. If a workflow needs per-repo From 092c6a8cebfec1b6fc120e2b6d18c1877c31f289 Mon Sep 17 00:00:00 2001 From: Mike Riddle Date: Mon, 2 Nov 2026 21:49:58 -0500 Subject: [PATCH 2/2] Address review of the setup-vagrant-libvirt action - CI runs the action's tests only when the action or ci.yml changes, so a Launchpad outage can't block an unrelated PR; `All checks` counts a skipped job as passing. - Name the install test cases by what they exercise, and call the ubuntu-latest row a canary for how consuming repos run. - README: document that the cache and PPA only apply on noble runners, so jobs on ubuntu-latest (now 26.04) always install HashiCorp's Vagrant; add a "Warming the cache" section and the 60-day schedule caveat; reword the source list and the repo README per review. - action.yml: "Set up" in the name, drop `branding` (Marketplace only), and finish the vagrant-version description. Co-Authored-By: Claude Opus 5.5 --- .../actions/setup-vagrant-libvirt/README.md | 72 ++++++++++++------- .../actions/setup-vagrant-libvirt/action.yml | 7 +- .github/workflows/ci.yml | 51 ++++++++++--- README.md | 18 ++--- 4 files changed, 97 insertions(+), 51 deletions(-) diff --git a/.github/actions/setup-vagrant-libvirt/README.md b/.github/actions/setup-vagrant-libvirt/README.md index f63eb39..6209013 100644 --- a/.github/actions/setup-vagrant-libvirt/README.md +++ b/.github/actions/setup-vagrant-libvirt/README.md @@ -6,22 +6,32 @@ GitHub Actions runner, for Beaker acceptance tests with Ubuntu stopped shipping `vagrant` and `vagrant-libvirt` after 23.10. [`ppa:evgeni/vagrant`](https://launchpad.net/~evgeni/+archive/ubuntu/vagrant) -carries matching builds of both, with Vagrant 2.3.7, the last MPL-licensed -release. Installing from the PPA alone fails the job whenever Launchpad is -down, so this action tries, in order: - -1. **The Actions cache**: the PPA's packages, saved by a `cache-only` run on - the default branch. No request goes to Launchpad. Their dependencies come - from the Ubuntu archive. -2. **The PPA**: `add-apt-repository`, then a refresh of the PPA's index alone - and the install. Each step is tried three times, 30s and then 60s apart. -3. **HashiCorp**: `vagrant` from , pinned - to `vagrant-version`, and `vagrant plugin install vagrant-libvirt`, pinned - to `vagrant-libvirt-version` and built against `libvirt-dev`. This is - BSL-licensed Vagrant, so the job gets a warning. - -On an Ubuntu series the PPA doesn't publish for (`ppa-series`), the action -goes straight to HashiCorp with a notice. +packages Vagrant 2.3.7 (the last MPL-licensed release) and a matching +vagrant-libvirt. Installing from the PPA alone fails the job whenever +Launchpad is down, so this action tries each source in turn: + +1. **The Actions cache**: the PPA's packages (including any dependencies + the PPA supplies), saved by a `cache-only` run on the default branch. + This path makes no requests to Launchpad; everything else comes from + the Ubuntu archive. +2. **The PPA**: add it, refresh only its index (so an error from that + source fails the attempt rather than being a warning), and install. + Each step is tried three times, 30s and then 60s apart. +3. **HashiCorp**: `vagrant` (pinned to `vagrant-version`) from + , plus `vagrant plugin install + vagrant-libvirt` (pinned to `vagrant-libvirt-version`), built against + `libvirt-dev`. HashiCorp's Vagrant is BSL-licensed, so the action emits + a warning when it falls back to it. + +### Which runner + +The cache and the PPA only apply on the Ubuntu series in `ppa-series`. The +PPA publishes for `noble` alone, so they need `runs-on: ubuntu-24.04`. On +any other series the action goes straight to HashiCorp, with a notice +rather than a warning. `ubuntu-latest` now resolves to `ubuntu-26.04` +(`resolute`), so a job on `ubuntu-latest` always installs HashiCorp's +Vagrant and never contacts Launchpad, and the cache below is only a +hedge for jobs on `ubuntu-24.04`. ## Usage @@ -37,10 +47,13 @@ In the acceptance job: run: bundle exec rake beaker:suites[default,almalinux9] ``` -GitHub only lets a pull request restore caches saved from its own ref, its -base branch or the default branch, so a cache saved by one pull request is -no use to the next. Save the packages from the default branch with a -separate workflow, using a runner on a series the PPA publishes for: +### Warming the cache + +**The cache only helps pull requests if the default branch fills it.** +A pull request can restore caches saved from its own ref, its base branch +or the default branch, but not caches saved by other pull requests. Add a +separate workflow that runs this action with `cache-only: 'true'` on the +default branch, on a runner whose Ubuntu series the PPA publishes for: ```yaml name: Vagrant package cache @@ -61,13 +74,18 @@ jobs: cache-only: 'true' ``` -A `cache-only` run looks up the packages the PPA currently provides and -derives the cache key from their file names. If that key is already cached, -it restores it, which also keeps GitHub from evicting it after seven days -without use. Otherwise it downloads the packages and saves them. A new PPA -build gets a new key, and installs restore the newest key for their Ubuntu -series and architecture. The run fails if the PPA can't be reached after -the retries. +A `cache-only` run asks apt which packages the PPA would currently supply +and derives the cache key from their file names. If an entry with that key +already exists, the run restores it, which resets GitHub's seven-day +eviction clock. Otherwise it downloads the packages and saves a new entry. +A new PPA build therefore gets a new key, and installs restore the newest +key for their Ubuntu series and architecture. The run fails if the PPA is +still unreachable after the retries, so an outage shows up in the +scheduled run. + +GitHub disables scheduled workflows in public repos after 60 days without +activity, so on a quiet repo the cache can expire and the next pull +request starts cold. ## Inputs diff --git a/.github/actions/setup-vagrant-libvirt/action.yml b/.github/actions/setup-vagrant-libvirt/action.yml index 48442fc..48d96e7 100644 --- a/.github/actions/setup-vagrant-libvirt/action.yml +++ b/.github/actions/setup-vagrant-libvirt/action.yml @@ -1,12 +1,9 @@ --- -name: 'Setup Vagrant with libvirt' +name: 'Set up Vagrant with libvirt' description: >- Install Vagrant, vagrant-libvirt and libvirt on an Ubuntu runner from ppa:evgeni/vagrant, the Actions cache, or HashiCorp's apt repository, so a Launchpad outage doesn't fail the job. -branding: - icon: 'box' - color: 'blue' inputs: cache-only: @@ -27,7 +24,7 @@ inputs: required: false default: 'noble' vagrant-version: - description: "HashiCorp's apt version of vagrant, used when the PPA can't be" + description: "HashiCorp's apt version of vagrant, for the fallback when the PPA can't be used" required: false default: '2.4.9-1' vagrant-libvirt-version: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab748fd..fb1c498 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,35 @@ jobs: - name: Run shellcheck on the actions' scripts run: shellcheck .github/actions/*/*.bash + # Which of the jobs below this PR needs. The action's tests depend on + # Launchpad, so they only run when the action or this workflow changes: + # an outage then can't block an unrelated fix. + changes: + name: 'Changed paths' + runs-on: ubuntu-latest + outputs: + vagrant: ${{ steps.diff.outputs.vagrant }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - id: diff + env: + BASE: ${{ github.event.pull_request.base.sha }} + run: | + git diff --name-only "$BASE" HEAD | tee changed + if grep -qE '^\.github/(actions/setup-vagrant-libvirt/|workflows/ci\.yml$)' changed; then + echo 'vagrant=true' >> "$GITHUB_OUTPUT" + else + echo 'vagrant=false' >> "$GITHUB_OUTPUT" + fi + # The setup-vagrant-libvirt action, run on real runners in each mode: # cache-only first, so the install jobs that follow can restore its packages vagrant-cache: name: 'setup-vagrant-libvirt: cache-only (${{ matrix.runner }})' + needs: changes + if: needs.changes.outputs.vagrant == 'true' strategy: matrix: runner: [ubuntu-24.04, ubuntu-latest] @@ -49,18 +74,21 @@ jobs: ! command -v vagrant # `noble` is the source expected where the runner is on noble, which the - # PPA publishes for; on any other series it is always hashicorp + # PPA publishes for; on any other series it is always hashicorp. The + # ubuntu-latest row is a canary for whatever series that label resolves to + # (currently 26.04, so HashiCorp), the way consuming repos run. vagrant-install: - name: 'setup-vagrant-libvirt: install (${{ matrix.runner }}, cache ${{ matrix.cache }}, Launchpad ${{ matrix.launchpad }})' - needs: vagrant-cache + name: 'setup-vagrant-libvirt: install (${{ matrix.runner }}, ${{ matrix.case }})' + needs: [changes, vagrant-cache] + if: needs.changes.outputs.vagrant == 'true' strategy: matrix: include: - - {runner: ubuntu-24.04, cache: 'true', launchpad: reachable, noble: cache} - - {runner: ubuntu-24.04, cache: 'true', launchpad: blocked, noble: cache} - - {runner: ubuntu-24.04, cache: 'false', launchpad: reachable, noble: ppa} - - {runner: ubuntu-24.04, cache: 'false', launchpad: blocked, noble: hashicorp} - - {runner: ubuntu-latest, cache: 'true', launchpad: reachable, noble: cache} + - {runner: ubuntu-24.04, case: cache hit, cache: 'true', launchpad: reachable, noble: cache} + - {runner: ubuntu-24.04, case: 'cache hit, Launchpad blocked', cache: 'true', launchpad: blocked, noble: cache} + - {runner: ubuntu-24.04, case: PPA, cache: 'false', launchpad: reachable, noble: ppa} + - {runner: ubuntu-24.04, case: 'PPA down, HashiCorp fallback', cache: 'false', launchpad: blocked, noble: hashicorp} + - {runner: ubuntu-latest, case: as consuming repos run, cache: 'true', launchpad: reachable, noble: cache} fail-fast: false runs-on: ${{ matrix.runner }} steps: @@ -93,12 +121,15 @@ jobs: # The one check for branch protection to require: GitHub only blocks a # merge on checks listed by name, and this job fails unless every job it - # needs succeeded. Add any new job in this workflow to `needs:`. + # needs succeeded or was skipped (the action tests, when `changes` says + # this PR doesn't touch the action). Add any new job in this workflow to + # `needs:`. all-checks: name: 'All checks' if: always() needs: - actionlint + - changes - vagrant-cache - vagrant-install runs-on: ubuntu-latest @@ -108,4 +139,4 @@ jobs: NEEDS: ${{ toJSON(needs) }} run: | jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<< "$NEEDS" - jq -e 'all(.[]; .result == "success")' <<< "$NEEDS" > /dev/null + jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<< "$NEEDS" > /dev/null diff --git a/README.md b/README.md index f70e947..6798bdd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ a fix is a tag bump here instead of a fleet-wide sync — and half-rolled-out changes (like [simp/puppetsync#84][84]) become structurally impossible. See [simp/puppetsync#85][85] for the background and migration plan. -This repo holds two kinds of workflow, and composite actions: +This repo holds two kinds of workflow, plus shared composite actions: - **Reusable workflows** (`on: workflow_call`) — called by a thin shim in each consuming repo. The original purpose of this repo. @@ -20,9 +20,8 @@ This repo holds two kinds of workflow, and composite actions: a task is fleet-wide and needs no per-repo configuration: one file to change, idempotent, and it covers repos outside puppetsync's sync set for free. - **Composite actions** (`.github/actions//action.yml`) — steps shared - across repos that have to run inside a job the repo owns, such as the - setup for an acceptance job with a repo-specific matrix. Kept here rather - than in a repo of their own, so there is one repo to manage. + across repos that must run inside a job the consuming repo owns, such as + setting up an acceptance job that has a repo-specific matrix. [reusing]: https://docs.github.com/en/actions/using-workflows/reusing-workflows [84]: https://github.com/simp/puppetsync/issues/84 @@ -99,11 +98,12 @@ keeping rollouts deliberate and reviewable. ## What belongs here (and what doesn't) -- **Here**: workflow *logic* shared across repos (`on: workflow_call`), and - org-level scheduled jobs that operate on the fleet through the API (`org_*`). -- **Here, too**: composite actions shared across repos, under - `.github/actions/`. Older actions live in their own `simp/github-action-*` - repos, e.g. +- **Here**: workflow *logic* shared across repos (`on: workflow_call`), + org-level scheduled jobs that operate on the fleet through the API + (`org_*`), and composite actions shared across repos + (`.github/actions//`). New actions go here rather than in a repo of + their own, so there is one repo to version, review and protect. Older + actions still live in their own `simp/github-action-*` repos, e.g. [github-action-build-and-sign-pkg-single-rpm](https://github.com/simp/github-action-build-and-sign-pkg-single-rpm). - **The consuming repo**: triggers, per-repo configuration (matrices, nodesets), and anything repo-specific. If a workflow needs per-repo