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 { formatGiteaCiWorkflowResult, verifyGiteaCiWorkflow } from "./verify-gitea-ci-workflow.mjs"; const VALID_WORKFLOW = `name: CI on: pull_request: workflow_dispatch: concurrency: group: \${{ github.workflow }}-\${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: quality: runs-on: toolctrl-ci steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Pin 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 Porting Docs baseline gates run: pnpm run verify:porting-docs-baseline - name: Verify Production Docs docs run: pnpm run verify:production-docs - name: Verify Release Docs release docs run: pnpm run verify:release-docs - name: Verify docs links run: 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: toolctrl-ci steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Pin 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: toolctrl-ci steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Pin 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: Verify workflow outbox native readiness run: pnpm run verify:workflow-outbox-native - name: Build Worker run: pnpm --filter @toolctrl/worker build - name: Health Check Worker run: pnpm --filter @toolctrl/worker health database: runs-on: toolctrl-ci steps: - name: Checkout uses: actions/checkout@v4 - name: Enable Corepack run: corepack enable - name: Pin pnpm run: corepack prepare pnpm@10.33.0 --activate - name: Install dependencies run: pnpm install --frozen-lockfile - name: Generate database client run: pnpm run db:generate - name: Verify database migration status run: pnpm run verify:db-migrate-status:staging env: COOLIFY_STAGING_SSH_HOST: \${{ vars.COOLIFY_STAGING_SSH_HOST }} COOLIFY_STAGING_SSH_USER: \${{ vars.COOLIFY_STAGING_SSH_USER }} COOLIFY_STAGING_WEB_RESOURCE_UUID: \${{ vars.COOLIFY_STAGING_WEB_RESOURCE_UUID }} COOLIFY_STAGING_SSH_KEY: \${{ secrets.COOLIFY_STAGING_SSH_KEY }} docker-smoke: runs-on: toolctrl-ci 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 Docker smoke targets shell: bash run: | set -euo pipefail docker build --target web --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-web:smoke . & web_pid=$! docker build --target api --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-api:smoke . & api_pid=$! docker build --target worker --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-worker:smoke . & worker_pid=$! status=0 wait "$web_pid" || status=$? wait "$api_pid" || status=$? wait "$worker_pid" || status=$? exit "$status" - 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: Validate API Docker runtime run: bash scripts/docker-api-runtime-smoke.sh toolctrl-api:smoke env: DOCKER_API_SMOKE_TRANSPORT: exec - 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: ./.gitea/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: toolctrl-ci 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(), "gitea-ci-workflow-")); fixtures.push(rootDir); if (workflowContent !== null) { writeFile(rootDir, ".gitea/workflows/ci.yml", workflowContent); writeFile(rootDir, ".gitea/workflows/_sentry-release.yml", SENTRY_REUSABLE_WORKFLOW); } return rootDir; } afterEach(() => { while (fixtures.length > 0) { fs.rmSync(fixtures.pop(), { recursive: true, force: true }); } }); describe("verify-gitea-ci-workflow", () => { it("passes for a valid minimal Gitea quality workflow with service jobs", () => { const rootDir = createFixture(); const result = verifyGiteaCiWorkflow(rootDir); const output = formatGiteaCiWorkflowResult(result); assert.deepEqual(result.failures, []); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: trigger \"push\" is disabled so deploy is not queued behind CI on main")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: trigger \"workflow_dispatch\" is defined")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: pnpm@10.33.0 is pinned")); assert.ok(result.passes.some((pass) => /^\.gitea\/workflows\/ci\.yml:\d+: runner is "toolctrl-ci"$/.test(pass))); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:ci-baseline\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:source-health\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:eslint-baseline\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:github-ci-workflow\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:api-inventory\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:health-baseline\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:tenant-auth-baseline\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run verify:tenant-isolation-report\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: command \"pnpm run test:mobile\" is ordered")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"api\" is defined")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"api\" includes scoped command \"pnpm run verify:openapi-security\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"api\" includes scoped command \"pnpm --filter @toolctrl/api typecheck\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"worker\" is defined")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"worker\" includes scoped command \"pnpm run verify:workflow-outbox-native\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"worker\" includes scoped command \"pnpm --filter @toolctrl/worker build\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"worker\" includes scoped command \"pnpm --filter @toolctrl/worker health\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" is defined")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"api\" runs independently of \"quality\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: service job \"worker\" runs independently of \"quality\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" runs independently of \"quality\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" includes command \"pnpm run verify:db-migrate-status:staging\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" scopes staging SSH host to the migrate-status step")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" scopes staging SSH key to the migrate-status step")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" verifies private Coolify databases without exposing DATABASE_URL")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: database job \"database\" contains no deploy/seed/schema mutation commands")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" is defined")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" needs \"quality\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" needs \"database\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" does not run Corepack setup")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" builds Docker target \"web\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" builds Docker targets in parallel")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" validates web Docker artifacts")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" validates web Docker runtime")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" validates API Docker runtime")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" validates worker Docker runtime")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" validates Docker Compose config")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: web source maps are uploaded as artifact \"web-client-sourcemaps\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/_sentry-release.yml: reusable workflow downloads requested source-map artifacts before upload")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Sentry web release consumes artifact \"web-client-sourcemaps\"")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: Docker smoke job \"docker-smoke\" contains no registry login/push/publish commands")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: no forbidden E2E/browser commands are present")); assert.ok(result.passes.some((pass) => pass === ".gitea/workflows/ci.yml: CI concurrency cancels superseded pull request and branch runs")); assert.match(output, /^PASS Gitea CI workflow verification/); assert.match(output, /Summary: \d+ passed, 0 observed, 0 failed/); }); it("fails when CI concurrency does not cancel superseded runs", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( /concurrency:\n group: \$\{\{ github\.workflow \}\}-\$\{\{ github\.event\.pull_request\.number \|\| github\.ref \}\}\n cancel-in-progress: true\n\n/u, "", ), ); const result = verifyGiteaCiWorkflow(rootDir); 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 rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Set up Docker Buildx\n", " - name: Enable Corepack\n run: corepack enable\n - name: Pin 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", ), ); const result = verifyGiteaCiWorkflow(rootDir); 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 rootDir = createFixture(null); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required Gitea CI workflow file/); }); it("fails when a required gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" run: pnpm lint\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm lint"/); }); it("fails when pnpm setup is present only outside the quality job", () => { const rootDir = createFixture( VALID_WORKFLOW.replace(" - name: Enable Corepack\n run: corepack enable\n", "") .replace(" - name: Pin pnpm\n run: corepack prepare pnpm@10.33.0 --activate\n", "") .replace(" - name: Install dependencies\n run: pnpm install --frozen-lockfile\n", ""), ); const result = verifyGiteaCiWorkflow(rootDir); const failures = result.failures.join("\n"); assert.match(failures, /\.gitea\/workflows\/ci\.yml: missing Corepack setup/); assert.match(failures, /\.gitea\/workflows\/ci\.yml: pnpm must be pinned to pnpm@10\.33\.0/); assert.match(failures, /\.gitea\/workflows\/ci\.yml: missing required command "pnpm install --frozen-lockfile"/); }); it("fails when a required quality gate only appears in a shell comment", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" run: pnpm run verify:docs-links\n", " run: |\n # pnpm run verify:docs-links\n echo skipped\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:docs-links"/); }); it("fails when a required quality gate is duplicated", () => { const rootDir = 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", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm run verify:source-health" must run exactly once to avoid redundant CI gates/); }); it("fails when a required quality gate is pass-opened with shell control operators", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" run: pnpm lint\n", " run: pnpm lint || true\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm lint" must not use shell success masking/); }); it("fails when a required quality gate disables errexit in a run block", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" run: pnpm lint\n", " run: |\n set +e\n pnpm lint\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm lint" must not use shell success masking/); }); it("fails when a service command ends with an unconditional true", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Typecheck API\n run: pnpm --filter @toolctrl/api typecheck\n", " - name: Typecheck API\n run: |\n pnpm --filter @toolctrl/api typecheck\n true\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "api" command "pnpm --filter @toolctrl\/api typecheck" must not use shell success masking/); }); it("accepts block-list needs syntax for dependent jobs", () => { const rootDir = createFixture( VALID_WORKFLOW.replace(" needs: [quality, api, worker, database]\n", " needs:\n - quality\n - api\n - worker\n - database\n"), ); const result = verifyGiteaCiWorkflow(rootDir); assert.deepEqual(result.failures, []); }); it("fails when a required quality gate is softened at step level", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Lint\n run: pnpm lint\n", " - name: Lint\n continue-on-error: true\n run: pnpm lint\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /command "pnpm lint" must not use continue-on-error or always\(\)/); }); it("fails when a required quality gate adds skip flags or suffixes", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" run: pnpm test\n", " run: pnpm test -- --passWithNoTests\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm test"/); }); it("fails when worker tests are narrowed to explicit files", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " run: pnpm --filter @toolctrl/worker exec vitest run\n", " run: pnpm --filter @toolctrl/worker exec vitest run src/handlers.test.ts src/health.test.ts", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "worker" missing scoped command "pnpm --filter @toolctrl\/worker exec vitest run"/); }); it("fails when the worker service job omits the workflow outbox native readiness gate", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Verify workflow outbox native readiness\n run: pnpm run verify:workflow-outbox-native\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "worker" missing scoped command "pnpm run verify:workflow-outbox-native"/); }); it("fails when the worker service job omits its health check", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Health Check Worker\n run: pnpm --filter @toolctrl/worker health\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "worker" missing scoped command "pnpm --filter @toolctrl\/worker health"/); }); it("fails when the env drift gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify env drift\n run: pnpm run verify:env-drift\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:env-drift"/); }); it("fails when the env drift gate runs after typecheck", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Verify env drift\n run: pnpm run verify:env-drift\n - name: Verify health baseline\n run: pnpm run verify:health-baseline\n - name: Verify API inventory\n run: pnpm run verify:api-inventory\n", " - name: Verify health baseline\n run: pnpm run verify:health-baseline\n - name: Verify API inventory\n run: pnpm run verify:api-inventory\n - name: Verify env drift\n run: pnpm run verify:env-drift\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm run verify:health-baseline" must run after previous quality gates/); }); it("fails when the health baseline gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify health baseline\n run: pnpm run verify:health-baseline\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:health-baseline"/); }); it("fails when the source health gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify source health\n run: pnpm run verify:source-health\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:source-health"/); }); it("fails when the ESLint baseline gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify ESLint baseline\n run: pnpm run verify:eslint-baseline\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:eslint-baseline"/); }); it("fails when the API inventory gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify API inventory\n run: pnpm run verify:api-inventory\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:api-inventory"/); }); it("fails when a service job command only appears in a shell comment", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Typecheck API\n run: pnpm --filter @toolctrl/api typecheck\n", " - name: Typecheck API\n run: |\n # pnpm --filter @toolctrl/api typecheck\n echo skipped\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "api" missing scoped command "pnpm --filter @toolctrl\/api typecheck"/); }); it("fails when the tenant isolation report gate is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify tenant isolation report\n run: pnpm run verify:tenant-isolation-report\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "pnpm run verify:tenant-isolation-report"/); }); it("fails when the API inventory gate runs after typecheck", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Verify API inventory\n run: pnpm run verify:api-inventory\n - name: Verify tenant isolation report\n run: pnpm run verify:tenant-isolation-report\n - name: Verify sidebar routes\n run: pnpm run verify:sidebar-routes\n - name: Verify i18n\n run: pnpm run verify:i18n\n", " - name: Verify tenant isolation report\n run: pnpm run verify:tenant-isolation-report\n - name: Verify sidebar routes\n run: pnpm run verify:sidebar-routes\n - name: Verify i18n\n run: pnpm run verify:i18n\n - name: Verify API inventory\n run: pnpm run verify:api-inventory\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm run verify:i18n" must run after previous quality gates/); }); it("fails when a job uses ubuntu-latest instead of the required Gitea runner", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" runs-on: toolctrl-ci\n", " runs-on: ubuntu-latest\n")); const result = verifyGiteaCiWorkflow(rootDir); const output = formatGiteaCiWorkflowResult(result); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml:\d+: runs-on must be "toolctrl-ci" but found "ubuntu-latest"/); assert.match(output, /^FAIL Gitea CI workflow verification/); }); it("fails when lint runs before typecheck", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Typecheck\n run: pnpm typecheck\n - name: Lint\n run: pnpm lint\n", " - name: Lint\n run: pnpm lint\n - name: Typecheck\n run: pnpm typecheck\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm lint" must run after previous quality gates/); }); it("fails when a service job is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(/ worker:\n[\s\S]*$/, "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required service job "worker"/); }); it("fails when a service job waits for the quality job", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" api:\n runs-on: toolctrl-ci\n steps:\n", " api:\n runs-on: toolctrl-ci\n needs: quality\n steps:\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "api" must not wait for "quality"; scoped service checks should run in parallel/); assert.doesNotMatch(result.failures.join("\n"), /service job "worker" must not wait for "quality"/); }); it("fails when a scoped service command is missing from the owning job", () => { const rootDir = 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", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "api" missing scoped command "pnpm exec vitest run apps\/api\/src"/); assert.doesNotMatch(result.failures.join("\n"), /service job "worker" missing scoped command "pnpm exec vitest run apps\/api\/src"/); }); it("fails when the API service job omits the OpenAPI security guard", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Verify OpenAPI security\n run: pnpm run verify:openapi-security\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: service job "api" missing scoped command "pnpm run verify:openapi-security"/); }); it("fails when the database job is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(/ database:\n[\s\S]*? docker-smoke:/, " docker-smoke:")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required database job "database"/); }); it("fails when the database job waits for the quality job", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" database:\n runs-on: toolctrl-ci\n steps:\n", " database:\n runs-on: toolctrl-ci\n needs: quality\n steps:\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" must not wait for "quality"; database verification should run in parallel/); }); it("fails when the database job omits migrate status verification", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n env:\n COOLIFY_STAGING_SSH_HOST: ${{ vars.COOLIFY_STAGING_SSH_HOST }}\n COOLIFY_STAGING_SSH_USER: ${{ vars.COOLIFY_STAGING_SSH_USER }}\n COOLIFY_STAGING_WEB_RESOURCE_UUID: ${{ vars.COOLIFY_STAGING_WEB_RESOURCE_UUID }}\n COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" missing command "pnpm run verify:db-migrate-status:staging"/); }); it("fails when the database job omits the step-level staging SSH key secret", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" must set COOLIFY_STAGING_SSH_KEY: \$\{\{ secrets\.COOLIFY_STAGING_SSH_KEY \}\} on the migrate-status step/); }); it("fails when the database job omits the step-level staging host variable", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" COOLIFY_STAGING_SSH_HOST: ${{ vars.COOLIFY_STAGING_SSH_HOST }}\n", "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" must set COOLIFY_STAGING_SSH_HOST: \$\{\{ vars\.COOLIFY_STAGING_SSH_HOST \}\} on the migrate-status step/); }); it("fails when the database job exposes DATABASE_URL at job scope", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" database:\n runs-on: toolctrl-ci\n steps:\n - name: Checkout\n", " database:\n runs-on: toolctrl-ci\n env:\n DATABASE_URL: ${{ secrets.DATABASE_URL }}\n steps:\n - name: Checkout\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" must not set DATABASE_URL at job scope/); }); it("fails when the database job uses the direct DATABASE_URL secret", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}\n", " COOLIFY_STAGING_SSH_KEY: ${{ secrets.COOLIFY_STAGING_SSH_KEY }}\n DATABASE_URL: ${{ secrets.DATABASE_URL }}\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" must not require direct DATABASE_URL access to private Coolify databases/); }); it("fails when the database job contains a migration deploy command", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n", " - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n - name: Deploy migrations\n run: pnpm --filter @toolctrl/db prisma migrate deploy\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" contains forbidden deploy\/seed\/schema mutation command: run: pnpm --filter @toolctrl\/db prisma migrate deploy/); }); it("fails when the database job hides a migration deploy command in a multiline run block", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n", " - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n - name: Deploy migrations\n run: |\n pnpm run db:generate\n pnpm --filter @toolctrl/db prisma migrate deploy\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" contains forbidden deploy\/seed\/schema mutation command: run: pnpm run db:generate\npnpm --filter @toolctrl\/db prisma migrate deploy/); }); it("fails when the database job contains an E2E browser command", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n", " - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n - name: Browser check\n run: pnpm exec playwright test\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" contains forbidden E2E\/browser command: run: pnpm exec playwright test/); }); it("fails when the database job hides an E2E browser command in a multiline run block", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n", " - name: Verify database migration status\n run: pnpm run verify:db-migrate-status:staging\n - name: Browser check\n run: |\n pnpm run db:generate\n pnpm exec playwright test\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: database job "database" contains forbidden E2E\/browser command: run: pnpm run db:generate\npnpm exec playwright test/); }); it("fails when the Docker smoke job is missing", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(/ docker-smoke:\n[\s\S]*$/, "")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required Docker smoke job "docker-smoke"/); }); it("fails when the Docker smoke job is missing a required dependency", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" needs: [quality, api, worker, database]\n", " needs: [quality, api]\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: Docker smoke job "docker-smoke" must declare needs including "worker"/); assert.doesNotMatch(result.failures.join("\n"), /must declare needs including "quality"/); }); it("fails when the Docker smoke job does not depend on the database job", () => { const rootDir = createFixture(VALID_WORKFLOW.replace(" needs: [quality, api, worker, database]\n", " needs: [quality, api, worker]\n")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: Docker smoke job "docker-smoke" must declare needs including "database"/); }); it("fails when a Docker target build is missing from the Docker smoke job", () => { const rootDir = createFixture( VALID_WORKFLOW.replace(" - name: Build API\n run: pnpm --filter @toolctrl/api build\n", " - name: Build API\n run: pnpm --filter @toolctrl/api build\n - name: Misplaced Docker API build\n run: docker buildx build --target api --file Dockerfile .\n").replace( " docker build --target api --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-api:smoke . &\n api_pid=$!\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: Docker smoke job "docker-smoke" missing Docker build for target "api"/); assert.doesNotMatch(result.failures.join("\n"), /missing Docker build for target "web"/); }); it("fails when a Docker smoke build omits the revision build arg", () => { const rootDir = createFixture(VALID_WORKFLOW.replace("docker build --target web --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-web:smoke .", "docker build --target web --tag toolctrl-web:smoke .")); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: Docker smoke job "docker-smoke" build for target "web" must pass --build-arg APP_REVISION=ci-smoke-revision/); }); it("fails when the Docker smoke job contains a forbidden publish command", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Validate Docker Compose config\n run: docker compose config\n", " - name: Validate Docker Compose config\n run: docker compose config\n - name: Publish image\n run: docker push registry.example.com/toolctrl/api:latest\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: Docker smoke job "docker-smoke" contains forbidden publish command: run: docker push registry\.example\.com\/toolctrl\/api:latest/); }); it("fails when the Docker smoke job omits web artifact validation", () => { const rootDir = 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", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); 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 the Docker smoke job omits web runtime validation", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Validate web Docker runtime\n run: bash scripts/docker-web-runtime-smoke.sh toolctrl-web:smoke\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); 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 the Docker smoke job omits API runtime validation", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Validate API Docker runtime\n run: bash scripts/docker-api-runtime-smoke.sh toolctrl-api:smoke\n env:\n DOCKER_API_SMOKE_TRANSPORT: exec\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); 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 the Docker smoke job omits worker runtime validation", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Validate worker Docker runtime\n run: bash scripts/docker-worker-runtime-smoke.sh toolctrl-worker:smoke\n", "", ), ); const result = verifyGiteaCiWorkflow(rootDir); 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 Docker smoke target builds are not parallelized", () => { const sequentialBuilds = ` - name: Build web Docker target run: docker build --target web --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-web:smoke . - name: Build api Docker target run: docker build --target api --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-api:smoke . - name: Build worker Docker target run: docker build --target worker --build-arg APP_REVISION=ci-smoke-revision --tag toolctrl-worker:smoke . `; const rootDir = createFixture( VALID_WORKFLOW.replace(/ - name: Build Docker smoke targets\n shell: bash\n run: \|\n(?: .*\n)+? - name: Validate web Docker artifacts\n/, `${sequentialBuilds} - name: Validate web Docker artifacts\n`), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" must build web, api, and worker Docker targets in one parallel bash step/); assert.doesNotMatch(result.failures.join("\n"), /missing Docker build for target "api"/); }); it("fails when the Docker smoke job hides a forbidden publish command in a multiline run block", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Validate Docker Compose config\n run: docker compose config\n", " - name: Validate Docker Compose config\n run: docker compose config\n - name: Publish image\n run: |\n docker compose config\n docker push registry.example.com/toolctrl/api:latest\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: Docker smoke job "docker-smoke" contains forbidden publish command: run: docker compose config\ndocker push registry\.example\.com\/toolctrl\/api:latest/); }); it("does not treat Docker publish commands outside Docker smoke as Docker smoke publish failures", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Build API\n run: pnpm --filter @toolctrl/api build\n", " - name: Build API\n run: pnpm --filter @toolctrl/api build\n - name: Misplaced publish\n run: docker push registry.example.com/toolctrl/api:latest\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.doesNotMatch(result.failures.join("\n"), /Docker smoke job "docker-smoke" contains forbidden publish command/); }); it("fails when a forbidden Playwright command is present", () => { const rootDir = createFixture(`${VALID_WORKFLOW} - name: E2E\n run: pnpm exec playwright test\n`); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: forbidden E2E\/browser command found: run: pnpm exec playwright test/); }); it("fails when a forbidden Playwright command is hidden in a multiline run block", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Build\n run: pnpm run build:ci\n", " - name: Build\n run: |\n pnpm run build:ci\n pnpm exec playwright test\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: forbidden E2E\/browser command found: run: pnpm run build:ci\npnpm exec playwright test/); }); it("fails when a forbidden browser command is present in a service job", () => { const rootDir = createFixture( VALID_WORKFLOW.replace( " - name: Build API\n run: pnpm --filter @toolctrl/api build\n", " - name: Build API\n run: pnpm --filter @toolctrl/api build\n - name: Browser smoke\n run: pnpm --filter @toolctrl/api exec browser test\n", ), ); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: forbidden E2E\/browser command found: run: pnpm --filter @toolctrl\/api exec browser test/); }); it("fails when the internal host override uses the reserved GITEA_ prefix", () => { const rootDir = createFixture(`env:\n GITEA_HOST_IP: \${{ vars.GITEA_HOST_IP }}\n${VALID_WORKFLOW}`); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /GITEA_HOST_IP is forbidden because Gitea reserves the GITEA_ variable prefix/); }); it("fails when the internal host override is written without token validation", () => { const rootDir = createFixture(`${VALID_WORKFLOW}\n host-smoke:\n runs-on: toolctrl-ci\n steps:\n - name: Resolve internal host\n run: |\n if [ -n \"\${TOOLCTRL_GITEA_HOST_IP:-}\" ]; then\n printf '%s git.tool-ctrl.de\\n' \"$TOOLCTRL_GITEA_HOST_IP\" >> /etc/hosts\n fi\n`); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /TOOLCTRL_GITEA_HOST_IP must be validated before writing to \/etc\/hosts/); }); it("fails when web source maps are not uploaded for the Sentry release", () => { const rootDir = 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 result = verifyGiteaCiWorkflow(rootDir); const failures = result.failures.join("\n"); assert.match(failures, /missing web source-map validation before Sentry artifact upload/); assert.match(failures, /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, ".gitea/workflows/_sentry-release.yml", "name: Sentry Release\n"); const result = verifyGiteaCiWorkflow(rootDir); assert.match(result.failures.join("\n"), /\.gitea\/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-gitea-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 Gitea CI workflow verification/); assert.match(error.stderr, /FAIL \.gitea\/workflows\/ci\.yml: missing required Gitea CI workflow file/); return true; }, ); }); });