Public: anyone can read this; the crew can change it.
The OpenAPI document describes accepting and dismissing a report, confirming a wait, a milestone's limb and proposal tokens #6491
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5115-the-openapi-document-describes-accepting → main 7 commits fork 2 threads · 2 unresolved
7 files
changed +105
−0
src/contract/openapi/routes/index.ts +5
| @@ -207,12 +207,15 @@ export { route as postApiV1ReposOwnerNameHooks } from "./postApiV1ReposOwnerName | ||
| 207 | 207 | export { route as postApiV1ReposOwnerNameIssues } from "./postApiV1ReposOwnerNameIssues.ts"; |
| 208 | 208 | export { route as postApiV1ReposOwnerNameIssuesNumberComments } from "./postApiV1ReposOwnerNameIssuesNumberComments.ts"; |
| 209 | 209 | export { route as postApiV1ReposOwnerNameIssuesNumberDependencies } from "./postApiV1ReposOwnerNameIssuesNumberDependencies.ts"; |
| 210 | export { route as postApiV1ReposOwnerNameIssuesNumberDependenciesBlockerConfirm } from "./postApiV1ReposOwnerNameIssuesNumberDependenciesBlockerConfirm.ts"; | |
| 210 | 211 | export { route as postApiV1ReposOwnerNameIssuesNumberPrivate } from "./postApiV1ReposOwnerNameIssuesNumberPrivate.ts"; |
| 211 | 212 | export { route as postApiV1ReposOwnerNameMakeItYours } from "./postApiV1ReposOwnerNameMakeItYours.ts"; |
| 213 | export { route as postApiV1ReposOwnerNameMilestonesNumberLimb } from "./postApiV1ReposOwnerNameMilestonesNumberLimb.ts"; | |
| 212 | 214 | export { route as postApiV1ReposOwnerNameOriginReconnect } from "./postApiV1ReposOwnerNameOriginReconnect.ts"; |
| 213 | 215 | export { route as postApiV1ReposOwnerNamePlan } from "./postApiV1ReposOwnerNamePlan.ts"; |
| 214 | 216 | export { route as postApiV1ReposOwnerNamePrompts } from "./postApiV1ReposOwnerNamePrompts.ts"; |
| 215 | 217 | export { route as postApiV1ReposOwnerNamePromptsIdRun } from "./postApiV1ReposOwnerNamePromptsIdRun.ts"; |
| 218 | export { route as postApiV1ReposOwnerNameProposalTokens } from "./postApiV1ReposOwnerNameProposalTokens.ts"; | |
| 216 | 219 | export { route as postApiV1ReposOwnerNamePulls } from "./postApiV1ReposOwnerNamePulls.ts"; |
| 217 | 220 | export { route as postApiV1ReposOwnerNamePullsNumberApproveHere } from "./postApiV1ReposOwnerNamePullsNumberApproveHere.ts"; |
| 218 | 221 | export { route as postApiV1ReposOwnerNamePullsNumberChecks } from "./postApiV1ReposOwnerNamePullsNumberChecks.ts"; |
| @@ -222,6 +225,8 @@ export { route as postApiV1ReposOwnerNamePullsNumberReviews } from "./postApiV1R | ||
| 222 | 225 | export { route as postApiV1ReposOwnerNamePullsNumberThreadsAnchorResolve } from "./postApiV1ReposOwnerNamePullsNumberThreadsAnchorResolve.ts"; |
| 223 | 226 | export { route as postApiV1ReposOwnerNamePullsNumberVariant } from "./postApiV1ReposOwnerNamePullsNumberVariant.ts"; |
| 224 | 227 | export { route as postApiV1ReposOwnerNameReports } from "./postApiV1ReposOwnerNameReports.ts"; |
| 228 | export { route as postApiV1ReposOwnerNameReportsIdAccept } from "./postApiV1ReposOwnerNameReportsIdAccept.ts"; | |
| 229 | export { route as postApiV1ReposOwnerNameReportsIdDismiss } from "./postApiV1ReposOwnerNameReportsIdDismiss.ts"; | |
| 225 | 230 | export { route as postApiV1ReposOwnerNameRunsIdCancel } from "./postApiV1ReposOwnerNameRunsIdCancel.ts"; |
| 226 | 231 | export { route as postApiV1ReposOwnerNameRunsIdSteer } from "./postApiV1ReposOwnerNameRunsIdSteer.ts"; |
| 227 | 232 | export { route as postApiV1ReposOwnerNameSpend } from "./postApiV1ReposOwnerNameSpend.ts"; |
src/contract/openapi/routes/postApiV1ReposOwnerNameIssuesNumberDependenciesBlockerConfirm.ts added +15
| @@ -0,0 +1,15 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "post", | |
| 5 | path: `${REPO}/issues/{number}/dependencies/{blocker}/confirm`, | |
| 6 | summary: | |
| 7 | "Confirm that an issue may go ahead although its blocker closed as dropped or split (rw scope; a person who writes, never the crew); answers 204", | |
| 8 | auth: true, | |
| 9 | status: 204, | |
| 10 | refusals: { | |
| 11 | not_found: "no such issue or blocker, or not yours to see (404)", | |
| 12 | unauthorized: "no account (401)", | |
| 13 | forbidden: "not a person who writes to the repository, or a crew member (403)", | |
| 14 | }, | |
| 15 | }; |
src/contract/openapi/routes/postApiV1ReposOwnerNameMilestonesNumberLimb.ts added +17
| @@ -0,0 +1,17 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "post", | |
| 5 | path: `${REPO}/milestones/{number}/limb`, | |
| 6 | summary: | |
| 7 | "Act on a whole limb: defer, resume, close, raise, lower, replan or takeback. A dry run unless `dryRun` is false (ro scope); applied needs an admin token and answers 201 with the receipt, undone at /api/v1/fleet/ops/{id}/undo", | |
| 8 | auth: true, | |
Applied ( |
||
| 9 | alsoStatus: [201], | |
| 10 | body: { action: "string", dryRun: "boolean", reason: "string", pillar: "string", under: "string" }, | |
| 11 | refusals: { | |
| 12 | bad_action: "action is not defer, resume, close, raise, lower, replan or takeback (422)", | |
| 13 | not_found: "no such repository or milestone, or not yours to see", | |
| 14 | unauthorized: "no account", | |
| 15 | forbidden: "the caller may not act on this limb", | |
| 16 | }, | |
| 17 | }; |
src/contract/openapi/routes/postApiV1ReposOwnerNameProposalTokens.ts added +17
| @@ -0,0 +1,17 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "post", | |
| 5 | path: `${REPO}/proposal-tokens`, | |
| 6 | summary: | |
| 7 | "Hand a child its own narrow credential to propose pulls to this repository (rw scope; a writer's); answers 201 with the token, whose secret shows once", | |
| 8 | auth: true, | |
| 9 | status: 201, | |
| 10 | body: { child: "string", origin: "string", childRepo: "string" }, | |
| 11 | refusals: { | |
| 12 | invalid_child: "child is not an acceptable name", | |
| 13 | invalid_origin: "origin is not an acceptable origin", | |
| 14 | too_large: "the body is over its byte limit (413)", | |
| 15 | not_found: "no such repository, or not one the caller writes to", | |
| 16 | }, | |
| 17 | }; |
src/contract/openapi/routes/postApiV1ReposOwnerNameReportsIdAccept.ts added +17
| @@ -0,0 +1,17 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "post", | |
| 5 | path: `${REPO}/reports/{id}/accept`, | |
| 6 | summary: | |
| 7 | "Accept a waiting report as an issue labelled outside (rw scope; a writer who is not on the crew); answers 201 with the issue; `title` rewords it, the marker stays", | |
| 8 | auth: true, | |
| 9 | status: 201, | |
| 10 | body: { title: "string" }, | |
| 11 | refusals: { | |
| 12 | not_found: "no such repository or report, or not yours to decide", | |
| 13 | forbidden: "not a person who writes to the repository", | |
| 14 | decided: "the report was already accepted or dismissed (409)", | |
| 15 | mirrored: "the repository mirrors an origin (409)", | |
| 16 | }, | |
| 17 | }; |
src/contract/openapi/routes/postApiV1ReposOwnerNameReportsIdDismiss.ts added +13
| @@ -0,0 +1,13 @@ | ||
| 1 | import { REPO, type Route } from "../shared.ts"; | |
| 2 | ||
| 3 | export const route: Route = { | |
| 4 | method: "post", | |
| 5 | path: `${REPO}/reports/{id}/dismiss`, | |
| 6 | summary: "Dismiss a waiting report (rw scope; a writer who is not on the crew); answers { id, state: \"dismissed\" }", | |
| 7 | auth: true, | |
| 8 | refusals: { | |
| 9 | not_found: "no such repository or report, or not yours to decide", | |
| 10 | forbidden: "not a person who writes to the repository", | |
| 11 | decided: "the report was already accepted or dismissed (409)", | |
| 12 | }, | |
| 13 | }; |
test/openapi.test.ts +21
| @@ -291,6 +291,27 @@ test("an app's routes are described, and env values are never read back", () => | ||
| 291 | 291 | expect(doc.paths[`${A}/{env}/redeploy`].post.responses["202"]).toBeDefined(); |
| 292 | 292 | }); |
| 293 | 293 | |
| 294 | test("triage, confirming a wait, a limb and proposal tokens are described", () => { | |
| 295 | const doc = openapi("https://f.test", "1") as any; | |
| 296 | const R = "/api/v1/repos/{owner}/{name}"; | |
| 297 | for (const [path, status] of [ | |
| 298 | [`${R}/reports/{id}/accept`, "201"], | |
| 299 | [`${R}/reports/{id}/dismiss`, "200"], | |
| 300 | [`${R}/issues/{number}/dependencies/{blocker}/confirm`, "204"], | |
| 301 | [`${R}/milestones/{number}/limb`, "200"], | |
| 302 | [`${R}/proposal-tokens`, "201"], | |
| 303 | ] as const) { | |
| 304 | const op = doc.paths[path]?.post; | |
| 305 | expect(op, path).toBeDefined(); | |
| 306 | expect(op.security, path).toBeDefined(); | |
| 307 | expect(op.responses[status], `${path} ${status}`).toBeDefined(); | |
| 308 | } | |
| 309 | expect(doc.paths[`${R}/milestones/{number}/limb`].post.responses["201"]).toBeDefined(); | |
| 310 | const confirm = doc.paths[`${R}/issues/{number}/dependencies/{blocker}/confirm`].post.responses.default.description; | |
| 311 | expect(confirm).toContain("no account (401)"); | |
| 312 | expect(confirm).not.toContain("422"); | |
| 313 | }); | |
| 314 | ||
| 294 | 315 | test("browsing an origin's code is described, and a refusal matches what the route answers", async () => { |
| 295 | 316 | const doc = openapi("https://f.test", "1") as any; |
| 296 | 317 | const B = "/api/v1/user/origins/{id}/browse/{owner}/{name}"; |
new above
offline
These codes go through
respondAccess, which answers not_found → 404, unauthorized → 401 and forbidden → 403. The 422 is only the fallback for codes it doesn't name. The preview answers 401 for no account. Drop the(422)or write the real statuses.