@kici-dev/agent 0.1.23 → 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/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/sandbox/capture-context.d.ts +11 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +17 -3
- package/dist/execution/sandbox/parallel-scheduler.d.ts +34 -0
- package/dist/execution/sandbox/step-loop.d.ts +91 -16
- 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 +22 -1
- package/dist/server.js +107 -52
- package/dist/workflow-runner.js +436 -121
- package/package.json +6 -6
- package/sbom.spdx.json +68 -68
package/dist/workflow-runner.js
CHANGED
|
@@ -7,8 +7,8 @@ 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
13
|
import { computeBackoffDelay, sha256File as sha256File$1 } from "@kici-dev/core";
|
|
14
14
|
import { calculateJwkThumbprint, decodeJwt, exportJWK, generateKeyPair } from "jose";
|
|
@@ -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,
|
|
@@ -1120,7 +1126,146 @@ function createRuleContext(event, changedFiles = [], env = {}, dispatchInputs =
|
|
|
1120
1126
|
};
|
|
1121
1127
|
}
|
|
1122
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
|
|
1123
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
|
+
}
|
|
1124
1269
|
/** Result of a rejected drift gate: the run was declined by a reviewer. */
|
|
1125
1270
|
var DriftGateRejectedError = class extends Error {
|
|
1126
1271
|
constructor(reason) {
|
|
@@ -1215,6 +1360,7 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1215
1360
|
const startTime = Date.now();
|
|
1216
1361
|
const abortController = new AbortController();
|
|
1217
1362
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
1363
|
+
const stepAbortSignal = opts.getStepAbortSignal?.(stepIndex);
|
|
1218
1364
|
try {
|
|
1219
1365
|
const phase = await Promise.race([
|
|
1220
1366
|
runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts),
|
|
@@ -1232,14 +1378,22 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1232
1378
|
jobDeadlineSignal.addEventListener("abort", () => {
|
|
1233
1379
|
reject(/* @__PURE__ */ new Error(`Step '${step.name}' aborted: job timeout exceeded`));
|
|
1234
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)));
|
|
1235
1389
|
})
|
|
1236
1390
|
]);
|
|
1237
1391
|
clearTimeout(timeoutId);
|
|
1238
1392
|
const durationMs = Date.now() - startTime;
|
|
1239
1393
|
const outputsPayload = phase.outputs != null ? phase.outputs : void 0;
|
|
1240
1394
|
if (outputsPayload) outputsMap.set(step.name, outputsPayload);
|
|
1241
|
-
const secretsAccessed = getSecretsAccessLog?.();
|
|
1242
|
-
emitSecretMountEvents(getSecretMountRecords?.(), stepIndex, sendFn);
|
|
1395
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1396
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1243
1397
|
const stepStatus = phase.status === "skipped" ? ExecutionStepStatus.enum.skipped : ExecutionStepStatus.enum.success;
|
|
1244
1398
|
sendFn({
|
|
1245
1399
|
type: "step.complete",
|
|
@@ -1263,10 +1417,27 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1263
1417
|
clearTimeout(timeoutId);
|
|
1264
1418
|
const durationMs = Date.now() - startTime;
|
|
1265
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
|
+
}
|
|
1266
1437
|
const exitCode = extractExitCode(e);
|
|
1267
1438
|
const signal = extractSignal(e);
|
|
1268
|
-
const secretsAccessed = getSecretsAccessLog?.();
|
|
1269
|
-
emitSecretMountEvents(getSecretMountRecords?.(), stepIndex, sendFn);
|
|
1439
|
+
const secretsAccessed = getSecretsAccessLog?.(stepIndex);
|
|
1440
|
+
emitSecretMountEvents(getSecretMountRecords?.(stepIndex), stepIndex, sendFn);
|
|
1270
1441
|
sendFn({
|
|
1271
1442
|
type: "step.complete",
|
|
1272
1443
|
stepIndex,
|
|
@@ -1395,7 +1566,7 @@ async function maybeGateStepApproval(step, stepIndex, opts) {
|
|
|
1395
1566
|
stepIndex,
|
|
1396
1567
|
line: `[kici] Step '${step.name}' ${why}.`
|
|
1397
1568
|
});
|
|
1398
|
-
await opts.disposeStepResources?.();
|
|
1569
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1399
1570
|
return {
|
|
1400
1571
|
result: {
|
|
1401
1572
|
name: step.name,
|
|
@@ -1421,15 +1592,16 @@ async function runObserverHook(args) {
|
|
|
1421
1592
|
startTime: opts.startTime ?? Date.now(),
|
|
1422
1593
|
...failedStep !== void 0 && { failedStep }
|
|
1423
1594
|
});
|
|
1424
|
-
const
|
|
1595
|
+
const ctx = opts.createStepContext(stepIndex, step.name);
|
|
1596
|
+
const hookResult = await captureWrap(opts)(hookStepIndex, () => executeHook({
|
|
1425
1597
|
hook,
|
|
1426
|
-
stepContext:
|
|
1598
|
+
stepContext: ctx,
|
|
1427
1599
|
outcome,
|
|
1428
1600
|
hookType,
|
|
1429
1601
|
stepIndex: hookStepIndex,
|
|
1430
1602
|
sendIpc: opts.sendIpc,
|
|
1431
1603
|
timeout: 3e5
|
|
1432
|
-
});
|
|
1604
|
+
}));
|
|
1433
1605
|
if (!hookResult.success) opts.sendIpc({
|
|
1434
1606
|
type: "log.line",
|
|
1435
1607
|
stepIndex,
|
|
@@ -1482,7 +1654,7 @@ async function runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts) {
|
|
|
1482
1654
|
async function runStepIteration(step, stepIndex, opts) {
|
|
1483
1655
|
const skippedResult = await evaluateStepRulesAndMaybeSkip(step, stepIndex, opts);
|
|
1484
1656
|
if (skippedResult) {
|
|
1485
|
-
await opts.disposeStepResources?.();
|
|
1657
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1486
1658
|
return {
|
|
1487
1659
|
result: skippedResult,
|
|
1488
1660
|
shouldBreak: false
|
|
@@ -1499,18 +1671,18 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1499
1671
|
opts
|
|
1500
1672
|
});
|
|
1501
1673
|
const stepCacheSpecs = opts.cachePhaseDeps ? normalizeCacheSpecs(step.cache) : [];
|
|
1502
|
-
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();
|
|
1503
1675
|
try {
|
|
1504
|
-
await opts.beforeStepEnvFiles?.();
|
|
1676
|
+
await opts.beforeStepEnvFiles?.(stepIndex);
|
|
1505
1677
|
const ctx = opts.createStepContext(stepIndex, step.name);
|
|
1506
1678
|
const timeoutMs = step.timeout ?? opts.defaultTimeoutMs;
|
|
1507
1679
|
let result;
|
|
1508
1680
|
try {
|
|
1509
|
-
result = await runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts);
|
|
1681
|
+
result = await captureWrap(opts)(stepIndex, () => runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts));
|
|
1510
1682
|
} finally {
|
|
1511
|
-
await opts.afterStepApplyEnvFiles?.();
|
|
1683
|
+
await opts.afterStepApplyEnvFiles?.(stepIndex);
|
|
1512
1684
|
}
|
|
1513
|
-
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);
|
|
1514
1686
|
if (opts.jobHooks?.afterStep) await runObserverHook({
|
|
1515
1687
|
hook: opts.jobHooks.afterStep,
|
|
1516
1688
|
hookType: "afterStep",
|
|
@@ -1530,7 +1702,7 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1530
1702
|
shouldBreak: false
|
|
1531
1703
|
};
|
|
1532
1704
|
} finally {
|
|
1533
|
-
await opts.disposeStepResources?.();
|
|
1705
|
+
await opts.disposeStepResources?.(stepIndex);
|
|
1534
1706
|
}
|
|
1535
1707
|
}
|
|
1536
1708
|
/**
|
|
@@ -1546,14 +1718,15 @@ async function runCompletionHook(args) {
|
|
|
1546
1718
|
stepIndex: -1,
|
|
1547
1719
|
line: `[kici] Running ${hookType} hook...`
|
|
1548
1720
|
});
|
|
1549
|
-
const
|
|
1721
|
+
const ctx = opts.createStepContext(hookStepIndex, hookType);
|
|
1722
|
+
const hookResult = await captureWrap(opts)(hookStepIndex, () => executeHook({
|
|
1550
1723
|
hook,
|
|
1551
|
-
stepContext:
|
|
1724
|
+
stepContext: ctx,
|
|
1552
1725
|
outcome,
|
|
1553
1726
|
hookType,
|
|
1554
1727
|
stepIndex: hookStepIndex,
|
|
1555
1728
|
sendIpc: opts.sendIpc
|
|
1556
|
-
});
|
|
1729
|
+
}));
|
|
1557
1730
|
if (hookResult.success) {
|
|
1558
1731
|
opts.sendIpc({
|
|
1559
1732
|
type: "log.line",
|
|
@@ -1651,9 +1824,24 @@ async function executeStepLoop(opts) {
|
|
|
1651
1824
|
const startTime = opts.startTime ?? Date.now();
|
|
1652
1825
|
const stepResults = [];
|
|
1653
1826
|
const state = { failed: false };
|
|
1654
|
-
|
|
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) {
|
|
1655
1833
|
if (opts.isAborted?.()) break;
|
|
1656
|
-
|
|
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);
|
|
1657
1845
|
stepResults.push(outcome.result);
|
|
1658
1846
|
if (outcome.failedStepName) {
|
|
1659
1847
|
state.failed = true;
|
|
@@ -1674,6 +1862,69 @@ async function executeStepLoop(opts) {
|
|
|
1674
1862
|
};
|
|
1675
1863
|
}
|
|
1676
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
|
|
1677
1928
|
//#region src/execution/env-init/init-phase.ts
|
|
1678
1929
|
/** Default init timeout when a spec sets none: 10 minutes. */
|
|
1679
1930
|
const DEFAULT_INIT_TIMEOUT_MS = 600 * 1e3;
|
|
@@ -3148,8 +3399,8 @@ function logSubprocessStreams(e, tokens) {
|
|
|
3148
3399
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
3149
3400
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
3150
3401
|
*/
|
|
3151
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
3152
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
3402
|
+
const AGENT_SDK_VERSION = "0.1.24";
|
|
3403
|
+
const AGENT_SDK_BUNDLE_HASH = "031712cf8cd02f483365113aad7e946da982110d544b177eb05edcc0d4421253";
|
|
3153
3404
|
/**
|
|
3154
3405
|
* Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
|
|
3155
3406
|
* subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
|
|
@@ -3511,7 +3762,7 @@ async function applyOverlay(config) {
|
|
|
3511
3762
|
*/
|
|
3512
3763
|
init_download();
|
|
3513
3764
|
init_dep_restore();
|
|
3514
|
-
const AGENT_VERSION = "0.1.
|
|
3765
|
+
const AGENT_VERSION = "0.1.24";
|
|
3515
3766
|
process.on("uncaughtException", (err) => {
|
|
3516
3767
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
3517
3768
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -3534,12 +3785,6 @@ const isForkMode = typeof process.send === "function";
|
|
|
3534
3785
|
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
3535
3786
|
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
3536
3787
|
/**
|
|
3537
|
-
* Current step index for console.log/console.error capture.
|
|
3538
|
-
* When >= 0, process.stdout/stderr writes are intercepted and sent as
|
|
3539
|
-
* log.line IPC messages for the given step. Set to -1 outside step execution.
|
|
3540
|
-
*/
|
|
3541
|
-
let captureStepIndex = -1;
|
|
3542
|
-
/**
|
|
3543
3788
|
* Workflow-level capture flag for the pre-step `prepare` phase
|
|
3544
3789
|
* (module load, concurrency-group evaluation, rule evaluation).
|
|
3545
3790
|
*
|
|
@@ -3550,17 +3795,18 @@ let captureStepIndex = -1;
|
|
|
3550
3795
|
* rule check functions lands in the job's workflow-level log file
|
|
3551
3796
|
* (`executions/{runId}/job-{name}/step--1.log`) alongside runner narration.
|
|
3552
3797
|
*
|
|
3553
|
-
* Mutually exclusive with
|
|
3554
|
-
*
|
|
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}).
|
|
3555
3801
|
*/
|
|
3556
3802
|
let capturePrepareActive = false;
|
|
3557
3803
|
/** The maskedSend function used by the output capture. Set during main(). */
|
|
3558
3804
|
let captureSendFn = null;
|
|
3559
3805
|
function captureIsActive() {
|
|
3560
|
-
return (
|
|
3806
|
+
return (currentCaptureStepIndex() >= 0 || capturePrepareActive) && captureSendFn !== null;
|
|
3561
3807
|
}
|
|
3562
3808
|
function captureTargetIndex() {
|
|
3563
|
-
return
|
|
3809
|
+
return currentCaptureStepIndex();
|
|
3564
3810
|
}
|
|
3565
3811
|
/**
|
|
3566
3812
|
* Install monkey-patches on process.stdout.write and process.stderr.write
|
|
@@ -3709,6 +3955,14 @@ let jobTimedOutMs;
|
|
|
3709
3955
|
*/
|
|
3710
3956
|
const jobDeadlineAbort = new AbortController();
|
|
3711
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
|
+
/**
|
|
3712
3966
|
* Pending promises for event.emit requests awaiting responses from the agent.
|
|
3713
3967
|
*
|
|
3714
3968
|
* Key: requestId (correlates EventEmitRequest -> EventEmitResponse)
|
|
@@ -4072,17 +4326,16 @@ function buildAttestProvenanceFn(request, workDir, getIdToken) {
|
|
|
4072
4326
|
* post-loop save phase skip exact-key hits.
|
|
4073
4327
|
*/
|
|
4074
4328
|
async function setupJobCache(stepCwd, stepCount, jobCache, sendIpc) {
|
|
4075
|
-
let cacheStepCursor = stepCount * 3 + 100;
|
|
4076
4329
|
const cachePhaseDeps = {
|
|
4077
4330
|
cache: createCacheApi(stepCwd, buildCacheTransport()),
|
|
4078
4331
|
sendIpc,
|
|
4079
|
-
nextStepIndex: ()
|
|
4332
|
+
nextStepIndex: createCacheStepIndexAllocator(stepCount)
|
|
4080
4333
|
};
|
|
4081
4334
|
const jobCacheSpecs = normalizeCacheSpecs(jobCache);
|
|
4082
4335
|
return {
|
|
4083
4336
|
cachePhaseDeps,
|
|
4084
4337
|
jobCacheSpecs,
|
|
4085
|
-
jobCacheRestore: jobCacheSpecs.length > 0 ? await restoreCacheSpecs(jobCacheSpecs, cachePhaseDeps) : /* @__PURE__ */ new Map()
|
|
4338
|
+
jobCacheRestore: jobCacheSpecs.length > 0 ? await restoreCacheSpecs(jobCacheSpecs, cachePhaseDeps, -1) : /* @__PURE__ */ new Map()
|
|
4086
4339
|
};
|
|
4087
4340
|
}
|
|
4088
4341
|
/**
|
|
@@ -4095,7 +4348,7 @@ async function setupJobCache(stepCwd, stepCount, jobCache, sendIpc) {
|
|
|
4095
4348
|
*/
|
|
4096
4349
|
async function maybeSaveJobCache(specs, restoreResults, deps, succeeded) {
|
|
4097
4350
|
if (specs.length === 0 || !succeeded) return;
|
|
4098
|
-
await saveCacheSpecs(specs, restoreResults, deps);
|
|
4351
|
+
await saveCacheSpecs(specs, restoreResults, deps, -1);
|
|
4099
4352
|
}
|
|
4100
4353
|
/**
|
|
4101
4354
|
* Dispatch an agent-to-runner message to the appropriate pending handler.
|
|
@@ -4104,6 +4357,7 @@ async function maybeSaveJobCache(specs, restoreResults, deps, succeeded) {
|
|
|
4104
4357
|
function dispatchAgentMessage(msg) {
|
|
4105
4358
|
if (msg.type === "abort") {
|
|
4106
4359
|
aborted = true;
|
|
4360
|
+
jobCancelAbort.abort();
|
|
4107
4361
|
if (msg.force) forceAborted = true;
|
|
4108
4362
|
} else if (msg.type === "event.emit.response") {
|
|
4109
4363
|
const pending = pendingEmitResponses.get(msg.requestId);
|
|
@@ -4136,6 +4390,7 @@ else getStdinRl().on("line", (line) => {
|
|
|
4136
4390
|
});
|
|
4137
4391
|
process.on("SIGTERM", () => {
|
|
4138
4392
|
aborted = true;
|
|
4393
|
+
jobCancelAbort.abort();
|
|
4139
4394
|
});
|
|
4140
4395
|
/**
|
|
4141
4396
|
* Create a Logger that sends log lines via IPC.
|
|
@@ -4370,7 +4625,7 @@ function buildSandboxShell(cwd, stepIndex, maskedSendFn) {
|
|
|
4370
4625
|
* NOT serialized across the process boundary. This means zx $ runs natively
|
|
4371
4626
|
* inside this process with full shell access.
|
|
4372
4627
|
*/
|
|
4373
|
-
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) {
|
|
4374
4629
|
const step$ = buildSandboxShell(workDir, stepIndex, maskedSendFn);
|
|
4375
4630
|
const log = createIpcLogger(stepIndex, stepName, maskedSendFn);
|
|
4376
4631
|
const rawPayload = rawPayloadFromEvent(request.event);
|
|
@@ -4389,6 +4644,7 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
4389
4644
|
return {
|
|
4390
4645
|
$: step$,
|
|
4391
4646
|
log,
|
|
4647
|
+
signal,
|
|
4392
4648
|
env: process.env,
|
|
4393
4649
|
setEnv: (key, value) => {
|
|
4394
4650
|
applyEnvDelta({
|
|
@@ -4962,7 +5218,7 @@ async function runCancelPathHooks(args) {
|
|
|
4962
5218
|
const ctx = createStepCtxWithCapture(hookStepIndex, label);
|
|
4963
5219
|
let hookResult;
|
|
4964
5220
|
try {
|
|
4965
|
-
hookResult = await executeHook({
|
|
5221
|
+
hookResult = await runInStepCapture(hookStepIndex, () => executeHook({
|
|
4966
5222
|
hook,
|
|
4967
5223
|
stepContext: ctx,
|
|
4968
5224
|
outcome: failedStep ? {
|
|
@@ -4972,9 +5228,9 @@ async function runCancelPathHooks(args) {
|
|
|
4972
5228
|
hookType,
|
|
4973
5229
|
stepIndex: hookStepIndex,
|
|
4974
5230
|
sendIpc: maskedSend
|
|
4975
|
-
});
|
|
5231
|
+
}));
|
|
4976
5232
|
} finally {
|
|
4977
|
-
await disposeStepResources();
|
|
5233
|
+
await disposeStepResources(hookStepIndex);
|
|
4978
5234
|
}
|
|
4979
5235
|
hookStepIndex++;
|
|
4980
5236
|
if (!hookResult.success) {
|
|
@@ -5025,29 +5281,66 @@ async function extractAndNormalizeSteps(workflow, request, apiTransport) {
|
|
|
5025
5281
|
} else rawSteps = extractSteps(workflow, request.jobName);
|
|
5026
5282
|
const refMap = /* @__PURE__ */ new WeakMap();
|
|
5027
5283
|
let stepCounter = 0;
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
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);
|
|
5044
5317
|
return {
|
|
5045
|
-
|
|
5046
|
-
|
|
5318
|
+
step,
|
|
5319
|
+
stepIndex
|
|
5047
5320
|
};
|
|
5048
|
-
}
|
|
5049
|
-
|
|
5050
|
-
|
|
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,
|
|
5051
5344
|
refMap,
|
|
5052
5345
|
driftDroppedJobs
|
|
5053
5346
|
};
|
|
@@ -5144,22 +5437,40 @@ async function applyEnvFilesDelta(envFiles, operatorSecretKeys, maskedSend) {
|
|
|
5144
5437
|
await truncateEnvFiles(envFiles);
|
|
5145
5438
|
}
|
|
5146
5439
|
/**
|
|
5147
|
-
* Build the step loop's KICI_ENV/KICI_PATH callbacks
|
|
5148
|
-
*
|
|
5149
|
-
*
|
|
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
|
|
5150
5444
|
* before-hook, so the shell sees them; the pre-fork env allowlist does not
|
|
5151
|
-
* re-filter runtime-set vars). `afterStepApplyEnvFiles` applies
|
|
5152
|
-
* delta
|
|
5153
|
-
|
|
5154
|
-
|
|
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();
|
|
5155
5458
|
return {
|
|
5156
|
-
beforeStepEnvFiles: async () => {
|
|
5157
|
-
|
|
5158
|
-
|
|
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;
|
|
5159
5467
|
},
|
|
5160
|
-
afterStepApplyEnvFiles: async () => {
|
|
5468
|
+
afterStepApplyEnvFiles: async (stepIndex) => {
|
|
5469
|
+
const files = perStep.get(stepIndex);
|
|
5470
|
+
if (!files) return;
|
|
5471
|
+
perStep.delete(stepIndex);
|
|
5161
5472
|
try {
|
|
5162
|
-
await applyEnvFilesDelta(
|
|
5473
|
+
await applyEnvFilesDelta(files, operatorSecretKeys, maskedSend);
|
|
5163
5474
|
} catch (err) {
|
|
5164
5475
|
maskedSend({
|
|
5165
5476
|
type: "log.line",
|
|
@@ -5294,7 +5605,7 @@ async function main() {
|
|
|
5294
5605
|
});
|
|
5295
5606
|
return waitForApiResponse(reqId);
|
|
5296
5607
|
};
|
|
5297
|
-
const { normalizedSteps, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport);
|
|
5608
|
+
const { normalizedSteps, nodes, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport);
|
|
5298
5609
|
const { operatorSecretKeys, outputsMap, secretOutputs, jobOutputsMap } = buildOutputInfrastructure(request, refMap);
|
|
5299
5610
|
const job = findJob(workflow, request.jobName);
|
|
5300
5611
|
await maybeSkipJobOnRules(job, request, normalizedSteps);
|
|
@@ -5304,33 +5615,43 @@ async function main() {
|
|
|
5304
5615
|
flushOutputCapture();
|
|
5305
5616
|
capturePrepareActive = false;
|
|
5306
5617
|
const stepCwd = sourceDir;
|
|
5307
|
-
const envFiles = await createEnvFiles(tmpdir());
|
|
5308
5618
|
await runInitPhaseOrFailJob({
|
|
5309
5619
|
job,
|
|
5310
5620
|
stepCwd,
|
|
5311
|
-
envFiles,
|
|
5621
|
+
envFiles: await createEnvFiles(tmpdir()),
|
|
5312
5622
|
operatorSecretKeys,
|
|
5313
5623
|
maskedSend
|
|
5314
5624
|
});
|
|
5315
5625
|
const { cachePhaseDeps, jobCacheSpecs, jobCacheRestore } = await setupJobCache(stepCwd, normalizedSteps.length, job?.cache, maskedSend);
|
|
5316
|
-
|
|
5317
|
-
|
|
5626
|
+
const stepTasks = new StepTaskRegistry();
|
|
5627
|
+
const stepAbortControllers = /* @__PURE__ */ new Map();
|
|
5318
5628
|
const createStepCtxWithCapture = (stepIndex, stepName) => {
|
|
5319
|
-
|
|
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
|
+
]);
|
|
5320
5636
|
const handle = buildStepSecrets(request, masker, () => {});
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
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);
|
|
5324
5642
|
if (globalRepoInfo) {
|
|
5325
5643
|
ctx.workflowRepo = globalRepoInfo.workflowRepo;
|
|
5326
5644
|
ctx.sourceRepo = globalRepoInfo.sourceRepo;
|
|
5327
5645
|
}
|
|
5328
5646
|
return ctx;
|
|
5329
5647
|
};
|
|
5330
|
-
const stepEnvHooks = buildStepEnvFileHooks(
|
|
5648
|
+
const stepEnvHooks = buildStepEnvFileHooks(operatorSecretKeys, maskedSend);
|
|
5331
5649
|
const jobStartTime = Date.now();
|
|
5332
5650
|
const loopResult = await executeStepLoop({
|
|
5333
5651
|
steps: normalizedSteps,
|
|
5652
|
+
stepNodes: nodes,
|
|
5653
|
+
abortStep: (stepIndex) => stepAbortControllers.get(stepIndex)?.abort(),
|
|
5654
|
+
getStepAbortSignal: (stepIndex) => stepAbortControllers.get(stepIndex)?.signal,
|
|
5334
5655
|
checkMode: request.checkMode,
|
|
5335
5656
|
createStepContext: createStepCtxWithCapture,
|
|
5336
5657
|
sendIpc: maskedSend,
|
|
@@ -5345,19 +5666,15 @@ async function main() {
|
|
|
5345
5666
|
isAborted: () => aborted,
|
|
5346
5667
|
jobDeadlineSignal: jobDeadlineAbort.signal,
|
|
5347
5668
|
startTime: jobStartTime,
|
|
5348
|
-
|
|
5669
|
+
runWithStepCapture: runInStepCapture,
|
|
5670
|
+
getSecretsAccessLog: (stepIndex) => {
|
|
5349
5671
|
flushOutputCapture();
|
|
5350
|
-
|
|
5351
|
-
return currentStepSecrets?.getAccessLog() ?? [];
|
|
5352
|
-
},
|
|
5353
|
-
getSecretMountRecords: () => {
|
|
5354
|
-
return currentStepSecrets ? [...currentStepSecrets.getMountRecords()] : [];
|
|
5672
|
+
return stepTasks.getAccessLog(stepIndex);
|
|
5355
5673
|
},
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
if (disposeFn) await disposeFn();
|
|
5674
|
+
getSecretMountRecords: (stepIndex) => stepTasks.getMountRecords(stepIndex),
|
|
5675
|
+
disposeStepResources: (stepIndex) => {
|
|
5676
|
+
stepAbortControllers.delete(stepIndex);
|
|
5677
|
+
return stepTasks.dispose(stepIndex);
|
|
5361
5678
|
},
|
|
5362
5679
|
beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
|
|
5363
5680
|
afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
|
|
@@ -5377,11 +5694,9 @@ async function main() {
|
|
|
5377
5694
|
jobStartTime,
|
|
5378
5695
|
outputsMap,
|
|
5379
5696
|
createStepCtxWithCapture,
|
|
5380
|
-
disposeStepResources:
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
currentStepSecrets = null;
|
|
5384
|
-
if (disposeFn) await disposeFn();
|
|
5697
|
+
disposeStepResources: (stepIndex) => {
|
|
5698
|
+
stepAbortControllers.delete(stepIndex);
|
|
5699
|
+
return stepTasks.dispose(stepIndex);
|
|
5385
5700
|
},
|
|
5386
5701
|
maskedSend
|
|
5387
5702
|
});
|
|
@@ -5448,6 +5763,6 @@ main().catch((error) => {
|
|
|
5448
5763
|
setTimeout(() => process.exit(1), 100);
|
|
5449
5764
|
});
|
|
5450
5765
|
//#endregion
|
|
5451
|
-
export { buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
|
|
5766
|
+
export { buildStepEnvFileHooks, buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
|
|
5452
5767
|
|
|
5453
5768
|
//# sourceMappingURL=workflow-runner.js.map
|