Module CI with GitHub Actions
This page assumes your module already has the Vox Pupuli test suite wired in, so that bundle exec rake validate lint works locally.
If it doesn’t yet, either set up voxpupuli-test by hand, or let Jig do it: jig new module for a new module, or jig convert from the root of an existing one, which overwrites Gemfile, Rakefile, and spec/spec_helper.rb with the same templates jig new module uses.
With that in place, the next step is to run the same tasks on every push and pull request.
Vox Pupuli publishes a set of reusable GitHub Actions workflows (gha-puppet) that do exactly that.
Nearly every module in the Vox Pupuli namespace uses them, and you can call them from your own repository with a workflow file of about ten lines.
This page covers what the workflow expects to find in your module, how to call it, and what to do instead if you only want a quick syntax check.
If you use GitLab CI or another runner, see Using VoxBox in CI instead.
What the workflow expects
gha-puppet doesn’t install tools or make assumptions about your module.
It checks out your repository, runs bundle install, and then calls the same rake tasks you run locally.
That means your module needs the same three files that the rest of the DevKit relies on:
-
A
Gemfilewith atestgroup containingvoxpupuli-testandpuppet_metadata, and anopenvoxgem line that reads its version from the environment. The workflow setsOPENVOX_GEM_VERSIONin every unit job, one job per OpenVox major release that therequirementsin yourmetadata.jsonallow, so each job installs the release it is testing.source ENV['GEM_SOURCE'] || 'https://rubygems.org' group :test do gem 'voxpupuli-test', '~> 14.0', :require => false gem 'puppet_metadata', '~> 6.1', :require => false end group :system_tests do gem 'voxpupuli-acceptance', '~> 4.4', :require => false end group :release do gem 'voxpupuli-release', '~> 5.3', :require => false end gem 'rake', :require => false gem 'openvox', ENV.fetch('OPENVOX_GEM_VERSION', [">= 7", "< 9"]), :require => false, :groups => [:test] -
A
Rakefilethat loads thevoxpupuli-testtasks. TheLoadErrorrescues let the same file work when only some gem groups are installed, which is how CI keeps the static-check job small.begin require 'voxpupuli/test/rake' rescue LoadError # only available if gem group test is installed end begin require 'voxpupuli/acceptance/rake' rescue LoadError # only available if gem group acceptance is installed end -
A
metadata.jsonwith arequirementsentry foropenvox(orpuppet; the tooling accepts either name) and, if you plan to add acceptance tests later, anoperatingsystem_supportlist. The workflow reads these to decide which OpenVox and Ruby versions to test against. See the module metadata reference for the full format.
If you scaffolded your module with jig new module, you already have all three.
If you ran jig convert (jig 2.4.0 or later) on an existing module, you have all three as well.
It works on PDK-generated and hand-maintained modules alike: when metadata.json is missing, convert creates it with an openvox requirements entry, pre-filled from a Modulefile if your module still carries one and completed from a short interview otherwise, and when the file exists but fails validation, convert repairs it.
Pass --dry-run to see what it would change first.
Adding the workflow
Create .github/workflows/ci.yml in your module:
name: CI
on:
pull_request: {}
push:
branches:
- main
concurrency:
group: $
cancel-in-progress: true
permissions:
contents: read
jobs:
puppet:
name: Puppet
uses: voxpupuli/gha-puppet/.github/workflows/basic.yml@v4
with:
# Set to false if your module doesn't use Rubocop.
rubocop: true
Commit it, push, and open a pull request. You’ll see three jobs under the Puppet heading:
| Job | What it runs |
|---|---|
| Static validations | bundle exec rake validate lint check, then rake rubocop (unless disabled), then metadata2gha to build the test matrix from metadata.json |
<version> (Ruby <version>) |
One job per supported OpenVox and Ruby combination, each running bundle exec rake parallel_spec |
| Test suite | A summary job that passes only if every job above passed. Use this one as your required status check in branch protection. |
The reusable workflow accepts a few inputs under with::
| Input | Default | Purpose |
|---|---|---|
rubocop |
true |
Run the rubocop task. Set to false if your module has no Ruby code or doesn’t inherit the Vox Pupuli Rubocop config. |
working-directory |
. |
Path to the module when it lives in a subdirectory, such as a control repository’s site-modules/. |
additional_packages |
'' |
Space-separated apt packages to install before the tests run, for gems with native extensions. |
timeout_minutes |
45 |
Cancel any job that runs longer than this. |
unit_runs_on |
ubuntu-24.04 |
Runner label for the unit-test jobs, for self-hosted runners. |
The gha-puppet README documents the full list, including the release workflow that Vox Pupuli uses to publish to the Forge.
Running the same checks locally
Everything the workflow does maps onto a rake task, so you can reproduce a CI failure without pushing:
bundle exec rake validate lint check rubocop
bundle exec rake parallel_spec
Or run them all at once with bundle exec rake test.
The pages on linting and unit testing explain what each task checks and how to configure it.
To test against a specific OpenVox release the way the matrix does, set the same environment variable the workflow uses:
OPENVOX_GEM_VERSION='~> 8.0' bundle install
bundle exec rake parallel_spec
Adding acceptance tests
When you’re ready to run your module against real operating systems, switch basic.yml to beaker.yml in the uses: line.
The beaker workflow runs everything above and then adds a job per platform listed in your metadata.json, using Podman containers on the GitHub-hosted runner (the beaker_hypervisor input defaults to container_podman).
The acceptance testing guide covers what the tests themselves look like.
If you only want a syntax check
The reusable workflow is the right tool for a module you maintain and publish.
For a one-off repository, or a module you’re only validating before applying it with puppet apply, you might want something lighter that doesn’t require adding a Gemfile and Rakefile.
This workflow installs OpenVox as a gem and runs the parser and linter directly:
name: Validate
on:
pull_request: {}
push:
branches:
- main
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
- run: gem install openvox puppet-lint
- name: Check syntax
run: find manifests -name '*.pp' -print0 | xargs -0 puppet parser validate
- name: Lint
run: puppet-lint manifests
Be aware of what this doesn’t catch.
puppet parser validate only checks that each file parses.
It won’t notice a class that doesn’t exist, a function from a module you forgot to declare as a dependency, or a template that renders garbage, because none of that is resolved until a catalog is compiled.
The voxpupuli-test unit tests catch all of those, which is why the reusable workflow is worth the extra setup as soon as the module matters.
Neither workflow applies your manifest to a machine. If that’s what you want to verify, acceptance tests are the supported way to do it.
Troubleshooting
The snippets on this page match gha-puppet v4 and the jig 2.5 templates as of October 2026.
If the workflow fails in a way this page doesn’t cover, compare your files against the gha-puppet repository, whose workflow files are the source of truth.
Could not locate Gemfile
The Static validations job fails on its first step, and every unit job shows as skipped:
Run bundle exec rake validate lint check
Could not locate Gemfile or .bundle/ directory
Error: Process completed with exit code 10.
Bundler 4 shortens the message to Could not locate Gemfile; the cause is the same.
Your repository has no Gemfile at the path the workflow is looking at.
Add one as described in What the workflow expects, or set working-directory if the module lives in a subdirectory.
unit → skipped [required to succeed]
The Test suite job reports the unit matrix as skipped.
This is never the root cause; it means the Static validations job failed before metadata2gha could produce a matrix.
Open that job’s log and fix the first error you see.
metadata2gha fails or produces an empty matrix
metadata.json is missing, isn’t valid JSON, or has no requirements entry for openvox or puppet.
Run bundle exec rake metadata_lint locally to see the specific complaint, or run jig convert, which creates a missing metadata.json and repairs one that fails validation.