@intentius/chant 0.124.30 → 0.124.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +38 -4
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +50 -0
- package/dist/cli/registry.d.ts +18 -0
- package/dist/cli/registry.d.ts.map +1 -1
- 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/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/pr-forge.d.ts +17 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +12 -6
- package/dist/pr-report-url.d.ts +8 -0
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/pr-report-url.js +47 -1
- package/dist/wave-runtime.mjs +159 -159
- package/dist/workspace/ci-run-index.d.ts +96 -0
- package/dist/workspace/ci-run-index.d.ts.map +1 -0
- package/dist/workspace/ci-run-index.js +162 -0
- package/dist/workspace/ci-run-index.schema.json +50 -0
- package/dist/workspace/ci-run.d.ts +84 -7
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +241 -14
- package/dist/workspace/ci-runner-key.d.ts +120 -0
- package/dist/workspace/ci-runner-key.d.ts.map +1 -0
- package/dist/workspace/ci-runner-key.js +217 -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 +13 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +69 -13
- package/dist/workspace/reason-codes.d.ts +6 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +7 -2
- 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/package.json +1 -1
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +38 -4
- package/src/cli/parse-args.ts +36 -0
- package/src/cli/registry.ts +18 -0
- package/src/op/local-executor.ts +31 -3
- package/src/op/redact.ts +93 -0
- package/src/op/step-console.ts +7 -2
- package/src/pr-forge.test.ts +29 -0
- package/src/pr-forge.ts +30 -6
- package/src/pr-report-url.ts +53 -1
- package/src/workspace/ci-run-index.schema.json +50 -0
- package/src/workspace/ci-run-index.test.ts +353 -0
- package/src/workspace/ci-run-index.ts +192 -0
- package/src/workspace/ci-run.test.ts +337 -37
- package/src/workspace/ci-run.ts +277 -15
- package/src/workspace/ci-runner-key.test.ts +166 -0
- package/src/workspace/ci-runner-key.ts +250 -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 +136 -37
- package/src/workspace/ci-serve.ts +66 -13
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +7 -2
- 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/cli/main.ts
CHANGED
|
@@ -254,7 +254,9 @@ CI:
|
|
|
254
254
|
[--events-json] [--report-url <url>] [--image <image>]
|
|
255
255
|
[--before-script <line>]... [--pr-build]
|
|
256
256
|
[--pr-build-command <line>] [--chant <command>]
|
|
257
|
-
[--pass-env <NAME>]... [--key <runner key.pem>]
|
|
257
|
+
[--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>]
|
|
258
|
+
[-o <dir>]
|
|
259
|
+
[--no-status] [--status-url <url template>]
|
|
258
260
|
Run the pull-request pipeline's plan or apply job as
|
|
259
261
|
a pipeline Op in a fresh worktree of --commit (default
|
|
260
262
|
HEAD) with a clean environment. --base and --pr stand
|
|
@@ -262,10 +264,23 @@ CI:
|
|
|
262
264
|
the job's build and lint lines first. Records the run as
|
|
263
265
|
pr-plan or pr-apply on chant/lifecycle, signed with
|
|
264
266
|
--key or CHANT_RUNNER_KEY, so ci tick from runs counts
|
|
265
|
-
it, and copies the report to -o (default .chant/pr).
|
|
267
|
+
it, and copies the report to -o (default .chant/pr). With a forge
|
|
268
|
+
token it sets a chant/pr-plan or chant/pr-apply commit
|
|
269
|
+
status (pending, then success or failure); --no-status
|
|
270
|
+
turns that off, and --status-url links it.
|
|
271
|
+
--secrets (or CHANT_CI_SECRETS) names a dotenv or
|
|
272
|
+
JSON file only its owner can read whose values the
|
|
273
|
+
job gets, and which are shown as *** in the events,
|
|
274
|
+
the record, the report and the output.
|
|
275
|
+
With --report-url it also adds the run to
|
|
276
|
+
<url>/runs/index.json, newest first, cut to the
|
|
277
|
+
newest --run-index-limit entries (default 200).
|
|
278
|
+
ci runs --report-url <url> [--json]
|
|
279
|
+
List the runs in the index at the report URL, newest
|
|
280
|
+
first.
|
|
266
281
|
ci serve --forge forgejo|github --repo <owner/repo> --env <env>
|
|
267
282
|
[--interval <s>] [--max-jobs <n>] [--state <dir>]
|
|
268
|
-
[--author <login>]... [--once] [--rerun-failed]
|
|
283
|
+
[--author <login>]... [--once] [--rerun-failed] [--unsigned]
|
|
269
284
|
[ci run's job flags]
|
|
270
285
|
Poll the forge for open pull requests and the target
|
|
271
286
|
branch's head, and run ci run plan for each new pull
|
|
@@ -275,7 +290,15 @@ CI:
|
|
|
275
290
|
each apply's resume record there, and runs at most
|
|
276
291
|
--max-jobs (default 1) jobs at once. --once runs one
|
|
277
292
|
poll and waits for its jobs. --rerun-failed runs a
|
|
278
|
-
failed apply or plan again.
|
|
293
|
+
failed apply or plan again. Refuses to start when its
|
|
294
|
+
runner key is not listed in .chant/trust.json at the
|
|
295
|
+
target's head, unless --unsigned
|
|
296
|
+
ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]
|
|
297
|
+
Make the host's runner key (Ed25519, mode 0600) in the
|
|
298
|
+
state directory when there is none and print its public
|
|
299
|
+
key. --trust commits its runner entry on a branch of its
|
|
300
|
+
own and pushes it for review; the target branch is never
|
|
301
|
+
written
|
|
279
302
|
ci workflow [--workflow <name>]... [--output <file>] Write
|
|
280
303
|
[--chant <command>] [--install <command>]
|
|
281
304
|
[--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>]
|
|
@@ -701,6 +724,13 @@ Workspace (level 1, #2524):
|
|
|
701
724
|
threshold of the old one. A commit an agent run made
|
|
702
725
|
is attested by the run's signed statement; with
|
|
703
726
|
attested-runs, only those commits must be
|
|
727
|
+
workspace trust runner add <principal> --key <ssh-ed25519 line or file> [--class runner|service] [--by <principal>] [--expect <digest>] [--dry-run]
|
|
728
|
+
workspace trust runner remove <principal> [--by <principal>] [--expect <digest>] [--dry-run]
|
|
729
|
+
List a runner's key under runners in .chant/trust.json,
|
|
730
|
+
granting a runner the runner role, or take both away,
|
|
731
|
+
editing the file in place. --expect refuses the write
|
|
732
|
+
when the file's digest has changed. Prints the
|
|
733
|
+
trust-write document and the new digest; never commits
|
|
704
734
|
workspace signers [--json] [rotate [--threshold <n>] | sign --key <file>]
|
|
705
735
|
Show the signer history at base, or propose the next
|
|
706
736
|
signer set and sign it with a current signer's key
|
|
@@ -1335,6 +1365,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1335
1365
|
{ name: "ci workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiWorkflow(ctx) },
|
|
1336
1366
|
// #4094: one pull-request job as a pipeline Op in a fresh checkout, the chant runner's first slice.
|
|
1337
1367
|
{ name: "ci run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run")).runCiRun(ctx) },
|
|
1368
|
+
// #4098: the run index at the report URL.
|
|
1369
|
+
{ name: "ci runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run-index")).runCiRuns(ctx) },
|
|
1338
1370
|
{ name: "ci serve", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-serve")).runCiServe(ctx) },
|
|
1339
1371
|
// #4026: the release cut from a green commit (ws-104). Imported on first use.
|
|
1340
1372
|
{ name: "release cut", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCut(ctx) },
|
|
@@ -1439,6 +1471,8 @@ export const commandRegistry: CommandDef[] = [
|
|
|
1439
1471
|
// #2548 — an agent session's member, write scope and spec, for a session that resumes.
|
|
1440
1472
|
{ name: "workspace agent", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/agent-cli")).runWorkspaceAgent(ctx) },
|
|
1441
1473
|
{ name: "workspace verify", handler: async (ctx) => (await import("../workspace/trust/verify-cli")).runWorkspaceVerify(ctx) },
|
|
1474
|
+
// #3510 — a runner entry of .chant/trust.json, written through chant so a box repo never edits it by hand (ws-074).
|
|
1475
|
+
{ name: "workspace trust", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/trust/trust-write")).runWorkspaceTrust(ctx) },
|
|
1442
1476
|
{ name: "workspace signers", handler: async (ctx) => (await import("../workspace/trust/signers-cli")).runWorkspaceSigners(ctx) },
|
|
1443
1477
|
{ name: "workspace evidence", handler: async (ctx) => (await import("../workspace/trust/evidence-cli")).runWorkspaceEvidence(ctx) },
|
|
1444
1478
|
|
package/src/cli/parse-args.ts
CHANGED
|
@@ -66,6 +66,9 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
66
66
|
"--confirm-prod",
|
|
67
67
|
"--once",
|
|
68
68
|
"--rerun-failed",
|
|
69
|
+
"--setup-key",
|
|
70
|
+
"--trust",
|
|
71
|
+
"--unsigned",
|
|
69
72
|
"--expire",
|
|
70
73
|
"--check-live",
|
|
71
74
|
"--check-snapshot",
|
|
@@ -388,6 +391,13 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
388
391
|
const url = args[++i];
|
|
389
392
|
if (!url || url.startsWith("-")) throw new Error("--report-url needs a URL: --report-url s3://<bucket>/<prefix> or file://<directory>");
|
|
390
393
|
result.reportUrl = url;
|
|
394
|
+
} else if (arg === "--no-status") {
|
|
395
|
+
// `chant ci run|serve --no-status` (#4097): post no commit status for the job.
|
|
396
|
+
result.noStatus = true;
|
|
397
|
+
} else if (arg === "--status-url") {
|
|
398
|
+
// `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries, in place of an https --report-url.
|
|
399
|
+
result.statusUrl = args[++i];
|
|
400
|
+
if (!result.statusUrl || result.statusUrl.startsWith("-")) throw new Error("--status-url needs a URL template: --status-url https://host/reports/{key}");
|
|
391
401
|
} else if (arg === "--allow-unsigned") {
|
|
392
402
|
// `chant release publish --allow-unsigned` (#4049).
|
|
393
403
|
result.allowUnsigned = true;
|
|
@@ -536,10 +546,19 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
536
546
|
const name = args[++i];
|
|
537
547
|
if (!name || name.startsWith("-")) throw new Error("--pass-env needs a variable's name, such as AWS_PROFILE: --pass-env <NAME>");
|
|
538
548
|
(result.passEnv ??= []).push(name);
|
|
549
|
+
} else if (arg === "--secrets") {
|
|
550
|
+
// `chant ci run|serve --secrets <file>` (#4096): the host's secrets file, readable only by its owner.
|
|
551
|
+
result.ciSecrets = args[++i];
|
|
552
|
+
if (!result.ciSecrets || result.ciSecrets.startsWith("-")) throw new Error("--secrets needs the host's secrets file, a dotenv or JSON file only its owner can read: --secrets <file>");
|
|
539
553
|
} else if (arg === "--repo") {
|
|
540
554
|
// `chant ci serve --repo <owner/repo>` (#4095): the repository whose pull requests and target branch it polls.
|
|
541
555
|
result.ciRepo = args[++i];
|
|
542
556
|
if (!result.ciRepo || !/^[^/\s-][^/\s]*\/[^/\s]+$/.test(result.ciRepo)) throw new Error("--repo needs the repository as owner/name: --repo <owner/repo>");
|
|
557
|
+
} else if (arg === "--run-index-limit") {
|
|
558
|
+
// `chant ci run --run-index-limit <n>` (#4098): how many runs the index at the report URL keeps.
|
|
559
|
+
const n = Number(args[++i]);
|
|
560
|
+
if (!Number.isInteger(n) || n < 1) throw new Error("--run-index-limit needs a whole number of runs, 1 or more: --run-index-limit <n>");
|
|
561
|
+
result.runIndexLimit = n;
|
|
543
562
|
} else if (arg === "--max-jobs") {
|
|
544
563
|
// `chant ci serve --max-jobs <n>` (#4095): how many jobs run at once.
|
|
545
564
|
const n = Number(args[++i]);
|
|
@@ -576,6 +595,10 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
576
595
|
// `chant workspace records review <id> --verdict agree|dissent|abstain` (#2670)
|
|
577
596
|
result.verdict = args[++i];
|
|
578
597
|
if (!result.verdict || result.verdict.startsWith("-")) throw new Error("--verdict needs agree, dissent or abstain");
|
|
598
|
+
} else if (arg === "--class") {
|
|
599
|
+
// `chant workspace trust runner add <principal> --class runner|service` (#3510).
|
|
600
|
+
result.runnerClass = args[++i];
|
|
601
|
+
if (!result.runnerClass || result.runnerClass.startsWith("-")) throw new Error("--class needs runner or service: --class <class>");
|
|
579
602
|
} else if (arg === "--by") {
|
|
580
603
|
// `chant workspace records review <id> --by <principal>` (#2670): whoever the caller says.
|
|
581
604
|
result.by = args[++i];
|
|
@@ -748,6 +771,19 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
748
771
|
}
|
|
749
772
|
} else if (arg === "--once") {
|
|
750
773
|
result.once = true;
|
|
774
|
+
} else if (arg === "--setup-key") {
|
|
775
|
+
// `chant ci serve|run --setup-key` (#4099): make the host's runner key and print its public half.
|
|
776
|
+
result.setupKey = true;
|
|
777
|
+
} else if (arg === "--trust") {
|
|
778
|
+
// `chant ci serve|run --setup-key --trust` (#4099): list the key in .chant/trust.json on a branch.
|
|
779
|
+
result.trustKey = true;
|
|
780
|
+
} else if (arg === "--unsigned") {
|
|
781
|
+
// `chant ci serve --unsigned` (#4099): serve without a runner key listed at the target's head.
|
|
782
|
+
result.unsigned = true;
|
|
783
|
+
} else if (arg === "--principal") {
|
|
784
|
+
// `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the runner's principal.
|
|
785
|
+
result.ciPrincipal = args[++i];
|
|
786
|
+
if (!result.ciPrincipal || result.ciPrincipal.startsWith("-")) throw new Error("--principal needs the runner's principal: --principal <name>");
|
|
751
787
|
} else if (arg === "--work") {
|
|
752
788
|
// `chant run <op> --work <id>` (#2748)
|
|
753
789
|
const value = args[++i];
|
package/src/cli/registry.ts
CHANGED
|
@@ -462,8 +462,16 @@ export interface ParsedArgs {
|
|
|
462
462
|
commit?: string;
|
|
463
463
|
/** `chant ci run --pass-env <NAME>`, repeatable (#4094): a variable of the runner's environment the job gets, beside the few it always gets. */
|
|
464
464
|
passEnv?: string[];
|
|
465
|
+
/** `chant ci run|serve --secrets <file>` (#4096): the host's secrets file. Default the file `CHANT_CI_SECRETS` names. */
|
|
466
|
+
ciSecrets?: string;
|
|
465
467
|
/** `chant ci workflow --chant <command>` (#3573): the command the tick step runs chant with, in place of the npx pin. Also `chant ci run --chant` (#4094): what runs chant in the job. */
|
|
466
468
|
ciChant?: string;
|
|
469
|
+
/** `chant ci run|serve --no-status` (#4097): post no commit status for the job. */
|
|
470
|
+
noStatus?: boolean;
|
|
471
|
+
/** `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries. `{key}` is `<pr>/<commit>[/<member>]`, as `--report-url` keys it. */
|
|
472
|
+
statusUrl?: string;
|
|
473
|
+
/** `chant ci run --run-index-limit <n>` (#4098): how many runs the index at the report URL keeps. Default 200. */
|
|
474
|
+
runIndexLimit?: number;
|
|
467
475
|
/** `chant ci serve --repo <owner/repo>` (#4095): the repository it polls. */
|
|
468
476
|
ciRepo?: string;
|
|
469
477
|
/** `chant ci serve --max-jobs <n>` (#4095): how many jobs run at once. Default 1. */
|
|
@@ -490,6 +498,16 @@ export interface ParsedArgs {
|
|
|
490
498
|
* `records-write.ts`.
|
|
491
499
|
*/
|
|
492
500
|
by?: string;
|
|
501
|
+
/** `chant ci serve|run --setup-key` (#4099): make the host's runner key in the state directory and print its public half. */
|
|
502
|
+
setupKey?: boolean;
|
|
503
|
+
/** `chant ci serve|run --setup-key --trust` (#4099): list the runner key in .chant/trust.json on a branch pushed for review. */
|
|
504
|
+
trustKey?: boolean;
|
|
505
|
+
/** `chant ci serve --unsigned` (#4099): start without a runner key listed in .chant/trust.json at the target's head. */
|
|
506
|
+
unsigned?: boolean;
|
|
507
|
+
/** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
|
|
508
|
+
ciPrincipal?: string;
|
|
509
|
+
/** `chant workspace trust runner add <principal> --class runner|service` (#3510): the runner entry's class. */
|
|
510
|
+
runnerClass?: string;
|
|
493
511
|
/**
|
|
494
512
|
* `chant workspace records review <id> --sign [<key file>]` (#2687): the key that seals the verdict, or true for git's user.signingkey.
|
|
495
513
|
* On `records new` and `records amend`, the key that seals the record's author (#2688).
|
package/src/op/local-executor.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { LiveRun, currentLiveRun, withLiveRun } from "./run-live";
|
|
|
56
56
|
import { runEnvOf } from "../lifecycle/run-ledger";
|
|
57
57
|
import { resolveCommitSha } from "../lifecycle/commit-sha";
|
|
58
58
|
import { resolveCiRun, type RunOrigin } from "../lifecycle/release-ledger";
|
|
59
|
+
import { redactDeep, redactText, type Redact } from "./redact";
|
|
59
60
|
import { runEventsOfStepRecord, type RunEventKind, type RunEventRuntime, type RunEventSkipReason } from "../run-event";
|
|
60
61
|
import { openRunEventStore, teeLines } from "../run-event-store";
|
|
61
62
|
import { openRunEventStream, runEventRunOf, type RunEventSink, type RunEventStream } from "../run-event-stream";
|
|
@@ -1377,6 +1378,17 @@ export interface RunOpOptions {
|
|
|
1377
1378
|
* needs one. The local runtime passes the configured lexicons' contracts.
|
|
1378
1379
|
*/
|
|
1379
1380
|
contracts?: ReadonlyMap<string, ActivityContract>;
|
|
1381
|
+
/**
|
|
1382
|
+
* Redacts secret values (#4096) from what the run writes about itself:
|
|
1383
|
+
* each {@link runEvents} line and each line kept on this host, each
|
|
1384
|
+
* {@link stepOutput} line and step label, and the run record, its step
|
|
1385
|
+
* records and its `failure`, before {@link signRecord} signs it and the
|
|
1386
|
+
* ledger stores it. The step records on the result are redacted too. This
|
|
1387
|
+
* is the one place a run's writes pass through, so a writer added here
|
|
1388
|
+
* later is redacted with the rest. `chant ci run` passes one built from
|
|
1389
|
+
* the host's secrets file (../workspace/ci-secrets.ts).
|
|
1390
|
+
*/
|
|
1391
|
+
redact?: Redact;
|
|
1380
1392
|
}
|
|
1381
1393
|
|
|
1382
1394
|
/**
|
|
@@ -1437,7 +1449,11 @@ export async function runOpLocally(
|
|
|
1437
1449
|
const events: { stream?: RunEventStream } = {};
|
|
1438
1450
|
try {
|
|
1439
1451
|
const stepConsole = options.stepOutput
|
|
1440
|
-
? {
|
|
1452
|
+
? {
|
|
1453
|
+
write: options.stepOutput,
|
|
1454
|
+
...(options.stepHeartbeatMs !== undefined ? { heartbeatMs: options.stepHeartbeatMs } : {}),
|
|
1455
|
+
...(options.redact ? { redact: options.redact } : {}),
|
|
1456
|
+
}
|
|
1441
1457
|
: undefined;
|
|
1442
1458
|
return await withLiveRun(live, () =>
|
|
1443
1459
|
withRunConsole(stepConsole, () =>
|
|
@@ -1478,7 +1494,10 @@ async function runOpInTurn(
|
|
|
1478
1494
|
const store = options.ledger
|
|
1479
1495
|
? await openRunEventStore(options.ledger.cwd ?? options.cwd ?? process.cwd(), runId)
|
|
1480
1496
|
: undefined;
|
|
1481
|
-
const
|
|
1497
|
+
const teed = teeLines(options.runEvents?.write, store?.write.bind(store));
|
|
1498
|
+
// #4096: every event line, to the caller and to this host's store alike, is redacted once, here.
|
|
1499
|
+
const redact = options.redact;
|
|
1500
|
+
const eventsWrite = teed && redact ? (line: string) => teed(redactText(line, redact)) : teed;
|
|
1482
1501
|
const stream = eventsWrite
|
|
1483
1502
|
? openRunEventStream(
|
|
1484
1503
|
runEventRunOf({
|
|
@@ -1620,7 +1639,7 @@ async function runOpInTurn(
|
|
|
1620
1639
|
point?: WaitingPoint,
|
|
1621
1640
|
): Promise<OpRunRecord> => {
|
|
1622
1641
|
const ended = options.now ?? new Date().toISOString();
|
|
1623
|
-
|
|
1642
|
+
let input = buildRunRecord(config, settled, {
|
|
1624
1643
|
started: startedAt,
|
|
1625
1644
|
ended,
|
|
1626
1645
|
status,
|
|
@@ -1638,6 +1657,15 @@ async function runOpInTurn(
|
|
|
1638
1657
|
const failure = options.describeFailure(settled);
|
|
1639
1658
|
if (failure) input.failure = failure;
|
|
1640
1659
|
}
|
|
1660
|
+
// #4096: the record is redacted before it is signed, so the stored record is the one the statement covers.
|
|
1661
|
+
// The step records on the result are redacted in place, so a caller printing them prints no secret.
|
|
1662
|
+
if (redact) {
|
|
1663
|
+
input = redactDeep(input, redact);
|
|
1664
|
+
for (const r of settled) {
|
|
1665
|
+
if (r.error !== undefined) r.error = redact(r.error);
|
|
1666
|
+
if (r.args) r.args = redactDeep(r.args, redact);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1641
1669
|
let record: OpRunRecord = { version: 1, ...input, id: runId };
|
|
1642
1670
|
if (options.signRecord) {
|
|
1643
1671
|
try {
|
package/src/op/redact.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction of secret values (#4096): what a run writes about itself, with
|
|
3
|
+
* each secret value replaced by `***`.
|
|
4
|
+
*
|
|
5
|
+
* The executor (./local-executor.ts) takes a {@link Redact} and applies it
|
|
6
|
+
* where a run writes: each run-event line, each line a step prints, and the
|
|
7
|
+
* run record before it is signed and stored. `chant ci run` builds one from
|
|
8
|
+
* the host's secrets file and the job's secret-looking variables
|
|
9
|
+
* (../workspace/ci-secrets.ts).
|
|
10
|
+
*
|
|
11
|
+
* A value shorter than {@link REDACT_MIN_LENGTH} characters is not
|
|
12
|
+
* redacted: a two- or three-character value matches too much ordinary text.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** What every redacted value is replaced with. */
|
|
16
|
+
export const REDACTED = "***";
|
|
17
|
+
|
|
18
|
+
/** The shortest value redacted. Shorter values are left as they are. */
|
|
19
|
+
export const REDACT_MIN_LENGTH = 4;
|
|
20
|
+
|
|
21
|
+
/** A text with every secret value it contains replaced by {@link REDACTED}. */
|
|
22
|
+
export type Redact = (text: string) => string;
|
|
23
|
+
|
|
24
|
+
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The redactor for `values`, or undefined when none is long enough to
|
|
28
|
+
* redact. A value that spans lines is also redacted line by line, since
|
|
29
|
+
* step output reaches a reader one line at a time.
|
|
30
|
+
*/
|
|
31
|
+
export function redactorOf(values: Iterable<string>): Redact | undefined {
|
|
32
|
+
const all = new Set<string>();
|
|
33
|
+
const add = (v: string): void => {
|
|
34
|
+
if (v.length < REDACT_MIN_LENGTH) return;
|
|
35
|
+
all.add(v);
|
|
36
|
+
// As a JSON string spells it, for a value with a quote, a backslash or a control character in it.
|
|
37
|
+
all.add(JSON.stringify(v).slice(1, -1));
|
|
38
|
+
};
|
|
39
|
+
for (const value of values) {
|
|
40
|
+
add(value);
|
|
41
|
+
if (value.includes("\n")) for (const line of value.split(/\r?\n/)) add(line.trim());
|
|
42
|
+
}
|
|
43
|
+
if (all.size === 0) return undefined;
|
|
44
|
+
// Longest first, so a value that contains another is replaced whole.
|
|
45
|
+
const pattern = new RegExp([...all].sort((a, b) => b.length - a.length).map(escapeRegExp).join("|"), "g");
|
|
46
|
+
return (text) => (typeof text === "string" && text.length >= REDACT_MIN_LENGTH ? text.replace(pattern, REDACTED) : text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `value` with `redact` applied to every string in it, keys included. Numbers, booleans and nulls are kept. */
|
|
50
|
+
export function redactDeep<T>(value: T, redact: Redact): T {
|
|
51
|
+
if (typeof value === "string") return redact(value) as T;
|
|
52
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, redact)) as T;
|
|
53
|
+
if (value !== null && typeof value === "object") {
|
|
54
|
+
const out: Record<string, unknown> = {};
|
|
55
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[redact(k)] = redactDeep(v, redact);
|
|
56
|
+
return out as T;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A JSON value's text, as `JSON.stringify` wrote it, with `redact` applied to
|
|
63
|
+
* its strings: parsed, redacted and written again, so a short value such as
|
|
64
|
+
* `true` that is also a secret doesn't break the document. Undefined when
|
|
65
|
+
* `text` is not JSON.
|
|
66
|
+
*/
|
|
67
|
+
function redactJsonText(text: string, redact: Redact): string | undefined {
|
|
68
|
+
let parsed: unknown;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(text);
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
if (parsed === null || typeof parsed !== "object") return undefined;
|
|
75
|
+
const indent = /^[[{]\r?\n(\s+)/.exec(text)?.[1]?.length;
|
|
76
|
+
const trailing = /\s*$/.exec(text)?.[0] ?? "";
|
|
77
|
+
return `${JSON.stringify(redactDeep(parsed, redact), null, indent)}${trailing}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A file's or line's text redacted: a JSON document as JSON, each line of a
|
|
82
|
+
* JSON Lines text as JSON, and any other text, or line, as text. The text
|
|
83
|
+
* comes back unchanged when it holds no secret.
|
|
84
|
+
*/
|
|
85
|
+
export function redactText(text: string, redact: Redact): string {
|
|
86
|
+
if (redact(text) === text) return text;
|
|
87
|
+
const whole = redactJsonText(text, redact);
|
|
88
|
+
if (whole !== undefined) return whole;
|
|
89
|
+
return text
|
|
90
|
+
.split("\n")
|
|
91
|
+
.map((line) => (redact(line) === line ? line : (redactJsonText(line, redact) ?? redact(line))))
|
|
92
|
+
.join("\n");
|
|
93
|
+
}
|
package/src/op/step-console.ts
CHANGED
|
@@ -41,6 +41,8 @@ interface StepConsole {
|
|
|
41
41
|
export interface RunConsole {
|
|
42
42
|
write: (line: string) => void;
|
|
43
43
|
heartbeatMs?: number;
|
|
44
|
+
/** Applied to each line before it is written, and to a step's call before it is cut to a label (#4096). */
|
|
45
|
+
redact?: (text: string) => string;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const store = new AsyncLocalStorage<StepConsole>();
|
|
@@ -60,7 +62,10 @@ function formatArgs(args: Record<string, unknown>): string {
|
|
|
60
62
|
/** The name a step's lines carry: its id, else `fn(args)` on one line, cut to {@link MAX_LABEL}. */
|
|
61
63
|
export function stepLabel(fn: string, args: Record<string, unknown>, id?: string): string {
|
|
62
64
|
if (id) return id;
|
|
63
|
-
|
|
65
|
+
// A secret in the call is redacted before the label is cut, so a cut never leaves part of one.
|
|
66
|
+
const redact = runStore.getStore()?.redact;
|
|
67
|
+
const raw = `${fn}(${formatArgs(args)})`;
|
|
68
|
+
const call = (redact ? redact(raw) : raw).replace(/\s+/g, " ");
|
|
64
69
|
return call.length > MAX_LABEL ? `${call.slice(0, MAX_LABEL - 4)}...)` : call;
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -72,7 +77,7 @@ export function stepLabel(fn: string, args: Record<string, unknown>, id?: string
|
|
|
72
77
|
export async function withStepConsole<T>(opts: { phase: string; label: string }, fn: () => Promise<T>): Promise<T> {
|
|
73
78
|
const run = runStore.getStore();
|
|
74
79
|
if (!run) return fn();
|
|
75
|
-
const
|
|
80
|
+
const write = run.redact ? (line: string) => run.write(run.redact!(line)) : run.write;
|
|
76
81
|
const prefix = `[${opts.phase}] ${opts.label}`;
|
|
77
82
|
const started = Date.now();
|
|
78
83
|
let spoke = false;
|
package/src/pr-forge.test.ts
CHANGED
|
@@ -912,3 +912,32 @@ describe("pull requests over REST (#3798)", () => {
|
|
|
912
912
|
await expect(forgejoForge({ ...fjOpts, fetch }).openPullRequest({ head: "h", title: "t", body: "b" })).rejects.toThrow(/no default branch.*pass base/);
|
|
913
913
|
});
|
|
914
914
|
});
|
|
915
|
+
|
|
916
|
+
describe("setCommitStatus, a runner job's status (#4097)", () => {
|
|
917
|
+
const long = "x".repeat(300);
|
|
918
|
+
|
|
919
|
+
test("GitHub posts state, context, a description cut to 140 and target_url", async () => {
|
|
920
|
+
const { fetch, calls } = fakeFetch({ [`POST /repos/acme/infra/statuses/${SHA}`]: {} });
|
|
921
|
+
const forge = githubForge({ apiBase: "https://api.github.com", repo: "acme/infra", token: "t0k", fetch });
|
|
922
|
+
await forge.setCommitStatus!(SHA, { context: "chant/pr-plan", state: "failure", description: long, targetUrl: "https://host/reports/7/abc/" });
|
|
923
|
+
expect(calls).toHaveLength(1);
|
|
924
|
+
expect(calls[0]).toMatchObject({ method: "POST", url: `https://api.github.com/repos/acme/infra/statuses/${SHA}` });
|
|
925
|
+
expect(calls[0].headers.Authorization).toBe("Bearer t0k");
|
|
926
|
+
expect(calls[0].body).toEqual({ state: "failure", context: "chant/pr-plan", description: "x".repeat(140), target_url: "https://host/reports/7/abc/" });
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
test("Forgejo posts to /repos/{owner}/{repo}/statuses/{sha} under /api/v1, and omits target_url when there is none", async () => {
|
|
930
|
+
const { fetch, calls } = fakeFetch({ [`POST /api/v1/repos/acme/infra/statuses/${SHA}`]: {} });
|
|
931
|
+
const forge = forgejoForge({ apiBase: "https://codeberg.org/api/v1", repo: "acme/infra", token: "t0k", host: "codeberg.org", fetch });
|
|
932
|
+
await forge.setCommitStatus!(SHA, { context: "chant/pr-apply", state: "pending", description: "pr-apply running" });
|
|
933
|
+
expect(calls[0]).toMatchObject({ method: "POST", url: `https://codeberg.org/api/v1/repos/acme/infra/statuses/${SHA}` });
|
|
934
|
+
expect(calls[0].headers.Authorization).toBe("token t0k");
|
|
935
|
+
expect(calls[0].body).toEqual({ state: "pending", context: "chant/pr-apply", description: "pr-apply running" });
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("a refusal throws, and GitLab's client has none", async () => {
|
|
939
|
+
const { fetch } = fakeFetch({ [`POST /repos/acme/infra/statuses/${SHA}`]: 403 });
|
|
940
|
+
await expect(githubForge({ apiBase: "https://api.github.com", repo: "acme/infra", token: "t", fetch }).setCommitStatus!(SHA, { context: "c", state: "success", description: "d" })).rejects.toBeInstanceOf(ForgeApiError);
|
|
941
|
+
expect(gitlabForge({ apiBase: "https://gl/api/v4", project: "42", token: "t", fetch }).setCommitStatus).toBeUndefined();
|
|
942
|
+
});
|
|
943
|
+
});
|
package/src/pr-forge.ts
CHANGED
|
@@ -61,6 +61,16 @@ export interface ForgeStatus {
|
|
|
61
61
|
pipelineId?: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** A runner job's commit status (#4097): the states GitHub and Forgejo both keep. */
|
|
65
|
+
export interface CommitStatus {
|
|
66
|
+
context: string;
|
|
67
|
+
state: "pending" | "success" | "failure" | "error";
|
|
68
|
+
/** Cut to 140 characters. */
|
|
69
|
+
description: string;
|
|
70
|
+
/** Where the status links to. Omitted from the request when absent. */
|
|
71
|
+
targetUrl?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/** A commit status's state, as a caller names it. */
|
|
65
75
|
export type ForgeStatusState = "pending" | "running" | "success" | "failed" | "failure" | "canceled" | "error";
|
|
66
76
|
|
|
@@ -309,6 +319,14 @@ export interface PrForge {
|
|
|
309
319
|
*/
|
|
310
320
|
addComment(pr: number, body: string): Promise<AddedComment>;
|
|
311
321
|
setStatus(sha: string, status: ForgeStatus): Promise<void>;
|
|
322
|
+
/**
|
|
323
|
+
* GitHub and Forgejo (#4097): set a commit status from a runner's job, one
|
|
324
|
+
* of `pending`, `success`, `failure` or `error`. The request is the one
|
|
325
|
+
* {@link setStatus} sends (`POST /repos/{owner}/{repo}/statuses/{sha}`), with
|
|
326
|
+
* the description cut to 140 characters and `target_url` only when given.
|
|
327
|
+
* `chant ci run` posts a job's progress through it.
|
|
328
|
+
*/
|
|
329
|
+
setCommitStatus?(sha: string, status: CommitStatus): Promise<void>;
|
|
312
330
|
/**
|
|
313
331
|
* Keep one OPEN tracking issue for `marker` in line with `finding`: open
|
|
314
332
|
* it, update its title and body, or, when `finding` is `null` and
|
|
@@ -614,6 +632,14 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
|
|
|
614
632
|
await api.call("PATCH", `${repo}/issues/${n}`, { state: "closed" });
|
|
615
633
|
},
|
|
616
634
|
};
|
|
635
|
+
const postStatus = async (sha: string, state: ForgeStatusState, context: string, description: string, url?: string): Promise<void> => {
|
|
636
|
+
await api.call("POST", `${repo}/statuses/${sha}`, {
|
|
637
|
+
state: statusStateFor(kind, state),
|
|
638
|
+
context,
|
|
639
|
+
description: description.slice(0, GITHUB_DESCRIPTION),
|
|
640
|
+
...(url ? { target_url: url } : {}),
|
|
641
|
+
});
|
|
642
|
+
};
|
|
617
643
|
return {
|
|
618
644
|
kind,
|
|
619
645
|
host,
|
|
@@ -621,12 +647,10 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
|
|
|
621
647
|
principalOf: (login) => forgePrincipalOf(kind, host, login),
|
|
622
648
|
...threadCalls(t),
|
|
623
649
|
async setStatus(sha, status) {
|
|
624
|
-
await
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
...(status.url ? { target_url: status.url } : {}),
|
|
629
|
-
});
|
|
650
|
+
await postStatus(sha, status.state, status.context, status.description, status.url);
|
|
651
|
+
},
|
|
652
|
+
async setCommitStatus(sha, status) {
|
|
653
|
+
await postStatus(sha, status.state, status.context, status.description, status.targetUrl);
|
|
630
654
|
},
|
|
631
655
|
async approvers(pr) {
|
|
632
656
|
// A reviewer's newest deciding review is their standing one. A comment
|
package/src/pr-report-url.ts
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { spawn } from "node:child_process";
|
|
38
|
-
import { mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
38
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
39
39
|
import { dirname, isAbsolute, join } from "node:path";
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
|
|
@@ -120,6 +120,58 @@ export function reportPut(target: ReportTarget, aws: AwsCli = realAwsCli): Repor
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** Reads one object: `key` below the URL. Resolves null for an object that is not there, and rejects with a line a person can read for any other failure. */
|
|
124
|
+
export type ReportGet = (key: string) => Promise<string | null>;
|
|
125
|
+
|
|
126
|
+
/** Runs the AWS CLI with `args` and resolves its stdout, rejecting with its error line on a non-zero exit. */
|
|
127
|
+
export type AwsRead = (args: string[]) => Promise<string>;
|
|
128
|
+
|
|
129
|
+
/** The AWS CLI on PATH, read for the body `aws s3 cp <s3 url> -` prints. */
|
|
130
|
+
export const realAwsRead: AwsRead = (args) =>
|
|
131
|
+
new Promise((resolveRead, reject) => {
|
|
132
|
+
const child = spawn(AWS_BINARY, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
133
|
+
const out: Buffer[] = [];
|
|
134
|
+
let stderr = "";
|
|
135
|
+
child.stdout.on("data", (chunk: Buffer) => out.push(chunk));
|
|
136
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
137
|
+
if (stderr.length < 4096) stderr += chunk.toString("utf8");
|
|
138
|
+
});
|
|
139
|
+
child.on("error", (err: NodeJS.ErrnoException) =>
|
|
140
|
+
reject(new Error(err.code === "ENOENT" ? "the AWS CLI (aws) is not on PATH; an s3 report URL is read with it" : err.message)),
|
|
141
|
+
);
|
|
142
|
+
child.on("close", (code) => {
|
|
143
|
+
if (code === 0) return resolveRead(Buffer.concat(out).toString("utf8"));
|
|
144
|
+
const line = stderr.trim().split("\n").filter(Boolean).at(-1) ?? "";
|
|
145
|
+
reject(new Error(`aws s3 cp exited ${code}${line ? `: ${line.slice(0, 300)}` : ""}`));
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/** What the AWS CLI says about an object that does not exist. */
|
|
150
|
+
const S3_ABSENT = /\(404\)|Not Found|NoSuchKey|does not exist/i;
|
|
151
|
+
|
|
152
|
+
/** A get for `target`. `aws` replaces the AWS CLI in a test. */
|
|
153
|
+
export function reportGet(target: ReportTarget, aws: AwsRead = realAwsRead): ReportGet {
|
|
154
|
+
if (target.scheme === "file") {
|
|
155
|
+
return async (key) => {
|
|
156
|
+
try {
|
|
157
|
+
return readFileSync(join(target.dir, ...key.split("/")), "utf-8");
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return async (key) => {
|
|
165
|
+
const object = `s3://${target.bucket}/${target.prefix ? `${target.prefix}/` : ""}${key}`;
|
|
166
|
+
try {
|
|
167
|
+
return await aws(["s3", "cp", object, "-", "--only-show-errors"]);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (S3_ABSENT.test(messageOf(err))) return null;
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
123
175
|
/** A stage's upload: its events as they are written, and its files at the end. */
|
|
124
176
|
export interface PrReportUpload {
|
|
125
177
|
/** The folder the stage's files go to: `<url>/<key>`. */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://intentius.io/chant/schemas/workspace/ci-run-index/v1/ci-run-index.schema.json",
|
|
4
|
+
"title": "chant ci run index",
|
|
5
|
+
"description": "The runs the chant runner has run, kept at <report-url>/runs/index.json (#4098, epic #4101) so a reader outside CI finds them without a forge or a workflow file. `chant ci run` adds an entry to the front after each job settles when --report-url is set, so the list is newest first, and keeps the newest 200 entries unless --run-index-limit says otherwise. `chant ci runs --json` prints the same document. An entry's `key` is the folder the job's report is under, below the report URL. The update is a read, a change and a write of the whole file, through the same s3:// and file:// paths the report upload uses; a file:// write goes through a temporary name and a rename. Two runners updating one index at once can lose an entry. That is out of scope, because `chant ci serve` is one host. Version 1 of the workspace read contract. Readers ignore fields they do not know; a field is only ever added within a version. Written by chant 0.124.31 and newer.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["$schema", "contract", "chant", "updated", "runs"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": { "const": "https://intentius.io/chant/schemas/workspace/ci-run-index/v1/ci-run-index.schema.json" },
|
|
10
|
+
"contract": { "const": 1 },
|
|
11
|
+
"chant": { "type": "string", "description": "The chant that last wrote the index." },
|
|
12
|
+
"updated": { "type": "string", "description": "When the index was last written." },
|
|
13
|
+
"runs": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"description": "Newest first, at most the limit the writer was given (200 by default).",
|
|
16
|
+
"items": { "$ref": "#/$defs/run" }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"$defs": {
|
|
20
|
+
"run": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["job", "pr", "commit", "status", "started", "ended", "runId", "key", "runOrigin"],
|
|
23
|
+
"if": { "properties": { "status": { "const": "fail" } } },
|
|
24
|
+
"then": { "required": ["failure"], "properties": { "failure": { "type": "object" } } },
|
|
25
|
+
"properties": {
|
|
26
|
+
"job": { "enum": ["plan", "apply"] },
|
|
27
|
+
"pr": { "type": ["integer", "null"], "minimum": 1, "description": "The pull or merge request number: the one the job's report names, else --pr. null when neither has one." },
|
|
28
|
+
"commit": { "type": "string", "pattern": "^[0-9a-f]{40,64}$", "description": "The full commit the job ran at." },
|
|
29
|
+
"status": { "enum": ["ok", "fail"] },
|
|
30
|
+
"failure": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": ["code"],
|
|
33
|
+
"description": "Present when status is fail. The same code the run record's failure carries: setup-failed, build-failed, a report's refusal or status, or command-failed.",
|
|
34
|
+
"properties": { "code": { "type": "string" } }
|
|
35
|
+
},
|
|
36
|
+
"member": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,39}$", "description": "The workspace member whose pipeline the job ran for. Absent outside a member." },
|
|
37
|
+
"started": { "type": "string", "description": "ISO-8601 instant the job started." },
|
|
38
|
+
"ended": { "type": "string", "description": "ISO-8601 instant the job settled." },
|
|
39
|
+
"runId": { "type": "string", "description": "The Op run id, as in the run ledger on chant/lifecycle." },
|
|
40
|
+
"key": { "type": "string", "description": "reportKey(pr, commit): <pr>/<commit>, with /<member> after for a member, and unknown for a missing pr. The job's report is <report-url>/<key>/pr-<job>.json." },
|
|
41
|
+
"runOrigin": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"required": ["forge"],
|
|
44
|
+
"description": "Where the run says it ran. { forge: \"chant\" } for a job the chant runner ran.",
|
|
45
|
+
"properties": { "forge": { "type": "string" } }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|