From 1a2ca13b92d54999d7d5e62c916aa571628a3d1c Mon Sep 17 00:00:00 2001 From: Vikram Vaswani Date: Thu, 28 May 2026 20:34:14 +0530 Subject: [PATCH 1/5] feat: add GitHub Actions runner steps Signed-off-by: Vikram Vaswani --- Jobs/GitHub-actions-runner.mdx | 388 +++++++++++++++++++++++++++++++++ docs.json | 3 +- 2 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 Jobs/GitHub-actions-runner.mdx diff --git a/Jobs/GitHub-actions-runner.mdx b/Jobs/GitHub-actions-runner.mdx new file mode 100644 index 00000000..62e27575 --- /dev/null +++ b/Jobs/GitHub-actions-runner.mdx @@ -0,0 +1,388 @@ +--- +title: "Run GitHub Actions on Blaxel" +description: "Run GitHub Actions self-hosted runners on Blaxel ephemeral micro-VMs." +sidebarTitle: "GitHub Actions runner" +--- + +Blaxel can act as a self-hosted GitHub Actions runner. Each workflow job runs inside an ephemeral micro-VM that is spun up on demand and discarded when the job finishes. + +There are two ways to connect GitHub to your Blaxel runner. The job configuration is identical in both cases, but the GitHub webhook is handled differently: + +- [Blaxel webhook handler](#option-a-blaxel-webhook-handler): Blaxel's GitHub App manages the webhook for you. Recommended for most users. +- [Custom webhook handler](#option-b-custom-webhook-handler): You provide a custom webhook handler. Recommended for deeper control over the integration. + +## Job configuration + + + This section applies to both options below. + + +1. Create the Dockerfile. + + The Dockerfile defines the micro-VM filesystem. A good starting point is the `catthehacker` Ubuntu image, which includes most tools found on GitHub-hosted runners. Here is an example of a Dockerfile for a Python test runner: + + ```dockerfile + FROM ghcr.io/catthehacker/ubuntu:full-24.04 + + USER root + + # Install Python + RUN apt-get update -qq \ + && apt-get install -y -qq --no-install-recommends \ + python3 python3-pip python3-venv \ + && rm -rf /var/lib/apt/lists/* + + # Install pytest + RUN pip3 install pytest pytest-cov pytest-xdist + + # Install GitHub Actions runner + ARG RUNNER_VERSION=2.333.0 + RUN curl -fSL --retry 3 --retry-delay 5 \ + -o /tmp/runner.tar.gz \ + "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" \ + && mkdir -p /actions-runner \ + && tar xzf /tmp/runner.tar.gz -C /actions-runner \ + && rm /tmp/runner.tar.gz + + COPY start.sh /start.sh + RUN chmod +x /start.sh + + ENV RUNNER_ALLOW_RUNASROOT=1 + + ENTRYPOINT ["bash", "/start.sh"] + ``` + + You can install additional tools or set environment variables using standard `RUN`, `COPY`, and `ENV` instructions. When you are ready to use a different image, edit the Dockerfile and redeploy. + +1. Create an entrypoint script. + + The Dockerfile must reference an entrypoint script (in this example, `start.sh`) that fetches the JIT config from Blaxel and starts the GitHub Actions runner. Here is an example: + + ```bash + #!/bin/bash + set -euo pipefail + + export RUNNER_ALLOW_RUNASROOT=1 + RUNNER_DIR="/actions-runner" + export HOME="${RUNNER_DIR}" + TASK_INDEX=${TASK_INDEX:-0} + + # fetch task data from Blaxel + if [ -n "${BL_EXECUTION_DATA_URL:-}" ]; then + echo "Fetching task data from Blaxel..." + TASK_DATA=$(curl -fsS --retry 3 --retry-delay 2 --connect-timeout 5 --max-time 30 "${BL_EXECUTION_DATA_URL}") + TASK_JSON=$(printf '%s' "${TASK_DATA}" | jq -c --argjson idx "${TASK_INDEX}" '.tasks[$idx]') + + if [ "$TASK_JSON" = "null" ]; then + TASK_JSON="" + fi + fi + + # fetch JIT_CONFIG + if [ -n "$TASK_JSON" ]; then + JIT_CONFIG=$(echo "${TASK_JSON}" | jq -r '.JIT_CONFIG // empty') + if [ -n "$JIT_CONFIG" ]; then + export ENCODED_JIT_CONFIG="$JIT_CONFIG" + echo "JIT config fetched successfully" + fi + fi + + # start GitHub Actions runner + cd "${RUNNER_DIR}" + echo "Starting GitHub Actions runner..." + if [ -n "${ENCODED_JIT_CONFIG:-}" ]; then + ./config.sh --unattended --jitconfig "${ENCODED_JIT_CONFIG}" + ./run.sh + else + echo "ERROR: No JIT config available" + exit 1 + fi + + ``` + +1. Create the `blaxel.toml` job configuration. + + The `blaxel.toml` file defines the job configuration. A sample job configuration is shown below. This configuration uses ephemeral volumes. + + ```toml + type = "job" + name = "pytest-runner" + + [runtime] + memory = 16384 # 16 GB of RAM allocated to the micro-VM + timeout = 3600 # maximum job duration: 1 hour (in seconds) + maxRetries = 0 # no automatic retries on failure + diskPercent = 5 # base root disk allocation (percentage) + + [[volumes]] + # Dedicated storage for the Docker daemon + name = "docker" + mountPath = "/var/lib/docker" + type = "ephemeral" + sizeMb = 10240 + + [[volumes]] + # General-purpose scratch space + name = "tmp" + mountPath = "/tmp" + type = "ephemeral" + sizeMb = 102400 + ``` + + If you plan to use Blaxel's webhook handler, also add the following field to the `blaxel.toml` file: + + ```toml + [githubRunner] + repositories = ["owner/repo"] + ``` + + The `repositories` field lists the GitHub repositories this runner is allowed to pick up jobs from. The format is `"owner/repo"`. This is only required when using Blaxel's webhook handler. + +1. Deploy the job. + + Once the job configuration is completed, deploy it to Blaxel: + + ```bash + bl deploy + ``` + +## GitHub integration + +### Option A: Blaxel webhook handler + +Blaxel's GitHub App receives `workflow_job` events from GitHub and automatically launches your job. + +1. Install the Blaxel GitHub App (first time only). + + - Log in to the Blaxel Console. + - Navigate to **Hosting** > **Jobs** > `` > **Settings**. + - Scroll to the **GitHub Runner** section. It should show as **Active**. + - Click **Edit**. + - Click **+**. + - Follow the instructions to authorize and install the Blaxel GitHub App on the repository. + - Select the repository and click **Save**. + +1. Redeploy the job. + + ```bash + bl deploy --skip-build + ``` + +1. Once configured, use `runs-on: [workspace/job-name]` in your GitHub Actions workflow to target this runner. For example: + + ```yaml + jobs: + build: + runs-on: my-blaxel-workspace/pytest-runner + steps: + - uses: actions/checkout@v4 + - run: echo "Running on Blaxel!" + ``` + +### Option B: Custom webhook handler + +Use this approach when you need deeper control over the process. This method allows you to use a custom webhook handler, which can be a Blaxel agent, a serverless function, or any service capable of accepting an HTTP POST request. + +Under this approach, when GitHub sends a `workflow_job` event, the handler: + +1. Verifies the webhook signature. +2. Checks whether the job's `runs-on` labels match the configured prefix (default: `blaxel`). +3. Calls the GitHub API to generate a JIT (Just-In-Time) runner configuration. +4. Launches a Blaxel job with that JIT config, which registers itself as a self-hosted runner and picks up the queued work. + +The handler uses label prefixes to decide which Blaxel job to spawn. Given `CATCH_LABEL=blaxel` (the default): + +| `runs-on` value | Blaxel job triggered | +|---|---| +| `blaxel-github-runner-full` | `github-runner-full` | +| `blaxel-my-custom-runner` | `my-custom-runner` | + +Everything after `-` is used as the job name on Blaxel. + +1. Configure environment variables for the handler as below: + + | Variable | Required | Description | + |---|---|---| + | `GITHUB_TOKEN` | Yes | Personal Access Token with admin access to target repos | + | `GITHUB_WEBHOOK_SECRET` | Yes | Secret used to verify webhook payloads from GitHub; you can choose any value and will enter it again when configuring the webhook in GitHub | + | `CATCH_LABEL` | No | Label prefix to match (default: `blaxel`) | + + + + If you deploy the handler as a Blaxel agent, set `public = true` in `blaxel.toml`. Authentication is handled through GitHub webhook signature verification, not Blaxel API keys. + + +2. Define the handler logic. + + Here is an example implementation in TypeScript: + + ```typescript + import '@blaxel/telemetry' + import { blJob } from "@blaxel/core"; + import express, { Request, Response } from "express"; + import crypto from "crypto"; + + const app = express(); + app.use(express.json()); + + const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; + const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || ""; + const CATCH_LABEL = process.env.CATCH_LABEL || "blaxel"; + + function verifySignature(payload: string, signature: string | undefined): boolean { + if (!GITHUB_WEBHOOK_SECRET) return true; + if (!signature) return false; + + const expected = "sha256=" + crypto + .createHmac("sha256", GITHUB_WEBHOOK_SECRET) + .update(payload) + .digest("hex"); + + return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); + } + + interface JitConfigResponse { + runner: { id: number; name: string }; + encoded_jit_config: string; + } + + async function generateJitConfig(repoFullName: string, labels: string[]): Promise { + const runnerName = `blaxel-${crypto.randomBytes(4).toString("hex")}`; + + const response = await fetch( + `https://api.github.com/repos/${repoFullName}/actions/runners/generate-jitconfig`, + { + method: "POST", + headers: { + Authorization: `Bearer ${GITHUB_TOKEN}`, + Accept: "application/vnd.github+json", + }, + body: JSON.stringify({ + name: runnerName, + runner_group_id: 1, + labels: labels, + work_folder: "_work", + }), + } + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`GitHub API error (${response.status}): ${error}`); + } + + return response.json() as Promise; + } + + function findJobName(requestedLabels: string[]): string | null { + const prefix = CATCH_LABEL + "-"; + const match = requestedLabels.find((l) => l.startsWith(prefix)); + if (!match) return null; + return match.slice(prefix.length) || null; + } + + app.post("/webhook", async (req: Request, res: Response) => { + const event = req.headers["x-github-event"] as string; + const signature = req.headers["x-hub-signature-256"] as string | undefined; + const payload = JSON.stringify(req.body); + + if (!verifySignature(payload, signature)) { + console.error("Invalid webhook signature"); + res.status(401).json({ error: "Invalid signature" }); + return; + } + + if (event !== "workflow_job") { + res.json({ status: "ignored", event }); + return; + } + + const { action, workflow_job } = req.body; + + if (action !== "queued") { + console.log(`Job ${workflow_job.id} action: ${action}, ignoring`); + res.json({ status: "ignored", action }); + return; + } + + const requestedLabels: string[] = workflow_job.labels || []; + const jobName = findJobName(requestedLabels); + + if (!jobName) { + console.log(`Job ${workflow_job.id} not targeted at us (labels: ${requestedLabels.join(", ")})`); + res.json({ status: "ignored", reason: "not_targeted" }); + return; + } + + const repoFullName: string = req.body.repository.full_name; + console.log(`Job ${workflow_job.id} queued for ${repoFullName}, spawning job "${jobName}"...`); + + try { + const jitConfig = await generateJitConfig(repoFullName, requestedLabels); + console.log(`JIT config generated for runner ${jitConfig.runner.name} (id: ${jitConfig.runner.id})`); + + const job = blJob(jobName); + await job.run([{ JIT_CONFIG: jitConfig.encoded_jit_config }]); + + console.log(`Runner job "${jobName}" launched for workflow job ${workflow_job.id}`); + res.json({ status: "runner_spawned", runner: jitConfig.runner.name, job: jobName }); + } catch (err: unknown) { + let message: string; + if (err instanceof Error) { + message = err.message; + if (err.stack) console.error(err.stack); + } else { + message = String(err); + } + console.error(`Failed to spawn runner for job "${jobName}":`, err); + res.status(500).json({ error: message, job: jobName }); + } + }); + + app.get("/health", (_req: Request, res: Response) => { + res.json({ status: "ok", catch_label: CATCH_LABEL }); + }); + + const HOST = process.env.HOST || "0.0.0.0"; + const PORT = parseInt(process.env.PORT || "8080", 10); + + app.listen(PORT, HOST, () => { + console.log(`GitHub Runner webhook agent listening on ${HOST}:${PORT}`); + console.log(`Catching jobs with label: "${CATCH_LABEL}"`); + }); + ``` + +3. Deploy your handler to the platform of your choice. Once deployed, note the public URL, as you will need it for the webhook. + +4. Configure the GitHub webhook. + + Go to your repository (or organization) and navigate to the list of webhooks (**Settings > Webhooks > Add webhook**). Configure a new webhook as follows: + + | Field | Value | + |---|---| + | **Payload URL** | `https:///webhook` | + | **Content type** | `application/json` | + | **Secret** | Same value as `GITHUB_WEBHOOK_SECRET` | + | **Events** | Select **"Workflow jobs"** only | + + 5. Use it in your workflows + + In any GitHub Actions workflow, set `runs-on` to target your Blaxel runner: + + ```yaml + jobs: + build: + runs-on: blaxel-pytest-runner + steps: + - uses: actions/checkout@v4 + - run: echo "Running on Blaxel!" + ``` + +The label `blaxel-pytest-runner` triggers the `pytest-runner` job on Blaxel. + +You can maintain multiple runner profiles by creating separate jobs with different Dockerfiles and routing to them via different labels: + +| `runs-on` label | Job | +|---|---| +| `blaxel-runner-slim` | `github-runner-slim` | +| `blaxel-runner-gpu` | `github-runner-gpu` | diff --git a/docs.json b/docs.json index ed833267..e031d2d1 100644 --- a/docs.json +++ b/docs.json @@ -102,7 +102,8 @@ "Jobs/Manage-job-execution-py" ] }, - "Jobs/Variables-and-secrets-jobs" + "Jobs/Variables-and-secrets-jobs", + "Jobs/GitHub-actions-runner" ] }, { From b60cd67dccbdef79a4d105e5af082f049e198ea2 Mon Sep 17 00:00:00 2001 From: Vikram Vaswani Date: Thu, 28 May 2026 20:45:23 +0530 Subject: [PATCH 2/5] docs: update deployment ref Signed-off-by: Vikram Vaswani --- deployment-reference.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deployment-reference.mdx b/deployment-reference.mdx index 0a4a6bbb..cf786bb9 100644 --- a/deployment-reference.mdx +++ b/deployment-reference.mdx @@ -70,6 +70,9 @@ The format of Blaxel's configuration file is described below. # maximum number of retries # maxRetries = 0 +# base root disk allocation as a percentage of available disk (only valid for resource type="job") +# diskPercent = 5 + # volumes (optional, for resource type="agent", "sandbox", "volume-template", "job") # attaches persistent storage to the resource # the resource must be pinned to the same region as the volume @@ -95,6 +98,11 @@ The format of Blaxel's configuration file is described below. # volume default size in MB (only valid for resource type="volume-template") # defaultSize = 1024 +# GitHub Runner integration (optional, only valid for resource type="job") +# [githubRunner] +# list of GitHub repositories this runner can pick up jobs from +# repositories = ["owner/repo"] + # job trigger (optional) # [[triggers]] # trigger identifier From 76f146407c22a6cd963b8c513e8d19ef9b9ae866 Mon Sep 17 00:00:00 2001 From: Vikram Vaswani Date: Wed, 3 Jun 2026 05:03:02 +0530 Subject: [PATCH 3/5] docs: switch to tutorial format, single option Signed-off-by: Vikram Vaswani --- Jobs/GitHub-actions-runner.mdx | 388 ---------------------------- Tutorials/GitHub-actions-runner.mdx | 237 +++++++++++++++++ docs.json | 9 +- 3 files changed, 244 insertions(+), 390 deletions(-) delete mode 100644 Jobs/GitHub-actions-runner.mdx create mode 100644 Tutorials/GitHub-actions-runner.mdx diff --git a/Jobs/GitHub-actions-runner.mdx b/Jobs/GitHub-actions-runner.mdx deleted file mode 100644 index 62e27575..00000000 --- a/Jobs/GitHub-actions-runner.mdx +++ /dev/null @@ -1,388 +0,0 @@ ---- -title: "Run GitHub Actions on Blaxel" -description: "Run GitHub Actions self-hosted runners on Blaxel ephemeral micro-VMs." -sidebarTitle: "GitHub Actions runner" ---- - -Blaxel can act as a self-hosted GitHub Actions runner. Each workflow job runs inside an ephemeral micro-VM that is spun up on demand and discarded when the job finishes. - -There are two ways to connect GitHub to your Blaxel runner. The job configuration is identical in both cases, but the GitHub webhook is handled differently: - -- [Blaxel webhook handler](#option-a-blaxel-webhook-handler): Blaxel's GitHub App manages the webhook for you. Recommended for most users. -- [Custom webhook handler](#option-b-custom-webhook-handler): You provide a custom webhook handler. Recommended for deeper control over the integration. - -## Job configuration - - - This section applies to both options below. - - -1. Create the Dockerfile. - - The Dockerfile defines the micro-VM filesystem. A good starting point is the `catthehacker` Ubuntu image, which includes most tools found on GitHub-hosted runners. Here is an example of a Dockerfile for a Python test runner: - - ```dockerfile - FROM ghcr.io/catthehacker/ubuntu:full-24.04 - - USER root - - # Install Python - RUN apt-get update -qq \ - && apt-get install -y -qq --no-install-recommends \ - python3 python3-pip python3-venv \ - && rm -rf /var/lib/apt/lists/* - - # Install pytest - RUN pip3 install pytest pytest-cov pytest-xdist - - # Install GitHub Actions runner - ARG RUNNER_VERSION=2.333.0 - RUN curl -fSL --retry 3 --retry-delay 5 \ - -o /tmp/runner.tar.gz \ - "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" \ - && mkdir -p /actions-runner \ - && tar xzf /tmp/runner.tar.gz -C /actions-runner \ - && rm /tmp/runner.tar.gz - - COPY start.sh /start.sh - RUN chmod +x /start.sh - - ENV RUNNER_ALLOW_RUNASROOT=1 - - ENTRYPOINT ["bash", "/start.sh"] - ``` - - You can install additional tools or set environment variables using standard `RUN`, `COPY`, and `ENV` instructions. When you are ready to use a different image, edit the Dockerfile and redeploy. - -1. Create an entrypoint script. - - The Dockerfile must reference an entrypoint script (in this example, `start.sh`) that fetches the JIT config from Blaxel and starts the GitHub Actions runner. Here is an example: - - ```bash - #!/bin/bash - set -euo pipefail - - export RUNNER_ALLOW_RUNASROOT=1 - RUNNER_DIR="/actions-runner" - export HOME="${RUNNER_DIR}" - TASK_INDEX=${TASK_INDEX:-0} - - # fetch task data from Blaxel - if [ -n "${BL_EXECUTION_DATA_URL:-}" ]; then - echo "Fetching task data from Blaxel..." - TASK_DATA=$(curl -fsS --retry 3 --retry-delay 2 --connect-timeout 5 --max-time 30 "${BL_EXECUTION_DATA_URL}") - TASK_JSON=$(printf '%s' "${TASK_DATA}" | jq -c --argjson idx "${TASK_INDEX}" '.tasks[$idx]') - - if [ "$TASK_JSON" = "null" ]; then - TASK_JSON="" - fi - fi - - # fetch JIT_CONFIG - if [ -n "$TASK_JSON" ]; then - JIT_CONFIG=$(echo "${TASK_JSON}" | jq -r '.JIT_CONFIG // empty') - if [ -n "$JIT_CONFIG" ]; then - export ENCODED_JIT_CONFIG="$JIT_CONFIG" - echo "JIT config fetched successfully" - fi - fi - - # start GitHub Actions runner - cd "${RUNNER_DIR}" - echo "Starting GitHub Actions runner..." - if [ -n "${ENCODED_JIT_CONFIG:-}" ]; then - ./config.sh --unattended --jitconfig "${ENCODED_JIT_CONFIG}" - ./run.sh - else - echo "ERROR: No JIT config available" - exit 1 - fi - - ``` - -1. Create the `blaxel.toml` job configuration. - - The `blaxel.toml` file defines the job configuration. A sample job configuration is shown below. This configuration uses ephemeral volumes. - - ```toml - type = "job" - name = "pytest-runner" - - [runtime] - memory = 16384 # 16 GB of RAM allocated to the micro-VM - timeout = 3600 # maximum job duration: 1 hour (in seconds) - maxRetries = 0 # no automatic retries on failure - diskPercent = 5 # base root disk allocation (percentage) - - [[volumes]] - # Dedicated storage for the Docker daemon - name = "docker" - mountPath = "/var/lib/docker" - type = "ephemeral" - sizeMb = 10240 - - [[volumes]] - # General-purpose scratch space - name = "tmp" - mountPath = "/tmp" - type = "ephemeral" - sizeMb = 102400 - ``` - - If you plan to use Blaxel's webhook handler, also add the following field to the `blaxel.toml` file: - - ```toml - [githubRunner] - repositories = ["owner/repo"] - ``` - - The `repositories` field lists the GitHub repositories this runner is allowed to pick up jobs from. The format is `"owner/repo"`. This is only required when using Blaxel's webhook handler. - -1. Deploy the job. - - Once the job configuration is completed, deploy it to Blaxel: - - ```bash - bl deploy - ``` - -## GitHub integration - -### Option A: Blaxel webhook handler - -Blaxel's GitHub App receives `workflow_job` events from GitHub and automatically launches your job. - -1. Install the Blaxel GitHub App (first time only). - - - Log in to the Blaxel Console. - - Navigate to **Hosting** > **Jobs** > `` > **Settings**. - - Scroll to the **GitHub Runner** section. It should show as **Active**. - - Click **Edit**. - - Click **+**. - - Follow the instructions to authorize and install the Blaxel GitHub App on the repository. - - Select the repository and click **Save**. - -1. Redeploy the job. - - ```bash - bl deploy --skip-build - ``` - -1. Once configured, use `runs-on: [workspace/job-name]` in your GitHub Actions workflow to target this runner. For example: - - ```yaml - jobs: - build: - runs-on: my-blaxel-workspace/pytest-runner - steps: - - uses: actions/checkout@v4 - - run: echo "Running on Blaxel!" - ``` - -### Option B: Custom webhook handler - -Use this approach when you need deeper control over the process. This method allows you to use a custom webhook handler, which can be a Blaxel agent, a serverless function, or any service capable of accepting an HTTP POST request. - -Under this approach, when GitHub sends a `workflow_job` event, the handler: - -1. Verifies the webhook signature. -2. Checks whether the job's `runs-on` labels match the configured prefix (default: `blaxel`). -3. Calls the GitHub API to generate a JIT (Just-In-Time) runner configuration. -4. Launches a Blaxel job with that JIT config, which registers itself as a self-hosted runner and picks up the queued work. - -The handler uses label prefixes to decide which Blaxel job to spawn. Given `CATCH_LABEL=blaxel` (the default): - -| `runs-on` value | Blaxel job triggered | -|---|---| -| `blaxel-github-runner-full` | `github-runner-full` | -| `blaxel-my-custom-runner` | `my-custom-runner` | - -Everything after `-` is used as the job name on Blaxel. - -1. Configure environment variables for the handler as below: - - | Variable | Required | Description | - |---|---|---| - | `GITHUB_TOKEN` | Yes | Personal Access Token with admin access to target repos | - | `GITHUB_WEBHOOK_SECRET` | Yes | Secret used to verify webhook payloads from GitHub; you can choose any value and will enter it again when configuring the webhook in GitHub | - | `CATCH_LABEL` | No | Label prefix to match (default: `blaxel`) | - - - - If you deploy the handler as a Blaxel agent, set `public = true` in `blaxel.toml`. Authentication is handled through GitHub webhook signature verification, not Blaxel API keys. - - -2. Define the handler logic. - - Here is an example implementation in TypeScript: - - ```typescript - import '@blaxel/telemetry' - import { blJob } from "@blaxel/core"; - import express, { Request, Response } from "express"; - import crypto from "crypto"; - - const app = express(); - app.use(express.json()); - - const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; - const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET || ""; - const CATCH_LABEL = process.env.CATCH_LABEL || "blaxel"; - - function verifySignature(payload: string, signature: string | undefined): boolean { - if (!GITHUB_WEBHOOK_SECRET) return true; - if (!signature) return false; - - const expected = "sha256=" + crypto - .createHmac("sha256", GITHUB_WEBHOOK_SECRET) - .update(payload) - .digest("hex"); - - return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); - } - - interface JitConfigResponse { - runner: { id: number; name: string }; - encoded_jit_config: string; - } - - async function generateJitConfig(repoFullName: string, labels: string[]): Promise { - const runnerName = `blaxel-${crypto.randomBytes(4).toString("hex")}`; - - const response = await fetch( - `https://api.github.com/repos/${repoFullName}/actions/runners/generate-jitconfig`, - { - method: "POST", - headers: { - Authorization: `Bearer ${GITHUB_TOKEN}`, - Accept: "application/vnd.github+json", - }, - body: JSON.stringify({ - name: runnerName, - runner_group_id: 1, - labels: labels, - work_folder: "_work", - }), - } - ); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`GitHub API error (${response.status}): ${error}`); - } - - return response.json() as Promise; - } - - function findJobName(requestedLabels: string[]): string | null { - const prefix = CATCH_LABEL + "-"; - const match = requestedLabels.find((l) => l.startsWith(prefix)); - if (!match) return null; - return match.slice(prefix.length) || null; - } - - app.post("/webhook", async (req: Request, res: Response) => { - const event = req.headers["x-github-event"] as string; - const signature = req.headers["x-hub-signature-256"] as string | undefined; - const payload = JSON.stringify(req.body); - - if (!verifySignature(payload, signature)) { - console.error("Invalid webhook signature"); - res.status(401).json({ error: "Invalid signature" }); - return; - } - - if (event !== "workflow_job") { - res.json({ status: "ignored", event }); - return; - } - - const { action, workflow_job } = req.body; - - if (action !== "queued") { - console.log(`Job ${workflow_job.id} action: ${action}, ignoring`); - res.json({ status: "ignored", action }); - return; - } - - const requestedLabels: string[] = workflow_job.labels || []; - const jobName = findJobName(requestedLabels); - - if (!jobName) { - console.log(`Job ${workflow_job.id} not targeted at us (labels: ${requestedLabels.join(", ")})`); - res.json({ status: "ignored", reason: "not_targeted" }); - return; - } - - const repoFullName: string = req.body.repository.full_name; - console.log(`Job ${workflow_job.id} queued for ${repoFullName}, spawning job "${jobName}"...`); - - try { - const jitConfig = await generateJitConfig(repoFullName, requestedLabels); - console.log(`JIT config generated for runner ${jitConfig.runner.name} (id: ${jitConfig.runner.id})`); - - const job = blJob(jobName); - await job.run([{ JIT_CONFIG: jitConfig.encoded_jit_config }]); - - console.log(`Runner job "${jobName}" launched for workflow job ${workflow_job.id}`); - res.json({ status: "runner_spawned", runner: jitConfig.runner.name, job: jobName }); - } catch (err: unknown) { - let message: string; - if (err instanceof Error) { - message = err.message; - if (err.stack) console.error(err.stack); - } else { - message = String(err); - } - console.error(`Failed to spawn runner for job "${jobName}":`, err); - res.status(500).json({ error: message, job: jobName }); - } - }); - - app.get("/health", (_req: Request, res: Response) => { - res.json({ status: "ok", catch_label: CATCH_LABEL }); - }); - - const HOST = process.env.HOST || "0.0.0.0"; - const PORT = parseInt(process.env.PORT || "8080", 10); - - app.listen(PORT, HOST, () => { - console.log(`GitHub Runner webhook agent listening on ${HOST}:${PORT}`); - console.log(`Catching jobs with label: "${CATCH_LABEL}"`); - }); - ``` - -3. Deploy your handler to the platform of your choice. Once deployed, note the public URL, as you will need it for the webhook. - -4. Configure the GitHub webhook. - - Go to your repository (or organization) and navigate to the list of webhooks (**Settings > Webhooks > Add webhook**). Configure a new webhook as follows: - - | Field | Value | - |---|---| - | **Payload URL** | `https:///webhook` | - | **Content type** | `application/json` | - | **Secret** | Same value as `GITHUB_WEBHOOK_SECRET` | - | **Events** | Select **"Workflow jobs"** only | - - 5. Use it in your workflows - - In any GitHub Actions workflow, set `runs-on` to target your Blaxel runner: - - ```yaml - jobs: - build: - runs-on: blaxel-pytest-runner - steps: - - uses: actions/checkout@v4 - - run: echo "Running on Blaxel!" - ``` - -The label `blaxel-pytest-runner` triggers the `pytest-runner` job on Blaxel. - -You can maintain multiple runner profiles by creating separate jobs with different Dockerfiles and routing to them via different labels: - -| `runs-on` label | Job | -|---|---| -| `blaxel-runner-slim` | `github-runner-slim` | -| `blaxel-runner-gpu` | `github-runner-gpu` | diff --git a/Tutorials/GitHub-actions-runner.mdx b/Tutorials/GitHub-actions-runner.mdx new file mode 100644 index 00000000..38ae215f --- /dev/null +++ b/Tutorials/GitHub-actions-runner.mdx @@ -0,0 +1,237 @@ +--- +title: "Run GitHub Actions on Blaxel" +description: "Run GitHub Actions self-hosted runners on Blaxel ephemeral micro-VMs using the Blaxel GitHub App." +sidebarTitle: "GitHub Actions runner" +--- + +Blaxel can act as a self-hosted GitHub Actions runner. Each workflow job runs inside an ephemeral micro-VM that is spun up on demand and discarded when the job finishes. The Blaxel GitHub App receives `workflow_job` events from GitHub and automatically launches your job. + + + This tutorial assumes that you have a Python project containing unit tests and a GitHub Actions workflow that runs those unit tests using `pytest`. + + +## Prerequisites + +- A Blaxel account and workspace +- A GitHub repository containing a Python project with tests +- The Blaxel CLI (`bl`) installed and authenticated + +## 1. Create the Dockerfile + +The Dockerfile defines the micro-VM filesystem. A good starting point is the `catthehacker` Ubuntu image, which includes most tools found on GitHub-hosted runners. Here is an example for a Python test runner: + +```dockerfile +FROM ghcr.io/catthehacker/ubuntu:full-24.04 + +USER root + +# Install Python +RUN apt-get update -qq \ + && apt-get install -y -qq --no-install-recommends \ + python3 python3-pip python3-venv \ + && rm -rf /var/lib/apt/lists/* + +# Install pytest +RUN pip3 install pytest pytest-cov pytest-xdist + +# Install GitHub Actions runner +ARG RUNNER_VERSION=2.333.0 +RUN curl -fSL --retry 3 --retry-delay 5 \ + -o /tmp/runner.tar.gz \ + "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" \ + && mkdir -p /actions-runner \ + && tar xzf /tmp/runner.tar.gz -C /actions-runner \ + && rm /tmp/runner.tar.gz + +COPY start.sh /start.sh +RUN chmod +x /start.sh + +ENV RUNNER_ALLOW_RUNASROOT=1 + +ENTRYPOINT ["bash", "/start.sh"] +``` + +You can install additional tools or set environment variables using standard `RUN`, `COPY`, and `ENV` instructions. When you are ready to use a different image, edit the Dockerfile and redeploy. + +## 2. Create the entrypoint script + +The Dockerfile references an entrypoint script (`start.sh`) that starts Docker, fetches the JIT config from Blaxel and starts the GitHub Actions runner: + +```bash +#!/bin/bash +set -euo pipefail + +export RUNNER_ALLOW_RUNASROOT=1 +RUNNER_DIR="/actions-runner" +export HOME="${RUNNER_DIR}" +TASK_INDEX=${TASK_INDEX:-0} + +start_docker() { + command -v dockerd &>/dev/null || return 0 + + local fs_type="unknown" + local fs_size="unknown" + if mountpoint -q /var/lib/docker 2>/dev/null; then + read -r fs_type fs_size < <(df -hT /var/lib/docker | awk 'NR == 2 {print $2, $3}') + fi + + # Docker overlay2 cannot use an overlay filesystem as its backing store. + # That happens when Blaxel overlays a non-empty image path with the volume. + if [ "${fs_type}" = "ext4" ] || [ "${fs_type}" = "xfs" ]; then + STORAGE_DRIVER="overlay2" + echo "Docker storage: ${fs_type} ${fs_size} -> overlay2" + else + STORAGE_DRIVER="vfs" + echo "Docker storage: ${fs_type} ${fs_size} -> vfs" + fi + + dockerd --storage-driver="$STORAGE_DRIVER" &>/var/log/dockerd.log & + DOCKERD_PID=$! + + for i in $(seq 1 30); do + docker info &>/dev/null && break + if ! kill -0 "$DOCKERD_PID" 2>/dev/null; then + echo "ERROR: dockerd died unexpectedly" + tail -20 /var/log/dockerd.log 2>/dev/null + return 1 + fi + sleep 1 + done + + if docker info &>/dev/null; then + echo "Docker ready ($STORAGE_DRIVER)" + else + echo "ERROR: dockerd failed to start" + tail -20 /var/log/dockerd.log 2>/dev/null + return 1 + fi +} + +start_docker + +# Fetch task arguments from Blaxel execution data +if [ -n "${BL_EXECUTION_DATA_URL:-}" ]; then + echo "Fetching task data from Blaxel..." + TASK_DATA=$(curl -sf "${BL_EXECUTION_DATA_URL}") + JIT_CONFIG=$(echo "${TASK_DATA}" | jq -r ".tasks[${TASK_INDEX}].JIT_CONFIG") +fi + +if [ -z "${JIT_CONFIG:-}" ] || [ "${JIT_CONFIG}" = "null" ]; then + echo "Error: JIT_CONFIG not found in task data" + exit 1 +fi + +cd "${RUNNER_DIR}" +echo "Starting GitHub Actions runner in JIT mode..." +exec ./run.sh --jitconfig "${JIT_CONFIG}" +``` + +## 3. Create `blaxel.toml` + +The `blaxel.toml` file defines the job configuration. This example uses ephemeral volumes and targets the GitHub repositories this runner is allowed to serve: + +```toml +type = "job" +name = "pytest-runner" + +[runtime] +memory = 16384 # 16 GB of RAM allocated to the micro-VM +timeout = 3600 # maximum job duration: 1 hour (in seconds) +maxRetries = 0 # no automatic retries on failure +diskPercent = 5 # base root disk allocation (percentage) + +[githubRunner] +repositories = ["owner/repo"] + +[[volumes]] +# Dedicated storage for the Docker daemon +name = "docker" +mountPath = "/var/lib/docker" +type = "ephemeral" +sizeMb = 10240 + +[[volumes]] +# General-purpose scratch space +name = "tmp" +mountPath = "/tmp" +type = "ephemeral" +sizeMb = 102400 +``` + +The `repositories` field lists the GitHub repositories this runner is allowed to pick up jobs from. The format is `owner/repo`. + +## 4. Deploy the job + +Deploy the job to Blaxel: + +```bash +bl deploy +``` + + + Due to the large size of the image, the build process can take up to 40 minutes in some cases. + + +## 5. Install the Blaxel GitHub App + +1. Log in to the Blaxel Console. +2. Navigate to **Hosting** > **Jobs** > `` > **Settings**. +3. Scroll to the **GitHub Runner** section. It should show as **Active**. +4. Click **Edit**, then click **+**. +5. Follow the instructions to authorize and install the Blaxel GitHub App on the repository. +6. Select the repository and click **Save**. + +## 6. Redeploy the job + +```bash +bl deploy --skip-build +``` + +## 7. Use the runner in your GitHub Actions workflow + +In any GitHub Actions workflow, set `runs-on` to `/` to target your Blaxel runner: + +```yaml +name: test + +on: + push: + branches: + main + +permissions: + contents: read + + +jobs: + test: + runs-on: my-blaxel-workspace/pytest-runner + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run tests + id: run-tests + run: | + pytest --tb=short -q +``` + +Each time this workflow runs, Blaxel spins up a fresh ephemeral micro-VM, executes the job, and discards the VM when it finishes. + +## Resources + + + + Learn more about Blaxel Jobs and configuration options. + + + Full reference for all `blaxel.toml` fields. + + diff --git a/docs.json b/docs.json index e031d2d1..bf811b9e 100644 --- a/docs.json +++ b/docs.json @@ -102,8 +102,7 @@ "Jobs/Manage-job-execution-py" ] }, - "Jobs/Variables-and-secrets-jobs", - "Jobs/GitHub-actions-runner" + "Jobs/Variables-and-secrets-jobs" ] }, { @@ -299,6 +298,12 @@ "Tutorials/Custom-Agents" ] }, + { + "group": "Jobs", + "pages": [ + "Tutorials/GitHub-actions-runner" + ] + }, { "group": "Complete examples", "pages": [ From 76a1af37f4f412ca707d6e67b20fc4b2c78c9bbf Mon Sep 17 00:00:00 2001 From: Vikram Vaswani Date: Wed, 3 Jun 2026 12:56:34 +0530 Subject: [PATCH 4/5] fix: fix link Signed-off-by: Vikram Vaswani --- Tutorials/GitHub-actions-runner.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tutorials/GitHub-actions-runner.mdx b/Tutorials/GitHub-actions-runner.mdx index 38ae215f..850ddd51 100644 --- a/Tutorials/GitHub-actions-runner.mdx +++ b/Tutorials/GitHub-actions-runner.mdx @@ -231,7 +231,7 @@ Each time this workflow runs, Blaxel spins up a fresh ephemeral micro-VM, execut Learn more about Blaxel Jobs and configuration options. - + Full reference for all `blaxel.toml` fields. From 582ebefa3c13269ddc8ce123cacb75988374ff85 Mon Sep 17 00:00:00 2001 From: Vikram Vaswani Date: Wed, 3 Jun 2026 12:58:11 +0530 Subject: [PATCH 5/5] fix: fix yaml Signed-off-by: Vikram Vaswani --- Tutorials/GitHub-actions-runner.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tutorials/GitHub-actions-runner.mdx b/Tutorials/GitHub-actions-runner.mdx index 850ddd51..82dd49d3 100644 --- a/Tutorials/GitHub-actions-runner.mdx +++ b/Tutorials/GitHub-actions-runner.mdx @@ -197,7 +197,7 @@ name: test on: push: branches: - main + - main permissions: contents: read