Add external CI disable endpoint and docs
swadhinbiswas committed 8 months ago
b6de549
1 parent a5ce3df
commit b6de549
4 files changed
+60 -2
Showing 4 changed files+60-2
▼doc/feature_audit.md
+1-1
| @@ -91,7 +91,7 @@ | ||
91 | 91 | | Feature | Status | Implementation Notes | |
92 | 92 | | :--- | :---: | :--- | |
93 | 93 | | Native CI pipeline support | ✅ | `pipeline.ts` - 1066 lines, GitHub Actions compatible | |
94 | -| External CI integration | ⚠️ | Provider-aware integration APIs and normalized status ingestion are implemented, with repo-level integration health/status + token rotation APIs (`external-ci`, `external-ci/checks`); provider-specific setup UX still maturing | | |
94 | +| External CI integration | ⚠️ | Provider-aware integration APIs and normalized status ingestion are implemented, with repo-level integration health/status + token rotate/disable APIs (`external-ci`, `external-ci/checks`); provider-specific setup UX still maturing | | |
95 | 95 | | Self-hosted runners | ✅ | `runner/` with Docker executor | |
96 | 96 | | Secrets management | ✅ | Schema + UI for repo/org secrets | |
97 | 97 | | Merge checks & gates | ⚠️ | Required status checks + external CI gate readiness enforced in merge queue; broader policy UX remains | |
98 | 98 | |
▼src/lib/openapi.ts
+15-0
| @@ -984,6 +984,21 @@ export const openApiSpec = { | ||
984 | 984 | 404: { description: "Repository not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } }, |
985 | 985 | }, |
986 | 986 | }, |
987 | + delete: { | |
988 | + tags: ["CI/CD"], | |
989 | + summary: "Disable external CI integration", | |
990 | + security: [{ bearerAuth: [] }], | |
991 | + parameters: [ | |
992 | + { name: "owner", in: "path", required: true, schema: { type: "string" } }, | |
993 | + { name: "repo", in: "path", required: true, schema: { type: "string" } }, | |
994 | + ], | |
995 | + responses: { | |
996 | + 200: { description: "Integration disabled" }, | |
997 | + 401: { description: "Unauthorized", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } }, | |
998 | + 403: { description: "Forbidden", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } }, | |
999 | + 404: { description: "Repository not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } }, | |
1000 | + }, | |
1001 | + }, | |
987 | 1002 | }, |
988 | 1003 | "/user/notification-digests/test": { |
989 | 1004 | post: { |
990 | 1005 | |
▼src/pages/api/repos/[owner]/[repo]/external-ci.ts
+22-0
| @@ -144,3 +144,25 @@ export const POST: APIRoute = withErrorHandler(async ({ params, locals, request | ||
144 | 144 | |
145 | 145 | return created({ token, rotated }); |
146 | 146 | }); |
147 | + | |
148 | +export const DELETE: APIRoute = withErrorHandler(async ({ params, locals }) => { | |
149 | + const { owner, repo } = params; | |
150 | + const user = locals.user; | |
151 | + | |
152 | + if (!user) return unauthorized(); | |
153 | + if (!owner || !repo) return badRequest("Missing parameters"); | |
154 | + | |
155 | + const db = getDatabase() as NodePgDatabase<typeof schema>; | |
156 | + const repository = await getRepository(owner, repo); | |
157 | + | |
158 | + if (!repository) return notFound("Repository not found"); | |
159 | + | |
160 | + if (!(await canAdminRepo(user.id, repository))) { | |
161 | + return forbidden(); | |
162 | + } | |
163 | + | |
164 | + await db.delete(schema.externalCiIntegrations) | |
165 | + .where(eq(schema.externalCiIntegrations.repositoryId, repository.id)); | |
166 | + | |
167 | + return success({ enabled: false }); | |
168 | +}); | |
147 | 169 | |
▼tests/integration/external-ci-route.test.ts
+22-1
| @@ -29,7 +29,7 @@ vi.mock("@/lib/utils", () => ({ | ||
29 | 29 | generateId: generateIdMock, |
30 | 30 | })); |
31 | 31 | |
32 | -import { GET as externalCiGet, POST as externalCiPost } from "@/pages/api/repos/[owner]/[repo]/external-ci"; | |
32 | +import { DELETE as externalCiDelete, GET as externalCiGet, POST as externalCiPost } from "@/pages/api/repos/[owner]/[repo]/external-ci"; | |
33 | 33 | |
34 | 34 | function makeDb(integration: any | null = null) { |
35 | 35 | return { |
| @@ -52,6 +52,9 @@ function makeDb(integration: any | null = null) { | ||
52 | 52 | where: vi.fn(async () => undefined), |
53 | 53 | })), |
54 | 54 | })), |
55 | + delete: vi.fn(() => ({ | |
56 | + where: vi.fn(async () => undefined), | |
57 | + })), | |
55 | 58 | }; |
56 | 59 | } |
57 | 60 | |
| @@ -122,4 +125,22 @@ describe("external ci route", () => { | ||
122 | 125 | expect(typeof body?.data?.token).toBe("string"); |
123 | 126 | expect(body?.data?.rotated).toBe(true); |
124 | 127 | }); |
128 | + | |
129 | + it("disables integration with delete", async () => { | |
130 | + mockDb = makeDb({ | |
131 | + id: "external-ci-1", | |
132 | + repositoryId: "repo-1", | |
133 | + name: "External CI", | |
134 | + }); | |
135 | + | |
136 | + const response = await externalCiDelete({ | |
137 | + params: { owner: "owner-1", repo: "demo" }, | |
138 | + locals: { user: { id: "user-1", isAdmin: false } }, | |
139 | + } as any); | |
140 | + | |
141 | + const body = await readJson(response); | |
142 | + expect(response.status).toBe(200); | |
143 | + expect(body?.data?.enabled).toBe(false); | |
144 | + expect(mockDb.delete).toHaveBeenCalledTimes(1); | |
145 | + }); | |
125 | 146 | }); |
126 | 147 | |