Public: anyone can read this; the crew can change it.

The OpenAPI document describes the fork tree: following, upstream, sync, make it yours and family #4880

Merged opened this

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

Merged

scout/forge:scout/4871-the-openapi-document-describes-the-fork → main 1 commit fork

  • Merged. scout/4871-the-openapi-document-describes-the-fork was merged into main as e5f300e.

Review

Sign in to review this change.
2 files changed +166 −0
src/contract/openapi.ts +138
@@ -121,6 +121,144 @@ export const ROUTES: readonly Route[] = [
121121 showProvenance: "boolean",
122122 },
123123 },
124 {
125 method: "get",
126 path: `${REPO}/following`,
127 summary:
128 "A fork's standing against its parent, read from git: { chain, severedAt, upstream, parent, parentBranch, parentFrozen, branch, state, behind, ahead, clean, conflicts, forkTip, parentTip, parentForge, converge }; a repository that only follows across forges answers { chain, severedAt, upstream }",
129 auth: false,
130 refusals: { not_a_fork: "404: this repository follows nothing" },
131 },
132 {
133 method: "get",
134 path: `${REPO}/following/proposals`,
135 summary: "How each change proposed to the parent forge stands, as the parent last said; answers { items }",
136 auth: false,
137 },
138 {
139 method: "post",
140 path: `${REPO}/following/converge`,
141 summary: "Ask the crew to converge a diverged fork with its parent; answers { kind, issue, pull, agent } (202 when queued, 200 when working or proposed)",
142 auth: true,
143 refusals: {
144 not_a_fork: "404: this repository follows nothing",
145 forbidden: "403: only someone who may direct the crew",
146 archived: "409: the repository is archived",
147 ceiling: "429: the crew is at its ceiling",
148 },
149 },
150 {
151 method: "post",
152 path: `${REPO}/following/propose`,
153 summary: "Open a pull on the parent with what the fork is ahead by; answers { number, opened, changes, title }",
154 auth: true,
155 refusals: {
156 not_a_fork: "404: this repository follows nothing",
157 nothing_to_propose: "409: nothing here is ahead of the parent",
158 refused: "409: the parent refused, with the reason",
159 },
160 },
161 {
162 method: "post",
163 path: `${REPO}/following/propose-parent`,
164 summary: "Across forges: push this forge's change to the parent forge and open the pull there; answers 201 when opened, 200 when one stands",
165 auth: true,
166 refusals: {
167 no_parent_forge: "404: this repository has no parent forge to propose to",
168 nothing_to_propose: "409: nothing here is ahead of the parent forge",
169 parent_unreachable: "502: the parent forge did not answer",
170 refused: "409: the parent forge refused, with the reason",
171 },
172 },
173 {
174 method: "post",
175 path: `${REPO}/following/sever`,
176 summary: "Stop following the parent forge; answers { severedAt }",
177 auth: true,
178 refusals: { not_following: "404: this repository follows nothing across forges" },
179 },
180 {
181 method: "post",
182 path: `${REPO}/following/take`,
183 summary:
184 "Take the parent's updates: a fork merges (the same answer as sync); the repository carrying this forge's code takes as a pull",
185 auth: true,
186 refusals: {
187 not_a_fork: "404: this repository follows nothing",
188 not_following: "404: this repository follows nothing across forges",
189 conflict: "409: both sides changed these files; the crew was told; { files, converge, issue }",
190 moved: "409: something moved while this was read; try again",
191 },
192 },
193 {
194 method: "patch",
195 path: "/api/v1/forge/following",
196 summary: "Choose how this forge follows the forge its code came from (its code repository's admin); answers { mode }",
197 auth: true,
198 body: { mode: "string" },
199 refusals: {
200 not_following: "404: this repository follows nothing across forges",
201 forbidden: "403: only this forge's owner can change how it follows",
202 invalid_mode: "422: mode is not one the forge knows",
203 severed: "409: this forge is severed and follows nothing",
204 },
205 },
206 {
207 method: "get",
208 path: `${REPO}/upstream`,
209 summary: "The outside upstream this repository follows, for an admin: { following } (null when it follows none)",
210 auth: true,
211 },
212 {
213 method: "post",
214 path: `${REPO}/upstream/check`,
215 summary: "Check the followed upstream now (admin scope); answers the check's result",
216 auth: true,
217 refusals: { busy: "409: a check is already running", invalid_url: "422: not an https or http git address" },
218 },
219 {
220 method: "post",
221 path: `${REPO}/sync`,
222 summary: "Sync a mirror with its origin, or let a fork take its parent's updates (a fork needs a writer)",
223 auth: true,
224 refusals: {
225 not_a_fork: "404: this repository follows nothing",
226 conflict: "409: both sides changed these files; the crew was told",
227 moved: "409: something moved while this was read; try again",
228 },
229 },
230 {
231 method: "post",
232 path: `${REPO}/make-it-yours`,
233 summary:
234 "Take an app as your own: a fork with its app; a dry run (the default, ro scope) answers the plan, dryRun false (rw scope) answers 201 with the plan and `url`",
235 auth: true,
236 body: { dryRun: "boolean", name: "string", visibility: "string" },
237 refusals: {
238 invalid: "422: visibility is public or private",
239 own: "409: this app is already yours",
240 unverified: "403: prove your email first",
241 limit: "429: at most so many copies a day; { resetsAt }",
242 repository_limit: "403: at the repository limit",
243 storage_limit: "403: at the storage limit",
244 not_member: "403: not a member",
245 },
246 },
247 {
248 method: "post",
249 path: `${REPO}/origin/reconnect`,
250 summary: "Reconnect a frozen mirror through one of your origins (admin scope, repository admin); answers the repository",
251 auth: true,
252 body: { originId: "string" },
253 },
254 { method: "get", path: `${REPO}/forks`, summary: "List the forks of a repository you can read", auth: false },
255 { method: "get", path: `${REPO}/tags`, summary: "List a repository's tags: [{ name, oid, commit }]", auth: false },
256 {
257 method: "get",
258 path: "/api/v1/family",
259 summary: "The forge's family: { parent, here, children, more }; the parent for anyone, the children for a site admin",
260 auth: false,
261 },
124262 { method: "get", path: `${REPO}/app/production/open`, summary: "Open the running app", auth: false },
125263 { method: "get", path: `${REPO}/map`, summary: "Read the plan: issues, their dependency edges and pillars", auth: false },
126264 {
test/openapi-oa-fork.test.ts added +28
@@ -0,0 +1,28 @@
1 /** The fork tree is in the served OpenAPI document. */
2 import { expect, test } from "bun:test";
3 import { openapi } from "../src/contract/openapi.ts";
4
5 const R = "/api/v1/repos/{owner}/{name}";
6 const WANT: readonly [string, string][] = [
7 ["get", `${R}/following`],
8 ["get", `${R}/following/proposals`],
9 ["post", `${R}/following/converge`],
10 ["post", `${R}/following/propose`],
11 ["post", `${R}/following/propose-parent`],
12 ["post", `${R}/following/sever`],
13 ["post", `${R}/following/take`],
14 ["patch", "/api/v1/forge/following"],
15 ["get", `${R}/upstream`],
16 ["post", `${R}/upstream/check`],
17 ["post", `${R}/sync`],
18 ["post", `${R}/make-it-yours`],
19 ["post", `${R}/origin/reconnect`],
20 ["get", `${R}/forks`],
21 ["get", `${R}/tags`],
22 ["get", "/api/v1/family"],
23 ];
24
25 test("each fork-tree path and method is in the document", () => {
26 const paths = openapi("http://x", "1").paths as Record<string, Record<string, unknown>>;
27 for (const [method, path] of WANT) expect(paths[path]?.[method]).toBeDefined();
28 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs