import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { afterEach, describe, it } from "vitest"; import { formatGithubCiWorkflowResult, verifyGithubCiWorkflow } from "./verify-github-ci-workflow.mjs"; const VALID_WORKFLOW = `name: CI on: push: pull_request: concurrency: group: \${{ github.workflow }}-\${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: quality: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Prepare pnpm run: corepack prepare pnpm@10.33.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Verify GitHub CI workflow run: pnpm run verify:github-ci-workflow - name: Verify Gitea CI workflow run: pnpm run verify:gitea-ci-workflow - name: Verify deploy workflow run: pnpm run verify:deploy-workflow - name: Verify deploy readiness run: pnpm run verify:deploy-readiness - name: Verify CI baseline run: pnpm run verify:ci-baseline - name: Verify source health run: pnpm run verify:source-health - name: Verify ESLint baseline run: pnpm run verify:eslint-baseline - name: Verify env drift run: pnpm run verify:env-drift - name: Verify health baseline run: pnpm run verify:health-baseline - name: Verify API inventory run: pnpm run verify:api-inventory - name: Verify tenant isolation report run: pnpm run verify:tenant-isolation-report - name: Verify sidebar routes run: pnpm run verify:sidebar-routes - name: Verify i18n run: pnpm run verify:i18n - name: Verify Tenant Auth baseline gates run: pnpm run verify:tenant-auth-baseline - name: Verify Tenant Auth onboarding flow run: pnpm run verify:onboarding-flow - name: Verify Phase C boundaries run: pnpm run verify:phase-c-boundaries - name: Verify Feature Manifest core without plugins run: pnpm run verify:core-build-without-plugins-smoke - name: Verify Feature Manifest removed modules run: pnpm run verify:removed-modules - name: Verify UI mock static baseline run: pnpm run verify:ui-visual - name: Test UI hot-swap run: pnpm run test:hot-swap - name: Verify accessibility coverage run: pnpm run verify:a11y-coverage - name: Test accessibility smoke run: pnpm run test:a11y - name: Verify docs run: | pnpm run verify:porting-docs-baseline pnpm run verify:production-docs pnpm run verify:release-docs pnpm run verify:docs-links - name: Typecheck run: pnpm typecheck - name: Lint run: pnpm lint - name: Foundation package tests run: pnpm run test:foundation - name: Package tests run: pnpm run test:packages - name: Mobile wrapper tests run: pnpm run test:mobile - name: Unit tests run: pnpm test - name: Build run: pnpm run build:ci - name: Verify bundle size run: pnpm run verify:bundle-size - name: Verify web Sentry sourcemaps run: test -n "$(find apps/web/build/client -type f -name '*.map' -print -quit)" - name: Upload web Sentry sourcemaps uses: actions/upload-artifact@v4 with: name: web-client-sourcemaps path: apps/web/build/client if-no-files-found: error retention-days: 7 - name: Verify production seed artifact run: pnpm run verify:production-seed-artifact api: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Prepare pnpm run: corepack prepare pnpm@10.33.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Verify OpenAPI security run: pnpm run verify:openapi-security - name: Typecheck API run: pnpm --filter @toolctrl/api typecheck - name: Test API run: pnpm exec vitest run apps/api/src - name: Build API run: pnpm --filter @toolctrl/api build worker: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Prepare pnpm run: corepack prepare pnpm@10.33.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Typecheck Worker run: pnpm --filter @toolctrl/worker typecheck - name: Test Worker run: pnpm --filter @toolctrl/worker exec vitest run - name: Build Worker run: pnpm --filter @toolctrl/worker build - name: Health Check Worker run: pnpm --filter @toolctrl/worker health database: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Prepare pnpm run: corepack prepare pnpm@10.33.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Verify database migration status run: pnpm run verify:db-migrate-status env: DATABASE_URL: \${{ secrets.DATABASE_URL }} - name: Generate database client run: pnpm run db:generate env: DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public docker-smoke: runs-on: ubuntu-latest needs: [quality, api, worker, database] steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build web Docker target uses: docker/build-push-action@v7 with: context: . target: web build-args: APP_REVISION=ci-smoke-revision tags: toolctrl-web:smoke load: true push: false cache-from: type=gha,scope=toolctrl-web-smoke cache-to: type=gha,mode=max,scope=toolctrl-web-smoke - name: Validate web Docker artifacts run: docker run --rm --entrypoint /bin/sh toolctrl-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js' - name: Validate web Docker runtime run: bash scripts/docker-web-runtime-smoke.sh toolctrl-web:smoke - name: Build API Docker target uses: docker/build-push-action@v7 with: context: . target: api build-args: APP_REVISION=ci-smoke-revision tags: toolctrl-api:smoke load: true push: false cache-from: type=gha,scope=toolctrl-api-smoke cache-to: type=gha,mode=max,scope=toolctrl-api-smoke - name: Validate API Docker runtime run: bash scripts/docker-api-runtime-smoke.sh toolctrl-api:smoke - name: Build worker Docker target uses: docker/build-push-action@v7 with: context: . target: worker build-args: APP_REVISION=ci-smoke-revision tags: toolctrl-worker:smoke load: true push: false cache-from: type=gha,scope=toolctrl-worker-smoke cache-to: type=gha,mode=max,scope=toolctrl-worker-smoke - name: Validate worker Docker runtime run: bash scripts/docker-worker-runtime-smoke.sh toolctrl-worker:smoke - name: Validate Docker Compose config run: docker compose config sentry-release-web: needs: resolve-sentry-environment uses: ./.github/workflows/_sentry-release.yml with: project: boilerplate-web environment: preview sourcemaps-path: apps/web/build/client sourcemaps-artifact: web-client-sourcemaps `; const SENTRY_REUSABLE_WORKFLOW = `name: Sentry Release on: workflow_call: inputs: sourcemaps-path: required: false type: string default: "none" sourcemaps-artifact: required: false type: string default: "none" jobs: release: runs-on: ubuntu-latest steps: - name: Download source-map artifact if: \${{ inputs.sourcemaps-artifact != 'none' }} uses: actions/download-artifact@v4 with: name: \${{ inputs.sourcemaps-artifact }} path: \${{ inputs.sourcemaps-path }} `; const fixtures = []; function writeFile(rootDir, relativePath, content) { const absolutePath = path.join(rootDir, relativePath); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); fs.writeFileSync(absolutePath, content); } function createFixture(workflowContent = VALID_WORKFLOW) { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "github-ci-workflow-")); fixtures.push(rootDir); if (workflowContent !== null) { writeFile(rootDir, ".github/workflows/ci.yml", workflowContent); writeFile(rootDir, ".github/workflows/_sentry-release.yml", SENTRY_REUSABLE_WORKFLOW); } return rootDir; } afterEach(() => { while (fixtures.length > 0) { fs.rmSync(fixtures.pop(), { recursive: true, force: true }); } }); describe("verify-github-ci-workflow", () => { it("passes for a valid GitHub CI workflow", () => { const result = verifyGithubCiWorkflow(createFixture()); const output = formatGithubCiWorkflowResult(result); assert.deepEqual(result.failures, []); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run verify:github-ci-workflow" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run verify:source-health" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run verify:eslint-baseline" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run verify:health-baseline" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run verify:tenant-auth-baseline" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run test:hot-swap" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: service job "api" does not need "quality"')); assert.ok(result.passes.includes('.github/workflows/ci.yml: service job "api" includes command "pnpm run verify:openapi-security"')); assert.ok(result.passes.includes('.github/workflows/ci.yml: service job "worker" does not need "quality"')); assert.ok(result.passes.includes('.github/workflows/ci.yml: database job "database" does not need "quality"')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" builds Docker target "worker"')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" loads Docker target "worker" for smoke validation')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" uses GitHub Actions cache for Docker target "worker"')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" does not run Corepack setup')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality command "pnpm run test:mobile" is ordered')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" validates web Docker artifacts')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" validates web Docker runtime')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" validates API Docker runtime')); assert.ok(result.passes.includes('.github/workflows/ci.yml: Docker smoke job "docker-smoke" validates worker Docker runtime')); assert.ok(result.passes.includes('.github/workflows/ci.yml: quality job uploads web source maps as artifact "web-client-sourcemaps"')); assert.ok(result.passes.includes(".github/workflows/_sentry-release.yml: reusable workflow downloads requested source-map artifacts before upload")); assert.ok(result.passes.includes('.github/workflows/ci.yml: Sentry web release consumes artifact "web-client-sourcemaps"')); assert.ok(result.passes.includes(".github/workflows/ci.yml: no forbidden E2E/browser commands are present")); assert.ok(result.passes.includes(".github/workflows/ci.yml: CI concurrency cancels superseded pull request and branch runs")); assert.match(output, /^PASS GitHub CI workflow verification/); }); it("fails when CI concurrency does not cancel superseded runs", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( /concurrency:\n group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.ref \}\}\n cancel-in-progress: true\n\n/u, "", ), ), ); assert.match(result.failures.join("\n"), /CI workflow must define concurrency by workflow plus PR\/ref with cancel-in-progress: true/); }); it("fails when Docker smoke installs pnpm dependencies", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Set up Docker Buildx\n uses: docker/setup-buildx-action@v3\n", " - name: Enable Corepack\n run: corepack enable\n - name: Prepare pnpm\n run: corepack prepare pnpm@10.33.0 --activate\n - name: Install dependencies\n run: pnpm install --frozen-lockfile\n - name: Set up Docker Buildx\n uses: docker/setup-buildx-action@v3\n", ), ), ); const failures = result.failures.join("\n"); assert.match(failures, /Docker smoke job "docker-smoke" must not run Corepack setup/); assert.match(failures, /Docker smoke job "docker-smoke" must not run "corepack prepare pnpm@10\.33\.0"/); assert.match(failures, /Docker smoke job "docker-smoke" must not run "pnpm install --frozen-lockfile"/); }); it("fails when the workflow file is missing", () => { const result = verifyGithubCiWorkflow(createFixture(null)); assert.match(result.failures.join("\n"), /\.github\/workflows\/ci\.yml: missing required GitHub CI workflow file/); }); it("fails when pnpm setup exists only outside the quality job", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace(" - name: Enable Corepack\n run: corepack enable\n", "") .replace(" - name: Prepare pnpm\n run: corepack prepare pnpm@10.33.0 --activate\n", "") .replace(" - name: Install dependencies\n run: pnpm install --frozen-lockfile\n", "") .replace(" api:\n", " other:\n runs-on: ubuntu-latest\n steps:\n - name: Enable Corepack\n run: corepack enable\n - name: Prepare pnpm\n run: corepack prepare pnpm@10.33.0 --activate\n - name: Install dependencies\n run: pnpm install --frozen-lockfile\n api:\n"), ), ); assert.match(result.failures.join("\n"), /quality job missing Corepack setup/); assert.match(result.failures.join("\n"), /quality job must pin pnpm@10\.33\.0/); assert.match(result.failures.join("\n"), /quality job missing "pnpm install --frozen-lockfile"/); }); it("fails when a quality gate is missing from the quality job", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" - name: Verify Phase C boundaries\n run: pnpm run verify:phase-c-boundaries\n", ""))); assert.match(result.failures.join("\n"), /quality job missing required command "pnpm run verify:phase-c-boundaries"/); }); it("fails when a quality gate is duplicated in the quality job", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Verify source health\n run: pnpm run verify:source-health\n", " - name: Verify source health\n run: pnpm run verify:source-health\n - name: Verify source health again\n run: pnpm run verify:source-health\n", ), ), ); assert.match(result.failures.join("\n"), /quality command "pnpm run verify:source-health" must run exactly once to avoid redundant CI gates/); }); it("fails when the health baseline gate is missing from the quality job", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" - name: Verify health baseline\n run: pnpm run verify:health-baseline\n", ""))); assert.match(result.failures.join("\n"), /quality job missing required command "pnpm run verify:health-baseline"/); }); it("fails when the source health gate is missing from the quality job", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" - name: Verify source health\n run: pnpm run verify:source-health\n", ""))); assert.match(result.failures.join("\n"), /quality job missing required command "pnpm run verify:source-health"/); }); it("fails when the ESLint baseline gate is missing from the quality job", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" - name: Verify ESLint baseline\n run: pnpm run verify:eslint-baseline\n", ""))); assert.match(result.failures.join("\n"), /quality job missing required command "pnpm run verify:eslint-baseline"/); }); it("fails when service commands are misplaced into another job", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace(" - name: Test API\n run: pnpm exec vitest run apps/api/src\n", "") .replace(" - name: Build Worker\n run: pnpm --filter @toolctrl/worker build\n", " - name: Build Worker\n run: pnpm --filter @toolctrl/worker build\n - name: Misplaced API tests\n run: pnpm exec vitest run apps/api/src\n"), ), ); assert.match(result.failures.join("\n"), /service job "api" missing command "pnpm exec vitest run apps\/api\/src"/); }); it("fails when service jobs drop repository setup", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace(" - name: Checkout\n uses: actions/checkout@v4\n - name: Enable Corepack\n run: corepack enable\n - name: Prepare pnpm\n run: corepack prepare pnpm@10.33.0 --activate\n - name: Install dependencies\n run: pnpm install --frozen-lockfile\n - name: Verify OpenAPI security\n", " - name: Verify OpenAPI security\n"), ), ); assert.match(result.failures.join("\n"), /service job "api" missing checkout step/); assert.match(result.failures.join("\n"), /service job "api" missing Corepack setup/); assert.match(result.failures.join("\n"), /service job "api" must pin pnpm@10\.33\.0/); assert.match(result.failures.join("\n"), /service job "api" missing "pnpm install --frozen-lockfile"/); }); it("accepts block-list needs syntax for Docker smoke fan-in", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace(" needs: [quality, api, worker, database]\n", " needs:\n - quality\n - api\n - worker\n - database\n"), ), ); assert.deepEqual(result.failures, []); }); it("fails when service jobs need quality", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace(" api:\n runs-on: ubuntu-latest\n", " api:\n runs-on: ubuntu-latest\n needs: quality\n") .replace(" worker:\n runs-on: ubuntu-latest\n", " worker:\n runs-on: ubuntu-latest\n needs:\n - quality\n"), ), ); const failures = result.failures.join("\n"); assert.match(failures, /service job "api" must not need "quality" so PR CI can run it in parallel/); assert.match(failures, /service job "worker" must not need "quality" so PR CI can run it in parallel/); }); it("fails when database job needs quality", () => { const result = verifyGithubCiWorkflow( createFixture(VALID_WORKFLOW.replace(" database:\n runs-on: ubuntu-latest\n", " database:\n runs-on: ubuntu-latest\n needs: [quality]\n")), ); assert.match(result.failures.join("\n"), /database job "database" must not need "quality" so PR CI can run it in parallel/); }); it("fails when a required quality gate is softened at step level", () => { const result = verifyGithubCiWorkflow( createFixture(VALID_WORKFLOW.replace(" - name: Lint\n run: pnpm lint\n", " - name: Lint\n if: always()\n run: pnpm lint\n")), ); assert.match(result.failures.join("\n"), /quality command "pnpm lint" must not use continue-on-error or always\(\)/); }); it("fails when a required quality gate masks shell failures in its run block", () => { const result = verifyGithubCiWorkflow( createFixture(VALID_WORKFLOW.replace(" - name: Lint\n run: pnpm lint\n", " - name: Lint\n run: |\n set +e\n pnpm lint\n")), ); assert.match(result.failures.join("\n"), /quality command "pnpm lint" must not use shell success masking/); }); it("fails when a service command is pass-opened with shell control operators", () => { const result = verifyGithubCiWorkflow( createFixture(VALID_WORKFLOW.replace(" - name: Typecheck API\n run: pnpm --filter @toolctrl/api typecheck\n", " - name: Typecheck API\n run: pnpm --filter @toolctrl/api typecheck || true\n")), ); assert.match(result.failures.join("\n"), /service job "api" command "pnpm --filter @toolctrl\/api typecheck" must not use shell success masking/); }); it("fails when Docker smoke validation ends with an unconditional true", () => { const result = verifyGithubCiWorkflow( createFixture(VALID_WORKFLOW.replace(" - name: Validate Docker Compose config\n run: docker compose config\n", " - name: Validate Docker Compose config\n run: |\n docker compose config\n true\n")), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" command "docker compose config" must not use shell success masking/); }); it("fails when Docker smoke drops repository checkout", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " docker-smoke:\n runs-on: ubuntu-latest\n needs: [quality, api, worker, database]\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n", " docker-smoke:\n runs-on: ubuntu-latest\n needs: [quality, api, worker, database]\n steps:\n", ), ), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" missing checkout step/); }); it("fails when database secrets are exposed at job scope", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" database:\n runs-on: ubuntu-latest\n", " database:\n runs-on: ubuntu-latest\n env:\n DATABASE_URL: ${{ secrets.DATABASE_URL }}\n"))); assert.match(result.failures.join("\n"), /database job "database" must not set DATABASE_URL at job scope/); }); it("fails when db:generate does not get a deterministic dummy database URL", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Generate database client\n run: pnpm run db:generate\n env:\n DATABASE_URL: postgresql://toolctrl:toolctrl@localhost:5432/toolctrl?schema=public\n", " - name: Generate database client\n run: pnpm run db:generate\n", ), ), ); assert.match(result.failures.join("\n"), /database job "database" must set DATABASE_URL: postgresql:\/\/toolctrl:toolctrl@localhost:5432\/toolctrl\?schema=public on the db:generate step/); }); it("fails when Docker smoke publishes an image", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Validate Docker Compose config\n run: docker compose config\n", " - name: Publish\n run: docker push registry.example.com/toolctrl/web:latest\n - name: Validate Docker Compose config\n run: docker compose config\n", ), ), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" contains forbidden publish command/); }); it("fails when browser E2E commands are added to GitHub CI", () => { const result = verifyGithubCiWorkflow(createFixture(`${VALID_WORKFLOW} - name: Browser E2E\n run: pnpm exec playwright test\n`)); assert.match(result.failures.join("\n"), /contains forbidden E2E\/browser command/); }); it("fails when Docker smoke builds omit the revision build arg", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" build-args: APP_REVISION=ci-smoke-revision\n", ""))); assert.match(result.failures.join("\n"), /\.github\/workflows\/ci\.yml: Docker smoke job "docker-smoke" build for target "web" must pass build-args: APP_REVISION=ci-smoke-revision/); }); it("fails when Docker smoke builds omit GitHub Actions cache", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace(" cache-from: type=gha,scope=toolctrl-web-smoke\n", "").replace(" cache-to: type=gha,mode=max,scope=toolctrl-web-smoke\n", ""), ), ); const failures = result.failures.join("\n"); assert.match(failures, /Docker smoke job "docker-smoke" build for target "web" must set cache-from: type=gha with scope=toolctrl-web-smoke/); assert.match(failures, /Docker smoke job "docker-smoke" build for target "web" must set cache-to: type=gha,mode=max with scope=toolctrl-web-smoke/); }); it("fails when Docker smoke builds do not load images for validation", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" load: true\n", ""))); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" build for target "web" must set load: true for smoke validation/); }); it("fails when Docker smoke builds change smoke tags", () => { const result = verifyGithubCiWorkflow(createFixture(VALID_WORKFLOW.replace(" tags: toolctrl-web:smoke\n", " tags: toolctrl-web:latest\n"))); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" build for target "web" must preserve tag "toolctrl-web:smoke"/); }); it("fails when Docker smoke omits web artifact validation", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Validate web Docker artifacts\n run: docker run --rm --entrypoint /bin/sh toolctrl-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/build/server-runtime/server.js'\n", "", ), ), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" missing command "docker run --rm --entrypoint \/bin\/sh toolctrl-web:smoke/); }); it("fails when Docker smoke omits web runtime validation", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Validate web Docker runtime\n run: bash scripts/docker-web-runtime-smoke.sh toolctrl-web:smoke\n", "", ), ), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" missing command "bash scripts\/docker-web-runtime-smoke\.sh toolctrl-web:smoke"/); }); it("fails when Docker smoke omits API runtime validation", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Validate API Docker runtime\n run: bash scripts/docker-api-runtime-smoke.sh toolctrl-api:smoke\n", "", ), ), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" missing command "bash scripts\/docker-api-runtime-smoke\.sh toolctrl-api:smoke"/); }); it("fails when Docker smoke omits worker runtime validation", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Validate worker Docker runtime\n run: bash scripts/docker-worker-runtime-smoke.sh toolctrl-worker:smoke\n", "", ), ), ); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" missing command "bash scripts\/docker-worker-runtime-smoke\.sh toolctrl-worker:smoke"/); }); it("fails when web source maps are not uploaded for the Sentry release", () => { const result = verifyGithubCiWorkflow( createFixture( VALID_WORKFLOW.replace( " - name: Verify web Sentry sourcemaps\n run: test -n \"$(find apps/web/build/client -type f -name '*.map' -print -quit)\"\n - name: Upload web Sentry sourcemaps\n uses: actions/upload-artifact@v4\n with:\n name: web-client-sourcemaps\n path: apps/web/build/client\n if-no-files-found: error\n retention-days: 7\n", "", ), ), ); const failures = result.failures.join("\n"); assert.match(failures, /quality job must validate web source-map files before Sentry artifact upload/); assert.match(failures, /quality job must upload apps\/web\/build\/client as artifact "web-client-sourcemaps"/); }); it("fails when the reusable Sentry workflow does not download source-map artifacts", () => { const rootDir = createFixture(); writeFile(rootDir, ".github/workflows/_sentry-release.yml", "name: Sentry Release\n"); const result = verifyGithubCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.github\/workflows\/_sentry-release\.yml: reusable workflow must accept sourcemaps-artifact and download it to inputs\.sourcemaps-path/); }); it("CLI exits non-zero and prints path-specific output for an invalid workflow", () => { const rootDir = createFixture(null); const scriptPath = path.resolve("scripts/verify-github-ci-workflow.mjs"); assert.throws( () => execFileSync(process.execPath, [scriptPath], { cwd: rootDir, encoding: "utf8", stdio: "pipe" }), (error) => { assert.equal(error.status, 1); assert.match(error.stderr, /^FAIL GitHub CI workflow verification/); assert.match(error.stderr, /FAIL \.github\/workflows\/ci\.yml: missing required GitHub CI workflow file/); return true; }, ); }); });