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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/5380-the-openapi-document-describes-a-pull-s → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/5380-the-openapi-document-describes-a-pull-s was merged into main as 751bb64.

Review

Sign in to review this change.
5 files changed +58 −4
scripts/shards.ts +1 −1
@@ -454,7 +454,7 @@ export function parseShardSpec(
454454 | { ok: true; none: true }
455455 | { ok: false; error: string } {
456456 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());

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.

458458 if (!m)
459459 return {
460460 ok: false,
src/contract/openapi.ts +38 −1
@@ -19,6 +19,8 @@ interface Route {
1919 readonly query?: Readonly<Record<string, string>>;
2020 /** The success status, when it is not 200. */
2121 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>>;
2224 /** The refusals it answers, by stable `error` code. */
2325 readonly refusals?: Readonly<Record<string, string>>;
2426 }
@@ -481,6 +483,27 @@ export const ROUTES: readonly Route[] = [
481483 refusals: { bad_anchor: "`path`, `line` and `side` (old or new) name no line of the pull's current diff (422)" },
482484 },
483485 { 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 },
484507 {
485508 method: "post",
486509 path: `${REPO}/pulls/{number}/approve-here`,
@@ -1369,7 +1392,21 @@ export function openapi(origin: string, version: string): Record<string, unknown
13691392 }),
13701393 ...(r.auth ? { security: [{ token: [] }] } : {}),
13711394 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 },
13731410 default: {
13741411 description:
13751412 "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[] = [
4646 "GET /api/v1/repos/{}/{}/labels",
4747 "GET /api/v1/repos/{}/{}/milestones",
4848 "GET /api/v1/repos/{}/{}/milestones/{}",
49 "GET /api/v1/repos/{}/{}/pulls/{}/checks",
5049 "GET /api/v1/repos/{}/{}/pulls/{}/review-states",
5150 "GET /api/v1/repos/{}/{}/pulls/{}/reviews",
5251 "GET /api/v1/repos/{}/{}/pulls/{}/threads",
@@ -100,7 +99,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [
10099 "POST /api/v1/repos/{}/{}/prompts/{}/run",
101100 "POST /api/v1/repos/{}/{}/proposal-tokens",
102101 "POST /api/v1/repos/{}/{}/pulls/{}/approve-here",
103 "POST /api/v1/repos/{}/{}/pulls/{}/checks",
104102 "POST /api/v1/repos/{}/{}/pulls/{}/comments",
105103 "POST /api/v1/repos/{}/{}/pulls/{}/looks",
106104 "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", () => {
513513 expect(parseShardSpec("1/3")).toEqual({ ok: true, index: 1, total: 3 });
514514 expect(parseShardSpec("3/3")).toEqual({ ok: true, index: 3, total: 3 });
515515 expect(parseShardSpec(" 2/4 ")).toEqual({ ok: true, index: 2, total: 4 });
516 expect(parseShardSpec("1000/1000")).toEqual({ ok: true, index: 1000, total: 1000 });
516517 for (const bad of ["0/3", "1/0", "4/3", "1", "1-3", "a/b", "1/3/5"])
517518 expect(parseShardSpec(bad).ok).toBe(false);
518519 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs