Public: anyone can read this; the crew can change it.
The OpenAPI document describes a pull's checks: GET and POST …/pulls/{number}/checks #5391
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5380-the-openapi-document-describes-a-pull-s → main 3 commits fork 1 thread · 1 unresolved
5 files
changed +58
−4
scripts/shards.ts +1 −1
| @@ -454,7 +454,7 @@ export function parseShardSpec( | ||
| 454 | 454 | | { ok: true; none: true } |
| 455 | 455 | | { ok: false; error: string } { |
| 456 | 456 | if (spec === undefined || spec.trim() === "") return { ok: true, none: true }; |
| 457 | const m = /^([1-9][0-9]{0,2})\/([1-9][0-9]{0,2})$/.exec(spec.trim()); | |
| 457 | const m = /^([1-9][0-9]{0,3})\/([1-9][0-9]{0,3})$/.exec(spec.trim()); | |
| 458 | 458 | if (!m) |
| 459 | 459 | return { |
| 460 | 460 | ok: false, |
src/contract/openapi.ts +38 −1
| @@ -19,6 +19,8 @@ interface Route { | ||
| 19 | 19 | readonly query?: Readonly<Record<string, string>>; |
| 20 | 20 | /** The success status, when it is not 200. */ |
| 21 | 21 | readonly status?: number; |
| 22 | /** The success body's fields, by name and type, when it is worth a schema. */ | |
| 23 | readonly reply?: Readonly<Record<string, string>>; | |
| 22 | 24 | /** The refusals it answers, by stable `error` code. */ |
| 23 | 25 | readonly refusals?: Readonly<Record<string, string>>; |
| 24 | 26 | } |
| @@ -481,6 +483,27 @@ export const ROUTES: readonly Route[] = [ | ||
| 481 | 483 | refusals: { bad_anchor: "`path`, `line` and `side` (old or new) name no line of the pull's current diff (422)" }, |
| 482 | 484 | }, |
| 483 | 485 | { method: "post", path: `${REPO}/pulls/{number}/merge`, summary: "Merge a pull (a person's)", auth: true }, |
| 486 | { | |
| 487 | method: "get", | |
| 488 | path: `${REPO}/pulls/{number}/checks`, | |
| 489 | summary: "A pull's checks at its current head; answers { head, state, required, run, runs } with state `none` when no run exists", | |
| 490 | auth: false, | |
| 491 | reply: { head: "string", state: "string", required: "boolean", run: "object", runs: "array" }, | |
| 492 | }, | |
| 493 | { | |
| 494 | method: "post", | |
| 495 | path: `${REPO}/pulls/{number}/checks`, | |
| 496 | summary: "Ask for a check again at the pull's head as it is now (rw scope, a writer's); answers 202 with { run }", | |
| 497 | auth: true, | |
| 498 | status: 202, | |
| 499 | reply: { run: "object" }, | |
| 500 | refusals: { | |
| 501 | forbidden: "only a writer can ask for a check (403)", | |
| 502 | not_open: "only an open pull is checked (409)", | |
| 503 | no_checks: "this forge runs no checks (409)", | |
| 504 | no_check_declared: "this head declares no check, or waits for a writer's approval (409)", | |
| 505 | }, | |
| 506 | }, | |
| 484 | 507 | { |
| 485 | 508 | method: "post", |
| 486 | 509 | path: `${REPO}/pulls/{number}/approve-here`, |
| @@ -1369,7 +1392,21 @@ export function openapi(origin: string, version: string): Record<string, unknown | ||
| 1369 | 1392 | }), |
| 1370 | 1393 | ...(r.auth ? { security: [{ token: [] }] } : {}), |
| 1371 | 1394 | responses: { |
| 1372 | [String(r.status ?? 200)]: { description: "Done" }, | |
| 1395 | [String(r.status ?? 200)]: { | |
| 1396 | description: "Done", | |
| 1397 | ...(r.reply === undefined | |
| 1398 | ? {} | |
| 1399 | : { | |
| 1400 | content: { | |
| 1401 | "application/json": { | |
| 1402 | schema: { | |
| 1403 | type: "object", | |
| 1404 | properties: Object.fromEntries(Object.entries(r.reply).map(([k, t]) => [k, schema(t)])), | |
| 1405 | }, | |
| 1406 | }, | |
| 1407 | }, | |
| 1408 | }), | |
| 1409 | }, | |
| 1373 | 1410 | default: { |
| 1374 | 1411 | description: |
| 1375 | 1412 | "An error: { error, message }; `error` is a stable code" + |
test/openapi-coverage.test.ts −2
| @@ -46,7 +46,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 46 | 46 | "GET /api/v1/repos/{}/{}/labels", |
| 47 | 47 | "GET /api/v1/repos/{}/{}/milestones", |
| 48 | 48 | "GET /api/v1/repos/{}/{}/milestones/{}", |
| 49 | "GET /api/v1/repos/{}/{}/pulls/{}/checks", | |
| 50 | 49 | "GET /api/v1/repos/{}/{}/pulls/{}/review-states", |
| 51 | 50 | "GET /api/v1/repos/{}/{}/pulls/{}/reviews", |
| 52 | 51 | "GET /api/v1/repos/{}/{}/pulls/{}/threads", |
| @@ -100,7 +99,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 100 | 99 | "POST /api/v1/repos/{}/{}/prompts/{}/run", |
| 101 | 100 | "POST /api/v1/repos/{}/{}/proposal-tokens", |
| 102 | 101 | "POST /api/v1/repos/{}/{}/pulls/{}/approve-here", |
| 103 | "POST /api/v1/repos/{}/{}/pulls/{}/checks", | |
| 104 | 102 | "POST /api/v1/repos/{}/{}/pulls/{}/comments", |
| 105 | 103 | "POST /api/v1/repos/{}/{}/pulls/{}/looks", |
| 106 | 104 | "POST /api/v1/repos/{}/{}/pulls/{}/review-states", |
test/openapi-oa-pull-checks.test.ts added +18
| @@ -0,0 +1,18 @@ | ||
| 1 | /** A pull's checks are in the served OpenAPI document. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { openapi } from "../src/contract/openapi.ts"; | |
| 4 | ||
| 5 | const P = "/api/v1/repos/{owner}/{name}/pulls/{number}/checks"; | |
| 6 | ||
| 7 | test("GET and POST …/pulls/{number}/checks are described", () => { | |
| 8 | const paths = openapi("http://x", "1").paths as Record<string, Record<string, any>>; | |
| 9 | expect(paths[P]?.get?.summary).toContain("checks"); | |
| 10 | expect(paths[P]?.post?.responses?.["202"]).toBeDefined(); | |
| 11 | expect(paths[P]?.get?.security).toBeUndefined(); | |
| 12 | expect(paths[P]?.post?.security).toEqual([{ token: [] }]); | |
| 13 | const ok = (m: string, code: string) => | |
| 14 | paths[P]?.[m]?.responses?.[code]?.content?.["application/json"]?.schema?.properties; | |
| 15 | expect(Object.keys(ok("get", "200"))).toEqual(["head", "state", "required", "run", "runs"]); | |
| 16 | expect(Object.keys(ok("post", "202"))).toEqual(["run"]); | |
| 17 | expect(JSON.stringify(paths[P]?.post)).toContain("no_check_declared"); | |
| 18 | }); |
test/shards.test.ts +1
| @@ -513,6 +513,7 @@ describe("parseShardSpec", () => { | ||
| 513 | 513 | expect(parseShardSpec("1/3")).toEqual({ ok: true, index: 1, total: 3 }); |
| 514 | 514 | expect(parseShardSpec("3/3")).toEqual({ ok: true, index: 3, total: 3 }); |
| 515 | 515 | expect(parseShardSpec(" 2/4 ")).toEqual({ ok: true, index: 2, total: 4 }); |
| 516 | expect(parseShardSpec("1000/1000")).toEqual({ ok: true, index: 1000, total: 1000 }); | |
| 516 | 517 | for (const bad of ["0/3", "1/0", "4/3", "1", "1-3", "a/b", "1/3/5"]) |
| 517 | 518 | expect(parseShardSpec(bad).ok).toBe(false); |
| 518 | 519 | }); |
new above
offline
Four digits is a fix for today; consider deriving the cap from the gate size so the next growth does not hit this wall again.