@intentius/chant 0.124.32 → 0.124.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -3
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/pr.d.ts +2 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +124 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +277 -74
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +99 -22
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +30 -1
- package/dist/cli/registry.d.ts +21 -5
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +9 -0
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +33 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +42 -11
- package/dist/components/pr-run.d.ts +1 -1
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/promote-rounds.d.ts +102 -0
- package/dist/components/promote-rounds.d.ts.map +1 -0
- package/dist/components/promote-rounds.js +174 -0
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-gate.d.ts +30 -0
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +92 -42
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +10 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +32 -4
- package/dist/graph-declared.d.ts +4 -0
- package/dist/graph-declared.d.ts.map +1 -1
- package/dist/graph-declared.js +11 -1
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon.d.ts +27 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +14 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +15 -5
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/wave-runtime.mjs +236 -229
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.js +13 -2
- package/dist/workspace/check.schema.json +88 -2
- package/dist/workspace/checks/ci-workflow.d.ts +17 -0
- package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/checks/ci-workflow.js +123 -0
- package/dist/workspace/checks/security.d.ts +50 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +8 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +12 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +89 -4
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +310 -23
- package/dist/workspace/ci-init.d.ts +100 -0
- package/dist/workspace/ci-init.d.ts.map +1 -0
- package/dist/workspace/ci-init.js +434 -0
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +35 -1
- package/dist/workspace/ci-run-index.d.ts +1 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.schema.json +1 -1
- package/dist/workspace/ci-run.d.ts +12 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +72 -16
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +76 -7
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +153 -19
- package/dist/workspace/ci-workflow.d.ts +91 -0
- package/dist/workspace/ci-workflow.d.ts.map +1 -0
- package/dist/workspace/ci-workflow.js +33 -0
- package/dist/workspace/declaration.d.ts +35 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +12 -2
- package/dist/workspace/declaration.schema.json +51 -0
- package/dist/workspace/init.js +2 -2
- package/dist/workspace/ls.d.ts +2 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -1
- package/dist/workspace/ls.schema.json +2 -1
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pr-report.schema.json +3 -3
- package/dist/workspace/reason-codes.d.ts +26 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +28 -0
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/release-workflow.d.ts +31 -0
- package/dist/workspace/release-workflow.d.ts.map +1 -0
- package/dist/workspace/release-workflow.js +150 -0
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/handlers/build.test.ts +36 -0
- package/src/cli/handlers/build.ts +18 -3
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr.test.ts +22 -0
- package/src/cli/handlers/pr.ts +127 -4
- package/src/cli/handlers/promote.test.ts +17 -0
- package/src/cli/handlers/promote.ts +296 -84
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/main.ts +99 -22
- package/src/cli/parse-args.ts +22 -1
- package/src/cli/registry.ts +21 -5
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.ts +11 -0
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.ts +56 -15
- package/src/components/pr-run.ts +1 -1
- package/src/components/promote-rounds.test.ts +220 -0
- package/src/components/promote-rounds.ts +232 -0
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-gate.test.ts +47 -0
- package/src/components/release-gate.ts +111 -46
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +32 -4
- package/src/graph-declared-exports.test.ts +40 -0
- package/src/graph-declared.ts +9 -1
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon.ts +41 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/pr-loop.test.ts +29 -0
- package/src/pr-loop.ts +26 -7
- package/src/pr-report-url.ts +2 -2
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/workspace/__fixtures__/contract-repo.ts +11 -2
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +88 -2
- package/src/workspace/checks/ci-workflow.test.ts +150 -0
- package/src/workspace/checks/ci-workflow.ts +129 -0
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +8 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +24 -0
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +5 -2
- package/src/workspace/ci-green.ts +359 -27
- package/src/workspace/ci-init.test.ts +159 -0
- package/src/workspace/ci-init.ts +445 -0
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -1
- package/src/workspace/ci-report-url.test.ts +6 -1
- package/src/workspace/ci-run-index.schema.json +1 -1
- package/src/workspace/ci-run-index.ts +1 -1
- package/src/workspace/ci-run.test.ts +24 -7
- package/src/workspace/ci-run.ts +78 -16
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +59 -0
- package/src/workspace/ci-serve.ts +177 -22
- package/src/workspace/ci-workflow.ts +71 -0
- package/src/workspace/declaration.schema.json +51 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +53 -3
- package/src/workspace/init.ts +2 -2
- package/src/workspace/ls.schema.json +2 -1
- package/src/workspace/ls.ts +3 -2
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pr-report.schema.json +3 -3
- package/src/workspace/reason-codes.test.ts +4 -0
- package/src/workspace/reason-codes.ts +28 -0
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/release-workflow.ts +167 -0
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WSP151, what a member under ci.workflow still lacks (#4168 step 6), and the
|
|
3
|
+
* reference page printed from CI_WORKFLOW_REQUIREMENTS.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { runDeclarationChecks } from "../checks";
|
|
13
|
+
import { ciWorkflowTable, CI_WORKFLOW_FINDING_CODES, workflowOf } from "../ci-workflow";
|
|
14
|
+
import { REASON_CODES } from "../reason-codes";
|
|
15
|
+
|
|
16
|
+
const DOCS = fileURLToPath(new URL("../../../../../docs/src/content/docs/", import.meta.url));
|
|
17
|
+
|
|
18
|
+
const scratch: string[] = [];
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
function repo(files: Record<string, string>): string {
|
|
23
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-ci-workflow-")));
|
|
24
|
+
scratch.push(root);
|
|
25
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
26
|
+
for (const [path, text] of Object.entries(files)) {
|
|
27
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
28
|
+
writeFileSync(join(root, path), text);
|
|
29
|
+
}
|
|
30
|
+
return root;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const member = (extra: Record<string, unknown> = {}) => ({ name: "app", dir: "app", kind: "other", because: "the app", ...extra });
|
|
34
|
+
const declaration = (extra: Record<string, unknown> = {}, members = [member()]) => JSON.stringify({ name: "acme", schema: 1, members, ...extra }, null, 2);
|
|
35
|
+
const green = (extra: Record<string, unknown> = {}) => ({ branch: "main", phases: { check: ["check"], suites: ["suites"] }, require: ["check", "suites"], preMerge: ["check"], ...extra });
|
|
36
|
+
|
|
37
|
+
const CONFIG = 'export default { lexicons: ["aws"], environments: ["staging", "prod"] };\n';
|
|
38
|
+
const COMPONENT = 'import { gate, phase } from "@intentius/chant/components/component";\nexport const app = { name: "app", deploy: [phase("Apply", [gate("prod")])] };\n';
|
|
39
|
+
const PIPELINE = [
|
|
40
|
+
"on: [pull_request, push]",
|
|
41
|
+
"jobs:",
|
|
42
|
+
" plan:",
|
|
43
|
+
" steps:",
|
|
44
|
+
" - run: chant ci run plan --in-place --no-push --env prod --gate pr-apply --forge github",
|
|
45
|
+
" record:",
|
|
46
|
+
" steps:",
|
|
47
|
+
" - run: chant ci run record --in-place --env prod --gate pr-apply --forge github",
|
|
48
|
+
"",
|
|
49
|
+
].join("\n");
|
|
50
|
+
const TICK = "jobs:\n tick:\n steps:\n - run: npx --yes @intentius/chant@0.124.33 ci tick\n";
|
|
51
|
+
const PUBLISH = "jobs:\n publish:\n steps:\n - run: npx chant release publish \"$LABEL\"\n";
|
|
52
|
+
|
|
53
|
+
/** A member under trunk-curated with everything the workflow needs. */
|
|
54
|
+
const complete = (extra: Record<string, string> = {}) => ({
|
|
55
|
+
"chant.workspace.json": declaration({ ci: { workflow: "trunk-curated", green: green() } }),
|
|
56
|
+
"app/chant.config.ts": CONFIG,
|
|
57
|
+
"app/app.component.ts": COMPONENT,
|
|
58
|
+
".github/workflows/chant-pr.yml": PIPELINE,
|
|
59
|
+
".github/workflows/chant-ci-green.yml": TICK,
|
|
60
|
+
".github/workflows/chant-release.yml": PUBLISH,
|
|
61
|
+
...extra,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The WSP151 findings as [code, entity]. */
|
|
65
|
+
const found = async (root: string) =>
|
|
66
|
+
(await runDeclarationChecks(root, (f) => f, { gather: false })).diagnostics.filter((d) => d.ruleId === "WSP151").map((d) => [d.code, d.entity ?? null]);
|
|
67
|
+
|
|
68
|
+
describe("ci.workflow (WSP151)", () => {
|
|
69
|
+
test("a declaration that names no workflow gets no finding", async () => {
|
|
70
|
+
const root = repo({ "chant.workspace.json": declaration(), "app/chant.config.ts": "export default {};\n" });
|
|
71
|
+
expect(await found(root)).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a workspace with everything trunk-curated needs passes", async () => {
|
|
75
|
+
expect(await found(repo(complete()))).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("an empty workspace under trunk-curated reports each missing piece", async () => {
|
|
79
|
+
const root = repo({ "chant.workspace.json": declaration({ ci: { workflow: "trunk-curated" } }), "app/chant.config.ts": "export default {};\n" });
|
|
80
|
+
expect(await found(root)).toEqual([
|
|
81
|
+
["ci-workflow-no-environments", "app"],
|
|
82
|
+
["ci-workflow-no-components", "app"],
|
|
83
|
+
["ci-workflow-green-undeclared", null],
|
|
84
|
+
["ci-workflow-pipeline-missing", null],
|
|
85
|
+
["ci-workflow-publish-missing", null],
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("no preMerge phases, and a required phase that matches the apply job", async () => {
|
|
90
|
+
const root = repo(complete({ "chant.workspace.json": declaration({ ci: { workflow: "trunk-curated", green: green({ phases: { check: ["check"], deploy: ["*apply"] }, require: ["check", "deploy"], preMerge: [] }) } }) }));
|
|
91
|
+
expect(await found(root)).toEqual([
|
|
92
|
+
["ci-workflow-no-pre-merge", null],
|
|
93
|
+
["ci-workflow-apply-in-green", null],
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a component with no gate", async () => {
|
|
98
|
+
const root = repo(complete({ "app/app.component.ts": 'export const app = { name: "app", deploy: [] };\n' }));
|
|
99
|
+
expect(await found(root)).toEqual([["ci-workflow-ungated-env", "app"]]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("trunk-curated with an apply job on merge differs", async () => {
|
|
103
|
+
const root = repo(complete({ ".github/workflows/chant-pr.yml": PIPELINE.replace("ci run record", "ci run apply") }));
|
|
104
|
+
expect(await found(root)).toEqual([["ci-workflow-pipeline-differs", null]]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("trunk-curated+apply-on-merge with only a record job differs, and with an apply job passes", async () => {
|
|
108
|
+
const decl = declaration({ ci: { workflow: "trunk-curated+apply-on-merge", green: green() } });
|
|
109
|
+
expect(await found(repo(complete({ "chant.workspace.json": decl })))).toEqual([["ci-workflow-pipeline-differs", null]]);
|
|
110
|
+
expect(await found(repo(complete({ "chant.workspace.json": decl, ".github/workflows/chant-pr.yml": PIPELINE.replace("ci run record", "ci run apply") })))).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a member's own workflow wins, and a top-level workflow skips members that don't build with chant", async () => {
|
|
114
|
+
const root = repo({
|
|
115
|
+
...complete(),
|
|
116
|
+
"chant.workspace.json": declaration({ ci: { green: green() } }, [member({ ci: { workflow: "trunk-curated" } }), member({ name: "docs", dir: "docs" })]),
|
|
117
|
+
"docs/README.md": "",
|
|
118
|
+
});
|
|
119
|
+
expect(await found(root)).toEqual([]);
|
|
120
|
+
const top = repo({ ...complete(), "docs/README.md": "", "chant.workspace.json": declaration({ ci: { workflow: "trunk-curated", green: green() } }, [member(), member({ name: "docs", dir: "docs" })]) });
|
|
121
|
+
expect(await found(top)).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("ci.green.preMerge must name declared phases", async () => {
|
|
125
|
+
const root = repo({ "chant.workspace.json": declaration({ ci: { green: green({ preMerge: ["lint"] }) } }) });
|
|
126
|
+
const report = await runDeclarationChecks(root, (f) => f, { gather: false });
|
|
127
|
+
expect(report.diagnostics.find((d) => d.ruleId === "WSP001")?.message).toMatch(/ci\.green\.preMerge names the phase "lint"/);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("workflowOf", () => {
|
|
132
|
+
test("the member's own wins over the top level's", () => {
|
|
133
|
+
const d = { ci: { green: null, workflow: "trunk-curated" as const, reportUrl: null } };
|
|
134
|
+
expect(workflowOf(d, { ciWorkflow: null })).toBe("trunk-curated");
|
|
135
|
+
expect(workflowOf(d, { ciWorkflow: "trunk-curated+apply-on-merge" })).toBe("trunk-curated+apply-on-merge");
|
|
136
|
+
expect(workflowOf({ ci: null }, null)).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("the ci.workflow codes and reference page", () => {
|
|
141
|
+
test("every code is a read-contract reason code", () => {
|
|
142
|
+
for (const code of CI_WORKFLOW_FINDING_CODES) expect(REASON_CODES, code).toContain(code);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("the CI workflows page carries the table CI_WORKFLOW_REQUIREMENTS prints", () => {
|
|
146
|
+
const page = readFileSync(join(DOCS, "reference", "ci-workflows.mdx"), "utf-8");
|
|
147
|
+
const table = /^\| Code \| Requirement \|[^\n]*\n(?:\|[^\n]*\n)*/m.exec(page)?.[0].trimEnd();
|
|
148
|
+
expect(table, "regenerate the table under Requirements from ciWorkflowTable()").toBe(ciWorkflowTable());
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WSP151 (#4168 step 6): what a member under `ci.workflow` still lacks.
|
|
3
|
+
*
|
|
4
|
+
* A declaration that names no workflow gets no finding from this file. The
|
|
5
|
+
* requirements and their codes are {@link CI_WORKFLOW_REQUIREMENTS}
|
|
6
|
+
* (../ci-workflow.ts). Like the security checklists (./security.ts), the
|
|
7
|
+
* check reads files and never runs them: `chant.config.ts` for the
|
|
8
|
+
* environments (its literal `environments`, as `workspace status` reads it),
|
|
9
|
+
* `*.component.ts` for components and gates, and the forge pipeline files for
|
|
10
|
+
* the jobs. A generated file that differs from what its generator writes is
|
|
11
|
+
* WSP101 to WSP106's to report; this check asks only whether the jobs the
|
|
12
|
+
* workflow needs are there.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { WorkspaceCheck, WorkspaceDiagnostic } from "../checks";
|
|
16
|
+
import type { CiWorkflow, Member } from "../declaration";
|
|
17
|
+
import { appliesOnMerge, STUB_MARKER, workflowOf, type CiWorkflowCode } from "../ci-workflow";
|
|
18
|
+
import { matchesCheckRun } from "../ci-green";
|
|
19
|
+
import { staticPromotionOrder } from "../promotion-order";
|
|
20
|
+
import { joinPath } from "../tree";
|
|
21
|
+
import { allPipelineFiles, commandLines, componentFiles, read } from "./security";
|
|
22
|
+
|
|
23
|
+
export const WSP_CI_WORKFLOW = "WSP151";
|
|
24
|
+
|
|
25
|
+
const memberDir = (dir: string) => (dir === "." ? "" : dir);
|
|
26
|
+
|
|
27
|
+
const PLAN = /\bci\s+run\s+plan\b|\bcomponents\s+pr-plan\b/;
|
|
28
|
+
const RECORD = /\bci\s+run\s+record\b|\bcomponents\s+pr-record\b/;
|
|
29
|
+
const APPLY = /\bci\s+run\s+apply\b|\bcomponents\s+pr-apply\b/;
|
|
30
|
+
const TICK = /\bci\s+tick\b/;
|
|
31
|
+
const PUBLISH = /\brelease\s+publish\b/;
|
|
32
|
+
|
|
33
|
+
/** The apply job names a required phase could match. */
|
|
34
|
+
const APPLY_JOB_NAMES = ["apply", "pr-apply"];
|
|
35
|
+
|
|
36
|
+
export const CI_WORKFLOW_CHECKS: readonly WorkspaceCheck[] = [
|
|
37
|
+
{
|
|
38
|
+
id: WSP_CI_WORKFLOW,
|
|
39
|
+
name: "ci-workflow",
|
|
40
|
+
description: "Under ci.workflow: ci.green with pre-merge phases and no apply job required, each member with a component, environments and a gate on each component, and pipeline files that plan, record (or apply under apply on merge), tick and publish.",
|
|
41
|
+
severity: "warning",
|
|
42
|
+
configurable: true,
|
|
43
|
+
check(ctx) {
|
|
44
|
+
const d = ctx.declaration;
|
|
45
|
+
const under = d.members
|
|
46
|
+
.map((m) => ({ member: m, workflow: workflowOf(d, m) }))
|
|
47
|
+
.filter((u): u is { member: Member; workflow: CiWorkflow } => u.workflow !== null)
|
|
48
|
+
// The top level's workflow covers the members that build with chant; a member that names one itself is held to it.
|
|
49
|
+
.filter((u) => u.member.ciWorkflow !== null || ctx.tree.stat(joinPath(memberDir(u.member.dir), "chant.config.ts")) === "file");
|
|
50
|
+
if (under.length === 0 && !d.ci?.workflow) return [];
|
|
51
|
+
const pointer = d.ci?.workflow ? "/ci/workflow" : `${under[0]!.member.pointer}/ci/workflow`;
|
|
52
|
+
const out: WorkspaceDiagnostic[] = [];
|
|
53
|
+
const finding = (code: CiWorkflowCode, message: string, at: { pointer?: string; entity?: string; treeFile?: string; line?: number } = {}): void => {
|
|
54
|
+
out.push({
|
|
55
|
+
checkId: this.id,
|
|
56
|
+
severity: this.severity,
|
|
57
|
+
code,
|
|
58
|
+
message: `${code}: ${message}`,
|
|
59
|
+
pointer: at.pointer ?? pointer,
|
|
60
|
+
...(at.entity !== undefined ? { entity: at.entity } : {}),
|
|
61
|
+
...(at.treeFile !== undefined ? { treeFile: at.treeFile, line: at.line ?? 1, column: 1 } : {}),
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const green = d.ci?.green ?? null;
|
|
66
|
+
if (!green) {
|
|
67
|
+
finding("ci-workflow-green-undeclared", "the workspace follows a ci.workflow and declares no ci.green, so no commit gets a verdict and no release can be cut; run chant ci init or add a ci.green block");
|
|
68
|
+
} else {
|
|
69
|
+
if ((green.preMerge ?? []).length === 0) {
|
|
70
|
+
finding("ci-workflow-no-pre-merge", "ci.green.preMerge names no phase, so nothing says which checks run before the branch moves; name the quick ones", { pointer: green.pointer });
|
|
71
|
+
}
|
|
72
|
+
for (const name of green.require) {
|
|
73
|
+
const phase = green.phases.find((p) => p.name === name);
|
|
74
|
+
const hit = phase?.runs.find((pattern) => APPLY_JOB_NAMES.some((job) => matchesCheckRun(pattern, job)));
|
|
75
|
+
if (phase && hit) {
|
|
76
|
+
finding("ci-workflow-apply-in-green", `the required phase ${name} matches the apply job with ${JSON.stringify(hit)}, so an apply that waits for a green commit would wait on itself; drop the pattern`, { pointer: phase.pointer });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const { member } of under) {
|
|
82
|
+
const dir = memberDir(member.dir);
|
|
83
|
+
const components = componentFiles(ctx.tree, dir);
|
|
84
|
+
if (components.length === 0) {
|
|
85
|
+
finding("ci-workflow-no-components", `member ${member.name} declares no component, so nothing of it can be cut or promoted; chant ci init writes a stub`, { pointer: member.pointer, entity: member.name });
|
|
86
|
+
} else if (components.every((f) => (read(ctx.tree, f) ?? "").includes(STUB_MARKER))) {
|
|
87
|
+
finding("ci-workflow-no-components", `member ${member.name}'s only component is the stub chant ci init wrote (${components[0]}); say how it is built and deployed, then delete its ${STUB_MARKER} line`, { pointer: member.pointer, entity: member.name, treeFile: components[0] });
|
|
88
|
+
}
|
|
89
|
+
const configPath = joinPath(dir, "chant.config.ts");
|
|
90
|
+
const config = read(ctx.tree, configPath);
|
|
91
|
+
// A config whose environments aren't literals can't be read without running it; say nothing rather than guess.
|
|
92
|
+
const order = config === null ? [] : staticPromotionOrder(config);
|
|
93
|
+
if (order !== undefined && order.length === 0) {
|
|
94
|
+
finding("ci-workflow-no-environments", `member ${member.name}'s chant.config.ts declares no environments, so a release has nowhere to be promoted to`, { pointer: member.pointer, entity: member.name, ...(config !== null ? { treeFile: configPath } : {}) });
|
|
95
|
+
}
|
|
96
|
+
for (const file of components) {
|
|
97
|
+
const text = read(ctx.tree, file) ?? "";
|
|
98
|
+
if (/\bgate\s*\(|kind\s*:\s*["']gate["']/.test(text)) continue;
|
|
99
|
+
const last = order && order.length > 0 ? order[order.length - 1]!.name : "the last environment";
|
|
100
|
+
finding("ci-workflow-ungated-env", `${file} has no gate, so a promote to ${last} waits on no approval; add a gate() step`, { pointer: member.pointer, entity: member.name, treeFile: file });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const files = allPipelineFiles(ctx);
|
|
105
|
+
const has = (re: RegExp) => files.some((f) => f.lines.some((l) => re.test(l)));
|
|
106
|
+
const applyOnMerge = under.some((u) => appliesOnMerge(u.workflow)) || (d.ci?.workflow ? appliesOnMerge(d.ci.workflow) : false);
|
|
107
|
+
const missing = [
|
|
108
|
+
...(has(PLAN) ? [] : ["a plan job (ci run plan)"]),
|
|
109
|
+
...(has(RECORD) || has(APPLY) ? [] : [applyOnMerge ? "an apply job (ci run apply)" : "a record job (ci run record)"]),
|
|
110
|
+
...(has(TICK) ? [] : ["chant ci tick"]),
|
|
111
|
+
];
|
|
112
|
+
if (missing.length > 0) {
|
|
113
|
+
finding("ci-workflow-pipeline-missing", `no pipeline file runs ${missing.join(", ")}; generate them with chant build --components --generate <forge> --pr-loop and chant ci workflow`);
|
|
114
|
+
}
|
|
115
|
+
if (!applyOnMerge) {
|
|
116
|
+
for (const l of commandLines(ctx, APPLY)) {
|
|
117
|
+
finding("ci-workflow-pipeline-differs", `${l.path} applies on merge, and the workflow is trunk-curated, where a merge records the plan and deploys nothing; regenerate without --apply-on-merge, or set ci.workflow to trunk-curated+apply-on-merge`, { treeFile: l.path, line: l.line });
|
|
118
|
+
}
|
|
119
|
+
} else if (!has(APPLY) && has(RECORD)) {
|
|
120
|
+
const l = commandLines(ctx, RECORD)[0]!;
|
|
121
|
+
finding("ci-workflow-pipeline-differs", `${l.path} records on merge, and the workflow is trunk-curated+apply-on-merge; regenerate with --apply-on-merge`, { treeFile: l.path, line: l.line });
|
|
122
|
+
}
|
|
123
|
+
if (!has(PUBLISH)) {
|
|
124
|
+
finding("ci-workflow-publish-missing", "no pipeline file runs chant release publish, so a cut release is never published; generate one with chant release workflow");
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The security group checklists, WSP161 to WSP165 (#4168 step 7).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
10
|
+
import { runDeclarationChecks } from "../checks";
|
|
11
|
+
|
|
12
|
+
const scratch: string[] = [];
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
function repo(files: Record<string, string>): string {
|
|
17
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-security-")));
|
|
18
|
+
scratch.push(root);
|
|
19
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
20
|
+
for (const [path, text] of Object.entries(files)) {
|
|
21
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
22
|
+
writeFileSync(join(root, path), text);
|
|
23
|
+
}
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const member = (extra: Record<string, unknown> = {}) => ({ name: "app", dir: "app", kind: "other", because: "the app", ...extra });
|
|
28
|
+
const declaration = (extra: Record<string, unknown> = {}, members = [member()]) => JSON.stringify({ name: "acme", schema: 1, members, ...extra }, null, 2);
|
|
29
|
+
|
|
30
|
+
/** The security findings as [id, code, entity]. */
|
|
31
|
+
const found = async (root: string) =>
|
|
32
|
+
(await runDeclarationChecks(root, (f) => f, { gather: false })).diagnostics.filter((d) => /^WSP16/.test(d.ruleId)).map((d) => [d.ruleId, d.code, d.entity ?? null]);
|
|
33
|
+
|
|
34
|
+
describe("security groups (WSP161 to WSP165)", () => {
|
|
35
|
+
test("a workspace that opts into no group gets no security finding", async () => {
|
|
36
|
+
const root = repo({ "chant.workspace.json": declaration(), "app/chant.config.ts": "export default { release: { publish: [{ npm: {} , sbom: false }] } };" });
|
|
37
|
+
expect(await found(root)).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("verified-releases with nothing set reports the signers file and the release gate", async () => {
|
|
41
|
+
const root = repo({ "chant.workspace.json": declaration({ security: ["verified-releases"] }), "app/x": "" });
|
|
42
|
+
expect(await found(root)).toEqual([
|
|
43
|
+
["WSP161", "security-signers-missing", null],
|
|
44
|
+
["WSP161", "security-release-gate-missing", null],
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("verified-releases passes with signers and a release gate, and flags --allow-unsigned in a publish job", async () => {
|
|
49
|
+
const root = repo({
|
|
50
|
+
"chant.workspace.json": declaration({ security: ["verified-releases"], identity: { gates: { release: { class: "person" } } } }),
|
|
51
|
+
".chant/allowed_signers": "",
|
|
52
|
+
".github/workflows/publish.yml": "jobs:\n publish:\n steps:\n - run: npx chant release publish v1 --allow-unsigned\n",
|
|
53
|
+
"app/x": "",
|
|
54
|
+
});
|
|
55
|
+
expect(await found(root)).toEqual([["WSP161", "security-publish-allows-unsigned", null]]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a member's group applies to that member only", async () => {
|
|
59
|
+
const root = repo({
|
|
60
|
+
"chant.workspace.json": declaration({}, [member({ security: ["supply-chain"] }), member({ name: "web", dir: "web" })]),
|
|
61
|
+
"app/chant.config.ts": "export default { release: { publish: [{ forge: {}, sbom: false }] } };",
|
|
62
|
+
"web/chant.config.ts": "export default { release: { publish: [{ forge: {}, sbom: false }] } };",
|
|
63
|
+
});
|
|
64
|
+
expect(await found(root)).toEqual([["WSP162", "security-sbom-disabled", "app"]]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("supply-chain reports an unsourced SBOM, missing component steps and a publish without provenance", async () => {
|
|
68
|
+
const root = repo({
|
|
69
|
+
"chant.workspace.json": declaration({ security: ["supply-chain"] }),
|
|
70
|
+
"app/chant.config.ts": "export default { release: { publish: [{ forge: {} }] } };",
|
|
71
|
+
"app/src/api.component.ts": "export const api = component({ deploy: [shell({ cmd: 'x' })] });",
|
|
72
|
+
".github/workflows/publish.yml": " - run: chant release publish v1\n",
|
|
73
|
+
});
|
|
74
|
+
expect((await found(root)).map((f) => f[1]).sort()).toEqual(["security-component-no-sbom", "security-component-no-verify", "security-publish-no-provenance", "security-sbom-unsourced"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a Cargo.lock above the member is an SBOM source", async () => {
|
|
78
|
+
const root = repo({
|
|
79
|
+
"chant.workspace.json": declaration({ security: ["supply-chain"] }),
|
|
80
|
+
"Cargo.lock": "version = 4\n",
|
|
81
|
+
"app/chant.config.ts": "export default { release: { publish: [{ forge: {} }] } };",
|
|
82
|
+
});
|
|
83
|
+
expect(await found(root)).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("who-ran-it reports signedSince, the runner key and an unsigned tick", async () => {
|
|
87
|
+
const root = repo({
|
|
88
|
+
"chant.workspace.json": declaration({ security: ["who-ran-it"], ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"] } } }),
|
|
89
|
+
".github/workflows/chant-ci-green.yml": " run: npx --yes @intentius/chant@1 ci tick\n",
|
|
90
|
+
"app/x": "",
|
|
91
|
+
});
|
|
92
|
+
expect((await found(root)).map((f) => f[1]).sort()).toEqual(["security-runner-untrusted", "security-signed-since-unset", "security-tick-no-key"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("who-ran-it passes with signedSince, a trusted runner and a keyed tick", async () => {
|
|
96
|
+
const root = repo({
|
|
97
|
+
"chant.workspace.json": declaration({ security: ["who-ran-it"], ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"], signedSince: "abc1234" } } }),
|
|
98
|
+
".chant/trust.json": JSON.stringify({ schema: 1, runners: [{ principal: "chant-runner:box", key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", class: "runner" }] }),
|
|
99
|
+
".github/workflows/chant-ci-green.yml": " env:\n CHANT_RUNNER_KEY: ${{ secrets.K }}\n run: npx chant ci tick\n",
|
|
100
|
+
"app/x": "",
|
|
101
|
+
});
|
|
102
|
+
expect(await found(root)).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("review-rules reports --no-require-review, attribution and an unsigned gate", async () => {
|
|
106
|
+
const root = repo({
|
|
107
|
+
"chant.workspace.json": declaration({ security: ["review-rules"] }),
|
|
108
|
+
".github/workflows/pr.yml": " - run: chant components pr-apply --base \"$BASE_SHA\" --env prod --gate deploy --forge github --no-require-review\n",
|
|
109
|
+
"app/x": "",
|
|
110
|
+
});
|
|
111
|
+
expect(await found(root)).toEqual([
|
|
112
|
+
["WSP164", "security-review-not-required", null],
|
|
113
|
+
["WSP164", "security-attribution-any", null],
|
|
114
|
+
["WSP164", "security-gate-unsigned", null],
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("review-rules passes with identified attribution and the apply gate signed", async () => {
|
|
119
|
+
const root = repo({
|
|
120
|
+
"chant.workspace.json": declaration({ security: ["review-rules"], identity: { attribution: "identified", gates: { deploy: { class: "person" } } } }),
|
|
121
|
+
".github/workflows/pr.yml": " - run: chant components pr-apply --base \"$BASE_SHA\" --env prod --gate deploy --forge github\n",
|
|
122
|
+
"app/x": "",
|
|
123
|
+
});
|
|
124
|
+
expect(await found(root)).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("isolated-runners reports a missing runner key, then a runner without the runner role, then passes", async () => {
|
|
128
|
+
const files = { "chant.workspace.json": declaration({ security: ["isolated-runners"] }), "app/x": "" };
|
|
129
|
+
expect(await found(repo(files))).toEqual([["WSP165", "security-runner-start-no-key", null]]);
|
|
130
|
+
const runner = { principal: "chant-runner:box", key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", class: "runner" };
|
|
131
|
+
expect(await found(repo({ ...files, ".chant/trust.json": JSON.stringify({ schema: 1, runners: [runner] }) }))).toEqual([["WSP165", "security-runner-start-no-role", null]]);
|
|
132
|
+
expect(await found(repo({ ...files, ".chant/trust.json": JSON.stringify({ schema: 1, runners: [runner], roles: { runner: ["chant-runner:box"] } }) }))).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("isolated-runners reports a trust.json that does not read", async () => {
|
|
136
|
+
expect(await found(repo({ "chant.workspace.json": declaration({ security: ["isolated-runners"] }), ".chant/trust.json": "{", "app/x": "" }))).toEqual([["WSP165", "security-trust-unreadable", null]]);
|
|
137
|
+
});
|
|
138
|
+
});
|