@kici-dev/agent 0.1.22 → 0.1.24
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/bootstrap/api-intercept.d.ts +20 -0
- package/dist/bootstrap/ensure-init-runner.d.ts +39 -0
- package/dist/bootstrap/pre-boot-send.d.ts +25 -0
- package/dist/bootstrap/reach.d.ts +17 -0
- package/dist/bootstrap/ssh-exec.d.ts +58 -0
- package/dist/execution/cache/cache-phase.d.ts +21 -4
- package/dist/execution/cache/index.d.ts +1 -1
- package/dist/execution/init-runner.d.ts +2 -1
- package/dist/execution/job-runner.d.ts +12 -0
- package/dist/execution/rule-evaluator.d.ts +4 -2
- package/dist/execution/sandbox/capture-context.d.ts +11 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +31 -3
- package/dist/execution/sandbox/parallel-scheduler.d.ts +34 -0
- package/dist/execution/sandbox/step-loop.d.ts +96 -17
- package/dist/execution/sandbox/step-task-registry.d.ts +26 -0
- package/dist/execution/sandbox/types.d.ts +2 -2
- package/dist/execution/sandbox/workflow-runner.d.ts +29 -2
- package/dist/server.js +433 -48
- package/dist/workflow-runner.js +499 -125
- package/package.json +6 -6
- package/sbom.spdx.json +68 -68
package/dist/workflow-runner.js
CHANGED
|
@@ -7,10 +7,10 @@ import os, { homedir, tmpdir } from "node:os";
|
|
|
7
7
|
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { $ } from "zx";
|
|
9
9
|
import { createLogger, deriveSharedSecret, initZx, normalizeLineEndings, sha256, sha256File, toErrorMessage } from "@kici-dev/shared";
|
|
10
|
-
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, TimeoutReason } from "@kici-dev/engine";
|
|
11
|
-
import { buildKiciApi, buildNeedsContext, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeApproval, normalizeCacheSpecs, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
10
|
+
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, StepConcurrencyKind, TimeoutReason } from "@kici-dev/engine";
|
|
11
|
+
import { buildKiciApi, buildNeedsContext, createStepSecrets, evaluateRules, isDynamicJobFn, isParallelGroup, normalizeApproval, normalizeCacheSpecs, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
12
12
|
import { OIDC_TOKEN_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/oidc-token-relay";
|
|
13
|
-
import { sha256File as sha256File$1 } from "@kici-dev/core";
|
|
13
|
+
import { computeBackoffDelay, sha256File as sha256File$1 } from "@kici-dev/core";
|
|
14
14
|
import { calculateJwkThumbprint, decodeJwt, exportJWK, generateKeyPair } from "jose";
|
|
15
15
|
import { IN_TOTO_PAYLOAD_TYPE, KICI_PROVENANCE_AUDIENCE, KICI_PROVENANCE_BUNDLE_MEDIA_TYPE } from "@kici-dev/engine/provenance/bundle";
|
|
16
16
|
import { IN_TOTO_STATEMENT_TYPE, KICI_WORKFLOW_BUILD_TYPE, SLSA_PROVENANCE_PREDICATE_TYPE } from "@kici-dev/engine/provenance/schema";
|
|
@@ -23,6 +23,7 @@ import { createGunzip } from "node:zlib";
|
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
24
24
|
import { c, x } from "tar";
|
|
25
25
|
import { runIdempotentStep } from "@kici-dev/core/idempotency";
|
|
26
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
27
|
import { execFile } from "node:child_process";
|
|
27
28
|
import { promisify } from "node:util";
|
|
28
29
|
import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
|
|
@@ -689,29 +690,34 @@ function createCacheApi(workDir, transport, roots) {
|
|
|
689
690
|
}
|
|
690
691
|
};
|
|
691
692
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
693
|
+
/** Block size reserved per owner for cache pseudo-step indices. */
|
|
694
|
+
const CACHE_INDEX_BLOCK = 1e3;
|
|
695
|
+
/**
|
|
696
|
+
* Build the cache pseudo-step index allocator. Each owner (a real step index, or
|
|
697
|
+
* {@link JOB_CACHE_OWNER}) gets its own disjoint block of {@link CACHE_INDEX_BLOCK}
|
|
698
|
+
* indices, all above every real-step and hook index (`stepCount * 3 + 100`). A
|
|
699
|
+
* step's two-or-more cache pseudo-steps are a pure function of its own owner
|
|
700
|
+
* index, so concurrent children never collide. Under sequential execution the
|
|
701
|
+
* emitted indices stay above all real/hook indices exactly as before.
|
|
702
|
+
*/
|
|
703
|
+
function createCacheStepIndexAllocator(stepCount) {
|
|
704
|
+
const cacheBase = stepCount * 3 + 100;
|
|
705
|
+
const counters = /* @__PURE__ */ new Map();
|
|
706
|
+
return (ownerStepIndex) => {
|
|
707
|
+
const n = counters.get(ownerStepIndex) ?? 0;
|
|
708
|
+
counters.set(ownerStepIndex, n + 1);
|
|
709
|
+
return cacheBase + (ownerStepIndex + 1) * CACHE_INDEX_BLOCK + n;
|
|
710
|
+
};
|
|
711
|
+
}
|
|
706
712
|
/**
|
|
707
713
|
* Restore every spec, surfacing each as a `cache:restore` pseudo-step. Returns
|
|
708
714
|
* a map keyed by spec key recording whether the EXACT key hit (so the save
|
|
709
715
|
* phase can skip a redundant save of an entry that already exists).
|
|
710
716
|
*/
|
|
711
|
-
async function restoreCacheSpecs(specs, deps) {
|
|
717
|
+
async function restoreCacheSpecs(specs, deps, ownerStepIndex) {
|
|
712
718
|
const results = /* @__PURE__ */ new Map();
|
|
713
719
|
for (const spec of specs) {
|
|
714
|
-
const stepIndex = deps.nextStepIndex();
|
|
720
|
+
const stepIndex = deps.nextStepIndex(ownerStepIndex);
|
|
715
721
|
deps.sendIpc({
|
|
716
722
|
type: "step.start",
|
|
717
723
|
stepIndex,
|
|
@@ -761,10 +767,10 @@ async function restoreCacheSpecs(specs, deps) {
|
|
|
761
767
|
* whose restore matched a different key via a `restoreKeys` prefix is still
|
|
762
768
|
* saved under its exact key.
|
|
763
769
|
*/
|
|
764
|
-
async function saveCacheSpecs(specs, restoreResults, deps) {
|
|
770
|
+
async function saveCacheSpecs(specs, restoreResults, deps, ownerStepIndex) {
|
|
765
771
|
for (const spec of specs) {
|
|
766
772
|
if (restoreResults.get(spec.key)?.matchedKey === spec.key) continue;
|
|
767
|
-
const stepIndex = deps.nextStepIndex();
|
|
773
|
+
const stepIndex = deps.nextStepIndex(ownerStepIndex);
|
|
768
774
|
deps.sendIpc({
|
|
769
775
|
type: "step.start",
|
|
770
776
|
stepIndex,
|
|
@@ -1106,17 +1112,160 @@ initZx();
|
|
|
1106
1112
|
* @param event - Event payload from the dispatch message
|
|
1107
1113
|
* @param changedFiles - List of files changed in this event
|
|
1108
1114
|
* @param env - Merged environment variables
|
|
1115
|
+
* @param dispatchInputs - Operator dispatch inputs (`ctx.dispatchInputs`)
|
|
1116
|
+
* @param fanout - Fan-out position (`ctx.fanout`); undefined on a non-fan-out job
|
|
1109
1117
|
*/
|
|
1110
|
-
function createRuleContext(event, changedFiles = [], env = {}) {
|
|
1118
|
+
function createRuleContext(event, changedFiles = [], env = {}, dispatchInputs = {}, fanout) {
|
|
1111
1119
|
return {
|
|
1112
1120
|
event,
|
|
1113
1121
|
changedFiles,
|
|
1114
1122
|
env,
|
|
1123
|
+
dispatchInputs,
|
|
1124
|
+
...fanout && { fanout },
|
|
1115
1125
|
$
|
|
1116
1126
|
};
|
|
1117
1127
|
}
|
|
1118
1128
|
//#endregion
|
|
1129
|
+
//#region src/execution/sandbox/parallel-scheduler.ts
|
|
1130
|
+
/**
|
|
1131
|
+
* Concurrency-aware scheduler for `parallel()` step groups.
|
|
1132
|
+
*
|
|
1133
|
+
* A parallel group's children each run as their own observable step (own logs,
|
|
1134
|
+
* status, timing, retry, cache, hooks — all task-scoped by the Phase 0 per-task
|
|
1135
|
+
* isolation) through the same `runStepIteration` machinery the sequential loop
|
|
1136
|
+
* uses. Children launch behind a `maxParallel` window (queued children report
|
|
1137
|
+
* `pending`); the group joins at a barrier. On the first non-`continueOnError`
|
|
1138
|
+
* child failure when `failFast`, every in-flight sibling's per-task abort
|
|
1139
|
+
* controller is fired so its step race rejects and it is reported `cancelled`
|
|
1140
|
+
* (which is NOT a failure).
|
|
1141
|
+
*/
|
|
1142
|
+
/**
|
|
1143
|
+
* Wrap `sendIpc` so a child's own `step.start` / `step.complete` messages carry
|
|
1144
|
+
* the parallel-child concurrency role + the group id. Cache/secret pseudo-step
|
|
1145
|
+
* messages (different stepIndex) pass through untouched.
|
|
1146
|
+
*/
|
|
1147
|
+
function stampChildSend(sendIpc, childStepIndex, groupId) {
|
|
1148
|
+
return (msg) => {
|
|
1149
|
+
if ((msg.type === "step.start" || msg.type === "step.complete") && msg.stepIndex === childStepIndex) {
|
|
1150
|
+
sendIpc({
|
|
1151
|
+
...msg,
|
|
1152
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1153
|
+
groupId
|
|
1154
|
+
});
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
sendIpc(msg);
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/** Announce a child queued behind the `maxParallel` window as `pending`. */
|
|
1161
|
+
function emitPending(opts, child, groupId) {
|
|
1162
|
+
opts.sendIpc({
|
|
1163
|
+
type: "step.start",
|
|
1164
|
+
stepIndex: child.stepIndex,
|
|
1165
|
+
stepName: child.step.name,
|
|
1166
|
+
state: "pending",
|
|
1167
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1168
|
+
groupId
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
/** Mark a child that never launched (fail-fast already tripped) as `cancelled`. */
|
|
1172
|
+
function emitCancelledSkip(opts, child, groupId) {
|
|
1173
|
+
opts.sendIpc({
|
|
1174
|
+
type: "step.start",
|
|
1175
|
+
stepIndex: child.stepIndex,
|
|
1176
|
+
stepName: child.step.name,
|
|
1177
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1178
|
+
groupId
|
|
1179
|
+
});
|
|
1180
|
+
opts.sendIpc({
|
|
1181
|
+
type: "step.complete",
|
|
1182
|
+
stepIndex: child.stepIndex,
|
|
1183
|
+
status: "cancelled",
|
|
1184
|
+
durationMs: 0,
|
|
1185
|
+
concurrencyKind: StepConcurrencyKind.enum["parallel-child"],
|
|
1186
|
+
groupId
|
|
1187
|
+
});
|
|
1188
|
+
return {
|
|
1189
|
+
name: child.step.name,
|
|
1190
|
+
stepIndex: child.stepIndex,
|
|
1191
|
+
status: "cancelled",
|
|
1192
|
+
durationMs: 0
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Run a parallel group: launch children with a bounded-concurrency window, join
|
|
1197
|
+
* at a barrier, and fail-fast-cancel in-flight siblings on the first hard
|
|
1198
|
+
* failure.
|
|
1199
|
+
*/
|
|
1200
|
+
async function runParallelGroup(node, opts) {
|
|
1201
|
+
const { children, failFast, groupId } = node;
|
|
1202
|
+
const limit = node.maxParallel && node.maxParallel > 0 ? node.maxParallel : children.length;
|
|
1203
|
+
const results = new Array(children.length);
|
|
1204
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
1205
|
+
let failed = false;
|
|
1206
|
+
let failedStepName;
|
|
1207
|
+
for (let i = limit; i < children.length; i++) emitPending(opts, children[i], groupId);
|
|
1208
|
+
let cursor = 0;
|
|
1209
|
+
const worker = async () => {
|
|
1210
|
+
for (;;) {
|
|
1211
|
+
const idx = cursor++;
|
|
1212
|
+
if (idx >= children.length) return;
|
|
1213
|
+
const child = children[idx];
|
|
1214
|
+
if (failFast && failed) {
|
|
1215
|
+
results[idx] = emitCancelledSkip(opts, child, groupId);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
const childOpts = {
|
|
1219
|
+
...opts,
|
|
1220
|
+
sendIpc: stampChildSend(opts.sendIpc, child.stepIndex, groupId)
|
|
1221
|
+
};
|
|
1222
|
+
inFlight.add(child.stepIndex);
|
|
1223
|
+
try {
|
|
1224
|
+
const outcome = await runStepIteration(child.step, child.stepIndex, childOpts);
|
|
1225
|
+
results[idx] = outcome.result;
|
|
1226
|
+
if (outcome.shouldBreak) {
|
|
1227
|
+
if (!failed) {
|
|
1228
|
+
failed = true;
|
|
1229
|
+
failedStepName = outcome.failedStepName ?? child.step.name;
|
|
1230
|
+
}
|
|
1231
|
+
if (failFast) {
|
|
1232
|
+
for (const sibling of inFlight) if (sibling !== child.stepIndex) opts.abortStep?.(sibling);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
} finally {
|
|
1236
|
+
inFlight.delete(child.stepIndex);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
};
|
|
1240
|
+
await Promise.all(Array.from({ length: Math.min(limit, children.length) }, () => worker()));
|
|
1241
|
+
return {
|
|
1242
|
+
failed,
|
|
1243
|
+
failedStepName,
|
|
1244
|
+
results
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
//#endregion
|
|
1119
1248
|
//#region src/execution/sandbox/step-loop.ts
|
|
1249
|
+
/**
|
|
1250
|
+
* Thrown inside the step race when a step's own per-task abort controller fires
|
|
1251
|
+
* (parallel fail-fast cancels an in-flight sibling). Distinguished from a
|
|
1252
|
+
* timeout/job-deadline reject so the loop reports the step as `cancelled`
|
|
1253
|
+
* (which is NOT a failure) rather than `failed`.
|
|
1254
|
+
*/
|
|
1255
|
+
var StepCancelledError = class StepCancelledError extends Error {
|
|
1256
|
+
name = "StepCancelledError";
|
|
1257
|
+
constructor(stepName) {
|
|
1258
|
+
super(`Step '${stepName}' was cancelled by parallel fail-fast`);
|
|
1259
|
+
Object.setPrototypeOf(this, StepCancelledError.prototype);
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
/**
|
|
1263
|
+
* Resolve the capture-scope wrapper from options, falling back to a direct call
|
|
1264
|
+
* when no capture wiring is present (unit harnesses).
|
|
1265
|
+
*/
|
|
1266
|
+
function captureWrap(opts) {
|
|
1267
|
+
return opts.runWithStepCapture ?? ((_stepIndex, fn) => fn());
|
|
1268
|
+
}
|
|
1120
1269
|
/** Result of a rejected drift gate: the run was declined by a reviewer. */
|
|
1121
1270
|
var DriftGateRejectedError = class extends Error {
|
|
1122
1271
|
constructor(reason) {
|
|
@@ -1211,6 +1360,7 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1211
1360
|
const startTime = Date.now();
|
|
1212
1361
|
const abortController = new AbortController();
|
|
1213
1362
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
1363
|
+
const stepAbortSignal = opts.getStepAbortSignal?.(stepIndex);
|
|
1214
1364
|
try {
|
|
1215
1365
|
const phase = await Promise.race([
|
|
1216
1366
|
runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts),
|
|
@@ -1228,14 +1378,22 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1228
1378
|
jobDeadlineSignal.addEventListener("abort", () => {
|
|
1229
1379
|
reject(/* @__PURE__ */ new Error(`Step '${step.name}' aborted: job timeout exceeded`));
|
|
1230
1380
|
});
|
|
1381
|
+
}),
|
|
1382
|
+
new Promise((_, reject) => {
|
|
1383
|
+
if (!stepAbortSignal) return;
|
|
1384
|
+
if (stepAbortSignal.aborted) {
|
|
1385
|
+
reject(new StepCancelledError(step.name));
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
stepAbortSignal.addEventListener("abort", () => reject(new StepCancelledError(step.name)));
|
|
1231
1389
|
})
|
|
1232
1390
|
]);
|
|
1233
1391
|
clearTimeout(timeoutId);
|
|
1234
1392
|
const durationMs = Date.now() - startTime;
|
|
1235
1393
|
const outputsPayload = phase.outputs != null ? phase.outputs : void 0;
|
|
1236
1394
|
if (outputsPayload) outputsMap.set(step.name, outputsPayload);
|
|
1237
|
-
const secretsAccessed = getSecretsAccessLog?.();
|
|
1238
|
-
emitSecretMountEvents(getSecretMountRecords?.(), stepIndex, sendFn);
|
|
1395
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1396
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1239
1397
|
const stepStatus = phase.status === "skipped" ? ExecutionStepStatus.enum.skipped : ExecutionStepStatus.enum.success;
|
|
1240
1398
|
sendFn({
|
|
1241
1399
|
type: "step.complete",
|
|
@@ -1259,10 +1417,27 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1259
1417
|
clearTimeout(timeoutId);
|
|
1260
1418
|
const durationMs = Date.now() - startTime;
|
|
1261
1419
|
const error = e instanceof Error ? e : new Error(String(e));
|
|
1420
|
+
if (e instanceof StepCancelledError) {
|
|
1421
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1422
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1423
|
+
sendFn({
|
|
1424
|
+
type: "step.complete",
|
|
1425
|
+
stepIndex,
|
|
1426
|
+
status: ExecutionStepStatus.enum.cancelled,
|
|
1427
|
+
durationMs,
|
|
1428
|
+
...secretsAccessed !== void 0 && { secretsAccessed }
|
|
1429
|
+
});
|
|
1430
|
+
return {
|
|
1431
|
+
name: step.name,
|
|
1432
|
+
stepIndex,
|
|
1433
|
+
status: ExecutionStepStatus.enum.cancelled,
|
|
1434
|
+
durationMs
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1262
1437
|
const exitCode = extractExitCode(e);
|
|
1263
1438
|
const signal = extractSignal(e);
|
|
1264
|
-
const secretsAccessed = getSecretsAccessLog?.();
|
|
1265
|
-
emitSecretMountEvents(getSecretMountRecords?.(), stepIndex, sendFn);
|
|
1439
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1440
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1266
1441
|
sendFn({
|
|
1267
1442
|
type: "step.complete",
|
|
1268
1443
|
stepIndex,
|
|
@@ -1316,7 +1491,7 @@ function extractSignal(error) {
|
|
|
1316
1491
|
*/
|
|
1317
1492
|
async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
|
|
1318
1493
|
if (!step.rules || step.rules.length === 0) return null;
|
|
1319
|
-
const ruleCtx = createRuleContext(opts.event, [], opts.env);
|
|
1494
|
+
const ruleCtx = createRuleContext(opts.event, [], opts.env, opts.dispatchInputs ?? {}, opts.fanout);
|
|
1320
1495
|
const ruleResult = await evaluateRules(step.rules, ruleCtx, step.name);
|
|
1321
1496
|
if (ruleResult.allPassed) return null;
|
|
1322
1497
|
opts.sendIpc({
|
|
@@ -1391,7 +1566,7 @@ async function maybeGateStepApproval(step, stepIndex, opts) {
|
|
|
1391
1566
|
stepIndex,
|
|
1392
1567
|
line: `[kici] Step '${step.name}' ${why}.`
|
|
1393
1568
|
});
|
|
1394
|
-
await opts.disposeStepResources?.();
|
|
1569
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1395
1570
|
return {
|
|
1396
1571
|
result: {
|
|
1397
1572
|
name: step.name,
|
|
@@ -1417,15 +1592,16 @@ async function runObserverHook(args) {
|
|
|
1417
1592
|
startTime: opts.startTime ?? Date.now(),
|
|
1418
1593
|
...failedStep !== void 0 && { failedStep }
|
|
1419
1594
|
});
|
|
1420
|
-
const
|
|
1595
|
+
const ctx = opts.createStepContext(stepIndex, step.name);
|
|
1596
|
+
const hookResult = await captureWrap(opts)(hookStepIndex, () => executeHook({
|
|
1421
1597
|
hook,
|
|
1422
|
-
stepContext:
|
|
1598
|
+
stepContext: ctx,
|
|
1423
1599
|
outcome,
|
|
1424
1600
|
hookType,
|
|
1425
1601
|
stepIndex: hookStepIndex,
|
|
1426
1602
|
sendIpc: opts.sendIpc,
|
|
1427
1603
|
timeout: 3e5
|
|
1428
|
-
});
|
|
1604
|
+
}));
|
|
1429
1605
|
if (!hookResult.success) opts.sendIpc({
|
|
1430
1606
|
type: "log.line",
|
|
1431
1607
|
stepIndex,
|
|
@@ -1442,10 +1618,43 @@ async function runObserverHook(args) {
|
|
|
1442
1618
|
* `ctx.secrets.mountFile` tmpdir, any env vars set via `exposeFile`) is
|
|
1443
1619
|
* removed even when the step throws, times out, or rule-skips.
|
|
1444
1620
|
*/
|
|
1621
|
+
/**
|
|
1622
|
+
* Run a step through its retry policy. Each call to `executeStepInLoop` is one
|
|
1623
|
+
* attempt: it sets up its own per-attempt timeout from `step.timeout` and returns
|
|
1624
|
+
* a `SandboxStepResult` (it never throws — a failed attempt is reported as a
|
|
1625
|
+
* `failed` status with an `error`). A failed attempt is retried while attempts
|
|
1626
|
+
* remain AND `retryIf(reconstructedError)` is true; backoff sleeps between
|
|
1627
|
+
* attempts. The retry loop runs to completion BEFORE the caller applies
|
|
1628
|
+
* `continueOnError` to the final outcome.
|
|
1629
|
+
*/
|
|
1630
|
+
async function runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts) {
|
|
1631
|
+
const retry = step.retry;
|
|
1632
|
+
const max = retry?.maxAttempts ?? 1;
|
|
1633
|
+
let result;
|
|
1634
|
+
for (let n = 1; n <= max; n++) {
|
|
1635
|
+
result = await executeStepInLoop(step, stepIndex, ctx, timeoutMs, opts.sendIpc, opts.outputsMap, opts.getSecretsAccessLog, opts.getSecretMountRecords, opts.jobDeadlineSignal, opts.checkMode ?? CheckMode.enum.apply, opts);
|
|
1636
|
+
if (result.status !== ExecutionStepStatus.enum.failed) return result;
|
|
1637
|
+
const err = new Error(result.error?.message ?? `Step '${step.name}' failed`);
|
|
1638
|
+
if (!(n < max && (retry?.retryIf?.(err) ?? true))) break;
|
|
1639
|
+
const delay = computeBackoffDelay(n, {
|
|
1640
|
+
maxAttempts: max,
|
|
1641
|
+
delayMs: retry.delayMs,
|
|
1642
|
+
backoff: retry.backoff,
|
|
1643
|
+
maxDelayMs: retry.maxDelayMs
|
|
1644
|
+
});
|
|
1645
|
+
opts.sendIpc({
|
|
1646
|
+
type: "log.line",
|
|
1647
|
+
stepIndex,
|
|
1648
|
+
line: `[kici] Step '${step.name}' attempt ${n}/${max} failed: ${err.message}; retrying in ${delay}ms`
|
|
1649
|
+
});
|
|
1650
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1651
|
+
}
|
|
1652
|
+
return result;
|
|
1653
|
+
}
|
|
1445
1654
|
async function runStepIteration(step, stepIndex, opts) {
|
|
1446
1655
|
const skippedResult = await evaluateStepRulesAndMaybeSkip(step, stepIndex, opts);
|
|
1447
1656
|
if (skippedResult) {
|
|
1448
|
-
await opts.disposeStepResources?.();
|
|
1657
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1449
1658
|
return {
|
|
1450
1659
|
result: skippedResult,
|
|
1451
1660
|
shouldBreak: false
|
|
@@ -1462,18 +1671,18 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1462
1671
|
opts
|
|
1463
1672
|
});
|
|
1464
1673
|
const stepCacheSpecs = opts.cachePhaseDeps ? normalizeCacheSpecs(step.cache) : [];
|
|
1465
|
-
const stepCacheRestore = stepCacheSpecs.length > 0 && opts.cachePhaseDeps ? await restoreCacheSpecs(stepCacheSpecs, opts.cachePhaseDeps) : /* @__PURE__ */ new Map();
|
|
1674
|
+
const stepCacheRestore = stepCacheSpecs.length > 0 && opts.cachePhaseDeps ? await restoreCacheSpecs(stepCacheSpecs, opts.cachePhaseDeps, stepIndex) : /* @__PURE__ */ new Map();
|
|
1466
1675
|
try {
|
|
1467
|
-
await opts.beforeStepEnvFiles?.();
|
|
1676
|
+
await opts.beforeStepEnvFiles?.(stepIndex);
|
|
1468
1677
|
const ctx = opts.createStepContext(stepIndex, step.name);
|
|
1469
1678
|
const timeoutMs = step.timeout ?? opts.defaultTimeoutMs;
|
|
1470
1679
|
let result;
|
|
1471
1680
|
try {
|
|
1472
|
-
result = await
|
|
1681
|
+
result = await captureWrap(opts)(stepIndex, () => runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts));
|
|
1473
1682
|
} finally {
|
|
1474
|
-
await opts.afterStepApplyEnvFiles?.();
|
|
1683
|
+
await opts.afterStepApplyEnvFiles?.(stepIndex);
|
|
1475
1684
|
}
|
|
1476
|
-
if (stepCacheSpecs.length > 0 && opts.cachePhaseDeps && result.status === ExecutionStepStatus.enum.success) await saveCacheSpecs(stepCacheSpecs, stepCacheRestore, opts.cachePhaseDeps);
|
|
1685
|
+
if (stepCacheSpecs.length > 0 && opts.cachePhaseDeps && result.status === ExecutionStepStatus.enum.success) await saveCacheSpecs(stepCacheSpecs, stepCacheRestore, opts.cachePhaseDeps, stepIndex);
|
|
1477
1686
|
if (opts.jobHooks?.afterStep) await runObserverHook({
|
|
1478
1687
|
hook: opts.jobHooks.afterStep,
|
|
1479
1688
|
hookType: "afterStep",
|
|
@@ -1493,7 +1702,7 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1493
1702
|
shouldBreak: false
|
|
1494
1703
|
};
|
|
1495
1704
|
} finally {
|
|
1496
|
-
await opts.disposeStepResources?.();
|
|
1705
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1497
1706
|
}
|
|
1498
1707
|
}
|
|
1499
1708
|
/**
|
|
@@ -1509,14 +1718,15 @@ async function runCompletionHook(args) {
|
|
|
1509
1718
|
stepIndex: -1,
|
|
1510
1719
|
line: `[kici] Running ${hookType} hook...`
|
|
1511
1720
|
});
|
|
1512
|
-
const
|
|
1721
|
+
const ctx = opts.createStepContext(hookStepIndex, hookType);
|
|
1722
|
+
const hookResult = await captureWrap(opts)(hookStepIndex, () => executeHook({
|
|
1513
1723
|
hook,
|
|
1514
|
-
stepContext:
|
|
1724
|
+
stepContext: ctx,
|
|
1515
1725
|
outcome,
|
|
1516
1726
|
hookType,
|
|
1517
1727
|
stepIndex: hookStepIndex,
|
|
1518
1728
|
sendIpc: opts.sendIpc
|
|
1519
|
-
});
|
|
1729
|
+
}));
|
|
1520
1730
|
if (hookResult.success) {
|
|
1521
1731
|
opts.sendIpc({
|
|
1522
1732
|
type: "log.line",
|
|
@@ -1614,9 +1824,24 @@ async function executeStepLoop(opts) {
|
|
|
1614
1824
|
const startTime = opts.startTime ?? Date.now();
|
|
1615
1825
|
const stepResults = [];
|
|
1616
1826
|
const state = { failed: false };
|
|
1617
|
-
|
|
1827
|
+
const nodes = opts.stepNodes ?? opts.steps.map((step, i) => ({
|
|
1828
|
+
kind: "sequential",
|
|
1829
|
+
step,
|
|
1830
|
+
stepIndex: i
|
|
1831
|
+
}));
|
|
1832
|
+
for (const node of nodes) {
|
|
1618
1833
|
if (opts.isAborted?.()) break;
|
|
1619
|
-
|
|
1834
|
+
if (node.kind === "parallel") {
|
|
1835
|
+
const groupOutcome = await runParallelGroup(node, opts);
|
|
1836
|
+
stepResults.push(...groupOutcome.results);
|
|
1837
|
+
if (groupOutcome.failed) {
|
|
1838
|
+
state.failed = true;
|
|
1839
|
+
state.failedStepName = groupOutcome.failedStepName;
|
|
1840
|
+
break;
|
|
1841
|
+
}
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
const outcome = await runStepIteration(node.step, node.stepIndex, opts);
|
|
1620
1845
|
stepResults.push(outcome.result);
|
|
1621
1846
|
if (outcome.failedStepName) {
|
|
1622
1847
|
state.failed = true;
|
|
@@ -1637,6 +1862,69 @@ async function executeStepLoop(opts) {
|
|
|
1637
1862
|
};
|
|
1638
1863
|
}
|
|
1639
1864
|
//#endregion
|
|
1865
|
+
//#region src/execution/sandbox/capture-context.ts
|
|
1866
|
+
/**
|
|
1867
|
+
* Attributes captured console output to the step whose run is currently on the
|
|
1868
|
+
* async call stack.
|
|
1869
|
+
*
|
|
1870
|
+
* The monkey-patched `process.stdout/stderr.write` reads
|
|
1871
|
+
* {@link currentCaptureStepIndex} to decide which `step-N.log` a console line
|
|
1872
|
+
* belongs to. Keying this on the async execution context (rather than a single
|
|
1873
|
+
* module global) makes attribution correct when more than one step body runs
|
|
1874
|
+
* concurrently: each step's `run` executes inside its own
|
|
1875
|
+
* {@link runInStepCapture} scope, so its writes resolve to its own index even
|
|
1876
|
+
* while a sibling step is mid-flight. Under sequential execution exactly one
|
|
1877
|
+
* scope is active at a time, identical to the former global.
|
|
1878
|
+
*/
|
|
1879
|
+
const stepCaptureStore = new AsyncLocalStorage();
|
|
1880
|
+
/**
|
|
1881
|
+
* Run `fn` with `stepIndex` as the active console-capture attribution for the
|
|
1882
|
+
* duration of its async execution (including everything it awaits).
|
|
1883
|
+
*/
|
|
1884
|
+
function runInStepCapture(stepIndex, fn) {
|
|
1885
|
+
return stepCaptureStore.run(stepIndex, fn);
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* The step index whose run is currently on the async stack, or `-1` when no
|
|
1889
|
+
* capture scope is active (workflow-level / between-steps output).
|
|
1890
|
+
*/
|
|
1891
|
+
function currentCaptureStepIndex() {
|
|
1892
|
+
return stepCaptureStore.getStore() ?? -1;
|
|
1893
|
+
}
|
|
1894
|
+
//#endregion
|
|
1895
|
+
//#region src/execution/sandbox/step-task-registry.ts
|
|
1896
|
+
/**
|
|
1897
|
+
* Per-task replacement for the runner's former `currentStepSecrets` /
|
|
1898
|
+
* `currentStepDispose` single-slots.
|
|
1899
|
+
*
|
|
1900
|
+
* The runner used to remember only the *most recent* step's secrets handle and
|
|
1901
|
+
* dispose closure; the access-log / mount-record / dispose reader callbacks read
|
|
1902
|
+
* that single slot. Under sequential execution that is correct (one step at a
|
|
1903
|
+
* time), but two concurrently-running steps would clobber each other's
|
|
1904
|
+
* secrets-audit trail. Keying every slot by the step's index keeps each step's
|
|
1905
|
+
* audit trail and teardown isolated — sequential behavior is identical, Phase 1
|
|
1906
|
+
* concurrency is correct.
|
|
1907
|
+
*/
|
|
1908
|
+
var StepTaskRegistry = class {
|
|
1909
|
+
#slots = /* @__PURE__ */ new Map();
|
|
1910
|
+
set(stepIndex, slot) {
|
|
1911
|
+
this.#slots.set(stepIndex, slot);
|
|
1912
|
+
}
|
|
1913
|
+
getAccessLog(stepIndex) {
|
|
1914
|
+
return this.#slots.get(stepIndex)?.secrets.getAccessLog() ?? [];
|
|
1915
|
+
}
|
|
1916
|
+
getMountRecords(stepIndex) {
|
|
1917
|
+
const slot = this.#slots.get(stepIndex);
|
|
1918
|
+
return slot ? [...slot.secrets.getMountRecords()] : [];
|
|
1919
|
+
}
|
|
1920
|
+
async dispose(stepIndex) {
|
|
1921
|
+
const slot = this.#slots.get(stepIndex);
|
|
1922
|
+
if (!slot) return;
|
|
1923
|
+
this.#slots.delete(stepIndex);
|
|
1924
|
+
await slot.dispose();
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
//#endregion
|
|
1640
1928
|
//#region src/execution/env-init/init-phase.ts
|
|
1641
1929
|
/** Default init timeout when a spec sets none: 10 minutes. */
|
|
1642
1930
|
const DEFAULT_INIT_TIMEOUT_MS = 600 * 1e3;
|
|
@@ -3111,8 +3399,8 @@ function logSubprocessStreams(e, tokens) {
|
|
|
3111
3399
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
3112
3400
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
3113
3401
|
*/
|
|
3114
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
3115
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
3402
|
+
const AGENT_SDK_VERSION = "0.1.24";
|
|
3403
|
+
const AGENT_SDK_BUNDLE_HASH = "031712cf8cd02f483365113aad7e946da982110d544b177eb05edcc0d4421253";
|
|
3116
3404
|
/**
|
|
3117
3405
|
* Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
|
|
3118
3406
|
* subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
|
|
@@ -3474,7 +3762,7 @@ async function applyOverlay(config) {
|
|
|
3474
3762
|
*/
|
|
3475
3763
|
init_download();
|
|
3476
3764
|
init_dep_restore();
|
|
3477
|
-
const AGENT_VERSION = "0.1.
|
|
3765
|
+
const AGENT_VERSION = "0.1.24";
|
|
3478
3766
|
process.on("uncaughtException", (err) => {
|
|
3479
3767
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
3480
3768
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -3497,12 +3785,6 @@ const isForkMode = typeof process.send === "function";
|
|
|
3497
3785
|
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
3498
3786
|
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
3499
3787
|
/**
|
|
3500
|
-
* Current step index for console.log/console.error capture.
|
|
3501
|
-
* When >= 0, process.stdout/stderr writes are intercepted and sent as
|
|
3502
|
-
* log.line IPC messages for the given step. Set to -1 outside step execution.
|
|
3503
|
-
*/
|
|
3504
|
-
let captureStepIndex = -1;
|
|
3505
|
-
/**
|
|
3506
3788
|
* Workflow-level capture flag for the pre-step `prepare` phase
|
|
3507
3789
|
* (module load, concurrency-group evaluation, rule evaluation).
|
|
3508
3790
|
*
|
|
@@ -3513,17 +3795,18 @@ let captureStepIndex = -1;
|
|
|
3513
3795
|
* rule check functions lands in the job's workflow-level log file
|
|
3514
3796
|
* (`executions/{runId}/job-{name}/step--1.log`) alongside runner narration.
|
|
3515
3797
|
*
|
|
3516
|
-
* Mutually exclusive with
|
|
3517
|
-
*
|
|
3798
|
+
* Mutually exclusive with a step capture scope: the step loop resets this flag
|
|
3799
|
+
* to false before any step runs inside its {@link runInStepCapture} scope (the
|
|
3800
|
+
* async-context source of {@link currentCaptureStepIndex}).
|
|
3518
3801
|
*/
|
|
3519
3802
|
let capturePrepareActive = false;
|
|
3520
3803
|
/** The maskedSend function used by the output capture. Set during main(). */
|
|
3521
3804
|
let captureSendFn = null;
|
|
3522
3805
|
function captureIsActive() {
|
|
3523
|
-
return (
|
|
3806
|
+
return (currentCaptureStepIndex() >= 0 || capturePrepareActive) && captureSendFn !== null;
|
|
3524
3807
|
}
|
|
3525
3808
|
function captureTargetIndex() {
|
|
3526
|
-
return
|
|
3809
|
+
return currentCaptureStepIndex();
|
|
3527
3810
|
}
|
|
3528
3811
|
/**
|
|
3529
3812
|
* Install monkey-patches on process.stdout.write and process.stderr.write
|
|
@@ -3672,6 +3955,14 @@ let jobTimedOutMs;
|
|
|
3672
3955
|
*/
|
|
3673
3956
|
const jobDeadlineAbort = new AbortController();
|
|
3674
3957
|
/**
|
|
3958
|
+
* Aborted when the job is being torn down by cancellation (abort IPC / SIGTERM),
|
|
3959
|
+
* as distinct from the wall-clock deadline (`jobDeadlineAbort`). Each step's
|
|
3960
|
+
* `ctx.signal` composes this with the deadline signal and its own per-step
|
|
3961
|
+
* controller, so a cancelled job cooperatively unwinds in-flight step bodies
|
|
3962
|
+
* that opted into `ctx.signal`. Inert for the rest of the runner today.
|
|
3963
|
+
*/
|
|
3964
|
+
const jobCancelAbort = new AbortController();
|
|
3965
|
+
/**
|
|
3675
3966
|
* Pending promises for event.emit requests awaiting responses from the agent.
|
|
3676
3967
|
*
|
|
3677
3968
|
* Key: requestId (correlates EventEmitRequest -> EventEmitResponse)
|
|
@@ -4035,17 +4326,16 @@ function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
|
4035
4326
|
* post-loop save phase skip exact-key hits.
|
|
4036
4327
|
*/
|
|
4037
4328
|
async function setupJobCache(stepCwd, stepCount, jobCache, sendIpc) {
|
|
4038
|
-
let cacheStepCursor = stepCount * 3 + 100;
|
|
4039
4329
|
const cachePhaseDeps = {
|
|
4040
4330
|
cache: createCacheApi(stepCwd, buildCacheTransport()),
|
|
4041
4331
|
sendIpc,
|
|
4042
|
-
nextStepIndex: ()
|
|
4332
|
+
nextStepIndex: createCacheStepIndexAllocator(stepCount)
|
|
4043
4333
|
};
|
|
4044
4334
|
const jobCacheSpecs = normalizeCacheSpecs(jobCache);
|
|
4045
4335
|
return {
|
|
4046
4336
|
cachePhaseDeps,
|
|
4047
4337
|
jobCacheSpecs,
|
|
4048
|
-
jobCacheRestore: jobCacheSpecs.length > 0 ? await restoreCacheSpecs(jobCacheSpecs, cachePhaseDeps) : /* @__PURE__ */ new Map()
|
|
4338
|
+
jobCacheRestore: jobCacheSpecs.length > 0 ? await restoreCacheSpecs(jobCacheSpecs, cachePhaseDeps, -1) : /* @__PURE__ */ new Map()
|
|
4049
4339
|
};
|
|
4050
4340
|
}
|
|
4051
4341
|
/**
|
|
@@ -4058,7 +4348,7 @@ async function setupJobCache(stepCwd, stepCount, jobCache, sendIpc) {
|
|
|
4058
4348
|
*/
|
|
4059
4349
|
async function maybeSaveJobCache(specs, restoreResults, deps, succeeded) {
|
|
4060
4350
|
if (specs.length === 0 || !succeeded) return;
|
|
4061
|
-
await saveCacheSpecs(specs, restoreResults, deps);
|
|
4351
|
+
await saveCacheSpecs(specs, restoreResults, deps, -1);
|
|
4062
4352
|
}
|
|
4063
4353
|
/**
|
|
4064
4354
|
* Dispatch an agent-to-runner message to the appropriate pending handler.
|
|
@@ -4067,6 +4357,7 @@ async function maybeSaveJobCache(specs, restoreResults, deps, succeeded) {
|
|
|
4067
4357
|
function dispatchAgentMessage(msg) {
|
|
4068
4358
|
if (msg.type === "abort") {
|
|
4069
4359
|
aborted = true;
|
|
4360
|
+
jobCancelAbort.abort();
|
|
4070
4361
|
if (msg.force) forceAborted = true;
|
|
4071
4362
|
} else if (msg.type === "event.emit.response") {
|
|
4072
4363
|
const pending = pendingEmitResponses.get(msg.requestId);
|
|
@@ -4099,6 +4390,7 @@ else getStdinRl().on("line", (line) => {
|
|
|
4099
4390
|
});
|
|
4100
4391
|
process.on("SIGTERM", () => {
|
|
4101
4392
|
aborted = true;
|
|
4393
|
+
jobCancelAbort.abort();
|
|
4102
4394
|
});
|
|
4103
4395
|
/**
|
|
4104
4396
|
* Create a Logger that sends log lines via IPC.
|
|
@@ -4333,7 +4625,7 @@ function buildSandboxShell(cwd, stepIndex, maskedSendFn) {
|
|
|
4333
4625
|
* NOT serialized across the process boundary. This means zx $ runs natively
|
|
4334
4626
|
* inside this process with full shell access.
|
|
4335
4627
|
*/
|
|
4336
|
-
function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedSendFn, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, secrets, masker) {
|
|
4628
|
+
function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedSendFn, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, secrets, masker, signal) {
|
|
4337
4629
|
const step$ = buildSandboxShell(workDir, stepIndex, maskedSendFn);
|
|
4338
4630
|
const log = createIpcLogger(stepIndex, stepName, maskedSendFn);
|
|
4339
4631
|
const rawPayload = rawPayloadFromEvent(request.event);
|
|
@@ -4352,6 +4644,7 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
4352
4644
|
return {
|
|
4353
4645
|
$: step$,
|
|
4354
4646
|
log,
|
|
4647
|
+
signal,
|
|
4355
4648
|
env: process.env,
|
|
4356
4649
|
setEnv: (key, value) => {
|
|
4357
4650
|
applyEnvDelta({
|
|
@@ -4407,12 +4700,32 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
4407
4700
|
...request.matrixValues && { matrix: request.matrixValues },
|
|
4408
4701
|
...request.host && { host: request.host },
|
|
4409
4702
|
...request.agent && { agent: request.agent },
|
|
4703
|
+
...(() => {
|
|
4704
|
+
const fanout = deriveFanout(request);
|
|
4705
|
+
return fanout ? { fanout } : {};
|
|
4706
|
+
})(),
|
|
4707
|
+
dispatchInputs: request.dispatchInputs ?? {},
|
|
4410
4708
|
...(() => {
|
|
4411
4709
|
const needs = buildStepNeedsContext(request.jobNeeds, request.upstreamJobOutputs, request.upstreamJobStatuses);
|
|
4412
4710
|
return needs ? { needs } : {};
|
|
4413
4711
|
})()
|
|
4414
4712
|
};
|
|
4415
4713
|
}
|
|
4714
|
+
/**
|
|
4715
|
+
* Derive the fan-out position (`ctx.fanout`) from a dispatch request. Returns
|
|
4716
|
+
* `undefined` for a non-fan-out job (no `fanoutTotal`), so `ctx.fanout` is only
|
|
4717
|
+
* set for `runsOnAll` host children and matrix combinations.
|
|
4718
|
+
*/
|
|
4719
|
+
function deriveFanout(request) {
|
|
4720
|
+
if (request.fanoutTotal === void 0) return void 0;
|
|
4721
|
+
const index = request.fanoutIndex ?? 0;
|
|
4722
|
+
return {
|
|
4723
|
+
index,
|
|
4724
|
+
total: request.fanoutTotal,
|
|
4725
|
+
first: index === 0,
|
|
4726
|
+
last: index === request.fanoutTotal - 1
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4416
4729
|
/** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
|
|
4417
4730
|
function rawPayloadFromEvent(event) {
|
|
4418
4731
|
if (!event) return void 0;
|
|
@@ -4905,7 +5218,7 @@ async function runCancelPathHooks(args) {
|
|
|
4905
5218
|
const ctx = createStepCtxWithCapture(hookStepIndex, label);
|
|
4906
5219
|
let hookResult;
|
|
4907
5220
|
try {
|
|
4908
|
-
hookResult = await executeHook({
|
|
5221
|
+
hookResult = await runInStepCapture(hookStepIndex, () => executeHook({
|
|
4909
5222
|
hook,
|
|
4910
5223
|
stepContext: ctx,
|
|
4911
5224
|
outcome: failedStep ? {
|
|
@@ -4915,9 +5228,9 @@ async function runCancelPathHooks(args) {
|
|
|
4915
5228
|
hookType,
|
|
4916
5229
|
stepIndex: hookStepIndex,
|
|
4917
5230
|
sendIpc: maskedSend
|
|
4918
|
-
});
|
|
5231
|
+
}));
|
|
4919
5232
|
} finally {
|
|
4920
|
-
await disposeStepResources();
|
|
5233
|
+
await disposeStepResources(hookStepIndex);
|
|
4921
5234
|
}
|
|
4922
5235
|
hookStepIndex++;
|
|
4923
5236
|
if (!hookResult.success) {
|
|
@@ -4968,29 +5281,66 @@ async function extractAndNormalizeSteps(workflow, request, apiTransport) {
|
|
|
4968
5281
|
} else rawSteps = extractSteps(workflow, request.jobName);
|
|
4969
5282
|
const refMap = /* @__PURE__ */ new WeakMap();
|
|
4970
5283
|
let stepCounter = 0;
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5284
|
+
const coerce = (stepOrFn) => {
|
|
5285
|
+
if (typeof stepOrFn === "function") {
|
|
5286
|
+
stepCounter++;
|
|
5287
|
+
const name = `step-${stepCounter}`;
|
|
5288
|
+
refMap.set(stepOrFn, name);
|
|
5289
|
+
return {
|
|
5290
|
+
_tag: "Step",
|
|
5291
|
+
name,
|
|
5292
|
+
run: stepOrFn,
|
|
5293
|
+
outputs: void 0
|
|
5294
|
+
};
|
|
5295
|
+
}
|
|
5296
|
+
const s = stepOrFn;
|
|
5297
|
+
if (!s.name) {
|
|
5298
|
+
stepCounter++;
|
|
5299
|
+
return {
|
|
5300
|
+
...s,
|
|
5301
|
+
name: `step-${stepCounter}`
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
return s;
|
|
5305
|
+
};
|
|
5306
|
+
const normalizedSteps = [];
|
|
5307
|
+
const nodes = [];
|
|
5308
|
+
let flatIndex = 0;
|
|
5309
|
+
let groupOrdinal = 0;
|
|
5310
|
+
for (const entry of rawSteps) {
|
|
5311
|
+
if (isParallelGroup(entry)) {
|
|
5312
|
+
const groupId = `g${groupOrdinal++}`;
|
|
5313
|
+
const children = entry.steps.map((child) => {
|
|
5314
|
+
const step = coerce(child);
|
|
5315
|
+
const stepIndex = flatIndex++;
|
|
5316
|
+
normalizedSteps.push(step);
|
|
4987
5317
|
return {
|
|
4988
|
-
|
|
4989
|
-
|
|
5318
|
+
step,
|
|
5319
|
+
stepIndex
|
|
4990
5320
|
};
|
|
4991
|
-
}
|
|
4992
|
-
|
|
4993
|
-
|
|
5321
|
+
});
|
|
5322
|
+
nodes.push({
|
|
5323
|
+
kind: "parallel",
|
|
5324
|
+
groupId,
|
|
5325
|
+
name: entry.name ?? groupId,
|
|
5326
|
+
failFast: entry.failFast,
|
|
5327
|
+
...entry.maxParallel !== void 0 && { maxParallel: entry.maxParallel },
|
|
5328
|
+
children
|
|
5329
|
+
});
|
|
5330
|
+
continue;
|
|
5331
|
+
}
|
|
5332
|
+
const step = coerce(entry);
|
|
5333
|
+
const stepIndex = flatIndex++;
|
|
5334
|
+
normalizedSteps.push(step);
|
|
5335
|
+
nodes.push({
|
|
5336
|
+
kind: "sequential",
|
|
5337
|
+
step,
|
|
5338
|
+
stepIndex
|
|
5339
|
+
});
|
|
5340
|
+
}
|
|
5341
|
+
return {
|
|
5342
|
+
normalizedSteps,
|
|
5343
|
+
nodes,
|
|
4994
5344
|
refMap,
|
|
4995
5345
|
driftDroppedJobs
|
|
4996
5346
|
};
|
|
@@ -5026,7 +5376,7 @@ function buildOutputInfrastructure(request, refMap) {
|
|
|
5026
5376
|
*/
|
|
5027
5377
|
async function maybeSkipJobOnRules(job, request, normalizedSteps) {
|
|
5028
5378
|
if (!job?.rules || job.rules.length === 0) return false;
|
|
5029
|
-
const ruleCtx = createRuleContext(request.event ?? {}, [], process.env);
|
|
5379
|
+
const ruleCtx = createRuleContext(request.event ?? {}, [], process.env, request.dispatchInputs ?? {}, deriveFanout(request));
|
|
5030
5380
|
if ((await evaluateRules(job.rules, ruleCtx, request.jobName)).allPassed) return false;
|
|
5031
5381
|
const skippedResults = normalizedSteps.map((s, i) => ({
|
|
5032
5382
|
name: s.name,
|
|
@@ -5087,22 +5437,40 @@ async function applyEnvFilesDelta(envFiles, operatorSecretKeys, maskedSend) {
|
|
|
5087
5437
|
await truncateEnvFiles(envFiles);
|
|
5088
5438
|
}
|
|
5089
5439
|
/**
|
|
5090
|
-
* Build the step loop's KICI_ENV/KICI_PATH callbacks
|
|
5091
|
-
*
|
|
5092
|
-
*
|
|
5440
|
+
* Build the step loop's KICI_ENV/KICI_PATH callbacks with a per-step delta-file
|
|
5441
|
+
* pair (keyed by step index). `beforeStepEnvFiles(stepIndex)` lazily creates the
|
|
5442
|
+
* step's pair and points the runner's process.env at it (each step's zx $
|
|
5443
|
+
* snapshots process.env at context creation, which happens AFTER this
|
|
5093
5444
|
* before-hook, so the shell sees them; the pre-fork env allowlist does not
|
|
5094
|
-
* re-filter runtime-set vars). `afterStepApplyEnvFiles` applies
|
|
5095
|
-
* delta
|
|
5096
|
-
|
|
5097
|
-
|
|
5445
|
+
* re-filter runtime-set vars). `afterStepApplyEnvFiles(stepIndex)` applies that
|
|
5446
|
+
* step's delta and releases the pair.
|
|
5447
|
+
*
|
|
5448
|
+
* Env-isolation contract: under sequential execution this is identical to a
|
|
5449
|
+
* single shared pair truncated between steps — each step still sees only its own
|
|
5450
|
+
* delta. The pair is now per-step so two concurrently-running steps cannot
|
|
5451
|
+
* corrupt each other's delta file. `process.env.KICI_ENV` / `process.env.KICI_PATH`
|
|
5452
|
+
* remain process-global, so Phase 1 forbids `setEnv` / `addPath` / `$KICI_ENV`
|
|
5453
|
+
* writes inside `parallel()` children (compile-time validation); Phase 0 only
|
|
5454
|
+
* makes the file pair per-task.
|
|
5455
|
+
*/
|
|
5456
|
+
function buildStepEnvFileHooks(operatorSecretKeys, maskedSend) {
|
|
5457
|
+
const perStep = /* @__PURE__ */ new Map();
|
|
5098
5458
|
return {
|
|
5099
|
-
beforeStepEnvFiles: async () => {
|
|
5100
|
-
|
|
5101
|
-
|
|
5459
|
+
beforeStepEnvFiles: async (stepIndex) => {
|
|
5460
|
+
let files = perStep.get(stepIndex);
|
|
5461
|
+
if (!files) {
|
|
5462
|
+
files = await createEnvFiles(tmpdir());
|
|
5463
|
+
perStep.set(stepIndex, files);
|
|
5464
|
+
}
|
|
5465
|
+
process.env.KICI_ENV = files.envFile;
|
|
5466
|
+
process.env.KICI_PATH = files.pathFile;
|
|
5102
5467
|
},
|
|
5103
|
-
afterStepApplyEnvFiles: async () => {
|
|
5468
|
+
afterStepApplyEnvFiles: async (stepIndex) => {
|
|
5469
|
+
const files = perStep.get(stepIndex);
|
|
5470
|
+
if (!files) return;
|
|
5471
|
+
perStep.delete(stepIndex);
|
|
5104
5472
|
try {
|
|
5105
|
-
await applyEnvFilesDelta(
|
|
5473
|
+
await applyEnvFilesDelta(files, operatorSecretKeys, maskedSend);
|
|
5106
5474
|
} catch (err) {
|
|
5107
5475
|
maskedSend({
|
|
5108
5476
|
type: "log.line",
|
|
@@ -5237,7 +5605,7 @@ async function main() {
|
|
|
5237
5605
|
});
|
|
5238
5606
|
return waitForApiResponse(reqId);
|
|
5239
5607
|
};
|
|
5240
|
-
const { normalizedSteps, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport);
|
|
5608
|
+
const { normalizedSteps, nodes, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport);
|
|
5241
5609
|
const { operatorSecretKeys, outputsMap, secretOutputs, jobOutputsMap } = buildOutputInfrastructure(request, refMap);
|
|
5242
5610
|
const job = findJob(workflow, request.jobName);
|
|
5243
5611
|
await maybeSkipJobOnRules(job, request, normalizedSteps);
|
|
@@ -5247,33 +5615,43 @@ async function main() {
|
|
|
5247
5615
|
flushOutputCapture();
|
|
5248
5616
|
capturePrepareActive = false;
|
|
5249
5617
|
const stepCwd = sourceDir;
|
|
5250
|
-
const envFiles = await createEnvFiles(tmpdir());
|
|
5251
5618
|
await runInitPhaseOrFailJob({
|
|
5252
5619
|
job,
|
|
5253
5620
|
stepCwd,
|
|
5254
|
-
envFiles,
|
|
5621
|
+
envFiles: await createEnvFiles(tmpdir()),
|
|
5255
5622
|
operatorSecretKeys,
|
|
5256
5623
|
maskedSend
|
|
5257
5624
|
});
|
|
5258
5625
|
const { cachePhaseDeps, jobCacheSpecs, jobCacheRestore } = await setupJobCache(stepCwd, normalizedSteps.length, job?.cache, maskedSend);
|
|
5259
|
-
|
|
5260
|
-
|
|
5626
|
+
const stepTasks = new StepTaskRegistry();
|
|
5627
|
+
const stepAbortControllers = /* @__PURE__ */ new Map();
|
|
5261
5628
|
const createStepCtxWithCapture = (stepIndex, stepName) => {
|
|
5262
|
-
|
|
5629
|
+
const stepAbort = new AbortController();
|
|
5630
|
+
stepAbortControllers.set(stepIndex, stepAbort);
|
|
5631
|
+
const signal = AbortSignal.any([
|
|
5632
|
+
stepAbort.signal,
|
|
5633
|
+
jobCancelAbort.signal,
|
|
5634
|
+
jobDeadlineAbort.signal
|
|
5635
|
+
]);
|
|
5263
5636
|
const handle = buildStepSecrets(request, masker, () => {});
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5637
|
+
stepTasks.set(stepIndex, {
|
|
5638
|
+
secrets: handle.secrets,
|
|
5639
|
+
dispose: handle.dispose
|
|
5640
|
+
});
|
|
5641
|
+
const ctx = createSandboxStepContext(stepCwd, stepIndex, stepName, request, maskedSend, outputsMap, refMap, operatorSecretKeys, secretOutputs, jobOutputsMap, handle.secrets, masker, signal);
|
|
5267
5642
|
if (globalRepoInfo) {
|
|
5268
5643
|
ctx.workflowRepo = globalRepoInfo.workflowRepo;
|
|
5269
5644
|
ctx.sourceRepo = globalRepoInfo.sourceRepo;
|
|
5270
5645
|
}
|
|
5271
5646
|
return ctx;
|
|
5272
5647
|
};
|
|
5273
|
-
const stepEnvHooks = buildStepEnvFileHooks(
|
|
5648
|
+
const stepEnvHooks = buildStepEnvFileHooks(operatorSecretKeys, maskedSend);
|
|
5274
5649
|
const jobStartTime = Date.now();
|
|
5275
5650
|
const loopResult = await executeStepLoop({
|
|
5276
5651
|
steps: normalizedSteps,
|
|
5652
|
+
stepNodes: nodes,
|
|
5653
|
+
abortStep: (stepIndex) => stepAbortControllers.get(stepIndex)?.abort(),
|
|
5654
|
+
getStepAbortSignal: (stepIndex) => stepAbortControllers.get(stepIndex)?.signal,
|
|
5277
5655
|
checkMode: request.checkMode,
|
|
5278
5656
|
createStepContext: createStepCtxWithCapture,
|
|
5279
5657
|
sendIpc: maskedSend,
|
|
@@ -5281,24 +5659,22 @@ async function main() {
|
|
|
5281
5659
|
outputsMap,
|
|
5282
5660
|
event: request.event ?? {},
|
|
5283
5661
|
env: process.env,
|
|
5662
|
+
dispatchInputs: request.dispatchInputs ?? {},
|
|
5663
|
+
fanout: deriveFanout(request),
|
|
5284
5664
|
jobHooks,
|
|
5285
5665
|
cachePhaseDeps,
|
|
5286
5666
|
isAborted: () => aborted,
|
|
5287
5667
|
jobDeadlineSignal: jobDeadlineAbort.signal,
|
|
5288
5668
|
startTime: jobStartTime,
|
|
5289
|
-
|
|
5669
|
+
runWithStepCapture: runInStepCapture,
|
|
5670
|
+
getSecretsAccessLog: (stepIndex) => {
|
|
5290
5671
|
flushOutputCapture();
|
|
5291
|
-
|
|
5292
|
-
return currentStepSecrets?.getAccessLog() ?? [];
|
|
5293
|
-
},
|
|
5294
|
-
getSecretMountRecords: () => {
|
|
5295
|
-
return currentStepSecrets ? [...currentStepSecrets.getMountRecords()] : [];
|
|
5672
|
+
return stepTasks.getAccessLog(stepIndex);
|
|
5296
5673
|
},
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
if (disposeFn) await disposeFn();
|
|
5674
|
+
getSecretMountRecords: (stepIndex) => stepTasks.getMountRecords(stepIndex),
|
|
5675
|
+
disposeStepResources: (stepIndex) => {
|
|
5676
|
+
stepAbortControllers.delete(stepIndex);
|
|
5677
|
+
return stepTasks.dispose(stepIndex);
|
|
5302
5678
|
},
|
|
5303
5679
|
beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
|
|
5304
5680
|
afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
|
|
@@ -5318,11 +5694,9 @@ async function main() {
|
|
|
5318
5694
|
jobStartTime,
|
|
5319
5695
|
outputsMap,
|
|
5320
5696
|
createStepCtxWithCapture,
|
|
5321
|
-
disposeStepResources:
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
currentStepSecrets = null;
|
|
5325
|
-
if (disposeFn) await disposeFn();
|
|
5697
|
+
disposeStepResources: (stepIndex) => {
|
|
5698
|
+
stepAbortControllers.delete(stepIndex);
|
|
5699
|
+
return stepTasks.dispose(stepIndex);
|
|
5326
5700
|
},
|
|
5327
5701
|
maskedSend
|
|
5328
5702
|
});
|
|
@@ -5389,6 +5763,6 @@ main().catch((error) => {
|
|
|
5389
5763
|
setTimeout(() => process.exit(1), 100);
|
|
5390
5764
|
});
|
|
5391
5765
|
//#endregion
|
|
5392
|
-
export { buildStepNeedsContext, createSandboxStepContext, rawPayloadFromEvent };
|
|
5766
|
+
export { buildStepEnvFileHooks, buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
|
|
5393
5767
|
|
|
5394
5768
|
//# sourceMappingURL=workflow-runner.js.map
|