Doing CI with GPUs and reliable runners without maintaining servers sounds expensive and complex, right? Well, it doesn't have to be. In this technical guide I show you step by step how to connect your GitHub Actions workflows so they run on Hugging Face Jobs, keeping GitHub as the orchestrator but using HF infrastructure to run the actual tasks.
Why you'd want this
GitHub Actions gives you a machine by default with runs-on: ubuntu-latest. Convenient, yes; perfect for everything, not really. Common limits:
- Latency or interruptions during GitHub maintenance.
- Generic images without the specific tooling you need.
- Almost no GPU access for many open source projects.
Can you imagine running the same test suite on a real GPU without maintaining your own runner? Trackio did it: they cut CPU job times by about 30% and added a GPU test suite that was previously impossible.
Basic architecture and core idea
The idea is simple and elegant: keep using GitHub Actions to define flows, but execute the steps in Hugging Face Jobs. To do that you build a bridge: a dispatcher that receives the workflow_job.queued webhook from the GitHub App and starts an HF Job that acts as an ephemeral runner.
Key components:
- A Space called
jobs-actions-dispatcherthat receives webhooks and launches Jobs. - A GitHub App that listens for queued jobs and creates temporary tokens to register runners.
- HF Jobs that launch containers with the required hardware (CPU or GPU).
From GitHub's point of view it looks like a self-hosted runner. From Hugging Face it's a Job that runs the workflow steps.
Step-by-step setup
- Duplicate the dispatcher Space
- Go to
huggingface/jobs-actions-dispatcherand duplicate the Space. - Owner: your HF user or organization.
- Name:
jobs-actions-dispatcher. - Hardware: use
cpu-upgradefor production;cpu-basicworks for tests but may go to sleep.
If you prefer the CLI:
export HF_NAMESPACE=your-username-or-org
export SPACE_ID="$HF_NAMESPACE/jobs-actions-dispatcher"
hf repo duplicate huggingface/jobs-actions-dispatcher "$SPACE_ID" \
--type space \
--flavor cpu-upgrade \
--exist-ok
export DISPATCHER_URL="https://$HF_NAMESPACE-jobs-actions-dispatcher.hf.space"
- Create and install the GitHub App
- Open the duplicated Space; the UI shows the webhook URL you need for the App.
- In the form enter the repo you want to connect: YOUR-GITHUB-ORG/YOUR-REPO.
- Create the GitHub App from the UI. The App needs permissions to listen to queued jobs and to create runner registration tokens.
- Upload the App credentials to the Space using the instructions the Space generates.
Important: store an HF_TOKEN in the Space secrets with permissions to launch Jobs; that token determines which namespace is billed for the Jobs.
- Optional: change billing namespace
If you want to bill Jobs to a different user or org:
export SPACE_ID=YOUR-HF-NAMESPACE/jobs-actions-dispatcher
hf spaces variables add "$SPACE_ID" -e HF_NAMESPACE=your-billing-namespace
hf spaces restart "$SPACE_ID"
- Change your GitHub workflows
The change is minimal. Replace runs-on: ubuntu-latest with a label managed by the dispatcher, for example:
- CPU:
runs-on: hf-jobs-cpu-upgrade - GPU:
runs-on: hf-jobs-t4-small
Minimal workflow example:
name: HF Jobs Test
on:
pull_request:
push:
branches: [main]
workflow_dispatch:
jobs:
test:
runs-on: hf-jobs-cpu-upgrade
steps:
- uses: actions/checkout@v4
- run: echo 'Hello from Hugging Face Jobs'
Then commit and push. The dispatcher will launch an HF Job that registers an ephemeral runner and executes the workflow.
Verification and useful commands
From the CLI you can check status with:
gh run list --repo YOUR-GITHUB-ORG/YOUR-REPO --limit 5
hf jobs ps --namespace "$HF_NAMESPACE"
hf spaces logs "$SPACE_ID"
Logs appear like in a normal Action, and you can also fetch them directly with hf jobs logs <job_id> > logs.txt to analyze locally.
Docker images and performance: practical lessons
Don't use an empty ubuntu:22.04 if you need tooling: you'll end up installing Playwright, Node, ffmpeg, sqlite, git and dependencies on every run.
Trackio switched to the Playwright image and a CUDA image for GPU:
- CPU image:
mcr.microsoft.com/playwright:v1.60.0-jammy - GPU image:
nvidia/cuda:12.4.0-runtime-ubuntu22.04
Results reported by Trackio:
| Runner setup | Runtime | Compared to GitHub average |
|---|---|---|
| GitHub ubuntu-latest baseline | 1m40s | baseline |
| HF Jobs CPU with Playwright image | 1m10s | -30s, ~30% faster |
| HF Jobs GPU, t4-small label | 45s | no GPU baseline on GitHub |
The key win was being able to run tests on real GPUs: 45s and costing less than a cent at the t4-small rate for that duration.
Best practices and tips
- Use a Docker image that already includes the tooling you need to avoid installing dependencies on every run.
- If you need datasets or models quickly, HF Jobs allows mounting volumes.
- Watch out for the dispatcher on
cpu-basicin production: it may sleep and leave workflows queued. - Keep the
HF_TOKENscoped minimally: permissions to launch Jobs only in the necessary namespace.
Small tip: in our bridge we also mirror GitHub logs into the HF Job and vice versa, so you don't lose context on either side.
Practical conclusion
If your project needs finer control over hardware, images, or access to accelerators, using Hugging Face Jobs as the backend for GitHub Actions is a practical, scalable solution. It requires an initial component (the dispatcher Space and the GitHub App), but daily integration is just changing runs-on to a label.
Want time savings, GPU access and easy-to-download logs? This flow can give you all that without the hassle of permanent runners.
