@kici-dev/agent 0.1.21 → 0.1.23
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/config.d.ts +2 -0
- package/dist/execution/job-runner.d.ts +22 -0
- package/dist/execution/reboot.d.ts +26 -0
- package/dist/execution/rule-evaluator.d.ts +4 -2
- package/dist/execution/sandbox/ipc-protocol.d.ts +24 -1
- package/dist/execution/sandbox/step-loop.d.ts +32 -6
- package/dist/execution/sandbox/workflow-runner.d.ts +17 -0
- package/dist/index.js +3 -1
- package/dist/server.js +486 -54
- package/dist/workflow-runner.js +234 -52
- package/package.json +5 -5
- package/sbom.spdx.json +62 -62
package/dist/workflow-runner.js
CHANGED
|
@@ -8,9 +8,9 @@ import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:pa
|
|
|
8
8
|
import { $ } from "zx";
|
|
9
9
|
import { createLogger, deriveSharedSecret, initZx, normalizeLineEndings, sha256, sha256File, toErrorMessage } from "@kici-dev/shared";
|
|
10
10
|
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, TimeoutReason } from "@kici-dev/engine";
|
|
11
|
-
import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn,
|
|
11
|
+
import { buildKiciApi, buildNeedsContext, createStepSecrets, evaluateRules, isDynamicJobFn, 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";
|
|
@@ -1106,17 +1106,28 @@ initZx();
|
|
|
1106
1106
|
* @param event - Event payload from the dispatch message
|
|
1107
1107
|
* @param changedFiles - List of files changed in this event
|
|
1108
1108
|
* @param env - Merged environment variables
|
|
1109
|
+
* @param dispatchInputs - Operator dispatch inputs (`ctx.dispatchInputs`)
|
|
1110
|
+
* @param fanout - Fan-out position (`ctx.fanout`); undefined on a non-fan-out job
|
|
1109
1111
|
*/
|
|
1110
|
-
function createRuleContext(event, changedFiles = [], env = {}) {
|
|
1112
|
+
function createRuleContext(event, changedFiles = [], env = {}, dispatchInputs = {}, fanout) {
|
|
1111
1113
|
return {
|
|
1112
1114
|
event,
|
|
1113
1115
|
changedFiles,
|
|
1114
1116
|
env,
|
|
1117
|
+
dispatchInputs,
|
|
1118
|
+
...fanout && { fanout },
|
|
1115
1119
|
$
|
|
1116
1120
|
};
|
|
1117
1121
|
}
|
|
1118
1122
|
//#endregion
|
|
1119
1123
|
//#region src/execution/sandbox/step-loop.ts
|
|
1124
|
+
/** Result of a rejected drift gate: the run was declined by a reviewer. */
|
|
1125
|
+
var DriftGateRejectedError = class extends Error {
|
|
1126
|
+
constructor(reason) {
|
|
1127
|
+
super(reason ? `approval rejected: ${reason}` : "approval rejected");
|
|
1128
|
+
this.name = "DriftGateRejectedError";
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1120
1131
|
/**
|
|
1121
1132
|
* Run one step honoring the run-level {@link CheckMode}, reusing the
|
|
1122
1133
|
* `runIdempotentStep` primitive for checked steps (never hand-rolled branching).
|
|
@@ -1124,10 +1135,13 @@ function createRuleContext(event, changedFiles = [], env = {}) {
|
|
|
1124
1135
|
* - Plain step (no `check`): in apply mode, runs as today; in any check mode it
|
|
1125
1136
|
* is skipped with `no_check` (a side-effecting step can't be safely previewed).
|
|
1126
1137
|
* - Checked step: adapted into an `IdempotentStep` and driven by the primitive
|
|
1127
|
-
* with `dryRun` set in check mode (so `apply`/`run` never fires)
|
|
1128
|
-
*
|
|
1138
|
+
* with `dryRun` set in check mode (so `apply`/`run` never fires). On drift the
|
|
1139
|
+
* summary is emitted as a log line. A `approval: { when: 'drift' }` step in
|
|
1140
|
+
* apply mode passes a `confirm` callback that round-trips a payload-bearing
|
|
1141
|
+
* step-approval; on reject the gate throws (fail-stop). Any other apply-mode
|
|
1142
|
+
* step uses `yes: true`.
|
|
1129
1143
|
*/
|
|
1130
|
-
async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn) {
|
|
1144
|
+
async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts) {
|
|
1131
1145
|
if (!step.check) {
|
|
1132
1146
|
if (checkMode !== CheckMode.enum.apply) return {
|
|
1133
1147
|
checkOutcome: CheckStepOutcome.enum.no_check,
|
|
@@ -1139,15 +1153,37 @@ async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn) {
|
|
|
1139
1153
|
outputs: await step.run(ctx)
|
|
1140
1154
|
};
|
|
1141
1155
|
}
|
|
1142
|
-
|
|
1156
|
+
let lastDrift = null;
|
|
1157
|
+
const adapted = {
|
|
1143
1158
|
name: step.name,
|
|
1144
|
-
check: () =>
|
|
1159
|
+
check: async () => {
|
|
1160
|
+
lastDrift = await step.check(ctx);
|
|
1161
|
+
return lastDrift;
|
|
1162
|
+
},
|
|
1145
1163
|
summarize: step.summarize,
|
|
1146
1164
|
apply: (drift) => step.run(ctx, drift),
|
|
1147
1165
|
whenInSync: step.whenInSync ? () => step.whenInSync(ctx) : void 0
|
|
1148
|
-
}
|
|
1166
|
+
};
|
|
1167
|
+
const driftGate = step.approval !== void 0 && normalizeApproval(step.approval).when === "drift" && checkMode === CheckMode.enum.apply && opts.awaitStepApprovalWithPayload !== void 0;
|
|
1168
|
+
const res = await runIdempotentStep(adapted, {
|
|
1149
1169
|
dryRun: checkMode !== CheckMode.enum.apply,
|
|
1150
|
-
|
|
1170
|
+
...driftGate ? { confirm: async () => {
|
|
1171
|
+
const norm = normalizeApproval(step.approval);
|
|
1172
|
+
const summaryMarkdown = step.summarize(lastDrift);
|
|
1173
|
+
const resolution = await opts.awaitStepApprovalWithPayload({
|
|
1174
|
+
stepIndex,
|
|
1175
|
+
stepName: step.name,
|
|
1176
|
+
clauses: norm.clauses,
|
|
1177
|
+
reason: norm.reason ?? `Approval required for drift in '${step.name}'`,
|
|
1178
|
+
...norm.timeoutSeconds !== void 0 && { timeoutSeconds: norm.timeoutSeconds },
|
|
1179
|
+
payload: {
|
|
1180
|
+
summaryMarkdown,
|
|
1181
|
+
drift: lastDrift
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
if (resolution.outcome === "approved") return true;
|
|
1185
|
+
throw new DriftGateRejectedError(resolution.outcome === "expired" ? "approval expired" : resolution.reason);
|
|
1186
|
+
} } : { yes: true },
|
|
1151
1187
|
log: (line) => sendFn({
|
|
1152
1188
|
type: "log.line",
|
|
1153
1189
|
stepIndex,
|
|
@@ -1170,7 +1206,7 @@ async function runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn) {
|
|
|
1170
1206
|
*
|
|
1171
1207
|
* Timeout pattern using Promise.race + AbortController, with IPC status reporting.
|
|
1172
1208
|
*/
|
|
1173
|
-
async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, outputsMap, getSecretsAccessLog, getSecretMountRecords, jobDeadlineSignal, checkMode
|
|
1209
|
+
async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, outputsMap, getSecretsAccessLog, getSecretMountRecords, jobDeadlineSignal, checkMode, opts) {
|
|
1174
1210
|
sendFn({
|
|
1175
1211
|
type: "step.start",
|
|
1176
1212
|
stepIndex,
|
|
@@ -1181,7 +1217,7 @@ async function executeStepInLoop(step, stepIndex, ctx, timeoutMs, sendFn, output
|
|
|
1181
1217
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
1182
1218
|
try {
|
|
1183
1219
|
const phase = await Promise.race([
|
|
1184
|
-
runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn),
|
|
1220
|
+
runStepWithCheckMode(step, stepIndex, ctx, checkMode, sendFn, opts),
|
|
1185
1221
|
new Promise((_, reject) => {
|
|
1186
1222
|
abortController.signal.addEventListener("abort", () => {
|
|
1187
1223
|
reject(/* @__PURE__ */ new Error(`Step '${step.name}' timed out after ${timeoutMs}ms`));
|
|
@@ -1284,7 +1320,7 @@ function extractSignal(error) {
|
|
|
1284
1320
|
*/
|
|
1285
1321
|
async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
|
|
1286
1322
|
if (!step.rules || step.rules.length === 0) return null;
|
|
1287
|
-
const ruleCtx = createRuleContext(opts.event, [], opts.env);
|
|
1323
|
+
const ruleCtx = createRuleContext(opts.event, [], opts.env, opts.dispatchInputs ?? {}, opts.fanout);
|
|
1288
1324
|
const ruleResult = await evaluateRules(step.rules, ruleCtx, step.name);
|
|
1289
1325
|
if (ruleResult.allPassed) return null;
|
|
1290
1326
|
opts.sendIpc({
|
|
@@ -1311,14 +1347,17 @@ async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
|
|
|
1311
1347
|
};
|
|
1312
1348
|
}
|
|
1313
1349
|
/**
|
|
1314
|
-
*
|
|
1315
|
-
* and the harness wired `awaitStepApproval`, block until the
|
|
1316
|
-
* resolves the hold.
|
|
1317
|
-
*
|
|
1350
|
+
* Pre-step manual approval gate. When the step declares `approval` with
|
|
1351
|
+
* `when: 'always'` and the harness wired `awaitStepApproval`, block until the
|
|
1352
|
+
* orchestrator resolves the hold. A `when: 'drift'` gate is NOT handled here —
|
|
1353
|
+
* it fires mid-execution inside `runStepWithCheckMode` once `check()` returns
|
|
1354
|
+
* drift. Returns a failed `StepIterationOutcome` (breaking the loop) on
|
|
1355
|
+
* reject/expired; returns null when approved or when no gate applies.
|
|
1318
1356
|
*/
|
|
1319
1357
|
async function maybeGateStepApproval(step, stepIndex, opts) {
|
|
1320
|
-
if (step.
|
|
1321
|
-
const normalized =
|
|
1358
|
+
if (step.approval === void 0 || !opts.awaitStepApproval) return null;
|
|
1359
|
+
const normalized = normalizeApproval(step.approval);
|
|
1360
|
+
if (normalized.when === "drift") return null;
|
|
1322
1361
|
opts.sendIpc({
|
|
1323
1362
|
type: "log.line",
|
|
1324
1363
|
stepIndex,
|
|
@@ -1407,6 +1446,39 @@ async function runObserverHook(args) {
|
|
|
1407
1446
|
* `ctx.secrets.mountFile` tmpdir, any env vars set via `exposeFile`) is
|
|
1408
1447
|
* removed even when the step throws, times out, or rule-skips.
|
|
1409
1448
|
*/
|
|
1449
|
+
/**
|
|
1450
|
+
* Run a step through its retry policy. Each call to `executeStepInLoop` is one
|
|
1451
|
+
* attempt: it sets up its own per-attempt timeout from `step.timeout` and returns
|
|
1452
|
+
* a `SandboxStepResult` (it never throws — a failed attempt is reported as a
|
|
1453
|
+
* `failed` status with an `error`). A failed attempt is retried while attempts
|
|
1454
|
+
* remain AND `retryIf(reconstructedError)` is true; backoff sleeps between
|
|
1455
|
+
* attempts. The retry loop runs to completion BEFORE the caller applies
|
|
1456
|
+
* `continueOnError` to the final outcome.
|
|
1457
|
+
*/
|
|
1458
|
+
async function runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts) {
|
|
1459
|
+
const retry = step.retry;
|
|
1460
|
+
const max = retry?.maxAttempts ?? 1;
|
|
1461
|
+
let result;
|
|
1462
|
+
for (let n = 1; n <= max; n++) {
|
|
1463
|
+
result = await executeStepInLoop(step, stepIndex, ctx, timeoutMs, opts.sendIpc, opts.outputsMap, opts.getSecretsAccessLog, opts.getSecretMountRecords, opts.jobDeadlineSignal, opts.checkMode ?? CheckMode.enum.apply, opts);
|
|
1464
|
+
if (result.status !== ExecutionStepStatus.enum.failed) return result;
|
|
1465
|
+
const err = new Error(result.error?.message ?? `Step '${step.name}' failed`);
|
|
1466
|
+
if (!(n < max && (retry?.retryIf?.(err) ?? true))) break;
|
|
1467
|
+
const delay = computeBackoffDelay(n, {
|
|
1468
|
+
maxAttempts: max,
|
|
1469
|
+
delayMs: retry.delayMs,
|
|
1470
|
+
backoff: retry.backoff,
|
|
1471
|
+
maxDelayMs: retry.maxDelayMs
|
|
1472
|
+
});
|
|
1473
|
+
opts.sendIpc({
|
|
1474
|
+
type: "log.line",
|
|
1475
|
+
stepIndex,
|
|
1476
|
+
line: `[kici] Step '${step.name}' attempt ${n}/${max} failed: ${err.message}; retrying in ${delay}ms`
|
|
1477
|
+
});
|
|
1478
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1479
|
+
}
|
|
1480
|
+
return result;
|
|
1481
|
+
}
|
|
1410
1482
|
async function runStepIteration(step, stepIndex, opts) {
|
|
1411
1483
|
const skippedResult = await evaluateStepRulesAndMaybeSkip(step, stepIndex, opts);
|
|
1412
1484
|
if (skippedResult) {
|
|
@@ -1434,7 +1506,7 @@ async function runStepIteration(step, stepIndex, opts) {
|
|
|
1434
1506
|
const timeoutMs = step.timeout ?? opts.defaultTimeoutMs;
|
|
1435
1507
|
let result;
|
|
1436
1508
|
try {
|
|
1437
|
-
result = await
|
|
1509
|
+
result = await runStepWithRetry(step, stepIndex, ctx, timeoutMs, opts);
|
|
1438
1510
|
} finally {
|
|
1439
1511
|
await opts.afterStepApplyEnvFiles?.();
|
|
1440
1512
|
}
|
|
@@ -3076,8 +3148,8 @@ function logSubprocessStreams(e, tokens) {
|
|
|
3076
3148
|
* no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
|
|
3077
3149
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
3078
3150
|
*/
|
|
3079
|
-
const AGENT_SDK_VERSION = "0.1.
|
|
3080
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
3151
|
+
const AGENT_SDK_VERSION = "0.1.23";
|
|
3152
|
+
const AGENT_SDK_BUNDLE_HASH = "3a83610d2d122b9f9b0f924b225a47050d0b35f00a8f3256a3e15f3ff3bf7ddc";
|
|
3081
3153
|
/**
|
|
3082
3154
|
* Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
|
|
3083
3155
|
* subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
|
|
@@ -3439,7 +3511,7 @@ async function applyOverlay(config) {
|
|
|
3439
3511
|
*/
|
|
3440
3512
|
init_download();
|
|
3441
3513
|
init_dep_restore();
|
|
3442
|
-
const AGENT_VERSION = "0.1.
|
|
3514
|
+
const AGENT_VERSION = "0.1.23";
|
|
3443
3515
|
process.on("uncaughtException", (err) => {
|
|
3444
3516
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
3445
3517
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -3832,35 +3904,49 @@ function waitForApprovalResolution(requestId) {
|
|
|
3832
3904
|
});
|
|
3833
3905
|
}
|
|
3834
3906
|
/**
|
|
3835
|
-
*
|
|
3836
|
-
* `
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3907
|
+
* Send a step-approval `approval.request` IPC (relayed by the agent over the WS
|
|
3908
|
+
* as a `step.approval-request`), await the matching `approval.resolved`, and map
|
|
3909
|
+
* it onto a `StepApprovalResolution`. A relay error is treated as a fail-closed
|
|
3910
|
+
* reject. Shared by the `when: 'always'` and `when: 'drift'` callbacks; the
|
|
3911
|
+
* latter carries a drift `payload`.
|
|
3839
3912
|
*/
|
|
3840
|
-
function
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
};
|
|
3913
|
+
async function requestStepApproval(req) {
|
|
3914
|
+
const requestId = randomUUID();
|
|
3915
|
+
sendMessage({
|
|
3916
|
+
type: "approval.request",
|
|
3917
|
+
requestId,
|
|
3918
|
+
stepIndex: req.stepIndex,
|
|
3919
|
+
stepName: req.stepName,
|
|
3920
|
+
clauses: req.clauses,
|
|
3921
|
+
reason: req.reason,
|
|
3922
|
+
...req.timeoutSeconds !== void 0 && { timeoutSeconds: req.timeoutSeconds },
|
|
3923
|
+
...req.payload !== void 0 && { payload: req.payload }
|
|
3924
|
+
});
|
|
3925
|
+
const resolution = await waitForApprovalResolution(requestId);
|
|
3926
|
+
if (resolution.error) return {
|
|
3927
|
+
outcome: "rejected",
|
|
3928
|
+
reason: resolution.error
|
|
3929
|
+
};
|
|
3930
|
+
return {
|
|
3931
|
+
outcome: resolution.outcome ?? "rejected",
|
|
3932
|
+
...resolution.reason !== void 0 && { reason: resolution.reason }
|
|
3861
3933
|
};
|
|
3862
3934
|
}
|
|
3863
3935
|
/**
|
|
3936
|
+
* Build the `awaitStepApproval` callback the step loop uses to block on an
|
|
3937
|
+
* `approval` step (`when: 'always'`).
|
|
3938
|
+
*/
|
|
3939
|
+
function buildAwaitStepApproval() {
|
|
3940
|
+
return (req) => requestStepApproval(req);
|
|
3941
|
+
}
|
|
3942
|
+
/**
|
|
3943
|
+
* Build the `awaitStepApprovalWithPayload` callback the step loop uses to block
|
|
3944
|
+
* on a `when: 'drift'` step mid-execution, carrying the computed drift payload.
|
|
3945
|
+
*/
|
|
3946
|
+
function buildAwaitStepApprovalWithPayload() {
|
|
3947
|
+
return (req) => requestStepApproval(req);
|
|
3948
|
+
}
|
|
3949
|
+
/**
|
|
3864
3950
|
* Build the {@link CacheTransport} the sandbox-side cache engine uses to reach
|
|
3865
3951
|
* the orchestrator. Each method sends a `cache.request` IPC (relayed by the
|
|
3866
3952
|
* agent over the WS as a `cache.user.*` message) and awaits the matching
|
|
@@ -4093,6 +4179,75 @@ function createIpcLogger(stepIndex, stepName, sendFn) {
|
|
|
4093
4179
|
}
|
|
4094
4180
|
};
|
|
4095
4181
|
}
|
|
4182
|
+
/** A lock needs entry maps to a base name (single/matrix/host) or a group name. */
|
|
4183
|
+
function needBaseName(need) {
|
|
4184
|
+
if (typeof need === "string") return {
|
|
4185
|
+
kind: "job",
|
|
4186
|
+
key: need
|
|
4187
|
+
};
|
|
4188
|
+
if (need && typeof need === "object") {
|
|
4189
|
+
if ("group" in need) return {
|
|
4190
|
+
kind: "group",
|
|
4191
|
+
key: need.group
|
|
4192
|
+
};
|
|
4193
|
+
if ("name" in need) return {
|
|
4194
|
+
kind: "job",
|
|
4195
|
+
key: need.name
|
|
4196
|
+
};
|
|
4197
|
+
}
|
|
4198
|
+
return null;
|
|
4199
|
+
}
|
|
4200
|
+
/**
|
|
4201
|
+
* Build `ctx.needs` for a job's steps from the dispatch envelope. Reconstructs
|
|
4202
|
+
* an {@link UpstreamSnapshot} from `upstreamJobOutputs` (flat per single job;
|
|
4203
|
+
* `byMatrix` / `byHost` envelopes per fan-out) + `upstreamJobStatuses` (keyed by
|
|
4204
|
+
* each upstream job/child name), then resolves the job's declared needs into the
|
|
4205
|
+
* `{ result, status }` / ordered-array shape via the shared SDK builder. Returns
|
|
4206
|
+
* undefined when the job declares no needs.
|
|
4207
|
+
*/
|
|
4208
|
+
function buildStepNeedsContext(declaredNeeds, upstreamJobOutputs, upstreamJobStatuses) {
|
|
4209
|
+
if (!declaredNeeds || declaredNeeds.length === 0) return void 0;
|
|
4210
|
+
const statuses = upstreamJobStatuses ?? {};
|
|
4211
|
+
const jobs = {};
|
|
4212
|
+
const groups = {};
|
|
4213
|
+
const snapStatuses = {};
|
|
4214
|
+
const resolvedNeeds = [];
|
|
4215
|
+
for (const need of declaredNeeds) {
|
|
4216
|
+
const base = needBaseName(need);
|
|
4217
|
+
if (!base) continue;
|
|
4218
|
+
const childNames = Object.keys(statuses).filter((n) => n.startsWith(`${base.key} (`));
|
|
4219
|
+
if (base.kind === "group" || childNames.length > 0) {
|
|
4220
|
+
groups[base.key] = [...childNames].sort();
|
|
4221
|
+
const envelope = upstreamJobOutputs?.[base.key];
|
|
4222
|
+
const bySuffix = envelopeChildOutputs(envelope);
|
|
4223
|
+
for (const child of childNames) {
|
|
4224
|
+
jobs[child] = bySuffix[child.slice(base.key.length + 2, -1)] ?? {};
|
|
4225
|
+
snapStatuses[child] = statuses[child];
|
|
4226
|
+
}
|
|
4227
|
+
resolvedNeeds.push({ group: base.key });
|
|
4228
|
+
} else {
|
|
4229
|
+
jobs[base.key] = upstreamJobOutputs?.[base.key] ?? {};
|
|
4230
|
+
if (statuses[base.key]) snapStatuses[base.key] = statuses[base.key];
|
|
4231
|
+
resolvedNeeds.push(base.key);
|
|
4232
|
+
}
|
|
4233
|
+
}
|
|
4234
|
+
return buildNeedsContext({
|
|
4235
|
+
jobs,
|
|
4236
|
+
groups,
|
|
4237
|
+
statuses: snapStatuses
|
|
4238
|
+
}, resolvedNeeds);
|
|
4239
|
+
}
|
|
4240
|
+
/**
|
|
4241
|
+
* Extract per-child output records from a fan-out outputs envelope, keyed by the
|
|
4242
|
+
* combination suffix (matrix `byMatrix`) or hostname (`runsOnAll` `byHost`).
|
|
4243
|
+
* Returns an empty map for a non-envelope value.
|
|
4244
|
+
*/
|
|
4245
|
+
function envelopeChildOutputs(envelope) {
|
|
4246
|
+
if (!envelope) return {};
|
|
4247
|
+
const byMatrix = envelope.byMatrix;
|
|
4248
|
+
const byHost = envelope.byHost;
|
|
4249
|
+
return byMatrix ?? byHost ?? {};
|
|
4250
|
+
}
|
|
4096
4251
|
/**
|
|
4097
4252
|
* Build StepSecrets from the job execution request, wired with the per-step
|
|
4098
4253
|
* file-mount host (used by `ctx.secrets.mountFile` / `exposeFile`).
|
|
@@ -4288,7 +4443,31 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
4288
4443
|
...request.provider && { provider: request.provider },
|
|
4289
4444
|
...request.matrixValues && { matrix: request.matrixValues },
|
|
4290
4445
|
...request.host && { host: request.host },
|
|
4291
|
-
...request.agent && { agent: request.agent }
|
|
4446
|
+
...request.agent && { agent: request.agent },
|
|
4447
|
+
...(() => {
|
|
4448
|
+
const fanout = deriveFanout(request);
|
|
4449
|
+
return fanout ? { fanout } : {};
|
|
4450
|
+
})(),
|
|
4451
|
+
dispatchInputs: request.dispatchInputs ?? {},
|
|
4452
|
+
...(() => {
|
|
4453
|
+
const needs = buildStepNeedsContext(request.jobNeeds, request.upstreamJobOutputs, request.upstreamJobStatuses);
|
|
4454
|
+
return needs ? { needs } : {};
|
|
4455
|
+
})()
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
/**
|
|
4459
|
+
* Derive the fan-out position (`ctx.fanout`) from a dispatch request. Returns
|
|
4460
|
+
* `undefined` for a non-fan-out job (no `fanoutTotal`), so `ctx.fanout` is only
|
|
4461
|
+
* set for `runsOnAll` host children and matrix combinations.
|
|
4462
|
+
*/
|
|
4463
|
+
function deriveFanout(request) {
|
|
4464
|
+
if (request.fanoutTotal === void 0) return void 0;
|
|
4465
|
+
const index = request.fanoutIndex ?? 0;
|
|
4466
|
+
return {
|
|
4467
|
+
index,
|
|
4468
|
+
total: request.fanoutTotal,
|
|
4469
|
+
first: index === 0,
|
|
4470
|
+
last: index === request.fanoutTotal - 1
|
|
4292
4471
|
};
|
|
4293
4472
|
}
|
|
4294
4473
|
/** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
|
|
@@ -4904,7 +5083,7 @@ function buildOutputInfrastructure(request, refMap) {
|
|
|
4904
5083
|
*/
|
|
4905
5084
|
async function maybeSkipJobOnRules(job, request, normalizedSteps) {
|
|
4906
5085
|
if (!job?.rules || job.rules.length === 0) return false;
|
|
4907
|
-
const ruleCtx = createRuleContext(request.event ?? {}, [], process.env);
|
|
5086
|
+
const ruleCtx = createRuleContext(request.event ?? {}, [], process.env, request.dispatchInputs ?? {}, deriveFanout(request));
|
|
4908
5087
|
if ((await evaluateRules(job.rules, ruleCtx, request.jobName)).allPassed) return false;
|
|
4909
5088
|
const skippedResults = normalizedSteps.map((s, i) => ({
|
|
4910
5089
|
name: s.name,
|
|
@@ -5159,6 +5338,8 @@ async function main() {
|
|
|
5159
5338
|
outputsMap,
|
|
5160
5339
|
event: request.event ?? {},
|
|
5161
5340
|
env: process.env,
|
|
5341
|
+
dispatchInputs: request.dispatchInputs ?? {},
|
|
5342
|
+
fanout: deriveFanout(request),
|
|
5162
5343
|
jobHooks,
|
|
5163
5344
|
cachePhaseDeps,
|
|
5164
5345
|
isAborted: () => aborted,
|
|
@@ -5180,7 +5361,8 @@ async function main() {
|
|
|
5180
5361
|
},
|
|
5181
5362
|
beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
|
|
5182
5363
|
afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
|
|
5183
|
-
awaitStepApproval: buildAwaitStepApproval()
|
|
5364
|
+
awaitStepApproval: buildAwaitStepApproval(),
|
|
5365
|
+
awaitStepApprovalWithPayload: buildAwaitStepApprovalWithPayload()
|
|
5184
5366
|
});
|
|
5185
5367
|
jobDeadline.clear();
|
|
5186
5368
|
await maybeSaveJobCache(jobCacheSpecs, jobCacheRestore, cachePhaseDeps, !aborted && loopResult.status === ExecutionStepStatus.enum.success);
|
|
@@ -5266,6 +5448,6 @@ main().catch((error) => {
|
|
|
5266
5448
|
setTimeout(() => process.exit(1), 100);
|
|
5267
5449
|
});
|
|
5268
5450
|
//#endregion
|
|
5269
|
-
export { createSandboxStepContext, rawPayloadFromEvent };
|
|
5451
|
+
export { buildStepNeedsContext, createSandboxStepContext, deriveFanout, rawPayloadFromEvent };
|
|
5270
5452
|
|
|
5271
5453
|
//# sourceMappingURL=workflow-runner.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kici-dev/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Customer-deployable agent for the KiCI CI/CD stack. Connects to an orchestrator, clones the workflow repo, executes steps, and streams logs back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ci",
|
|
@@ -64,10 +64,10 @@
|
|
|
64
64
|
"yaml": "^2.9.0",
|
|
65
65
|
"zod": "^4.4.3",
|
|
66
66
|
"zx": "^8.8.5",
|
|
67
|
-
"@kici-dev/core": "0.1.
|
|
68
|
-
"@kici-dev/engine": "0.1.
|
|
69
|
-
"@kici-dev/sdk": "0.1.
|
|
70
|
-
"@kici-dev/shared": "0.1.
|
|
67
|
+
"@kici-dev/core": "0.1.23",
|
|
68
|
+
"@kici-dev/engine": "0.1.23",
|
|
69
|
+
"@kici-dev/sdk": "0.1.23",
|
|
70
|
+
"@kici-dev/shared": "0.1.23"
|
|
71
71
|
},
|
|
72
72
|
"kici": {
|
|
73
73
|
"metrics": {
|