@kici-dev/agent 0.5.0 → 0.6.0
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/checkout/clone-job-repos.d.ts +53 -0
- package/dist/checkout/credential-helper-bin.d.ts +28 -0
- package/dist/checkout/credential-helper-host.d.ts +48 -0
- package/dist/checkout/credential-helper.d.ts +44 -0
- package/dist/checkout/git-clone.d.ts +26 -0
- package/dist/checkout/grant-table.d.ts +30 -0
- package/dist/checkout/job-git-credentials.d.ts +49 -0
- package/dist/checkout/write-elevation.d.ts +40 -0
- package/dist/config.d.ts +38 -0
- package/dist/container-ts-loader-hook.js +2047 -1814
- package/dist/execution/between-jobs-controller.d.ts +50 -0
- package/dist/execution/between-jobs-reset.d.ts +25 -0
- package/dist/execution/cleanup-rerun.d.ts +21 -0
- package/dist/execution/dynamic-job-serializer.d.ts +6 -2
- package/dist/execution/image-build/build-engine.d.ts +75 -0
- package/dist/execution/image-build/build-step.d.ts +57 -0
- package/dist/execution/image-build/resolve-build-spec.d.ts +41 -0
- package/dist/execution/image-build/runtime-facts.d.ts +31 -0
- package/dist/execution/job-runner.d.ts +48 -0
- package/dist/execution/sandbox/bare-metal-sandbox.d.ts +25 -1
- package/dist/execution/sandbox/container-sandbox.d.ts +97 -2
- package/dist/execution/sandbox/fork-runner.d.ts +55 -1
- package/dist/execution/sandbox/image-preflight.d.ts +38 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +96 -2
- package/dist/execution/sandbox/kici-runtime.d.ts +48 -0
- package/dist/execution/sandbox/step-loop.d.ts +7 -0
- package/dist/execution/sandbox/types.d.ts +55 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +23 -3
- package/dist/idle-shutdown.d.ts +24 -0
- package/dist/index.js +133 -62
- package/dist/metrics/prometheus.d.ts +26 -0
- package/dist/server.js +2050 -312
- package/dist/workflow-runner-bundle.js +41961 -41319
- package/dist/workflow-runner.js +332 -136
- package/dist/ws/orchestrator-client.d.ts +95 -3
- package/package.json +10 -10
- package/sbom.spdx.json +460 -455
package/dist/workflow-runner.js
CHANGED
|
@@ -8,7 +8,7 @@ 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 { createTempScope, makeTempDir } from "@kici-dev/core/tmp";
|
|
11
|
-
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, LogStream, StepConcurrencyKind, TimeoutReason, artifactInvalidNameError, checkArtifactName } from "@kici-dev/engine";
|
|
11
|
+
import { CacheOutcome, CacheStepType, CheckMode, CheckStepOutcome, ExecutionJobStatus, ExecutionStepStatus, LogStream, StepConcurrencyKind, TimeoutReason, artifactInvalidNameError, checkArtifactName, reservedEventNamePrefix } from "@kici-dev/engine";
|
|
12
12
|
import { execFile, execFileSync } from "node:child_process";
|
|
13
13
|
import { buildKiciApi, buildNeedsContext, createRuleContext, createStepSecrets, evaluateRules, isDynamicJobFn, isEventDefinition, isParallelGroup, normalizeApproval, normalizeCacheSpecs, provenanceSubjectIsPath, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
|
|
14
14
|
import { OIDC_TOKEN_REQUEST_METHOD } from "@kici-dev/engine/protocol/messages/oidc-token-relay";
|
|
@@ -123,9 +123,11 @@ async function setupSshAuth(opts) {
|
|
|
123
123
|
if (opts.hostKeyPolicy === "pinned" && !opts.knownHosts) throw new Error("pinned hostKeyPolicy requires knownHosts content");
|
|
124
124
|
const { path: tempDir, cleanup } = await makeTempDir("ssh");
|
|
125
125
|
const keyPath = join(tempDir, "id");
|
|
126
|
-
|
|
126
|
+
const pem = opts.privateKey.endsWith("\n") ? opts.privateKey : `${opts.privateKey}\n`;
|
|
127
|
+
await writeFile(keyPath, pem, { mode: 384 });
|
|
127
128
|
const knownHostsPath = join(tempDir, "known_hosts");
|
|
128
|
-
|
|
129
|
+
const knownHostsBody = opts.hostKeyPolicy === "pinned" ? opts.knownHosts : "";
|
|
130
|
+
await writeFile(knownHostsPath, knownHostsBody, { mode: 384 });
|
|
129
131
|
const parts = [
|
|
130
132
|
"ssh",
|
|
131
133
|
"-i",
|
|
@@ -462,13 +464,14 @@ async function signStatementDsse(payloadType, statementBytes) {
|
|
|
462
464
|
const kid = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
463
465
|
publicJwk.kid = kid;
|
|
464
466
|
const pae = dssePae(payloadType, statementBytes);
|
|
467
|
+
const sig = new Uint8Array(await crypto.subtle.sign({
|
|
468
|
+
name: "ECDSA",
|
|
469
|
+
hash: "SHA-256"
|
|
470
|
+
}, privateKey, pae));
|
|
465
471
|
return {
|
|
466
472
|
envelope: buildDsseEnvelope(payloadType, statementBytes, [{
|
|
467
473
|
keyid: kid,
|
|
468
|
-
sig
|
|
469
|
-
name: "ECDSA",
|
|
470
|
-
hash: "SHA-256"
|
|
471
|
-
}, privateKey, pae))
|
|
474
|
+
sig
|
|
472
475
|
}]),
|
|
473
476
|
publicJwk
|
|
474
477
|
};
|
|
@@ -495,7 +498,8 @@ async function attestProvenance(deps, input) {
|
|
|
495
498
|
startedOn: now,
|
|
496
499
|
finishedOn: now
|
|
497
500
|
});
|
|
498
|
-
const
|
|
501
|
+
const statementBytes = new TextEncoder().encode(JSON.stringify(statement));
|
|
502
|
+
const { envelope, publicJwk } = await signStatementDsse(IN_TOTO_PAYLOAD_TYPE, statementBytes);
|
|
499
503
|
const bundle = {
|
|
500
504
|
mediaType: KICI_PROVENANCE_BUNDLE_MEDIA_TYPE,
|
|
501
505
|
dsseEnvelope: envelope,
|
|
@@ -780,7 +784,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
780
784
|
await extractTarball(data, scratchDir);
|
|
781
785
|
await moveScratchIntoRepo(scratchDir, workDir);
|
|
782
786
|
await cleanupScratch(scratchDir);
|
|
783
|
-
const sizeMB = (data.length /
|
|
787
|
+
const sizeMB = (data.length / 1048576).toFixed(2);
|
|
784
788
|
logger$6.info("Dependencies restored from cache (file)", {
|
|
785
789
|
sizeMB,
|
|
786
790
|
targetDir: workDir
|
|
@@ -814,7 +818,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
814
818
|
var logger$6, DOWNLOAD_TIMEOUT_MS$2, SCRATCH_DIR_BASENAME_PREFIX, SCRATCH_DIR_GIT_EXCLUDE_GLOB;
|
|
815
819
|
var init_dep_restore = __esmMin((() => {
|
|
816
820
|
logger$6 = createLogger({ prefix: "dep-restore" });
|
|
817
|
-
DOWNLOAD_TIMEOUT_MS$2 =
|
|
821
|
+
DOWNLOAD_TIMEOUT_MS$2 = 3e5;
|
|
818
822
|
SCRATCH_DIR_BASENAME_PREFIX = ".dep-restore-scratch-";
|
|
819
823
|
SCRATCH_DIR_GIT_EXCLUDE_GLOB = `.kici/${SCRATCH_DIR_BASENAME_PREFIX}*`;
|
|
820
824
|
}));
|
|
@@ -952,8 +956,8 @@ var logger$5, DOWNLOAD_TIMEOUT_MS$1, UPLOAD_TIMEOUT_MS, UPLOAD_RETRY_BASE_DELAY_
|
|
|
952
956
|
var init_download = __esmMin((() => {
|
|
953
957
|
init_dep_restore();
|
|
954
958
|
logger$5 = createLogger({ prefix: "agent:download" });
|
|
955
|
-
DOWNLOAD_TIMEOUT_MS$1 =
|
|
956
|
-
UPLOAD_TIMEOUT_MS =
|
|
959
|
+
DOWNLOAD_TIMEOUT_MS$1 = 3e5;
|
|
960
|
+
UPLOAD_TIMEOUT_MS = 3e5;
|
|
957
961
|
UPLOAD_RETRY_BASE_DELAY_MS = 500;
|
|
958
962
|
PresignedUploadHttpError = class extends Error {
|
|
959
963
|
statusCode;
|
|
@@ -986,11 +990,13 @@ init_download();
|
|
|
986
990
|
* right destination (repo entries under `workDir`, home entries under the
|
|
987
991
|
* homedir). Extraction lands in a scratch dir first, then moves each group
|
|
988
992
|
* into place so a partial restore never leaves half-written paths in the live
|
|
989
|
-
* tree (mirrors dep-restore).
|
|
993
|
+
* tree (mirrors dep-restore). The scratch dir honors `KICI_TMPDIR`, which may
|
|
994
|
+
* sit on a different filesystem from the workspace, so the move falls back to
|
|
995
|
+
* copy-then-remove on `EXDEV` rather than assuming a same-filesystem rename.
|
|
990
996
|
*/
|
|
991
997
|
const logger$4 = createLogger({ prefix: "cache-engine" });
|
|
992
998
|
/** Download timeout for a presigned cache GET: 5 minutes. */
|
|
993
|
-
const DOWNLOAD_TIMEOUT_MS =
|
|
999
|
+
const DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
994
1000
|
/**
|
|
995
1001
|
* Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
|
|
996
1002
|
* Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
|
|
@@ -1065,6 +1071,32 @@ async function packCachePaths(workDir, paths, roots) {
|
|
|
1065
1071
|
await cleanup();
|
|
1066
1072
|
}
|
|
1067
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Move a tree from `src` to `dest`. Attempts a rename (same-filesystem, cheap)
|
|
1076
|
+
* and falls back to copy-then-remove only on `EXDEV` — the errno `rename(2)`
|
|
1077
|
+
* reports when the scratch dir (which honors `KICI_TMPDIR`) is on a different
|
|
1078
|
+
* filesystem from the destination workspace. `verbatimSymlinks` keeps a cached
|
|
1079
|
+
* symlink graph intact (mirroring packCachePaths), and `preserveTimestamps`
|
|
1080
|
+
* keeps mtimes stable so the fallback is byte-for-byte equivalent to the
|
|
1081
|
+
* rename. Any non-`EXDEV` error propagates so a real permission or corruption
|
|
1082
|
+
* failure is never silently turned into a copy.
|
|
1083
|
+
*/
|
|
1084
|
+
async function moveOrCopy(src, dest) {
|
|
1085
|
+
try {
|
|
1086
|
+
await rename(src, dest);
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
if (err.code !== "EXDEV") throw err;
|
|
1089
|
+
await cp(src, dest, {
|
|
1090
|
+
recursive: true,
|
|
1091
|
+
verbatimSymlinks: true,
|
|
1092
|
+
preserveTimestamps: true
|
|
1093
|
+
});
|
|
1094
|
+
await rm(src, {
|
|
1095
|
+
recursive: true,
|
|
1096
|
+
force: true
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1068
1100
|
/** Move the extracted `__repo__` / `__home__` groups from a scratch dir into place. */
|
|
1069
1101
|
async function moveAnchoredGroups(scratchDir, workDir, home) {
|
|
1070
1102
|
for (const anchor of await readdir(scratchDir)) {
|
|
@@ -1078,7 +1110,7 @@ async function moveAnchoredGroups(scratchDir, workDir, home) {
|
|
|
1078
1110
|
recursive: true,
|
|
1079
1111
|
force: true
|
|
1080
1112
|
});
|
|
1081
|
-
await
|
|
1113
|
+
await moveOrCopy(join(anchorDir, child), dest);
|
|
1082
1114
|
}
|
|
1083
1115
|
}
|
|
1084
1116
|
}
|
|
@@ -1557,7 +1589,7 @@ function buildMergedFlatSecrets(orchestratorSecrets, namespacedSecrets) {
|
|
|
1557
1589
|
//#endregion
|
|
1558
1590
|
//#region src/execution/hook-executor.ts
|
|
1559
1591
|
/** Default hook timeout: 5 minutes */
|
|
1560
|
-
const DEFAULT_HOOK_TIMEOUT_MS =
|
|
1592
|
+
const DEFAULT_HOOK_TIMEOUT_MS = 3e5;
|
|
1561
1593
|
/**
|
|
1562
1594
|
* Build outcome metadata from execution state.
|
|
1563
1595
|
*
|
|
@@ -2435,6 +2467,7 @@ async function runJobCompletionHooks(opts, initial, outputsMap, startTime) {
|
|
|
2435
2467
|
opts
|
|
2436
2468
|
});
|
|
2437
2469
|
}
|
|
2470
|
+
opts.sendIpc({ type: "completion-hooks-done" });
|
|
2438
2471
|
return state;
|
|
2439
2472
|
}
|
|
2440
2473
|
/**
|
|
@@ -2451,7 +2484,7 @@ async function runJobCompletionHooks(opts, initial, outputsMap, startTime) {
|
|
|
2451
2484
|
async function executeStepLoop(opts) {
|
|
2452
2485
|
const startTime = opts.startTime ?? Date.now();
|
|
2453
2486
|
const stepResults = [];
|
|
2454
|
-
const state = { failed:
|
|
2487
|
+
const state = { failed: opts.forceInitialFailure === true };
|
|
2455
2488
|
const nodes = opts.stepNodes ?? opts.steps.map((step, i) => ({
|
|
2456
2489
|
kind: "sequential",
|
|
2457
2490
|
step,
|
|
@@ -2555,7 +2588,7 @@ var StepTaskRegistry = class {
|
|
|
2555
2588
|
//#endregion
|
|
2556
2589
|
//#region src/execution/env-init/init-phase.ts
|
|
2557
2590
|
/** Default init timeout when a spec sets none: 10 minutes. */
|
|
2558
|
-
const DEFAULT_INIT_TIMEOUT_MS =
|
|
2591
|
+
const DEFAULT_INIT_TIMEOUT_MS = 6e5;
|
|
2559
2592
|
/**
|
|
2560
2593
|
* Marker thrown when an init command exceeds its wall-clock budget. Carries the
|
|
2561
2594
|
* distinct P3 timeout reason so the phase result + job.complete report a timeout
|
|
@@ -2921,6 +2954,37 @@ function armJobDeadline(timeoutMs, onTimeout) {
|
|
|
2921
2954
|
//#endregion
|
|
2922
2955
|
//#region src/checkout/git-clone.ts
|
|
2923
2956
|
/**
|
|
2957
|
+
* Point a clone at the agent's credential helper.
|
|
2958
|
+
*
|
|
2959
|
+
* Only the helper PATH is written — never a secret — which is what makes it
|
|
2960
|
+
* safe to persist in `.git/config`. `useHttpPath` makes git include
|
|
2961
|
+
* `path=owner/repo.git` in every credential query, without which the helper
|
|
2962
|
+
* could not tell one repository from another and a write grant could not be
|
|
2963
|
+
* confined to its own repo.
|
|
2964
|
+
*/
|
|
2965
|
+
function configureCredentialHelper(workDir, helperPath) {
|
|
2966
|
+
execFileSync("git", [
|
|
2967
|
+
"-C",
|
|
2968
|
+
workDir,
|
|
2969
|
+
"config",
|
|
2970
|
+
"credential.helper",
|
|
2971
|
+
helperPath
|
|
2972
|
+
], {
|
|
2973
|
+
stdio: "pipe",
|
|
2974
|
+
timeout: 1e4
|
|
2975
|
+
});
|
|
2976
|
+
execFileSync("git", [
|
|
2977
|
+
"-C",
|
|
2978
|
+
workDir,
|
|
2979
|
+
"config",
|
|
2980
|
+
"credential.useHttpPath",
|
|
2981
|
+
"true"
|
|
2982
|
+
], {
|
|
2983
|
+
stdio: "pipe",
|
|
2984
|
+
timeout: 1e4
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2924
2988
|
* Strip auth credentials from git error messages to prevent token leakage.
|
|
2925
2989
|
* Node's execFileSync includes the full command line (including -c http.extraHeader
|
|
2926
2990
|
* and GIT_SSH_COMMAND flags) in error messages, which would expose Base64-encoded
|
|
@@ -2947,7 +3011,7 @@ function redactSensitive(input) {
|
|
|
2947
3011
|
* @throws Error if clone fails or SHA does not match
|
|
2948
3012
|
*/
|
|
2949
3013
|
async function gitClone(options) {
|
|
2950
|
-
const { repoUrl, ref, sha, workDir, token, gitAuth, depth = 1 } = options;
|
|
3014
|
+
const { repoUrl, ref, sha, workDir, token, gitAuth, depth = 1, credentialHelperPath, sshCleanupRegistry } = options;
|
|
2951
3015
|
const auth = gitAuth ? gitAuth : token ? {
|
|
2952
3016
|
kind: "basic",
|
|
2953
3017
|
user: "x-access-token",
|
|
@@ -3001,6 +3065,7 @@ async function gitClone(options) {
|
|
|
3001
3065
|
} catch (err) {
|
|
3002
3066
|
throw sanitizeGitError(err);
|
|
3003
3067
|
}
|
|
3068
|
+
if (credentialHelperPath) configureCredentialHelper(workDir, credentialHelperPath);
|
|
3004
3069
|
if (!sha || sha === "HEAD") return;
|
|
3005
3070
|
const envOpts = env ? { env } : {};
|
|
3006
3071
|
if (!execFileSync("git", [
|
|
@@ -3056,11 +3121,90 @@ async function gitClone(options) {
|
|
|
3056
3121
|
if (!recheckedSha.startsWith(sha)) throw new Error(`SHA mismatch: expected ${sha}, got ${recheckedSha}`);
|
|
3057
3122
|
}
|
|
3058
3123
|
} finally {
|
|
3059
|
-
if (sshSetup)
|
|
3124
|
+
if (sshSetup) {
|
|
3125
|
+
if (sshCleanupRegistry) {
|
|
3126
|
+
const setup = sshSetup;
|
|
3127
|
+
sshCleanupRegistry.defer(() => setup.cleanup().catch(() => {}));
|
|
3128
|
+
} else await sshSetup.cleanup().catch(() => {});
|
|
3129
|
+
}
|
|
3060
3130
|
if (safeDirCleanup) await safeDirCleanup();
|
|
3061
3131
|
}
|
|
3062
3132
|
}
|
|
3063
3133
|
//#endregion
|
|
3134
|
+
//#region src/checkout/clone-job-repos.ts
|
|
3135
|
+
/**
|
|
3136
|
+
* Clone a job's repositories — from the host or from inside the sandbox.
|
|
3137
|
+
*
|
|
3138
|
+
* This used to live only inside the workflow runner, which meant the clone
|
|
3139
|
+
* always happened wherever the runner ran: for a container job, inside the
|
|
3140
|
+
* customer's image, which therefore had to ship git. Extracting it lets the
|
|
3141
|
+
* AGENT clone on the host and copy the tree in, so the image needs no git —
|
|
3142
|
+
* and it puts clone-time credentials on the host, where the credential helper
|
|
3143
|
+
* already works, rather than needing a route into a container hardened with
|
|
3144
|
+
* `CapDrop: ALL`.
|
|
3145
|
+
*
|
|
3146
|
+
* Both callers run the SAME code: the runner keeps calling it for bare-metal
|
|
3147
|
+
* and for the legacy container path, and the agent calls it for a host-side
|
|
3148
|
+
* checkout. A second implementation would be two subtly different clones.
|
|
3149
|
+
*/
|
|
3150
|
+
/**
|
|
3151
|
+
* Clone whatever this job needs, or nothing.
|
|
3152
|
+
*
|
|
3153
|
+
* Three modes, unchanged from where this logic used to live: full-repo overlay
|
|
3154
|
+
* (no clone), global dual-clone (workflow repo + source repo), and the ordinary
|
|
3155
|
+
* single-repo clone.
|
|
3156
|
+
*/
|
|
3157
|
+
async function cloneJobRepos(request, dirs, deps) {
|
|
3158
|
+
if (request.checkout === false) return;
|
|
3159
|
+
const helper = request.credentialHelperPath ? { credentialHelperPath: request.credentialHelperPath } : {};
|
|
3160
|
+
if (request.fullRepo) {
|
|
3161
|
+
await mkdir(dirs.workDir, { recursive: true });
|
|
3162
|
+
deps.log("Full-repo mode: skipping git clone (workspace from overlay tarball)");
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
if (deps.isGlobal) {
|
|
3166
|
+
await mkdir(dirs.workflowDir, { recursive: true });
|
|
3167
|
+
await mkdir(dirs.sourceDir, { recursive: true });
|
|
3168
|
+
const workflowAuth = request.workflowAuth ?? request.sourceAuth;
|
|
3169
|
+
const sourceAuth = request.sourceAuth ?? request.workflowAuth;
|
|
3170
|
+
deps.log(`Global workflow: cloning workflow repo ${request.workflowRepoUrl} ref=${request.workflowRef} into ${dirs.workflowDir}`);
|
|
3171
|
+
await gitClone({
|
|
3172
|
+
repoUrl: request.workflowRepoUrl,
|
|
3173
|
+
ref: request.workflowRef ?? "",
|
|
3174
|
+
sha: request.workflowSha ?? "",
|
|
3175
|
+
workDir: dirs.workflowDir,
|
|
3176
|
+
gitAuth: workflowAuth,
|
|
3177
|
+
token: workflowAuth ? void 0 : request.token,
|
|
3178
|
+
...helper
|
|
3179
|
+
});
|
|
3180
|
+
await deps.excludeScratchFromGit(dirs.workflowDir);
|
|
3181
|
+
deps.log(`Global workflow: cloning source repo ${request.repoUrl} ref=${request.ref} into ${dirs.sourceDir}`);
|
|
3182
|
+
await gitClone({
|
|
3183
|
+
repoUrl: request.repoUrl,
|
|
3184
|
+
ref: request.ref,
|
|
3185
|
+
sha: request.sha,
|
|
3186
|
+
workDir: dirs.sourceDir,
|
|
3187
|
+
gitAuth: sourceAuth,
|
|
3188
|
+
token: sourceAuth ? void 0 : request.token,
|
|
3189
|
+
...helper
|
|
3190
|
+
});
|
|
3191
|
+
deps.log("Dual-clone complete");
|
|
3192
|
+
return;
|
|
3193
|
+
}
|
|
3194
|
+
deps.log(`Cloning ${request.repoUrl} ref=${request.ref} into ${dirs.workDir}`);
|
|
3195
|
+
await gitClone({
|
|
3196
|
+
repoUrl: request.repoUrl,
|
|
3197
|
+
ref: request.ref,
|
|
3198
|
+
sha: request.sha,
|
|
3199
|
+
workDir: dirs.workDir,
|
|
3200
|
+
gitAuth: request.sourceAuth,
|
|
3201
|
+
token: request.sourceAuth ? void 0 : request.token,
|
|
3202
|
+
...helper
|
|
3203
|
+
});
|
|
3204
|
+
await deps.excludeScratchFromGit(dirs.workDir);
|
|
3205
|
+
deps.log("Clone complete");
|
|
3206
|
+
}
|
|
3207
|
+
//#endregion
|
|
3064
3208
|
//#region src/execution/npm-resolver.ts
|
|
3065
3209
|
/**
|
|
3066
3210
|
* Resolve the npm CLI path relative to the running Node.js binary.
|
|
@@ -3589,7 +3733,7 @@ const logger$2 = createLogger({ prefix: "dep-installer" });
|
|
|
3589
3733
|
const execFileAsync = promisify(execFile);
|
|
3590
3734
|
/** Install subprocess timeout (10 min) and stdout/stderr buffer (128 MiB). */
|
|
3591
3735
|
const INSTALL_TIMEOUT_MS = 6e5;
|
|
3592
|
-
const INSTALL_MAX_BUFFER =
|
|
3736
|
+
const INSTALL_MAX_BUFFER = 134217728;
|
|
3593
3737
|
/**
|
|
3594
3738
|
* Detect the package manager for the cloned repo from its committed manifests.
|
|
3595
3739
|
* A pnpm workspace's `packageManager` field + `pnpm-lock.yaml` live at the repo
|
|
@@ -4002,7 +4146,8 @@ function buildGeneratorContext(input) {
|
|
|
4002
4146
|
*/
|
|
4003
4147
|
async function resolveWorkflowSdkSetters(workflowFilePath) {
|
|
4004
4148
|
try {
|
|
4005
|
-
const
|
|
4149
|
+
const sdkEntry = createRequire(workflowFilePath).resolve("@kici-dev/sdk");
|
|
4150
|
+
const sdk = await import(pathToFileURL(sdkEntry).href);
|
|
4006
4151
|
if (typeof sdk.setStepOutputsMap === "function" && typeof sdk.setStepRefMap === "function" && typeof sdk.setJobOutputsMap === "function") return {
|
|
4007
4152
|
setStepOutputsMap: sdk.setStepOutputsMap,
|
|
4008
4153
|
setStepRefMap: sdk.setStepRefMap,
|
|
@@ -4015,8 +4160,8 @@ async function resolveWorkflowSdkSetters(workflowFilePath) {
|
|
|
4015
4160
|
setJobOutputsMap
|
|
4016
4161
|
};
|
|
4017
4162
|
}
|
|
4018
|
-
const AGENT_SDK_VERSION = "0.
|
|
4019
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
4163
|
+
const AGENT_SDK_VERSION = "0.6.0";
|
|
4164
|
+
const AGENT_SDK_BUNDLE_HASH = "22faf0da45de7c243ce87f80fd33ee51b1df52809fde457b16bb821678f65eb3";
|
|
4020
4165
|
/**
|
|
4021
4166
|
* Register the ESM loader hook that transforms `.ts` / `.tsx` files on the fly
|
|
4022
4167
|
* for subsequent dynamic `import()` calls. Idempotent at our level via the
|
|
@@ -4362,7 +4507,8 @@ async function applyOverlay(config) {
|
|
|
4362
4507
|
throw new Error(`Overlay download failed from ${tarballUrl.replace(/\?.*$/, "?[redacted]")}: ${toErrorMessage(err)}`);
|
|
4363
4508
|
}
|
|
4364
4509
|
const cliPubKeyBuf = Buffer.from(cliPublicKey, "base64");
|
|
4365
|
-
const
|
|
4510
|
+
const orchPrivKeyBuf = Buffer.from(orchestratorPrivateKey, "base64");
|
|
4511
|
+
const aesKey = deriveSharedSecret(orchPrivKeyBuf, cliPubKeyBuf);
|
|
4366
4512
|
const decryptedData = decryptBuffer(encryptedData, aesKey);
|
|
4367
4513
|
logger.info("Extracting overlay tarball", { size: decryptedData.length });
|
|
4368
4514
|
const extractDir = path.join(tmpDir, "extracted");
|
|
@@ -4448,7 +4594,7 @@ async function applyOverlay(config) {
|
|
|
4448
4594
|
*/
|
|
4449
4595
|
init_download();
|
|
4450
4596
|
init_dep_restore();
|
|
4451
|
-
const AGENT_VERSION = "0.
|
|
4597
|
+
const AGENT_VERSION = "0.6.0";
|
|
4452
4598
|
process.on("uncaughtException", (err) => {
|
|
4453
4599
|
process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
|
|
4454
4600
|
if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
|
|
@@ -4665,6 +4811,56 @@ const EMIT_RESPONSE_TIMEOUT_MS = 5e3;
|
|
|
4665
4811
|
* (per research doc pitfall 5: event was already persisted by orchestrator,
|
|
4666
4812
|
* so it will still be routed even if the ack is lost).
|
|
4667
4813
|
*/
|
|
4814
|
+
/** In-flight git write-grant requests, keyed by requestId. */
|
|
4815
|
+
const pendingGitGrants = /* @__PURE__ */ new Map();
|
|
4816
|
+
/** How long to wait for the agent to answer a grant request. */
|
|
4817
|
+
const GIT_GRANT_TIMEOUT_MS = 6e4;
|
|
4818
|
+
function waitForGitGrant(requestId) {
|
|
4819
|
+
return new Promise((resolve) => {
|
|
4820
|
+
const timer = setTimeout(() => {
|
|
4821
|
+
pendingGitGrants.delete(requestId);
|
|
4822
|
+
resolve({
|
|
4823
|
+
type: "git.grant.response",
|
|
4824
|
+
requestId,
|
|
4825
|
+
error: "timed out waiting for the agent to answer a git write-grant request"
|
|
4826
|
+
});
|
|
4827
|
+
}, GIT_GRANT_TIMEOUT_MS);
|
|
4828
|
+
pendingGitGrants.set(requestId, (r) => {
|
|
4829
|
+
clearTimeout(timer);
|
|
4830
|
+
pendingGitGrants.delete(requestId);
|
|
4831
|
+
resolve(r);
|
|
4832
|
+
});
|
|
4833
|
+
});
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Open a write window for one repository, run `fn`, and close it.
|
|
4837
|
+
*
|
|
4838
|
+
* The revoke rides a `finally`, so a throwing callback still closes the
|
|
4839
|
+
* window — and the agent's TTL is the backstop if this process dies outright.
|
|
4840
|
+
*/
|
|
4841
|
+
async function withRepoWrite(repository, opts, fn, send, wait) {
|
|
4842
|
+
const requestId = randomUUID();
|
|
4843
|
+
send({
|
|
4844
|
+
type: "git.grant.request",
|
|
4845
|
+
requestId,
|
|
4846
|
+
op: "elevate",
|
|
4847
|
+
repository,
|
|
4848
|
+
permissions: opts.permissions ?? { contents: "write" },
|
|
4849
|
+
...opts.credential ? { credentialName: opts.credential } : {}
|
|
4850
|
+
});
|
|
4851
|
+
const granted = await wait(requestId);
|
|
4852
|
+
if (granted.error) throw new Error(`Cannot open a git write window for '${repository}': ${granted.error}`);
|
|
4853
|
+
try {
|
|
4854
|
+
await fn();
|
|
4855
|
+
} finally {
|
|
4856
|
+
send({
|
|
4857
|
+
type: "git.grant.request",
|
|
4858
|
+
requestId: randomUUID(),
|
|
4859
|
+
op: "revoke",
|
|
4860
|
+
grantId: granted.grantId
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
}
|
|
4668
4864
|
function waitForEmitResponse(requestId) {
|
|
4669
4865
|
return new Promise((resolve) => {
|
|
4670
4866
|
const timer = setTimeout(() => {
|
|
@@ -4849,7 +5045,7 @@ const pendingApprovalResolutions = /* @__PURE__ */ new Map();
|
|
|
4849
5045
|
* and sends `expired` when it lapses; this is a safety net well above any sane
|
|
4850
5046
|
* approval window so the runner cannot hang forever if the resolution is lost.
|
|
4851
5047
|
*/
|
|
4852
|
-
const APPROVAL_RESOLUTION_TIMEOUT_MS =
|
|
5048
|
+
const APPROVAL_RESOLUTION_TIMEOUT_MS = 6048e5;
|
|
4853
5049
|
/** Wait for an approval.resolved from the agent with the given requestId. */
|
|
4854
5050
|
function waitForApprovalResolution(requestId) {
|
|
4855
5051
|
return new Promise((resolve) => {
|
|
@@ -5175,12 +5371,15 @@ function dispatchAgentMessage(msg) {
|
|
|
5175
5371
|
} else if (msg.type === "event.emit.response") {
|
|
5176
5372
|
const pending = pendingEmitResponses.get(msg.requestId);
|
|
5177
5373
|
if (pending) pending.resolve(msg);
|
|
5178
|
-
} else if (msg.type === "
|
|
5374
|
+
} else if (msg.type === "git.grant.response") pendingGitGrants.get(msg.requestId)?.(msg);
|
|
5375
|
+
else if (msg.type === "concurrency.ack") {
|
|
5179
5376
|
if (pendingConcurrencyAck) pendingConcurrencyAck.resolve(msg);
|
|
5180
5377
|
} else if (msg.type === "agent.api.response") {
|
|
5181
5378
|
const pending = pendingApiResponses.get(msg.requestId);
|
|
5182
|
-
if (pending)
|
|
5183
|
-
|
|
5379
|
+
if (pending) {
|
|
5380
|
+
if (msg.error) pending.reject(new Error(msg.error));
|
|
5381
|
+
else pending.resolve(msg.result);
|
|
5382
|
+
}
|
|
5184
5383
|
} else if (msg.type === "cache.response") {
|
|
5185
5384
|
const pending = pendingCacheResponses.get(msg.requestId);
|
|
5186
5385
|
if (pending) pending.resolve(msg);
|
|
@@ -5276,16 +5475,22 @@ function needBaseName(need) {
|
|
|
5276
5475
|
* `{ result, status }` / ordered-array shape via the shared SDK builder. Returns
|
|
5277
5476
|
* undefined when the job declares no needs.
|
|
5278
5477
|
*/
|
|
5279
|
-
function buildStepNeedsContext(declaredNeeds, upstreamJobOutputs, upstreamJobStatuses) {
|
|
5478
|
+
function buildStepNeedsContext(declaredNeeds, upstreamJobOutputs, upstreamJobStatuses, upstreamInvokeResults) {
|
|
5280
5479
|
if (!declaredNeeds || declaredNeeds.length === 0) return void 0;
|
|
5281
5480
|
const statuses = upstreamJobStatuses ?? {};
|
|
5282
5481
|
const jobs = {};
|
|
5283
5482
|
const groups = {};
|
|
5284
5483
|
const snapStatuses = {};
|
|
5484
|
+
const invokeResults = {};
|
|
5285
5485
|
const resolvedNeeds = [];
|
|
5286
5486
|
for (const need of declaredNeeds) {
|
|
5287
5487
|
const base = needBaseName(need);
|
|
5288
5488
|
if (!base) continue;
|
|
5489
|
+
if (base.kind === "job" && upstreamInvokeResults?.[base.key]) {
|
|
5490
|
+
invokeResults[base.key] = upstreamInvokeResults[base.key];
|
|
5491
|
+
resolvedNeeds.push(base.key);
|
|
5492
|
+
continue;
|
|
5493
|
+
}
|
|
5289
5494
|
const childNames = Object.keys(statuses).filter((n) => n.startsWith(`${base.key} (`));
|
|
5290
5495
|
if (base.kind === "group" || childNames.length > 0) {
|
|
5291
5496
|
groups[base.key] = [...childNames].sort();
|
|
@@ -5302,11 +5507,13 @@ function buildStepNeedsContext(declaredNeeds, upstreamJobOutputs, upstreamJobSta
|
|
|
5302
5507
|
resolvedNeeds.push(base.key);
|
|
5303
5508
|
}
|
|
5304
5509
|
}
|
|
5305
|
-
|
|
5510
|
+
const snapshot = {
|
|
5306
5511
|
jobs,
|
|
5307
5512
|
groups,
|
|
5308
5513
|
statuses: snapStatuses
|
|
5309
|
-
}
|
|
5514
|
+
};
|
|
5515
|
+
if (Object.keys(invokeResults).length > 0) snapshot.invokeResults = invokeResults;
|
|
5516
|
+
return buildNeedsContext(snapshot, resolvedNeeds);
|
|
5310
5517
|
}
|
|
5311
5518
|
/**
|
|
5312
5519
|
* Extract per-child output records from a fan-out outputs envelope, keyed by the
|
|
@@ -5347,7 +5554,8 @@ function buildStepSecrets(request, masker, onMaskerSecretsAdded) {
|
|
|
5347
5554
|
async writeMountedFile(args) {
|
|
5348
5555
|
const dir = await ensureTmpdir();
|
|
5349
5556
|
mountCounter += 1;
|
|
5350
|
-
const
|
|
5557
|
+
const filename = args.name ?? `secret-${mountCounter}`;
|
|
5558
|
+
const filePath = join(dir, filename);
|
|
5351
5559
|
await fsPromises.writeFile(filePath, args.content);
|
|
5352
5560
|
await fsPromises.chmod(filePath, args.mode);
|
|
5353
5561
|
masker.registerSecrets({ [`__mount_${mountCounter}__`]: args.content });
|
|
@@ -5443,6 +5651,19 @@ function resolveEmitEventName(nameOrDefinition) {
|
|
|
5443
5651
|
return isEventDefinition(nameOrDefinition) ? nameOrDefinition.name : nameOrDefinition;
|
|
5444
5652
|
}
|
|
5445
5653
|
/**
|
|
5654
|
+
* Reject a user `ctx.emit` whose event name uses a reserved prefix: `kici.`
|
|
5655
|
+
* (KiCI-internal system events — the event scaler's scale-up / scale-down) or
|
|
5656
|
+
* `__` (the events the orchestrator mints for itself). A workflow step must
|
|
5657
|
+
* forge neither — a `__` name is dispatched as a TRUSTED ref and skips the
|
|
5658
|
+
* event-storm rate limiter, so emitting one would be a privilege escalation,
|
|
5659
|
+
* not merely a naming collision. Throws a clear error at the emit call; the
|
|
5660
|
+
* orchestrator enforces the same reservation authoritatively.
|
|
5661
|
+
*/
|
|
5662
|
+
function assertUserEmittableEventName(eventName) {
|
|
5663
|
+
const reservedPrefix = reservedEventNamePrefix(eventName);
|
|
5664
|
+
if (reservedPrefix) throw new Error(`event name prefix "${reservedPrefix}" is reserved for KiCI internal events and cannot be emitted from a workflow step (got "${eventName}")`);
|
|
5665
|
+
}
|
|
5666
|
+
/**
|
|
5446
5667
|
* Sanitize a raw identifier into a valid temp label: lowercase, every
|
|
5447
5668
|
* non-`[a-z0-9-]` char to `-`, falling back to `'step'` when the result is
|
|
5448
5669
|
* empty. Applied to both the caller-supplied `ctx.mktemp(label)` and the
|
|
@@ -5545,6 +5766,7 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
5545
5766
|
artifacts: createArtifactsApi(workDir, buildArtifactTransport()),
|
|
5546
5767
|
emit: async (nameOrDefinition, payload, options) => {
|
|
5547
5768
|
const eventName = resolveEmitEventName(nameOrDefinition);
|
|
5769
|
+
assertUserEmittableEventName(eventName);
|
|
5548
5770
|
const reqId = randomUUID();
|
|
5549
5771
|
sendMessage({
|
|
5550
5772
|
type: "event.emit",
|
|
@@ -5569,6 +5791,13 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
5569
5791
|
mktemp: (label) => jobTempScope.mktemp(sanitizeTempLabel(label ?? stepName)),
|
|
5570
5792
|
mktempFile: (label, opts) => jobTempScope.mktempFile(sanitizeTempLabel(label ?? stepName), opts),
|
|
5571
5793
|
kici,
|
|
5794
|
+
repo: {
|
|
5795
|
+
identifier: repoIdentifierFromUrl(request.repoUrl),
|
|
5796
|
+
path: workDir,
|
|
5797
|
+
ref: request.ref,
|
|
5798
|
+
sha: request.sha,
|
|
5799
|
+
withWrite: (opts, fn) => withRepoWrite(repoIdentifierFromUrl(request.repoUrl), opts, fn, sendMessage, waitForGitGrant)
|
|
5800
|
+
},
|
|
5572
5801
|
attestProvenance: buildAttestProvenanceFn(request, workDir, (o) => kici.oidc.token(o)),
|
|
5573
5802
|
...rawPayload && { rawPayload },
|
|
5574
5803
|
...request.provider && { provider: request.provider },
|
|
@@ -5581,7 +5810,7 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
|
|
|
5581
5810
|
})(),
|
|
5582
5811
|
dispatchInputs: request.dispatchInputs ?? {},
|
|
5583
5812
|
...(() => {
|
|
5584
|
-
const needs = buildStepNeedsContext(request.jobNeeds, request.upstreamJobOutputs, request.upstreamJobStatuses);
|
|
5813
|
+
const needs = buildStepNeedsContext(request.jobNeeds, request.upstreamJobOutputs, request.upstreamJobStatuses, request.upstreamInvokeResults);
|
|
5585
5814
|
return needs ? { needs } : {};
|
|
5586
5815
|
})()
|
|
5587
5816
|
};
|
|
@@ -5639,80 +5868,22 @@ function abortAndExit(reason) {
|
|
|
5639
5868
|
* progress IPC log lines as before.
|
|
5640
5869
|
*/
|
|
5641
5870
|
async function cloneRepoIfRequested(request, workDir, workflowDir, sourceDir, isGlobal) {
|
|
5642
|
-
|
|
5643
|
-
if (request.fullRepo) {
|
|
5644
|
-
trace("fullRepo mode -- skipping git clone, workspace from overlay");
|
|
5645
|
-
await fsPromises.mkdir(workDir, { recursive: true });
|
|
5646
|
-
sendMessage({
|
|
5647
|
-
type: "log.line",
|
|
5648
|
-
stepIndex: -1,
|
|
5649
|
-
line: "[workflow-runner] Full-repo mode: skipping git clone (workspace from overlay tarball)"
|
|
5650
|
-
});
|
|
5651
|
-
return;
|
|
5652
|
-
}
|
|
5653
|
-
if (isGlobal) {
|
|
5654
|
-
trace(`starting dual-clone (global workflow)`);
|
|
5655
|
-
await fsPromises.mkdir(workflowDir, { recursive: true });
|
|
5656
|
-
await fsPromises.mkdir(sourceDir, { recursive: true });
|
|
5657
|
-
const workflowAuth = request.workflowAuth ?? request.sourceAuth;
|
|
5658
|
-
const sourceAuth = request.sourceAuth ?? request.workflowAuth;
|
|
5659
|
-
sendMessage({
|
|
5660
|
-
type: "log.line",
|
|
5661
|
-
stepIndex: -1,
|
|
5662
|
-
line: `[workflow-runner] Global workflow: cloning workflow repo ${request.workflowRepoUrl} ref=${request.workflowRef} into ${workflowDir}`
|
|
5663
|
-
});
|
|
5664
|
-
await gitClone({
|
|
5665
|
-
repoUrl: request.workflowRepoUrl,
|
|
5666
|
-
ref: request.workflowRef ?? "",
|
|
5667
|
-
sha: request.workflowSha ?? "",
|
|
5668
|
-
workDir: workflowDir,
|
|
5669
|
-
gitAuth: workflowAuth,
|
|
5670
|
-
token: workflowAuth ? void 0 : request.token
|
|
5671
|
-
});
|
|
5672
|
-
trace("workflow repo clone complete");
|
|
5673
|
-
await excludeScratchFromGit(workflowDir);
|
|
5674
|
-
sendMessage({
|
|
5675
|
-
type: "log.line",
|
|
5676
|
-
stepIndex: -1,
|
|
5677
|
-
line: `[workflow-runner] Global workflow: cloning source repo ${request.repoUrl} ref=${request.ref} into ${sourceDir}`
|
|
5678
|
-
});
|
|
5679
|
-
await gitClone({
|
|
5680
|
-
repoUrl: request.repoUrl,
|
|
5681
|
-
ref: request.ref,
|
|
5682
|
-
sha: request.sha,
|
|
5683
|
-
workDir: sourceDir,
|
|
5684
|
-
gitAuth: sourceAuth,
|
|
5685
|
-
token: sourceAuth ? void 0 : request.token
|
|
5686
|
-
});
|
|
5687
|
-
trace("source repo clone complete");
|
|
5688
|
-
sendMessage({
|
|
5689
|
-
type: "log.line",
|
|
5690
|
-
stepIndex: -1,
|
|
5691
|
-
line: "[workflow-runner] Dual-clone complete"
|
|
5692
|
-
});
|
|
5693
|
-
return;
|
|
5694
|
-
}
|
|
5695
|
-
trace("starting git clone");
|
|
5696
|
-
sendMessage({
|
|
5697
|
-
type: "log.line",
|
|
5698
|
-
stepIndex: -1,
|
|
5699
|
-
line: `[workflow-runner] Cloning ${request.repoUrl} ref=${request.ref} into ${workDir}`
|
|
5700
|
-
});
|
|
5701
|
-
await gitClone({
|
|
5702
|
-
repoUrl: request.repoUrl,
|
|
5703
|
-
ref: request.ref,
|
|
5704
|
-
sha: request.sha,
|
|
5871
|
+
await cloneJobRepos(request, {
|
|
5705
5872
|
workDir,
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
}
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5873
|
+
workflowDir,
|
|
5874
|
+
sourceDir
|
|
5875
|
+
}, {
|
|
5876
|
+
isGlobal,
|
|
5877
|
+
log: (line) => {
|
|
5878
|
+
trace(line);
|
|
5879
|
+
sendMessage({
|
|
5880
|
+
type: "log.line",
|
|
5881
|
+
stepIndex: -1,
|
|
5882
|
+
line: `[workflow-runner] ${line}`
|
|
5883
|
+
});
|
|
5884
|
+
},
|
|
5885
|
+
excludeScratchFromGit
|
|
5714
5886
|
});
|
|
5715
|
-
trace("git clone complete");
|
|
5716
5887
|
}
|
|
5717
5888
|
/**
|
|
5718
5889
|
* Phase 1b — Apply the encrypted overlay tarball when present (test runs
|
|
@@ -6155,6 +6326,7 @@ async function runCancelPathHooks(args) {
|
|
|
6155
6326
|
stepIndex: -1,
|
|
6156
6327
|
line: `[kici] Cancel complete, job status: ${cancelFailureReason ? "failed" : "cancelled"}`
|
|
6157
6328
|
});
|
|
6329
|
+
maskedSend({ type: "completion-hooks-done" });
|
|
6158
6330
|
return {
|
|
6159
6331
|
finalStatus: ExecutionJobStatus.enum.failed,
|
|
6160
6332
|
cancelFailureReason
|
|
@@ -6565,6 +6737,30 @@ async function runInitPhaseOrFailJob(args) {
|
|
|
6565
6737
|
* 7. Execute steps sequentially with IPC reporting
|
|
6566
6738
|
* 8. Send job.complete and exit
|
|
6567
6739
|
*/
|
|
6740
|
+
/**
|
|
6741
|
+
* Extract + normalize the job's steps, or return an empty set for a cleanup-only
|
|
6742
|
+
* re-run (which runs no steps and skips the generator re-evaluation this would
|
|
6743
|
+
* otherwise trigger). Owns the `apiTransport` closure the generator uses.
|
|
6744
|
+
*/
|
|
6745
|
+
async function extractStepsForRun(workflow, request, globalRepoInfo) {
|
|
6746
|
+
if (request.cleanupOnly) return {
|
|
6747
|
+
normalizedSteps: [],
|
|
6748
|
+
nodes: [],
|
|
6749
|
+
refMap: /* @__PURE__ */ new WeakMap(),
|
|
6750
|
+
driftDroppedJobs: []
|
|
6751
|
+
};
|
|
6752
|
+
const apiTransport = async (method, params) => {
|
|
6753
|
+
const reqId = randomUUID();
|
|
6754
|
+
sendMessage({
|
|
6755
|
+
type: "agent.api.request",
|
|
6756
|
+
requestId: reqId,
|
|
6757
|
+
method,
|
|
6758
|
+
params: params ?? {}
|
|
6759
|
+
});
|
|
6760
|
+
return waitForApiResponse(reqId);
|
|
6761
|
+
};
|
|
6762
|
+
return extractAndNormalizeSteps(workflow, request, apiTransport, globalRepoInfo);
|
|
6763
|
+
}
|
|
6568
6764
|
async function main() {
|
|
6569
6765
|
trace(`main() started, isForkMode=${isForkMode}, pid=${process.pid}`);
|
|
6570
6766
|
sendMessage({ type: "ready" });
|
|
@@ -6572,7 +6768,7 @@ async function main() {
|
|
|
6572
6768
|
const request = (await receiveRequest()).request;
|
|
6573
6769
|
trace(`execute request received: workDir=${request.workDir}, workflow=${request.workflowName}, job=${request.jobName}`);
|
|
6574
6770
|
const workDir = request.workDir;
|
|
6575
|
-
const defaultTimeoutMs = request.defaultStepTimeoutMs ??
|
|
6771
|
+
const defaultTimeoutMs = request.defaultStepTimeoutMs ?? 18e5;
|
|
6576
6772
|
const isGlobal = request.isGlobalWorkflow === true;
|
|
6577
6773
|
const workflowDir = isGlobal ? join(workDir, "workflow") : workDir;
|
|
6578
6774
|
const sourceDir = isGlobal ? join(workDir, "source") : workDir;
|
|
@@ -6596,48 +6792,47 @@ async function main() {
|
|
|
6596
6792
|
});
|
|
6597
6793
|
jobDeadlineAbort.abort();
|
|
6598
6794
|
});
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6795
|
+
if (!request.cleanupOnly) {
|
|
6796
|
+
await cloneRepoIfRequested(request, workDir, workflowDir, sourceDir, isGlobal);
|
|
6797
|
+
await applyOverlayIfRequested(request, workflowDir);
|
|
6798
|
+
await makeOverlayGitUsable(request, workflowDir);
|
|
6799
|
+
if (aborted) abortAndExit("aborted after clone");
|
|
6800
|
+
await installDependenciesIfNeeded(workflowDir, request);
|
|
6801
|
+
if (aborted) abortAndExit("aborted after deps");
|
|
6802
|
+
await restoreSourceTarballIfRequested(workflowDir, request);
|
|
6803
|
+
}
|
|
6606
6804
|
const loaded = await loadWorkflowModuleWithCapture(workflowDir, request, isGlobal, maskedSend);
|
|
6607
6805
|
const module = loaded.module;
|
|
6608
6806
|
const workflow = extractWorkflow(module, request.workflowName);
|
|
6609
|
-
await evaluateConcurrencyGroupIfPresent(workflow, request);
|
|
6807
|
+
if (!request.cleanupOnly) await evaluateConcurrencyGroupIfPresent(workflow, request);
|
|
6610
6808
|
const globalRepoInfo = setupGlobalWorkflowEnv(request, isGlobal, workflowDir, sourceDir);
|
|
6611
|
-
const
|
|
6612
|
-
const reqId = randomUUID();
|
|
6613
|
-
sendMessage({
|
|
6614
|
-
type: "agent.api.request",
|
|
6615
|
-
requestId: reqId,
|
|
6616
|
-
method,
|
|
6617
|
-
params: params ?? {}
|
|
6618
|
-
});
|
|
6619
|
-
return waitForApiResponse(reqId);
|
|
6620
|
-
};
|
|
6621
|
-
const { normalizedSteps, nodes, refMap, driftDroppedJobs } = await extractAndNormalizeSteps(workflow, request, apiTransport, globalRepoInfo);
|
|
6809
|
+
const { normalizedSteps, nodes, refMap, driftDroppedJobs } = await extractStepsForRun(workflow, request, globalRepoInfo);
|
|
6622
6810
|
const { operatorSecretKeys, outputsMap, secretOutputs, jobOutputsMap } = buildOutputInfrastructure(request, refMap, loaded.sdkSetters);
|
|
6623
6811
|
const job = findJob(workflow, request.jobName);
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6812
|
+
if (!request.cleanupOnly) {
|
|
6813
|
+
const jobHasRules = (job?.rules?.length ?? 0) > 0;
|
|
6814
|
+
const anyStepHasRules = normalizedSteps.some((s) => (s.rules?.length ?? 0) > 0);
|
|
6815
|
+
await resolveChangedFilesForRules(request, sourceDir, jobHasRules || anyStepHasRules);
|
|
6816
|
+
await maybeSkipJobOnRules(job, request, normalizedSteps, globalRepoInfo);
|
|
6817
|
+
if (aborted) abortAndExit("aborted after rules");
|
|
6818
|
+
}
|
|
6629
6819
|
const jobHooks = collectJobHooks(job);
|
|
6820
|
+
maskedSend({
|
|
6821
|
+
type: "hooks-declared",
|
|
6822
|
+
declaresCleanup: Boolean(jobHooks.onFailure || jobHooks.cleanup)
|
|
6823
|
+
});
|
|
6630
6824
|
flushOutputCapture();
|
|
6631
6825
|
capturePrepareActive = false;
|
|
6632
6826
|
const stepCwd = sourceDir;
|
|
6633
|
-
await
|
|
6827
|
+
const envFiles = await createEnvFiles(tmpdir());
|
|
6828
|
+
if (!request.cleanupOnly) await runInitPhaseOrFailJob({
|
|
6634
6829
|
job,
|
|
6635
6830
|
stepCwd,
|
|
6636
|
-
envFiles
|
|
6831
|
+
envFiles,
|
|
6637
6832
|
operatorSecretKeys,
|
|
6638
6833
|
maskedSend
|
|
6639
6834
|
});
|
|
6640
|
-
const { cachePhaseDeps, jobCacheSpecs, jobCacheRestore } = await setupJobCache(stepCwd, normalizedSteps.length, job?.cache, maskedSend);
|
|
6835
|
+
const { cachePhaseDeps, jobCacheSpecs, jobCacheRestore } = await setupJobCache(stepCwd, normalizedSteps.length, request.cleanupOnly ? void 0 : job?.cache, maskedSend);
|
|
6641
6836
|
const stepTasks = new StepTaskRegistry();
|
|
6642
6837
|
const stepAbortControllers = /* @__PURE__ */ new Map();
|
|
6643
6838
|
const jobTempScope = createTempScope();
|
|
@@ -6669,6 +6864,7 @@ async function main() {
|
|
|
6669
6864
|
abortStep: (stepIndex) => stepAbortControllers.get(stepIndex)?.abort(),
|
|
6670
6865
|
getStepAbortSignal: (stepIndex) => stepAbortControllers.get(stepIndex)?.signal,
|
|
6671
6866
|
checkMode: request.checkMode,
|
|
6867
|
+
...request.cleanupOnly ? { forceInitialFailure: true } : {},
|
|
6672
6868
|
createStepContext: createStepCtxWithCapture,
|
|
6673
6869
|
sendIpc: maskedSend,
|
|
6674
6870
|
defaultTimeoutMs,
|
|
@@ -6777,6 +6973,6 @@ main().catch((error) => {
|
|
|
6777
6973
|
setTimeout(() => process.exit(1), 100);
|
|
6778
6974
|
});
|
|
6779
6975
|
//#endregion
|
|
6780
|
-
export { buildConcurrencyGroupContext, buildJobRuleCompletion, buildSandboxShell, buildStepEnvFileHooks, buildStepLoopRuleInputs, buildStepNeedsContext, coerceStep, createSandboxStepContext, deriveFanout, drainJobTempScope, rawPayloadFromEvent, resolveChangedFilesForRules, resolveEmitEventName, sanitizeTempLabel, setupGlobalWorkflowEnv };
|
|
6976
|
+
export { assertUserEmittableEventName, buildConcurrencyGroupContext, buildJobRuleCompletion, buildSandboxShell, buildStepEnvFileHooks, buildStepLoopRuleInputs, buildStepNeedsContext, coerceStep, createSandboxStepContext, deriveFanout, drainJobTempScope, rawPayloadFromEvent, resolveChangedFilesForRules, resolveEmitEventName, sanitizeTempLabel, setupGlobalWorkflowEnv, withRepoWrite };
|
|
6781
6977
|
|
|
6782
6978
|
//# sourceMappingURL=workflow-runner.js.map
|