Only registered runners claim jobs; personal tokens can't (#118)
* Only registered runners claim jobs: personal tokens can't A personal token kept on a runner machine can be read by the jobs it runs. Runners register with a one-hour registration token (or the signed-in CLI does it for them) and claim with their own token, so the personal-token path is closed. e2e tests claim as registered runners. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yhgAu6oKfqes812Yd4qjo * Email e2e: the ignored-repository runs claim as a registered runner #114 added a personal-token claim that this PR turns away. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yhgAu6oKfqes812Yd4qjo * Environment reviewers e2e: the deploy jobs claim as a registered runner Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yhgAu6oKfqes812Yd4qjo --------- Co-authored-by: Claude <noreply@anthropic.com>
Canberk Demirci authored · GitHub committed signed1 parent ebca377commit d22843396f713412f94ff4d3a6095ee33408978bBrowse files
8 files changed+106−35
Files (8)
- apps/diffinery/docs/actions/runners.md+1−1
- apps/diffinery/src/http/api.ts+4−7
- apps/diffinery/src/repo/RepoDO.ts+0−3
- apps/diffinery/src/repo/actions.ts+0−9
- apps/diffinery/test/e2e/e2e.test.mjs+26−5
- apps/diffinery/test/e2e/email.test.mjs+23−2
- apps/diffinery/test/e2e/registry.test.mjs+22−1
- apps/diffinery/test/e2e/security.test.mjs+30−7
modifiedapps/diffinery/docs/actions/runners.md+1−1View file
| @@ -272,7 +272,7 @@ | ||
| 272 | 272 | |
| 273 | 273 | If the runner can't reach Diffinery it prints `warning: ... (will retry)` and keeps polling. A `401`, `403` or `404` from the claim request stops the runner, because retrying won't help. For a removed runner it says `runner <name> was removed from <owner>/<repo>; delete <file> and register it again`. |
| 274 | 274 | |
| 275 | -A repository admin's personal token with the `admin` scope can also claim jobs, sending the runner's name and labels in the body (`{"name": "my-mac", "labels": ["macos", "arm64"]}`). This is how runners worked before registration, and the CLI still falls back to it on a Diffinery server without registration. | |
| 275 | +Only registered runners can claim jobs. A personal token, even a repository admin's, gets `403` with `personal tokens can't claim jobs; register this runner: ...`. Runners used to claim that way before registration. If an older runner stops with that message, upgrade the CLI and start it again while signed in, which registers it, or register it with `--registration-token`. | |
| 276 | 276 | |
| 277 | 277 | ### Manage runners |
| 278 | 278 |
modifiedapps/diffinery/src/http/api.ts+4−7View file
| @@ -883,8 +883,8 @@ | ||
| 883 | 883 | return c.json({ run_id: run.id, html_url: `${c.env.PUBLIC_URL}/${P(c).owner}/${P(c).repo}/actions/runs/${run.id}` }, 201); |
| 884 | 884 | }); |
| 885 | 885 | // Self-hosted runners poll this to pick up `runs-on: [self-hosted, ...]` jobs (see `diffinery runner`). |
| 886 | -// A registered runner claims with its own token and the labels it registered with; a repository | |
| 887 | -// admin's personal token still works too, offering the labels in the body. | |
| 886 | +// Only a registered runner claims, with its own token and the labels it registered with. A person's | |
| 887 | +// token can't: it would sit on the runner machine where the jobs it runs could read it. | |
| 888 | 888 | api.post("/repos/:owner/:repo/actions/runners/claim", async (c) => { |
| 889 | 889 | const p = c.get("principal"); |
| 890 | 890 | if (p.type === "repo-token" && p.token.startsWith("drn_")) { |
| @@ -894,11 +894,8 @@ | ||
| 894 | 894 | if (!job) return c.body(null, 204); |
| 895 | 895 | return c.json({ job_id: job.jobId, run_id: job.runId, name: job.name, token: job.token, api_url: new URL(c.req.url).origin }); |
| 896 | 896 | } |
| 897 | - const { stub, caller } = await loadRepo(c, P(c).owner, P(c).repo, "admin"); | |
| 898 | - const b = await body<{ name?: string; labels?: string[] }>(c); | |
| 899 | - const job = await stub.claimJob(b.name ?? "runner", Array.isArray(b.labels) ? b.labels.map(String) : [], caller); | |
| 900 | - if (!job) return c.body(null, 204); | |
| 901 | - return c.json({ job_id: job.jobId, run_id: job.runId, name: job.name, token: job.token, api_url: new URL(c.req.url).origin }); | |
| 897 | + await loadRepo(c, P(c).owner, P(c).repo, "admin"); | |
| 898 | + throw new HttpError(403, `personal tokens can't claim jobs; register this runner: run \`diffinery runner -R ${P(c).owner}/${P(c).repo}\` signed in with an up-to-date CLI, or pass --registration-token (see /docs/actions/runners)`); | |
| 902 | 899 | }); |
| 903 | 900 | // GitHub's runner registration token: an hour to register runners (`diffinery runner --registration-token`). |
| 904 | 901 | api.post("/repos/:owner/:repo/actions/runners/registration-token", async (c) => { |
modifiedapps/diffinery/src/repo/RepoDO.ts+0−3View file
| @@ -942,9 +942,6 @@ | ||
| 942 | 942 | runJobs(...a: Parameters<Actions["jobs"]>) { |
| 943 | 943 | return this.actions.jobs(...a); |
| 944 | 944 | } |
| 945 | - claimJob(...a: Parameters<Actions["claimJob"]>) { | |
| 946 | - return this.actions.claimJob(...a); | |
| 947 | - } | |
| 948 | 945 | claimJobAsRunner(...a: Parameters<Actions["claimJobAsRunner"]>) { |
| 949 | 946 | return this.actions.claimJobAsRunner(...a); |
| 950 | 947 | } |
modifiedapps/diffinery/src/repo/actions.ts+0−9View file
| @@ -1276,15 +1276,6 @@ | ||
| 1276 | 1276 | await this.completeJob(jobId, { conclusion: "failure", outputs: {}, failure: { message: exitCode ? `The runner stopped unexpectedly (exit code ${exitCode}).` : "The runner's container stopped before the job finished, without an exit code (the platform restarted or replaced it)." } }); |
| 1277 | 1277 | } |
| 1278 | 1278 | |
| 1279 | - /** | |
| 1280 | - * A self-hosted runner asks for work. Returns the oldest waiting job whose | |
| 1281 | - * runs-on labels are all offered by the runner, with a fresh job token. | |
| 1282 | - */ | |
| 1283 | - async claimJob(runnerName: string, runnerLabels: string[], caller: Caller): Promise<{ jobId: string; token: string; runId: string; name: string } | null> { | |
| 1284 | - if (!atLeast(caller.role, "admin")) throw new HttpError(403, "admin access is required to run self-hosted jobs"); | |
| 1285 | - return this.assignJob(runnerName, runnerLabels, null); | |
| 1286 | - } | |
| 1287 | - | |
| 1288 | 1279 | /** The oldest waiting job whose runs-on labels the runner all offers, handed to it with a fresh job token. */ |
| 1289 | 1280 | private async assignJob(runnerName: string, runnerLabels: string[], runnerId: number | null): Promise<{ jobId: string; token: string; runId: string; name: string } | null> { |
| 1290 | 1281 | const offered = new Set(["self-hosted", ...runnerLabels.map((l) => l.toLowerCase())]); |
modifiedapps/diffinery/test/e2e/e2e.test.mjs+26−5View file
| @@ -53,6 +53,27 @@ | ||
| 53 | 53 | } |
| 54 | 54 | return { status: res.status, json }; |
| 55 | 55 | } |
| 56 | + | |
| 57 | +/** | |
| 58 | + * Claim a waiting self-hosted job as a registered runner offering `labels`, the way `diffinery runner` | |
| 59 | + * does (personal tokens can't claim jobs): one runner per repository and set of labels. | |
| 60 | + */ | |
| 61 | +const e2eRunners = new Map(); | |
| 62 | +async function runnerClaim(repoPath, labels = ["linux"]) { | |
| 63 | + const key = `${repoPath} ${labels.join(",")}`; | |
| 64 | + if (!e2eRunners.has(key)) { | |
| 65 | + const reg = await api("POST", `${repoPath}/actions/runners/registration-token`); | |
| 66 | + assert.equal(reg.status, 201, JSON.stringify(reg.json)); | |
| 67 | + const runner = await api("POST", `${repoPath}/actions/runners/register`, { registration_token: reg.json.token, name: `e2e-runner-${e2eRunners.size + 1}`, labels }); | |
| 68 | + assert.equal(runner.status, 201, JSON.stringify(runner.json)); | |
| 69 | + e2eRunners.set(key, runner.json.token); | |
| 70 | + } | |
| 71 | + const r = await api("POST", `${repoPath}/actions/runners/claim`, {}, e2eRunners.get(key)); | |
| 72 | + // A repository deleted and created again under the same name needs a new runner. | |
| 73 | + if (r.status !== 401) return r; | |
| 74 | + e2eRunners.delete(key); | |
| 75 | + return runnerClaim(repoPath, labels); | |
| 76 | +} | |
| 56 | 77 | async function ok(method, path, body, tok) { |
| 57 | 78 | const r = await api(method, path, body, tok); |
| 58 | 79 | assert.ok(r.status >= 200 && r.status < 300, `${method} ${path} -> ${r.status} ${JSON.stringify(r.json)}`); |
| @@ -111,7 +132,7 @@ | ||
| 111 | 132 | */ |
| 112 | 133 | async function runOneJob(owner, repo, { allowFailure = false, envFor = () => ({}), timeoutMs = 180_000, labels = ["linux"] } = {}) { |
| 113 | 134 | const claim = await until("a job to claim", async () => { |
| 114 | - const r = await api("POST", `/repos/${owner}/${repo}/actions/runners/claim`, { name: "e2e", labels }); | |
| 135 | + const r = await runnerClaim(`/repos/${owner}/${repo}`, labels); | |
| 115 | 136 | return r.status === 200 ? r.json : null; |
| 116 | 137 | }); |
| 117 | 138 | const jobEnv = { ...envFor(claim), DIFFINERY_API_URL: claim.api_url, DIFFINERY_JOB_ID: claim.job_id, DIFFINERY_JOB_TOKEN: claim.token }; |
| @@ -1235,7 +1256,7 @@ | ||
| 1235 | 1256 | /** Claims the job a push started and starts it, as an older runner would, without running steps. */ |
| 1236 | 1257 | const startJob = async () => { |
| 1237 | 1258 | const claim = await until("a job to claim", async () => { |
| 1238 | - const r = await api("POST", `/repos/${owner}/legacy-cache/actions/runners/claim`, { name: "old-runner", labels: ["linux"] }); | |
| 1259 | + const r = await runnerClaim(`/repos/${owner}/legacy-cache`, ["linux"]); | |
| 1239 | 1260 | return r.status === 200 ? r.json : null; |
| 1240 | 1261 | }); |
| 1241 | 1262 | const h = { authorization: `Bearer ${claim.token}` }; |
| @@ -1297,7 +1318,7 @@ | ||
| 1297 | 1318 | git(src, ["remote", "add", "origin", `${base}/${owner}/legacy-artifacts.git`]); |
| 1298 | 1319 | git(src, ["push", "-q", "origin", "main"]); |
| 1299 | 1320 | const claim = await until("a job to claim", async () => { |
| 1300 | - const r = await api("POST", `/repos/${owner}/legacy-artifacts/actions/runners/claim`, { name: "old-runner", labels: ["linux"] }); | |
| 1321 | + const r = await runnerClaim(`/repos/${owner}/legacy-artifacts`, ["linux"]); | |
| 1301 | 1322 | return r.status === 200 ? r.json : null; |
| 1302 | 1323 | }); |
| 1303 | 1324 | const at = `${base}/runner/jobs/${claim.job_id}/artifacts/out`; |
| @@ -1333,7 +1354,7 @@ | ||
| 1333 | 1354 | git(src, ["remote", "add", "origin", `${base}/${owner}/legacy-rest.git`]); |
| 1334 | 1355 | git(src, ["push", "-q", "origin", "main"]); |
| 1335 | 1356 | const claim = await until("a job to claim", async () => { |
| 1336 | - const r = await api("POST", `/repos/${owner}/legacy-rest/actions/runners/claim`, { name: "old-runner", labels: ["linux"] }); | |
| 1357 | + const r = await runnerClaim(`/repos/${owner}/legacy-rest`, ["linux"]); | |
| 1337 | 1358 | return r.status === 200 ? r.json : null; |
| 1338 | 1359 | }); |
| 1339 | 1360 | const h = { authorization: `Bearer ${claim.token}` }; |
| @@ -2505,7 +2526,7 @@ | ||
| 2505 | 2526 | assert.equal((await api("PUT", `/repos/${owner}/imported-app/actions/secrets/DEPLOY_KEY`, { value: "never-for-forks" })).status, 204); |
| 2506 | 2527 | await ok("PATCH", `/repos/${owner}/imported-app/pulls/6`, { state: "open" }); |
| 2507 | 2528 | const forkJob = await until("the fork pull request's job", async () => { |
| 2508 | - const r = await api("POST", `/repos/${owner}/imported-app/actions/runners/claim`, { name: "fork-runner", labels: ["linux"] }); | |
| 2529 | + const r = await runnerClaim(`/repos/${owner}/imported-app`, ["linux"]); | |
| 2509 | 2530 | return r.status === 200 ? r.json : null; |
| 2510 | 2531 | }); |
| 2511 | 2532 | const forkSpec = await (await fetch(`${base}/runner/jobs/${forkJob.job_id}`, { headers: { authorization: `Bearer ${forkJob.token}` } })).json(); |
modifiedapps/diffinery/test/e2e/email.test.mjs+23−2View file
| @@ -51,6 +51,27 @@ | ||
| 51 | 51 | } |
| 52 | 52 | return { status: res.status, json }; |
| 53 | 53 | } |
| 54 | + | |
| 55 | +/** | |
| 56 | + * Claim a waiting self-hosted job as a registered runner offering `labels`, the way `diffinery runner` | |
| 57 | + * does (personal tokens can't claim jobs): one runner per repository and set of labels. | |
| 58 | + */ | |
| 59 | +const e2eRunners = new Map(); | |
| 60 | +async function runnerClaim(repoPath, labels = ["linux"]) { | |
| 61 | + const key = `${repoPath} ${labels.join(",")}`; | |
| 62 | + if (!e2eRunners.has(key)) { | |
| 63 | + const reg = await api("POST", `${repoPath}/actions/runners/registration-token`); | |
| 64 | + assert.equal(reg.status, 201, JSON.stringify(reg.json)); | |
| 65 | + const runner = await api("POST", `${repoPath}/actions/runners/register`, { registration_token: reg.json.token, name: `e2e-runner-${e2eRunners.size + 1}`, labels }); | |
| 66 | + assert.equal(runner.status, 201, JSON.stringify(runner.json)); | |
| 67 | + e2eRunners.set(key, runner.json.token); | |
| 68 | + } | |
| 69 | + const r = await api("POST", `${repoPath}/actions/runners/claim`, {}, e2eRunners.get(key)); | |
| 70 | + // A repository deleted and created again under the same name needs a new runner. | |
| 71 | + if (r.status !== 401) return r; | |
| 72 | + e2eRunners.delete(key); | |
| 73 | + return runnerClaim(repoPath, labels); | |
| 74 | +} | |
| 54 | 75 | async function ok(method, path, body, tok) { |
| 55 | 76 | const r = await api(method, path, body, tok); |
| 56 | 77 | assert.ok(r.status >= 200 && r.status < 300, `${method} ${path} -> ${r.status} ${JSON.stringify(r.json)}`); |
| @@ -245,7 +266,7 @@ | ||
| 245 | 266 | git(src, ["remote", "add", "origin", `${base}/${owner}/mailbox.git`]); |
| 246 | 267 | git(src, ["push", "-q", "origin", "main"]); |
| 247 | 268 | const claim = await until("a job to claim", async () => { |
| 248 | - const r = await api("POST", `/repos/${owner}/mailbox/actions/runners/claim`, { name: "e2e", labels: ["linux"] }); | |
| 269 | + const r = await runnerClaim(`/repos/${owner}/mailbox`, ["linux"]); | |
| 249 | 270 | return r.status === 200 ? r.json : null; |
| 250 | 271 | }); |
| 251 | 272 | await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: claim.api_url, DIFFINERY_JOB_ID: claim.job_id, DIFFINERY_JOB_TOKEN: claim.token, DIFFINERY_WORK_ROOT: join(work, `job-${claim.job_id}`) }, 180_000); |
| @@ -262,7 +283,7 @@ | ||
| 262 | 283 | git(src, ["commit", "-q", "-m", `Add ${file}`]); |
| 263 | 284 | git(src, ["push", "-q", "origin", "main"]); |
| 264 | 285 | const c = await until("a job to claim", async () => { |
| 265 | - const r = await api("POST", `/repos/${owner}/mailbox/actions/runners/claim`, { name: "e2e", labels: ["linux"] }); | |
| 286 | + const r = await runnerClaim(`/repos/${owner}/mailbox`, ["linux"]); | |
| 266 | 287 | return r.status === 200 ? r.json : null; |
| 267 | 288 | }); |
| 268 | 289 | await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: c.api_url, DIFFINERY_JOB_ID: c.job_id, DIFFINERY_JOB_TOKEN: c.token, DIFFINERY_WORK_ROOT: join(work, `job-${c.job_id}`) }, 180_000); |
modifiedapps/diffinery/test/e2e/registry.test.mjs+22−1View file
| @@ -75,6 +75,27 @@ | ||
| 75 | 75 | } |
| 76 | 76 | return { status: res.status, json }; |
| 77 | 77 | } |
| 78 | + | |
| 79 | +/** | |
| 80 | + * Claim a waiting self-hosted job as a registered runner offering `labels`, the way `diffinery runner` | |
| 81 | + * does (personal tokens can't claim jobs): one runner per repository and set of labels. | |
| 82 | + */ | |
| 83 | +const e2eRunners = new Map(); | |
| 84 | +async function runnerClaim(repoPath, labels = ["linux"]) { | |
| 85 | + const key = `${repoPath} ${labels.join(",")}`; | |
| 86 | + if (!e2eRunners.has(key)) { | |
| 87 | + const reg = await api("POST", `${repoPath}/actions/runners/registration-token`); | |
| 88 | + assert.equal(reg.status, 201, JSON.stringify(reg.json)); | |
| 89 | + const runner = await api("POST", `${repoPath}/actions/runners/register`, { registration_token: reg.json.token, name: `e2e-runner-${e2eRunners.size + 1}`, labels }); | |
| 90 | + assert.equal(runner.status, 201, JSON.stringify(runner.json)); | |
| 91 | + e2eRunners.set(key, runner.json.token); | |
| 92 | + } | |
| 93 | + const r = await api("POST", `${repoPath}/actions/runners/claim`, {}, e2eRunners.get(key)); | |
| 94 | + // A repository deleted and created again under the same name needs a new runner. | |
| 95 | + if (r.status !== 401) return r; | |
| 96 | + e2eRunners.delete(key); | |
| 97 | + return runnerClaim(repoPath, labels); | |
| 98 | +} | |
| 78 | 99 | async function ok(method, path, body, tok) { |
| 79 | 100 | const r = await api(method, path, body, tok); |
| 80 | 101 | assert.ok(r.status >= 200 && r.status < 300, `${method} ${path} -> ${r.status} ${JSON.stringify(r.json)}`); |
| @@ -431,7 +452,7 @@ | ||
| 431 | 452 | |
| 432 | 453 | async function runOneJob(repo) { |
| 433 | 454 | const claim = await until("a job to claim", async () => { |
| 434 | - const r = await api("POST", `/repos/${owner}/${repo}/actions/runners/claim`, { name: "e2e", labels: ["linux"] }); | |
| 455 | + const r = await runnerClaim(`/repos/${owner}/${repo}`, ["linux"]); | |
| 435 | 456 | return r.status === 200 ? r.json : null; |
| 436 | 457 | }); |
| 437 | 458 | const r = await spawnAsync(process.execPath, [AGENT], { DIFFINERY_API_URL: claim.api_url, DIFFINERY_JOB_ID: claim.job_id, DIFFINERY_JOB_TOKEN: claim.token, DIFFINERY_WORK_ROOT: join(work, `job-${claim.job_id}`) }, 180_000); |
modifiedapps/diffinery/test/e2e/security.test.mjs+30−7View file
| @@ -80,6 +80,27 @@ | ||
| 80 | 80 | } |
| 81 | 81 | return { status: res.status, json }; |
| 82 | 82 | } |
| 83 | + | |
| 84 | +/** | |
| 85 | + * Claim a waiting self-hosted job as a registered runner offering `labels`, the way `diffinery runner` | |
| 86 | + * does (personal tokens can't claim jobs): one runner per repository and set of labels. | |
| 87 | + */ | |
| 88 | +const e2eRunners = new Map(); | |
| 89 | +async function runnerClaim(repoPath, labels = ["linux"]) { | |
| 90 | + const key = `${repoPath} ${labels.join(",")}`; | |
| 91 | + if (!e2eRunners.has(key)) { | |
| 92 | + const reg = await api("POST", `${repoPath}/actions/runners/registration-token`); | |
| 93 | + assert.equal(reg.status, 201, JSON.stringify(reg.json)); | |
| 94 | + const runner = await api("POST", `${repoPath}/actions/runners/register`, { registration_token: reg.json.token, name: `e2e-runner-${e2eRunners.size + 1}`, labels }); | |
| 95 | + assert.equal(runner.status, 201, JSON.stringify(runner.json)); | |
| 96 | + e2eRunners.set(key, runner.json.token); | |
| 97 | + } | |
| 98 | + const r = await api("POST", `${repoPath}/actions/runners/claim`, {}, e2eRunners.get(key)); | |
| 99 | + // A repository deleted and created again under the same name needs a new runner. | |
| 100 | + if (r.status !== 401) return r; | |
| 101 | + e2eRunners.delete(key); | |
| 102 | + return runnerClaim(repoPath, labels); | |
| 103 | +} | |
| 83 | 104 | async function ok(method, path, body, as) { |
| 84 | 105 | const r = await api(method, path, body, as); |
| 85 | 106 | assert.ok(r.status >= 200 && r.status < 300, `${method} ${path} -> ${r.status} ${JSON.stringify(r.json)}`); |
| @@ -1699,7 +1720,7 @@ | ||
| 1699 | 1720 | await ok("PUT", `${R}/contents/.github/workflows/ci.yml`, { message: "ci", content: b64(wf) }); |
| 1700 | 1721 | const specs = {}; |
| 1701 | 1722 | await until("both jobs claimed", async () => { |
| 1702 | - const r = await api("POST", `${R}/actions/runners/claim`, { name: "secrets-runner", labels: ["linux"] }); | |
| 1723 | + const r = await runnerClaim(R, ["linux"]); | |
| 1703 | 1724 | if (r.status === 200) { |
| 1704 | 1725 | const spec = await (await fetch(`${base}/runner/jobs/${r.json.job_id}`, { headers: { authorization: `Bearer ${r.json.token}` } })).json(); |
| 1705 | 1726 | specs[spec.jobKey] = Object.keys(spec.secrets).sort(); |
| @@ -1723,7 +1744,7 @@ | ||
| 1723 | 1744 | const secretsOfNextJob = async () => { |
| 1724 | 1745 | let names; |
| 1725 | 1746 | await until("the agent's job", async () => { |
| 1726 | - const r = await api("POST", `${R}/actions/runners/claim`, { name: "agent-runner", labels: ["linux"] }); | |
| 1747 | + const r = await runnerClaim(R, ["linux"]); | |
| 1727 | 1748 | if (r.status !== 200) return false; |
| 1728 | 1749 | const spec = await (await fetch(`${base}/runner/jobs/${r.json.job_id}`, { headers: { authorization: `Bearer ${r.json.token}` } })).json(); |
| 1729 | 1750 | names = Object.keys(spec.secrets).sort(); |
| @@ -1786,7 +1807,7 @@ | ||
| 1786 | 1807 | await ok("PUT", `${R}/contents/.github/workflows/ci.yml`, { message: "ci", content: b64(wf) }); |
| 1787 | 1808 | let claim; |
| 1788 | 1809 | await until("the job claimed", async () => { |
| 1789 | - const r = await api("POST", `${R}/actions/runners/claim`, { name: "log-runner", labels: ["linux"] }); | |
| 1810 | + const r = await runnerClaim(R, ["linux"]); | |
| 1790 | 1811 | if (r.status === 200) claim = r.json; |
| 1791 | 1812 | return !!claim; |
| 1792 | 1813 | }); |
| @@ -1822,7 +1843,7 @@ | ||
| 1822 | 1843 | await ok("PUT", `/repos/${OWNER}/pkg-other/contents/.github/workflows/ci.yml`, { message: "ci", content: b64(wf) }); |
| 1823 | 1844 | let claim; |
| 1824 | 1845 | await until("the job claimed", async () => { |
| 1825 | - const r = await api("POST", `/repos/${OWNER}/pkg-other/actions/runners/claim`, { name: "pkg-runner", labels: ["linux"] }); | |
| 1846 | + const r = await runnerClaim(`/repos/${OWNER}/pkg-other`, ["linux"]); | |
| 1826 | 1847 | if (r.status === 200) claim = r.json; |
| 1827 | 1848 | return !!claim; |
| 1828 | 1849 | }); |
| @@ -1853,7 +1874,7 @@ | ||
| 1853 | 1874 | await ok("PUT", `${R}/contents/.github/workflows/ci.yml`, { message: "ci", content: b64(wf) }); |
| 1854 | 1875 | const tokens = {}; |
| 1855 | 1876 | await until("both jobs claimed", async () => { |
| 1856 | - const r = await api("POST", `${R}/actions/runners/claim`, { name: "oidc-runner", labels: ["linux"] }); | |
| 1877 | + const r = await runnerClaim(R, ["linux"]); | |
| 1857 | 1878 | if (r.status === 200) { |
| 1858 | 1879 | const spec = await (await fetch(`${base}/runner/jobs/${r.json.job_id}`, { headers: { authorization: `Bearer ${r.json.token}` } })).json(); |
| 1859 | 1880 | const res = await fetch(`${base}/runner/jobs/${r.json.job_id}/oidc?audience=sts.example.com`, { headers: { authorization: `Bearer ${r.json.token}` } }); |
| @@ -1943,6 +1964,8 @@ | ||
| 1943 | 1964 | // A runner's token claims this repository's jobs and nothing else, with the labels it registered. |
| 1944 | 1965 | await status(404, "GET", R, undefined, m1.token); |
| 1945 | 1966 | await status(401, "POST", `/repos/${OWNER}/runner-other/actions/runners/claim`, {}, m1.token); |
| 1967 | + // A personal token, even a repository admin's, can't claim jobs: only registered runners do. | |
| 1968 | + assert.match((await status(403, "POST", `${R}/actions/runners/claim`, { name: "old-runner", labels: ["linux"] })).message, /register this runner/); | |
| 1946 | 1969 | const wf = "name: ci\non: push\njobs:\n gpu:\n runs-on: [self-hosted, gpu]\n steps:\n - run: echo gpu\n a:\n runs-on: [self-hosted, linux]\n steps:\n - run: echo a\n b:\n runs-on: [self-hosted, linux]\n steps:\n - run: echo b\n"; |
| 1947 | 1970 | await ok("PUT", `${R}/contents/.github/workflows/ci.yml`, { message: "ci", content: b64(wf) }); |
| 1948 | 1971 | let first; |
| @@ -1994,7 +2017,7 @@ | ||
| 1994 | 2017 | if (write) await ok("PUT", `/repos/guild/${name}/contents/.github/workflows/ci.yml`, { message: "ci", content: b64(wf) }); |
| 1995 | 2018 | let names; |
| 1996 | 2019 | await until(`${name}'s job claimed`, async () => { |
| 1997 | - const r = await api("POST", `/repos/guild/${name}/actions/runners/claim`, { name: "org-secrets", labels: ["linux"] }); | |
| 2020 | + const r = await runnerClaim(`/repos/guild/${name}`, ["linux"]); | |
| 1998 | 2021 | if (r.status !== 200) return false; |
| 1999 | 2022 | const spec = await (await fetch(`${base}/runner/jobs/${r.json.job_id}`, { headers: { authorization: `Bearer ${r.json.token}` } })).json(); |
| 2000 | 2023 | names = Object.keys(spec.secrets).filter((k) => !/_TOKEN$/.test(k)).sort(); |
| @@ -2128,7 +2151,7 @@ | ||
| 2128 | 2151 | |
| 2129 | 2152 | const wf = (n) => `name: deploy\non: push\njobs:\n ship${n}:\n runs-on: [self-hosted, linux]\n environment: production\n steps:\n - run: echo ship\n env:\n K: \${{ secrets.DEPLOY_KEY }}\n`; |
| 2130 | 2153 | const claim = async () => { |
| 2131 | - const r = await api("POST", `${R}/actions/runners/claim`, { name: "gate-runner", labels: ["linux"] }); | |
| 2154 | + const r = await runnerClaim(R, ["linux"]); | |
| 2132 | 2155 | if (r.status !== 200) return null; |
| 2133 | 2156 | const spec = await (await fetch(`${base}/runner/jobs/${r.json.job_id}`, { headers: { authorization: `Bearer ${r.json.token}` } })).json(); |
| 2134 | 2157 | await fetch(`${base}/runner/jobs/${r.json.job_id}/complete`, { method: "POST", headers: { authorization: `Bearer ${r.json.token}`, "content-type": "application/json" }, body: JSON.stringify({ conclusion: "success", outputs: {} }) }); |