Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

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