Skip to content

Commit b9aa684

Browse files
kmruizCopilot
andauthored
chore(ci): add cleanup script on CI for atlas envs (#608)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1d33b4b commit b9aa684

File tree

10 files changed

+137
-2
lines changed

10 files changed

+137
-2
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
name: "Cleanup stale Atlas test environments"
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 0 * * *"
7+
8+
permissions: {}
9+
10+
jobs:
11+
cleanup-envs:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: GitHubSecurityLab/actions-permissions/monitor@v1
15+
- uses: actions/checkout@v5
16+
- uses: actions/setup-node@v5
17+
with:
18+
node-version-file: package.json
19+
cache: "npm"
20+
- name: Install dependencies
21+
run: npm ci
22+
- name: Run cleanup script
23+
env:
24+
MDB_MCP_API_CLIENT_ID: ${{ secrets.TEST_ATLAS_CLIENT_ID }}
25+
MDB_MCP_API_CLIENT_SECRET: ${{ secrets.TEST_ATLAS_CLIENT_SECRET }}
26+
MDB_MCP_API_BASE_URL: ${{ vars.TEST_ATLAS_BASE_URL }}
27+
run: npm run atlas:cleanup
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
"generate": "./scripts/generate.sh",
5656
"test": "vitest --project eslint-rules --project unit-and-integration --coverage",
5757
"pretest:accuracy": "npm run build",
58-
"test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh"
58+
"test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh",
59+
"atlas:cleanup": "vitest --project atlas-cleanup"
5960
},
6061
"license": "Apache-2.0",
6162
"devDependencies": {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { Group, AtlasOrganization } from "../src/common/atlas/openapi.js";
2+
import { ApiClient } from "../src/common/atlas/apiClient.js";
3+
import { ConsoleLogger } from "../src/common/logger.js";
4+
import { Keychain } from "../src/lib.js";
5+
import { describe, it } from "vitest";
6+
7+
function isOlderThanADay(date: string): boolean {
8+
const oneDayInMs = 24 * 60 * 60 * 1000;
9+
const projectDate = new Date(date);
10+
const currentDate = new Date();
11+
return currentDate.getTime() - projectDate.getTime() > oneDayInMs;
12+
}
13+
14+
async function findTestOrganization(client: ApiClient): Promise<AtlasOrganization> {
15+
const orgs = await client.listOrganizations();
16+
const testOrg = orgs?.results?.find((org) => org.name === "MongoDB MCP Test");
17+
18+
if (!testOrg) {
19+
throw new Error('Test organization "MongoDB MCP Test" not found.');
20+
}
21+
22+
return testOrg;
23+
}
24+
25+
async function findAllTestProjects(client: ApiClient, orgId: string): Promise<Group[]> {
26+
const projects = await client.listOrganizationProjects({
27+
params: {
28+
path: {
29+
orgId,
30+
},
31+
},
32+
});
33+
34+
const testProjects = projects?.results?.filter((proj) => proj.name.startsWith("testProj-")) || [];
35+
return testProjects.filter((proj) => isOlderThanADay(proj.created));
36+
}
37+
38+
async function deleteAllClustersOnStaleProject(client: ApiClient, projectId: string): Promise<void> {
39+
const allClusters = await client
40+
.listClusters({
41+
params: {
42+
path: {
43+
groupId: projectId || "",
44+
},
45+
},
46+
})
47+
.then((res) => res.results || []);
48+
49+
await Promise.allSettled(
50+
allClusters.map((cluster) =>
51+
client.deleteCluster({ params: { path: { groupId: projectId || "", clusterName: cluster.name || "" } } })
52+
)
53+
);
54+
}
55+
56+
async function main(): Promise<void> {
57+
const apiClient = new ApiClient(
58+
{
59+
baseUrl: process.env.MDB_MCP_API_BASE_URL || "https://cloud-dev.mongodb.com",
60+
credentials: {
61+
clientId: process.env.MDB_MCP_API_CLIENT_ID || "",
62+
clientSecret: process.env.MDB_MCP_API_CLIENT_SECRET || "",
63+
},
64+
},
65+
new ConsoleLogger(Keychain.root)
66+
);
67+
68+
const testOrg = await findTestOrganization(apiClient);
69+
const testProjects = await findAllTestProjects(apiClient, testOrg.id || "");
70+
71+
if (testProjects.length === 0) {
72+
console.log("No stale test projects found for cleanup.");
73+
}
74+
75+
for (const project of testProjects) {
76+
console.log(`Cleaning up project: ${project.name} (${project.id})`);
77+
if (!project.id) {
78+
console.warn(`Skipping project with missing ID: ${project.name}`);
79+
continue;
80+
}
81+
82+
await deleteAllClustersOnStaleProject(apiClient, project.id);
83+
await apiClient.deleteProject({
84+
params: {
85+
path: {
86+
groupId: project.id,
87+
},
88+
},
89+
});
90+
console.log(`Deleted project: ${project.name} (${project.id})`);
91+
}
92+
93+
return;
94+
}
95+
96+
describe("Cleanup Atlas Test Leftovers", () => {
97+
it("should clean up stale test projects", async () => {
98+
await main();
99+
});
100+
});

vitest.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default defineConfig({
2626
test: {
2727
name: "unit-and-integration",
2828
include: ["**/*.test.ts"],
29-
exclude: [...vitestDefaultExcludes, "tests/accuracy/**"],
29+
exclude: [...vitestDefaultExcludes, "scripts/**", "tests/accuracy/**"],
3030
},
3131
},
3232
{
@@ -43,6 +43,13 @@ export default defineConfig({
4343
include: ["eslint-rules/*.test.js"],
4444
},
4545
},
46+
{
47+
extends: true,
48+
test: {
49+
name: "atlas-cleanup",
50+
include: ["scripts/cleanupAtlasTestLeftovers.test.ts"],
51+
},
52+
},
4653
],
4754
},
4855
});

0 commit comments

Comments
 (0)