Public: anyone can read this; the crew can change it.
The OpenAPI document describes mirroring and forking from an origin, its job and stream, and origin apps #5456
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5152-the-openapi-document-describes-mirroring → main 4 commits fork
2 files
changed +80
−0
src/contract/openapi.ts +59
| @@ -737,6 +737,65 @@ export const ROUTES: readonly Route[] = [ | ||
| 737 | 737 | auth: true, |
| 738 | 738 | body: { originId: "string" }, |
| 739 | 739 | }, |
| 740 | { | |
| 741 | method: "post", | |
| 742 | path: "/api/v1/user/origins/{id}/mirror", | |
| 743 | summary: "Mirror a repository from one of your origins as a job; answers 202 with { job } to poll or stream", | |
| 744 | auth: true, | |
| 745 | body: { fullName: "string", name: "string" }, | |
| 746 | status: 202, | |
| 747 | refusals: { not_found: "404: no such origin of yours, or no such repository there" }, | |
| 748 | }, | |
| 749 | { | |
| 750 | method: "post", | |
| 751 | path: "/api/v1/user/origins/{id}/fork", | |
| 752 | summary: "Fork a repository at its origin, then mirror the fork as a job; answers 202 with { remote, existed, job, mirroredAs }", | |
| 753 | auth: true, | |
| 754 | body: { fullName: "string" }, | |
| 755 | status: 202, | |
| 756 | refusals: { | |
| 757 | not_found: "404: no such origin of yours", | |
| 758 | origin_unreachable: "422: the origin could not be reached", | |
| 759 | fork_timeout: "504: the origin took the fork and it has not appeared yet; try again shortly", | |
| 760 | mirror_failed: "502: a local copy of the fork could not be made", | |
| 761 | }, | |
| 762 | }, | |
| 763 | { | |
| 764 | method: "get", | |
| 765 | path: "/api/v1/user/origins/{id}/mirror/{job}", | |
| 766 | summary: "Read a mirror job's stage: { job }; stages run creating, fetching, labels, issues, pulls, comments, crew, then done or failed", | |
| 767 | auth: true, | |
| 768 | refusals: { not_found: "404: no such origin or job of yours" }, | |
| 769 | }, | |
| 770 | { | |
| 771 | method: "get", | |
| 772 | path: "/api/v1/user/origins/{id}/mirror/{job}/stream", | |
| 773 | summary: "Watch a mirror job live; answers text/event-stream, one event per stage (mirror.creating, mirror.fetching, mirror.labels, mirror.issues, mirror.pulls, mirror.comments, mirror.crew, mirror.done, mirror.failed) carrying the job, and ends after done or failed", | |
| 774 | auth: true, | |
| 775 | refusals: { not_found: "404: no such origin or job of yours" }, | |
| 776 | }, | |
| 777 | { method: "get", path: "/api/v1/user/origin-apps", summary: "List your registered OAuth applications, one per origin host", auth: true }, | |
| 778 | { | |
| 779 | method: "post", | |
| 780 | path: "/api/v1/user/origin-apps", | |
| 781 | summary: "Register an OAuth application for an origin host (a forgejo or gitea needs its own; github may fall back to the forge's); answers 201 with the application", | |
| 782 | auth: true, | |
| 783 | body: { host: "string", kind: "string", clientId: "string", clientSecret: "string" }, | |
| 784 | status: 201, | |
| 785 | refusals: { | |
| 786 | unknown_host: "422: that does not look like a forge host", | |
| 787 | private_host: "422: that address is inside a network, not on the internet", | |
| 788 | unreachable: "422: that host could not be reached", | |
| 789 | }, | |
| 790 | }, | |
| 791 | { | |
| 792 | method: "delete", | |
| 793 | path: "/api/v1/user/origin-apps/{id}", | |
| 794 | summary: "Remove a registered OAuth application of yours; answers 204", | |
| 795 | auth: true, | |
| 796 | status: 204, | |
| 797 | refusals: { not_found: "404: no such application of yours" }, | |
| 798 | }, | |
| 740 | 799 | { method: "get", path: `${REPO}/forks`, summary: "List the forks of a repository you can read", auth: false }, |
| 741 | 800 | { method: "get", path: `${REPO}/tags`, summary: "List a repository's tags: [{ name, oid, commit }]", auth: false }, |
| 742 | 801 | { |
test/openapi.test.ts +21
| @@ -220,6 +220,27 @@ test("an app's routes are described, and env values are never read back", () => | ||
| 220 | 220 | expect(doc.paths[`${A}/{env}/redeploy`].post.responses["202"]).toBeDefined(); |
| 221 | 221 | }); |
| 222 | 222 | |
| 223 | test("mirroring and forking from an origin, its job and stream, and origin apps are described", () => { | |
| 224 | const doc = openapi("https://f.test", "1") as any; | |
| 225 | const want: [string, string][] = [ | |
| 226 | ["/api/v1/user/origins/{id}/mirror", "post"], | |
| 227 | ["/api/v1/user/origins/{id}/fork", "post"], | |
| 228 | ["/api/v1/user/origins/{id}/mirror/{job}", "get"], | |
| 229 | ["/api/v1/user/origins/{id}/mirror/{job}/stream", "get"], | |
| 230 | ["/api/v1/migrate/{job}/stream", "get"], | |
| 231 | ["/api/v1/user/origin-apps", "get"], | |
| 232 | ["/api/v1/user/origin-apps", "post"], | |
| 233 | ["/api/v1/user/origin-apps/{id}", "delete"], | |
| 234 | ]; | |
| 235 | for (const [path, method] of want) { | |
| 236 | expect(doc.paths[path]?.[method]).toBeDefined(); | |
| 237 | expect(doc.paths[path][method].security).toBeDefined(); | |
| 238 | } | |
| 239 | const row = ROUTES.find((r: any) => r.path === "/api/v1/user/origins/{id}/mirror/{job}/stream") as any; | |
| 240 | expect(row.summary).toContain("text/event-stream"); | |
| 241 | expect(row.summary).toContain("mirror.done"); | |
| 242 | }); | |
| 243 | ||
| 223 | 244 | test("the admin's writes are described: a capacity setting, a limit and a team", () => { |
| 224 | 245 | const doc = openapi("https://f.test", "1") as any; |
| 225 | 246 | for (const path of ["capacity/{key}", "limits/{key}", "teams/{slug}"]) { |
new above
offline