@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
|
@@ -12,19 +12,22 @@
|
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { createPublicKey, generateKeyPairSync } from "node:crypto";
|
|
15
|
-
import { chmodSync, existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
-
import { afterAll, describe, expect, test } from "vitest";
|
|
18
|
-
import { cleanScratch, declaration, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
18
|
+
import { cleanScratch, declaration, git, repoFixture, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
|
|
19
19
|
import { checkRunEventLines } from "./__fixtures__/run-event-check";
|
|
20
|
-
import { ciJobCommand, ciJobEnv, ciJobFailure, ciJobOp, ciRun, CiRunError, formatCiRun } from "./ci-run";
|
|
21
|
-
import {
|
|
20
|
+
import { ciJobCommand, ciJobEnv, ciJobFailure, ciJobOp, ciLoopOptions, ciRun, CiRunError, ciStatusContext, ciStatusFromEnv, ciStatusUrl, formatCiRun } from "./ci-run";
|
|
21
|
+
import type { CommitStatus } from "../pr-forge";
|
|
22
|
+
import { ciList, ciTick, evaluateCommit, readOpRunSource, revokedAnnotation } from "./ci-green";
|
|
22
23
|
import type { StepRecord } from "../op/local-executor";
|
|
23
24
|
import { sshFromEd25519 } from "./trust/dsse";
|
|
24
25
|
import { verifyOpRunStatement } from "./trust/op-run-statement";
|
|
25
26
|
import { readRunLedger } from "../lifecycle/run-ledger";
|
|
26
27
|
import { prLoopJobs, PR_LOOP_SETUP } from "../components/pr-pipeline";
|
|
27
28
|
import { parseArgs } from "../cli/parse-args";
|
|
29
|
+
import { ciSecretsFor, readCiSecrets } from "./ci-secrets";
|
|
30
|
+
import { runEventFileName, runEventsDir } from "../run-event-store";
|
|
28
31
|
|
|
29
32
|
afterAll(cleanScratch);
|
|
30
33
|
|
|
@@ -51,11 +54,17 @@ function commitAt(root: string, hours: number, file: string): string {
|
|
|
51
54
|
return git(root, "rev-parse", "HEAD");
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
/**
|
|
58
|
+
* A checkout on main with a bare origin: member `app`, `ci.green` from runs, and BOT as the runner `chant-runner`.
|
|
59
|
+
* The history is two commits, `base` and then `sha`, which changes app/a.txt, and both are pushed to origin.
|
|
60
|
+
*/
|
|
61
|
+
const workspaceTemplate = repoFixture(() => {
|
|
62
|
+
const top = scratchDir("chant-ci-run-template-");
|
|
63
|
+
const origin = join(top, "origin");
|
|
64
|
+
const root = join(top, "root");
|
|
65
|
+
mkdirSync(origin);
|
|
66
|
+
mkdirSync(root);
|
|
57
67
|
git(origin, "init", "-q", "--bare");
|
|
58
|
-
const root = scratchDir("chant-ci-run-");
|
|
59
68
|
git(root, "init", "-q");
|
|
60
69
|
git(root, "symbolic-ref", "HEAD", "refs/heads/main");
|
|
61
70
|
git(root, "config", "user.name", "t");
|
|
@@ -68,7 +77,17 @@ function workspace(): { root: string; app: string } {
|
|
|
68
77
|
});
|
|
69
78
|
git(root, "remote", "add", "origin", origin);
|
|
70
79
|
commitAt(root, 23, "app/init.txt");
|
|
71
|
-
|
|
80
|
+
commitAt(root, 2, "app/a.txt");
|
|
81
|
+
git(root, "push", "-q", "origin", "main");
|
|
82
|
+
return top;
|
|
83
|
+
}, "chant-ci-run-");
|
|
84
|
+
|
|
85
|
+
/** A copy of the template repository, its origin pointed at the copy of the bare one. */
|
|
86
|
+
function workspace(): { root: string; app: string; base: string; sha: string } {
|
|
87
|
+
const top = workspaceTemplate();
|
|
88
|
+
const root = join(top, "root");
|
|
89
|
+
git(root, "remote", "set-url", "origin", join(top, "origin"));
|
|
90
|
+
return { root, app: join(root, "app"), base: git(root, "rev-parse", "HEAD~1"), sha: git(root, "rev-parse", "HEAD") };
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
/**
|
|
@@ -180,26 +199,46 @@ describe("the job as an Op (#4094)", () => {
|
|
|
180
199
|
expect(args).toMatchObject({ command: "ci", path: "run", extraPositional: "apply", env: "prod", base: "abc", commit: "def", passEnv: ["AWS_PROFILE", "AWS_REGION"], ciChant: "npx chant", key: "k.pem" });
|
|
181
200
|
expect(() => parseArgs(["ci", "run", "plan", "--commit"])).toThrow(/--commit needs/);
|
|
182
201
|
});
|
|
202
|
+
|
|
203
|
+
test("--wave-gate (#4003): the apply job runs pr-apply --wave-gate, the waves in order with no --wave, and the plan passes the gate for its note", () => {
|
|
204
|
+
for (const path of ["run", "serve"]) {
|
|
205
|
+
const args = parseArgs(["ci", path, ...(path === "run" ? ["apply"] : []), "--env", "prod", "--gate", "infra", "--wave-gate", "release"]);
|
|
206
|
+
expect(ciLoopOptions(args)).toEqual({ gate: "infra", waveGate: "release" });
|
|
207
|
+
}
|
|
208
|
+
const [plan, apply] = prLoopJobs("forgejo", "prod", ciLoopOptions(parseArgs(["ci", "run", "apply", "--env", "prod", "--wave-gate", "release"])));
|
|
209
|
+
expect(apply!.command).toContain("--require-review --wave-gate release --resume .chant/pr-resume/pr-apply.json");
|
|
210
|
+
expect(apply!.command).not.toContain("--wave ");
|
|
211
|
+
expect(plan!.command).toContain("--wave-gate release");
|
|
212
|
+
expect(plan!.command).not.toContain("--wave ");
|
|
213
|
+
const op = ciJobOp({ job: apply!, env: "prod", cwd: "/w", jobEnv: {}, setup: [], build: [] });
|
|
214
|
+
expect(op.phases.map((p) => p.name)).toEqual(["apply"]);
|
|
215
|
+
});
|
|
183
216
|
});
|
|
184
217
|
|
|
185
218
|
describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
219
|
+
// The plan and the apply run once, in beforeAll, and the tests below read what they left. A job
|
|
220
|
+
// takes a few hundred milliseconds here and several times that on a loaded runner, so two jobs and
|
|
221
|
+
// a tick in one test came close to the unit budget (#2817). A file's beforeAll is not counted in it.
|
|
222
|
+
let w: ReturnType<typeof workspace>;
|
|
223
|
+
let worktrees: string;
|
|
224
|
+
let chant: string;
|
|
225
|
+
let reports: string;
|
|
226
|
+
let events: string[];
|
|
227
|
+
let plan: Awaited<ReturnType<typeof ciRun>>;
|
|
228
|
+
let apply: Awaited<ReturnType<typeof ciRun>>;
|
|
229
|
+
|
|
230
|
+
beforeAll(async () => {
|
|
231
|
+
w = workspace();
|
|
232
|
+
worktrees = git(w.root, "worktree", "list", "--porcelain");
|
|
233
|
+
chant = fakeChant();
|
|
234
|
+
reports = scratchDir("chant-ci-run-reports-");
|
|
235
|
+
events = [];
|
|
236
|
+
plan = await ciRun({ image: JOB_IMAGE,
|
|
237
|
+
cwd: w.app,
|
|
199
238
|
job: "plan",
|
|
200
239
|
env: "prod",
|
|
201
240
|
forge: "forgejo",
|
|
202
|
-
base,
|
|
241
|
+
base: w.base,
|
|
203
242
|
pr: 7,
|
|
204
243
|
chant,
|
|
205
244
|
hostEnv: HOST,
|
|
@@ -209,6 +248,11 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
209
248
|
runId: "chant-plan-1",
|
|
210
249
|
runEvents: (line) => events.push(line),
|
|
211
250
|
});
|
|
251
|
+
apply = await ciRun({ image: JOB_IMAGE, cwd: w.app, job: "apply", env: "prod", forge: "forgejo", base: w.base, chant, hostEnv: HOST, runnerKey: BOT.pem, output: join(reports, "apply") });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("the plan runs in a fresh worktree, in the member's directory, with a clean environment", () => {
|
|
255
|
+
const { root, base, sha } = w;
|
|
212
256
|
expect(plan.status).toBe("ok");
|
|
213
257
|
expect(plan.warnings).toEqual([]);
|
|
214
258
|
expect(plan.signer).toBe("chant-runner");
|
|
@@ -224,14 +268,18 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
224
268
|
expect(pwd).toBe(join(plan.checkout, "app"));
|
|
225
269
|
expect(pwd.startsWith(realpathSync(root))).toBe(false);
|
|
226
270
|
expect(existsSync(plan.checkout)).toBe(false);
|
|
227
|
-
expect(git(root, "worktree", "list", "--porcelain")).toBe(
|
|
271
|
+
expect(git(root, "worktree", "list", "--porcelain")).toBe(worktrees);
|
|
228
272
|
expect(JSON.parse(readFileSync(join(out, "pr-plan.json"), "utf-8"))).toEqual({ stage: "pr-plan" });
|
|
229
273
|
|
|
230
|
-
// The environment is clean: the job's variables and the few the runner hands on.
|
|
274
|
+
// The environment is clean: the job's variables and the few the runner hands on. The copied
|
|
275
|
+
// report shows the values of the variables whose names look secret as *** (#4096).
|
|
231
276
|
const env = envFile(out, "pr-plan");
|
|
232
|
-
expect(env).toMatchObject({ BASE_SHA: base, PR_NUMBER: "7", GITHUB_SHA: sha, CHANT_ENV: "prod", GITHUB_TOKEN: "
|
|
277
|
+
expect(env).toMatchObject({ BASE_SHA: base, PR_NUMBER: "7", GITHUB_SHA: sha, CHANT_ENV: "prod", GITHUB_TOKEN: "***", AWS_ACCESS_KEY_ID: "***" });
|
|
233
278
|
for (const name of ["NOT_FOR_JOBS", "CHANT_RUNNER_KEY", "GITHUB_RUN_ID"]) expect(env).not.toHaveProperty(name);
|
|
279
|
+
});
|
|
234
280
|
|
|
281
|
+
test("the plan and the apply are recorded signed, with a pipeline run's events", async () => {
|
|
282
|
+
const { app, base, sha } = w;
|
|
235
283
|
// The record: Op pr-plan at the commit, run by chant, with a statement that verifies.
|
|
236
284
|
expect(plan.record).toMatchObject({ op: "pr-plan", env: "prod", status: "ok", gitSha: sha, runOrigin: { forge: "chant" }, id: "chant-plan-1" });
|
|
237
285
|
const stored = (await readRunLedger("prod", "pr-plan", { cwd: app })).records.at(-1)!;
|
|
@@ -244,13 +292,14 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
244
292
|
expect(stream[0]).toMatchObject({ kind: "pipeline", runtime: "ci", name: "pr-plan", runId: "chant-plan-1", gitSha: sha, runOrigin: { forge: "chant" } });
|
|
245
293
|
expect(stream.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
|
|
246
294
|
|
|
247
|
-
const apply = await ciRun({ image: JOB_IMAGE, cwd: app, job: "apply", env: "prod", forge: "forgejo", base, chant, hostEnv: HOST, runnerKey: BOT.pem, output: join(reports, "apply") });
|
|
248
295
|
expect(apply.status).toBe("ok");
|
|
249
296
|
const applyArgs = readFileSync(join(reports, "apply", "pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
250
297
|
expect(applyArgs).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--require-review", "--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
251
298
|
expect(formatCiRun(apply)).toContain("signed by chant-runner");
|
|
299
|
+
});
|
|
252
300
|
|
|
253
|
-
|
|
301
|
+
test("the records reached origin, and a tick from runs tags the commit green with no forge", async () => {
|
|
302
|
+
const { root, app, sha } = w;
|
|
254
303
|
expect(git(root, "ls-remote", "origin", "chant/lifecycle")).toContain(git(root, "rev-parse", "chant/lifecycle"));
|
|
255
304
|
const tick = await ciTick({ cwd: app, now: NOW, runnerKey: BOT.pem });
|
|
256
305
|
expect(tick.source).toBe("runs");
|
|
@@ -261,10 +310,8 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
261
310
|
expect(record.phases.apply![0]).toMatchObject({ name: "pr-apply", opRun: { id: apply.record.id, signer: "chant-runner" } });
|
|
262
311
|
});
|
|
263
312
|
|
|
264
|
-
test("a failing job is recorded failed, and a key from the environment that trust.json does not list leaves it unsigned", async () => {
|
|
265
|
-
const { root, app } = workspace();
|
|
266
|
-
const base = git(root, "rev-parse", "HEAD");
|
|
267
|
-
commitAt(root, 2, "app/a.txt");
|
|
313
|
+
test("a failing job is recorded failed, and a key from the environment that trust.json does not list leaves it unsigned, quietly", async () => {
|
|
314
|
+
const { root, app, base } = workspace();
|
|
268
315
|
const stranger = runnerKey();
|
|
269
316
|
const lines: string[] = [];
|
|
270
317
|
const r = await ciRun({ image: JOB_IMAGE,
|
|
@@ -286,23 +333,34 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
286
333
|
expect(r.record.status).toBe("fail");
|
|
287
334
|
expect(r.record.statement).toBeUndefined();
|
|
288
335
|
expect(r.signer).toBeNull();
|
|
289
|
-
|
|
336
|
+
// #4156: signing is opt-in, so an unlisted key is a note for --verbose, not a warning.
|
|
337
|
+
expect(r.warnings).toEqual([]);
|
|
338
|
+
expect(r.notes[0]).toMatch(/CHANT_RUNNER_KEY is set, and the run will not be signed: .*lists no runner with this key/);
|
|
339
|
+
// The command's exit code is the job's, as a forge job's was before it ran through ci run.
|
|
340
|
+
expect(r.exitCode).toBe(3);
|
|
290
341
|
expect(r.failure).toMatchObject({ phase: "apply", error: expect.stringContaining("command exited 3") });
|
|
291
342
|
// The report says nothing about a failure, so the record names the command.
|
|
292
343
|
expect(r.record.failure).toMatchObject({ code: "command-failed", phase: "apply", exitCode: 3, reason: expect.stringContaining("command exited 3") });
|
|
293
344
|
expect(lines.some((l) => l.includes("fake pr-apply ran"))).toBe(true);
|
|
294
345
|
expect(r.report).not.toBeNull();
|
|
295
|
-
expect(formatCiRun(r)).toContain("
|
|
346
|
+
expect(formatCiRun(r)).toContain("on chant/lifecycle, unsigned");
|
|
296
347
|
|
|
297
|
-
// The same key given with --key refuses the run before anything runs.
|
|
348
|
+
// The same key given with --key refuses the run before anything runs, and so does a key that is not one.
|
|
298
349
|
await expect(ciRun({ image: JOB_IMAGE, cwd: app, job: "apply", env: "prod", forge: "github", base, chant: fakeChant(), hostEnv: HOST, runnerKey: stranger.pem, push: false })).rejects.toThrow(/would not be signed/);
|
|
350
|
+
await expect(ciRun({ image: JOB_IMAGE, cwd: app, job: "apply", env: "prod", forge: "github", base, chant: fakeChant(), hostEnv: HOST, runnerKey: "not a key", runnerKeyFromEnv: true, push: false })).rejects.toThrow(/in CHANT_RUNNER_KEY is not an Ed25519 private key in PEM/);
|
|
299
351
|
expect((await readRunLedger("prod", "pr-apply", { cwd: app })).records).toHaveLength(1);
|
|
352
|
+
|
|
353
|
+
// With ci.green.signedSince the same run warns, and so does one with no key.
|
|
354
|
+
writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { ...GREEN, signedSince: base } } }) });
|
|
355
|
+
const opted = { image: JOB_IMAGE, cwd: app, job: "apply" as const, env: "prod", forge: "github" as const, base, chant: fakeChant(), hostEnv: HOST, push: false, output: join(scratchDir("chant-ci-run-out-"), "pr") };
|
|
356
|
+
const warned = await ciRun({ ...opted, runnerKey: stranger.pem, runnerKeyFromEnv: true });
|
|
357
|
+
expect(warned.warnings[0]).toMatch(/CHANT_RUNNER_KEY is set, and the run will not be signed: [\s\S]*, and ci.green.signedSince is [0-9a-f]{40}, so it will not count/);
|
|
358
|
+
const keyless = await ciRun(opted);
|
|
359
|
+
expect(keyless.warnings).toEqual([expect.stringMatching(/^there is no runner key, so the run's record is unsigned, and ci.green.signedSince is/)]);
|
|
300
360
|
});
|
|
301
361
|
|
|
302
362
|
test("--pr-build runs the job's build and lint lines after --before-script and before the command, and a failing build fails the job", async () => {
|
|
303
|
-
const { root, app } = workspace();
|
|
304
|
-
const base = git(root, "rev-parse", "HEAD");
|
|
305
|
-
const sha = commitAt(root, 2, "app/a.txt");
|
|
363
|
+
const { root, app, base, sha } = workspace();
|
|
306
364
|
const chant = fakeChant();
|
|
307
365
|
const out = join(scratchDir("chant-ci-run-build-"), "pr");
|
|
308
366
|
const ok = await ciRun({ image: JOB_IMAGE,
|
|
@@ -349,9 +407,7 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
349
407
|
});
|
|
350
408
|
|
|
351
409
|
test("a failed job's record and settle event say why from its pr report, signed, and a revoke note names the code", async () => {
|
|
352
|
-
const { root, app } = workspace();
|
|
353
|
-
const base = git(root, "rev-parse", "HEAD");
|
|
354
|
-
const sha = commitAt(root, 2, "app/a.txt");
|
|
410
|
+
const { root, app, base, sha } = workspace();
|
|
355
411
|
const refusedReport = { stage: "apply", status: "refused", refusal: "not-a-reviewer", message: "ana approved the plan, and ana has not approved the pull request on the forge" };
|
|
356
412
|
const extra = { FAKE_EXIT: "3", FAKE_REPORT: JSON.stringify(refusedReport) };
|
|
357
413
|
const events: string[] = [];
|
|
@@ -414,3 +470,425 @@ describe("chant ci run, then ci tick from runs (#4094)", () => {
|
|
|
414
470
|
expect(at([ok("build"), fail("apply", 1)])!.code).toBe("command-failed");
|
|
415
471
|
});
|
|
416
472
|
});
|
|
473
|
+
|
|
474
|
+
describe("a commit status per job (#4097)", () => {
|
|
475
|
+
/** A fake forge client: it records each status, and fails on the states in `refuse`. */
|
|
476
|
+
function fakeForge(refuse: string[] = []): { client: { setCommitStatus(sha: string, status: CommitStatus): Promise<void> }; posts: { sha: string; status: CommitStatus }[] } {
|
|
477
|
+
const posts: { sha: string; status: CommitStatus }[] = [];
|
|
478
|
+
return {
|
|
479
|
+
posts,
|
|
480
|
+
client: {
|
|
481
|
+
async setCommitStatus(sha, status) {
|
|
482
|
+
if (refuse.includes(status.state)) throw new Error(`POST /statuses/${sha} answered 500`);
|
|
483
|
+
posts.push({ sha, status });
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function job(over: Record<string, unknown>) {
|
|
490
|
+
const { root, app, base, sha } = workspace();
|
|
491
|
+
const r = await ciRun({ image: JOB_IMAGE, cwd: app, job: "plan", env: "prod", forge: "forgejo", base, pr: 7, chant: fakeChant(), hostEnv: HOST, push: false, output: join(scratchDir("chant-ci-status-"), "pr"), ...over } as Parameters<typeof ciRun>[0]);
|
|
492
|
+
return { r, sha };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
test("a good job posts pending, then success, at its commit", async () => {
|
|
496
|
+
const forge = fakeForge();
|
|
497
|
+
const { r, sha } = await job({ status: { client: forge.client } });
|
|
498
|
+
expect(r.status).toBe("ok");
|
|
499
|
+
expect(r.warnings).toEqual([]);
|
|
500
|
+
expect(forge.posts.map((p) => [p.sha, p.status.context, p.status.state])).toEqual([[sha, "chant/pr-plan", "pending"], [sha, "chant/pr-plan", "success"]]);
|
|
501
|
+
expect(forge.posts[0]!.status.description).toBe("pr-plan running");
|
|
502
|
+
expect(forge.posts[1]!.status.description).toMatch(/^pr-plan passed in \d+s$/);
|
|
503
|
+
expect(forge.posts[1]!.status).not.toHaveProperty("targetUrl");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("a failed job posts pending, then failure with the run record's code and reason", async () => {
|
|
507
|
+
const forge = fakeForge();
|
|
508
|
+
const report = JSON.stringify({ stage: "apply", status: "refused", refusal: "not-approved", message: "no reviewer approved this plan" });
|
|
509
|
+
const { r } = await job({ job: "apply", pr: undefined, hostEnv: { ...HOST, FAKE_EXIT: "1", FAKE_REPORT: report }, passEnv: ["FAKE_EXIT", "FAKE_REPORT"], status: { client: forge.client } });
|
|
510
|
+
expect(r.status).toBe("fail");
|
|
511
|
+
expect(forge.posts.map((p) => [p.status.context, p.status.state])).toEqual([["chant/pr-apply", "pending"], ["chant/pr-apply", "failure"]]);
|
|
512
|
+
expect(r.record.failure!.code).toBe("not-approved");
|
|
513
|
+
expect(forge.posts[1]!.status.description).toBe("not-approved: no reviewer approved this plan");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("a long failure reason is cut to the forge's 140 characters", async () => {
|
|
517
|
+
const forge = fakeForge();
|
|
518
|
+
const report = JSON.stringify({ stage: "apply", status: "refused", refusal: "plan-changed", message: "y".repeat(400) });
|
|
519
|
+
await job({ job: "apply", pr: undefined, hostEnv: { ...HOST, FAKE_EXIT: "1", FAKE_REPORT: report }, passEnv: ["FAKE_EXIT", "FAKE_REPORT"], status: { client: forge.client } });
|
|
520
|
+
const d = forge.posts[1]!.status.description;
|
|
521
|
+
expect(d).toHaveLength(140);
|
|
522
|
+
expect(d.startsWith("plan-changed: yyy")).toBe(true);
|
|
523
|
+
expect(d.endsWith("...")).toBe(true);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("a renamed gate names the apply's context", async () => {
|
|
527
|
+
const forge = fakeForge();
|
|
528
|
+
await job({ job: "apply", pr: undefined, loop: { gate: "prod-approval" }, status: { client: forge.client } });
|
|
529
|
+
expect(forge.posts.map((p) => p.status.context)).toEqual(["chant/prod-approval", "chant/prod-approval"]);
|
|
530
|
+
expect(ciStatusContext("plan", "prod-approval")).toBe("chant/pr-plan");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// One job per test: each runs a whole Op in a worktree, and three in one test went over the unit budget.
|
|
534
|
+
test("target_url is an https --report-url with the key", async () => {
|
|
535
|
+
const https = fakeForge();
|
|
536
|
+
const { sha } = await job({ loop: { reportUrl: "https://host/reports/" }, status: { client: https.client } });
|
|
537
|
+
const key = `7/${sha}/app`;
|
|
538
|
+
expect(https.posts.map((p) => p.status.targetUrl)).toEqual([`https://host/reports/${key}/`, `https://host/reports/${key}/`]);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* An `aws` stand-in first on PATH for the test. The run index of an s3 --report-url is read and
|
|
543
|
+
* written with the AWS CLI, and the real one reaches for credentials and the network: seconds on a
|
|
544
|
+
* runner that has it. The stub takes a put and answers a get with a missing object.
|
|
545
|
+
*/
|
|
546
|
+
function withStubAws(): () => void {
|
|
547
|
+
const dir = scratchDir("chant-ci-run-aws-");
|
|
548
|
+
const file = join(dir, "aws");
|
|
549
|
+
writeFileSync(file, ['#!/bin/sh', 'if [ "$3" = "-" ]; then cat > /dev/null; exit 0; fi', 'echo "An error occurred (404) when calling the HeadObject operation: Key not found" >&2', "exit 1", ""].join("\n"));
|
|
550
|
+
chmodSync(file, 0o755);
|
|
551
|
+
const saved = process.env.PATH;
|
|
552
|
+
process.env.PATH = `${dir}:${saved}`;
|
|
553
|
+
return () => {
|
|
554
|
+
process.env.PATH = saved;
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
test("target_url is the --status-url template for an s3 --report-url", async () => {
|
|
559
|
+
const s3 = fakeForge();
|
|
560
|
+
const restore = withStubAws();
|
|
561
|
+
try {
|
|
562
|
+
const mapped = await job({ loop: { reportUrl: "s3://bucket/prefix" }, status: { client: s3.client, urlTemplate: "https://host/reports/{key}" } });
|
|
563
|
+
expect(s3.posts.map((p) => p.status.targetUrl)).toEqual([`https://host/reports/7/${mapped.sha}/app`, `https://host/reports/7/${mapped.sha}/app`]);
|
|
564
|
+
} finally {
|
|
565
|
+
restore();
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("an s3 or file --report-url with no --status-url gives no target_url", async () => {
|
|
570
|
+
const bare = fakeForge();
|
|
571
|
+
const restore = withStubAws();
|
|
572
|
+
try {
|
|
573
|
+
await job({ loop: { reportUrl: "s3://bucket/prefix" }, status: { client: bare.client } });
|
|
574
|
+
} finally {
|
|
575
|
+
restore();
|
|
576
|
+
}
|
|
577
|
+
expect(bare.posts.every((p) => p.status.targetUrl === undefined)).toBe(true);
|
|
578
|
+
expect(ciStatusUrl({ reportUrl: "file:///tmp/r", commit: "abc" })).toBeUndefined();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("--no-status, a missing token and GitLab give no client; a bad --status-url is refused", () => {
|
|
582
|
+
const env = { GITHUB_TOKEN: "t", GITHUB_REPOSITORY: "acme/estate", GITHUB_SERVER_URL: "http://forgejo.localhost", GITHUB_API_URL: "http://forgejo.localhost/api/v1" };
|
|
583
|
+
expect(ciStatusFromEnv({ forge: "forgejo", hostEnv: env, noStatus: true })).toEqual({});
|
|
584
|
+
expect(ciStatusFromEnv({ forge: "forgejo", hostEnv: { GITHUB_REPOSITORY: "acme/estate" } })).toEqual({});
|
|
585
|
+
expect(ciStatusFromEnv({ forge: "gitlab", hostEnv: { ...env, GITLAB_TOKEN: "t" } })).toEqual({});
|
|
586
|
+
expect(ciStatusFromEnv({ forge: "forgejo", hostEnv: env }).status?.client.setCommitStatus).toBeTypeOf("function");
|
|
587
|
+
expect(ciStatusFromEnv({ forge: "forgejo", hostEnv: env, statusUrl: "https://h/{key}" }).status?.urlTemplate).toBe("https://h/{key}");
|
|
588
|
+
expect(ciStatusFromEnv({ forge: "forgejo", hostEnv: { GITHUB_TOKEN: "t" } }).warning).toMatch(/no commit status/);
|
|
589
|
+
expect(() => ciStatusFromEnv({ forge: "forgejo", hostEnv: env, statusUrl: "s3://b/{key}" })).toThrow(/--status-url takes an http or https URL/);
|
|
590
|
+
const flags = parseArgs(["ci", "run", "plan", "--no-status", "--status-url", "https://h/{key}"]);
|
|
591
|
+
expect(flags).toMatchObject({ noStatus: true, statusUrl: "https://h/{key}" });
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("a status the forge refuses is a warning, and the job stays ok", async () => {
|
|
595
|
+
const forge = fakeForge(["pending", "success"]);
|
|
596
|
+
const { r } = await job({ status: { client: forge.client } });
|
|
597
|
+
expect(r.status).toBe("ok");
|
|
598
|
+
expect(r.record.status).toBe("ok");
|
|
599
|
+
expect(r.warnings).toHaveLength(2);
|
|
600
|
+
expect(r.warnings[0]).toMatch(/could not set the chant\/pr-plan status: POST \/statuses\/[0-9a-f]+ answered 500/);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
/** A secrets file only its owner can read, as `--secrets` takes it. */
|
|
605
|
+
function secretsFile(text: string, name = "secrets.env"): string {
|
|
606
|
+
const file = join(scratchDir("chant-ci-run-secrets-"), name);
|
|
607
|
+
writeFileSync(file, text, { mode: 0o600 });
|
|
608
|
+
chmodSync(file, 0o600);
|
|
609
|
+
return file;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Every file under `dir`, with its text. */
|
|
613
|
+
function filesUnder(dir: string): Record<string, string> {
|
|
614
|
+
const out: Record<string, string> = {};
|
|
615
|
+
for (const entry of readdirSync(dir, { recursive: true, withFileTypes: true })) {
|
|
616
|
+
if (!entry.isFile()) continue;
|
|
617
|
+
const path = join(entry.parentPath, entry.name);
|
|
618
|
+
out[path] = readFileSync(path, "utf-8");
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
describe("the job's secrets from the host (#4096)", () => {
|
|
624
|
+
test("a secret reaches the step, and the file's allowlist keeps one from the plan", async () => {
|
|
625
|
+
const { app, base } = workspace();
|
|
626
|
+
const secrets = readCiSecrets(
|
|
627
|
+
secretsFile(JSON.stringify({ secrets: { DB_PASSWORD: "hunter22-db", DEPLOY_KEY: "deploy-key-value" }, jobs: { plan: ["DB_PASSWORD"] } }), "secrets.json"),
|
|
628
|
+
);
|
|
629
|
+
const out = join(scratchDir("chant-ci-run-secret-"), "pr");
|
|
630
|
+
const r = await ciRun({
|
|
631
|
+
image: JOB_IMAGE,
|
|
632
|
+
cwd: app,
|
|
633
|
+
job: "plan",
|
|
634
|
+
env: "prod",
|
|
635
|
+
forge: "forgejo",
|
|
636
|
+
base,
|
|
637
|
+
pr: 5,
|
|
638
|
+
chant: fakeChant(),
|
|
639
|
+
hostEnv: HOST,
|
|
640
|
+
secrets,
|
|
641
|
+
// The step checks the value it got, and that the plan got no DEPLOY_KEY.
|
|
642
|
+
beforeScript: ['test "$DB_PASSWORD" = "hunter22-db" && test -z "${DEPLOY_KEY+set}" && mkdir -p .chant/pr && echo reached > .chant/pr/reached'],
|
|
643
|
+
push: false,
|
|
644
|
+
output: out,
|
|
645
|
+
});
|
|
646
|
+
expect(r.status).toBe("ok");
|
|
647
|
+
expect(readFileSync(join(out, "reached"), "utf-8").trim()).toBe("reached");
|
|
648
|
+
const env = envFile(out, "pr-plan");
|
|
649
|
+
// The value reached the command too, and the copied report shows it as ***.
|
|
650
|
+
expect(env.DB_PASSWORD).toBe("***");
|
|
651
|
+
expect(env).not.toHaveProperty("DEPLOY_KEY");
|
|
652
|
+
expect(env).not.toHaveProperty("CHANT_RUNNER_KEY");
|
|
653
|
+
|
|
654
|
+
// The apply is not in the allowlist, so it gets every secret.
|
|
655
|
+
const [, apply] = prLoopJobs("forgejo", "prod");
|
|
656
|
+
const applyEnv = ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { base, commit: base }, hostEnv: HOST, secrets: ciSecretsFor(secrets, "apply") });
|
|
657
|
+
expect(applyEnv).toMatchObject({ DB_PASSWORD: "hunter22-db", DEPLOY_KEY: "deploy-key-value" });
|
|
658
|
+
// A secret can replace a forge token, and never the variables the runner sets.
|
|
659
|
+
const over = ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { base, commit: base }, hostEnv: HOST, secrets: { GITHUB_TOKEN: "from-file", BASE_SHA: "x", CHANT_RUNNER_KEY: "k" } });
|
|
660
|
+
expect(over).toMatchObject({ GITHUB_TOKEN: "from-file", BASE_SHA: base });
|
|
661
|
+
expect(over).not.toHaveProperty("CHANT_RUNNER_KEY");
|
|
662
|
+
expect(() => ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { base, commit: base }, hostEnv: HOST, passEnv: ["CHANT_RUNNER_KEY"] })).toThrow(/never passed to a job/);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("a secret the job prints, writes in its report or fails with is *** in the events, the signed record, the copied report and the output", async () => {
|
|
666
|
+
const { app, base } = workspace();
|
|
667
|
+
const SECRET = "s3cret pass#1";
|
|
668
|
+
const TOKEN = "tok-abc123";
|
|
669
|
+
const secrets = readCiSecrets(secretsFile(`# the host's secrets\nDB_PASSWORD="${SECRET}"\nexport PIN=abc\n`));
|
|
670
|
+
expect(secrets.values).toEqual({ DB_PASSWORD: SECRET, PIN: "abc" });
|
|
671
|
+
|
|
672
|
+
const leaky = join(scratchDir("chant-ci-run-bin-"), "chant");
|
|
673
|
+
writeFileSync(
|
|
674
|
+
leaky,
|
|
675
|
+
[
|
|
676
|
+
"#!/bin/sh",
|
|
677
|
+
"mkdir -p .chant/pr",
|
|
678
|
+
'env | sort > .chant/pr/pr-apply.env',
|
|
679
|
+
'printf \'{"stage":"apply","status":"refused","refusal":"not-approved","message":"the deploy failed with %s"}\' "$DB_PASSWORD" > .chant/pr/pr-apply.json',
|
|
680
|
+
'printf \'{\\n "entries": [\\n { "password": "%s" }\\n ]\\n}\\n\' "$DB_PASSWORD" > .chant/pr/change-set.json',
|
|
681
|
+
'echo "connecting with $DB_PASSWORD and $MY_API_TOKEN, pin $PIN"',
|
|
682
|
+
'echo "login refused for $DB_PASSWORD" >&2',
|
|
683
|
+
"exit 3",
|
|
684
|
+
"",
|
|
685
|
+
].join("\n"),
|
|
686
|
+
);
|
|
687
|
+
chmodSync(leaky, 0o755);
|
|
688
|
+
|
|
689
|
+
const events: string[] = [];
|
|
690
|
+
const lines: string[] = [];
|
|
691
|
+
const out = join(scratchDir("chant-ci-run-leak-"), "pr");
|
|
692
|
+
const r = await ciRun({
|
|
693
|
+
image: JOB_IMAGE,
|
|
694
|
+
cwd: app,
|
|
695
|
+
job: "apply",
|
|
696
|
+
env: "prod",
|
|
697
|
+
forge: "forgejo",
|
|
698
|
+
base,
|
|
699
|
+
chant: leaky,
|
|
700
|
+
hostEnv: { ...HOST, MY_API_TOKEN: TOKEN },
|
|
701
|
+
passEnv: ["MY_API_TOKEN"],
|
|
702
|
+
secrets,
|
|
703
|
+
runnerKey: BOT.pem,
|
|
704
|
+
push: false,
|
|
705
|
+
output: out,
|
|
706
|
+
runId: "chant-leak-1",
|
|
707
|
+
runEvents: (line) => events.push(line),
|
|
708
|
+
stepOutput: (line) => lines.push(line),
|
|
709
|
+
});
|
|
710
|
+
expect(r.status).toBe("fail");
|
|
711
|
+
const leaks = (text: string): boolean => text.includes(SECRET) || text.includes(TOKEN);
|
|
712
|
+
|
|
713
|
+
// The events, as written and as kept on this host, still valid lines.
|
|
714
|
+
expect(events.some(leaks)).toBe(false);
|
|
715
|
+
const stream = checkRunEventLines(events);
|
|
716
|
+
expect(stream.at(-1)).toMatchObject({ type: "run-settle", status: "failed", failure: { code: "not-approved", reason: "the deploy failed with ***" } });
|
|
717
|
+
const kept = readFileSync(join((await runEventsDir(app))!, runEventFileName("chant-leak-1")), "utf-8");
|
|
718
|
+
expect(leaks(kept)).toBe(false);
|
|
719
|
+
expect(kept).toContain("***");
|
|
720
|
+
|
|
721
|
+
// The record: the step's error and failure.reason redacted, and the statement still verifies.
|
|
722
|
+
expect(leaks(JSON.stringify(r.record))).toBe(false);
|
|
723
|
+
expect(r.record.failure).toMatchObject({ code: "not-approved", reason: "the deploy failed with ***", exitCode: 3 });
|
|
724
|
+
const stored = (await readRunLedger("prod", "pr-apply", { cwd: app })).records.at(-1)!;
|
|
725
|
+
expect(leaks(JSON.stringify(stored))).toBe(false);
|
|
726
|
+
expect(stored.phases.at(-1)!.steps[0]!.error).toContain("login refused for ***");
|
|
727
|
+
expect(verifyOpRunStatement(stored, RUNNERS, { runner: ["chant-runner"] })).toMatchObject({ status: "signed", signer: "chant-runner" });
|
|
728
|
+
|
|
729
|
+
// The copied report and change set: redacted, and still JSON in the layout the job wrote.
|
|
730
|
+
const files = filesUnder(out);
|
|
731
|
+
expect(Object.values(files).some(leaks)).toBe(false);
|
|
732
|
+
expect(JSON.parse(files[join(out, "pr-apply.json")]!)).toMatchObject({ message: "the deploy failed with ***" });
|
|
733
|
+
expect(files[join(out, "change-set.json")]).toBe('{\n "entries": [\n {\n "password": "***"\n }\n ]\n}\n');
|
|
734
|
+
expect(envFile(out, "pr-apply")).toMatchObject({ DB_PASSWORD: "***", MY_API_TOKEN: "***" });
|
|
735
|
+
|
|
736
|
+
// What ci run prints: the step's lines, the result's failure and the summary. A value under 4 characters is kept.
|
|
737
|
+
expect(lines.some(leaks)).toBe(false);
|
|
738
|
+
expect(lines.some((l) => l.endsWith("| connecting with *** and ***, pin abc"))).toBe(true);
|
|
739
|
+
expect(leaks(r.failure!.error)).toBe(false);
|
|
740
|
+
expect(leaks(formatCiRun(r))).toBe(false);
|
|
741
|
+
expect(formatCiRun(r)).toContain("not-approved in apply, exit 3: the deploy failed with ***");
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
describe("a generated forge job runs ci run --in-place (#4156)", () => {
|
|
746
|
+
// What a GitHub or Forgejo job has in its environment: the forge's run, job and attempt, and the secret.
|
|
747
|
+
const FORGE = { GITHUB_RUN_ID: "4242", GITHUB_RUN_ATTEMPT: "2", GITHUB_JOB: "apply", GITHUB_REPOSITORY: "acme/estate", GITHUB_SERVER_URL: "http://forgejo.localhost" };
|
|
748
|
+
|
|
749
|
+
test("the generated job's line parses to the same job, and --in-place, --no-push, --no-require-review and --wave reach it", () => {
|
|
750
|
+
const [plan, apply] = prLoopJobs("forgejo", "prod", { events: true, reportUrl: "s3://b/p", requireReview: false, waveGate: "release", branch: "trunk" });
|
|
751
|
+
expect(plan!.run.command).toBe('chant ci run plan --in-place --no-push --base "$BASE_SHA" --pr "$PR_NUMBER" --env prod --gate pr-apply --forge forgejo --wave-gate release --events-json --report-url "s3://b/p"');
|
|
752
|
+
const args = parseArgs(apply!.run.command.replace(/"\$BASE_SHA"/, "b".repeat(40)).replace(/"/g, "").split(" ").slice(1).concat(["--wave", "2"]));
|
|
753
|
+
expect(args).toMatchObject({ ciInPlace: true, requireReview: false, wave: 2, waveGate: "release", branch: "trunk", eventsJson: true, reportUrl: "s3://b/p" });
|
|
754
|
+
expect(ciLoopOptions(args)).toEqual({ gate: "pr-apply", branch: "trunk", events: true, reportUrl: "s3://b/p", waveGate: "release", requireReview: false });
|
|
755
|
+
expect(parseArgs(["ci", "run", "plan", "--no-push"]).noPush).toBe(true);
|
|
756
|
+
// ci run rebuilds pr-apply from those options: the very command the job ran before #4156.
|
|
757
|
+
expect(prLoopJobs("forgejo", "prod", ciLoopOptions(args))[1]!.command).toBe(apply!.command);
|
|
758
|
+
expect(apply!.run.env.CHANT_RUNNER_KEY).toBe("${{ secrets.CHANT_RUNNER_KEY }}");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("in the job's own checkout, with its environment less the key, a signed record names the forge's run and verifies, and ci list sees it", async () => {
|
|
762
|
+
const { root, app, base, sha } = workspace();
|
|
763
|
+
const chant = fakeChant();
|
|
764
|
+
const before = git(root, "worktree", "list", "--porcelain");
|
|
765
|
+
const r = await ciRun({ cwd: app, job: "apply", env: "prod", forge: "forgejo", base, chant, hostEnv: { ...HOST, ...FORGE }, runnerKey: BOT.pem, runnerKeyFromEnv: true, inPlace: true, runId: "forge-apply-1" });
|
|
766
|
+
expect(r.status).toBe("ok");
|
|
767
|
+
expect(r.warnings).toEqual([]);
|
|
768
|
+
expect(r.signer).toBe("chant-runner");
|
|
769
|
+
expect(r.pushed).toBe(true);
|
|
770
|
+
|
|
771
|
+
// In place: no worktree, the report where the forge keeps it, and no setup phase.
|
|
772
|
+
expect(git(root, "worktree", "list", "--porcelain")).toBe(before);
|
|
773
|
+
expect(readFileSync(join(app, ".chant/pr/pr-apply.pwd"), "utf-8").trim()).toBe(realpathSync(app));
|
|
774
|
+
expect(r.report).toBe(join(realpathSync(app), ".chant/pr"));
|
|
775
|
+
expect(r.record.phases.map((p) => p.name)).toEqual(["apply"]);
|
|
776
|
+
const argv = readFileSync(join(app, ".chant/pr/pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
777
|
+
expect(argv).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--require-review", "--resume", ".chant/pr-resume/pr-apply.json"]);
|
|
778
|
+
// The forge job's whole environment reaches the step, but never the runner key.
|
|
779
|
+
const env = envFile(join(app, ".chant/pr"), "pr-apply");
|
|
780
|
+
expect(env).toMatchObject({ NOT_FOR_JOBS: "leak", GITHUB_RUN_ID: "4242", BASE_SHA: base, GITHUB_SHA: sha });
|
|
781
|
+
expect(env).not.toHaveProperty("CHANT_RUNNER_KEY");
|
|
782
|
+
|
|
783
|
+
// The record: the forge's run, job and attempt as runOrigin, signed, and it verifies against trust.json.
|
|
784
|
+
const origin = { forge: "github", repo: "acme/estate", url: "http://forgejo.localhost/acme/estate/actions/runs/4242", jobId: "apply", attempt: 2 };
|
|
785
|
+
expect(r.record).toMatchObject({ op: "pr-apply", gitSha: sha, id: "forge-apply-1", runOrigin: origin });
|
|
786
|
+
const stored = (await readRunLedger("prod", "pr-apply", { cwd: app })).records.at(-1)!;
|
|
787
|
+
expect(verifyOpRunStatement(stored, RUNNERS, { runner: ["chant-runner"] })).toMatchObject({ status: "signed", signer: "chant-runner" });
|
|
788
|
+
expect(git(root, "ls-remote", "origin", "chant/lifecycle")).toContain(git(root, "rev-parse", "chant/lifecycle"));
|
|
789
|
+
|
|
790
|
+
const list = (await ciList({ cwd: app, now: NOW })) as Extract<Awaited<ReturnType<typeof ciList>>, { commits: unknown }>;
|
|
791
|
+
const listed = list.commits.find((c) => c.sha === sha)!;
|
|
792
|
+
expect(listed.phases.find((p) => p.phase === "apply")!.runs).toEqual([expect.objectContaining({ name: "pr-apply", opRun: { id: "forge-apply-1", env: "prod", signer: "chant-runner" } })]);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("with no key the job runs, warns of nothing, and its unsigned record counts in ci list and the run index", async () => {
|
|
796
|
+
const { app, base, sha } = workspace();
|
|
797
|
+
const reports = scratchDir("chant-ci-run-index-");
|
|
798
|
+
const { CHANT_RUNNER_KEY: _key, ...keyless } = HOST;
|
|
799
|
+
const r = await ciRun({ cwd: app, job: "plan", env: "prod", forge: "forgejo", base, pr: 7, chant: fakeChant(), hostEnv: { ...keyless, ...FORGE, GITHUB_JOB: "plan" }, inPlace: true, push: false, runId: "forge-plan-1", loop: { reportUrl: `file://${reports}` } });
|
|
800
|
+
expect(r.status).toBe("ok");
|
|
801
|
+
expect(r.warnings).toEqual([]);
|
|
802
|
+
expect(r.notes).toEqual(["there is no runner key, so the run's record is unsigned"]);
|
|
803
|
+
expect(r.signer).toBeNull();
|
|
804
|
+
expect(r.record.statement).toBeUndefined();
|
|
805
|
+
expect(r.record.runOrigin).toMatchObject({ forge: "github", jobId: "plan", attempt: 2 });
|
|
806
|
+
// --no-push: the plan's record stays on this checkout's chant/lifecycle.
|
|
807
|
+
expect(r.pushed).toBe(false);
|
|
808
|
+
|
|
809
|
+
const list = (await ciList({ cwd: app, now: NOW })) as Extract<Awaited<ReturnType<typeof ciList>>, { commits: unknown }>;
|
|
810
|
+
const plan = list.commits.find((c) => c.sha === sha)!.phases.find((p) => p.phase === "plan")!;
|
|
811
|
+
expect(plan.result).toBe("pass");
|
|
812
|
+
expect(plan.runs[0]).toMatchObject({ name: "pr-plan", opRun: { id: "forge-plan-1", signer: null } });
|
|
813
|
+
|
|
814
|
+
const index = JSON.parse(readFileSync(join(reports, "runs", "index.json"), "utf-8")) as { runs: Record<string, unknown>[] };
|
|
815
|
+
expect(index.runs[0]).toMatchObject({ job: "plan", runId: "forge-plan-1", signer: null, runOrigin: { forge: "github", jobId: "plan", attempt: 2 } });
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
test("a failed command's exit code is the job's, and --wave reaches pr-apply", async () => {
|
|
819
|
+
const { app, base } = workspace();
|
|
820
|
+
const r = await ciRun({ cwd: app, job: "apply", env: "prod", forge: "github", base, chant: fakeChant(), hostEnv: { ...HOST, ...FORGE, FAKE_EXIT: "3" }, inPlace: true, push: false, loop: { waveGate: "release" }, wave: 2 });
|
|
821
|
+
expect(r.status).toBe("fail");
|
|
822
|
+
expect(r.exitCode).toBe(3);
|
|
823
|
+
const argv = readFileSync(join(app, ".chant/pr/pr-apply.args"), "utf-8").trimEnd().split("\n");
|
|
824
|
+
expect(argv.slice(-4)).toEqual(["--resume", ".chant/pr-resume/pr-apply.json", "--wave", "2"]);
|
|
825
|
+
await expect(ciRun({ cwd: app, job: "plan", env: "prod", forge: "github", base, pr: 1, chant: fakeChant(), hostEnv: HOST, inPlace: true, push: false, wave: 2 })).rejects.toThrow(/--wave names the one wave an apply runs/);
|
|
826
|
+
// In place runs HEAD only.
|
|
827
|
+
await expect(ciRun({ cwd: app, job: "apply", env: "prod", forge: "github", base, commit: base, chant: fakeChant(), hostEnv: HOST, inPlace: true, push: false })).rejects.toThrow(/--in-place runs the checkout's HEAD/);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
describe("ci run --local (#4161)", () => {
|
|
832
|
+
test("the job runs on this machine: no push, no status, no report upload or run index, a local record ci tick ignores, and events that say local", async () => {
|
|
833
|
+
const { root, app, base, sha } = workspace();
|
|
834
|
+
const reports = scratchDir("chant-ci-run-local-");
|
|
835
|
+
const remoteBefore = git(root, "ls-remote", "origin");
|
|
836
|
+
const posts: CommitStatus[] = [];
|
|
837
|
+
const events: string[] = [];
|
|
838
|
+
const r = await ciRun({
|
|
839
|
+
image: JOB_IMAGE,
|
|
840
|
+
cwd: app,
|
|
841
|
+
job: "plan",
|
|
842
|
+
env: "prod",
|
|
843
|
+
forge: "forgejo",
|
|
844
|
+
base,
|
|
845
|
+
pr: 7,
|
|
846
|
+
chant: fakeChant(),
|
|
847
|
+
hostEnv: HOST,
|
|
848
|
+
runnerKey: BOT.pem,
|
|
849
|
+
runnerKeyFromEnv: true,
|
|
850
|
+
local: true,
|
|
851
|
+
runId: "local-plan-1",
|
|
852
|
+
output: join(scratchDir("chant-ci-run-local-out-"), "pr"),
|
|
853
|
+
loop: { reportUrl: `file://${reports}`, events: true },
|
|
854
|
+
status: { client: { async setCommitStatus(_sha, status) { posts.push(status); } } },
|
|
855
|
+
runEvents: (line) => events.push(line),
|
|
856
|
+
});
|
|
857
|
+
expect(r.status).toBe("ok");
|
|
858
|
+
expect(r.warnings).toEqual([]);
|
|
859
|
+
expect(r.local).toBe(true);
|
|
860
|
+
|
|
861
|
+
// Nothing left this machine: origin is as it was, the fake forge saw no status, and the report URL holds nothing.
|
|
862
|
+
expect(r.pushed).toBe(false);
|
|
863
|
+
expect(git(root, "ls-remote", "origin")).toBe(remoteBefore);
|
|
864
|
+
expect(posts).toEqual([]);
|
|
865
|
+
expect(readdirSync(reports)).toEqual([]);
|
|
866
|
+
// The command got --no-status and no --report-url, so pr-plan posts and uploads nothing either.
|
|
867
|
+
const argv = readFileSync(join(r.report!, "pr-plan.args"), "utf-8").trimEnd().split("\n");
|
|
868
|
+
expect(argv).toContain("--no-status");
|
|
869
|
+
expect(argv).not.toContain("--report-url");
|
|
870
|
+
expect(argv).toContain("--events-json");
|
|
871
|
+
|
|
872
|
+
// The record is on this checkout's chant/lifecycle, and says local.
|
|
873
|
+
expect(r.record).toMatchObject({ op: "pr-plan", gitSha: sha, id: "local-plan-1", runOrigin: { forge: "local" } });
|
|
874
|
+
const stored = (await readRunLedger("prod", "pr-plan", { cwd: app })).records.at(-1)!;
|
|
875
|
+
expect(stored).toMatchObject({ id: "local-plan-1", runOrigin: { forge: "local" } });
|
|
876
|
+
expect(formatCiRun(r)).toContain("local run: the record stays on this machine");
|
|
877
|
+
|
|
878
|
+
// The events stream as the job runs, and say where it ran.
|
|
879
|
+
const stream = checkRunEventLines(events);
|
|
880
|
+
expect(stream[0]).toMatchObject({ type: "run-start", kind: "pipeline", runtime: "local", runId: "local-plan-1", gitSha: sha, runOrigin: { forge: "local" } });
|
|
881
|
+
expect(stream.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
|
|
882
|
+
|
|
883
|
+
// A local run is never CI's verdict, signed or not.
|
|
884
|
+
const runs = await readOpRunSource(app, root, git(root, "rev-parse", "HEAD"));
|
|
885
|
+
expect(runs.checkRuns(sha)).toEqual([]);
|
|
886
|
+
expect(runs.ignored.get(sha)).toBeUndefined();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
test("--local with --in-place is refused before anything runs, and the flag parses", async () => {
|
|
890
|
+
const { app, base } = workspace();
|
|
891
|
+
await expect(ciRun({ cwd: app, job: "plan", env: "prod", forge: "github", base, pr: 1, chant: fakeChant(), hostEnv: HOST, inPlace: true, local: true })).rejects.toThrow(/--in-place .* --local/);
|
|
892
|
+
expect(parseArgs(["ci", "run", "plan", "--local"]).local).toBe(true);
|
|
893
|
+
});
|
|
894
|
+
});
|