{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-09-migrating-your-github-ci-to-hugging-face-jobs/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "If you have a GitHub repository and you have GitHub Actions enabled, you probably use GitHub-hosted runners for CI. That is the default for many projects because it is simple: add a workflow, write `runs-on: ubuntu-latest`, and GitHub gives you a machine.\n\nThat default is convenient, but it also has limits. GitHub Actions can be slow or down for maintenance, the hosted machines are generic, and GPU access is not something most open-source projects can just turn on. For [Trackio](https://github.com/gradio-app/trackio), those limits started to matter. We wanted both reliable CPU CI for basic unit tests and frontend checks, but also GPU CI for tests that need to run on actual CUDA hardware.\n\nSo built an alternative: keep GitHub Actions in charge of CI, but run the jobs on [Hugging Face Jobs](https://huggingface.co/docs/hub/en/jobs-overview).\n\nThe result: Trackio's CI now runs on Hugging Face Jobs and streams back real-time logs, **cutting our CI time for CPU jobs by about 30% and enabling a whole new test suite that runs on GPU machines**!\n\nIn this article, we explain step-by-step how to recreate the same setup for your GitHub repo. If you are using an agent, you can point it to this article, since we provide CLI instructions alongside browser-based instructions for us humans.\n\nLet's start with a quick intro to Hugging Face Jobs!\n\n## What is Hugging Face Jobs?\n\n[Hugging Face Jobs](https://huggingface.co/docs/hub/en/jobs-overview) lets you run commands or scripts on Hugging Face's serverless infrastructure with almost any hardware flavor. A Job is essentially:\n\n- a command to run\n- a Docker image, from Docker Hub or a Hugging Face Space\n- a hardware flavor, such as CPU or `t4-small` or `h200` GPU\n- optional environment variables and secrets\n\nFor example, you can run:\n\n```bash\nhf jobs run python:3.12 python -c \"print('Hello world')\"\n```\n\nor\n\n```bash\nhf jobs uv run --flavor a10g-small \"https://raw.githubusercontent.com/huggingface/trl/main/trl/scripts/sft.py\" \n```\n\nThat makes Jobs a natural fit for CI. CI jobs are already command-driven, already run in clean environments, and often benefit from choosing exactly the right hardware. For ML libraries, the GPU case is especially compelling: you can run a test suite on real GPU hardware without maintaining your own always-on runner.\n\nThe key step is connecting GitHub Actions to HF Jobs, which we describe below.\n\n## The architecture\n\nFor this setup, we created [`huggingface/jobs-actions`](https://github.com/huggingface/jobs-actions), a small bridge that turns a GitHub Actions job into an ephemeral self-hosted runner running inside an HF Job.\n\nThe complete flow looks like this:\n\n1. A pull request triggers a GitHub Actions workflow.\n2. GitHub queues any job whose `runs-on` label is not available, for example `hf-jobs-cpu-upgrade` or `hf-jobs-t4-small`, and sends a signed `workflow_job.queued` webhook to the dispatcher through the GitHub App.\n3. The dispatcher Space verifies the webhook, checks for an `hf-jobs-*` label, mints a short-lived GitHub runner registration token, and starts an HF Job on the matching hardware.\n4. The HF Job boots an ephemeral GitHub Actions runner and registers it with the repo using that one-shot token.\n5. GitHub assigns the pending workflow job to that runner; the runner executes the CI job, reports status back to GitHub, and exits.\n\nFrom GitHub's point of view, this is just a self-hosted runner. From Hugging Face's point of view, it is just a Job that launches a container to run the workflow steps from the repo’s GitHub Actions.\n\n## Step 1: Duplicate the dispatcher Space\n\nThe first thing you need is the dispatcher. This is a small Docker Space that receives GitHub `workflow_job` webhook events and launches HF Jobs in response.\n\nCreate this first because the GitHub App needs a webhook URL, and that URL comes from the Space. This Space should be under your own namespace or under a Hugging Face org that you have write access to.\n\n#### Web setup\n\nGo to [`huggingface/jobs-actions-dispatcher`](https://huggingface.co/spaces/huggingface/jobs-actions-dispatcher) and click **Duplicate this Space**.\n\n![image](https://github.com/user-attachments/assets/c8b450c3-b801-43dc-97ff-954d9bbaf975)\n\nUse:\n\n```text\nOwner: your HF user or org\nName: jobs-actions-dispatcher\nHardware: cpu-upgrade\n```\n\nUse `cpu-upgrade` for real CI so the dispatcher stays available for GitHub webhooks. `cpu-basic` is fine for testing and will probably work, but it can sleep after inactivity; if GitHub's webhook arrives while it is waking up, the workflow may stay queued forever.\n\nAfter it builds, open the duplicated Space. You will see a section that says \"Required Space secrets,\" which you can ignore for now. The landing page should display the GitHub App webhook URL you need in the next step. It will look like this:\n\n```text\nhttps://YOUR-HF-NAMESPACE-jobs-actions-dispatcher.hf.space/webhook\n```\n\n#### CLI setup\n\nIf you'd prefer to set up the dispatcher Space with an agent or use a CLI workflow:\n\n```bash\nexport HF_NAMESPACE=your-hf-user-or-org\nexport SPACE_ID=\"$HF_NAMESPACE/jobs-actions-dispatcher\"\n\nhf repo duplicate huggingface/jobs-actions-dispatcher \"$SPACE_ID\" \\\n  --type space \\\n  --flavor cpu-upgrade \\\n  --exist-ok\n```\n\nThen set:\n\n```bash\nexport DISPATCHER_URL=\"https://${HF_NAMESPACE}-jobs-actions-dispatcher.hf.space\"\n```\n\n## Step 2: Create and install the GitHub App\n\nNext, create and install the GitHub App from the dispatcher Space itself. This App needs permission to listen for queued workflow jobs and create ephemeral self-hosted runner registration tokens.\n\n### Web setup\n\nOpen your duplicated dispatcher Space:\n\n```text\nhttps://YOUR-HF-NAMESPACE-jobs-actions-dispatcher.hf.space\n```\n\nIn the setup form, enter the GitHub repo whose CI should run on HF Jobs:\n\n```text\nYOUR-GITHUB-ORG/YOUR-REPO\n```\n\nThen click the button to create the GitHub App. GitHub will ask you to choose a name for the App; the name can be anything, as long as it is available in your GitHub account or org. After you submit, the final screen tells you exactly how to upload the App credentials to the dispatcher Space with the `hf` CLI.\n\n**Important note**: you will need to provide an [Hugging Face token](https://huggingface.co/settings/tokens) that has permissions to launch Jobs, corresponding to your personal account or an org under which Jobs should be charged. This token should be saved as the `HF_TOKEN` secret in your dispatcher Space.\n\nFinally, you will install the App on the same GitHub repo you entered in the Space. In the Trackio setup, we installed it on `gradio-app/trackio`.\n\n### Agent-assisted setup\n\nThe GitHub App manifest flow is still browser-based, but an agent can follow the same Space-driven path:\n\n```bash\nexport HF_NAMESPACE=your-hf-user-or-org\nexport GITHUB_REPO=YOUR-GITHUB-ORG/YOUR-REPO\nopen \"https://${HF_NAMESPACE}-jobs-actions-dispatcher.hf.space\"\n```\n\nPaste `$GITHUB_REPO` into the Space, click the GitHub App creation button, choose any available App name, and follow the generated GitHub instructions.\n\nAfter the App exists, install it on your repo from the App settings page. For a GitHub org, the installation settings are under:\n\n```text\nhttps://github.com/organizations/YOUR-GITHUB-ORG/settings/installations\n```\n\n## Step 3: Final dispatcher settings\n\nAt this point, the dispatcher Space should be configured. The GitHub App setup flow generated the commands that upload the App credentials, webhook secret, and Hugging Face token to the Space.\n\n![image](https://github.com/user-attachments/assets/0fc8ac73-f93a-419b-bd80-70da2756f50c)\n\nBy default, HF Jobs are launched under the same namespace as the dispatcher Space. Optionally, set `HF_NAMESPACE` as a Space variable if you want to bill jobs to a different Hugging Face user or org:\n\n```bash\nexport SPACE_ID=YOUR-HF-NAMESPACE/jobs-actions-dispatcher\nhf spaces variables add \"$SPACE_ID\" -e HF_NAMESPACE=your-billing-namespace\nhf spaces restart \"$SPACE_ID\"\n```\n\nThe token you set in Step 2 should correspond to this namespace.\n\n## Step 4: Change `runs-on`\n\nThe actual workflow change is small. Instead of:\n\n```yaml\nruns-on: ubuntu-latest\n```\n\nuse one of the labels handled by the dispatcher:\n\n```yaml\nruns-on: hf-jobs-cpu-upgrade\n```\n\nFor GPU tests, use a GPU label:\n\n```yaml\nruns-on: hf-jobs-t4-small\n```\n\nFor any GitHub Action you'd like to run on HF Jobs, this 1-line change is all you need!\n\n## Step 5: Test it out\n\nTo add a minimal smoke-test workflow from the CLI:\n\n```bash\nmkdir -p .github/workflows\ncat > .github/workflows/hf-jobs-test.yml <<'EOF'\nname: HF Jobs Test\n\non:\n  pull_request:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: hf-jobs-cpu-upgrade\n    steps:\n      - uses: actions/checkout@v4\n      - run: echo \"Hello from Hugging Face Jobs\"\nEOF\n\ngit add .github/workflows/hf-jobs-test.yml\ngit commit -m \"Run CI on Hugging Face Jobs\"\ngit push\n```\n\nTo verify from the CLI:\n\n```bash\ngh run list --repo YOUR-GITHUB-ORG/YOUR-REPO --limit 5\nhf jobs ps --namespace \"$HF_NAMESPACE\"\nhf spaces logs \"$SPACE_ID\"\n```\n\nYou should be able to see logs just like a regular GitHub Action—for example, in this [Trackio PR #565](https://github.com/gradio-app/trackio/pull/565).\n\nAnd that's it!\n\n*Note on choosing the right Docker image*\n\nOur first CPU setup used `ubuntu:22.04` and installed missing system packages during every run. That worked, but it was slower than it needed to be. GitHub's `ubuntu-latest` image includes a lot of developer tooling by default; a bare Ubuntu image does not.\n\nFor Trackio, the UI tests need Playwright browsers, Node, ffmpeg, sqlite, git, and normal Linux build dependencies. Hugging Face Jobs supports using any [Docker image](https://huggingface.co/docs/hub/jobs-popular-images), so we switched to the Microsoft Playwright image, which worked well:\n\n```text\nmcr.microsoft.com/playwright:v1.60.0-jammy\n```\n\nFor GPU jobs, we used:\n\n```text\nnvidia/cuda:12.4.0-runtime-ubuntu22.04\n```\n\n## Results\n\nHere are the numbers from the Trackio CI:\n\n| Runner setup                    | Runtime | Compared to GitHub average    |\n| ------------------------------- | ------- | ----------------------------- |\n| GitHub `ubuntu-latest` baseline | `1m40s` | baseline                      |\n| HF Jobs CPU, Playwright image   | `1m10s` | `-30s`, about `30%` faster    |\n| HF Jobs GPU, `t4-small` label   | `45s`   | no GitHub-hosted GPU baseline |\n\nThe biggest win was GPU CI. The Trackio GPU check ran on HF Jobs and passed in `45s`, costing less than a cent at the `t4-small` rate for that duration.\n\nThe CPU result was also encouraging. With the right image, the Linux test job was faster than the GitHub-hosted baseline. That suggests HF Jobs can be a practical CI backend, especially for ML projects that need custom images or accelerators.\n\nLogs were another pleasant surprise. GitHub Actions logs are useful, but the web UI can be heavy for large logs. HF Jobs logs are easy to fetch from the CLI:\n\n```bash\nhf jobs logs <job_id> > logs.txt\n```\n\nThat makes them easy to inspect with local tools or coding agents. In our bridge, we also mirrored the GitHub Actions job log into the HF Job log, so either system had enough information to debug a run.\n\nFinally, although we didn't need them for Trackio's CI, HF Jobs also [supports mounting volumes](https://huggingface.co/docs/huggingface_hub/en/guides/jobs#mount-a-volume), which can be very helpful if you need to load datasets or models from Hugging Face quickly as part of your CI.\n\nHopefully, this gives you all you need to try HF Jobs for running your GitHub Actions!\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<p>If you have a GitHub repository and you have GitHub Actions enabled, you probably use GitHub-hosted runners for CI. That is the default for many projects because it is simple: add a workflow, write <code>runs-on: ubuntu-latest</code>, and GitHub gives you a machine.</p>\n<p>That default is convenient, but it also has limits. GitHub Actions can be slow or down for maintenance, the hosted machines are generic, and GPU access is not something most open-source projects can just turn on. For <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/gradio-app/trackio\">Trackio</a>, those limits started to matter. We wanted both reliable CPU CI for basic unit tests and frontend checks, but also GPU CI for tests that need to run on actual CUDA hardware.</p>\n<p>So built an alternative: keep GitHub Actions in charge of CI, but run the jobs on <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/hub/en/jobs-overview\">Hugging Face Jobs</a>.</p>\n<p>The result: Trackio's CI now runs on Hugging Face Jobs and streams back real-time logs, <strong>cutting our CI time for CPU jobs by about 30% and enabling a whole new test suite that runs on GPU machines</strong>!</p>\n<p>In this article, we explain step-by-step how to recreate the same setup for your GitHub repo. If you are using an agent, you can point it to this article, since we provide CLI instructions alongside browser-based instructions for us humans.</p>\n<p>Let's start with a quick intro to Hugging Face Jobs!</p>\n<h2>What is Hugging Face Jobs?</h2>\n<p><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/hub/en/jobs-overview\">Hugging Face Jobs</a> lets you run commands or scripts on Hugging Face's serverless infrastructure with almost any hardware flavor. A Job is essentially:</p>\n<ul>\n<li>a command to run</li>\n<li>a Docker image, from Docker Hub or a Hugging Face Space</li>\n<li>a hardware flavor, such as CPU or <code>t4-small</code> or <code>h200</code> GPU</li>\n<li>optional environment variables and secrets</li>\n</ul>\n<p>For example, you can run:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> jobs run python:3.12 python<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>c</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>print(&#39;Hello world&#39;)<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n</span></span></code></pre>\n<p>or</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> jobs uv run<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>flavor</span> a10g-small <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>https://raw.githubusercontent.com/huggingface/trl/main/trl/scripts/sft.py<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> </span>\n</span></span></code></pre>\n<p>That makes Jobs a natural fit for CI. CI jobs are already command-driven, already run in clean environments, and often benefit from choosing exactly the right hardware. For ML libraries, the GPU case is especially compelling: you can run a test suite on real GPU hardware without maintaining your own always-on runner.</p>\n<p>The key step is connecting GitHub Actions to HF Jobs, which we describe below.</p>\n<h2>The architecture</h2>\n<p>For this setup, we created <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/huggingface/jobs-actions\"><code>huggingface/jobs-actions</code></a>, a small bridge that turns a GitHub Actions job into an ephemeral self-hosted runner running inside an HF Job.</p>\n<p>The complete flow looks like this:</p>\n<ol>\n<li>A pull request triggers a GitHub Actions workflow.</li>\n<li>GitHub queues any job whose <code>runs-on</code> label is not available, for example <code>hf-jobs-cpu-upgrade</code> or <code>hf-jobs-t4-small</code>, and sends a signed <code>workflow_job.queued</code> webhook to the dispatcher through the GitHub App.</li>\n<li>The dispatcher Space verifies the webhook, checks for an <code>hf-jobs-*</code> label, mints a short-lived GitHub runner registration token, and starts an HF Job on the matching hardware.</li>\n<li>The HF Job boots an ephemeral GitHub Actions runner and registers it with the repo using that one-shot token.</li>\n<li>GitHub assigns the pending workflow job to that runner; the runner executes the CI job, reports status back to GitHub, and exits.</li>\n</ol>\n<p>From GitHub's point of view, this is just a self-hosted runner. From Hugging Face's point of view, it is just a Job that launches a container to run the workflow steps from the repo’s GitHub Actions.</p>\n<h2>Step 1: Duplicate the dispatcher Space</h2>\n<p>The first thing you need is the dispatcher. This is a small Docker Space that receives GitHub <code>workflow_job</code> webhook events and launches HF Jobs in response.</p>\n<p>Create this first because the GitHub App needs a webhook URL, and that URL comes from the Space. This Space should be under your own namespace or under a Hugging Face org that you have write access to.</p>\n<h4>Web setup</h4>\n<p>Go to <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/spaces/huggingface/jobs-actions-dispatcher\"><code>huggingface/jobs-actions-dispatcher</code></a> and click <strong>Duplicate this Space</strong>.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://github.com/user-attachments/assets/c8b450c3-b801-43dc-97ff-954d9bbaf975\" alt=\"image\" loading=\"eager\" decoding=\"async\" fetchpriority=\"high\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>Use:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">Owner: your HF user or org\nName: jobs-actions-dispatcher\nHardware: cpu-upgrade\n</span></code></pre>\n<p>Use <code>cpu-upgrade</code> for real CI so the dispatcher stays available for GitHub webhooks. <code>cpu-basic</code> is fine for testing and will probably work, but it can sleep after inactivity; if GitHub's webhook arrives while it is waking up, the workflow may stay queued forever.</p>\n<p>After it builds, open the duplicated Space. You will see a section that says &quot;Required Space secrets,&quot; which you can ignore for now. The landing page should display the GitHub App webhook URL you need in the next step. It will look like this:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">https://YOUR-HF-NAMESPACE-jobs-actions-dispatcher.hf.space/webhook\n</span></code></pre>\n<h4>CLI setup</h4>\n<p>If you'd prefer to set up the dispatcher Space with an agent or use a CLI workflow:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-storage syntax-modifier syntax-shell\">export</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">HF_NAMESPACE</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\">your-hf-user-or-org</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-storage syntax-modifier syntax-shell\">export</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">SPACE_ID</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\"><span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">HF_NAMESPACE</span></span>/jobs-actions-dispatcher<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span></span>\n\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> repo duplicate huggingface/jobs-actions-dispatcher <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SPACE_ID</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>type</span> space <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>flavor</span> cpu-upgrade <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>exist-ok</span></span>\n</span></span></code></pre>\n<p>Then set:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-storage syntax-modifier syntax-shell\">export</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">DISPATCHER_URL</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\"><span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>https://<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell\">{</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">HF_NAMESPACE</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell\">}</span></span>-jobs-actions-dispatcher.hf.space<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span></span>\n</span></span></code></pre>\n<h2>Step 2: Create and install the GitHub App</h2>\n<p>Next, create and install the GitHub App from the dispatcher Space itself. This App needs permission to listen for queued workflow jobs and create ephemeral self-hosted runner registration tokens.</p>\n<h3>Web setup</h3>\n<p>Open your duplicated dispatcher Space:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">https://YOUR-HF-NAMESPACE-jobs-actions-dispatcher.hf.space\n</span></code></pre>\n<p>In the setup form, enter the GitHub repo whose CI should run on HF Jobs:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">YOUR-GITHUB-ORG/YOUR-REPO\n</span></code></pre>\n<p>Then click the button to create the GitHub App. GitHub will ask you to choose a name for the App; the name can be anything, as long as it is available in your GitHub account or org. After you submit, the final screen tells you exactly how to upload the App credentials to the dispatcher Space with the <code>hf</code> CLI.</p>\n<p><strong>Important note</strong>: you will need to provide an <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/settings/tokens\">Hugging Face token</a> that has permissions to launch Jobs, corresponding to your personal account or an org under which Jobs should be charged. This token should be saved as the <code>HF_TOKEN</code> secret in your dispatcher Space.</p>\n<p>Finally, you will install the App on the same GitHub repo you entered in the Space. In the Trackio setup, we installed it on <code>gradio-app/trackio</code>.</p>\n<h3>Agent-assisted setup</h3>\n<p>The GitHub App manifest flow is still browser-based, but an agent can follow the same Space-driven path:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-storage syntax-modifier syntax-shell\">export</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">HF_NAMESPACE</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\">your-hf-user-or-org</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-storage syntax-modifier syntax-shell\">export</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">GITHUB_REPO</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\">YOUR-GITHUB-ORG/YOUR-REPO</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">open</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>https://<span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-begin syntax-shell\">{</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">HF_NAMESPACE</span></span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-section syntax-expansion syntax-parameter syntax-end syntax-shell\">}</span></span>-jobs-actions-dispatcher.hf.space<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n</span></span></code></pre>\n<p>Paste <code>$GITHUB_REPO</code> into the Space, click the GitHub App creation button, choose any available App name, and follow the generated GitHub instructions.</p>\n<p>After the App exists, install it on your repo from the App settings page. For a GitHub org, the installation settings are under:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">https://github.com/organizations/YOUR-GITHUB-ORG/settings/installations\n</span></code></pre>\n<h2>Step 3: Final dispatcher settings</h2>\n<p>At this point, the dispatcher Space should be configured. The GitHub App setup flow generated the commands that upload the App credentials, webhook secret, and Hugging Face token to the Space.</p>\n<p><picture class=\"article-picture article-picture-fallback\" style=\"--image-ratio:16 / 9\"><img src=\"https://github.com/user-attachments/assets/0fc8ac73-f93a-419b-bd80-70da2756f50c\" alt=\"image\" loading=\"lazy\" decoding=\"async\" fetchpriority=\"low\" referrerpolicy=\"no-referrer\" class=\"progressive-image\"></picture></p>\n<p>By default, HF Jobs are launched under the same namespace as the dispatcher Space. Optionally, set <code>HF_NAMESPACE</code> as a Space variable if you want to bill jobs to a different Hugging Face user or org:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-storage syntax-modifier syntax-shell\">export</span> <span class=\"syntax-variable syntax-other syntax-readwrite syntax-assignment syntax-shell\">SPACE_ID</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-shell\">=</span><span class=\"syntax-string syntax-unquoted syntax-shell\">YOUR-HF-NAMESPACE/jobs-actions-dispatcher</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> spaces variables add <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SPACE_ID</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>e</span> HF_NAMESPACE=your-billing-namespace</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> spaces restart <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SPACE_ID</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n</span></span></code></pre>\n<p>The token you set in Step 2 should correspond to this namespace.</p>\n<h2>Step 4: Change <code>runs-on</code></h2>\n<p>The actual workflow change is small. Instead of:</p>\n<pre><code class=\"language-yaml\"><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">runs-on</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">ubuntu-latest</span>\n</span></span></code></pre>\n<p>use one of the labels handled by the dispatcher:</p>\n<pre><code class=\"language-yaml\"><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">runs-on</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">hf-jobs-cpu-upgrade</span>\n</span></span></code></pre>\n<p>For GPU tests, use a GPU label:</p>\n<pre><code class=\"language-yaml\"><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">runs-on</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">hf-jobs-t4-small</span>\n</span></span></code></pre>\n<p>For any GitHub Action you'd like to run on HF Jobs, this 1-line change is all you need!</p>\n<h2>Step 5: Test it out</h2>\n<p>To add a minimal smoke-test workflow from the CLI:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">mkdir</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span> .github/workflows</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">cat</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell\">&gt;</span> .github/workflows/hf-jobs-test.yml <span class=\"syntax-string syntax-unquoted syntax-heredoc syntax-shell\"><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell\">&lt;&lt;</span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&#39;</span><span class=\"syntax-keyword syntax-control syntax-heredoc-token syntax-shell\">EOF</span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&#39;</span></span><span class=\"syntax-string syntax-unquoted syntax-heredoc syntax-shell\">\nname: HF Jobs Test\n\non:\n  pull_request:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: hf-jobs-cpu-upgrade\n    steps:\n      - uses: actions/checkout@v4\n      - run: echo &quot;Hello from Hugging Face Jobs&quot;\n<span class=\"syntax-keyword syntax-control syntax-heredoc-token syntax-shell\">EOF</span></span></span>\n\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">git</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> add .github/workflows/hf-jobs-test.yml</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">git</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> commit<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>m</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span>Run CI on Hugging Face Jobs<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">git</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> push</span>\n</span></span></code></pre>\n<p>To verify from the CLI:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">gh</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> run list<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>repo</span> YOUR-GITHUB-ORG/YOUR-REPO<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>limit</span> 5</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> jobs ps<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>namespace</span> <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">HF_NAMESPACE</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> spaces logs <span class=\"syntax-string syntax-quoted syntax-double syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-shell\">&quot;</span><span class=\"syntax-meta syntax-group syntax-expansion syntax-parameter syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-variable syntax-shell\">$</span><span class=\"syntax-variable syntax-other syntax-readwrite syntax-shell\">SPACE_ID</span></span><span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-shell\">&quot;</span></span></span>\n</span></span></code></pre>\n<p>You should be able to see logs just like a regular GitHub Action—for example, in this <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/gradio-app/trackio/pull/565\">Trackio PR #565</a>.</p>\n<p>And that's it!</p>\n<p><em>Note on choosing the right Docker image</em></p>\n<p>Our first CPU setup used <code>ubuntu:22.04</code> and installed missing system packages during every run. That worked, but it was slower than it needed to be. GitHub's <code>ubuntu-latest</code> image includes a lot of developer tooling by default; a bare Ubuntu image does not.</p>\n<p>For Trackio, the UI tests need Playwright browsers, Node, ffmpeg, sqlite, git, and normal Linux build dependencies. Hugging Face Jobs supports using any <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/hub/jobs-popular-images\">Docker image</a>, so we switched to the Microsoft Playwright image, which worked well:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">mcr.microsoft.com/playwright:v1.60.0-jammy\n</span></code></pre>\n<p>For GPU jobs, we used:</p>\n<pre><code class=\"language-text\"><span class=\"code-snippet\">nvidia/cuda:12.4.0-runtime-ubuntu22.04\n</span></code></pre>\n<h2>Results</h2>\n<p>Here are the numbers from the Trackio CI:</p>\n<table>\n<thead>\n<tr>\n<th>Runner setup</th>\n<th>Runtime</th>\n<th>Compared to GitHub average</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>GitHub <code>ubuntu-latest</code> baseline</td>\n<td><code>1m40s</code></td>\n<td>baseline</td>\n</tr>\n<tr>\n<td>HF Jobs CPU, Playwright image</td>\n<td><code>1m10s</code></td>\n<td><code>-30s</code>, about <code>30%</code> faster</td>\n</tr>\n<tr>\n<td>HF Jobs GPU, <code>t4-small</code> label</td>\n<td><code>45s</code></td>\n<td>no GitHub-hosted GPU baseline</td>\n</tr>\n</tbody>\n</table>\n<p>The biggest win was GPU CI. The Trackio GPU check ran on HF Jobs and passed in <code>45s</code>, costing less than a cent at the <code>t4-small</code> rate for that duration.</p>\n<p>The CPU result was also encouraging. With the right image, the Linux test job was faster than the GitHub-hosted baseline. That suggests HF Jobs can be a practical CI backend, especially for ML projects that need custom images or accelerators.</p>\n<p>Logs were another pleasant surprise. GitHub Actions logs are useful, but the web UI can be heavy for large logs. HF Jobs logs are easy to fetch from the CLI:</p>\n<pre><code class=\"language-bash\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">hf</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> jobs logs <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell\">&lt;</span>job_id<span class=\"syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell\">&gt;</span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-redirection syntax-shell\">&gt;</span> logs.txt</span>\n</span></span></code></pre>\n<p>That makes them easy to inspect with local tools or coding agents. In our bridge, we also mirrored the GitHub Actions job log into the HF Job log, so either system had enough information to debug a run.</p>\n<p>Finally, although we didn't need them for Trackio's CI, HF Jobs also <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/huggingface_hub/en/guides/jobs#mount-a-volume\">supports mounting volumes</a>, which can be very helpful if you need to load datasets or models from Hugging Face quickly as part of your CI.</p>\n<p>Hopefully, this gives you all you need to try HF Jobs for running your GitHub Actions!</p>\n",
  "content_markdown": "If you have a GitHub repository and you have GitHub Actions enabled, you probably use GitHub-hosted runners for CI. That is the default for many projects because it is simple: add a workflow, write `runs-on: ubuntu-latest`, and GitHub gives you a machine.\n\nThat default is convenient, but it also has limits. GitHub Actions can be slow or down for maintenance, the hosted machines are generic, and GPU access is not something most open-source projects can just turn on. For [Trackio](https://github.com/gradio-app/trackio), those limits started to matter. We wanted both reliable CPU CI for basic unit tests and frontend checks, but also GPU CI for tests that need to run on actual CUDA hardware.\n\nSo built an alternative: keep GitHub Actions in charge of CI, but run the jobs on [Hugging Face Jobs](https://huggingface.co/docs/hub/en/jobs-overview).\n\nThe result: Trackio's CI now runs on Hugging Face Jobs and streams back real-time logs, **cutting our CI time for CPU jobs by about 30% and enabling a whole new test suite that runs on GPU machines**!\n\nIn this article, we explain step-by-step how to recreate the same setup for your GitHub repo. If you are using an agent, you can point it to this article, since we provide CLI instructions alongside browser-based instructions for us humans.\n\nLet's start with a quick intro to Hugging Face Jobs!\n\n## What is Hugging Face Jobs?\n\n[Hugging Face Jobs](https://huggingface.co/docs/hub/en/jobs-overview) lets you run commands or scripts on Hugging Face's serverless infrastructure with almost any hardware flavor. A Job is essentially:\n\n- a command to run\n- a Docker image, from Docker Hub or a Hugging Face Space\n- a hardware flavor, such as CPU or `t4-small` or `h200` GPU\n- optional environment variables and secrets\n\nFor example, you can run:\n\n```bash\nhf jobs run python:3.12 python -c \"print('Hello world')\"\n```\n\nor\n\n```bash\nhf jobs uv run --flavor a10g-small \"https://raw.githubusercontent.com/huggingface/trl/main/trl/scripts/sft.py\" \n```\n\nThat makes Jobs a natural fit for CI. CI jobs are already command-driven, already run in clean environments, and often benefit from choosing exactly the right hardware. For ML libraries, the GPU case is especially compelling: you can run a test suite on real GPU hardware without maintaining your own always-on runner.\n\nThe key step is connecting GitHub Actions to HF Jobs, which we describe below.\n\n## The architecture\n\nFor this setup, we created [`huggingface/jobs-actions`](https://github.com/huggingface/jobs-actions), a small bridge that turns a GitHub Actions job into an ephemeral self-hosted runner running inside an HF Job.\n\nThe complete flow looks like this:\n\n1. A pull request triggers a GitHub Actions workflow.\n2. GitHub queues any job whose `runs-on` label is not available, for example `hf-jobs-cpu-upgrade` or `hf-jobs-t4-small`, and sends a signed `workflow_job.queued` webhook to the dispatcher through the GitHub App.\n3. The dispatcher Space verifies the webhook, checks for an `hf-jobs-*` label, mints a short-lived GitHub runner registration token, and starts an HF Job on the matching hardware.\n4. The HF Job boots an ephemeral GitHub Actions runner and registers it with the repo using that one-shot token.\n5. GitHub assigns the pending workflow job to that runner; the runner executes the CI job, reports status back to GitHub, and exits.\n\nFrom GitHub's point of view, this is just a self-hosted runner. From Hugging Face's point of view, it is just a Job that launches a container to run the workflow steps from the repo’s GitHub Actions.\n\n## Step 1: Duplicate the dispatcher Space\n\nThe first thing you need is the dispatcher. This is a small Docker Space that receives GitHub `workflow_job` webhook events and launches HF Jobs in response.\n\nCreate this first because the GitHub App needs a webhook URL, and that URL comes from the Space. This Space should be under your own namespace or under a Hugging Face org that you have write access to.\n\n#### Web setup\n\nGo to [`huggingface/jobs-actions-dispatcher`](https://huggingface.co/spaces/huggingface/jobs-actions-dispatcher) and click **Duplicate this Space**.\n\n![image](https://github.com/user-attachments/assets/c8b450c3-b801-43dc-97ff-954d9bbaf975)\n\nUse:\n\n```text\nOwner: your HF user or org\nName: jobs-actions-dispatcher\nHardware: cpu-upgrade\n```\n\nUse `cpu-upgrade` for real CI so the dispatcher stays available for GitHub webhooks. `cpu-basic` is fine for testing and will probably work, but it can sleep after inactivity; if GitHub's webhook arrives while it is waking up, the workflow may stay queued forever.\n\nAfter it builds, open the duplicated Space. You will see a section that says \"Required Space secrets,\" which you can ignore for now. The landing page should display the GitHub App webhook URL you need in the next step. It will look like this:\n\n```text\nhttps://YOUR-HF-NAMESPACE-jobs-actions-dispatcher.hf.space/webhook\n```\n\n#### CLI setup\n\nIf you'd prefer to set up the dispatcher Space with an agent or use a CLI workflow:\n\n```bash\nexport HF_NAMESPACE=your-hf-user-or-org\nexport SPACE_ID=\"$HF_NAMESPACE/jobs-actions-dispatcher\"\n\nhf repo duplicate huggingface/jobs-actions-dispatcher \"$SPACE_ID\" \\\n  --type space \\\n  --flavor cpu-upgrade \\\n  --exist-ok\n```\n\nThen set:\n\n```bash\nexport DISPATCHER_URL=\"https://${HF_NAMESPACE}-jobs-actions-dispatcher.hf.space\"\n```\n\n## Step 2: Create and install the GitHub App\n\nNext, create and install the GitHub App from the dispatcher Space itself. This App needs permission to listen for queued workflow jobs and create ephemeral self-hosted runner registration tokens.\n\n### Web setup\n\nOpen your duplicated dispatcher Space:\n\n```text\nhttps://YOUR-HF-NAMESPACE-jobs-actions-dispatcher.hf.space\n```\n\nIn the setup form, enter the GitHub repo whose CI should run on HF Jobs:\n\n```text\nYOUR-GITHUB-ORG/YOUR-REPO\n```\n\nThen click the button to create the GitHub App. GitHub will ask you to choose a name for the App; the name can be anything, as long as it is available in your GitHub account or org. After you submit, the final screen tells you exactly how to upload the App credentials to the dispatcher Space with the `hf` CLI.\n\n**Important note**: you will need to provide an [Hugging Face token](https://huggingface.co/settings/tokens) that has permissions to launch Jobs, corresponding to your personal account or an org under which Jobs should be charged. This token should be saved as the `HF_TOKEN` secret in your dispatcher Space.\n\nFinally, you will install the App on the same GitHub repo you entered in the Space. In the Trackio setup, we installed it on `gradio-app/trackio`.\n\n### Agent-assisted setup\n\nThe GitHub App manifest flow is still browser-based, but an agent can follow the same Space-driven path:\n\n```bash\nexport HF_NAMESPACE=your-hf-user-or-org\nexport GITHUB_REPO=YOUR-GITHUB-ORG/YOUR-REPO\nopen \"https://${HF_NAMESPACE}-jobs-actions-dispatcher.hf.space\"\n```\n\nPaste `$GITHUB_REPO` into the Space, click the GitHub App creation button, choose any available App name, and follow the generated GitHub instructions.\n\nAfter the App exists, install it on your repo from the App settings page. For a GitHub org, the installation settings are under:\n\n```text\nhttps://github.com/organizations/YOUR-GITHUB-ORG/settings/installations\n```\n\n## Step 3: Final dispatcher settings\n\nAt this point, the dispatcher Space should be configured. The GitHub App setup flow generated the commands that upload the App credentials, webhook secret, and Hugging Face token to the Space.\n\n![image](https://github.com/user-attachments/assets/0fc8ac73-f93a-419b-bd80-70da2756f50c)\n\nBy default, HF Jobs are launched under the same namespace as the dispatcher Space. Optionally, set `HF_NAMESPACE` as a Space variable if you want to bill jobs to a different Hugging Face user or org:\n\n```bash\nexport SPACE_ID=YOUR-HF-NAMESPACE/jobs-actions-dispatcher\nhf spaces variables add \"$SPACE_ID\" -e HF_NAMESPACE=your-billing-namespace\nhf spaces restart \"$SPACE_ID\"\n```\n\nThe token you set in Step 2 should correspond to this namespace.\n\n## Step 4: Change `runs-on`\n\nThe actual workflow change is small. Instead of:\n\n```yaml\nruns-on: ubuntu-latest\n```\n\nuse one of the labels handled by the dispatcher:\n\n```yaml\nruns-on: hf-jobs-cpu-upgrade\n```\n\nFor GPU tests, use a GPU label:\n\n```yaml\nruns-on: hf-jobs-t4-small\n```\n\nFor any GitHub Action you'd like to run on HF Jobs, this 1-line change is all you need!\n\n## Step 5: Test it out\n\nTo add a minimal smoke-test workflow from the CLI:\n\n```bash\nmkdir -p .github/workflows\ncat > .github/workflows/hf-jobs-test.yml <<'EOF'\nname: HF Jobs Test\n\non:\n  pull_request:\n  push:\n    branches: [main]\n  workflow_dispatch:\n\njobs:\n  test:\n    runs-on: hf-jobs-cpu-upgrade\n    steps:\n      - uses: actions/checkout@v4\n      - run: echo \"Hello from Hugging Face Jobs\"\nEOF\n\ngit add .github/workflows/hf-jobs-test.yml\ngit commit -m \"Run CI on Hugging Face Jobs\"\ngit push\n```\n\nTo verify from the CLI:\n\n```bash\ngh run list --repo YOUR-GITHUB-ORG/YOUR-REPO --limit 5\nhf jobs ps --namespace \"$HF_NAMESPACE\"\nhf spaces logs \"$SPACE_ID\"\n```\n\nYou should be able to see logs just like a regular GitHub Action—for example, in this [Trackio PR #565](https://github.com/gradio-app/trackio/pull/565).\n\nAnd that's it!\n\n*Note on choosing the right Docker image*\n\nOur first CPU setup used `ubuntu:22.04` and installed missing system packages during every run. That worked, but it was slower than it needed to be. GitHub's `ubuntu-latest` image includes a lot of developer tooling by default; a bare Ubuntu image does not.\n\nFor Trackio, the UI tests need Playwright browsers, Node, ffmpeg, sqlite, git, and normal Linux build dependencies. Hugging Face Jobs supports using any [Docker image](https://huggingface.co/docs/hub/jobs-popular-images), so we switched to the Microsoft Playwright image, which worked well:\n\n```text\nmcr.microsoft.com/playwright:v1.60.0-jammy\n```\n\nFor GPU jobs, we used:\n\n```text\nnvidia/cuda:12.4.0-runtime-ubuntu22.04\n```\n\n## Results\n\nHere are the numbers from the Trackio CI:\n\n| Runner setup                    | Runtime | Compared to GitHub average    |\n| ------------------------------- | ------- | ----------------------------- |\n| GitHub `ubuntu-latest` baseline | `1m40s` | baseline                      |\n| HF Jobs CPU, Playwright image   | `1m10s` | `-30s`, about `30%` faster    |\n| HF Jobs GPU, `t4-small` label   | `45s`   | no GitHub-hosted GPU baseline |\n\nThe biggest win was GPU CI. The Trackio GPU check ran on HF Jobs and passed in `45s`, costing less than a cent at the `t4-small` rate for that duration.\n\nThe CPU result was also encouraging. With the right image, the Linux test job was faster than the GitHub-hosted baseline. That suggests HF Jobs can be a practical CI backend, especially for ML projects that need custom images or accelerators.\n\nLogs were another pleasant surprise. GitHub Actions logs are useful, but the web UI can be heavy for large logs. HF Jobs logs are easy to fetch from the CLI:\n\n```bash\nhf jobs logs <job_id> > logs.txt\n```\n\nThat makes them easy to inspect with local tools or coding agents. In our bridge, we also mirrored the GitHub Actions job log into the HF Job log, so either system had enough information to debug a run.\n\nFinally, although we didn't need them for Trackio's CI, HF Jobs also [supports mounting volumes](https://huggingface.co/docs/huggingface_hub/en/guides/jobs#mount-a-volume), which can be very helpful if you need to load datasets or models from Hugging Face quickly as part of your CI.\n\nHopefully, this gives you all you need to try HF Jobs for running your GitHub Actions!\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-06-09T00:00:00+00:00",
  "date_modified": "2026-06-09T00:00:00+00:00",
  "date_published": "2026-06-09T00:00:00+00:00",
  "description": "If you have a GitHub repository and you have GitHub Actions enabled, you probably use GitHub-hosted runners for CI. That is the default for many projects because it is simple: add a workflow, write runs-on: ubuntu-latest, and GitHub gives…",
  "external_url": "https://huggingface.co/blog/github-ci-hf-jobs",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2026/06/2026-06-09-migrating-your-github-ci-to-hugging-face-jobs.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2026/06/2026-06-09-migrating-your-github-ci-to-hugging-face-jobs.md"
  },
  "id": "urn:aggr:item:9d4ac813c9f57f29e84a903d0c2eff37b2c23c9f",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/github-ci-hf-jobs"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/github-ci-hf-jobs",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-09-migrating-your-github-ci-to-hugging-face-jobs/#webpage"
    },
    "dateModified": "2026-06-09T00:00:00+00:00",
    "datePublished": "2026-06-09T00:00:00+00:00",
    "headline": "Migrating Your GitHub CI to Hugging Face Jobs",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://huggingface.co/blog/assets/github-ci-hf-jobs/thumbnail.gif",
      "width": 160
    },
    "timeRequired": "PT8M",
    "url": "https://huggingface.co/blog/github-ci-hf-jobs",
    "wordCount": 1754
  },
  "name": "Archived snapshot: Migrating Your GitHub CI to Hugging Face Jobs",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://huggingface.co/blog/assets/github-ci-hf-jobs/thumbnail.gif",
    "width": 160
  },
  "reading_minutes": 8,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "Migrating Your GitHub CI to Hugging Face Jobs",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-06-09-migrating-your-github-ci-to-hugging-face-jobs/",
  "word_count": 1754
}