429 lines
15 KiB
JavaScript
429 lines
15 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
const repoRoot = path.resolve(__dirname, "..", "..", "..");
|
|
|
|
function readText(relativePath) {
|
|
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8").replace(/\r\n/g, "\n");
|
|
}
|
|
|
|
const packageJson = JSON.parse(readText("package.json"));
|
|
const generatedFiles = JSON.parse(readText("tools/config/generated-files.json"));
|
|
const ciWorkflow = readText(".github/workflows/ci.yml");
|
|
const offlineCatalogBuilder = readText("tools/scripts/build-aas-v1-offline-catalog.js");
|
|
const canonicalMergeScript = readText("tools/scripts/merge_canonical_sync_pr.cjs");
|
|
const publishWorkflow = readText(".github/workflows/publish-npm.yml");
|
|
const releaseWorkflowScript = readText("tools/scripts/release_workflow.js");
|
|
const hygieneWorkflowPath = path.join(repoRoot, ".github", "workflows", "repo-hygiene.yml");
|
|
|
|
const prepareReleaseBlock = releaseWorkflowScript.slice(
|
|
releaseWorkflowScript.indexOf("function prepareRelease"),
|
|
releaseWorkflowScript.indexOf("function publishRelease"),
|
|
);
|
|
|
|
assert.ok(
|
|
packageJson.scripts["sync:release-state"],
|
|
"package.json should expose a deterministic release-state sync command",
|
|
);
|
|
assert.ok(
|
|
packageJson.scripts["check:warning-budget"],
|
|
"package.json should expose a warning-budget guardrail command",
|
|
);
|
|
assert.ok(
|
|
packageJson.scripts["check:readme-credits"],
|
|
"package.json should expose a README credit validation command",
|
|
);
|
|
assert.ok(
|
|
packageJson.scripts["merge:batch"],
|
|
"package.json should expose a maintainer merge-batch command",
|
|
);
|
|
assert.ok(
|
|
packageJson.scripts["audit:maintainer"],
|
|
"package.json should expose a maintainer audit command",
|
|
);
|
|
assert.ok(
|
|
packageJson.scripts["sync:web-assets"],
|
|
"package.json should expose a web-asset sync command for tracked web artifacts",
|
|
);
|
|
assert.match(
|
|
packageJson.scripts["sync:release-state"],
|
|
/sync:web-assets/,
|
|
"sync:release-state should refresh tracked web assets before auditing release drift",
|
|
);
|
|
assert.match(
|
|
packageJson.scripts["sync:release-state"],
|
|
/check:warning-budget/,
|
|
"sync:release-state should enforce the frozen validation warning budget",
|
|
);
|
|
assert.match(
|
|
packageJson.scripts["sync:release-state"],
|
|
/chain/,
|
|
"sync:release-state should rebuild canonical release and plugin state",
|
|
);
|
|
assert.match(packageJson.scripts.chain, /plugin-compat:sync/);
|
|
assert.match(packageJson.scripts.chain, /bundles:sync/);
|
|
const releaseSuiteBlock = releaseWorkflowScript.slice(
|
|
releaseWorkflowScript.indexOf("function runReleaseSuite"),
|
|
releaseWorkflowScript.indexOf("function runReleasePreflight"),
|
|
);
|
|
assert.match(
|
|
releaseSuiteBlock,
|
|
/sync:release-state[\s\S]*plugin-compat:check[\s\S]*bundles:check/,
|
|
"every release suite should explicitly prove plugin compatibility and bundle alignment after regeneration",
|
|
);
|
|
assert.match(
|
|
packageJson.scripts["sync:repo-state"],
|
|
/sync:web-assets/,
|
|
"sync:repo-state should refresh tracked web assets before maintainer audits",
|
|
);
|
|
assert.match(
|
|
packageJson.scripts["sync:repo-state"],
|
|
/check:warning-budget/,
|
|
"sync:repo-state should enforce the frozen validation warning budget",
|
|
);
|
|
assert.match(
|
|
packageJson.scripts.chain,
|
|
/build:aas-v1-catalog/,
|
|
"chain should refresh the offline AAS v1 catalog after skill index generation",
|
|
);
|
|
assert.strictEqual(
|
|
packageJson.scripts["app:install"],
|
|
"cd apps/web-app && npm ci",
|
|
"app:install should use npm ci for deterministic web-app installs",
|
|
);
|
|
|
|
for (const filePath of [
|
|
"apps/web-app/public/sitemap.xml",
|
|
"apps/web-app/public/skills.json.backup",
|
|
"data/plugin-compatibility.json",
|
|
"data/aas-v1/",
|
|
".agents/plugins/",
|
|
".claude-plugin/plugin.json",
|
|
".claude-plugin/marketplace.json",
|
|
"plugins/",
|
|
]) {
|
|
assert.ok(
|
|
generatedFiles.derivedFiles.includes(filePath),
|
|
`generated-files derivedFiles should include ${filePath}`,
|
|
);
|
|
}
|
|
|
|
for (const retiredCoreAsset of [
|
|
"tools/lib/aas-v1/metadata-overrides.v1.json",
|
|
"tools/lib/aas-v1/metadata-reviews.v1.json",
|
|
"tools/lib/aas-v1/review-queue.v1.json",
|
|
]) {
|
|
assert.ok(
|
|
!generatedFiles.derivedFiles.includes(retiredCoreAsset),
|
|
`generated-files should not retain retired Core policy asset ${retiredCoreAsset}`,
|
|
);
|
|
}
|
|
|
|
const webAppGitignore = readText("apps/web-app/.gitignore");
|
|
assert.match(
|
|
webAppGitignore,
|
|
/^coverage$/m,
|
|
"web-app coverage output should be ignored so maintainer sync jobs stay clean",
|
|
);
|
|
|
|
for (const filePath of [
|
|
"README.md",
|
|
"package.json",
|
|
"apps/web-app/index.html",
|
|
"apps/web-app/public/llms.txt",
|
|
"docs/users/getting-started.md",
|
|
"docs/users/bundles.md",
|
|
"docs/users/claude-code-skills.md",
|
|
"docs/users/gemini-cli-skills.md",
|
|
"docs/users/usage.md",
|
|
"docs/users/visual-guide.md",
|
|
"docs/users/kiro-integration.md",
|
|
"docs/maintainers/repo-growth-seo.md",
|
|
"docs/maintainers/skills-update-guide.md",
|
|
"docs/integrations/jetski-cortex.md",
|
|
"docs/integrations/jetski-gemini-loader/README.md",
|
|
]) {
|
|
assert.ok(
|
|
generatedFiles.mixedFiles.includes(filePath),
|
|
`generated-files mixedFiles should include ${filePath}`,
|
|
);
|
|
}
|
|
|
|
assert.match(
|
|
ciWorkflow,
|
|
/- name: Run repo-state sync[\s\S]*?run: npm run sync:repo-state/,
|
|
"main CI should use the unified repo-state sync command",
|
|
);
|
|
assert.ok(
|
|
ciWorkflow.indexOf("- name: Install PR policy dependencies") <
|
|
ciWorkflow.indexOf("- name: Intake PR change"),
|
|
"PR policy dependencies must be installed before preflight executes",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/GH_TOKEN: \$\{\{ github\.token \}\}/,
|
|
"main CI should provide GH_TOKEN for contributor synchronization",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/main-validation-and-sync:[\s\S]*?concurrency:[\s\S]*?group: canonical-main-sync[\s\S]*?cancel-in-progress: false/,
|
|
"main validation should serialize canonical sync writers",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/pip install -r tools\/requirements\.txt/g,
|
|
"CI workflows should install Python dependencies from tools/requirements.txt",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/- name: Audit npm dependencies[\s\S]*?run: npm audit --audit-level=high/,
|
|
"CI should run npm audit at high severity",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/source-validation:[\s\S]*?- uses: actions\/checkout@[a-f0-9]{40}[\s\S]*?with:[\s\S]*?fetch-depth: 0/,
|
|
"source-validation should use an unshallowed checkout so base-branch diffs have a merge base",
|
|
);
|
|
|
|
const pagesWorkflow = readText(".github/workflows/pages.yml");
|
|
assert.match(
|
|
pagesWorkflow,
|
|
/- name: Checkout[\s\S]*?uses: actions\/checkout@[a-f0-9]{40}[\s\S]*?with:[\s\S]*?fetch-depth: 0[\s\S]*?persist-credentials: false/,
|
|
"Pages should use an unshallowed, credential-free checkout because canonical provenance validation reads git history",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/artifact-preview:[\s\S]*?actions\/checkout@[a-f0-9]{40}[\s\S]*?fetch-depth: 0[\s\S]*?persist-credentials: false/,
|
|
"artifact-preview should retain history because canonical provenance generation reads git history",
|
|
);
|
|
assert.doesNotMatch(
|
|
offlineCatalogBuilder,
|
|
/buildMetadataOverrides|metadata-overrides|review-queue/,
|
|
"offline catalog builds should not bind retired Core policy or review assets",
|
|
);
|
|
assert.match(
|
|
offlineCatalogBuilder,
|
|
/catalogSchemaVersion:\s*versions\.catalogSchemaVersion/,
|
|
"offline catalog manifests should bind the explicit catalog schema version",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/source-validation:[\s\S]*?- name: Fetch base branch[\s\S]*?run: git fetch origin "\$\{\{ github\.base_ref \|\| 'main' \}\}"/,
|
|
"source-validation should fetch the PR base branch before changed-skill README credit checks",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/- name: Verify README source credits for changed skills[\s\S]*?run: npm run check:readme-credits -- --base "origin\/\$\{\{ github\.base_ref \}\}" --head HEAD/,
|
|
"PR CI should verify README source credits for changed skills",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/source-validation:[\s\S]*?- name: Fetch base branch[\s\S]*?- name: Install npm dependencies[\s\S]*?- name: Verify README source credits for changed skills/,
|
|
"source-validation should fetch the base branch before running the changed-skill README credit check",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/main-validation-and-sync:[\s\S]*?- name: Audit npm dependencies[\s\S]*?run: npm audit --audit-level=high/,
|
|
"main validation should enforce npm audit before syncing canonical state",
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/main-validation-and-sync:[\s\S]*?continue-on-error: true/,
|
|
"main validation should not treat high-severity npm audit findings as non-blocking",
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/^ - name: Generate index$/m,
|
|
"main CI should not keep the old standalone Generate index step",
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/^ - name: Update README$/m,
|
|
"main CI should not keep the old standalone Update README step",
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/^ - name: Build catalog$/m,
|
|
"main CI should not keep the old standalone Build catalog step",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/uses: peter-evans\/create-pull-request@[a-f0-9]{40}/,
|
|
"main CI should publish canonical drift through a pinned pull-request action",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/branch: automation\/canonical-repo-state/,
|
|
"main CI should maintain one fixed canonical-sync branch",
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/gh workflow run ci\.yml --ref "\$PR_BRANCH" -f canonical_sync_pr=true/,
|
|
"canonical checks must remain associated with the pull request",
|
|
);
|
|
assert.match(
|
|
canonicalMergeScript,
|
|
/actions\/runs\/\$\{run\.id\}\/rerun/,
|
|
"main CI should restart the PR-associated workflow suppressed for a GITHUB_TOKEN-created PR",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/- name: Reproduce canonical-sync PR from main[\s\S]*?GH_TOKEN: \$\{\{ github\.token \}\}[\s\S]*?run: npm run sync:repo-state/,
|
|
"canonical reproducibility should scope a read token to contributor synchronization",
|
|
);
|
|
assert.doesNotMatch(
|
|
ciWorkflow,
|
|
/git push origin (?:HEAD|main)/,
|
|
"main CI must not push directly to protected main",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/git ls-files --others --exclude-standard/,
|
|
"main CI should fail if canonical sync leaves unmanaged untracked drift",
|
|
);
|
|
assert.match(
|
|
ciWorkflow,
|
|
/git diff --name-only/,
|
|
"main CI should fail if canonical sync leaves unmanaged tracked drift",
|
|
);
|
|
|
|
assert.ok(fs.existsSync(hygieneWorkflowPath), "repo hygiene workflow should exist");
|
|
|
|
const hygieneWorkflow = readText(".github/workflows/repo-hygiene.yml");
|
|
assert.match(hygieneWorkflow, /^on:\n workflow_dispatch:\n schedule:/m, "repo hygiene workflow should support schedule and manual runs");
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/concurrency:\n\s+group: canonical-main-sync\n\s+cancel-in-progress: false/,
|
|
"repo hygiene workflow should serialize canonical sync writers with main CI",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/GH_TOKEN: \$\{\{ github\.token \}\}/,
|
|
"repo hygiene workflow should provide GH_TOKEN for gh-based contributor sync",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/pip install -r tools\/requirements\.txt/,
|
|
"repo hygiene workflow should install Python dependencies from tools/requirements.txt",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/run: npm audit --audit-level=high/,
|
|
"repo hygiene workflow should block on high-severity npm audit findings before syncing",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/run: npm run sync:repo-state/,
|
|
"repo hygiene workflow should run the unified repo-state sync command",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/generated_files\.js --include-mixed/,
|
|
"repo hygiene workflow should resolve and stage the mixed generated files contract",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/uses: peter-evans\/create-pull-request@[a-f0-9]{40}/,
|
|
"repo hygiene should publish canonical drift through a pinned pull-request action",
|
|
);
|
|
assert.doesNotMatch(
|
|
hygieneWorkflow,
|
|
/gh workflow run ci\.yml --ref "\$PR_BRANCH" -f canonical_sync_pr=true/,
|
|
"repo hygiene should use the exact PR-associated workflow instead of a redundant dispatch",
|
|
);
|
|
assert.doesNotMatch(
|
|
hygieneWorkflow,
|
|
/git push origin (?:HEAD|main)/,
|
|
"repo hygiene must not push directly to protected main",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/git ls-files --others --exclude-standard/,
|
|
"repo hygiene workflow should fail if canonical sync leaves unmanaged untracked drift",
|
|
);
|
|
assert.match(
|
|
hygieneWorkflow,
|
|
/git diff --name-only/,
|
|
"repo hygiene workflow should fail if canonical sync leaves unmanaged tracked drift",
|
|
);
|
|
|
|
assert.match(publishWorkflow, /run: npm ci/, "npm publish workflow should install dependencies");
|
|
assert.match(
|
|
publishWorkflow,
|
|
/node-version: "22\.23\.1"/,
|
|
"npm publish workflow should use the supported Node 22 runtime",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/npm publish --tag next/,
|
|
"npm prereleases should publish to the next dist-tag",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/else[\s\S]*npm publish --tag latest/,
|
|
"stable npm releases should publish explicitly to latest",
|
|
);
|
|
assert.doesNotMatch(
|
|
publishWorkflow,
|
|
/^\s*npm publish\s*$/mu,
|
|
"npm releases should never publish without an explicit dist-tag",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/semver\.test/,
|
|
"npm releases should fail closed on an invalid package version",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/pip install -r tools\/requirements\.txt/,
|
|
"npm publish workflow should install Python dependencies from tools/requirements.txt",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/run: npm audit --audit-level=high/,
|
|
"npm publish workflow should block on high-severity npm audit findings",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/run: npm run app:install/,
|
|
"npm publish workflow should install web-app dependencies before building",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/run: npm run sync:release-state/,
|
|
"npm publish workflow should verify canonical release artifacts",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/run: git diff --exit-code/,
|
|
"npm publish workflow should fail if canonical sync would leave release drift",
|
|
);
|
|
assert.match(publishWorkflow, /run: npm run test/, "npm publish workflow should run tests before publish");
|
|
assert.match(publishWorkflow, /run: npm run app:build/, "npm publish workflow should build the app before publish");
|
|
assert.match(
|
|
releaseWorkflowScript,
|
|
/runCommand\("npm", \["run", "app:install"\], projectRoot\);[\s\S]*runCommand\("npm", \["run", "app:build"\], projectRoot\);/,
|
|
"release workflow should install web-app dependencies before building the app",
|
|
);
|
|
assert.ok(
|
|
prepareReleaseBlock.indexOf('["run", "sync:metadata", "--", "--refresh-volatile"]') <
|
|
prepareReleaseBlock.indexOf("runReleaseSuite(projectRoot)"),
|
|
"release preparation should refresh volatile metadata before generating canonical release artifacts",
|
|
);
|
|
assert.match(
|
|
releaseWorkflowScript,
|
|
/const releaseBranch = `release\/v\$\{version\}`/,
|
|
"release preparation should use a protected release branch",
|
|
);
|
|
assert.doesNotMatch(
|
|
releaseWorkflowScript,
|
|
/\["push", "origin", "main"\]/,
|
|
"release tooling must not push directly to protected main",
|
|
);
|
|
assert.match(
|
|
publishWorkflow,
|
|
/npm pack --dry-run --json/,
|
|
"npm publish workflow should dry-run package creation before publishing",
|
|
);
|