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

Merged opened this

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

Merged

scout/forge:scout/5115-the-openapi-document-describes-accepting → main 7 commits fork 2 threads · 2 unresolved

  • Merged. scout/5115-the-openapi-document-describes-accepting was merged into main as 4bbe55d.

Review

Sign in to review this change.
7 files changed +105 −0
src/contract/openapi/routes/index.ts +5
@@ -207,12 +207,15 @@ export { route as postApiV1ReposOwnerNameHooks } from "./postApiV1ReposOwnerName
207207 export { route as postApiV1ReposOwnerNameIssues } from "./postApiV1ReposOwnerNameIssues.ts";
208208 export { route as postApiV1ReposOwnerNameIssuesNumberComments } from "./postApiV1ReposOwnerNameIssuesNumberComments.ts";
209209 export { route as postApiV1ReposOwnerNameIssuesNumberDependencies } from "./postApiV1ReposOwnerNameIssuesNumberDependencies.ts";
210 export { route as postApiV1ReposOwnerNameIssuesNumberDependenciesBlockerConfirm } from "./postApiV1ReposOwnerNameIssuesNumberDependenciesBlockerConfirm.ts";
210211 export { route as postApiV1ReposOwnerNameIssuesNumberPrivate } from "./postApiV1ReposOwnerNameIssuesNumberPrivate.ts";
211212 export { route as postApiV1ReposOwnerNameMakeItYours } from "./postApiV1ReposOwnerNameMakeItYours.ts";
213 export { route as postApiV1ReposOwnerNameMilestonesNumberLimb } from "./postApiV1ReposOwnerNameMilestonesNumberLimb.ts";
212214 export { route as postApiV1ReposOwnerNameOriginReconnect } from "./postApiV1ReposOwnerNameOriginReconnect.ts";
213215 export { route as postApiV1ReposOwnerNamePlan } from "./postApiV1ReposOwnerNamePlan.ts";
214216 export { route as postApiV1ReposOwnerNamePrompts } from "./postApiV1ReposOwnerNamePrompts.ts";
215217 export { route as postApiV1ReposOwnerNamePromptsIdRun } from "./postApiV1ReposOwnerNamePromptsIdRun.ts";
218 export { route as postApiV1ReposOwnerNameProposalTokens } from "./postApiV1ReposOwnerNameProposalTokens.ts";
216219 export { route as postApiV1ReposOwnerNamePulls } from "./postApiV1ReposOwnerNamePulls.ts";
217220 export { route as postApiV1ReposOwnerNamePullsNumberApproveHere } from "./postApiV1ReposOwnerNamePullsNumberApproveHere.ts";
218221 export { route as postApiV1ReposOwnerNamePullsNumberChecks } from "./postApiV1ReposOwnerNamePullsNumberChecks.ts";
@@ -222,6 +225,8 @@ export { route as postApiV1ReposOwnerNamePullsNumberReviews } from "./postApiV1R
222225 export { route as postApiV1ReposOwnerNamePullsNumberThreadsAnchorResolve } from "./postApiV1ReposOwnerNamePullsNumberThreadsAnchorResolve.ts";
223226 export { route as postApiV1ReposOwnerNamePullsNumberVariant } from "./postApiV1ReposOwnerNamePullsNumberVariant.ts";
224227 export { route as postApiV1ReposOwnerNameReports } from "./postApiV1ReposOwnerNameReports.ts";
228 export { route as postApiV1ReposOwnerNameReportsIdAccept } from "./postApiV1ReposOwnerNameReportsIdAccept.ts";
229 export { route as postApiV1ReposOwnerNameReportsIdDismiss } from "./postApiV1ReposOwnerNameReportsIdDismiss.ts";
225230 export { route as postApiV1ReposOwnerNameRunsIdCancel } from "./postApiV1ReposOwnerNameRunsIdCancel.ts";
226231 export { route as postApiV1ReposOwnerNameRunsIdSteer } from "./postApiV1ReposOwnerNameRunsIdSteer.ts";
227232 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)",
outdated

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.

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 (dryRun: false) answers 201, but the document lists only 200. Add alsoStatus: [201] and assert it in the test.

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", () =>
291291 expect(doc.paths[`${A}/{env}/redeploy`].post.responses["202"]).toBeDefined();
292292 });
293293
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
294315 test("browsing an origin's code is described, and a refusal matches what the route answers", async () => {
295316 const doc = openapi("https://f.test", "1") as any;
296317 const B = "/api/v1/user/origins/{id}/browse/{owner}/{name}";

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs