Add external CI disable endpoint and docs

swadhinbiswas committed 8 months ago
b6de549
1 parent a5ce3df
commit b6de549
4 files changed
+60 -2
▼doc/feature_audit.md
+1-1
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
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
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
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
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
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