How to run GitHub Actions locally¶
Running GitHub Actions locally provides a powerful way to develop, test, and debug CI/CD workflows. This approach offers faster feedback loops and greater control over the execution environment while maintaining compatibility with your existing GitHub Actions workflows.
The github-runner SDK lets a workshop act as a just-in-time runner
for GitHub workflow jobs.
Running jobs locally makes a few things easier:
Inspecting logs and other files after a failed run
Interactive debugging, profiling, and tracing
Testing with new, unusual, or expensive hardware
Shorter feedback loops while ensuring consistency with remote runs
Prerequisites¶
Before getting started, ensure you have:
Workshop installed and properly configured
A GitHub account with admin permissions on the target repository, or “self-hosted runners” permission for organization-level runners
Set up the workshop¶
To run GitHub Actions locally,
create or update your workshop definition
to include the github-runner SDK:
name: ci
base: ubuntu@24.04
sdks:
- name: github-runner
This installs the official Runner client
and an unofficial helper script named github-runner.
Don’t forget to launch or refresh the workshop.
Note
GitHub-hosted runners have a lot of preinstalled software, most of which isn’t included in workshops by default. If a workflow-based run fails because of missing software, we recommend installing it as part of the workflow. This makes local and remote runs more consistent. Some actions (e.g., setup-python) provide additional features like caching.
Some tools (notably Docker) aren’t as easy to install during a job,
but are available as SDKs.
Others (such as yq) are useful for development in addition to CI.
These can be sketched into an SDK alongside github-runner;
refer to the See also section for details.
Run a workflow locally¶
Now everything is set up to run a workflow locally.
Start the runner¶
Start the Runner client inside the workshop:
$ workshop exec ci github-runner --label=workshop <OWNER>[/<REPO>]
Replace <OWNER>/<REPO> with the full repository name
(e.g., canonical/workshop).
If omitted,
the script tries to detect this information from the local repository.
For organization-level runners,
make sure to provide the organization name (e.g., canonical).
The --label option adds a label to the runner,
to distinguish it from GitHub-hosted runners
and other self-hosted runners (if any).
Use --help to see the full list of options.
When the script runs for the first time, it will request authorization using a one-time code. Access can be revoked at any time via the GitHub App.
After a few seconds, the runner should be ready for incoming jobs. The next step is to configure jobs to use the runner.
Runner options¶
The github-runner command supports several options:
$ workshop exec ci github-runner --help
Key options include:
Option |
Description |
|---|---|
|
Specify a unique runner name |
|
Add a prefix to the runner name (defaults to hostname) |
|
Add custom labels to the runner |
|
Exit after running a single job |
|
Add runner to a specific runner group |
Configure your workflow¶
Add the workshop label
to the runs-on option in the workflow file.
Consider making this configurable, if only to avoid repeatedly editing the workflow. For example:
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
inputs:
runner:
description: Where to run the job
type: choice
required: true
options: [ubuntu-latest, workshop]
default: ubuntu-latest
jobs:
test:
runs-on: ["${{ inputs.runner || 'ubuntu-latest' }}"]
steps:
- uses: actions/checkout@v6
- run: make test
Run the workflow¶
The specific steps depend on the workflow.
For the above example:
commit the updated workflow to the main branch,
find it in the Actions tab of the repository,
and select Run workflow.
Pick whichever branch you like,
as long as the runner is set to workshop.
The Runner client should print a few logs when a job starts and finishes. Full logs can still be viewed on GitHub.
Tips¶
Take care when logging. Some actions could leak sensitive information about the runner, such as its IP address.
The Runner client runs one job at a time. To run several jobs in parallel, use multiple workshops. For example:
$ mkdir -p .workshop
$ mv workshop.yaml .workshop/ci.yaml
$ sed 's/name: ci/name: ci2/' <.workshop/ci.yaml >.workshop/ci2.yaml
$ workshop launch ci2
$ workshop exec ci2 github-runner --label=workshop
The Runner client doesn’t clean up after itself. This can be helpful for debugging but may cause issues for some workflows. To avoid these issues, refresh the workshop after each job. For example:
$ while workshop exec ci github-runner --label=workshop --once; do
workshop refresh ci
done
In rare cases (like a power outage at the wrong time), runners can remain attached to the repository indefinitely. These can be removed manually in the repository or organization settings.
For quick iteration, the runner can be made conditional on the branch name:
on:
push:
branches:
- main
- workshop-runner/**
jobs:
test:
runs-on: ["${{ startsWith(github.ref_name, 'workshop-runner/') && 'workshop' || 'ubuntu-latest' }}"]
steps:
- uses: actions/checkout@v6
- run: make test
Security considerations¶
When running actions locally:
Be cautious with secrets and sensitive data
Mind that actions may leak information about your local environment
Consider using separate workshops for different projects
Regularly review and rotate access tokens
Monitor actions for unexpected behavior
See also¶
How-to guides:
Tutorial: