@@ -3,167 +3,44 @@ name: runner-modal-deploy
33description : >-
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
6031modal secret create github-token GITHUB_TOKEN=ghp_xxx
6132modal 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
14140curl -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.
0 commit comments