1601 lines
75 KiB
JavaScript
1601 lines
75 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:
|
|
verify-1:
|
|
runs-on: toolctrl-ci
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Enable Corepack
|
|
run: corepack enable
|
|
- name: Pin pnpm
|
|
run: corepack prepare pnpm@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- 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 production dependency licenses
|
|
run: pnpm run verify:licenses
|
|
- name: Audit production dependencies
|
|
run: pnpm audit --prod --audit-level high
|
|
- 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 UI mock static baseline
|
|
run: pnpm run verify:ui-visual
|
|
- name: Test UI hot-swap
|
|
run: pnpm run test:hot-swap
|
|
verify-2:
|
|
runs-on: toolctrl-ci
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Enable Corepack
|
|
run: corepack enable
|
|
- name: Pin pnpm
|
|
run: corepack prepare pnpm@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- 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 removed modules
|
|
run: pnpm run verify:removed-modules
|
|
- name: Verify accessibility coverage
|
|
run: pnpm run verify:a11y-coverage
|
|
- name: Test accessibility smoke
|
|
run: pnpm run test:a11y
|
|
- name: Verify docs links
|
|
run: pnpm run verify:docs-links
|
|
typecheck-test:
|
|
runs-on: toolctrl-ci
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Enable Corepack
|
|
run: corepack enable
|
|
- name: Pin pnpm
|
|
run: corepack prepare pnpm@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Run parallel quality groups
|
|
run: bash scripts/ci/run-quality-groups.sh
|
|
build:
|
|
runs-on: toolctrl-ci
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Enable Corepack
|
|
run: corepack enable
|
|
- name: Pin pnpm
|
|
run: corepack prepare pnpm@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Verify Feature Manifest core without plugins
|
|
run: pnpm run verify:core-build-without-plugins-smoke
|
|
- name: Build
|
|
run: pnpm run build:ci
|
|
- name: Verify web runtime dependency closure
|
|
run: pnpm run verify:runtime-closures -- --runtime=web
|
|
- name: Verify web Sentry sourcemaps
|
|
run: test -n "$(find apps/web/build-sourcemaps/core -type f -name '*.map' -print -quit)" && test -n "$(find apps/web/build-sourcemaps/portal -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-sourcemaps
|
|
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@11.12.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 @hivegrid/api typecheck
|
|
- name: Test API
|
|
run: pnpm exec vitest run apps/api/src
|
|
- name: Build API
|
|
run: pnpm --filter @hivegrid/api build
|
|
- name: Verify API runtime dependency closure
|
|
run: pnpm run verify:runtime-closures -- --runtime=api
|
|
mcp:
|
|
runs-on: toolctrl-ci
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Enable Corepack
|
|
run: corepack enable
|
|
- name: Pin pnpm
|
|
run: corepack prepare pnpm@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Typecheck MCP
|
|
run: pnpm --filter @hivegrid/mcp typecheck
|
|
- name: Test MCP
|
|
run: pnpm --filter @hivegrid/mcp test
|
|
- name: Build MCP
|
|
run: pnpm --filter @hivegrid/mcp build
|
|
- name: Verify MCP runtime dependency closure
|
|
run: pnpm run verify:runtime-closures -- --runtime=mcp
|
|
hocuspocus:
|
|
runs-on: toolctrl-ci
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Enable Corepack
|
|
run: corepack enable
|
|
- name: Pin pnpm
|
|
run: corepack prepare pnpm@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Typecheck Hocuspocus
|
|
run: pnpm --filter @hivegrid/hocuspocus typecheck
|
|
- name: Test Hocuspocus
|
|
run: pnpm --filter @hivegrid/hocuspocus test
|
|
- name: Build Hocuspocus
|
|
run: pnpm --filter @hivegrid/hocuspocus build
|
|
- name: Verify Hocuspocus runtime dependency closure
|
|
run: pnpm run verify:runtime-closures -- --runtime=hocuspocus
|
|
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@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Typecheck Worker
|
|
run: pnpm --filter @hivegrid/worker typecheck
|
|
- name: Test Worker
|
|
run: pnpm --filter @hivegrid/worker exec vitest run
|
|
- name: Verify workflow outbox native readiness
|
|
run: pnpm run verify:workflow-outbox-native
|
|
- name: Build Worker
|
|
run: pnpm --filter @hivegrid/worker build
|
|
- name: Verify worker runtime dependency closure
|
|
run: pnpm run verify:runtime-closures -- --runtime=worker
|
|
- name: Health Check Worker
|
|
run: pnpm --filter @hivegrid/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@11.12.0 --activate
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
- name: Generate database client
|
|
run: pnpm run db:generate
|
|
- name: Verify migrations schema parity
|
|
run: pnpm run verify:migrations-parity
|
|
- name: Verify Prisma baseline equivalence
|
|
run: pnpm run verify:prisma-baseline-equivalence
|
|
- name: Verify tenant policy contract
|
|
run: pnpm run verify:tenant-policy-contract
|
|
- name: Verify RLS release readiness
|
|
run: pnpm run verify:rls-release-readiness
|
|
- name: Verify RLS against fresh PostgreSQL
|
|
run: pnpm run verify:rls-live
|
|
env:
|
|
HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY: "1"
|
|
- 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
|
|
timeout-minutes: 45
|
|
needs: [verify-1, verify-2, typecheck-test, build, api, worker, database]
|
|
env:
|
|
DOCKER_BUILDKIT: "1"
|
|
DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS: "90"
|
|
DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS: "180"
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
- name: Build Docker smoke targets
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
(
|
|
set -euo pipefail
|
|
docker build --target web --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --build-arg VITE_HOCUSPOCUS_URL=wss://collab.invalid --tag hivegrid-web:smoke .
|
|
docker build --target api --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-api:smoke .
|
|
docker build --target mcp --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-mcp:smoke .
|
|
docker build --target worker --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-worker:smoke .
|
|
docker build --target portal-domain-controller --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-portal-domain-controller:smoke .
|
|
) &
|
|
root_targets_pid=$!
|
|
docker build --file apps/hocuspocus/Dockerfile --target hocuspocus --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-hocuspocus:smoke . &
|
|
hocuspocus_pid=$!
|
|
status=0
|
|
wait "$root_targets_pid" || status=$?
|
|
wait "$hocuspocus_pid" || status=$?
|
|
exit "$status"
|
|
- name: Validate web Docker artifacts
|
|
run: docker run --rm --entrypoint /bin/sh hivegrid-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/apps/web/build-portal/server/index.js && test -d /myapp/apps/web/build-portal/client && test -f /myapp/build/server-runtime/server.js'
|
|
- name: Validate web Docker runtime
|
|
run: bash scripts/docker-web-runtime-smoke.sh hivegrid-web:smoke
|
|
- name: Validate API Docker runtime
|
|
run: bash scripts/docker-api-runtime-smoke.sh hivegrid-api:smoke
|
|
env:
|
|
DOCKER_API_SMOKE_TRANSPORT: exec
|
|
- name: Validate MCP Docker runtime
|
|
run: bash scripts/docker-mcp-runtime-smoke.sh hivegrid-mcp:smoke
|
|
- name: Validate worker Docker runtime
|
|
run: bash scripts/docker-worker-runtime-smoke.sh hivegrid-worker:smoke
|
|
- name: Validate Hocuspocus Docker runtime
|
|
run: bash scripts/docker-hocuspocus-runtime-smoke.sh hivegrid-hocuspocus:smoke
|
|
- name: Validate Docker Compose config
|
|
run: docker compose config
|
|
`;
|
|
|
|
const ISOLATED_DOCKER_WORKFLOW = fs.readFileSync(path.resolve(".gitea/workflows/ci.yml"), "utf8");
|
|
const ISOLATED_DOCKER_SUITE = fs.readFileSync(path.resolve("scripts/ci/docker-smoke-suite.sh"), "utf8");
|
|
const QUALITY_GROUP_RUNNER = fs.readFileSync(path.resolve("scripts/ci/run-quality-groups.sh"), "utf8");
|
|
|
|
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);
|
|
writeFile(rootDir, "scripts/ci/run-quality-groups.sh", QUALITY_GROUP_RUNNER);
|
|
}
|
|
|
|
return rootDir;
|
|
}
|
|
|
|
function createIsolatedDockerFixture({ workflow = ISOLATED_DOCKER_WORKFLOW, suite = ISOLATED_DOCKER_SUITE } = {}) {
|
|
const rootDir = createFixture(workflow);
|
|
if (suite !== null) {
|
|
writeFile(rootDir, "scripts/ci/docker-smoke-suite.sh", suite);
|
|
}
|
|
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: quality job "verify-1" pnpm@11.12.0 is pinned'));
|
|
assert.ok(result.passes.some((pass) => pass === '.gitea/workflows/ci.yml: quality job "verify-2" pnpm@11.12.0 is pinned'));
|
|
assert.ok(result.passes.some((pass) => pass === '.gitea/workflows/ci.yml: quality job "typecheck-test" pnpm@11.12.0 is pinned'));
|
|
assert.ok(result.passes.some((pass) => pass === '.gitea/workflows/ci.yml: quality job "build" pnpm@11.12.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: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: 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 @hivegrid/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 @hivegrid/worker build"')
|
|
);
|
|
assert.ok(
|
|
result.passes.some((pass) => pass === '.gitea/workflows/ci.yml: service job "worker" includes scoped command "pnpm --filter @hivegrid/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" does not run Corepack setup'));
|
|
assert.ok(result.passes.some((pass) => pass === '.gitea/workflows/ci.yml: Docker smoke job "docker-smoke" does not create an unused Buildx builder'));
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) => pass === '.gitea/workflows/ci.yml: Docker smoke job "docker-smoke" enables BuildKit explicitly with DOCKER_BUILDKIT=1'
|
|
)
|
|
);
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) => pass === '.gitea/workflows/ci.yml: Docker smoke job "docker-smoke" allows loaded-runner commands up to 90 seconds'
|
|
)
|
|
);
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) => pass === '.gitea/workflows/ci.yml: Docker smoke job "docker-smoke" allows loaded-runner artifact checks up to 180 seconds'
|
|
)
|
|
);
|
|
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 web build exercises the public Hocuspocus URL build arg"));
|
|
assert.ok(
|
|
result.passes.some(
|
|
(pass) => pass === '.gitea/workflows/ci.yml: Docker smoke job "docker-smoke" serializes root Docker targets while Hocuspocus builds 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: 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: Build Docker smoke targets\n",
|
|
" - name: Enable Corepack\n run: corepack enable\n - name: Pin pnpm\n run: corepack prepare pnpm@11.12.0 --activate\n - name: Install dependencies\n run: pnpm install --frozen-lockfile\n - name: Build Docker smoke targets\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@11\.12\.0"/);
|
|
assert.match(failures, /Docker smoke job "docker-smoke" must not run "pnpm install --frozen-lockfile"/);
|
|
});
|
|
|
|
it("fails when Docker smoke creates an unused Buildx builder", () => {
|
|
const workflow = VALID_WORKFLOW.replace(
|
|
" - name: Build Docker smoke targets\n",
|
|
" - name: Set up Docker Buildx\n uses: docker/setup-buildx-action@v3\n - name: Build Docker smoke targets\n"
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(createFixture(workflow));
|
|
|
|
assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" must not use docker\/setup-buildx-action/);
|
|
});
|
|
|
|
it("fails when Docker smoke does not set BuildKit to one", () => {
|
|
const workflow = VALID_WORKFLOW.replace(' DOCKER_BUILDKIT: "1"\n', ' DOCKER_BUILDKIT: "0"\n');
|
|
|
|
const result = verifyGiteaCiWorkflow(createFixture(workflow));
|
|
|
|
assert.match(result.failures.join("\n"), /Docker smoke job "docker-smoke" must set job-level DOCKER_BUILDKIT: "1"/);
|
|
});
|
|
|
|
it("fails when Docker smoke keeps the loaded-runner command timeout at 30 seconds", () => {
|
|
const workflow = VALID_WORKFLOW.replace(
|
|
' DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS: "90"\n',
|
|
' DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS: "30"\n'
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(createFixture(workflow));
|
|
|
|
assert.match(
|
|
result.failures.join("\n"),
|
|
/Docker smoke job "docker-smoke" must set job-level DOCKER_SMOKE_COMMAND_TIMEOUT_SECONDS: "90"/
|
|
);
|
|
});
|
|
|
|
it("fails when Docker smoke keeps the artifact timeout at 60 seconds", () => {
|
|
const workflow = VALID_WORKFLOW.replace(
|
|
' DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS: "180"\n',
|
|
' DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS: "60"\n'
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(createFixture(workflow));
|
|
|
|
assert.match(
|
|
result.failures.join("\n"),
|
|
/Docker smoke job "docker-smoke" must set job-level DOCKER_SMOKE_ARTIFACT_TIMEOUT_SECONDS: "180"/
|
|
);
|
|
});
|
|
|
|
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: bash scripts/ci/run-quality-groups.sh\n", ""));
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: missing required command "bash scripts\/ci\/run-quality-groups\.sh"/);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"license",
|
|
" - name: Verify production dependency licenses\n run: pnpm run verify:licenses\n",
|
|
/missing required command "pnpm run verify:licenses"/,
|
|
],
|
|
[
|
|
"audit",
|
|
" - name: Audit production dependencies\n run: pnpm audit --prod --audit-level high\n",
|
|
/missing required command "pnpm audit --prod --audit-level high"/,
|
|
],
|
|
])("fails when the global %s supply-chain gate is missing", (_label, step, expected) => {
|
|
const result = verifyGiteaCiWorkflow(createFixture(VALID_WORKFLOW.replace(step, "")));
|
|
|
|
assert.match(result.failures.join("\n"), expected);
|
|
});
|
|
|
|
it("fails when pnpm setup is missing from one split quality job", () => {
|
|
// String.replace removes only the first occurrence, i.e. the "verify-1" job's setup.
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(" - name: Enable Corepack\n run: corepack enable\n", "")
|
|
.replace(" - name: Pin pnpm\n run: corepack prepare pnpm@11.12.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: quality job "verify-1" missing Corepack setup/);
|
|
assert.match(failures, /\.gitea\/workflows\/ci\.yml: quality job "verify-1" pnpm must be pinned to pnpm@11\.12\.0/);
|
|
assert.match(failures, /\.gitea\/workflows\/ci\.yml: quality job "verify-1" 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: bash scripts/ci/run-quality-groups.sh\n",
|
|
" run: bash scripts/ci/run-quality-groups.sh || true\n"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /command "bash scripts\/ci\/run-quality-groups\.sh" 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: bash scripts/ci/run-quality-groups.sh\n",
|
|
" run: |\n set +e\n bash scripts/ci/run-quality-groups.sh\n"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /command "bash scripts\/ci\/run-quality-groups\.sh" 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 @hivegrid/api typecheck\n",
|
|
" - name: Typecheck API\n run: |\n pnpm --filter @hivegrid/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 @hivegrid\/api typecheck" must not use shell success masking/
|
|
);
|
|
});
|
|
|
|
it("accepts block-list needs syntax for dependent jobs", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" needs: [verify-1, verify-2, typecheck-test, build, api, worker, database]\n",
|
|
" needs:\n - verify-1\n - verify-2\n - typecheck-test\n - build\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: Run parallel quality groups\n run: bash scripts/ci/run-quality-groups.sh\n",
|
|
" - name: Run parallel quality groups\n continue-on-error: true\n run: bash scripts/ci/run-quality-groups.sh\n"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /command "bash scripts\/ci\/run-quality-groups\.sh" must not use continue-on-error or always\(\)/);
|
|
});
|
|
|
|
it("fails when the tenant policy gate is disabled with a false condition", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" - name: Verify tenant policy contract\n run: pnpm run verify:tenant-policy-contract\n",
|
|
" - name: Verify tenant policy contract\n if: ${{ false }}\n run: pnpm run verify:tenant-policy-contract\n"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /command "pnpm run verify:tenant-policy-contract" must not use conditional execution/);
|
|
});
|
|
|
|
it("fails when the live RLS gate is disabled with a false condition", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" - name: Verify RLS against fresh PostgreSQL\n run: pnpm run verify:rls-live\n env:\n HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY: \"1\"\n",
|
|
" - name: Verify RLS against fresh PostgreSQL\n if: false\n run: pnpm run verify:rls-live\n env:\n HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY: \"1\"\n"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /command "pnpm run verify:rls-live" must not use conditional execution/);
|
|
});
|
|
|
|
it("fails when the live RLS gate permits the feature-branch overlay", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" env:\n HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY: \"1\"\n",
|
|
"",
|
|
),
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /live RLS gate must set HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY: "1"/);
|
|
});
|
|
|
|
it("fails when the database job drops the RLS release-readiness gate", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(" - name: Verify RLS release readiness\n run: pnpm run verify:rls-release-readiness\n", "")
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /database job "database" missing command "pnpm run verify:rls-release-readiness"/);
|
|
});
|
|
|
|
it("fails when a required quality gate adds skip flags or suffixes", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" run: bash scripts/ci/run-quality-groups.sh\n",
|
|
" run: bash scripts/ci/run-quality-groups.sh --skip-tests\n"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /missing required command "bash scripts\/ci\/run-quality-groups\.sh"/);
|
|
});
|
|
|
|
it("fails when worker tests are narrowed to explicit files", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" run: pnpm --filter @hivegrid/worker exec vitest run\n",
|
|
" run: pnpm --filter @hivegrid/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 @hivegrid\/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 @hivegrid/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 @hivegrid\/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 @hivegrid/api typecheck\n",
|
|
" - name: Typecheck API\n run: |\n # pnpm --filter @hivegrid/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 @hivegrid\/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 a verify-2 gate runs out of its canonical order", () => {
|
|
// tenant-isolation-report is canonically the first verify-2 gate; moving it after the
|
|
// sidebar/i18n gates pushes those earlier gates out of order within the owning job.
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" - 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 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"
|
|
)
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /\.gitea\/workflows\/ci\.yml: command "pnpm run verify:sidebar-routes" 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();
|
|
writeFile(
|
|
rootDir,
|
|
"scripts/ci/run-quality-groups.sh",
|
|
QUALITY_GROUP_RUNNER.replace(
|
|
" pnpm --filter @hivegrid/portal-domain-controller typecheck\n",
|
|
" pnpm lint\n pnpm --filter @hivegrid/portal-domain-controller typecheck\n"
|
|
).replace(" pnpm lint\n}\n\nrun_foundation_packages", "}\n\nrun_foundation_packages")
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /typecheck-lint group must run controller typecheck, web typegen\/typecheck, and lint in order/);
|
|
});
|
|
|
|
it("fails when test readers start before Prisma generation completes", () => {
|
|
const rootDir = createFixture();
|
|
const unsafeRunner = QUALITY_GROUP_RUNNER.replace("pnpm run db:generate\n", "").replace(
|
|
"QUALITY_FAILED=0\n",
|
|
"pnpm run db:generate\nQUALITY_FAILED=0\n"
|
|
);
|
|
writeFile(rootDir, "scripts/ci/run-quality-groups.sh", unsafeRunner);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must generate the Prisma client before starting parallel quality groups/);
|
|
});
|
|
|
|
it.each([
|
|
["foundation Vitest pool", "pnpm run test:foundation --maxWorkers=2", "pnpm run test:foundation"],
|
|
[
|
|
"package workspace fan-out",
|
|
"PNPM_CONFIG_WORKSPACE_CONCURRENCY=2 pnpm run test:packages --maxWorkers=1",
|
|
"pnpm run test:packages --maxWorkers=1",
|
|
],
|
|
["package Vitest pool", "pnpm run test:packages --maxWorkers=1", "pnpm run test:packages"],
|
|
])("fails when the %s cap is removed", (_name, boundedCommand, unsafeCommand) => {
|
|
const rootDir = createFixture();
|
|
writeFile(rootDir, "scripts/ci/run-quality-groups.sh", QUALITY_GROUP_RUNNER.replace(boundedCommand, unsafeCommand));
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /foundation\/package tests must keep the exact bounded worker and workspace-concurrency commands/);
|
|
});
|
|
|
|
it("fails when the core Vitest pool cap is removed", () => {
|
|
const rootDir = createFixture();
|
|
writeFile(rootDir, "scripts/ci/run-quality-groups.sh", QUALITY_GROUP_RUNNER.replace("pnpm run test --maxWorkers=4", "pnpm run test"));
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /core-tests group must run pnpm run test --maxWorkers=4/);
|
|
});
|
|
|
|
it("fails when one quality process group is omitted", () => {
|
|
const rootDir = createFixture();
|
|
writeFile(
|
|
rootDir,
|
|
"scripts/ci/run-quality-groups.sh",
|
|
QUALITY_GROUP_RUNNER.replace('start_group "core-tests" run_core_tests\n', "")
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must start exactly three named background groups and capture every PID/);
|
|
});
|
|
|
|
it("fails when quality process results are not all waited", () => {
|
|
const rootDir = createFixture();
|
|
writeFile(rootDir, "scripts/ci/run-quality-groups.sh", QUALITY_GROUP_RUNNER.replace('if wait "$pid"; then', "if true; then"));
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must wait every named PID, report its status, and propagate aggregate failure/);
|
|
});
|
|
|
|
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 @hivegrid/worker build\n",
|
|
" - name: Build Worker\n run: pnpm --filter @hivegrid/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 Hocuspocus skips its post-build runtime closure gate", () => {
|
|
const result = verifyGiteaCiWorkflow(
|
|
createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" - name: Verify Hocuspocus runtime dependency closure\n run: pnpm run verify:runtime-closures -- --runtime=hocuspocus\n",
|
|
""
|
|
)
|
|
)
|
|
);
|
|
|
|
assert.match(result.failures.join("\n"), /service job "hocuspocus" missing scoped command "pnpm run verify:runtime-closures -- --runtime=hocuspocus"/);
|
|
});
|
|
|
|
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 drops the tenant policy contract gate", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(" - name: Verify tenant policy contract\n run: pnpm run verify:tenant-policy-contract\n", "")
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /database job "database" missing command "pnpm run verify:tenant-policy-contract"/);
|
|
});
|
|
|
|
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 @hivegrid/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 @hivegrid\/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 @hivegrid/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 @hivegrid\/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 a Docker target build is missing from the Docker smoke job", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" - name: Build API\n run: pnpm --filter @hivegrid/api build\n",
|
|
" - name: Build API\n run: pnpm --filter @hivegrid/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=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-api:smoke .\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=0123456789abcdef0123456789abcdef01234567 --build-arg VITE_HOCUSPOCUS_URL=wss://collab.invalid --tag hivegrid-web:smoke .",
|
|
"docker build --target web --build-arg VITE_HOCUSPOCUS_URL=wss://collab.invalid --tag hivegrid-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=0123456789abcdef0123456789abcdef01234567/
|
|
);
|
|
});
|
|
|
|
it("fails when the Docker smoke web build does not exercise the Hocuspocus build arg", () => {
|
|
const rootDir = createFixture(VALID_WORKFLOW.replace(" --build-arg VITE_HOCUSPOCUS_URL=wss://collab.invalid", ""));
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /web build must exercise --build-arg VITE_HOCUSPOCUS_URL=wss:\/\/collab\.invalid/);
|
|
});
|
|
|
|
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/hivegrid/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\/hivegrid\/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 hivegrid-web:smoke -lc 'test -f /myapp/apps/web/build/server/index.js && test -d /myapp/apps/web/build/client && test -f /myapp/apps/web/build-portal/server/index.js && test -d /myapp/apps/web/build-portal/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 hivegrid-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 hivegrid-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 hivegrid-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 hivegrid-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 hivegrid-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 hivegrid-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 hivegrid-worker:smoke"/
|
|
);
|
|
});
|
|
|
|
it("fails when the Docker smoke job omits Hocuspocus runtime validation", () => {
|
|
const result = verifyGiteaCiWorkflow(
|
|
createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" - name: Validate Hocuspocus Docker runtime\n run: bash scripts/docker-hocuspocus-runtime-smoke.sh hivegrid-hocuspocus:smoke\n",
|
|
""
|
|
)
|
|
)
|
|
);
|
|
|
|
assert.match(result.failures.join("\n"), /missing command "bash scripts\/docker-hocuspocus-runtime-smoke\.sh hivegrid-hocuspocus:smoke"/);
|
|
});
|
|
|
|
it("fails when the Hocuspocus smoke image uses the root Dockerfile", () => {
|
|
const result = verifyGiteaCiWorkflow(
|
|
createFixture(VALID_WORKFLOW.replace("docker build --file apps/hocuspocus/Dockerfile --target hocuspocus", "docker build --target hocuspocus"))
|
|
);
|
|
|
|
assert.match(result.failures.join("\n"), /must build Hocuspocus from apps\/hocuspocus\/Dockerfile/);
|
|
});
|
|
|
|
it("fails when a root Docker smoke target is parallelized against the shared cache", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" docker build --target api --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-api:smoke .\n",
|
|
" docker build --target api --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-api:smoke . &\n"
|
|
)
|
|
);
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must serialize root Docker targets and propagate failures while Hocuspocus builds in parallel/);
|
|
assert.doesNotMatch(result.failures.join("\n"), /missing Docker build for target "api"/);
|
|
});
|
|
|
|
it("fails when Hocuspocus no longer builds alongside the serialized root targets", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" docker build --file apps/hocuspocus/Dockerfile --target hocuspocus --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-hocuspocus:smoke . &\n",
|
|
" docker build --file apps/hocuspocus/Dockerfile --target hocuspocus --build-arg APP_REVISION=0123456789abcdef0123456789abcdef01234567 --tag hivegrid-hocuspocus:smoke .\n"
|
|
)
|
|
);
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must serialize root Docker targets and propagate failures while Hocuspocus builds in parallel/);
|
|
assert.doesNotMatch(result.failures.join("\n"), /missing Docker build for target "hocuspocus"/);
|
|
});
|
|
|
|
it("fails when the grouped Docker smoke build does not propagate a child failure", () => {
|
|
const rootDir = createFixture(VALID_WORKFLOW.replace(' exit "$status"\n', ""));
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must serialize root Docker targets and propagate failures while Hocuspocus builds in parallel/);
|
|
});
|
|
|
|
it("fails when the grouped Docker smoke build captures the root PID too late", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" root_targets_pid=$!\n docker build --file apps/hocuspocus/Dockerfile",
|
|
" docker build --file apps/hocuspocus/Dockerfile"
|
|
).replace(" --tag hivegrid-hocuspocus:smoke . &\n hocuspocus_pid=$!", " --tag hivegrid-hocuspocus:smoke . &\n root_targets_pid=$!\n hocuspocus_pid=$!")
|
|
);
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must serialize root Docker targets and propagate failures while Hocuspocus builds in parallel/);
|
|
});
|
|
|
|
it("fails when the grouped Docker smoke build resets status after waiting", () => {
|
|
const rootDir = createFixture(
|
|
VALID_WORKFLOW.replace(
|
|
" status=0\n wait \"$root_targets_pid\" || status=$?\n wait \"$hocuspocus_pid\" || status=$?",
|
|
" wait \"$root_targets_pid\" || status=$?\n wait \"$hocuspocus_pid\" || status=$?\n status=0"
|
|
)
|
|
);
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /must serialize root Docker targets and propagate failures while Hocuspocus builds in parallel/);
|
|
});
|
|
|
|
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/hivegrid/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\/hivegrid\/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 @hivegrid/api build\n",
|
|
" - name: Build API\n run: pnpm --filter @hivegrid/api build\n - name: Misplaced publish\n run: docker push registry.example.com/hivegrid/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 @hivegrid/api build\n",
|
|
" - name: Build API\n run: pnpm --filter @hivegrid/api build\n - name: Browser smoke\n run: pnpm --filter @hivegrid/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 @hivegrid\/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 \"\${HIVEGRID_GITEA_HOST_IP:-}\" ]; then\n printf '%s git.tool-ctrl.de\\n' \"$HIVEGRID_GITEA_HOST_IP\" >> /etc/hosts\n fi\n`
|
|
);
|
|
|
|
const result = verifyGiteaCiWorkflow(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /HIVEGRID_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-sourcemaps/core -type f -name '*.map' -print -quit)\" && test -n \"$(find apps/web/build-sourcemaps/portal -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-sourcemaps\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-sourcemaps 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("accepts the isolated, SHA-scoped and parallel-wave Docker smoke suite", () => {
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture());
|
|
|
|
assert.deepEqual(result.failures, []);
|
|
assert.ok(result.passes.includes(".gitea/workflows/ci.yml: CI concurrency cancels superseded pull request and branch runs"));
|
|
assert.ok(result.passes.includes(".gitea/workflows/ci.yml: run-scoped Docker smokes may execute concurrently across refs"));
|
|
assert.ok(result.passes.includes("scripts/ci/docker-smoke-suite.sh: all images build in parallel before the isolated runtime wave"));
|
|
assert.ok(result.passes.includes("scripts/ci/docker-smoke-suite.sh: parallel waves collect every child result and propagate failures"));
|
|
assert.ok(result.passes.includes("scripts/ci/docker-smoke-suite.sh: every build and runtime phase has a command-level timeout"));
|
|
assert.ok(
|
|
result.passes.includes(
|
|
"scripts/ci/docker-smoke-suite.sh: Compose validation uses a non-secret fixture without rendering resolved values"
|
|
)
|
|
);
|
|
});
|
|
|
|
it("fails when the isolated Docker smoke suite file is missing", () => {
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite: null }));
|
|
|
|
assert.match(result.failures.join("\n"), /missing required isolated Docker smoke suite/);
|
|
});
|
|
|
|
it("fails when Docker smoke regains a cross-ref serialization group", () => {
|
|
const workflow = ISOLATED_DOCKER_WORKFLOW.replace(
|
|
" timeout-minutes: 45\n",
|
|
" timeout-minutes: 45\n concurrency:\n group: hivegrid-cloud-docker-smoke\n cancel-in-progress: false\n"
|
|
);
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ workflow }));
|
|
|
|
assert.match(result.failures.join("\n"), /must not use job-level concurrency/);
|
|
});
|
|
|
|
it("fails when workflow concurrency keeps superseded Docker smoke runs queued", () => {
|
|
const workflow = ISOLATED_DOCKER_WORKFLOW.replace(" cancel-in-progress: true\n", " cancel-in-progress: false\n");
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ workflow }));
|
|
|
|
assert.match(result.failures.join("\n"), /must define concurrency by workflow plus PR\/ref with cancel-in-progress: true/);
|
|
});
|
|
|
|
it("fails when Docker smoke stops scoping resources by run attempt", () => {
|
|
const workflow = ISOLATED_DOCKER_WORKFLOW.replace(
|
|
" DOCKER_SMOKE_RUN_ID: ${{ gitea.run_id }}-${{ gitea.run_attempt }}\n",
|
|
" DOCKER_SMOKE_RUN_ID: shared\n"
|
|
);
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ workflow }));
|
|
|
|
assert.match(result.failures.join("\n"), /must set DOCKER_SMOKE_RUN_ID from gitea.run_id and gitea.run_attempt/);
|
|
});
|
|
|
|
it("fails when Docker smoke renders resolved Compose credentials", () => {
|
|
const suite = ISOLATED_DOCKER_SUITE.replace(
|
|
"docker compose --env-file .env.test config --quiet",
|
|
"docker compose config"
|
|
);
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
|
|
assert.match(result.failures.join("\n"), /Compose validation must use \.env\.test and --quiet/);
|
|
});
|
|
|
|
it("fails when a global Docker smoke image tag is reintroduced", () => {
|
|
const suite = ISOLATED_DOCKER_SUITE.replace('WEB_IMAGE="hivegrid-web:smoke-${DOCKER_SMOKE_RUN_ID}"', 'WEB_IMAGE="hivegrid-web:smoke"');
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
const failures = result.failures.join("\n");
|
|
|
|
assert.match(failures, /web image tag must include DOCKER_SMOKE_RUN_ID/);
|
|
assert.match(failures, /global :smoke image tags are forbidden/);
|
|
});
|
|
|
|
it("fails when artifact validation loses its run-scoped one-shot name", () => {
|
|
const suite = ISOLATED_DOCKER_SUITE.replace('docker run --rm --name "$WEB_ARTIFACT_CONTAINER_NAME"', "docker run --rm");
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
|
|
assert.match(result.failures.join("\n"), /web artifact validation must use a run-scoped named container/);
|
|
});
|
|
|
|
it("fails when a fixed fake Docker revision is reintroduced", () => {
|
|
const suite = `${ISOLATED_DOCKER_SUITE}\n# 0123456789abcdef0123456789abcdef01234567\n`;
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
|
|
assert.match(result.failures.join("\n"), /fixed fake image revisions are forbidden/);
|
|
});
|
|
|
|
it("fails when the runtime wave starts before the build barrier", () => {
|
|
const suite = ISOLATED_DOCKER_SUITE.replace(
|
|
" run_parallel_wave builds \\\n build_web build_worker build_api build_mcp build_hocuspocus build_portal_controller\n",
|
|
" run_parallel_wave runtimes \\\n validate_web validate_worker validate_api validate_mcp validate_hocuspocus validate_portal_controller\n"
|
|
);
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
|
|
assert.match(result.failures.join("\n"), /must complete the six-target build wave before the six-target runtime wave/);
|
|
});
|
|
|
|
it("fails when a runtime phase loses its command timeout", () => {
|
|
const suite = ISOLATED_DOCKER_SUITE.replace(
|
|
' docker_smoke_run_with_grace "runtime:worker" "$DOCKER_SMOKE_SERVICE_RUNTIME_TIMEOUT_SECONDS" "$DOCKER_SMOKE_RUNTIME_GRACE_SECONDS" \\\n',
|
|
""
|
|
);
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
|
|
assert.match(result.failures.join("\n"), /missing timeout stages: docker_smoke_run_with_grace "runtime:worker"/);
|
|
});
|
|
|
|
it("fails when parallel-wave child results are not awaited", () => {
|
|
const suite = ISOLATED_DOCKER_SUITE.replace(' if wait "${pids[$index]}"; then\n', " if true; then\n");
|
|
const result = verifyGiteaCiWorkflow(createIsolatedDockerFixture({ suite }));
|
|
|
|
assert.match(result.failures.join("\n"), /parallel waves must track, await, and propagate every child result/);
|
|
});
|
|
|
|
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;
|
|
}
|
|
);
|
|
});
|
|
});
|