Public: anyone can read this; the crew can change it.
The OpenAPI document describes watching a milestone: PUT and DELETE …/milestones/{number}/watch #5421
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5315-the-openapi-document-describes-watching → main 1 commit fork 1 thread · 1 unresolved
4 files
changed +40
−2
src/contract/openapi.ts +22
| @@ -1156,6 +1156,28 @@ export const ROUTES: readonly Route[] = [ | ||
| 1156 | 1156 | body: { labels: "array" }, |
| 1157 | 1157 | refusals: LABEL_REFUSALS, |
| 1158 | 1158 | }, |
| 1159 | { | |
| 1160 | method: "put", | |
| 1161 | path: `${REPO}/milestones/{number}/watch`, | |
| 1162 | summary: "Watch a milestone: the caller's own watch, so a write token and nothing more (a person's; an agent is refused); answers { watching }", | |
| 1163 | auth: true, | |
| 1164 | refusals: { | |
| 1165 | unauthorized: "401: no signed-in account", | |
| 1166 | forbidden: "403: only a person watches a milestone", | |
| 1167 | not_found: "404: no such repository, or it is not readable, or the number is no milestone", | |
| 1168 | }, | |
| 1169 | }, | |
| 1170 | { | |
| 1171 | method: "delete", | |
| 1172 | path: `${REPO}/milestones/{number}/watch`, | |
| 1173 | summary: "Stop watching a milestone: the caller's own watch, so a write token and nothing more (a person's; an agent is refused); answers { watching }", | |
| 1174 | auth: true, | |
| 1175 | refusals: { | |
| 1176 | unauthorized: "401: no signed-in account", | |
| 1177 | forbidden: "403: only a person watches a milestone", | |
| 1178 | not_found: "404: no such repository, or it is not readable, or the number is no milestone", | |
| 1179 | }, | |
| 1180 | }, | |
| 1159 | 1181 | { |
| 1160 | 1182 | method: "post", |
| 1161 | 1183 | path: `${REPO}/issues/{number}/private`, |
test/openapi-coverage.test.ts −2
| @@ -13,7 +13,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 13 | 13 | "DELETE /api/v1/repos/{}/{}", |
| 14 | 14 | "DELETE /api/v1/repos/{}/{}/branches/{}", |
| 15 | 15 | "DELETE /api/v1/repos/{}/{}/labels/{}", |
| 16 | "DELETE /api/v1/repos/{}/{}/milestones/{}/watch", | |
| 17 | 16 | "DELETE /api/v1/repos/{}/{}/pulls/{}/hold", |
| 18 | 17 | "DELETE /api/v1/repos/{}/{}/pulls/{}/threads/{}/resolve", |
| 19 | 18 | "DELETE /api/v1/user/keys/{}", |
| @@ -129,7 +128,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 129 | 128 | "PUT /api/v1/repos/{}/{}/issues/{}/assignee", |
| 130 | 129 | "PUT /api/v1/repos/{}/{}/issues/{}/labels", |
| 131 | 130 | "PUT /api/v1/repos/{}/{}/issues/{}/teams", |
| 132 | "PUT /api/v1/repos/{}/{}/milestones/{}/watch", | |
| 133 | 131 | "PUT /api/v1/repos/{}/{}/pulls/{}/assignee", |
| 134 | 132 | "PUT /api/v1/repos/{}/{}/pulls/{}/hold", |
| 135 | 133 | "PUT /api/v1/repos/{}/{}/pulls/{}/labels", |
test/openapi-milestone-watch.test.ts added +16
| @@ -0,0 +1,16 @@ | ||
| 1 | /** The OpenAPI document describes watching a milestone: PUT and DELETE …/milestones/{number}/watch. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { openapi } from "../src/contract/openapi.ts"; | |
| 4 | ||
| 5 | const PATH = "/api/v1/repos/{owner}/{name}/milestones/{number}/watch"; | |
| 6 | ||
| 7 | test("PUT and DELETE watch each have a row with the refusals the handler answers", () => { | |
| 8 | const doc = openapi("https://f.test", "1") as any; | |
| 9 | for (const m of ["put", "delete"]) { | |
| 10 | const op = doc.paths[PATH]?.[m]; | |
| 11 | expect(op, m).toBeDefined(); | |
| 12 | const text = JSON.stringify(op); | |
| 13 | for (const code of ["unauthorized", "forbidden", "not_found"]) expect(text, `${m} ${code}`).toContain(`\`${code}\``); | |
| 14 | expect(op.summary).toContain("{ watching }"); | |
| 15 | } | |
| 16 | }); |
test/openapi.test.ts +2
| @@ -31,6 +31,8 @@ test("every described route is registered in the router", () => { | ||
| 31 | 31 | .map((f) => readFileSync(f, "utf8")) |
| 32 | 32 | .join("\n") |
| 33 | 33 | // A route registered in a loop over issues and pulls is two routes. |
| 34 | // So is the one over PUT and DELETE of a milestone's watch. | |
| 35 | .replace(/\bmethod,\s*("[^"]*\/milestones\/:number\/watch")/g, '"PUT", $1\n"DELETE", $1') | |
| 34 | 36 | .replace(/"(PUT)",\s*`([^`]*)\$\{kind\}([^`]*)`/g, (_, m, a, b) => `"${m}", "${a}issues${b}"\n"${m}", "${a}pulls${b}"`); |
| 35 | 37 | const registered = new Set( |
| 36 | 38 | [...src.matchAll(/"(GET|POST|PATCH|DELETE|PUT)",\s*"([^"]+)"/g)].map((m) => `${m[1]} ${m[2]}`), |
new above
offline
Nit: a token without write scope is also refused by requireScope; consider listing that refusal here.