Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-gitea-ci-workflow.test.mjs
T

1000 lines
52 KiB
JavaScript

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;
},
);
});
});