Skip to content

Commit d2440ba

Browse files
committed
Slim integrator skills to product rules only.
Drop customer-shaped recipes and reference kits so agents follow README examples instead of one integration.
1 parent 419dfd7 commit d2440ba

6 files changed

Lines changed: 64 additions & 557 deletions

File tree

‎skills/runner-modal-deploy/SKILL.md‎

Lines changed: 22 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -3,167 +3,44 @@ name: runner-modal-deploy
33
description: >-
44
Deploys runner-modal GitHub Actions self-hosted runner pools on Modal
55
Sandboxes. Use when creating Modal Secrets, writing Runner.create apps,
6-
running modal deploy, registering GitHub workflow_job webhooks, tearing
7-
down a pool, or setting up Modal CI for a repo. Not for hosted
8-
ubuntu-latest runners or generic Modal Apps.
6+
running modal deploy, registering GitHub workflow_job webhooks, or
7+
tearing down a pool. Not for hosted ubuntu-latest or generic Modal Apps.
98
---
109

1110
# Deploying runner-modal
1211

13-
Self-hosted GitHub Actions on Modal: one webhook Function per pool, one
14-
ephemeral Job Sandbox per `workflow_job`. Operator-deployed, not a managed
15-
service.
12+
Operator-deployed webhook Function + one Job Sandbox per `workflow_job`.
13+
Not a managed service. Human DX: `README.md`. Trust: `SECURITY.md`.
1614

17-
Human DX: `README.md`. Trust model: `SECURITY.md`.
18-
Workflows: `runner-modal-workflows`. Job images: `runner-modal-images`.
19-
Failures: `runner-modal-troubleshoot`.
15+
Copy `examples/basic/app.py` (CPU) or `examples/gpu/app.py` (GPU). Do not
16+
invent a second resource profile on the same App.
2017

21-
Reference integration: [Nordlys-Labs/nordlys `runners/`](https://github.com/Nordlys-Labs/nordlys/tree/main/runners).
18+
## Rules
2219

23-
## Workflow
24-
25-
Copy and check off:
26-
27-
```
28-
- [ ] Pin runner-modal
29-
- [ ] Split Secrets
30-
- [ ] One App per pool
31-
- [ ] modal deploy
32-
- [ ] Webhook per pool
33-
- [ ] Health check
34-
```
35-
36-
### 1. Pin runner-modal
37-
38-
Separate uv project (Nordlys keeps this in `runners/`). PyPI is not set up;
39-
Image builds need `add_local_python_source("runner_modal")` at deploy time.
40-
41-
```toml
42-
[project]
43-
name = "my-ci-runners"
44-
requires-python = ">=3.12"
45-
dependencies = ["modal>=1.5.2,<2", "runner-modal"]
46-
47-
[tool.uv.sources]
48-
runner-modal = { git = "https://github.com/modal-projects/runner-modal.git", tag = "v0.1.0" }
49-
```
50-
51-
```bash
52-
uv sync
53-
```
54-
55-
### 2. Split Secrets
56-
57-
Never merge these. Workflow code on a Job can read every mounted env key.
20+
- Pin with `uv add git+https://github.com/modal-projects/runner-modal.git@v0.1.0`.
21+
Deploy from a checkout so Images can `add_local_python_source("runner_modal")`.
22+
- Two named Secrets, never one: `github-token` (`GITHUB_TOKEN`, Jobs only)
23+
and `github-webhook` (`WEBHOOK_SECRET`, webhook Function only).
24+
- One `Runner.create` per `modal.App` = one resource profile. GPU is a
25+
separate App. `repositories` required. Labels must include `self-hosted`.
26+
- `cpu` / `memory` / `gpu` are Job defaults, not webhook Function size.
27+
- Do not use a Modal Server for the webhook (503 on zero→one). Inputs queue.
28+
- Redeploy overwrites Runner meta (last deploy wins).
5829

5930
```bash
6031
modal secret create github-token GITHUB_TOKEN=ghp_xxx
6132
modal secret create github-webhook WEBHOOK_SECRET=$(openssl rand -hex 32)
33+
modal deploy examples/basic/app.py
6234
```
6335

64-
| Secret | Keys | Mounted on |
65-
|--------|------|------------|
66-
| `github-token` | `GITHUB_TOKEN` | Job Sandboxes only |
67-
| `github-webhook` | `WEBHOOK_SECRET` | webhook Function only |
68-
69-
PAT or GitHub App: `POST /repos/{owner}/{repo}/actions/runners/generate-jitconfig`
70-
on allowlisted repos only (Administration: Read & write).
71-
72-
### 3. One App per pool
73-
74-
One `Runner.create` per `modal.App` = one resource profile. GPU is a
75-
**separate App**, not a label on the CPU pool. Redeploy overwrites Runner
76-
meta (last deploy wins).
77-
78-
`repositories` is required and non-empty. `labels` must include `self-hosted`.
79-
Admission is fail-closed.
80-
81-
Start from `examples/basic/app.py` or `examples/gpu/app.py`:
82-
83-
```python
84-
import modal
85-
from runner_modal import Runner
86-
87-
app = modal.App("myorg-ci")
88-
github = modal.Secret.from_name("github-token", required_keys=["GITHUB_TOKEN"])
89-
webhook = modal.Secret.from_name("github-webhook", required_keys=["WEBHOOK_SECRET"])
90-
91-
Runner.create(
92-
app=app,
93-
name="ci",
94-
github_secret=github,
95-
webhook_secret=webhook,
96-
repositories=["YOUR_ORG/YOUR_REPO"],
97-
labels=["self-hosted", "modal", "ci"],
98-
region="us-east",
99-
cpu=2.0,
100-
memory=4096,
101-
max_concurrent=20,
102-
min_containers=0,
103-
idle_timeout=900,
104-
)
105-
```
106-
107-
`cpu` / `memory` / `gpu` are Job Sandbox defaults. The webhook Function stays
108-
small (`buffer_containers=1` by default). Do not attach this as a Modal Server
109-
(503 on zero→one). Inputs queue on the Function.
110-
111-
Nordlys shape: three pools, three Apps (`nordlys-ci`, `nordlys-ci-core`,
112-
`nordlys-ci-gpu`), same two Secrets.
113-
114-
### 4. Deploy
115-
116-
```bash
117-
uv run modal deploy cpu_app.py
118-
```
119-
120-
`url` is `None` until the Function is deployed.
121-
122-
### 5. Webhook per pool
123-
124-
```python
125-
from runner_modal import Runner
126-
print(Runner.from_name("ci").url) # …/webhook → payload is {url}/github
127-
```
128-
129-
| Field | Value |
130-
|-------|--------|
131-
| Payload URL | `{url}/github` |
132-
| Content type | `application/json` |
133-
| Secret | same as `WEBHOOK_SECRET` |
134-
| Events | **Workflow jobs** only |
135-
136-
One webhook per pool. Same secret is fine.
137-
138-
### 6. Health
36+
Webhook: `{Runner.from_name("ci").url}/github`, `application/json`,
37+
**Workflow jobs** only, same `WEBHOOK_SECRET`. One webhook per pool.
13938

14039
```bash
14140
curl -sS "$URL/health"
14241
```
14342

144-
Returns `runner`, `labels`, `repositories` (count), `active_runners`,
145-
`max_concurrent`. `GET /health` is public.
146-
147-
## Ops
148-
149-
| Task | Command |
150-
|------|---------|
151-
| Logs (webhook Function) | `modal app logs <app-name>` |
152-
| Teardown pool state | `uv run python -c "from runner_modal import Runner; Runner.objects.delete('ci')"` |
153-
| Stop App | `modal app stop <app-name>` then remove the repo webhook |
154-
155-
`Runner.objects.delete` removes `{name}-runner-meta`, `{name}-runner-deliveries`,
156-
and `{name}-cache`. GitHub runner rows are ephemeral (JIT); they appear only
157-
while a Job runs.
158-
159-
## Cost knobs
160-
161-
Per pool: `cpu` / `memory` / `gpu`, soft `max_concurrent` (list-then-create,
162-
not a lock), `idle_timeout` (Job linger), `min_containers` / `buffer_containers`
163-
(webhook warm pool; keep at `0` / `1`).
164-
165-
## Public-repo forks
43+
Teardown: `Runner.objects.delete(name)`, then `modal app stop` and remove
44+
the GitHub webhook. JIT runner rows exist only while a Job runs.
16645

167-
Self-hosted Jobs run workflow code with `GITHUB_TOKEN` in the Sandbox. Prefer
168-
approval for outside collaborators and read-only default workflow permissions.
169-
See `SECURITY.md`.
46+
`max_concurrent` is soft (list-then-create), not a lock.
Lines changed: 16 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,26 @@
11
---
22
name: runner-modal-images
33
description: >-
4-
Customizes runner-modal Job images on Modal (Named Image {name}-job,
5-
GPU pools, Docker VM runtime, shared /cache Volume). Use when rebuilding
6-
the Actions runner recipe, publishing over ci-job, adding gcc/cmake/CUDA,
7-
enabling cache=True, or setting experimental_options vm_runtime. Not for
8-
generic Modal Image tutorials or webhook Function sizing.
4+
Customizes runner-modal Job images (Named Image {name}-job, GPU, Docker
5+
VM, /cache). Use when changing the Job recipe, enabling cache=True, or
6+
setting vm_runtime. Not for generic Modal Image tutorials.
97
---
108

119
# Customizing runner-modal Job images
1210

13-
Webhook `Job.create` loads the published Named Image `{name}-job`.
14-
`Runner.create(image=)` is the **webhook Function** image
15-
(`control_plane_image()`), not the Job image.
11+
Jobs load the published Named Image `{name}-job`.
12+
`Runner.create(image=)` is the **webhook Function** image only.
1613

17-
Recipes: [references/image-recipes.md](references/image-recipes.md).
18-
Deploy first: `runner-modal-deploy`. Workflows: `runner-modal-workflows`.
14+
## Rules
1915

20-
Worked example: [Nordlys `runners/core_app.py`](https://github.com/Nordlys-Labs/nordlys/blob/main/runners/core_app.py)
21-
(Ubuntu 24.04 + gcc 13, publish over `ci_core-job`, `cache=True`).
16+
- Default CPU: `Runner.create` publishes `default_image()` as `{name}-job`.
17+
- GPU: separate App, `gpu=` on `Runner.create`. See `examples/gpu/app.py`.
18+
- Docker-in-CI: `experimental_options={"vm_runtime": True}` (no GPU).
19+
- Custom toolchain: build with `Runner.install_actions_runner`, then
20+
`.publish(f"{name}-job")` **after** `Runner.create`. Last publish wins.
21+
Keep `JOB_DEPS`, `add_local_python_source("runner_modal", copy=True)`,
22+
and `RUNNER_ALLOW_RUNASROOT=1`. No `Path` / `add_local_dir`.
23+
- `cache=True` mounts `{name}-cache` at `/cache` for every Job in that
24+
pool. Same-pool only. Default is `cache=False`.
2225

23-
## Choose a path
24-
25-
| Need | Do |
26-
|------|-----|
27-
| Default CPU CI | `Runner.create` only. Publishes `{name}-job` from `default_image()` (Debian slim + Actions runner) |
28-
| CUDA / ML | Separate pool. `gpu="T4"` (or other) on `Runner.create`. Default Job image is enough if CUDA comes from pip wheels |
29-
| Docker-in-CI | `experimental_options={"vm_runtime": True}` — **no GPU**. Library publishes `docker_image()` |
30-
| Different distro / gcc / cmake | Rebuild with `install_actions_runner`, then publish over `{name}-job` **after** `Runner.create` (last publish wins) |
31-
| Shared compile cache | `cache=True` → Volume at `/cache`, same-pool only |
32-
33-
Do not invent `ResourceSpec` / `DockerResources`. Flat kwargs: `cpu`,
34-
`memory`, `gpu`, `experimental_options`. Modal enforces GPU vs VM limits.
35-
36-
## Default publish
37-
38-
`Runner.create` builds `default_image()` (or `docker_image()` when
39-
`vm_runtime`) and publishes it as `{name}-job`. Webhook Jobs use
40-
`Image.from_name`. Every deploy republishes the default recipe first.
41-
42-
## Custom Job image
43-
44-
Mirror the library recipe, then overwrite the Named Image. `add_local_python_source("runner_modal", copy=True)` is required until PyPI.
45-
46-
```python
47-
import modal
48-
from runner_modal import Runner
49-
from runner_modal.meta import JOB_DEPS
50-
51-
APP_NAME = "myorg-ci-core"
52-
RUNNER_NAME = "ci_core"
53-
54-
app = modal.App(APP_NAME)
55-
github = modal.Secret.from_name("github-token", required_keys=["GITHUB_TOKEN"])
56-
webhook = modal.Secret.from_name("github-webhook", required_keys=["WEBHOOK_SECRET"])
57-
58-
core_job_image = Runner.install_actions_runner(
59-
modal.Image.from_registry("ubuntu:24.04", add_python="3.12")
60-
.apt_install(
61-
"curl", "ca-certificates", "git", "libicu-dev",
62-
"liblttng-ust1t64", "libssl3t64", "tar", "unzip", "zip",
63-
"build-essential", "gcc-13", "g++-13", "ninja-build", "pkg-config",
64-
)
65-
.uv_pip_install(*JOB_DEPS)
66-
.uv_pip_install("cmake>=3.28", "conan>=2")
67-
.add_local_python_source("runner_modal", copy=True)
68-
.env({"RUNNER_ALLOW_RUNASROOT": "1", "CC": "gcc-13", "CXX": "g++-13"})
69-
)
70-
71-
Runner.create(
72-
app=app,
73-
name=RUNNER_NAME,
74-
github_secret=github,
75-
webhook_secret=webhook,
76-
repositories=["YOUR_ORG/YOUR_REPO"],
77-
labels=["self-hosted", "modal", "ci-core"],
78-
cpu=4.0,
79-
memory=8192,
80-
cache=True,
81-
min_containers=0,
82-
idle_timeout=900,
83-
)
84-
85-
build_app = modal.App.lookup(APP_NAME, create_if_missing=True)
86-
core_job_image.build(build_app).publish(f"{RUNNER_NAME}-job")
87-
```
88-
89-
Order: `Runner.create` (publishes stock `{name}-job`) → custom
90-
`.publish(f"{name}-job")`. Last publish wins. Redeploy both steps.
91-
92-
No `Path` / `add_local_dir`. Use uv Image methods.
93-
94-
## GPU pool
95-
96-
Separate App. Host driver comes from Modal; CUDA userspace from wheels is
97-
fine on the default Job image.
98-
99-
```python
100-
Runner.create(
101-
app=app,
102-
name="ci_gpu",
103-
github_secret=github,
104-
webhook_secret=webhook,
105-
repositories=["YOUR_ORG/YOUR_REPO"],
106-
labels=["self-hosted", "modal", "ci-gpu"],
107-
gpu="T4",
108-
cpu=4.0,
109-
memory=16384,
110-
max_concurrent=2,
111-
min_containers=0,
112-
idle_timeout=900,
113-
)
114-
```
115-
116-
See `examples/gpu/app.py`.
117-
118-
## Docker-in-CI
119-
120-
```python
121-
Runner.create(
122-
…,
123-
experimental_options={"vm_runtime": True},
124-
# no gpu=
125-
)
126-
```
127-
128-
## `/cache`
129-
130-
`cache=True` creates `{name}-cache` and mounts it at `/cache` for every Job
131-
in that pool. Same-pool Jobs can read/write each other’s files. Default is
132-
`cache=False`. This is not GitHub `actions/cache`.
133-
134-
Workflows must point tools at `/cache` (`runner-modal-workflows`).
26+
Point tools at `/cache` in the workflow when you enable it.

0 commit comments

Comments
 (0)