diff --git a/.github/workflows/cleanup-atlas-env.yml b/.github/workflows/cleanup-atlas-env.yml new file mode 100644 index 00000000..8da64583 --- /dev/null +++ b/.github/workflows/cleanup-atlas-env.yml @@ -0,0 +1,27 @@ +--- +name: "Cleanup stale Atlas test environments" +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +permissions: {} + +jobs: + cleanup-envs: + runs-on: ubuntu-latest + steps: + - uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version-file: package.json + cache: "npm" + - name: Install dependencies + run: npm ci + - name: Run cleanup script + env: + MDB_MCP_API_CLIENT_ID: ${{ secrets.TEST_ATLAS_CLIENT_ID }} + MDB_MCP_API_CLIENT_SECRET: ${{ secrets.TEST_ATLAS_CLIENT_SECRET }} + MDB_MCP_API_BASE_URL: ${{ vars.TEST_ATLAS_BASE_URL }} + run: npm run atlas:cleanup diff --git a/.github/workflows/code_health_fork.yaml b/.github/workflows/code-health-fork.yml similarity index 100% rename from .github/workflows/code_health_fork.yaml rename to .github/workflows/code-health-fork.yml diff --git a/.github/workflows/code_health.yaml b/.github/workflows/code-health.yml similarity index 100% rename from .github/workflows/code_health.yaml rename to .github/workflows/code-health.yml diff --git a/.github/workflows/dependabot_pr.yaml b/.github/workflows/dependabot-pr.yml similarity index 100% rename from .github/workflows/dependabot_pr.yaml rename to .github/workflows/dependabot-pr.yml diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yml similarity index 100% rename from .github/workflows/docker.yaml rename to .github/workflows/docker.yml diff --git a/.github/workflows/prepare_release.yaml b/.github/workflows/prepare-release.yml similarity index 100% rename from .github/workflows/prepare_release.yaml rename to .github/workflows/prepare-release.yml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yml similarity index 100% rename from .github/workflows/publish.yaml rename to .github/workflows/publish.yml diff --git a/package.json b/package.json index e689ea9c..6c327f09 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "generate": "./scripts/generate.sh", "test": "vitest --project eslint-rules --project unit-and-integration --coverage", "pretest:accuracy": "npm run build", - "test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh" + "test:accuracy": "sh ./scripts/accuracy/runAccuracyTests.sh", + "atlas:cleanup": "vitest --project atlas-cleanup" }, "license": "Apache-2.0", "devDependencies": { diff --git a/scripts/cleanupAtlasTestLeftovers.test.ts b/scripts/cleanupAtlasTestLeftovers.test.ts new file mode 100644 index 00000000..24351c8b --- /dev/null +++ b/scripts/cleanupAtlasTestLeftovers.test.ts @@ -0,0 +1,100 @@ +import type { Group, AtlasOrganization } from "../src/common/atlas/openapi.js"; +import { ApiClient } from "../src/common/atlas/apiClient.js"; +import { ConsoleLogger } from "../src/common/logger.js"; +import { Keychain } from "../src/lib.js"; +import { describe, it } from "vitest"; + +function isOlderThanADay(date: string): boolean { + const oneDayInMs = 24 * 60 * 60 * 1000; + const projectDate = new Date(date); + const currentDate = new Date(); + return currentDate.getTime() - projectDate.getTime() > oneDayInMs; +} + +async function findTestOrganization(client: ApiClient): Promise { + const orgs = await client.listOrganizations(); + const testOrg = orgs?.results?.find((org) => org.name === "MongoDB MCP Test"); + + if (!testOrg) { + throw new Error('Test organization "MongoDB MCP Test" not found.'); + } + + return testOrg; +} + +async function findAllTestProjects(client: ApiClient, orgId: string): Promise { + const projects = await client.listOrganizationProjects({ + params: { + path: { + orgId, + }, + }, + }); + + const testProjects = projects?.results?.filter((proj) => proj.name.startsWith("testProj-")) || []; + return testProjects.filter((proj) => isOlderThanADay(proj.created)); +} + +async function deleteAllClustersOnStaleProject(client: ApiClient, projectId: string): Promise { + const allClusters = await client + .listClusters({ + params: { + path: { + groupId: projectId || "", + }, + }, + }) + .then((res) => res.results || []); + + await Promise.allSettled( + allClusters.map((cluster) => + client.deleteCluster({ params: { path: { groupId: projectId || "", clusterName: cluster.name || "" } } }) + ) + ); +} + +async function main(): Promise { + const apiClient = new ApiClient( + { + baseUrl: process.env.MDB_MCP_API_BASE_URL || "https://cloud-dev.mongodb.com", + credentials: { + clientId: process.env.MDB_MCP_API_CLIENT_ID || "", + clientSecret: process.env.MDB_MCP_API_CLIENT_SECRET || "", + }, + }, + new ConsoleLogger(Keychain.root) + ); + + const testOrg = await findTestOrganization(apiClient); + const testProjects = await findAllTestProjects(apiClient, testOrg.id || ""); + + if (testProjects.length === 0) { + console.log("No stale test projects found for cleanup."); + } + + for (const project of testProjects) { + console.log(`Cleaning up project: ${project.name} (${project.id})`); + if (!project.id) { + console.warn(`Skipping project with missing ID: ${project.name}`); + continue; + } + + await deleteAllClustersOnStaleProject(apiClient, project.id); + await apiClient.deleteProject({ + params: { + path: { + groupId: project.id, + }, + }, + }); + console.log(`Deleted project: ${project.name} (${project.id})`); + } + + return; +} + +describe("Cleanup Atlas Test Leftovers", () => { + it("should clean up stale test projects", async () => { + await main(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 903a174a..a8967a47 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ test: { name: "unit-and-integration", include: ["**/*.test.ts"], - exclude: [...vitestDefaultExcludes, "tests/accuracy/**"], + exclude: [...vitestDefaultExcludes, "scripts/**", "tests/accuracy/**"], }, }, { @@ -43,6 +43,13 @@ export default defineConfig({ include: ["eslint-rules/*.test.js"], }, }, + { + extends: true, + test: { + name: "atlas-cleanup", + include: ["scripts/cleanupAtlasTestLeftovers.test.ts"], + }, + }, ], }, });