@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
|
+
* `chant release workflow` (#4168 step 6): the publish workflow, generated
|
|
3
|
+
* the way `chant ci workflow` writes the tick, where the guides used to have
|
|
4
|
+
* one pasted (guide/release-from-workstation.mdx).
|
|
5
|
+
*
|
|
6
|
+
* The job runs on a `release/**` tag, checks the tag out, fetches the ci tags
|
|
7
|
+
* and `chant/lifecycle`, builds what the targets upload, and runs `chant
|
|
8
|
+
* release publish` for the label the tag names. The security groups decide
|
|
9
|
+
* the rest, and nothing is on without one (#4168 section 8):
|
|
10
|
+
*
|
|
11
|
+
* - `supply-chain` adds `--require-provenance`, so a run with no OIDC token
|
|
12
|
+
* fails rather than publishing without provenance.
|
|
13
|
+
* - `verified-releases` adds a `chant release verify` step before the publish.
|
|
14
|
+
*
|
|
15
|
+
* The OIDC permission (`id-token: write`, or GitLab's `id_tokens`) is always
|
|
16
|
+
* there: it costs nothing, and with it what the job uploads carries
|
|
17
|
+
* provenance whether or not the group requires it.
|
|
18
|
+
*/
|
|
19
|
+
import { GENERATED_MARKER } from "../discovery/files.js";
|
|
20
|
+
export const RELEASE_WORKFLOW_FORGES = ["github", "forgejo", "gitlab"];
|
|
21
|
+
/** Where each forge's publish workflow is written, relative to the repository root. */
|
|
22
|
+
export const RELEASE_WORKFLOW_FILES = {
|
|
23
|
+
github: ".github/workflows/chant-release.yml",
|
|
24
|
+
forgejo: ".forgejo/workflows/chant-release.yml",
|
|
25
|
+
// GitLab reads one .gitlab-ci.yml, which includes this file: `include: { local: .gitlab/chant-release.yml }`.
|
|
26
|
+
gitlab: ".gitlab/chant-release.yml",
|
|
27
|
+
};
|
|
28
|
+
/** The flags `chant release publish` gets under the groups. */
|
|
29
|
+
export function publishFlags(groups) {
|
|
30
|
+
return [...(groups.has("supply-chain") ? ["--require-provenance"] : []), "--yes"];
|
|
31
|
+
}
|
|
32
|
+
/** The workflow file's text, without the generated-by header. */
|
|
33
|
+
export function renderReleaseWorkflow(input) {
|
|
34
|
+
const chant = input.chant ?? "npx chant";
|
|
35
|
+
const install = input.install ?? "npm ci";
|
|
36
|
+
const build = input.build === undefined ? "npm run build" : input.build;
|
|
37
|
+
const root = input.workspaceRoot && input.workspaceRoot !== "." ? input.workspaceRoot : null;
|
|
38
|
+
const verify = input.groups.has("verified-releases");
|
|
39
|
+
const flags = publishFlags(input.groups).join(" ");
|
|
40
|
+
if (input.forge === "gitlab") {
|
|
41
|
+
const label = '"${CI_COMMIT_TAG#release/}"';
|
|
42
|
+
const lines = [
|
|
43
|
+
"chant-release-publish:",
|
|
44
|
+
" rules:",
|
|
45
|
+
" - if: $CI_COMMIT_TAG =~ /^release\\//",
|
|
46
|
+
" id_tokens:",
|
|
47
|
+
" SIGSTORE_ID_TOKEN:",
|
|
48
|
+
" aud: sigstore",
|
|
49
|
+
" script:",
|
|
50
|
+
...(root ? [` - cd ${root}`] : []),
|
|
51
|
+
` - ${install}`,
|
|
52
|
+
...(build ? [` - ${build}`] : []),
|
|
53
|
+
" - git fetch origin 'refs/tags/ci/*:refs/tags/ci/*'",
|
|
54
|
+
" - git fetch origin chant/lifecycle:chant/lifecycle",
|
|
55
|
+
...(verify ? [` - ${chant} release verify ${label}`] : []),
|
|
56
|
+
` - ${chant} release publish ${label} ${flags}`,
|
|
57
|
+
];
|
|
58
|
+
return `${lines.join("\n")}\n`;
|
|
59
|
+
}
|
|
60
|
+
const label = '"${GITHUB_REF_NAME#release/}"';
|
|
61
|
+
const lines = [
|
|
62
|
+
"name: chant release publish",
|
|
63
|
+
"on:",
|
|
64
|
+
" push:",
|
|
65
|
+
' tags: ["release/**"]',
|
|
66
|
+
"permissions:",
|
|
67
|
+
" contents: write",
|
|
68
|
+
" id-token: write",
|
|
69
|
+
"jobs:",
|
|
70
|
+
" publish:",
|
|
71
|
+
" runs-on: ubuntu-latest",
|
|
72
|
+
...(root ? [" defaults:", " run:", ` working-directory: ${root}`] : []),
|
|
73
|
+
" steps:",
|
|
74
|
+
" - uses: actions/checkout@v4",
|
|
75
|
+
" with:",
|
|
76
|
+
" fetch-depth: 0",
|
|
77
|
+
" - uses: actions/setup-node@v4",
|
|
78
|
+
" with:",
|
|
79
|
+
" node-version: 24",
|
|
80
|
+
" registry-url: https://registry.npmjs.org",
|
|
81
|
+
` - run: ${install}`,
|
|
82
|
+
...(build ? [` - run: ${build}`] : []),
|
|
83
|
+
" - run: git fetch origin 'refs/tags/ci/*:refs/tags/ci/*'",
|
|
84
|
+
" - run: git fetch origin chant/lifecycle:chant/lifecycle",
|
|
85
|
+
...(verify ? [` - run: ${chant} release verify ${label}`] : []),
|
|
86
|
+
` - run: ${chant} release publish ${label} ${flags}`,
|
|
87
|
+
];
|
|
88
|
+
return `${lines.join("\n")}\n`;
|
|
89
|
+
}
|
|
90
|
+
/** The header that marks the file generated, naming the command that regenerates it. */
|
|
91
|
+
export function releaseWorkflowHeader(command) {
|
|
92
|
+
return `# ${GENERATED_MARKER}. Regenerate with: ${command} (in the workspace root)\n`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `chant release workflow [--forge github|gitlab|forgejo] [--install <command>]
|
|
96
|
+
* [--build <command>|--no-build] [--chant <command>] [--output <file>]`: write
|
|
97
|
+
* the publish workflow for the workspace's forge, under its security groups.
|
|
98
|
+
*/
|
|
99
|
+
export async function runReleaseWorkflow(ctx) {
|
|
100
|
+
const { existsSync, mkdirSync, realpathSync, writeFileSync } = await import("node:fs");
|
|
101
|
+
const { dirname, join, relative, resolve, sep } = await import("node:path");
|
|
102
|
+
const { formatError, formatSuccess } = await import("../cli/format.js");
|
|
103
|
+
const { findWorkspaceRoot } = await import("../project-root.js");
|
|
104
|
+
const { gitTop } = await import("./tree.js");
|
|
105
|
+
const { securityGroupsAt } = await import("./security.js");
|
|
106
|
+
const { shellArg } = await import("./member-pipeline.js");
|
|
107
|
+
const usage = "chant release workflow [--forge github|gitlab|forgejo] [--install <command>] [--build <command>] [--chant <command>] [--output <file>]";
|
|
108
|
+
const { args } = ctx;
|
|
109
|
+
const found = findWorkspaceRoot(process.cwd());
|
|
110
|
+
if (!found) {
|
|
111
|
+
console.error(formatError({ message: "no chant.workspace.json between this directory and the git root", hint: usage }));
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
const root = realpathSync(found.dir);
|
|
115
|
+
const repoRoot = gitTop(root) ?? root;
|
|
116
|
+
const forge = (args.forge ??
|
|
117
|
+
(existsSync(join(repoRoot, ".github", "workflows")) ? "github" : existsSync(join(repoRoot, ".forgejo", "workflows")) ? "forgejo" : existsSync(join(repoRoot, ".gitlab-ci.yml")) ? "gitlab" : "github"));
|
|
118
|
+
if (!RELEASE_WORKFLOW_FORGES.includes(forge)) {
|
|
119
|
+
console.error(formatError({ message: `--forge ${JSON.stringify(forge)} is not one of ${RELEASE_WORKFLOW_FORGES.join(", ")}`, hint: usage }));
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const groups = securityGroupsAt(root);
|
|
123
|
+
const workspaceRoot = relative(repoRoot, root).split(sep).join("/") || ".";
|
|
124
|
+
const text = renderReleaseWorkflow({
|
|
125
|
+
forge,
|
|
126
|
+
groups,
|
|
127
|
+
workspaceRoot,
|
|
128
|
+
...(args.ciInstall ? { install: args.ciInstall } : {}),
|
|
129
|
+
...(args.buildCommand ? { build: args.buildCommand } : {}),
|
|
130
|
+
...(args.ciChant ? { chant: args.ciChant } : {}),
|
|
131
|
+
});
|
|
132
|
+
const target = args.output ? resolve(process.cwd(), args.output) : join(repoRoot, RELEASE_WORKFLOW_FILES[forge]);
|
|
133
|
+
const parts = ["chant", "release", "workflow", "--forge", forge];
|
|
134
|
+
if (args.ciInstall)
|
|
135
|
+
parts.push("--install", args.ciInstall);
|
|
136
|
+
if (args.buildCommand)
|
|
137
|
+
parts.push("--build", args.buildCommand);
|
|
138
|
+
if (args.ciChant)
|
|
139
|
+
parts.push("--chant", args.ciChant);
|
|
140
|
+
if (args.output)
|
|
141
|
+
parts.push("--output", relative(root, target).split(sep).join("/"));
|
|
142
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
143
|
+
writeFileSync(target, releaseWorkflowHeader(parts.map(shellArg).join(" ")) + text);
|
|
144
|
+
const shown = relative(process.cwd(), target).split(sep).join("/") || target;
|
|
145
|
+
const extras = [groups.has("supply-chain") ? "requires OIDC provenance (supply-chain)" : null, groups.has("verified-releases") ? "verifies the release first (verified-releases)" : null].filter(Boolean);
|
|
146
|
+
console.log(formatSuccess(`Wrote ${shown}: publishes the release a release/** tag names${extras.length > 0 ? `, and ${extras.join(" and ")}` : ""}.`));
|
|
147
|
+
if (forge === "gitlab")
|
|
148
|
+
console.log(`Include it from .gitlab-ci.yml: include: [{ local: ${RELEASE_WORKFLOW_FILES.gitlab} }]`);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Declaration } from "./declaration.js";
|
|
2
|
+
import { type ScopeSource } from "./write-scope.js";
|
|
3
|
+
export declare const SECURITY_GROUPS: readonly ["verified-releases", "supply-chain", "who-ran-it", "review-rules", "isolated-runners"];
|
|
4
|
+
export type SecurityGroup = (typeof SECURITY_GROUPS)[number];
|
|
5
|
+
/**
|
|
6
|
+
* The groups `declaration` turns on for `member` (a member name): the
|
|
7
|
+
* top-level list plus that member's own. Empty for no declaration or no
|
|
8
|
+
* `security` key anywhere.
|
|
9
|
+
*/
|
|
10
|
+
export declare function securityGroupsOf(declaration: Pick<Declaration, "security" | "members"> | null | undefined, member?: string | null): ReadonlySet<SecurityGroup>;
|
|
11
|
+
/** The member of `source`'s declaration whose directory holds `cwd`, or null. */
|
|
12
|
+
export declare function memberAtCwd(source: Pick<ScopeSource, "declaration" | "rootOnDisk">, cwd: string): string | null;
|
|
13
|
+
/** The groups `source` turns on for a command run in `cwd`: the top-level list and the list of the member holding `cwd`. */
|
|
14
|
+
export declare function securityGroupsIn(source: Pick<ScopeSource, "declaration" | "rootOnDisk">, cwd: string): ReadonlySet<SecurityGroup>;
|
|
15
|
+
/**
|
|
16
|
+
* The groups the workspace holding `cwd` turns on for the member holding
|
|
17
|
+
* `cwd`, read at base (the target branch tip) when there is one, else from
|
|
18
|
+
* the working tree, as the release gate is read. Empty outside a workspace.
|
|
19
|
+
*/
|
|
20
|
+
export declare function securityGroupsAt(cwd: string, at?: string): ReadonlySet<SecurityGroup>;
|
|
21
|
+
/** One row of the security feature table (#4168 section 8). */
|
|
22
|
+
export interface SecurityFeature {
|
|
23
|
+
feature: string;
|
|
24
|
+
/** What happens with no group and no setting. */
|
|
25
|
+
byDefault: string;
|
|
26
|
+
/** What turns it on alone. */
|
|
27
|
+
setting: string;
|
|
28
|
+
group: SecurityGroup | null;
|
|
29
|
+
/** The `workspace check` reason codes that report it missing under its group. */
|
|
30
|
+
codes: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Every security feature, the group that raises it and the setting that
|
|
34
|
+
* turns it on alone. The reference page's table is printed from this list
|
|
35
|
+
* (`securityFeatureTable`), and a test compares the two.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SECURITY_FEATURES: readonly SecurityFeature[];
|
|
38
|
+
/** The reference page's table of {@link SECURITY_FEATURES}, as markdown. */
|
|
39
|
+
export declare function securityFeatureTable(): string;
|
|
40
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/workspace/security.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAE9D,eAAO,MAAM,eAAe,YAAI,mBAAmB,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,CAAU,CAAC;AAEhI,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,GAAE,MAAM,GAAG,IAAW,GAAG,WAAW,CAAC,aAAa,CAAC,CAGpK;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,aAAa,GAAG,YAAY,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW/G;AAED,4HAA4H;AAC5H,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,WAAW,EAAE,aAAa,GAAG,YAAY,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC,aAAa,CAAC,CAEjI;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC,aAAa,CAAC,CAErF;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,iFAAiF;IACjF,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EAgBvD,CAAC;AAEF,4EAA4E;AAC5E,wBAAgB,oBAAoB,IAAI,MAAM,CAG7C"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The security groups a workspace opts into (#4168 section 8): the
|
|
3
|
+
* declaration's top-level `security`, such as `["verified-releases"]`, plus
|
|
4
|
+
* the `security` of the member a command runs in (#4168 step 7). With no
|
|
5
|
+
* `security` key none is on, and a security feature the workspace has not
|
|
6
|
+
* turned on never refuses, warns or hides anything (the rule #4156 set for
|
|
7
|
+
* runner signing).
|
|
8
|
+
*
|
|
9
|
+
* What each group turns on:
|
|
10
|
+
* - `verified-releases`: `chant release publish` refuses a release with no
|
|
11
|
+
* verified signature, as the `release` gate in `identity.gates` does, and
|
|
12
|
+
* `chant components promote --release` verifies the release first.
|
|
13
|
+
* - `supply-chain`: a publish without OIDC provenance warns, and a target
|
|
14
|
+
* with no SBOM source is refused (`publish-sbom-missing`).
|
|
15
|
+
* - `who-ran-it`: `chant ci tick` refuses to tag without a runner key.
|
|
16
|
+
* - `review-rules`: `chant components pr-apply` counts an approval only from
|
|
17
|
+
* someone who approved the pull request on the forge, as
|
|
18
|
+
* `--require-review` does, and a generated pull-request pipeline passes
|
|
19
|
+
* the flag.
|
|
20
|
+
* - `isolated-runners`: `chant ci run apply`, under `chant ci serve` too,
|
|
21
|
+
* refuses to start without a provisioner's runner-start statement for the
|
|
22
|
+
* job (#4147), as `--require-runner-start` does, and a generated
|
|
23
|
+
* pull-request pipeline passes the flag. A `--local` run is exempt.
|
|
24
|
+
*
|
|
25
|
+
* Each feature still turns on alone: `identity.gates.release`,
|
|
26
|
+
* `--require-provenance`, `--require-review`, `ci.green.signedSince`.
|
|
27
|
+
* `chant workspace check` reports what a declared group still lacks
|
|
28
|
+
* (./checks/security.ts, WSP161 to WSP165). {@link SECURITY_FEATURES} is the
|
|
29
|
+
* table the reference page prints.
|
|
30
|
+
*/
|
|
31
|
+
import { realpathSync } from "node:fs";
|
|
32
|
+
import { relative, sep } from "node:path";
|
|
33
|
+
import { memberHolding } from "./record-assets.js";
|
|
34
|
+
import { scopeSource } from "./write-scope.js";
|
|
35
|
+
export const SECURITY_GROUPS = ["verified-releases", "supply-chain", "who-ran-it", "review-rules", "isolated-runners"];
|
|
36
|
+
/**
|
|
37
|
+
* The groups `declaration` turns on for `member` (a member name): the
|
|
38
|
+
* top-level list plus that member's own. Empty for no declaration or no
|
|
39
|
+
* `security` key anywhere.
|
|
40
|
+
*/
|
|
41
|
+
export function securityGroupsOf(declaration, member = null) {
|
|
42
|
+
const own = member === null ? [] : (declaration?.members?.find((m) => m.name === member)?.security ?? []);
|
|
43
|
+
return new Set([...(declaration?.security ?? []), ...own]);
|
|
44
|
+
}
|
|
45
|
+
/** The member of `source`'s declaration whose directory holds `cwd`, or null. */
|
|
46
|
+
export function memberAtCwd(source, cwd) {
|
|
47
|
+
if (!source.declaration || source.rootOnDisk === null)
|
|
48
|
+
return null;
|
|
49
|
+
let rel;
|
|
50
|
+
try {
|
|
51
|
+
rel = relative(realpathSync(source.rootOnDisk), realpathSync(cwd));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
rel = relative(source.rootOnDisk, cwd);
|
|
55
|
+
}
|
|
56
|
+
if (rel.startsWith(".."))
|
|
57
|
+
return null;
|
|
58
|
+
const posix = sep === "/" ? rel : rel.split(sep).join("/");
|
|
59
|
+
return memberHolding(posix === "" ? "." : posix, source.declaration.members);
|
|
60
|
+
}
|
|
61
|
+
/** The groups `source` turns on for a command run in `cwd`: the top-level list and the list of the member holding `cwd`. */
|
|
62
|
+
export function securityGroupsIn(source, cwd) {
|
|
63
|
+
return securityGroupsOf(source.declaration, memberAtCwd(source, cwd));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The groups the workspace holding `cwd` turns on for the member holding
|
|
67
|
+
* `cwd`, read at base (the target branch tip) when there is one, else from
|
|
68
|
+
* the working tree, as the release gate is read. Empty outside a workspace.
|
|
69
|
+
*/
|
|
70
|
+
export function securityGroupsAt(cwd, at) {
|
|
71
|
+
return securityGroupsIn(scopeSource(cwd, at), cwd);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Every security feature, the group that raises it and the setting that
|
|
75
|
+
* turns it on alone. The reference page's table is printed from this list
|
|
76
|
+
* (`securityFeatureTable`), and a test compares the two.
|
|
77
|
+
*/
|
|
78
|
+
export const SECURITY_FEATURES = [
|
|
79
|
+
{ feature: "Publish refuses an unsigned release", byDefault: "off: publishes and records `signature: unsigned`", setting: "`identity.gates.release`", group: "verified-releases", codes: ["security-signers-missing", "security-publish-allows-unsigned"] },
|
|
80
|
+
{ feature: "Signed cut in the release gate's class", byDefault: "off", setting: "`identity.gates.release` with a `class`", group: "verified-releases", codes: ["security-release-gate-missing"] },
|
|
81
|
+
{ feature: "Promote verifies the release", byDefault: "off: `chant release verify` on demand", setting: "`chant release verify`", group: "verified-releases", codes: [] },
|
|
82
|
+
{ feature: "SBOM on every target", byDefault: "off: a target with no SBOM source publishes without one", setting: "`sbom` on the target", group: "supply-chain", codes: ["security-sbom-disabled", "security-sbom-unsourced"] },
|
|
83
|
+
{ feature: "OIDC provenance", byDefault: "off: a plan field", setting: "`--require-provenance`", group: "supply-chain", codes: ["security-publish-no-provenance"] },
|
|
84
|
+
{ feature: "SBOM and verify steps on components", byDefault: "off", setting: "a `generate-sbom` or `verify` step", group: "supply-chain", codes: ["security-component-no-sbom", "security-component-no-verify"] },
|
|
85
|
+
{ feature: "Signed green tags and runs", byDefault: "off: every tag counts", setting: "`ci.green.signedSince`", group: "who-ran-it", codes: ["security-signed-since-unset"] },
|
|
86
|
+
{ feature: "Trusted runner keys", byDefault: "off", setting: "`.chant/trust.json` with a runner role", group: "who-ran-it", codes: ["security-runner-untrusted"] },
|
|
87
|
+
{ feature: "Tick signs its tags", byDefault: "off: unsigned tags", setting: "`--key` or `CHANT_RUNNER_KEY`", group: "who-ran-it", codes: ["security-tick-no-key"] },
|
|
88
|
+
{ feature: "Forge review required, authors never count", byDefault: "off", setting: "`--require-review`, `requireReview: true`", group: "review-rules", codes: ["security-review-not-required"] },
|
|
89
|
+
{ feature: "Identified attribution", byDefault: "off: `any`", setting: "`identity.attribution: \"identified\"`", group: "review-rules", codes: ["security-attribution-any"] },
|
|
90
|
+
{ feature: "Signed approvals", byDefault: "off", setting: "`identity.gates.<gate>`", group: "review-rules", codes: ["security-gate-unsigned"] },
|
|
91
|
+
{ feature: "Apply jobs require a runner-start statement", byDefault: "off", setting: "`--require-runner-start`", group: "isolated-runners", codes: ["security-runner-start-no-key", "security-runner-start-no-role", "security-trust-unreadable"] },
|
|
92
|
+
{ feature: "Secrets file", byDefault: "off", setting: "`--secrets`", group: null, codes: [] },
|
|
93
|
+
{ feature: "Env allowlist and log redaction", byDefault: "on: `ci run`'s contract, a listed exception", setting: "`--pass-env`", group: null, codes: [] },
|
|
94
|
+
];
|
|
95
|
+
/** The reference page's table of {@link SECURITY_FEATURES}, as markdown. */
|
|
96
|
+
export function securityFeatureTable() {
|
|
97
|
+
const rows = SECURITY_FEATURES.map((f) => `| ${f.feature} | ${f.byDefault} | ${f.setting} | ${f.group ? `\`${f.group}\`` : "none"} | ${f.codes.map((c) => `\`${c}\``).join(", ") || "none"} |`);
|
|
98
|
+
return ["| Feature | Default | Turns it on alone | Group | Checked as |", "|---|---|---|---|---|", ...rows].join("\n");
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.34",
|
|
4
4
|
"description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"./workspace/*.schema.json": "./src/workspace/*.schema.json",
|
|
54
54
|
"./notify.schema.json": "./src/notify.schema.json",
|
|
55
|
+
"./health.schema.json": "./src/health.schema.json",
|
|
55
56
|
"./workspace/factory-rules": {
|
|
56
57
|
"development": "./src/workspace/factory-rules.ts",
|
|
57
58
|
"types": "./dist/workspace/factory-rules.d.ts",
|
|
@@ -103,6 +104,12 @@
|
|
|
103
104
|
"import": "./dist/notify-verify.js",
|
|
104
105
|
"default": "./src/notify-verify.ts"
|
|
105
106
|
},
|
|
107
|
+
"./runner-start": {
|
|
108
|
+
"development": "./src/runner-start.ts",
|
|
109
|
+
"types": "./dist/runner-start.d.ts",
|
|
110
|
+
"import": "./dist/runner-start.js",
|
|
111
|
+
"default": "./src/runner-start.ts"
|
|
112
|
+
},
|
|
106
113
|
"./notify": {
|
|
107
114
|
"development": "./src/notify.ts",
|
|
108
115
|
"types": "./dist/notify.d.ts",
|
|
@@ -18,7 +18,7 @@ A reviewer who approved this pull request approves the plan with:
|
|
|
18
18
|
chant approve pr-12 pr-apply --plan jcs1-sha256:7623a8677afb0bf98d77c602521a3abfa96f76cff25675d350a28c2e26a7bad0 --approver github:<your-login> --sign
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
On merge the plan is recorded, and a release deploys it. With apply on merge it applies if the plan is still this one; if it moved, the apply refuses and names both digests.
|
|
22
22
|
|
|
23
23
|
### Plan summary
|
|
24
24
|
|
package/src/ci-jobs.ts
CHANGED
|
@@ -417,6 +417,12 @@ export const GITLAB_SCHEDULE_SOURCE = '$CI_PIPELINE_SOURCE == "schedule"';
|
|
|
417
417
|
/** A job variable that makes the runner clone the whole history, which a job reading the base commit needs. */
|
|
418
418
|
export const GITLAB_FULL_HISTORY = { GIT_DEPTH: "0" } as const;
|
|
419
419
|
|
|
420
|
+
/** The tags a build request is pushed as (#4169), as a glob for a trigger. */
|
|
421
|
+
export const BUILD_REQUEST_TAGS = "ci/build/**";
|
|
422
|
+
|
|
423
|
+
/** The `rules:` if-expression for a build request's tag (#4169). */
|
|
424
|
+
export const GITLAB_BUILD_REQUEST_RULE = "$CI_COMMIT_TAG =~ /^ci\\/build\\//";
|
|
425
|
+
|
|
420
426
|
/** The `rules:` if-expression for a push to `branch`, a quoted name or an expression such as `$CI_DEFAULT_BRANCH`. */
|
|
421
427
|
export function gitlabPushRule(branch: string): string {
|
|
422
428
|
return `$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == ${branch}`;
|
|
@@ -462,11 +468,14 @@ export function gitlabRules(subject: string, trigger: OpTrigger, selector: { var
|
|
|
462
468
|
return { if: `${GITLAB_MERGE_REQUEST_SOURCE} && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "${branch}"` };
|
|
463
469
|
});
|
|
464
470
|
}
|
|
465
|
-
case "push":
|
|
466
|
-
|
|
471
|
+
case "push": {
|
|
472
|
+
const rules = pushBranches(trigger.branches).map((branch) => {
|
|
467
473
|
assertGitlabBranch(subject, branch);
|
|
468
474
|
return { if: gitlabPushRule(JSON.stringify(branch)) };
|
|
469
475
|
});
|
|
476
|
+
// #4169: a newest build also runs at each build request's tag.
|
|
477
|
+
return trigger.build === "newest" ? [...rules, { if: GITLAB_BUILD_REQUEST_RULE }] : rules;
|
|
478
|
+
}
|
|
470
479
|
}
|
|
471
480
|
}
|
|
472
481
|
|
|
@@ -148,6 +148,42 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
148
148
|
vi.restoreAllMocks();
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test("--pr-loop asks for --require-review only under --require-review or the review-rules security group (#4168)", async () => {
|
|
152
|
+
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
|
|
153
|
+
const { tmpdir } = await import("node:os");
|
|
154
|
+
const { join } = await import("node:path");
|
|
155
|
+
const { execFileSync } = await import("node:child_process");
|
|
156
|
+
const root = mkdtempSync(join(tmpdir(), "chant-build-review-rules-"));
|
|
157
|
+
try {
|
|
158
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
159
|
+
mkdirSync(join(root, "app"));
|
|
160
|
+
const declare = (extra: Record<string, unknown>) =>
|
|
161
|
+
writeFileSync(join(root, "chant.workspace.json"), JSON.stringify({ name: "acme", schema: 1, ...extra, members: [{ name: "app", dir: "app", kind: "chant", because: "the app" }] }));
|
|
162
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "jobs: {}", stages: [], jobs: [] });
|
|
163
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
164
|
+
const path = join(root, "app");
|
|
165
|
+
const output = join(root, "out.yml");
|
|
166
|
+
|
|
167
|
+
declare({});
|
|
168
|
+
expect(await runBuild({ args: makeArgs({ path, output, generate: "github", prLoop: true }), plugins: [], serializers: [] })).toBe(0);
|
|
169
|
+
expect(generateComponentsPipelineMock.mock.calls[0][2].prLoop).toEqual({});
|
|
170
|
+
expect(await runBuild({ args: makeArgs({ path, output, generate: "github", prLoop: true, requireReview: true }), plugins: [], serializers: [] })).toBe(0);
|
|
171
|
+
expect(generateComponentsPipelineMock.mock.calls[1][2].prLoop).toEqual({ requireReview: true });
|
|
172
|
+
|
|
173
|
+
declare({ security: ["review-rules"] });
|
|
174
|
+
expect(await runBuild({ args: makeArgs({ path, output, generate: "github", prLoop: true }), plugins: [], serializers: [] })).toBe(0);
|
|
175
|
+
expect(generateComponentsPipelineMock.mock.calls[2][2].prLoop).toEqual({ requireReview: true });
|
|
176
|
+
|
|
177
|
+
// #4168 step 7: a member's own group counts, and isolated-runners asks for --require-runner-start.
|
|
178
|
+
writeFileSync(join(root, "chant.workspace.json"), JSON.stringify({ name: "acme", schema: 1, members: [{ name: "app", dir: "app", kind: "chant", because: "the app", security: ["isolated-runners"] }] }));
|
|
179
|
+
expect(await runBuild({ args: makeArgs({ path, output, generate: "github", prLoop: true }), plugins: [], serializers: [] })).toBe(0);
|
|
180
|
+
expect(generateComponentsPipelineMock.mock.calls[3][2].prLoop).toEqual({ requireRunnerStart: true });
|
|
181
|
+
} finally {
|
|
182
|
+
rmSync(root, { recursive: true, force: true });
|
|
183
|
+
vi.restoreAllMocks();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
151
187
|
test("--pr-loop without --report-url bakes the workspace's ci.reportUrl into the jobs, and the flag wins (#4160)", async () => {
|
|
152
188
|
const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
|
|
153
189
|
const { tmpdir } = await import("node:os");
|
|
@@ -17,8 +17,12 @@ import type { PrLoopPipelineOptions } from "../../components/pr-pipeline";
|
|
|
17
17
|
* flags the generated jobs pass to pr-plan and pr-apply. `--pr-build` and
|
|
18
18
|
* `--pr-build-command` (#4088) add the build and lint steps before both.
|
|
19
19
|
* `reportUrl` is `--report-url`, else the workspace's `ci.reportUrl` (#4160).
|
|
20
|
+
* `requireReview` is `--require-review`, or a workspace that opts into the
|
|
21
|
+
* `review-rules` security group (#4168); off otherwise. `requireRunnerStart`
|
|
22
|
+
* is `--require-runner-start`, or the `isolated-runners` group. `--apply-on-merge`
|
|
23
|
+
* (#4168) writes the apply job on merge in place of the record job.
|
|
20
24
|
*/
|
|
21
|
-
function prLoopOption(args: CommandContext["args"], reportUrl: string | undefined): PrLoopPipelineOptions {
|
|
25
|
+
function prLoopOption(args: CommandContext["args"], reportUrl: string | undefined, requireReview = false, requireRunnerStart = false): PrLoopPipelineOptions {
|
|
22
26
|
return {
|
|
23
27
|
...(args.gate ? { gate: args.gate } : {}),
|
|
24
28
|
...(args.branch ? { branch: args.branch } : {}),
|
|
@@ -26,6 +30,9 @@ function prLoopOption(args: CommandContext["args"], reportUrl: string | undefine
|
|
|
26
30
|
...(reportUrl ? { reportUrl } : {}),
|
|
27
31
|
...(args.prBuild ? { build: true } : {}),
|
|
28
32
|
...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
|
|
33
|
+
...(args.requireReview === true || requireReview ? { requireReview: true } : {}),
|
|
34
|
+
...(args.requireRunnerStart === true || requireRunnerStart ? { requireRunnerStart: true } : {}),
|
|
35
|
+
...(args.applyOnMerge ? { applyOnMerge: true } : {}),
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -102,6 +109,14 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
102
109
|
return 1;
|
|
103
110
|
}
|
|
104
111
|
}
|
|
112
|
+
// #4168: the review-rules group turns on --require-review in the generated apply. Read only inside a workspace.
|
|
113
|
+
let reviewRules = false;
|
|
114
|
+
let isolatedRunners = false;
|
|
115
|
+
if (found && args.prLoop) {
|
|
116
|
+
const groups = (await import("../../workspace/security")).securityGroupsAt(resolve(args.path));
|
|
117
|
+
reviewRules = groups.has("review-rules");
|
|
118
|
+
isolatedRunners = groups.has("isolated-runners");
|
|
119
|
+
}
|
|
105
120
|
if (found) {
|
|
106
121
|
memberPipeline = await import("../../workspace/member-pipeline");
|
|
107
122
|
try {
|
|
@@ -116,7 +131,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
116
131
|
params: args.param,
|
|
117
132
|
paramsFile: args.paramsFile,
|
|
118
133
|
...(args.waveGate ? { waveGate: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } } : {}),
|
|
119
|
-
...(args.prLoop ? { prLoop: prLoopOption(args, reportUrl) } : {}),
|
|
134
|
+
...(args.prLoop ? { prLoop: prLoopOption(args, reportUrl, reviewRules, isolatedRunners) } : {}),
|
|
120
135
|
...(args.image ? { image: args.image } : {}),
|
|
121
136
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
122
137
|
});
|
|
@@ -146,7 +161,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
146
161
|
? { gatedWaves: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } }
|
|
147
162
|
: {}),
|
|
148
163
|
// #3183: a plan job per pull request and an apply job per merge.
|
|
149
|
-
...(args.prLoop ? { prLoop: prLoopOption(args, reportUrl) } : {}),
|
|
164
|
+
...(args.prLoop ? { prLoop: prLoopOption(args, reportUrl, reviewRules, isolatedRunners) } : {}),
|
|
150
165
|
// #4002: the job image and the lines that install what it lacks, chant itself included.
|
|
151
166
|
...(args.image ? { image: args.image } : {}),
|
|
152
167
|
...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { createMockPlugin } from "@intentius/chant-test-utils";
|
|
3
|
+
import type { BuildResult } from "../../build";
|
|
4
|
+
import type { LexiconPlugin } from "../../lexicon";
|
|
5
|
+
import type { ParsedArgs } from "../registry";
|
|
6
|
+
import { HEALTH_SCHEMA_ID } from "../../health";
|
|
7
|
+
import { UNOBSERVED_REASONS } from "../../observation";
|
|
8
|
+
import { contract, validSchema } from "../../workspace/__fixtures__/contract-repo";
|
|
9
|
+
import schema from "../../health.schema.json";
|
|
10
|
+
|
|
11
|
+
const { expectValid, validate } = contract(schema);
|
|
12
|
+
|
|
13
|
+
const buildMock = vi.fn();
|
|
14
|
+
const loadChantConfigMock = vi.fn();
|
|
15
|
+
vi.mock("../../build", () => ({ build: (...args: unknown[]) => buildMock(...args) }));
|
|
16
|
+
vi.mock("../../config", async () => {
|
|
17
|
+
const actual = await vi.importActual<typeof import("../../config")>("../../config");
|
|
18
|
+
return { ...actual, loadChantConfig: (...args: unknown[]) => loadChantConfigMock(...args) };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const { runLifecycleHealth } = await import("./lifecycle-health");
|
|
22
|
+
|
|
23
|
+
function args(overrides: Partial<ParsedArgs> = {}): ParsedArgs {
|
|
24
|
+
return { command: "lifecycle", path: "health", format: "", fix: false, watch: false, verbose: false, help: false, extraPositional: "prod", ...overrides } as ParsedArgs;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildResult(entities: Array<[string, string, string]>): BuildResult {
|
|
28
|
+
return {
|
|
29
|
+
outputs: new Map([["aws", "{}"]]),
|
|
30
|
+
entities: new Map(entities.map(([name, lexicon, entityType]) => [name, { lexicon, entityType, kind: "resource", props: {} }])),
|
|
31
|
+
dependencies: new Map(),
|
|
32
|
+
errors: [],
|
|
33
|
+
warnings: [],
|
|
34
|
+
manifest: { lexicons: ["aws"], outputs: {}, deployOrder: ["aws"] },
|
|
35
|
+
sourceFileCount: 1,
|
|
36
|
+
} as unknown as BuildResult;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("chant lifecycle health (#4177)", () => {
|
|
40
|
+
let out: string[];
|
|
41
|
+
let err: string[];
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
out = [];
|
|
44
|
+
err = [];
|
|
45
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => { out.push(s); });
|
|
46
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => { err.push(s); });
|
|
47
|
+
loadChantConfigMock.mockResolvedValue({ config: { environments: ["prod"] } });
|
|
48
|
+
buildMock.mockReset();
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => vi.restoreAllMocks());
|
|
51
|
+
|
|
52
|
+
test("asks only about the types the lexicon names, and prints chant-health/v1", async () => {
|
|
53
|
+
buildMock.mockResolvedValue(buildResult([["web", "aws", "AWS::ECS::Service"], ["bucket", "aws", "AWS::S3::Bucket"]]));
|
|
54
|
+
const asked: string[][] = [];
|
|
55
|
+
const plugin: LexiconPlugin = {
|
|
56
|
+
...createMockPlugin({ name: "aws" }),
|
|
57
|
+
healthTypes: ["AWS::ECS::Service"],
|
|
58
|
+
observeHealth: async (o) => {
|
|
59
|
+
asked.push([...o.entities.keys()]);
|
|
60
|
+
return { entities: { web: { type: "AWS::ECS::Service", state: "unhealthy", detail: "0/2 running", observedAt: "t" } }, unobserved: {}, absent: [] };
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const exit = await runLifecycleHealth({ args: args({ json: true }), plugins: [plugin], serializers: [plugin.serializer] });
|
|
64
|
+
expect(exit).toBe(0);
|
|
65
|
+
expect(asked).toEqual([["web"]]);
|
|
66
|
+
const json = JSON.parse(out[0]!);
|
|
67
|
+
expect(json.schema).toBe("chant-health/v1");
|
|
68
|
+
expect(typeof json.chantVersion).toBe("string");
|
|
69
|
+
expect(json.lexicons.aws.entities.web.state).toBe("unhealthy");
|
|
70
|
+
expect(json.summary).toEqual({ healthy: 0, unhealthy: 1, unknown: 0, unobserved: 0, absent: 0 });
|
|
71
|
+
expectValid(json);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a reader that throws is not read, never unhealthy", async () => {
|
|
75
|
+
buildMock.mockResolvedValue(buildResult([["web", "aws", "AWS::ECS::Service"]]));
|
|
76
|
+
const plugin: LexiconPlugin = {
|
|
77
|
+
...createMockPlugin({ name: "aws" }),
|
|
78
|
+
healthTypes: ["AWS::ECS::Service"],
|
|
79
|
+
observeHealth: async () => {
|
|
80
|
+
throw new Error("no route to host");
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
await runLifecycleHealth({ args: args(), plugins: [plugin], serializers: [plugin.serializer] });
|
|
84
|
+
expect(out.join("\n")).toContain("not read web (AWS::ECS::Service): read-failed, no route to host");
|
|
85
|
+
expect(err.join("\n")).toContain("0 healthy, 0 unhealthy, 0 unknown, 1 not observed");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("refuses with no environment, and with no lexicon that reads health", async () => {
|
|
89
|
+
const plain = createMockPlugin({ name: "aws" });
|
|
90
|
+
expect(await runLifecycleHealth({ args: args({ extraPositional: undefined }), plugins: [plain], serializers: [] })).toBe(1);
|
|
91
|
+
expect(await runLifecycleHealth({ args: args(), plugins: [plain], serializers: [] })).toBe(1);
|
|
92
|
+
expect(err.join("\n")).toContain("No configured lexicon has a health read");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("the JSON of a read that failed follows the schema", async () => {
|
|
96
|
+
buildMock.mockResolvedValue(buildResult([["web", "aws", "AWS::ECS::Service"], ["gone", "aws", "AWS::ECS::Service"]]));
|
|
97
|
+
const plugin: LexiconPlugin = {
|
|
98
|
+
...createMockPlugin({ name: "aws" }),
|
|
99
|
+
healthTypes: ["AWS::ECS::Service"],
|
|
100
|
+
observeHealth: async () => ({ entities: {}, unobserved: { web: { type: "AWS::ECS::Service", reason: "no-credentials", detail: "no AWS credentials" } }, absent: ["gone"] }),
|
|
101
|
+
};
|
|
102
|
+
await runLifecycleHealth({ args: args({ json: true }), plugins: [plugin], serializers: [plugin.serializer] });
|
|
103
|
+
const json = JSON.parse(out[0]!);
|
|
104
|
+
expect(json.summary).toEqual({ healthy: 0, unhealthy: 0, unknown: 0, unobserved: 1, absent: 1 });
|
|
105
|
+
expectValid(json);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("the chant-health/v1 schema (#4177)", () => {
|
|
110
|
+
test("is a valid schema with the exported id, and names every unobserved reason", () => {
|
|
111
|
+
expect(validSchema(schema)).toBe(true);
|
|
112
|
+
expect(schema.$id).toBe(HEALTH_SCHEMA_ID);
|
|
113
|
+
expect([...schema.$defs.hole.properties.reason.enum].sort()).toEqual([...UNOBSERVED_REASONS].sort());
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a document with neither lexicons nor stacks, or a state outside the three, does not validate", () => {
|
|
117
|
+
const base = { schema: "chant-health/v1", chantVersion: "0.124.33", environment: "prod", observedAt: "t", summary: { healthy: 0, unhealthy: 0, unknown: 0, unobserved: 0, absent: 0 } };
|
|
118
|
+
expect(validate(base)).toBe(false);
|
|
119
|
+
expectValid({ ...base, stacks: { east: {} } });
|
|
120
|
+
const bad = { ...base, lexicons: { aws: { entities: { web: { type: "AWS::ECS::Service", state: "down", detail: "", observedAt: "t" } }, unobserved: {}, absent: [] } } };
|
|
121
|
+
expect(validate(bad)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|