Public: anyone can read this; the crew can change it.
The OpenAPI document describes a pull's labels and hold #5405
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5383-the-openapi-document-describes-a-pull-s → main 2 commits fork
5 files
changed +50
−2
src/contract/openapi/routes/deleteApiV1ReposOwnerNamePullsNumberHold.ts added +10
| @@ -0,0 +1,10 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "delete", | |
| 5 | path: `${REPO}/pulls/{number}/hold`, | |
| 6 | summary: | |
| 7 | "Let a held pull go; a person who writes the repository releases, never an agent (rw scope); answers `{ held: false, released }`, where `released` is false when nothing held it", | |
| 8 | auth: true, | |
| 9 | refusals: { forbidden: "you are an agent, or you cannot write the repository (403)" }, | |
| 10 | }; |
src/contract/openapi/routes/index.ts +2
| @@ -16,6 +16,7 @@ export { route as deleteApiV1ReposOwnerNameHooksId } from "./deleteApiV1ReposOwn | ||
| 16 | 16 | export { route as deleteApiV1ReposOwnerNameIssuesNumberDependenciesBlocker } from "./deleteApiV1ReposOwnerNameIssuesNumberDependenciesBlocker.ts"; |
| 17 | 17 | export { route as deleteApiV1ReposOwnerNameMilestonesNumberWatch } from "./deleteApiV1ReposOwnerNameMilestonesNumberWatch.ts"; |
| 18 | 18 | export { route as deleteApiV1ReposOwnerNamePromptsId } from "./deleteApiV1ReposOwnerNamePromptsId.ts"; |
| 19 | export { route as deleteApiV1ReposOwnerNamePullsNumberHold } from "./deleteApiV1ReposOwnerNamePullsNumberHold.ts"; | |
| 19 | 20 | export { route as deleteApiV1ReposOwnerNamePullsNumberThreadsAnchorResolve } from "./deleteApiV1ReposOwnerNamePullsNumberThreadsAnchorResolve.ts"; |
| 20 | 21 | export { route as deleteApiV1SecretsScopeName } from "./deleteApiV1SecretsScopeName.ts"; |
| 21 | 22 | export { route as deleteApiV1Sessions } from "./deleteApiV1Sessions.ts"; |
| @@ -202,6 +203,7 @@ export { route as putApiV1ReposOwnerNameIssuesNumberLabels } from "./putApiV1Rep | ||
| 202 | 203 | export { route as putApiV1ReposOwnerNameIssuesNumberTeams } from "./putApiV1ReposOwnerNameIssuesNumberTeams.ts"; |
| 203 | 204 | export { route as putApiV1ReposOwnerNameMilestonesNumberWatch } from "./putApiV1ReposOwnerNameMilestonesNumberWatch.ts"; |
| 204 | 205 | export { route as putApiV1ReposOwnerNamePullsNumberAssignee } from "./putApiV1ReposOwnerNamePullsNumberAssignee.ts"; |
| 206 | export { route as putApiV1ReposOwnerNamePullsNumberHold } from "./putApiV1ReposOwnerNamePullsNumberHold.ts"; | |
| 205 | 207 | export { route as putApiV1ReposOwnerNamePullsNumberLabels } from "./putApiV1ReposOwnerNamePullsNumberLabels.ts"; |
| 206 | 208 | export { route as putApiV1ReposOwnerNameUpstream } from "./putApiV1ReposOwnerNameUpstream.ts"; |
| 207 | 209 | export { route as putApiV1SecretsScopeName } from "./putApiV1SecretsScopeName.ts"; |
src/contract/openapi/routes/putApiV1ReposOwnerNamePullsNumberHold.ts added +16
| @@ -0,0 +1,16 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "put", | |
| 5 | path: `${REPO}/pulls/{number}/hold`, | |
| 6 | summary: | |
| 7 | "Hold an open pull with one line that says why, so no rule reviews or lands it until it is released; a person who writes the repository holds, never an agent (rw scope); answers `{ held: true, hold }`", | |
| 8 | auth: true, | |
| 9 | body: { why: "string" }, | |
| 10 | refusals: { | |
| 11 | forbidden: "you are an agent, or you cannot write the repository (403)", | |
| 12 | not_open: "the pull is closed or merged (409)", | |
| 13 | empty_why: "`why` is empty (422)", | |
| 14 | why_too_long: "`why` is longer than 200 characters (422)", | |
| 15 | }, | |
| 16 | }; |
test/openapi-coverage.test.ts −2
| @@ -11,7 +11,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 11 | 11 | "DELETE /api/v1/boards/{}", |
| 12 | 12 | "DELETE /api/v1/orgs/{}/teams/{}/members/{}", |
| 13 | 13 | "DELETE /api/v1/repos/{}/{}/labels/{}", |
| 14 | "DELETE /api/v1/repos/{}/{}/pulls/{}/hold", | |
| 15 | 14 | "DELETE /api/v1/user/keys/{}", |
| 16 | 15 | "DELETE /api/v1/user/origins/{}", |
| 17 | 16 | "DELETE /api/v1/user/pins/{}/{}", |
| @@ -91,7 +90,6 @@ const NOT_YET_DESCRIBED: readonly string[] = [ | ||
| 91 | 90 | "PUT /api/v1/boards/{}", |
| 92 | 91 | "PUT /api/v1/front", |
| 93 | 92 | "PUT /api/v1/repos/{}/{}/crew/{}", |
| 94 | "PUT /api/v1/repos/{}/{}/pulls/{}/hold", | |
| 95 | 93 | "PUT /api/v1/repos/{}/{}/pulls/{}/labels", |
| 96 | 94 | "PUT /api/v1/user/pins/{}/{}", |
| 97 | 95 | ]; |
test/openapi-oa-pull-hold.test.ts added +22
| @@ -0,0 +1,22 @@ | ||
| 1 | /** The OpenAPI document describes a pull's labels and hold. */ | |
| 2 | import { expect, test } from "bun:test"; | |
| 3 | import { openapi } from "../src/contract/openapi.ts"; | |
| 4 | ||
| 5 | const R = "/api/v1/repos/{owner}/{name}/pulls/{number}"; | |
| 6 | ||
| 7 | test("a pull's labels and hold have rows, with the refusals the handlers answer", () => { | |
| 8 | const doc = openapi("https://f.test", "1") as any; | |
| 9 | expect(doc.paths[`${R}/labels`]?.put, "put labels").toBeDefined(); | |
| 10 | const put = doc.paths[`${R}/hold`]?.put; | |
| 11 | const del = doc.paths[`${R}/hold`]?.delete; | |
| 12 | expect(put, "put hold").toBeDefined(); | |
| 13 | expect(del, "delete hold").toBeDefined(); | |
| 14 | const text = JSON.stringify(put); | |
| 15 | for (const [code, status] of [["forbidden", 403], ["not_open", 409], ["empty_why", 422], ["why_too_long", 422]] as const) | |
| 16 | expect(text, code).toMatch(new RegExp(`\`${code}\`[^}]*\\(${status}\\)`)); | |
| 17 | expect(JSON.stringify(del)).toMatch(/`forbidden`[^}]*\(403\)/); | |
| 18 | expect(put.summary).toContain("never an agent"); | |
| 19 | expect(del.summary).toContain("never an agent"); | |
| 20 | const schema = put.requestBody.content["application/json"].schema; | |
| 21 | expect(schema.properties.why.type).toBe("string"); | |
| 22 | }); |
new above
offline