@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
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
* host. It polls the forge for the open pull requests into the target branch
|
|
4
4
|
* and for the branch's head, and runs the pull-request jobs with
|
|
5
5
|
* {@link ciRun} (./ci-run.ts): `plan` for each new pull request head, and
|
|
6
|
-
* `
|
|
7
|
-
* the previous poll saw (the push's `before`).
|
|
6
|
+
* `record` for each new commit on the target branch (#4168), measured from
|
|
7
|
+
* the head the previous poll saw (the push's `before`). The record applies
|
|
8
|
+
* nothing; with `applyOnMerge` (`--apply-on-merge`) the commit gets `apply`
|
|
9
|
+
* in its place, as before #4168. After each job settles it runs `ci tick`
|
|
10
|
+
* from the runs the jobs recorded, when the declaration has `ci.green`.
|
|
11
|
+
* What the list below says of the apply holds for it under `applyOnMerge`.
|
|
8
12
|
*
|
|
9
13
|
* - Dedupe. A state file in the state directory keeps every job it started,
|
|
10
|
-
* keyed by job and commit (`plan:<pr>:<sha>`, `apply:<sha>`), with how it
|
|
14
|
+
* keyed by job and commit (`plan:<pr>:<sha>`, `record:<sha>`, `apply:<sha>`), with how it
|
|
11
15
|
* ended, and the target head the last apply was measured to. A job already
|
|
12
16
|
* in it is not started again, unless it failed and `rerunFailed` asks for
|
|
13
17
|
* it: the forge's "re-run failed jobs".
|
|
@@ -37,6 +41,14 @@
|
|
|
37
41
|
* - `[skip ci]` (#4122). A target head or pull request head whose commit
|
|
38
42
|
* message says `[skip ci]` or `[ci skip]` is recorded as seen and not run,
|
|
39
43
|
* as Actions does.
|
|
44
|
+
* - Build requests (#4169). With `buildCommand`, each `ci/build/<sha>`
|
|
45
|
+
* tag the tick or `ci run --at` pushed is served once: the command runs
|
|
46
|
+
* in a fresh detached worktree of the commit, with `CHANT_BUILD_COMMIT`,
|
|
47
|
+
* `CHANT_BUILD_PHASES` (comma-separated) and `CHANT_BUILD_REASON` set,
|
|
48
|
+
* and is expected to run the phases' Ops (`chant run <op>`), whose
|
|
49
|
+
* records the tick reads. A build counts against `maxJobs`, and the state
|
|
50
|
+
* file keeps it under `build:<sha>`. Without `buildCommand` the tags are
|
|
51
|
+
* left alone.
|
|
40
52
|
* - The forge's ledger (#4121). Before each job, when no other job of this
|
|
41
53
|
* serve runs, the clone's `chant/lifecycle` follows the forge's: it is
|
|
42
54
|
* deleted when the forge has none, and reset to the forge's when neither
|
|
@@ -54,9 +66,11 @@
|
|
|
54
66
|
* turn a checked payload into the same triggers and schedule them.
|
|
55
67
|
*/
|
|
56
68
|
|
|
57
|
-
import { execFileSync, spawnSync } from "node:child_process";
|
|
58
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
69
|
+
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
70
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
71
|
+
import { tmpdir } from "node:os";
|
|
59
72
|
import { join, resolve } from "node:path";
|
|
73
|
+
import { CI_BUILD_TAG_PREFIX, readBuildRequests, type BuildRequest } from "./ci-builds";
|
|
60
74
|
import { prApplyGroup, prApplyRecordKey } from "../components/pr-pipeline";
|
|
61
75
|
import { forgeFromEnv, type OpenPullRequest } from "../pr-forge";
|
|
62
76
|
import type { CommandContext } from "../cli/registry";
|
|
@@ -65,7 +79,7 @@ import { acquireLease, currentHolderId, releaseLease, DEFAULT_LEASE_TTL_MS, type
|
|
|
65
79
|
import { parseDuration } from "../op/duration";
|
|
66
80
|
import { reportUrlOf } from "./ci-report-url";
|
|
67
81
|
import { CI_JOB_OPS, ciJobPlace, ciLoopOptions, ciSecretsOf, ciRun, ciStatusFromEnv, CiRunError, type CiJobName, type CiRunRequest, type CiRunResult } from "./ci-run";
|
|
68
|
-
import { RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green";
|
|
82
|
+
import { CiGreenError, ciTick, RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green";
|
|
69
83
|
import { gitTop } from "./tree";
|
|
70
84
|
import { ciServeRunnerKey, formatRunnerKeySetup, setupRunnerKey } from "./ci-runner-key";
|
|
71
85
|
|
|
@@ -80,10 +94,14 @@ export interface CiServeForge {
|
|
|
80
94
|
pullRequestFor?(sha: string): Promise<number | null>;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* A job the forge's state calls for: the plan of a pull request's head, or
|
|
99
|
+
* for a target-branch commit its record (#4168) or, with `applyOnMerge`,
|
|
100
|
+
* its apply.
|
|
101
|
+
*/
|
|
84
102
|
export type CiServeTrigger =
|
|
85
103
|
| { job: "plan"; commit: string; base: string; pr: number; rerun?: boolean }
|
|
86
|
-
| { job: "apply"; commit: string; base: string; rerun?: boolean };
|
|
104
|
+
| { job: "apply" | "record"; commit: string; base: string; rerun?: boolean };
|
|
87
105
|
|
|
88
106
|
/** One job in the state file. */
|
|
89
107
|
export interface CiServeJobState {
|
|
@@ -116,6 +134,8 @@ export interface CiServeState {
|
|
|
116
134
|
jobs: Record<string, CiServeJobState>;
|
|
117
135
|
/** Plans seen and not run, by state key, with why (#4122). */
|
|
118
136
|
skipped?: Record<string, "skip-ci">;
|
|
137
|
+
/** The build requests served (#4169), by `build:<sha>`, with how each ended. */
|
|
138
|
+
builds?: Record<string, { tag: string; status: "running" | "ok" | "fail"; startedAt: string; settledAt?: string }>;
|
|
119
139
|
}
|
|
120
140
|
|
|
121
141
|
/** What a pass did, in order, for the log. */
|
|
@@ -128,6 +148,9 @@ export type CiServeEvent =
|
|
|
128
148
|
| { type: "settled"; job: CiJobName; commit: string; pr?: number; status: "ok" | "fail"; failure?: string; report?: string }
|
|
129
149
|
| { type: "not-run"; job: CiJobName; commit: string; pr?: number; reason: "not-fast-forward" | "skip-ci"; branch?: string; from?: string }
|
|
130
150
|
| { type: "ledger"; action: "deleted" | "reset"; local: string; forge?: string }
|
|
151
|
+
| { type: "build"; commit: string; phases: string[]; status: "started" | "ok" | "fail" | "held"; exitCode?: number }
|
|
152
|
+
/** `ci tick` after the pass (#4168): the tags it made from the runs. */
|
|
153
|
+
| { type: "tick"; made: string[] }
|
|
131
154
|
| { type: "error"; message: string };
|
|
132
155
|
|
|
133
156
|
/** The `ciRun` request fields `ci serve` passes through to every job: the generator's options and the runner's. */
|
|
@@ -149,6 +172,18 @@ export interface CiServeOptions {
|
|
|
149
172
|
authors?: readonly string[];
|
|
150
173
|
/** Run again the failed apply at the target's head, and the failed plans of open heads. */
|
|
151
174
|
rerunFailed?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Apply each new target-branch commit (#4168), as `ci serve` did before
|
|
177
|
+
* #4168. Default false: each new commit gets the record job, which applies
|
|
178
|
+
* nothing.
|
|
179
|
+
*/
|
|
180
|
+
applyOnMerge?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Run after each pass that settled a job (#4168): `ci tick` from the runs
|
|
183
|
+
* the jobs recorded. Returns the tags it made, or undefined when the
|
|
184
|
+
* declaration has no `ci.green`. The command passes one; a test may not.
|
|
185
|
+
*/
|
|
186
|
+
tick?: () => Promise<string[] | undefined>;
|
|
152
187
|
/** Passed to each job. `job.hostEnv` gets `GITHUB_REPOSITORY` set to `repo`. */
|
|
153
188
|
job?: CiServeJobOptions;
|
|
154
189
|
/** This process's lease identity. Default `currentHolderId()`. */
|
|
@@ -158,18 +193,26 @@ export interface CiServeOptions {
|
|
|
158
193
|
now?: () => Date;
|
|
159
194
|
/** Runs a job. Default {@link ciRun}. */
|
|
160
195
|
run?: (req: CiRunRequest) => Promise<CiRunResult>;
|
|
196
|
+
/**
|
|
197
|
+
* The shell line that builds a requested commit (#4169), run in a fresh
|
|
198
|
+
* worktree of it. Without it `ci/build/<sha>` tags are not served.
|
|
199
|
+
*/
|
|
200
|
+
buildCommand?: string;
|
|
201
|
+
/** Runs a build. Default: `sh -c <buildCommand>` in a detached worktree of the commit. */
|
|
202
|
+
build?: (input: { top: string; request: BuildRequest; command: string }) => Promise<number>;
|
|
161
203
|
}
|
|
162
204
|
|
|
163
205
|
/** The jobs this process runs now, by state key, shared by every pass of one loop. */
|
|
164
206
|
export interface CiServeRuntime {
|
|
165
|
-
|
|
207
|
+
/** Jobs and build requests (#4169) alike, by state key: they share `maxJobs`. */
|
|
208
|
+
inflight: Map<string, Promise<unknown>>;
|
|
166
209
|
}
|
|
167
210
|
|
|
168
211
|
export const newCiServeRuntime = (): CiServeRuntime => ({ inflight: new Map() });
|
|
169
212
|
|
|
170
213
|
/** The state key of a job. */
|
|
171
214
|
export function ciServeKey(t: { job: CiJobName; commit: string; pr?: number }): string {
|
|
172
|
-
return t.job === "plan" ? `plan:${t.pr}:${t.commit}` :
|
|
215
|
+
return t.job === "plan" ? `plan:${t.pr}:${t.commit}` : `${t.job}:${t.commit}`;
|
|
173
216
|
}
|
|
174
217
|
|
|
175
218
|
const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
@@ -324,6 +367,8 @@ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime =
|
|
|
324
367
|
const triggers: CiServeTrigger[] = [];
|
|
325
368
|
const events: CiServeEvent[] = [];
|
|
326
369
|
const [head, prs] = await Promise.all([opts.client.branchHead(place.branch), opts.client.openPullRequests(place.branch)]);
|
|
370
|
+
// #4168: a new target commit is recorded, and applied only with applyOnMerge.
|
|
371
|
+
const onMerge: "apply" | "record" = opts.applyOnMerge ? "apply" : "record";
|
|
327
372
|
|
|
328
373
|
if (head === undefined) {
|
|
329
374
|
events.push({ type: "no-branch", branch: place.branch });
|
|
@@ -336,7 +381,7 @@ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime =
|
|
|
336
381
|
} else if (head !== state.target) {
|
|
337
382
|
const from = state.target;
|
|
338
383
|
const move = targetMove(place.top, place.branch, from, head);
|
|
339
|
-
const entry = state.jobs[ciServeKey({ job:
|
|
384
|
+
const entry = state.jobs[ciServeKey({ job: onMerge, commit: head })];
|
|
340
385
|
if (move === "unknown") {
|
|
341
386
|
events.push({ type: "error", message: `${place.branch} is at ${head.slice(0, 12)} on the forge, and that commit could not be fetched; the next poll tries again` });
|
|
342
387
|
} else if (move === "other" || (!entry && skipsCi(place.top, head))) {
|
|
@@ -344,13 +389,13 @@ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime =
|
|
|
344
389
|
update((s) => {
|
|
345
390
|
s.target = head;
|
|
346
391
|
});
|
|
347
|
-
events.push({ type: "not-run", job:
|
|
348
|
-
} else if (!entry) triggers.push({ job:
|
|
349
|
-
else if (opts.rerunFailed && failedEntry(entry, ciServeKey(entry), rt)) triggers.push({ job:
|
|
392
|
+
events.push({ type: "not-run", job: onMerge, commit: head, reason: move === "other" ? "not-fast-forward" : "skip-ci", ...(move === "other" ? { branch: place.branch, from } : {}) });
|
|
393
|
+
} else if (!entry) triggers.push({ job: onMerge, commit: head, base: from });
|
|
394
|
+
else if (opts.rerunFailed && failedEntry(entry, ciServeKey(entry), rt)) triggers.push({ job: onMerge, commit: head, base: entry.base, rerun: true });
|
|
350
395
|
} else {
|
|
351
|
-
const key = ciServeKey({ job:
|
|
396
|
+
const key = ciServeKey({ job: onMerge, commit: head });
|
|
352
397
|
const entry = state.jobs[key];
|
|
353
|
-
if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job:
|
|
398
|
+
if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: onMerge, commit: head, base: entry.base, rerun: true });
|
|
354
399
|
}
|
|
355
400
|
|
|
356
401
|
const authors = opts.authors?.length ? new Set(opts.authors.map((a) => a.toLowerCase())) : undefined;
|
|
@@ -453,7 +498,7 @@ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly C
|
|
|
453
498
|
const entry: CiServeJobState = { job: t.job, commit: t.commit, base: t.base, ...pr, status: "running", runId, attempts: attempt, startedAt: now().toISOString() };
|
|
454
499
|
update((s) => {
|
|
455
500
|
s.jobs[key] = entry;
|
|
456
|
-
if (t.job
|
|
501
|
+
if (t.job !== "plan" && !t.rerun) s.target = t.commit;
|
|
457
502
|
});
|
|
458
503
|
emit({ type: "started", job: t.job, commit: t.commit, ...pr, base: t.base, runId, attempt, ...(resume && existsSync(resume) ? { resume } : {}) });
|
|
459
504
|
|
|
@@ -468,7 +513,7 @@ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly C
|
|
|
468
513
|
emit({ type: "error", message: `${key} settled, and the state file could not be written: ${message(err)}` });
|
|
469
514
|
}
|
|
470
515
|
emit({ type: "settled", job: t.job, commit: t.commit, ...pr, status: settled.status === "ok" ? "ok" : "fail", ...(settled.failure ? { failure: settled.failure } : {}), ...(settled.report ? { report: settled.report } : {}) });
|
|
471
|
-
return settled;
|
|
516
|
+
return serveTick(opts).then(() => settled);
|
|
472
517
|
});
|
|
473
518
|
rt.inflight.set(key, job);
|
|
474
519
|
started.push(job);
|
|
@@ -496,7 +541,7 @@ async function runJob(
|
|
|
496
541
|
tryGit(place.top, ["fetch", "--quiet", "--no-tags", "origin", t.job === "plan" ? `refs/pull/${t.pr}/head` : place.branch]);
|
|
497
542
|
const hostEnv = { ...(opts.job?.hostEnv ?? process.env), GITHUB_REPOSITORY: opts.repo };
|
|
498
543
|
// An apply is for a merge: pass the pull request whose merge commit it is, so the commit status link and the run index use `<pr>/<commit>`. None found, or a forge that errs, is today's behaviour.
|
|
499
|
-
if (t.job
|
|
544
|
+
if (t.job !== "plan" && opts.client.pullRequestFor) merged = (await opts.client.pullRequestFor(t.commit).catch(() => null)) ?? undefined;
|
|
500
545
|
const run = opts.run ?? ciRun;
|
|
501
546
|
const r = await run({
|
|
502
547
|
...opts.job,
|
|
@@ -522,13 +567,101 @@ async function runJob(
|
|
|
522
567
|
}
|
|
523
568
|
}
|
|
524
569
|
|
|
525
|
-
/** One pass: observe, schedule, and with `wait` wait for the jobs it started. */
|
|
570
|
+
/** One pass: observe, schedule, serve build requests, and with `wait` wait for the jobs it started. */
|
|
526
571
|
export async function ciServePass(opts: CiServeOptions, rt: CiServeRuntime = newCiServeRuntime(), wait = true): Promise<{ events: CiServeEvent[]; jobs: CiServeJobState[] }> {
|
|
527
572
|
const observed = await ciServeObserve(opts, rt);
|
|
528
573
|
for (const e of observed.events) opts.log?.(e);
|
|
529
574
|
const scheduled = await ciServeSchedule(opts, observed.triggers, rt);
|
|
575
|
+
const builds = opts.buildCommand ? ciServeBuilds(opts, rt) : { started: [], events: [] };
|
|
530
576
|
const jobs = wait ? await Promise.all(scheduled.started) : [];
|
|
531
|
-
|
|
577
|
+
if (wait) await Promise.all(builds.started);
|
|
578
|
+
return { events: [...observed.events, ...scheduled.events, ...builds.events], jobs };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Serve the build requests (#4169): fetch the `ci/build/*` tags, and start
|
|
583
|
+
* `buildCommand` for each one not in the state file yet, oldest first, while
|
|
584
|
+
* fewer than `maxJobs` jobs run. A build that fails is recorded and not run
|
|
585
|
+
* again; the tick decides what its phases say.
|
|
586
|
+
*/
|
|
587
|
+
export function ciServeBuilds(opts: CiServeOptions, rt: CiServeRuntime): { started: Promise<void>[]; events: CiServeEvent[] } {
|
|
588
|
+
const place = servePlace(opts);
|
|
589
|
+
const fresh = { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) };
|
|
590
|
+
const update = (fn: (s: CiServeState) => void): void => {
|
|
591
|
+
const st = readCiServeState(place.stateFile, fresh);
|
|
592
|
+
fn(st);
|
|
593
|
+
writeState(place.stateFile, st);
|
|
594
|
+
};
|
|
595
|
+
const events: CiServeEvent[] = [];
|
|
596
|
+
const started: Promise<void>[] = [];
|
|
597
|
+
tryGit(place.top, ["fetch", "--quiet", "--prune", "origin", `+refs/tags/${CI_BUILD_TAG_PREFIX}*:refs/tags/${CI_BUILD_TAG_PREFIX}*`]);
|
|
598
|
+
const served = readCiServeState(place.stateFile, fresh).builds ?? {};
|
|
599
|
+
const max = opts.maxJobs ?? 1;
|
|
600
|
+
const now = opts.now ?? (() => new Date());
|
|
601
|
+
const requests = [...readBuildRequests(place.top).values()].sort((a, b) => a.at.localeCompare(b.at));
|
|
602
|
+
for (const request of requests) {
|
|
603
|
+
const key = `build:${request.commit}`;
|
|
604
|
+
if (served[key]?.tag === request.tag || rt.inflight.has(key)) continue;
|
|
605
|
+
const log = (e: CiServeEvent) => {
|
|
606
|
+
events.push(e);
|
|
607
|
+
opts.log?.(e);
|
|
608
|
+
};
|
|
609
|
+
if (rt.inflight.size >= max) {
|
|
610
|
+
log({ type: "build", commit: request.commit, phases: request.phases, status: "held" });
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
update((st) => {
|
|
614
|
+
st.builds = { ...st.builds, [key]: { tag: request.tag, status: "running", startedAt: now().toISOString() } };
|
|
615
|
+
});
|
|
616
|
+
log({ type: "build", commit: request.commit, phases: request.phases, status: "started" });
|
|
617
|
+
const build = opts.build ?? runBuild;
|
|
618
|
+
const job = build({ top: place.top, request, command: opts.buildCommand! })
|
|
619
|
+
.catch(() => 1)
|
|
620
|
+
.then((code) => {
|
|
621
|
+
update((st) => {
|
|
622
|
+
st.builds = { ...st.builds, [key]: { tag: request.tag, status: code === 0 ? "ok" : "fail", startedAt: st.builds?.[key]?.startedAt ?? now().toISOString(), settledAt: now().toISOString() } };
|
|
623
|
+
});
|
|
624
|
+
opts.log?.({ type: "build", commit: request.commit, phases: request.phases, status: code === 0 ? "ok" : "fail", exitCode: code });
|
|
625
|
+
});
|
|
626
|
+
const tracked = job.finally(() => rt.inflight.delete(key));
|
|
627
|
+
rt.inflight.set(key, tracked);
|
|
628
|
+
started.push(tracked);
|
|
629
|
+
}
|
|
630
|
+
return { started, events };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** `sh -c <command>` in a fresh detached worktree of the request's commit, removed after. Resolves to the exit code. */
|
|
634
|
+
async function runBuild(input: { top: string; request: BuildRequest; command: string }): Promise<number> {
|
|
635
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-ci-build-"));
|
|
636
|
+
try {
|
|
637
|
+
execFileSync("git", ["worktree", "add", "--quiet", "--detach", dir, input.request.commit], { cwd: input.top, stdio: ["ignore", "pipe", "pipe"] });
|
|
638
|
+
return await new Promise<number>((done) => {
|
|
639
|
+
const child = spawn("sh", ["-c", input.command], {
|
|
640
|
+
cwd: dir,
|
|
641
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
642
|
+
env: { ...process.env, CHANT_BUILD_COMMIT: input.request.commit, CHANT_BUILD_PHASES: input.request.phases.join(","), CHANT_BUILD_REASON: input.request.reason },
|
|
643
|
+
});
|
|
644
|
+
child.on("error", () => done(127));
|
|
645
|
+
child.on("close", (code) => done(code ?? 1));
|
|
646
|
+
});
|
|
647
|
+
} finally {
|
|
648
|
+
tryGit(input.top, ["worktree", "remove", "--force", dir]);
|
|
649
|
+
rmSync(dir, { recursive: true, force: true });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** `ci tick` after a job settled (#4168), logged; a tick that fails is an error event and never fails the job. */
|
|
654
|
+
async function serveTick(opts: CiServeOptions): Promise<CiServeEvent | undefined> {
|
|
655
|
+
if (!opts.tick) return undefined;
|
|
656
|
+
let event: CiServeEvent | undefined;
|
|
657
|
+
try {
|
|
658
|
+
const made = await opts.tick();
|
|
659
|
+
event = made === undefined ? undefined : { type: "tick", made };
|
|
660
|
+
} catch (err) {
|
|
661
|
+
event = { type: "error", message: `ci tick after a job settled: ${message(err)}` };
|
|
662
|
+
}
|
|
663
|
+
if (event) opts.log?.(event);
|
|
664
|
+
return event;
|
|
532
665
|
}
|
|
533
666
|
|
|
534
667
|
/**
|
|
@@ -579,6 +712,13 @@ export function formatCiServeEvent(e: CiServeEvent): string {
|
|
|
579
712
|
return e.action === "deleted"
|
|
580
713
|
? `the forge has no ${LEDGER}, so this clone's, at ${e.local.slice(0, 12)}, was deleted`
|
|
581
714
|
: `the forge's ${LEDGER} at ${e.forge!.slice(0, 12)} does not contain this clone's at ${e.local.slice(0, 12)}, so this clone's was reset to the forge's and not pushed`;
|
|
715
|
+
case "build": {
|
|
716
|
+
const what = `the build of ${e.phases.join(", ")} at ${e.commit.slice(0, 12)}`;
|
|
717
|
+
if (e.status === "held") return `${what} waits: --max-jobs jobs run`;
|
|
718
|
+
return e.status === "started" ? `${what} started` : e.status === "ok" ? `${what} passed` : `${what} failed${e.exitCode !== undefined ? ` with exit code ${e.exitCode}` : ""}`;
|
|
719
|
+
}
|
|
720
|
+
case "tick":
|
|
721
|
+
return e.made.length > 0 ? `ci tick made ${e.made.join(", ")}` : "ci tick: no tag due";
|
|
582
722
|
case "error":
|
|
583
723
|
return `error: ${e.message}`;
|
|
584
724
|
}
|
|
@@ -615,7 +755,7 @@ export function runnerKeySetupCommand(ctx: CommandContext): number {
|
|
|
615
755
|
const USAGE =
|
|
616
756
|
"chant ci serve --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>] | " +
|
|
617
757
|
"chant ci serve --forge forgejo|github --repo <owner/repo> --env <env> [--interval <s>] [--max-jobs <n>] [--state <dir>] " +
|
|
618
|
-
"[--author <login>]... [--once] [--rerun-failed] [--gate <name>] [--wave-gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--no-status] [--status-url <url template>] " +
|
|
758
|
+
"[--author <login>]... [--once] [--rerun-failed] [--build-command <line>] [--gate <name>] [--wave-gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--no-status] [--status-url <url template>] " +
|
|
619
759
|
"[--image <image>] [--before-script <line>]... [--pr-build] [--pr-build-command <line>] [--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [--unsigned]";
|
|
620
760
|
|
|
621
761
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
@@ -638,6 +778,8 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
|
|
|
638
778
|
if (!args.ciRepo) throw new CiRunError("--repo names the repository whose pull requests ci serve polls: --repo <owner/repo>");
|
|
639
779
|
if (!args.env) throw new CiRunError("--env names the environment the pipeline is for: --env <env>");
|
|
640
780
|
for (const name of args.passEnv ?? []) if (!ENV_NAME.test(name)) throw new CiRunError(`--pass-env takes a variable's name, not ${JSON.stringify(name)}`);
|
|
781
|
+
// #4168: a gate per wave gates the apply on merge, which a merge runs only when opted in.
|
|
782
|
+
if (args.waveGate && !args.applyOnMerge) throw new CiRunError("--wave-gate gates the apply on merge, so it needs --apply-on-merge (#4168); without it a merge gets the record job, which applies nothing");
|
|
641
783
|
const intervalMs = ciServeInterval(args.interval);
|
|
642
784
|
const cwd = process.cwd();
|
|
643
785
|
const top = gitTop(cwd);
|
|
@@ -670,6 +812,8 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
|
|
|
670
812
|
const { status, warning } = ciStatusFromEnv({ forge, hostEnv, noStatus: args.noStatus, statusUrl: args.statusUrl });
|
|
671
813
|
if (warning) console.error(formatWarning({ message: warning }));
|
|
672
814
|
if (!client.openPullRequests || !client.branchHead) throw new CiRunError(`the ${forge} client cannot list pull requests`);
|
|
815
|
+
// #4168: a host that applied each merge before the upgrade hears that it no longer does.
|
|
816
|
+
if (!args.applyOnMerge) console.error("ci serve: each new commit on the target branch gets the record job, which applies nothing (#4168); --apply-on-merge applies it");
|
|
673
817
|
const opts: CiServeOptions = {
|
|
674
818
|
cwd,
|
|
675
819
|
forge,
|
|
@@ -680,6 +824,17 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
|
|
|
680
824
|
...(args.maxJobs ? { maxJobs: args.maxJobs } : {}),
|
|
681
825
|
...(args.ciAuthors?.length ? { authors: args.ciAuthors } : {}),
|
|
682
826
|
...(args.rerunFailed ? { rerunFailed: true } : {}),
|
|
827
|
+
...(args.ciBuildCommand ? { buildCommand: args.ciBuildCommand } : {}),
|
|
828
|
+
...(args.applyOnMerge ? { applyOnMerge: true } : {}),
|
|
829
|
+
// #4168: the tick from the runs the jobs recorded, signed with the runs' key. No ci.green, no tick.
|
|
830
|
+
tick: async () => {
|
|
831
|
+
try {
|
|
832
|
+
return (await ciTick({ cwd, source: "runs", ...(runnerKey !== undefined ? { runnerKey } : {}) })).made;
|
|
833
|
+
} catch (err) {
|
|
834
|
+
if (err instanceof CiGreenError && err.code === "ci-green-undeclared") return undefined;
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
},
|
|
683
838
|
job: {
|
|
684
839
|
loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
|
|
685
840
|
...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prescribed delivery workflows (#4168 section 7): `ci.workflow` in the
|
|
3
|
+
* declaration, at the top level or on a member, which wins.
|
|
4
|
+
*
|
|
5
|
+
* `trunk-curated`: a merge records the plan and deploys nothing (#4168 4.1),
|
|
6
|
+
* the quick checks run before the branch moves (4.4), and a release is cut
|
|
7
|
+
* from a green commit and promoted through the member's environments, gated
|
|
8
|
+
* in the last (4.2). `trunk-curated+apply-on-merge` is the same with the apply
|
|
9
|
+
* on merge turned back on (section 5).
|
|
10
|
+
*
|
|
11
|
+
* A workflow requires no security group: those are opt-in on their own
|
|
12
|
+
* (./security.ts). `chant workspace check` reports what a member under a
|
|
13
|
+
* workflow still lacks (./checks/ci-workflow.ts, WSP151), and `chant ci
|
|
14
|
+
* init` writes what it can. {@link CI_WORKFLOW_REQUIREMENTS} is the table the
|
|
15
|
+
* reference page prints.
|
|
16
|
+
*/
|
|
17
|
+
import type { CiWorkflow, Declaration, Member } from "./declaration";
|
|
18
|
+
|
|
19
|
+
/** The marker a component stub `chant ci init` writes carries until someone fills it in. WSP151 reports a member whose only components carry it. */
|
|
20
|
+
export const STUB_MARKER = "TODO(chant ci init)";
|
|
21
|
+
|
|
22
|
+
export const CI_WORKFLOWS = ["trunk-curated", "trunk-curated+apply-on-merge"] as const satisfies readonly CiWorkflow[];
|
|
23
|
+
|
|
24
|
+
/** Whether `workflow` turns the apply on merge back on. */
|
|
25
|
+
export function appliesOnMerge(workflow: CiWorkflow): boolean {
|
|
26
|
+
return workflow === "trunk-curated+apply-on-merge";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The workflow `member` follows: its own `ci.workflow`, else the top level's, else null. */
|
|
30
|
+
export function workflowOf(declaration: Pick<Declaration, "ci">, member: Pick<Member, "ciWorkflow"> | null): CiWorkflow | null {
|
|
31
|
+
return member?.ciWorkflow ?? declaration.ci?.workflow ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One requirement of the workflows, with the `workspace check` code that reports it missing. */
|
|
35
|
+
export interface CiWorkflowRequirement {
|
|
36
|
+
/** The reason code WSP151 reports. */
|
|
37
|
+
code: string;
|
|
38
|
+
/** What the workflow needs. */
|
|
39
|
+
requirement: string;
|
|
40
|
+
/** What adds it. */
|
|
41
|
+
fix: string;
|
|
42
|
+
/** Whether it applies per member (true) or once for the workspace. */
|
|
43
|
+
perMember: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Every requirement, in the order `workspace check` reports them. The
|
|
48
|
+
* reference page's table is printed from this list (`ciWorkflowTable`), and a
|
|
49
|
+
* test compares the two.
|
|
50
|
+
*/
|
|
51
|
+
export const CI_WORKFLOW_REQUIREMENTS = [
|
|
52
|
+
{ code: "ci-workflow-green-undeclared", requirement: "`ci.green` is declared, so commits get a verdict and a release is cut only from a green one", fix: "`chant ci init`, or a hand-written `ci.green` block", perMember: false },
|
|
53
|
+
{ code: "ci-workflow-no-pre-merge", requirement: "`ci.green.preMerge` names at least one phase, the quick checks that run before the branch moves", fix: "`preMerge` in `ci.green`", perMember: false },
|
|
54
|
+
{ code: "ci-workflow-apply-in-green", requirement: "no phase in `ci.green.require` matches an apply job, which would wait on its own verdict", fix: "drop the apply job's pattern from the required phases", perMember: false },
|
|
55
|
+
{ code: "ci-workflow-no-components", requirement: "the member declares a component (`*.component.ts`)", fix: "`chant ci init` writes a stub", perMember: true },
|
|
56
|
+
{ code: "ci-workflow-no-environments", requirement: "the member's `chant.config.ts` declares `environments`, the promotion order", fix: "`environments` in `chant.config.ts`", perMember: true },
|
|
57
|
+
{ code: "ci-workflow-ungated-env", requirement: "each component has a gate, so a promote to the last environment waits on an approval", fix: "a `gate()` step in the component", perMember: true },
|
|
58
|
+
{ code: "ci-workflow-pipeline-missing", requirement: "a pipeline file runs the plan job, the merge job (`ci run record`, or `ci run apply` under apply on merge) and `chant ci tick`", fix: "`chant build --components --generate <forge> --pr-loop` and `chant ci workflow`", perMember: false },
|
|
59
|
+
{ code: "ci-workflow-pipeline-differs", requirement: "the merge job matches the workflow: it records under `trunk-curated` and applies under `trunk-curated+apply-on-merge`", fix: "regenerate the pull-request pipeline, with `--apply-on-merge` only under apply on merge", perMember: false },
|
|
60
|
+
{ code: "ci-workflow-publish-missing", requirement: "a pipeline file runs `chant release publish`", fix: "`chant release workflow`", perMember: false },
|
|
61
|
+
] as const satisfies readonly CiWorkflowRequirement[];
|
|
62
|
+
|
|
63
|
+
export type CiWorkflowCode = (typeof CI_WORKFLOW_REQUIREMENTS)[number]["code"];
|
|
64
|
+
|
|
65
|
+
export const CI_WORKFLOW_FINDING_CODES = CI_WORKFLOW_REQUIREMENTS.map((r) => r.code) as readonly CiWorkflowCode[];
|
|
66
|
+
|
|
67
|
+
/** The requirements table the reference page carries, row for row. */
|
|
68
|
+
export function ciWorkflowTable(): string {
|
|
69
|
+
const rows = CI_WORKFLOW_REQUIREMENTS.map((r) => `| \`${r.code}\` | ${r.requirement} | ${r.perMember ? "each member" : "the workspace"} | ${r.fix} |`);
|
|
70
|
+
return ["| Code | Requirement | Checked for | Added by |", "|---|---|---|---|", ...rows].join("\n");
|
|
71
|
+
}
|
|
@@ -178,6 +178,9 @@
|
|
|
178
178
|
"green": {
|
|
179
179
|
"$ref": "#/$defs/ciGreen"
|
|
180
180
|
},
|
|
181
|
+
"workflow": {
|
|
182
|
+
"$ref": "#/$defs/ciWorkflow"
|
|
183
|
+
},
|
|
181
184
|
"reportUrl": {
|
|
182
185
|
"type": "string",
|
|
183
186
|
"minLength": 1,
|
|
@@ -236,6 +239,14 @@
|
|
|
236
239
|
"description": "A deploy counts as failed when, within this many days after it, the commit it deployed is revoked or the environment is rolled back from it. The default is 7."
|
|
237
240
|
}
|
|
238
241
|
}
|
|
242
|
+
},
|
|
243
|
+
"security": {
|
|
244
|
+
"type": "array",
|
|
245
|
+
"uniqueItems": true,
|
|
246
|
+
"description": "The security groups the workspace opts into (#4168), for every member; a member's own security adds to them. With none, a security feature the workspace has not turned on never refuses, warns or hides anything. verified-releases: release publish and promote --release take only a verified release. supply-chain: a publish without OIDC provenance warns, and a target with no SBOM source is refused. who-ran-it: ci tick needs a runner key. review-rules: pr-apply requires a forge review. isolated-runners: an apply job requires a runner-start statement. workspace check reports what a declared group lacks (WSP161 to WSP165). Added in schema 1 by chant 0.124.33, so a declaration that uses it sets minReader to 0.124.33 or newer.",
|
|
247
|
+
"items": {
|
|
248
|
+
"enum": ["verified-releases", "supply-chain", "who-ran-it", "review-rules", "isolated-runners"]
|
|
249
|
+
}
|
|
239
250
|
}
|
|
240
251
|
},
|
|
241
252
|
"patternProperties": {
|
|
@@ -483,10 +494,31 @@
|
|
|
483
494
|
"minLength": 1,
|
|
484
495
|
"description": "Where an embedded member's files come from. Versions live in the lock."
|
|
485
496
|
},
|
|
497
|
+
"ci": {
|
|
498
|
+
"type": "object",
|
|
499
|
+
"additionalProperties": false,
|
|
500
|
+
"patternProperties": {
|
|
501
|
+
"^x-": true
|
|
502
|
+
},
|
|
503
|
+
"description": "This member's own CI settings (#4168 step 6). Added in schema 1 by chant 0.124.34, so a declaration that uses it sets minReader to 0.124.34 or newer.",
|
|
504
|
+
"properties": {
|
|
505
|
+
"workflow": {
|
|
506
|
+
"$ref": "#/$defs/ciWorkflow"
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
},
|
|
486
510
|
"travel": {
|
|
487
511
|
"type": "boolean",
|
|
488
512
|
"description": "Whether the member goes with an export (#2524 D10, #2552). chant workspace export writes only members that set travel to true into the export member, and refuses to name one that does not. Added in schema 1 by chant 0.101.0, so a declaration that uses it sets minReader to 0.101.0 or newer."
|
|
489
513
|
},
|
|
514
|
+
"security": {
|
|
515
|
+
"type": "array",
|
|
516
|
+
"uniqueItems": true,
|
|
517
|
+
"description": "The security groups this member opts into, added to the declaration's top-level security (#4168). A release, publish or pull-request job of this member runs under both lists. Added in schema 1 by chant 0.124.33, so a declaration that uses it sets minReader to 0.124.33 or newer.",
|
|
518
|
+
"items": {
|
|
519
|
+
"enum": ["verified-releases", "supply-chain", "who-ran-it", "review-rules", "isolated-runners"]
|
|
520
|
+
}
|
|
521
|
+
},
|
|
490
522
|
"because": {
|
|
491
523
|
"type": "string",
|
|
492
524
|
"minLength": 1,
|
|
@@ -1520,6 +1552,14 @@
|
|
|
1520
1552
|
"$ref": "#/$defs/ciPhaseName"
|
|
1521
1553
|
}
|
|
1522
1554
|
},
|
|
1555
|
+
"preMerge": {
|
|
1556
|
+
"type": "array",
|
|
1557
|
+
"uniqueItems": true,
|
|
1558
|
+
"description": "The phases that must pass before the branch moves (#4168 4.4), by name: the quick checks a pull request's checks, a merge queue, chant ci serve landing a branch, or chant ci run --local give before a change lands. Each must be a declared phase. The trunk-curated workflow needs at least one. Added in schema 1 by chant 0.124.34, so a declaration that uses it sets minReader to 0.124.34 or newer.",
|
|
1559
|
+
"items": {
|
|
1560
|
+
"$ref": "#/$defs/ciPhaseName"
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1523
1563
|
"signedSince": {
|
|
1524
1564
|
"type": "string",
|
|
1525
1565
|
"pattern": "^[0-9a-f]{40}([0-9a-f]{24})?$",
|
|
@@ -1538,6 +1578,10 @@
|
|
|
1538
1578
|
},
|
|
1539
1579
|
"additionalProperties": false
|
|
1540
1580
|
},
|
|
1581
|
+
"ciWorkflow": {
|
|
1582
|
+
"enum": ["trunk-curated", "trunk-curated+apply-on-merge"],
|
|
1583
|
+
"description": "A prescribed delivery workflow (#4168). trunk-curated: a merge records the plan and deploys nothing; releases are cut from green commits and promoted through each member's environments, gated in the last. trunk-curated+apply-on-merge: the same, with the apply on merge turned back on. chant workspace check reports what a member under one still lacks (WSP151), and chant ci init writes it. At the top level for every member, or on a member, which wins. Added in schema 1 by chant 0.124.34, so a declaration that uses it sets minReader to 0.124.34 or newer."
|
|
1584
|
+
},
|
|
1541
1585
|
"ciPhaseName": {
|
|
1542
1586
|
"type": "string",
|
|
1543
1587
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$"
|
|
@@ -1562,6 +1606,13 @@
|
|
|
1562
1606
|
"fail"
|
|
1563
1607
|
],
|
|
1564
1608
|
"description": "What a check run that finished as skipped counts as. fail (the default): it never passes. pass: it passes, for a job a path filter runs only on some commits."
|
|
1609
|
+
},
|
|
1610
|
+
"build": {
|
|
1611
|
+
"enum": [
|
|
1612
|
+
"every",
|
|
1613
|
+
"newest"
|
|
1614
|
+
],
|
|
1615
|
+
"description": "Added by #4169, read by chant 0.124.33 and newer. Which first-parent commits of the branch the phase runs on. every (the default): each one. newest: only the newest commit when a runner frees. Under either, a run that was cancelled leaves the commit unbuilt rather than red. Under newest, a commit a newer run superseded is unbuilt too, and a red commit starts a bisect over the unbuilt commits back to the last one the phase passed on. chant ci workflow writes the triggers and concurrency of the workflows holding a phase that sets build, and leaves them alone for a phase that does not."
|
|
1565
1616
|
}
|
|
1566
1617
|
},
|
|
1567
1618
|
"patternProperties": {
|
|
@@ -459,6 +459,23 @@ describe("ci.reportUrl (#4160)", () => {
|
|
|
459
459
|
});
|
|
460
460
|
});
|
|
461
461
|
|
|
462
|
+
describe("security (#4168)", () => {
|
|
463
|
+
test("the groups are read in file order, empty when absent, and securityGroupsOf reads them", async () => {
|
|
464
|
+
const { securityGroupsOf } = await import("./security");
|
|
465
|
+
expect(parse(base([])).security).toEqual([]);
|
|
466
|
+
const decl = parse(base([], { security: ["supply-chain", "verified-releases"] }));
|
|
467
|
+
expect(decl.security).toEqual(["supply-chain", "verified-releases"]);
|
|
468
|
+
expect([...securityGroupsOf(decl)]).toEqual(["supply-chain", "verified-releases"]);
|
|
469
|
+
expect(securityGroupsOf(null).size).toBe(0);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("an unknown group, a repeated group and a non-array are refused", () => {
|
|
473
|
+
for (const security of [["signed"], ["supply-chain", "supply-chain"], "supply-chain"]) {
|
|
474
|
+
expect(failure(base([], { security })).code).toBe("declaration-invalid");
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
462
479
|
describe("metrics (#4039)", () => {
|
|
463
480
|
test("failureWindowDays is read as written, without x- keys, and the block is null when absent", () => {
|
|
464
481
|
expect(parse(base([])).metrics).toBeNull();
|