-
Notifications
You must be signed in to change notification settings - Fork 2
feat: document GitHub Actions runner config #584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
vikram-blaxel
wants to merge
5
commits into
main
Choose a base branch
from
pm-2182-github-runner
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1a2ca13
feat: add GitHub Actions runner steps
vikram-blaxel b60cd67
docs: update deployment ref
vikram-blaxel 76f1464
docs: switch to tutorial format, single option
vikram-blaxel 76a1af3
fix: fix link
vikram-blaxel 582ebef
fix: fix yaml
vikram-blaxel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| <Note> | ||
| This section applies to both options below. | ||
| </Note> | ||
|
|
||
| 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** > `<your-job-name>` > **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 `<CATCH_LABEL>-` 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`) | | ||
|
|
||
|
|
||
| <Tip> | ||
| 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. | ||
| </Tip> | ||
|
|
||
| 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<JitConfigResponse> { | ||
| 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<JitConfigResponse>; | ||
| } | ||
|
|
||
| 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://<your-handler-url>/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` | | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.