Repository navigation
Add the setup-vagrant-libvirt composite action #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
michael-riddle
merged 2 commits into
simp:main
from
michael-riddle:setup-vagrant-libvirt
Nov 3, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # 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) | ||
| 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 | ||
| <https://apt.releases.hashicorp.com>, 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 | ||
|
|
||
| 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] | ||
| ``` | ||
|
|
||
| ### 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 | ||
|
|
||
| '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 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 | ||
|
|
||
| | 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) | |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| --- | ||
| 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. | ||
|
|
||
| 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, for the fallback when the PPA can't be used" | ||
| 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 | ||
169 changes: 169 additions & 0 deletions
169
.github/actions/setup-vagrant-libvirt/setup-vagrant-libvirt.bash
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.