@intentius/chant 0.124.32 → 0.124.33
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 +16 -1
- 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 +81 -21
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +23 -1
- package/dist/cli/registry.d.ts +18 -4
- 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/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-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-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 +235 -228
- package/dist/workspace/check.schema.json +70 -2
- package/dist/workspace/checks/security.d.ts +35 -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 +5 -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-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/declaration.d.ts +17 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +3 -0
- package/dist/workspace/declaration.schema.json +23 -0
- 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 +17 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +18 -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/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 +16 -1
- 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 +81 -21
- package/src/cli/parse-args.ts +17 -1
- package/src/cli/registry.ts +18 -4
- 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/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- 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-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/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +70 -2
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +5 -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-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -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/declaration.schema.json +23 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +22 -1
- 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 +2 -0
- package/src/workspace/reason-codes.ts +18 -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/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
package/src/cli/main.ts
CHANGED
|
@@ -249,22 +249,28 @@ CI:
|
|
|
249
249
|
chant/lifecycle; with ci.green.signedSince, the ones a
|
|
250
250
|
runner key signed. Pushes the
|
|
251
251
|
tags. --key, or the PEM in CHANT_RUNNER_KEY, signs
|
|
252
|
-
each tag with a runner key.
|
|
252
|
+
each tag with a runner key. A cancelled run leaves a
|
|
253
|
+
commit unbuilt, not red, and a failed run is run
|
|
254
|
+
again once. A phase with build: newest leaves the
|
|
255
|
+
commits a newer run superseded unbuilt, and a red
|
|
256
|
+
one starts a bisect: a ci/build/<sha> tag the tick
|
|
257
|
+
starts the phase's workflow at. Each commit judged
|
|
258
|
+
gets a _ci/<sha>.json record on chant/lifecycle.
|
|
253
259
|
ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>]
|
|
254
260
|
[--forge github|gitlab|forgejo] [--gate <name>] [--wave-gate <name>]
|
|
255
261
|
[--branch <name>] [--events-json] [--report-url <url>] [--image <image>]
|
|
256
262
|
[--before-script <line>]... [--pr-build]
|
|
257
263
|
[--pr-build-command <line>] [--chant <command>]
|
|
258
264
|
[--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>]
|
|
259
|
-
[-o <dir>] [--wave <k>] [--
|
|
265
|
+
[-o <dir>] [--wave <k>] [--require-review]
|
|
260
266
|
[--no-status] [--status-url <url template>]
|
|
261
|
-
[--in-place | --local] [--no-push] [--verbose]
|
|
262
|
-
Run the pull-request pipeline's plan or apply job as
|
|
267
|
+
[--in-place | --local] [--no-push] [--require-runner-start] [--verbose]
|
|
268
|
+
Run the pull-request pipeline's plan, record or apply job as
|
|
263
269
|
a pipeline Op in a fresh worktree of --commit (default
|
|
264
270
|
HEAD) with a clean environment. --base and --pr stand
|
|
265
271
|
in for the forge's event values. --pr-build runs
|
|
266
272
|
the job's build and lint lines first. Records the run as
|
|
267
|
-
pr-plan or pr-apply on chant/lifecycle, which ci tick
|
|
273
|
+
pr-plan, pr-record or pr-apply on chant/lifecycle, which ci tick
|
|
268
274
|
from runs counts, signed when --key or CHANT_RUNNER_KEY
|
|
269
275
|
gives a runner key, and copies the report to -o
|
|
270
276
|
(default .chant/pr). --in-place runs in this checkout
|
|
@@ -286,17 +292,39 @@ CI:
|
|
|
286
292
|
kept on the local chant/lifecycle with runOrigin
|
|
287
293
|
local and is never pushed, no status is set, nothing
|
|
288
294
|
goes under a report URL, and ci tick does not count it.
|
|
295
|
+
--require-runner-start refuses the job before its
|
|
296
|
+
first step unless a runner-start statement, signed by
|
|
297
|
+
a runner key .chant/trust.json at --base lists, names
|
|
298
|
+
this forge job.
|
|
299
|
+
ci runner-start sign --key <runner key.pem> --principal <name> --job <json|@file> [-o <file>]
|
|
300
|
+
For the provisioner that starts a runner for one job:
|
|
301
|
+
sign the job's system, repo, run, attempt, job,
|
|
302
|
+
commit and runner name, and print the envelope.
|
|
303
|
+
ci runner-start verify [--base <rev>] [--commit <sha>] [--json]
|
|
304
|
+
In the job: check the statement in CHANT_RUNNER_START,
|
|
305
|
+
or the file CHANT_RUNNER_START_FILE names (default
|
|
306
|
+
/run/chant/runner-start.json), against the forge's
|
|
307
|
+
variables and .chant/trust.json at --base (default
|
|
308
|
+
origin/HEAD). Exits 1 unless it verifies.
|
|
309
|
+
ci run --at <sha> Ask for every required phase at a commit of the
|
|
310
|
+
green branch with a ci/build/<sha> tag, and start the
|
|
311
|
+
workflows holding them there on GitHub when a token
|
|
312
|
+
is set; otherwise the next tick, or ci serve
|
|
313
|
+
--build-command, does.
|
|
289
314
|
ci runs [--report-url <url>] [--json]
|
|
290
315
|
List the runs in the index at the report URL (default
|
|
291
316
|
the workspace's ci.reportUrl), newest first.
|
|
292
317
|
ci serve --forge forgejo|github --repo <owner/repo> --env <env>
|
|
293
318
|
[--interval <s>] [--max-jobs <n>] [--state <dir>]
|
|
294
319
|
[--author <login>]... [--once] [--rerun-failed] [--unsigned]
|
|
295
|
-
[ci run's job flags]
|
|
320
|
+
[--apply-on-merge] [--build-command <line>] [ci run's job flags]
|
|
296
321
|
Poll the forge for open pull requests and the target
|
|
297
322
|
branch's head, and run ci run plan for each new pull
|
|
298
|
-
request head and ci run
|
|
299
|
-
the branch
|
|
323
|
+
request head and ci run record for each new commit on
|
|
324
|
+
the branch, then ci tick from the runs when the
|
|
325
|
+
workspace declares ci.green. A merge deploys nothing;
|
|
326
|
+
--apply-on-merge runs ci run apply in place of the
|
|
327
|
+
record, and --wave-gate needs it. Dedupes on job and commit in a state file
|
|
300
328
|
under --state, takes a lease per apply group, keeps
|
|
301
329
|
each apply's resume record there, and runs at most
|
|
302
330
|
--max-jobs (default 1) jobs at once. --once runs one
|
|
@@ -304,7 +332,10 @@ CI:
|
|
|
304
332
|
failed apply or plan again. Signs runs with a runner
|
|
305
333
|
key when .chant/trust.json at the target's head lists
|
|
306
334
|
it, and serves unsigned otherwise; with
|
|
307
|
-
ci.green.signedSince it refuses to start without one
|
|
335
|
+
ci.green.signedSince it refuses to start without one.
|
|
336
|
+
--build-command runs <line> once per ci/build/<sha>
|
|
337
|
+
tag, in a fresh worktree of the commit, with
|
|
338
|
+
CHANT_BUILD_COMMIT and CHANT_BUILD_PHASES set
|
|
308
339
|
ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]
|
|
309
340
|
Make the host's runner key (Ed25519, mode 0600) in the
|
|
310
341
|
state directory when there is none and print its public
|
|
@@ -322,10 +353,14 @@ CI:
|
|
|
322
353
|
or a GitHub App's id and key, pushes the tags with a
|
|
323
354
|
token that may tag commits changing workflow files.
|
|
324
355
|
--runner-key-secret hands the tick the runner key it
|
|
325
|
-
signs its tags with.
|
|
356
|
+
signs its tags with. For each phase that sets build,
|
|
357
|
+
it also writes the triggers and concurrency of the
|
|
358
|
+
workflows holding it: newest builds on pushes and by
|
|
359
|
+
hand, one run at a time, and every gives each commit
|
|
360
|
+
a run of its own.
|
|
326
361
|
|
|
327
362
|
Releases:
|
|
328
|
-
release cut [<label>] [--yes] [--build <command>] [--param <k=v>]
|
|
363
|
+
release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>]
|
|
329
364
|
[--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>]
|
|
330
365
|
[--actor <name>] [--sign [<key file>]] [--json]
|
|
331
366
|
A dry run of a cut of this member from HEAD, which
|
|
@@ -344,19 +379,29 @@ Releases:
|
|
|
344
379
|
requires it. A project with no components releases
|
|
345
380
|
what chant build --env <env> writes: --src <dir> is
|
|
346
381
|
the build root, -o <file> the output (dist/build.yaml),
|
|
347
|
-
--lexicon narrows it.
|
|
382
|
+
--lexicon narrows it. --at <sha> cuts that commit of
|
|
383
|
+
trunk from a temporary worktree instead of HEAD, so
|
|
384
|
+
the checkout need not be clean. The label reaches
|
|
385
|
+
every compile and build as CHANT_RELEASE_LABEL.
|
|
386
|
+
release candidates [--env <env>] [--json]
|
|
387
|
+
The green commits of trunk a release can be cut from,
|
|
388
|
+
newest first, each with the commits, work items, pull
|
|
389
|
+
requests and flags it would ship, and what each
|
|
390
|
+
environment runs.
|
|
348
391
|
release publish <label|digest> [--yes] [--allow-unsigned]
|
|
349
392
|
[--require-provenance] [--json]
|
|
350
393
|
Publish a cut release to the targets release.publish
|
|
351
394
|
in chant.config.ts declares (npm, OCI registries,
|
|
352
|
-
forge release assets), each with
|
|
353
|
-
|
|
354
|
-
Refuses a revoked release,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
395
|
+
forge release assets), each with the release's
|
|
396
|
+
provenance and an SBOM when it has a source. A dry
|
|
397
|
+
run without --yes. Refuses a revoked release, and
|
|
398
|
+
with a target a checkout that is not the release's
|
|
399
|
+
commit. Under security verified-releases it refuses
|
|
400
|
+
one whose signature does not verify against
|
|
401
|
+
.chant/allowed_signers unless --allow-unsigned; under
|
|
402
|
+
supply-chain it warns when the run has no OIDC token
|
|
403
|
+
and refuses a target with no SBOM source.
|
|
404
|
+
--require-provenance refuses a run with no OIDC.
|
|
360
405
|
release verify <label|digest> [--json]
|
|
361
406
|
Check, offline, that the release was signed by a key
|
|
362
407
|
.chant/allowed_signers lists at the release's commit.
|
|
@@ -887,6 +932,10 @@ Lifecycle (alias: lc):
|
|
|
887
932
|
estate size rather than stack count (#2405)
|
|
888
933
|
lifecycle affected Stacks a change affects (--base <ref> [--include-dependents])
|
|
889
934
|
--json: emit the ChangeSet as JSON
|
|
935
|
+
lifecycle health <env> Whether each declared service is up, separate from
|
|
936
|
+
drift: ECS running vs desired, Route 53 health
|
|
937
|
+
checks, Container Apps revisions, Cloud Run Ready.
|
|
938
|
+
A failed read is "not read", never unhealthy (--json)
|
|
890
939
|
lifecycle whoami <env> Who chant would act as in each configured lexicon,
|
|
891
940
|
and what that principal is scoped to — read-only,
|
|
892
941
|
before anything acts (--json, --strict)
|
|
@@ -931,7 +980,12 @@ Component release ledger + status:
|
|
|
931
980
|
report and note (--base <ref> --pr <n>
|
|
932
981
|
[--forge github|gitlab|forgejo] posts them);
|
|
933
982
|
applies nothing
|
|
934
|
-
components pr-
|
|
983
|
+
components pr-record On merge (#4168): plan the same members again,
|
|
984
|
+
store the change set and write the merge record
|
|
985
|
+
_merges/<commit>.json; applies and refuses
|
|
986
|
+
nothing, and a plan that differs from the
|
|
987
|
+
approved one sets a failing chant/record status
|
|
988
|
+
components pr-apply With --apply-on-merge: plan the same members again and apply
|
|
935
989
|
them only if an approval stands for that digest
|
|
936
990
|
(chant approve pr-<n> pr-apply --plan <digest>);
|
|
937
991
|
a moved plan refuses with exit 3
|
|
@@ -1386,6 +1440,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1386
1440
|
// #4094: one pull-request job as a pipeline Op in a fresh checkout, the chant runner's first slice.
|
|
1387
1441
|
{ name: "ci run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run")).runCiRun(ctx) },
|
|
1388
1442
|
// #4098: the run index at the report URL.
|
|
1443
|
+
// #4147: a provisioner's signed word that it started a runner for one job, signed and checked.
|
|
1444
|
+
{ name: "ci runner-start", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-runner-start")).runCiRunnerStart(ctx) },
|
|
1389
1445
|
{ name: "ci runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run-index")).runCiRuns(ctx) },
|
|
1390
1446
|
{ name: "ci serve", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-serve")).runCiServe(ctx) },
|
|
1391
1447
|
// #4026: the release cut from a green commit (ws-104). Imported on first use.
|
|
@@ -1394,6 +1450,7 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1394
1450
|
{ name: "release publish", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleasePublish(ctx) },
|
|
1395
1451
|
// #4049: check a release's signature offline.
|
|
1396
1452
|
{ name: "release verify", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseVerify(ctx) },
|
|
1453
|
+
{ name: "release candidates", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCandidates(ctx) },
|
|
1397
1454
|
{ name: "release list", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseList(ctx) },
|
|
1398
1455
|
{ name: "release export", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseExport(ctx) },
|
|
1399
1456
|
{ name: "release compile", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCompile(ctx) },
|
|
@@ -1504,6 +1561,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1504
1561
|
{ name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
|
|
1505
1562
|
{ name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
|
|
1506
1563
|
{ name: "lifecycle whoami", requiresPlugins: true, handler: runLifecycleWhoami },
|
|
1564
|
+
// #4177 — runtime health, separate from drift. Imported on first use.
|
|
1565
|
+
{ name: "lifecycle health", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/lifecycle-health")).runLifecycleHealth(ctx) },
|
|
1507
1566
|
{ name: "lifecycle teardown", requiresPlugins: true, handler: runLifecycleTeardown },
|
|
1508
1567
|
{ name: "lifecycle log", handler: runLifecycleLog },
|
|
1509
1568
|
// #4036 — retention's garbage collection. Imported on first use.
|
|
@@ -1519,6 +1578,7 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1519
1578
|
// #3183 — the pull-request loop the generated CI runs. Imported on first use.
|
|
1520
1579
|
{ name: "components pr-plan", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPlan(ctx) },
|
|
1521
1580
|
{ name: "components pr-apply", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrApply(ctx) },
|
|
1581
|
+
{ name: "components pr-record", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrRecord(ctx) },
|
|
1522
1582
|
{ name: "components status", requiresPlugins: true, handler: runComponentsStatus },
|
|
1523
1583
|
{ name: "components release", handler: runComponentsReleaseRecord },
|
|
1524
1584
|
{ name: "components export", handler: runComponentsExport },
|
package/src/cli/parse-args.ts
CHANGED
|
@@ -70,6 +70,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
70
70
|
"--trust",
|
|
71
71
|
"--unsigned",
|
|
72
72
|
"--in-place",
|
|
73
|
+
"--require-runner-start",
|
|
73
74
|
"--no-push",
|
|
74
75
|
"--no-require-review",
|
|
75
76
|
"--expire",
|
|
@@ -409,9 +410,17 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
409
410
|
result.requireProvenance = true;
|
|
410
411
|
} else if (arg === "--pr-loop") {
|
|
411
412
|
result.prLoop = true;
|
|
413
|
+
} else if (arg === "--apply-on-merge") {
|
|
414
|
+
// `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
|
|
415
|
+
result.applyOnMerge = true;
|
|
412
416
|
} else if (arg === "--pr-build") {
|
|
413
417
|
// `chant build --pr-loop --pr-build` (#4088), and `chant ci run --pr-build` (#4094).
|
|
414
418
|
result.prBuild = true;
|
|
419
|
+
} else if (arg === "--build-command") {
|
|
420
|
+
// `chant ci serve --build-command <line>` (#4169): the line that builds a requested commit.
|
|
421
|
+
const line = args[++i];
|
|
422
|
+
if (line === undefined || line === "") throw new Error("--build-command needs a shell line: --build-command '<line>'");
|
|
423
|
+
result.ciBuildCommand = line;
|
|
415
424
|
} else if (arg === "--pr-build-command") {
|
|
416
425
|
const line = args[++i];
|
|
417
426
|
if (line === undefined || line === "") throw new Error("--pr-build-command needs a shell line: --pr-build-command '<line>'");
|
|
@@ -783,11 +792,18 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
783
792
|
} else if (arg === "--in-place") {
|
|
784
793
|
// `chant ci run --in-place` (#4156): run the job in this checkout, as a generated forge job does.
|
|
785
794
|
result.ciInPlace = true;
|
|
795
|
+
} else if (arg === "--require-runner-start") {
|
|
796
|
+
// `chant ci run --require-runner-start` (#4147): refuse the job unless a provisioner's runner-start statement names it.
|
|
797
|
+
result.requireRunnerStart = true;
|
|
798
|
+
} else if (arg === "--job") {
|
|
799
|
+
// `chant ci runner-start sign --job <json|@file>` (#4147): the CI job a runner was started for.
|
|
800
|
+
result.ciJob = args[++i];
|
|
801
|
+
if (!result.ciJob || result.ciJob.startsWith("-")) throw new Error("--job needs the job as JSON or @<file>: --job @job.json");
|
|
786
802
|
} else if (arg === "--no-push") {
|
|
787
803
|
// `chant ci run --no-push` (#4156): keep the run record on the local chant/lifecycle.
|
|
788
804
|
result.noPush = true;
|
|
789
805
|
} else if (arg === "--no-require-review") {
|
|
790
|
-
// `chant ci run apply --no-require-review` (#4156): the apply without pr-apply's --require-review.
|
|
806
|
+
// `chant ci run apply --no-require-review` (#4156): the apply without pr-apply's --require-review, the default since #4168.
|
|
791
807
|
result.requireReview = false;
|
|
792
808
|
} else if (arg === "--unsigned") {
|
|
793
809
|
// `chant ci serve --unsigned` (#4099): serve without a runner key listed at the target's head.
|
package/src/cli/registry.ts
CHANGED
|
@@ -311,8 +311,10 @@ export interface ParsedArgs {
|
|
|
311
311
|
/**
|
|
312
312
|
* `chant components pr-apply --require-review` (#3183) — count an approval
|
|
313
313
|
* only when its approver has a standing approving review on the pull
|
|
314
|
-
* request. Needs `--forge`. `chant ci run apply --
|
|
315
|
-
*
|
|
314
|
+
* request. Needs `--forge`. `chant ci run apply --require-review`
|
|
315
|
+
* passes it on, and `chant build --pr-loop --require-review` generates a
|
|
316
|
+
* pipeline that does (#4168); `--no-require-review` (#4156) sets it false,
|
|
317
|
+
* which is the default.
|
|
316
318
|
*/
|
|
317
319
|
requireReview?: boolean;
|
|
318
320
|
/**
|
|
@@ -323,12 +325,20 @@ export interface ParsedArgs {
|
|
|
323
325
|
reportUrl?: string;
|
|
324
326
|
/**
|
|
325
327
|
* `chant build --components --generate <lexicon> --pr-loop` (#3183) —
|
|
326
|
-
* generate the pull-request pipeline: plan on the pull request,
|
|
327
|
-
* merge
|
|
328
|
+
* generate the pull-request pipeline: plan on the pull request, and on
|
|
329
|
+
* merge record the plan (#4168), or apply it with `--apply-on-merge`.
|
|
328
330
|
*/
|
|
329
331
|
prLoop?: boolean;
|
|
332
|
+
/**
|
|
333
|
+
* `chant build --pr-loop --apply-on-merge` and `chant ci serve
|
|
334
|
+
* --apply-on-merge` (#4168): on merge apply the approved plan, as before
|
|
335
|
+
* #4168, in place of recording it. Opt-in: a merge deploys nothing without it.
|
|
336
|
+
*/
|
|
337
|
+
applyOnMerge?: boolean;
|
|
330
338
|
/** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
|
|
331
339
|
prBuild?: boolean;
|
|
340
|
+
/** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
|
|
341
|
+
ciBuildCommand?: string;
|
|
332
342
|
/** `chant build --pr-loop --pr-build-command <line>` (#4088) — the build line to run in place of `chant build --env <env>`. */
|
|
333
343
|
prBuildCommand?: string;
|
|
334
344
|
/**
|
|
@@ -511,6 +521,10 @@ export interface ParsedArgs {
|
|
|
511
521
|
unsigned?: boolean;
|
|
512
522
|
/** `chant ci run --in-place` (#4156): run the job in this checkout at HEAD, as a generated forge job does. */
|
|
513
523
|
ciInPlace?: boolean;
|
|
524
|
+
/** `chant ci run --require-runner-start` (#4147): refuse the job unless a runner-start statement names it. */
|
|
525
|
+
requireRunnerStart?: boolean;
|
|
526
|
+
/** `chant ci runner-start sign --job <json|@file>` (#4147): the CI job a runner was started for. */
|
|
527
|
+
ciJob?: string;
|
|
514
528
|
/** `chant ci run --no-push` (#4156): write the run record on the local chant/lifecycle and push nothing. */
|
|
515
529
|
noPush?: boolean;
|
|
516
530
|
/** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
|
+
import { componentDeployInputs, directoryDigest, directoryFiles, stepInputRefs } from "./deploy-inputs";
|
|
7
|
+
import { componentPlanDigest } from "./gate-plan";
|
|
8
|
+
import type { DriverComponent, DriverPhase } from "./driver";
|
|
9
|
+
|
|
10
|
+
let root: string;
|
|
11
|
+
|
|
12
|
+
const write = (path: string, content: string): void => {
|
|
13
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
14
|
+
writeFileSync(join(root, path), content);
|
|
15
|
+
};
|
|
16
|
+
const deploy = (...steps: Array<Record<string, unknown>>): DriverPhase[] => [{ phase: "Deploy", steps: steps as never }];
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
root = mkdtempSync(join(tmpdir(), "deploy-inputs-"));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
rmSync(root, { recursive: true, force: true });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("stepInputRefs", () => {
|
|
27
|
+
test("a kind's own fields and the template fields, literal paths only", () => {
|
|
28
|
+
const refs = stepInputRefs("Deploy", { kind: "helm-upgrade", release: "web", chart: "chart", values: ["a.yaml", "@Cfg.values"] } as never);
|
|
29
|
+
expect(refs.map((r) => [r.field, r.path])).toEqual([["values", "a.yaml"], ["chart", "chart"]]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("an archive path or a wired value is a build output and is not named", () => {
|
|
33
|
+
expect(stepInputRefs("Deploy", { kind: "cfn-deploy", template: "archive:stack.json" } as never)).toEqual([]);
|
|
34
|
+
expect(stepInputRefs("Deploy", { kind: "s3-sync", from: "@Build.dir", to: "s3://b" } as never)).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a Docker build's Dockerfile is named under its context", () => {
|
|
38
|
+
const refs = stepInputRefs("Build", { kind: "docker-build", context: "app", dockerfile: "Dockerfile.prod" } as never);
|
|
39
|
+
expect(refs.map((r) => r.path)).toEqual(["app", "app/Dockerfile.prod"]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("componentDeployInputs", () => {
|
|
44
|
+
test("a file by its bytes, a directory by its files; a missing path is left out", () => {
|
|
45
|
+
write("values.yaml", "a: 1\n");
|
|
46
|
+
write("chart/Chart.yaml", "name: c\n");
|
|
47
|
+
const read = componentDeployInputs(deploy({ kind: "helm-upgrade", chart: "chart", values: ["values.yaml", "gone.yaml"] }), root);
|
|
48
|
+
expect(read.inputs.map((i) => i.path)).toEqual(["chart", "values.yaml"]);
|
|
49
|
+
expect(read.inputs.find((i) => i.path === "values.yaml")?.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
50
|
+
expect(read.unbound).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a directory's digest moves with a file's bytes, a new file and a renamed one", () => {
|
|
54
|
+
write("site/index.html", "<h1>a</h1>");
|
|
55
|
+
const before = directoryDigest(root, "site");
|
|
56
|
+
write("site/index.html", "<h1>b</h1>");
|
|
57
|
+
const edited = directoryDigest(root, "site");
|
|
58
|
+
expect(edited).not.toBe(before);
|
|
59
|
+
write("site/about.html", "x");
|
|
60
|
+
expect(directoryDigest(root, "site")).not.toBe(edited);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("the same files under another parent hash the same: the digest reads paths from the directory", () => {
|
|
64
|
+
write("a/chart/Chart.yaml", "name: c\n");
|
|
65
|
+
write("b/chart/Chart.yaml", "name: c\n");
|
|
66
|
+
expect(directoryDigest(root, "a/chart")).toBe(directoryDigest(root, "b/chart"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("in a git repository, a directory leaves out what git ignores, and a directory git ignores is a build output", () => {
|
|
70
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
71
|
+
write(".gitignore", "node_modules/\ndist/\n");
|
|
72
|
+
write("app/server.js", "1");
|
|
73
|
+
write("app/node_modules/dep/index.js", "1");
|
|
74
|
+
write("dist/index.html", "1");
|
|
75
|
+
expect(directoryFiles(root, "app")).toEqual(["app/server.js"]);
|
|
76
|
+
const read = componentDeployInputs(deploy({ kind: "docker-build", context: "app" }, { kind: "s3-sync", from: "dist", to: "s3://b" }), root);
|
|
77
|
+
expect(read.inputs.map((i) => i.path)).toEqual(["app"]);
|
|
78
|
+
expect(read.unbound).toEqual([{ phase: "Deploy", kind: "s3-sync", reason: expect.stringMatching(/"dist" is a directory git ignores, so a build output/) }]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a file git ignores is still hashed when a step names it, as a template chant build writes", () => {
|
|
82
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
83
|
+
write(".gitignore", "dist/\n");
|
|
84
|
+
write("dist/stack.json", "{}");
|
|
85
|
+
expect(componentDeployInputs(deploy({ kind: "cfn-deploy", template: "dist/stack.json" }), root).inputs.map((i) => i.path)).toEqual(["dist/stack.json"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("a shell step is unbound; `include` limits the steps read", () => {
|
|
89
|
+
write("chart/Chart.yaml", "x");
|
|
90
|
+
const phases = deploy({ kind: "shell", cmd: "./go.sh", reason: "t" }, { kind: "helm-upgrade", chart: "chart" });
|
|
91
|
+
expect(componentDeployInputs(phases, root).unbound.map((u) => u.kind)).toEqual(["shell"]);
|
|
92
|
+
expect(componentDeployInputs(phases, root, (s) => s.kind !== "helm-upgrade").inputs).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("componentPlanDigest with inputs", () => {
|
|
97
|
+
const component: DriverComponent = { name: "web", dependsOn: [], deploy: deploy({ kind: "helm-upgrade", chart: "chart" }) };
|
|
98
|
+
|
|
99
|
+
test("no inputs keeps the digest a composition had before #4155", () => {
|
|
100
|
+
expect(componentPlanDigest({ environment: "prod", component, inputs: [] })).toBe(componentPlanDigest({ environment: "prod", component }));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("an input's digest moves the plan digest", () => {
|
|
104
|
+
const a = componentPlanDigest({ environment: "prod", component, inputs: [{ path: "chart", digest: "sha256:aa" }] });
|
|
105
|
+
const b = componentPlanDigest({ environment: "prod", component, inputs: [{ path: "chart", digest: "sha256:bb" }] });
|
|
106
|
+
expect(a).not.toBe(b);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local files a component's steps deploy from, and their content digests
|
|
3
|
+
* (#4155).
|
|
4
|
+
*
|
|
5
|
+
* A component gate approves `componentPlanDigest` (./gate-plan.ts). A step
|
|
6
|
+
* whose capability has a `plan` is bound by what it plans. Every other step
|
|
7
|
+
* was bound only by its input as written, so a Helm chart, a kustomize
|
|
8
|
+
* directory or a static site's tree could change after review without moving
|
|
9
|
+
* the digest. This module names those files and hashes them, so the digest
|
|
10
|
+
* moves when they do.
|
|
11
|
+
*
|
|
12
|
+
* Which fields name a path is data, as the template fields are in
|
|
13
|
+
* ./release-compile.ts and the unit fields in ./deploy-units.ts: a kind's
|
|
14
|
+
* fields in {@link DEPLOY_INPUT_FIELDS}, plus `template`, `manifest` and
|
|
15
|
+
* `values` on any step ({@link TEMPLATE_FIELDS}). Only a literal path counts:
|
|
16
|
+
* a value wired from another step (`@Build.uri`) or an `archive:` path is a
|
|
17
|
+
* build output, and binding it would make an approval unusable by a build
|
|
18
|
+
* that is not reproducible. The build steps' own sources are bound instead.
|
|
19
|
+
*
|
|
20
|
+
* - A file is hashed by its bytes.
|
|
21
|
+
* - A directory is hashed over the files under it that git does not ignore
|
|
22
|
+
* (`git ls-files --cached --others --exclude-standard`), each by its path
|
|
23
|
+
* and bytes, so `node_modules` and a build's `dist` inside a Docker context
|
|
24
|
+
* are left out. Outside a git repository every file is hashed except under
|
|
25
|
+
* `.git` and `node_modules`.
|
|
26
|
+
* - A directory git ignores as a whole is a build output. It is not hashed;
|
|
27
|
+
* the step is listed as unbound, with that reason.
|
|
28
|
+
* - A path that does not exist is left out. One that appears later moves the
|
|
29
|
+
* digest.
|
|
30
|
+
*
|
|
31
|
+
* A step whose kind runs content chant can't name, such as `shell`, is listed
|
|
32
|
+
* as unbound ({@link UNBOUND_KINDS}), so the plan note can show a reviewer
|
|
33
|
+
* what the approval does not cover.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
38
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
39
|
+
import { sha256Hex } from "../content-digest";
|
|
40
|
+
import { computePlanDigest } from "../lifecycle/plan-digest";
|
|
41
|
+
import type { DriverGate, DriverPhase, DriverStep } from "./driver";
|
|
42
|
+
|
|
43
|
+
/** The fields that name every step's deployed file, whatever its kind (as ./release-compile.ts reads them). */
|
|
44
|
+
export const TEMPLATE_FIELDS: readonly string[] = ["template", "manifest", "values"];
|
|
45
|
+
|
|
46
|
+
/** Per kind, the fields beyond {@link TEMPLATE_FIELDS} that name a local file or directory the step deploys or builds from. */
|
|
47
|
+
export const DEPLOY_INPUT_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
48
|
+
"helm-upgrade": ["chart"],
|
|
49
|
+
"kustomize-apply": ["dir"],
|
|
50
|
+
"fly-release": ["plan"],
|
|
51
|
+
"fly-rollback": ["plan"],
|
|
52
|
+
"s3-sync": ["from"],
|
|
53
|
+
"r2-sync": ["from"],
|
|
54
|
+
"copy-to-host": ["from"],
|
|
55
|
+
"wrangler-deploy": ["config"],
|
|
56
|
+
"wrangler-versions-promote": ["config"],
|
|
57
|
+
"docker-build": ["context"],
|
|
58
|
+
"zip-package": ["source"],
|
|
59
|
+
"jvm-build": ["path"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Kinds that deploy content chant can't name from their input: a command, an agent's work. */
|
|
63
|
+
export const UNBOUND_KINDS: readonly string[] = ["shell", "run-agent"];
|
|
64
|
+
|
|
65
|
+
/** One path a step names. */
|
|
66
|
+
export interface DeployInputRef {
|
|
67
|
+
phase: string;
|
|
68
|
+
kind: string;
|
|
69
|
+
field: string;
|
|
70
|
+
/** Relative to the member's directory, as written. */
|
|
71
|
+
path: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A named path and the digest of what is there. */
|
|
75
|
+
export interface DeployInputDigest {
|
|
76
|
+
path: string;
|
|
77
|
+
/** `sha256:<hex>` of a file's bytes, or a plan digest over a directory's files. */
|
|
78
|
+
digest: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A step the digest does not cover, and why. */
|
|
82
|
+
export interface UnboundStep {
|
|
83
|
+
phase: string;
|
|
84
|
+
kind: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What a component's steps deploy from: the digests, and the steps they can't cover. */
|
|
89
|
+
export interface ComponentDeployInputs {
|
|
90
|
+
inputs: DeployInputDigest[];
|
|
91
|
+
unbound: UnboundStep[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
|
|
95
|
+
return (step as { kind?: unknown }).kind === "gate";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
|
|
99
|
+
return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isLiteralPath = (v: unknown): v is string =>
|
|
103
|
+
typeof v === "string" && v !== "" && !v.startsWith("@") && !v.startsWith("$") && !v.startsWith("archive:");
|
|
104
|
+
|
|
105
|
+
/** Every capability step of a composition with its phase, nested phases included, gates left out. */
|
|
106
|
+
export function stepsWithPhase(phases: readonly DriverPhase[]): Array<{ phase: string; step: DriverStep }> {
|
|
107
|
+
const out: Array<{ phase: string; step: DriverStep }> = [];
|
|
108
|
+
const walk = (phase: DriverPhase): void => {
|
|
109
|
+
for (const step of phase.steps) {
|
|
110
|
+
if (isPhase(step)) walk(step);
|
|
111
|
+
else if (!isGate(step)) out.push({ phase: phase.phase, step });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
phases.forEach(walk);
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The literal paths one step names, in field order. A Docker build's Dockerfile is named under its context. */
|
|
119
|
+
export function stepInputRefs(phase: string, step: DriverStep): DeployInputRef[] {
|
|
120
|
+
const out: DeployInputRef[] = [];
|
|
121
|
+
for (const field of [...TEMPLATE_FIELDS, ...(DEPLOY_INPUT_FIELDS[step.kind] ?? [])]) {
|
|
122
|
+
const raw = step[field];
|
|
123
|
+
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
124
|
+
if (isLiteralPath(value)) out.push({ phase, kind: step.kind, field, path: value });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (step.kind === "docker-build" && isLiteralPath(step.context) && isLiteralPath(step.dockerfile)) {
|
|
128
|
+
out.push({ phase, kind: step.kind, field: "dockerfile", path: join(step.context, step.dockerfile) });
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function git(root: string, args: string[]): { ok: boolean; out: string } {
|
|
134
|
+
const r = spawnSync("git", ["-C", root, ...args], { encoding: "utf-8", maxBuffer: 256 * 1024 * 1024 });
|
|
135
|
+
return { ok: r.status === 0, out: r.stdout ?? "" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The files under `dir` (relative to `root`) a directory digest covers, relative to `root`, sorted. */
|
|
139
|
+
export function directoryFiles(root: string, dir: string): string[] {
|
|
140
|
+
const listed = git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", dir]);
|
|
141
|
+
if (listed.ok) {
|
|
142
|
+
return [...new Set(listed.out.split("\0").filter((p) => p !== ""))]
|
|
143
|
+
.filter((p) => {
|
|
144
|
+
const full = resolve(root, p);
|
|
145
|
+
return existsSync(full) && statSync(full).isFile();
|
|
146
|
+
})
|
|
147
|
+
.sort();
|
|
148
|
+
}
|
|
149
|
+
return walkFiles(root, dir);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Every file under `dir`, relative to `root`, sorted, leaving out `.git` and `node_modules`. What a cut records of a directory git ignores. */
|
|
153
|
+
export function walkFiles(root: string, dir: string): string[] {
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
const walk = (abs: string): void => {
|
|
156
|
+
for (const entry of readdirSync(abs, { withFileTypes: true })) {
|
|
157
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
158
|
+
const full = join(abs, entry.name);
|
|
159
|
+
if (entry.isDirectory()) walk(full);
|
|
160
|
+
else if (entry.isFile()) out.push(relative(root, full).split(sep).join("/"));
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
walk(resolve(root, dir));
|
|
164
|
+
return out.sort();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Whether git ignores `path` (relative to `root`). False outside a git repository. */
|
|
168
|
+
export function ignoredByGit(root: string, path: string): boolean {
|
|
169
|
+
return git(root, ["check-ignore", "-q", "--", path]).ok;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** `sha256:<hex>` of a file's bytes. */
|
|
173
|
+
export function fileDigest(file: string): string {
|
|
174
|
+
return `sha256:${sha256Hex(readFileSync(file))}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The digest of the files under `dir`: a plan digest over each file's path (from `dir`) and bytes. */
|
|
178
|
+
export function directoryDigest(root: string, dir: string): string {
|
|
179
|
+
const base = resolve(root, dir);
|
|
180
|
+
const files = directoryFiles(root, dir).map((p) => ({
|
|
181
|
+
path: relative(base, resolve(root, p)).split(sep).join("/"),
|
|
182
|
+
digest: fileDigest(resolve(root, p)),
|
|
183
|
+
}));
|
|
184
|
+
return computePlanDigest("directory", files);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* What a component's steps deploy from, read under `root` (the member's
|
|
189
|
+
* directory). `include` picks the steps to read, all of them by default; the
|
|
190
|
+
* wave planner passes the steps without a `plan`, which their plan does not
|
|
191
|
+
* already cover.
|
|
192
|
+
*/
|
|
193
|
+
export function componentDeployInputs(
|
|
194
|
+
phases: readonly DriverPhase[],
|
|
195
|
+
root: string,
|
|
196
|
+
include: (step: DriverStep) => boolean = () => true,
|
|
197
|
+
): ComponentDeployInputs {
|
|
198
|
+
const inputs = new Map<string, string>();
|
|
199
|
+
const unbound: UnboundStep[] = [];
|
|
200
|
+
for (const { phase, step } of stepsWithPhase(phases)) {
|
|
201
|
+
if (!include(step)) continue;
|
|
202
|
+
if (UNBOUND_KINDS.includes(step.kind)) {
|
|
203
|
+
unbound.push({ phase, kind: step.kind, reason: `a ${step.kind} step runs content chant can't name from its input` });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
for (const ref of stepInputRefs(phase, step)) {
|
|
207
|
+
if (inputs.has(ref.path)) continue;
|
|
208
|
+
const full = resolve(root, ref.path);
|
|
209
|
+
if (!existsSync(full)) continue;
|
|
210
|
+
if (statSync(full).isDirectory()) {
|
|
211
|
+
if (ignoredByGit(root, ref.path)) {
|
|
212
|
+
unbound.push({ phase, kind: step.kind, reason: `its ${ref.field} "${ref.path}" is a directory git ignores, so a build output; bind its sources through the step that builds it` });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
inputs.set(ref.path, directoryDigest(root, ref.path));
|
|
216
|
+
} else {
|
|
217
|
+
inputs.set(ref.path, fileDigest(full));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
inputs: [...inputs].map(([path, digest]) => ({ path, digest })).sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)),
|
|
223
|
+
unbound,
|
|
224
|
+
};
|
|
225
|
+
}
|