@intentius/chant 0.124.30 → 0.124.32
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/bin/chant +7 -2
- package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +10 -7
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.js +18 -12
- package/dist/cli/commands/import.d.ts +10 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/onboard.js +8 -4
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +2 -1
- package/dist/cli/dependencies.d.ts +18 -0
- package/dist/cli/dependencies.d.ts.map +1 -0
- package/dist/cli/dependencies.js +120 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +23 -4
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +12 -4
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +16 -6
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lint.js +6 -0
- package/dist/cli/handlers/operator.d.ts +2 -2
- package/dist/cli/handlers/operator.js +3 -3
- package/dist/cli/handlers/pr.d.ts +14 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +488 -32
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +3 -1
- package/dist/cli/handlers/run.d.ts +13 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +38 -15
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +74 -12
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +65 -0
- package/dist/cli/registry.d.ts +29 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/registry.js +6 -2
- package/dist/codegen/generate-post-synth-barrel.js +2 -2
- package/dist/codegen/lexicon-regen.d.ts +4 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.js +1 -1
- package/dist/codegen/pinned-upgrade.js +2 -2
- package/dist/components/capability.d.ts +44 -3
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/capability.js +21 -0
- package/dist/components/changed-files.d.ts +7 -1
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/changed-files.js +9 -1
- package/dist/components/driver.d.ts +38 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +47 -6
- package/dist/components/fan-out-record.d.ts +6 -1
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +24 -0
- package/dist/components/fan-out-run.d.ts +20 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +21 -1
- package/dist/components/pr-pipeline.d.ts +65 -2
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +107 -15
- package/dist/components/pr-run.d.ts +127 -29
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +290 -70
- package/dist/components/wave-plan.d.ts +5 -2
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +12 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +16 -6
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.js +6 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.js +8 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/fold.js +68 -1
- package/dist/fold/subset.d.ts +37 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/fold/subset.js +88 -0
- package/dist/lexicon-module.d.ts +10 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +53 -8
- package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
- package/dist/lexicon-plugin-helpers.js +6 -4
- package/dist/lexicon.d.ts +2 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/affected.d.ts.map +1 -1
- package/dist/lifecycle/affected.js +7 -1
- package/dist/lifecycle/release-ledger.d.ts +21 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +24 -0
- package/dist/lifecycle/work-dir.d.ts +45 -0
- package/dist/lifecycle/work-dir.d.ts.map +1 -0
- package/dist/lifecycle/work-dir.js +78 -0
- package/dist/lint/discover.d.ts +17 -9
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +62 -54
- package/dist/module-source.d.ts +57 -0
- package/dist/module-source.d.ts.map +1 -0
- package/dist/module-source.js +104 -0
- package/dist/op/activities/factory.js +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +13 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/gate.js +3 -0
- package/dist/op/local-executor.d.ts +12 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -3
- package/dist/op/op-waves-run.d.ts.map +1 -1
- package/dist/op/op-waves-run.js +20 -5
- package/dist/op/operator.d.ts +2 -2
- package/dist/op/operator.js +2 -2
- package/dist/op/redact.d.ts +34 -0
- package/dist/op/redact.d.ts.map +1 -0
- package/dist/op/redact.js +94 -0
- package/dist/op/step-console.d.ts +2 -0
- package/dist/op/step-console.d.ts.map +1 -1
- package/dist/op/step-console.js +5 -2
- package/dist/op/steward.d.ts +6 -4
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/steward.js +5 -3
- package/dist/op/work-lease-run.d.ts +20 -7
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/op/work-lease-run.js +56 -12
- package/dist/pr-forge.d.ts +27 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +51 -6
- package/dist/pr-loop.d.ts +141 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +122 -6
- package/dist/pr-report-url.d.ts +10 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +52 -6
- package/dist/ts-loader.d.ts +38 -4
- package/dist/ts-loader.d.ts.map +1 -1
- package/dist/ts-loader.js +190 -16
- package/dist/wave-runtime.mjs +208 -208
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -1
- package/dist/workspace/chant-migrations.js +2 -0
- package/dist/workspace/checks/boxes.d.ts +6 -6
- package/dist/workspace/checks/boxes.js +9 -9
- package/dist/workspace/ci-green-forge.d.ts +6 -4
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +28 -12
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +49 -21
- package/dist/workspace/ci-list.schema.json +12 -6
- package/dist/workspace/ci-report-url.d.ts +18 -0
- package/dist/workspace/ci-report-url.d.ts.map +1 -0
- package/dist/workspace/ci-report-url.js +33 -0
- package/dist/workspace/ci-run-index.d.ts +108 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +164 -0
- package/dist/workspace/ci-run-index.schema.json +52 -0
- package/dist/workspace/ci-run.d.ts +159 -9
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +419 -50
- package/dist/workspace/ci-runner-key.d.ts +133 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +237 -0
- package/dist/workspace/ci-secrets.d.ts +65 -0
- package/dist/workspace/ci-secrets.d.ts.map +1 -0
- package/dist/workspace/ci-secrets.js +211 -0
- package/dist/workspace/ci-serve.d.ts +67 -4
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +240 -35
- package/dist/workspace/declaration.d.ts +8 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +11 -2
- package/dist/workspace/declaration.schema.json +7 -2
- package/dist/workspace/export.d.ts +1 -1
- package/dist/workspace/export.d.ts.map +1 -1
- package/dist/workspace/export.js +7 -3
- package/dist/workspace/ls.d.ts +6 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -0
- package/dist/workspace/ls.schema.json +7 -1
- package/dist/workspace/pin-integrity.d.ts.map +1 -1
- package/dist/workspace/pin-integrity.js +3 -2
- package/dist/workspace/pr-report.schema.json +74 -7
- package/dist/workspace/reason-codes.d.ts +7 -3
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +9 -4
- package/dist/workspace/run-event.schema.json +3 -1
- package/dist/workspace/status.schema.json +3 -1
- package/dist/workspace/trust/trust-write.d.ts +109 -0
- package/dist/workspace/trust/trust-write.d.ts.map +1 -0
- package/dist/workspace/trust/trust-write.js +320 -0
- package/dist/workspace/trust-write.schema.json +265 -0
- package/dist/workspace/write-scope.d.ts +9 -0
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/workspace/write-scope.js +11 -1
- package/package.json +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
- package/src/cli/commands/audit.test.ts +2 -2
- package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
- package/src/cli/commands/check-lexicon-plugin.ts +11 -8
- package/src/cli/commands/check-lexicon.test.ts +1 -1
- package/src/cli/commands/check-lexicon.ts +16 -11
- package/src/cli/commands/import.ts +40 -0
- package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
- package/src/cli/commands/init-lexicon.test.ts +8 -5
- package/src/cli/commands/onboard.test.ts +45 -0
- package/src/cli/commands/onboard.ts +8 -4
- package/src/cli/commands/update.test.ts +6 -0
- package/src/cli/commands/update.ts +4 -1
- package/src/cli/compiled-cli.e2e.test.ts +76 -11
- package/src/cli/dependencies.e2e.test.ts +101 -0
- package/src/cli/dependencies.test.ts +93 -0
- package/src/cli/dependencies.ts +122 -0
- package/src/cli/handlers/build.test.ts +69 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +73 -0
- package/src/cli/handlers/fan-out.ts +11 -4
- package/src/cli/handlers/graph.test.ts +76 -0
- package/src/cli/handlers/graph.ts +19 -12
- package/src/cli/handlers/lint.test.ts +42 -0
- package/src/cli/handlers/lint.ts +6 -0
- package/src/cli/handlers/operator.ts +3 -3
- package/src/cli/handlers/pr-waves.test.ts +449 -0
- package/src/cli/handlers/pr.test.ts +134 -0
- package/src/cli/handlers/pr.ts +523 -31
- package/src/cli/handlers/promote.test.ts +26 -1
- package/src/cli/handlers/promote.ts +3 -2
- package/src/cli/handlers/run-components-exit.test.ts +99 -0
- package/src/cli/handlers/run-signer.test.ts +79 -0
- package/src/cli/handlers/run.ts +34 -16
- package/src/cli/main.test.ts +19 -0
- package/src/cli/main.ts +73 -12
- package/src/cli/parse-args.ts +48 -0
- package/src/cli/registry.ts +35 -4
- package/src/codegen/generate-post-synth-barrel.ts +2 -2
- package/src/codegen/lexicon-regen.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -2
- package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
- package/src/components/capability.ts +51 -3
- package/src/components/changed-files.test.ts +16 -1
- package/src/components/changed-files.ts +9 -1
- package/src/components/driver.test.ts +36 -1
- package/src/components/driver.ts +76 -4
- package/src/components/fan-out-record.ts +29 -1
- package/src/components/fan-out-run.test.ts +35 -0
- package/src/components/fan-out-run.ts +40 -1
- package/src/components/pr-pipeline.test.ts +100 -0
- package/src/components/pr-pipeline.ts +132 -17
- package/src/components/pr-run.test.ts +323 -18
- package/src/components/pr-run.ts +354 -75
- package/src/components/wave-plan.ts +16 -4
- package/src/discovery/fold-host-method.test.ts +87 -0
- package/src/discovery/fold-import.ts +15 -5
- package/src/discovery/fold-package-composite.test.ts +27 -0
- package/src/discovery/fold-specifiers.test.ts +8 -0
- package/src/discovery/sandbox/bundle.ts +6 -0
- package/src/discovery/specifier-resolve.ts +8 -2
- package/src/fold/fold.test.ts +72 -0
- package/src/fold/fold.ts +71 -0
- package/src/fold/subset.test.ts +31 -0
- package/src/fold/subset.ts +99 -0
- package/src/lexicon-dist.e2e.test.ts +254 -0
- package/src/lexicon-module.test.ts +47 -0
- package/src/lexicon-module.ts +49 -7
- package/src/lexicon-plugin-helpers.ts +6 -4
- package/src/lexicon.ts +2 -0
- package/src/lifecycle/affected.test.ts +7 -0
- package/src/lifecycle/affected.ts +6 -1
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lifecycle/work-dir.test.ts +77 -0
- package/src/lifecycle/work-dir.ts +82 -0
- package/src/lint/discover.test.ts +46 -2
- package/src/lint/discover.ts +55 -49
- package/src/module-source.test.ts +85 -0
- package/src/module-source.ts +112 -0
- package/src/op/activities/factory.ts +1 -1
- package/src/op/discover-root.test.ts +31 -2
- package/src/op/discover.ts +14 -4
- package/src/op/gate.test.ts +30 -0
- package/src/op/gate.ts +3 -0
- package/src/op/local-executor.ts +31 -3
- package/src/op/op-waves-run.ts +20 -5
- package/src/op/op-waves.test.ts +49 -0
- package/src/op/operator.ts +2 -2
- package/src/op/redact.ts +93 -0
- package/src/op/step-console.ts +7 -2
- package/src/op/steward.ts +6 -4
- package/src/op/work-lease-run.test.ts +88 -2
- package/src/op/work-lease-run.ts +52 -12
- package/src/pr-forge-bundle.test.ts +2 -2
- package/src/pr-forge.test.ts +71 -0
- package/src/pr-forge.ts +70 -6
- package/src/pr-loop-bundle.test.ts +2 -1
- package/src/pr-loop.test.ts +117 -1
- package/src/pr-loop.ts +252 -10
- package/src/pr-report-url.ts +58 -6
- package/src/terraform/__fixtures__/advise.test.ts +21 -2
- package/src/ts-loader.test.ts +89 -1
- package/src/ts-loader.ts +180 -13
- package/src/workspace/changes.test.ts +1 -1
- package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
- package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
- package/src/workspace/chant-migrations.e2e.test.ts +5 -0
- package/src/workspace/chant-migrations.ts +2 -0
- package/src/workspace/checks/boxes.ts +9 -9
- package/src/workspace/ci-green-forge.ts +6 -4
- package/src/workspace/ci-green-runs.test.ts +53 -11
- package/src/workspace/ci-green.test.ts +1 -1
- package/src/workspace/ci-green.ts +50 -22
- package/src/workspace/ci-list.schema.json +12 -6
- package/src/workspace/ci-report-url.test.ts +220 -0
- package/src/workspace/ci-report-url.ts +33 -0
- package/src/workspace/ci-run-index.schema.json +52 -0
- package/src/workspace/ci-run-index.test.ts +392 -0
- package/src/workspace/ci-run-index.ts +206 -0
- package/src/workspace/ci-run.test.ts +520 -42
- package/src/workspace/ci-run.ts +483 -51
- package/src/workspace/ci-runner-key.test.ts +204 -0
- package/src/workspace/ci-runner-key.ts +275 -0
- package/src/workspace/ci-secrets.test.ts +106 -0
- package/src/workspace/ci-secrets.ts +213 -0
- package/src/workspace/ci-serve.test.ts +282 -39
- package/src/workspace/ci-serve.ts +231 -34
- package/src/workspace/declaration.schema.json +7 -2
- package/src/workspace/declaration.test.ts +23 -0
- package/src/workspace/declaration.ts +19 -3
- package/src/workspace/export.ts +6 -3
- package/src/workspace/ls.schema.json +7 -1
- package/src/workspace/ls.ts +7 -2
- package/src/workspace/pin-integrity.test.ts +5 -0
- package/src/workspace/pin-integrity.ts +3 -2
- package/src/workspace/pr-report.schema.json +74 -7
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +9 -4
- package/src/workspace/run-event.schema.json +3 -1
- package/src/workspace/status.schema.json +3 -1
- package/src/workspace/trust/trust-write.test.ts +179 -0
- package/src/workspace/trust/trust-write.ts +377 -0
- package/src/workspace/trust-write.schema.json +265 -0
- package/src/workspace/write-scope.test.ts +29 -1
- package/src/workspace/write-scope.ts +12 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #4044: chant run results as a status source for `ci tick`. With
|
|
3
3
|
* `ci.green.source: "runs"` (or `--source runs`) the tick reads Op runs from
|
|
4
|
-
* the run ledger on `chant/lifecycle` instead of a forge's check runs.
|
|
5
|
-
*
|
|
6
|
-
* `.chant/trust.json` lists for a principal
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* the run ledger on `chant/lifecycle` instead of a forge's check runs.
|
|
5
|
+
* Signing is opt-in (#4156): an unsigned run counts with no signer, and a run
|
|
6
|
+
* whose statement a runner key `.chant/trust.json` lists for a principal
|
|
7
|
+
* holding the `runner` role signed counts with that signer. A statement that
|
|
8
|
+
* does not verify is ignored and reported. With `ci.green.signedSince` an
|
|
9
|
+
* unsigned run on that commit or after it is ignored too. A tick given a
|
|
10
|
+
* runner key signs its tags, and every tag names the runs behind it.
|
|
9
11
|
*
|
|
10
12
|
* Each test works in its own temp repository with a bare origin, and every
|
|
11
13
|
* key is a throwaway one. Nothing is written in process.cwd().
|
|
@@ -149,19 +151,61 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
149
151
|
expect((await ciTick({ cwd: root, now: NOW, runnerKey: BOT.pem })).made).toEqual([]);
|
|
150
152
|
});
|
|
151
153
|
|
|
152
|
-
test("an unsigned run
|
|
154
|
+
test("an unsigned run counts with no signer: with no key anywhere the tick tags the commit and ci list shows it (#4156)", async () => {
|
|
153
155
|
const { root } = workspace();
|
|
154
156
|
const a = commitAt(root, 3, "app/a.txt");
|
|
157
|
+
const lint = await runOp(root, "lint");
|
|
158
|
+
await runOp(root, "test");
|
|
159
|
+
expect(lint.statement).toBeUndefined();
|
|
160
|
+
push(root);
|
|
161
|
+
|
|
162
|
+
const list = (await ciList({ cwd: root, now: NOW })) as Extract<Awaited<ReturnType<typeof ciList>>, { commits: unknown }>;
|
|
163
|
+
readList.expectValid(list);
|
|
164
|
+
const listed = list.commits.find((c) => c.sha === a)!;
|
|
165
|
+
expect(listed.phases.map((p) => [p.phase, p.result])).toEqual([
|
|
166
|
+
["lint", "pass"],
|
|
167
|
+
["test", "pass"],
|
|
168
|
+
]);
|
|
169
|
+
expect(listed.phases[0]!.runs[0]).toMatchObject({ name: "lint", id: null, opRun: { id: lint.id, env: "ci", signer: null } });
|
|
170
|
+
expect(listed.ignoredRuns).toEqual([]);
|
|
171
|
+
|
|
172
|
+
// No runner key: the tick still tags, unsigned, and the tag names the unsigned runs.
|
|
173
|
+
const tick = await ciTick({ cwd: root, now: NOW });
|
|
174
|
+
expect(tick.made).toEqual([`ci/green/${a}`]);
|
|
175
|
+
expect(tick.signer).toBeNull();
|
|
176
|
+
const record = recordOf(root, `ci/green/${a}`) as { statement?: unknown; phases: Record<string, { opRun: Record<string, unknown> }[]> };
|
|
177
|
+
expect(record.statement).toBeUndefined();
|
|
178
|
+
expect(record.phases.lint![0]!.opRun).toEqual({ id: lint.id, env: "ci", signer: null });
|
|
179
|
+
|
|
180
|
+
// A signed run beside them shows its signer.
|
|
181
|
+
const signed = await runOp(root, "lint", { pem: BOT.pem });
|
|
182
|
+
const source = await readOpRunSource(root, root, git(root, "rev-parse", "HEAD"));
|
|
183
|
+
expect(source.checkRuns(a).map((r) => [r.opRun!.id, r.opRun!.signer])).toEqual([
|
|
184
|
+
[lint.id, null],
|
|
185
|
+
[signed.id, "ci-bot"],
|
|
186
|
+
[expect.any(String), null],
|
|
187
|
+
]);
|
|
188
|
+
expect(source.ignored.size).toBe(0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("with ci.green.signedSince an unsigned run neither greens nor revokes a commit from that one on, and is reported", async () => {
|
|
192
|
+
const { root } = workspace();
|
|
193
|
+
const a = commitAt(root, 3, "app/a.txt");
|
|
194
|
+
git(root, "push", "-q", "origin", "main");
|
|
195
|
+
// The opt-in: every run on a, and after it, must be signed.
|
|
196
|
+
writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { ...GREEN, signedSince: a } } }) });
|
|
155
197
|
await runOp(root, "lint", { pem: BOT.pem });
|
|
156
198
|
const unsigned = await runOp(root, "test");
|
|
157
199
|
expect(unsigned.statement).toBeUndefined();
|
|
158
200
|
push(root);
|
|
159
201
|
|
|
202
|
+
// The opt-in also asks for signed tags, so the tick needs the key.
|
|
203
|
+
await expect(ciTick({ cwd: root, now: NOW })).rejects.toThrow(/needs a statement signed by a runner key, and none was given/);
|
|
160
204
|
const tick = await ciTick({ cwd: root, now: NOW, runnerKey: BOT.pem });
|
|
161
205
|
expect(tick.made).toEqual([]);
|
|
162
206
|
const at = tick.commits.find((c) => c.sha === a)!;
|
|
163
207
|
expect(at.verdict?.verdict).toBe("pending");
|
|
164
|
-
expect(at.ignoredRuns).toEqual([{ run: unsigned.id, op: "test", env: "ci", status: "unsigned", reason: expect.stringContaining("
|
|
208
|
+
expect(at.ignoredRuns).toEqual([{ run: unsigned.id, op: "test", env: "ci", status: "unsigned", reason: expect.stringContaining("ci.green.signedSince is") }]);
|
|
165
209
|
expect(formatTick(tick)).toContain(`ignored run ${unsigned.id} of test in ci (unsigned)`);
|
|
166
210
|
expect(git(root, "tag", "-l")).toBe("");
|
|
167
211
|
|
|
@@ -173,7 +217,6 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
173
217
|
["test", "pending"],
|
|
174
218
|
]);
|
|
175
219
|
expect(listed.ignoredRuns?.map((r) => r.status)).toEqual(["unsigned"]);
|
|
176
|
-
expect(formatList(list)).toContain(`ignored run ${unsigned.id} of test in ci (unsigned)`);
|
|
177
220
|
|
|
178
221
|
// Signed now, the commit goes green; a later unsigned failure does not revoke it.
|
|
179
222
|
await runOp(root, "test", { pem: BOT.pem });
|
|
@@ -186,7 +229,7 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
186
229
|
expect(after.commits.find((c) => c.sha === a)!.ignoredRuns?.map((r) => r.status)).toEqual(["unsigned", "unsigned"]);
|
|
187
230
|
});
|
|
188
231
|
|
|
189
|
-
test("a run signed by an unlisted key, or whose record changed after signing, does not count", async () => {
|
|
232
|
+
test("a run signed by an unlisted key, or whose record changed after signing, does not count, with or without the opt-in", async () => {
|
|
190
233
|
const { root } = workspace();
|
|
191
234
|
const a = commitAt(root, 3, "app/a.txt");
|
|
192
235
|
// A key trust.json does not list can't sign a run here at all.
|
|
@@ -219,13 +262,12 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
|
|
|
219
262
|
expect(record).toMatchObject({ source: "runs", phase: "test", run: { name: "test", id: null, conclusion: "failure", opRun: { id: red.id, signer: "ci-bot" } } });
|
|
220
263
|
});
|
|
221
264
|
|
|
222
|
-
test("a tick from runs
|
|
265
|
+
test("a tick from runs without a runner key makes unsigned tags, --source overrides the declaration, and ci list refuses --forge", async () => {
|
|
223
266
|
const { root } = workspace({ ...GREEN, source: "checks" });
|
|
224
267
|
const a = commitAt(root, 3, "app/a.txt");
|
|
225
268
|
await runOp(root, "lint", { pem: BOT.pem });
|
|
226
269
|
await runOp(root, "test", { pem: BOT.pem });
|
|
227
270
|
push(root);
|
|
228
|
-
await expect(ciTick({ cwd: root, now: NOW, source: "runs" })).rejects.toThrow(/every tag it makes from them is signed by a runner key, and none was given/);
|
|
229
271
|
expect((await ciTick({ cwd: root, now: NOW, source: "runs", dryRun: true })).made).toEqual([`ci/green/${a}`]);
|
|
230
272
|
expect(git(root, "tag", "-l")).toBe("");
|
|
231
273
|
// The declaration says checks, so without --source the tick wants a forge.
|
|
@@ -51,7 +51,7 @@ describe("the ci.green block (#3573)", () => {
|
|
|
51
51
|
});
|
|
52
52
|
expect(parseDeclaration(decl({ green: { ...GREEN, signedSince: "a".repeat(40) } }), "chant.workspace.json").ci!.green!.signedSince).toBe("a".repeat(40));
|
|
53
53
|
expect(parseDeclaration(decl({ green: { ...GREEN, window: "90m" } }), "chant.workspace.json").ci!.green!.window).toBe("90m");
|
|
54
|
-
expect(parseDeclaration(decl({}), "chant.workspace.json").ci).toEqual({ green: null });
|
|
54
|
+
expect(parseDeclaration(decl({}), "chant.workspace.json").ci).toEqual({ green: null, reportUrl: null });
|
|
55
55
|
expect(parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [] }), "chant.workspace.json").ci).toBeNull();
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -41,12 +41,16 @@
|
|
|
41
41
|
* pipelines run as chant Ops, such as one on GitLab or Forgejo, gets green
|
|
42
42
|
* and revoked tags with no forge check runs. A phase's patterns then match
|
|
43
43
|
* Op names, a run counts for the commit its record names as `gitSha`, and
|
|
44
|
-
* the latest run of each Op decides, by its end time.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* the latest run of each Op decides, by its end time. Signing is opt-in
|
|
45
|
+
* (#4156): an unsigned run counts, with no signer, and a run whose statement
|
|
46
|
+
* a runner key holding the `runner` role at base signed counts with that
|
|
47
|
+
* signer (`./trust/op-run-statement.ts`). A run whose statement is there and
|
|
48
|
+
* does not verify is ignored and reported, so it neither greens nor revokes
|
|
49
|
+
* a commit. With `ci.green.signedSince`, an unsigned run counts only on a
|
|
50
|
+
* strict first-parent ancestor of that commit, as an unsigned tag does. A
|
|
51
|
+
* tick given a runner key signs the tags it makes, and each run in the
|
|
52
|
+
* tag's record names the Op run's id and carries its statement when it has
|
|
53
|
+
* one.
|
|
50
54
|
*/
|
|
51
55
|
|
|
52
56
|
import { judgeCommits, mergeCommits, readWorkIndex, type CommitLink } from "./commit-links";
|
|
@@ -237,7 +241,7 @@ export function windowMs(window: string): number {
|
|
|
237
241
|
*/
|
|
238
242
|
const runRecord = (r: RunVerdict) =>
|
|
239
243
|
r.opRun
|
|
240
|
-
? { name: r.name, id: null, conclusion: r.conclusion, url: r.url, opRun: { id: r.opRun.id, env: r.opRun.env, signer: r.opRun.signer, statement: r.opRun.statement } }
|
|
244
|
+
? { name: r.name, id: null, conclusion: r.conclusion, url: r.url, opRun: { id: r.opRun.id, env: r.opRun.env, signer: r.opRun.signer, ...(r.opRun.statement ? { statement: r.opRun.statement } : {}) } }
|
|
241
245
|
: { name: r.name, id: r.id, conclusion: r.conclusion, url: r.url };
|
|
242
246
|
|
|
243
247
|
/** `source: "runs"` on a record made from chant runs (#4044); nothing for check runs, so their records read as before. */
|
|
@@ -443,6 +447,21 @@ export function readCiTags(top: string, green: CiGreen | null = null, base: stri
|
|
|
443
447
|
return out;
|
|
444
448
|
}
|
|
445
449
|
|
|
450
|
+
/**
|
|
451
|
+
* Whether the workspace at `cwd` requires signed runs (#4156): its
|
|
452
|
+
* `ci.green.signedSince`, or null when it sets none, declares no
|
|
453
|
+
* `ci.green`, or has no declaration chant can read. Signing is opt-in, so
|
|
454
|
+
* only this makes `ci serve` refuse to start without a trusted runner key
|
|
455
|
+
* and `ci run` and `chant run` warn about a run they leave unsigned.
|
|
456
|
+
*/
|
|
457
|
+
export function signedRunsRequired(cwd: string): string | null {
|
|
458
|
+
try {
|
|
459
|
+
return readDeclaration(locateWorkspace(cwd).tree).ci?.green?.signedSince ?? null;
|
|
460
|
+
} catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
446
465
|
/** The declaration's `ci.green`, read where `cwd` is, and the git top. */
|
|
447
466
|
function readGreen(cwd: string): { green: CiGreen; top: string; workspace: { name: string; root: string; file: string } } {
|
|
448
467
|
const located = locateWorkspace(cwd);
|
|
@@ -553,9 +572,6 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
553
572
|
`ci.green.signedSince is ${green.signedSince}, so every tag the tick makes needs a statement signed by a runner key, and none was given: pass --key <runner key.pem> or set CHANT_RUNNER_KEY`,
|
|
554
573
|
);
|
|
555
574
|
}
|
|
556
|
-
if (green.source === "runs" && req.runnerKey === undefined && !req.dryRun) {
|
|
557
|
-
throw new Error("the tick reads chant runs (source runs), and every tag it makes from them is signed by a runner key, and none was given: pass --key <runner key.pem> or set CHANT_RUNNER_KEY");
|
|
558
|
-
}
|
|
559
575
|
const signer = req.runnerKey !== undefined ? ciTagSigner(top, base, req.runnerKey) : undefined;
|
|
560
576
|
const links = await commitLinks(req.cwd, top, tracking, commits.map((c) => c.sha));
|
|
561
577
|
const tags = readCiTags(top, green, base);
|
|
@@ -566,7 +582,7 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
566
582
|
if (fetched.status === "diverged" || fetched.status === "unreachable") {
|
|
567
583
|
throw new Error(`could not fetch chant/lifecycle, where the Op runs are recorded, from ${fetched.remote}: ${fetched.status === "diverged" ? "the local branch and the remote's have diverged" : fetched.stderr}`);
|
|
568
584
|
}
|
|
569
|
-
runs = await readOpRunSource(req.cwd, top, base);
|
|
585
|
+
runs = await readOpRunSource(req.cwd, top, base, green.signedSince);
|
|
570
586
|
}
|
|
571
587
|
const planned = await planTick({
|
|
572
588
|
green,
|
|
@@ -605,7 +621,7 @@ export async function ciTick(req: TickRequest): Promise<TickResult> {
|
|
|
605
621
|
|
|
606
622
|
/** The Op runs on the ledger, judged, as check runs by commit. */
|
|
607
623
|
export interface OpRunSource {
|
|
608
|
-
/** The
|
|
624
|
+
/** The runs on `sha` that count, each as a check run named for its Op. */
|
|
609
625
|
checkRuns(sha: string): CheckRun[];
|
|
610
626
|
/** The runs on each commit that do not count, by commit. */
|
|
611
627
|
ignored: Map<string, IgnoredOpRun[]>;
|
|
@@ -619,7 +635,7 @@ const RUN_LEDGER_FILE = /^runs__(.+)\.jsonl$/;
|
|
|
619
635
|
* going. The id is the run's end time in milliseconds, so the latest run of
|
|
620
636
|
* an Op is its latest attempt.
|
|
621
637
|
*/
|
|
622
|
-
export function opRunCheckRun(record: OpRunRecord, signer: string): CheckRun {
|
|
638
|
+
export function opRunCheckRun(record: OpRunRecord, signer: string | null): CheckRun {
|
|
623
639
|
const settled = record.status === "ok" || record.status === "fail";
|
|
624
640
|
const ended = Date.parse(record.ended);
|
|
625
641
|
return {
|
|
@@ -629,7 +645,7 @@ export function opRunCheckRun(record: OpRunRecord, signer: string): CheckRun {
|
|
|
629
645
|
conclusion: settled ? (record.status === "ok" ? "success" : "failure") : null,
|
|
630
646
|
completedAt: settled ? record.ended : null,
|
|
631
647
|
url: record.runOrigin?.url ?? null,
|
|
632
|
-
opRun: { id: record.id, env: record.env, signer, statement: record.statement
|
|
648
|
+
opRun: { id: record.id, env: record.env, signer, ...(signer !== null && record.statement ? { statement: record.statement } : {}) },
|
|
633
649
|
...(record.status === "fail" && record.failure ? { failureCode: record.failure.code } : {}),
|
|
634
650
|
};
|
|
635
651
|
}
|
|
@@ -637,11 +653,16 @@ export function opRunCheckRun(record: OpRunRecord, signer: string): CheckRun {
|
|
|
637
653
|
/**
|
|
638
654
|
* Judge every Op run record that names a commit against `.chant/trust.json`
|
|
639
655
|
* at `base` (#4044), from the run ledger the project at `cwd` writes to. A
|
|
640
|
-
* record with a valid statement becomes a check run on its `gitSha
|
|
641
|
-
*
|
|
656
|
+
* record with a valid statement becomes a check run on its `gitSha` with
|
|
657
|
+
* its signer, and an unsigned one becomes a check run with signer null
|
|
658
|
+
* (#4156). A record whose statement does not verify is kept as an ignored
|
|
659
|
+
* run. With `signedSince` (`ci.green.signedSince`) an unsigned record counts
|
|
660
|
+
* only on a strict first-parent ancestor of that commit, and is ignored
|
|
661
|
+
* elsewhere. Reads local refs only.
|
|
642
662
|
*/
|
|
643
|
-
export async function readOpRunSource(cwd: string, top: string, base: string): Promise<OpRunSource> {
|
|
663
|
+
export async function readOpRunSource(cwd: string, top: string, base: string, signedSince: string | null = null): Promise<OpRunSource> {
|
|
644
664
|
const policy = readTrustPolicy(gitRevisionSource(top, base), base);
|
|
665
|
+
const before = signedSince ? unsignedHistory(top, signedSince) : null;
|
|
645
666
|
const byCommit = new Map<string, CheckRun[]>();
|
|
646
667
|
const ignored = new Map<string, IgnoredOpRun[]>();
|
|
647
668
|
for (const env of await listLedgerTrees({ cwd })) {
|
|
@@ -651,9 +672,15 @@ export async function readOpRunSource(cwd: string, top: string, base: string): P
|
|
|
651
672
|
const { records } = await readRunLedger(env, op, { cwd });
|
|
652
673
|
for (const record of records) {
|
|
653
674
|
if (!record.gitSha) continue;
|
|
675
|
+
// #4161: a job `ci run --local` ran stays on the machine that ran it, and is never CI's verdict.
|
|
676
|
+
if (record.runOrigin?.forge === "local") continue;
|
|
654
677
|
const v = verifyOpRunStatement(record, policy.runners, policy.roles);
|
|
655
|
-
|
|
656
|
-
|
|
678
|
+
const counts = v.status === "signed" || (v.status === "unsigned" && (before === null || before.has(record.gitSha)));
|
|
679
|
+
if (counts) byCommit.set(record.gitSha, [...(byCommit.get(record.gitSha) ?? []), opRunCheckRun(record, v.status === "signed" ? v.signer : null)]);
|
|
680
|
+
else {
|
|
681
|
+
const reason = v.status === "unsigned" ? `${v.reason}; ci.green.signedSince is ${signedSince}, so a run on a commit that is not a strict first-parent ancestor of it needs one` : v.reason;
|
|
682
|
+
ignored.set(record.gitSha, [...(ignored.get(record.gitSha) ?? []), { run: record.id, op: record.op, env: record.env, status: v.status, reason }]);
|
|
683
|
+
}
|
|
657
684
|
}
|
|
658
685
|
}
|
|
659
686
|
}
|
|
@@ -836,7 +863,7 @@ export interface CiListRun {
|
|
|
836
863
|
conclusion: string | null;
|
|
837
864
|
url: string | null;
|
|
838
865
|
/** The Op run, when the run is a chant run (#4044): its id, env and the runner whose statement it carries. Absent for a check run. */
|
|
839
|
-
opRun?: { id: string; env: string; signer: string };
|
|
866
|
+
opRun?: { id: string; env: string; signer: string | null };
|
|
840
867
|
}
|
|
841
868
|
|
|
842
869
|
export interface CiListPhase {
|
|
@@ -895,7 +922,8 @@ const asRun = (r: unknown): CiListRun | null => {
|
|
|
895
922
|
const o = r as Record<string, unknown>;
|
|
896
923
|
if (typeof o.name !== "string") return null;
|
|
897
924
|
const op = o.opRun as Record<string, unknown> | undefined;
|
|
898
|
-
const
|
|
925
|
+
const signer = op && typeof op === "object" ? (typeof op.signer === "string" ? op.signer : op.signer === null ? null : undefined) : undefined;
|
|
926
|
+
const opRun = op && typeof op === "object" && typeof op.id === "string" && typeof op.env === "string" && signer !== undefined ? { opRun: { id: op.id, env: op.env, signer } } : {};
|
|
899
927
|
return { name: o.name, id: typeof o.id === "number" ? o.id : null, conclusion: typeof o.conclusion === "string" ? o.conclusion : null, url: typeof o.url === "string" ? o.url : null, ...opRun };
|
|
900
928
|
};
|
|
901
929
|
|
|
@@ -973,7 +1001,7 @@ export async function ciList(req: ListRequest): Promise<CiListDocument> {
|
|
|
973
1001
|
const tags = readCiTags(top, green, base);
|
|
974
1002
|
const source = req.source ?? green.source;
|
|
975
1003
|
if (source === "runs" && req.forge) throw new Error("--forge reads check runs, and the source is chant runs (ci.green.source or --source runs); drop one");
|
|
976
|
-
const runs = source === "runs" ? await readOpRunSource(req.cwd, top, base) : undefined;
|
|
1004
|
+
const runs = source === "runs" ? await readOpRunSource(req.cwd, top, base, green.signedSince) : undefined;
|
|
977
1005
|
const out: CiListCommit[] = [];
|
|
978
1006
|
for (const c of commits) {
|
|
979
1007
|
const g = tags.green.get(c.sha);
|
|
@@ -240,8 +240,11 @@
|
|
|
240
240
|
"description": "The env the run is recorded under."
|
|
241
241
|
},
|
|
242
242
|
"signer": {
|
|
243
|
-
"type":
|
|
244
|
-
|
|
243
|
+
"type": [
|
|
244
|
+
"string",
|
|
245
|
+
"null"
|
|
246
|
+
],
|
|
247
|
+
"description": "The runner principal whose statement the run carries. null for an unsigned run, which counts unless ci.green.signedSince asks for a statement (#4156, chant 0.124.32 and newer)."
|
|
245
248
|
}
|
|
246
249
|
},
|
|
247
250
|
"description": "Added within contract version 1 by #4044, written by chant 0.124.28 and newer. Present only when the run is a chant Op run (ci.green.source runs): name is then the Op's name and id is null. Absent for a check run."
|
|
@@ -334,8 +337,11 @@
|
|
|
334
337
|
"description": "The env the run is recorded under."
|
|
335
338
|
},
|
|
336
339
|
"signer": {
|
|
337
|
-
"type":
|
|
338
|
-
|
|
340
|
+
"type": [
|
|
341
|
+
"string",
|
|
342
|
+
"null"
|
|
343
|
+
],
|
|
344
|
+
"description": "The runner principal whose statement the run carries. null for an unsigned run, which counts unless ci.green.signedSince asks for a statement (#4156, chant 0.124.32 and newer)."
|
|
339
345
|
}
|
|
340
346
|
},
|
|
341
347
|
"description": "Added within contract version 1 by #4044, written by chant 0.124.28 and newer. Present only when the run is a chant Op run (ci.green.source runs): name is then the Op's name and id is null. Absent for a check run."
|
|
@@ -432,7 +438,7 @@
|
|
|
432
438
|
},
|
|
433
439
|
"ignoredRuns": {
|
|
434
440
|
"type": "array",
|
|
435
|
-
"description": "Added within contract version 1 by #4044, written by chant 0.124.28 and newer. Present only when the source is chant runs (ci.green.source or --source runs): the Op runs on the commit that do not count
|
|
441
|
+
"description": "Added within contract version 1 by #4044, written by chant 0.124.28 and newer. Present only when the source is chant runs (ci.green.source or --source runs): the Op runs on the commit that do not count: a statement that does not verify (invalid, untrusted or mismatch), or, under ci.green.signedSince, no statement on a commit that needs one (#4156). They neither green nor revoke the commit.",
|
|
436
442
|
"items": {
|
|
437
443
|
"type": "object",
|
|
438
444
|
"required": [
|
|
@@ -460,7 +466,7 @@
|
|
|
460
466
|
"untrusted",
|
|
461
467
|
"mismatch"
|
|
462
468
|
],
|
|
463
|
-
"description": "unsigned: the record carries no statement. invalid: the statement is not an op-run statement, or names another signer than the key's. untrusted: no runner key .chant/trust.json lists at base signed it, or its principal holds no runner role. mismatch: it signs another run, commit or record than the ledger holds."
|
|
469
|
+
"description": "unsigned: the record carries no statement, on a commit ci.green.signedSince asks one for (#4156: without signedSince an unsigned run counts). invalid: the statement is not an op-run statement, or names another signer than the key's. untrusted: no runner key .chant/trust.json lists at base signed it, or its principal holds no runner role. mismatch: it signs another run, commit or record than the ledger holds."
|
|
464
470
|
},
|
|
465
471
|
"reason": {
|
|
466
472
|
"type": "string"
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #4160: the report URL a workspace declares as `ci.reportUrl`. `chant ci
|
|
3
|
+
* run`, `ci serve` and `ci runs` use it when `--report-url` is not given, the
|
|
4
|
+
* flag wins, and `chant workspace ls --json` carries it for readers.
|
|
5
|
+
*
|
|
6
|
+
* The jobs are real `ci run` jobs in a temp repository with a stub chant and
|
|
7
|
+
* file:// report URLs in other temp directories. Nothing is pushed and no
|
|
8
|
+
* test makes a network call: `ci serve` is stopped by its first poll, which
|
|
9
|
+
* has no forge to reach.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { execFileSync } from "node:child_process";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
|
17
|
+
import { cleanScratch, contract, declaration, git, repoFixture, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
18
|
+
import { parseArgs } from "../cli/parse-args";
|
|
19
|
+
import type { CommandContext } from "../cli/registry";
|
|
20
|
+
import { declaredReportUrl, reportUrlOf } from "./ci-report-url";
|
|
21
|
+
import { runCiRun } from "./ci-run";
|
|
22
|
+
import { runCiServe } from "./ci-serve";
|
|
23
|
+
import { CI_RUN_INDEX_KEY, runCiRuns, type CiRunIndex } from "./ci-run-index";
|
|
24
|
+
import { listWorkspace, type LsDocument } from "./ls";
|
|
25
|
+
import lsSchema from "./ls.schema.json";
|
|
26
|
+
|
|
27
|
+
const home = process.cwd();
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
process.chdir(home);
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
vi.unstubAllEnvs();
|
|
32
|
+
cleanScratch();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const MEMBERS = [{ name: "app", dir: "app", kind: "other", because: "the app" }];
|
|
36
|
+
const urlOf = (dir: string) => pathToFileURL(dir).href;
|
|
37
|
+
const reportsDir = () => scratchDir("chant-report-url-reports-");
|
|
38
|
+
const indexOf = (dir: string): CiRunIndex => JSON.parse(readFileSync(join(dir, CI_RUN_INDEX_KEY), "utf-8"));
|
|
39
|
+
|
|
40
|
+
/** One member and two commits. The declaration in the working tree is rewritten per test. */
|
|
41
|
+
const repoTemplate = repoFixture(() => {
|
|
42
|
+
const root = scratchDir("chant-report-url-template-");
|
|
43
|
+
git(root, "init", "-q");
|
|
44
|
+
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
45
|
+
git(root, "config", "user.name", "t");
|
|
46
|
+
git(root, "config", "user.email", "t@example.com");
|
|
47
|
+
writeFiles(root, { "chant.workspace.json": declaration(MEMBERS), ".gitignore": ".chant/pr/\n", "app/a.txt": "a\n" });
|
|
48
|
+
const commit = (msg: string) => {
|
|
49
|
+
execFileSync("git", ["-c", "commit.gpgsign=false", "add", "-A"], { cwd: root, stdio: "ignore" });
|
|
50
|
+
execFileSync("git", ["-c", "commit.gpgsign=false", "commit", "-q", "-m", msg], { cwd: root, stdio: "ignore" });
|
|
51
|
+
};
|
|
52
|
+
commit("init");
|
|
53
|
+
writeFiles(root, { "app/a.txt": "b\n" });
|
|
54
|
+
commit("change");
|
|
55
|
+
return root;
|
|
56
|
+
}, "chant-report-url-");
|
|
57
|
+
|
|
58
|
+
/** A copy of the repository whose declaration sets `ci` as given, and a stub chant that writes an empty report. */
|
|
59
|
+
function workspace(ci?: Record<string, unknown>): { root: string; app: string; base: string; chant: string } {
|
|
60
|
+
const root = repoTemplate();
|
|
61
|
+
writeFiles(root, { "chant.workspace.json": declaration(MEMBERS, ci ? { ci } : {}) });
|
|
62
|
+
const chant = join(scratchDir("chant-report-url-bin-"), "chant");
|
|
63
|
+
writeFileSync(chant, ["#!/bin/sh", "mkdir -p .chant/pr", 'echo "{}" > ".chant/pr/$2.json"', "exit 0", ""].join("\n"));
|
|
64
|
+
chmodSync(chant, 0o755);
|
|
65
|
+
return { root, app: join(root, "app"), base: git(root, "rev-parse", "HEAD~1"), chant };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run a ci command line in `cwd`, with what it printed. */
|
|
69
|
+
async function cli(cwd: string, argv: string[], run: (ctx: CommandContext) => Promise<number>) {
|
|
70
|
+
process.chdir(cwd);
|
|
71
|
+
const out: string[] = [];
|
|
72
|
+
const err: string[] = [];
|
|
73
|
+
vi.spyOn(console, "log").mockImplementation((s: string) => void out.push(String(s)));
|
|
74
|
+
vi.spyOn(console, "error").mockImplementation((s: string) => void err.push(String(s)));
|
|
75
|
+
vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
76
|
+
const code = await run({ args: parseArgs(argv) } as unknown as CommandContext);
|
|
77
|
+
return { code, out: out.join("\n"), err: err.join("\n") };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const runPlan = (w: ReturnType<typeof workspace>, extra: string[] = []) =>
|
|
81
|
+
cli(
|
|
82
|
+
w.app,
|
|
83
|
+
["ci", "run", "plan", "--env", "prod", "--base", w.base, "--pr", "7", "--forge", "forgejo", "--no-status", "--chant", w.chant, "-o", join(scratchDir("chant-report-url-out-"), "pr"), ...extra],
|
|
84
|
+
runCiRun,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
describe("reportUrlOf", () => {
|
|
88
|
+
test("the flag wins, else the declared URL, else none", async () => {
|
|
89
|
+
const w = workspace({ reportUrl: "s3://acme-reports/prod/" });
|
|
90
|
+
expect(await declaredReportUrl(w.app)).toBe("s3://acme-reports/prod");
|
|
91
|
+
expect(await reportUrlOf(undefined, w.root)).toBe("s3://acme-reports/prod");
|
|
92
|
+
expect(await reportUrlOf("file:///elsewhere", w.app)).toBe("file:///elsewhere");
|
|
93
|
+
expect(await reportUrlOf(undefined, workspace().app)).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a directory outside any workspace reads nothing", async () => {
|
|
97
|
+
expect(await reportUrlOf(undefined, scratchDir("chant-report-url-plain-"))).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a declared URL no job can upload to fails with its place and reason code, unless the flag is given", async () => {
|
|
101
|
+
const w = workspace({ reportUrl: "https://reports.example/acme" });
|
|
102
|
+
await expect(declaredReportUrl(w.app)).rejects.toThrow(/chant\.workspace\.json:\d+:\d+: ci\.reportUrl https:\/\/reports\.example\/acme: takes an s3:\/\/<bucket>\/<prefix> or file:\/\/<directory> URL \[declaration-invalid\]/);
|
|
103
|
+
expect(await reportUrlOf("file:///elsewhere", w.app)).toBe("file:///elsewhere");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("chant workspace ls --json carries ci.reportUrl", () => {
|
|
108
|
+
const { expectValid } = contract(lsSchema);
|
|
109
|
+
const ciOf = (doc: LsDocument) => {
|
|
110
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
111
|
+
return doc.ci;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
test("present when declared, and the document validates", () => {
|
|
115
|
+
const doc = listWorkspace({ cwd: workspace({ reportUrl: "s3://acme-reports/prod" }).root });
|
|
116
|
+
expectValid(doc);
|
|
117
|
+
expect(ciOf(doc)).toEqual({ green: null, reportUrl: "s3://acme-reports/prod" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("absent when the ci block sets none, and ci stays null with no ci block", () => {
|
|
121
|
+
const green = { branch: "main", phases: { build: ["build"] }, require: ["build"] };
|
|
122
|
+
const withGreen = listWorkspace({ cwd: workspace({ green }).root });
|
|
123
|
+
expectValid(withGreen);
|
|
124
|
+
expect(ciOf(withGreen)).not.toHaveProperty("reportUrl");
|
|
125
|
+
expect(ciOf(listWorkspace({ cwd: workspace().root }))).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("an invalid one is the declaration-invalid failure", () => {
|
|
129
|
+
const doc = listWorkspace({ cwd: workspace({ reportUrl: "ftp://x/y" }).root });
|
|
130
|
+
expectValid(doc);
|
|
131
|
+
expect(doc).toMatchObject({ error: { code: "declaration-invalid", location: { file: "chant.workspace.json" } } });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("chant ci run", () => {
|
|
136
|
+
// The two whole jobs run once, in beforeAll, and the tests below read what
|
|
137
|
+
// they left, as ci-run.test.ts does (#4115, #4124). The first job of a file
|
|
138
|
+
// pays for loading the job's modules, which on a loaded CI shard came to
|
|
139
|
+
// over the unit budget (#2817); a file's beforeAll is not counted in it.
|
|
140
|
+
const ran: Record<"declared" | "flag", { code: number; err: string; declared: CiRunIndex | null; flag: CiRunIndex | null }> = {} as never;
|
|
141
|
+
beforeAll(async () => {
|
|
142
|
+
const read = (dir: string) => (existsSync(join(dir, CI_RUN_INDEX_KEY)) ? indexOf(dir) : null);
|
|
143
|
+
for (const kind of ["declared", "flag"] as const) {
|
|
144
|
+
const declared = reportsDir();
|
|
145
|
+
const flag = reportsDir();
|
|
146
|
+
const w = workspace({ reportUrl: urlOf(declared) });
|
|
147
|
+
const r = await runPlan(w, kind === "flag" ? ["--report-url", urlOf(flag)] : []);
|
|
148
|
+
ran[kind] = { code: r.code, err: r.err, declared: read(declared), flag: read(flag) };
|
|
149
|
+
process.chdir(home);
|
|
150
|
+
vi.restoreAllMocks();
|
|
151
|
+
}
|
|
152
|
+
}, 60_000);
|
|
153
|
+
|
|
154
|
+
test("without --report-url the job's run index goes to the declared URL", () => {
|
|
155
|
+
const r = ran.declared;
|
|
156
|
+
expect(r.code, r.err).toBe(0);
|
|
157
|
+
expect(r.declared?.runs).toHaveLength(1);
|
|
158
|
+
expect(r.declared?.runs[0]).toMatchObject({ job: "plan", pr: 7 });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("--report-url wins over the declared URL", () => {
|
|
162
|
+
const r = ran.flag;
|
|
163
|
+
expect(r.code, r.err).toBe(0);
|
|
164
|
+
expect(r.flag?.runs).toHaveLength(1);
|
|
165
|
+
expect(r.declared).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("an invalid declared URL stops the job before it runs", async () => {
|
|
169
|
+
const w = workspace({ reportUrl: "https://reports.example/acme" });
|
|
170
|
+
const r = await runPlan(w);
|
|
171
|
+
expect(r.code).toBe(1);
|
|
172
|
+
expect(r.err).toMatch(/\[declaration-invalid\]/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("chant ci runs", () => {
|
|
177
|
+
test("lists the index at the declared URL, and --report-url wins", async () => {
|
|
178
|
+
const declared = reportsDir();
|
|
179
|
+
const w = workspace({ reportUrl: urlOf(declared) });
|
|
180
|
+
expect((await runPlan(w)).code).toBe(0);
|
|
181
|
+
|
|
182
|
+
const listed = await cli(w.app, ["ci", "runs", "--json"], runCiRuns);
|
|
183
|
+
expect(listed.code, listed.err).toBe(0);
|
|
184
|
+
expect((JSON.parse(listed.out) as CiRunIndex).runs).toHaveLength(1);
|
|
185
|
+
|
|
186
|
+
const other = reportsDir();
|
|
187
|
+
const flagged = await cli(w.app, ["ci", "runs", "--report-url", urlOf(other)], runCiRuns);
|
|
188
|
+
expect(flagged.code).toBe(1);
|
|
189
|
+
expect(flagged.err).toContain(`there is no run index at ${urlOf(other)}`);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("with neither a flag nor a declared URL it says how to name one", async () => {
|
|
193
|
+
const r = await cli(workspace().root, ["ci", "runs"], runCiRuns);
|
|
194
|
+
expect(r.code).toBe(1);
|
|
195
|
+
expect(r.err).toContain("--report-url names where the index is kept, unless the workspace declares ci.reportUrl");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("chant ci serve", () => {
|
|
200
|
+
// No token and no server: the first poll fails, so the serve stops without reaching a forge.
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
for (const name of ["CHANT_FORGE_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "GITHUB_SERVER_URL", "CHANT_RUNNER_KEY"]) vi.stubEnv(name, undefined);
|
|
203
|
+
});
|
|
204
|
+
const SERVE = ["ci", "serve", "--forge", "forgejo", "--repo", "demo/estate", "--env", "local", "--once", "--unsigned", "--image", "node:22-bookworm"];
|
|
205
|
+
|
|
206
|
+
test("says it uses the declared URL when --report-url is not given", async () => {
|
|
207
|
+
const declared = reportsDir();
|
|
208
|
+
const w = workspace({ reportUrl: urlOf(declared) });
|
|
209
|
+
const state = join(scratchDir("chant-report-url-state-"), "ci-serve");
|
|
210
|
+
const r = await cli(w.root, [...SERVE, "--state", state], runCiServe);
|
|
211
|
+
expect(r.err).toContain(`ci serve: reports and the run index go to ${urlOf(declared)}, the declaration's ci.reportUrl`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("says nothing of the declaration when --report-url is given", async () => {
|
|
215
|
+
const w = workspace({ reportUrl: urlOf(reportsDir()) });
|
|
216
|
+
const state = join(scratchDir("chant-report-url-state-"), "ci-serve");
|
|
217
|
+
const r = await cli(w.root, [...SERVE, "--state", state, "--report-url", urlOf(reportsDir())], runCiServe);
|
|
218
|
+
expect(r.err).not.toContain("ci.reportUrl");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report URL a command uses (#4160): `--report-url` when it is given,
|
|
3
|
+
* else the workspace declaration's `ci.reportUrl`, else none.
|
|
4
|
+
*
|
|
5
|
+
* `chant build --pr-loop`, `chant ci run`, `chant ci serve` and `chant ci
|
|
6
|
+
* runs` call this. The declaration is read from the working tree of the
|
|
7
|
+
* workspace that holds `cwd`. A directory with no declaration between it and
|
|
8
|
+
* the git root reads nothing and loads no declaration module, so a project
|
|
9
|
+
* outside a workspace behaves as before. A declaration that can't be read
|
|
10
|
+
* is an error naming its place and its reason code, such as
|
|
11
|
+
* `declaration-invalid`, since the command would otherwise run without the
|
|
12
|
+
* URL the workspace may declare.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { findWorkspaceRoot } from "../project-root";
|
|
16
|
+
|
|
17
|
+
/** The declared `ci.reportUrl` of the workspace holding `cwd`, or undefined. */
|
|
18
|
+
export async function declaredReportUrl(cwd: string): Promise<string | undefined> {
|
|
19
|
+
const found = findWorkspaceRoot(cwd);
|
|
20
|
+
if (!found) return undefined;
|
|
21
|
+
const [{ readDeclaration, WorkspaceReadError }, { workingTree }] = await Promise.all([import("./declaration"), import("./tree")]);
|
|
22
|
+
try {
|
|
23
|
+
return readDeclaration(workingTree(found.dir)).ci?.reportUrl ?? undefined;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
26
|
+
throw Object.assign(new Error(`${err.describe()} [${err.code}]`), { code: err.code });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `flag` when given, else the declared `ci.reportUrl` (#4160). */
|
|
31
|
+
export async function reportUrlOf(flag: string | undefined, cwd: string): Promise<string | undefined> {
|
|
32
|
+
return flag ?? (await declaredReportUrl(cwd));
|
|
33
|
+
}
|