@kici-dev/agent 0.4.0 → 0.5.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/execution/download.d.ts +27 -2
- package/dist/execution/generator-context.d.ts +54 -0
- package/dist/execution/global-eval-runner.d.ts +92 -0
- package/dist/execution/global-workflow-env.d.ts +57 -0
- package/dist/execution/init-runner.d.ts +60 -3
- package/dist/execution/job-runner.d.ts +98 -0
- package/dist/execution/sandbox/step-loop.d.ts +10 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +52 -1
- package/dist/execution/workflow-loader.d.ts +8 -1
- package/dist/index.js +89 -19
- package/dist/metrics/prometheus.d.ts +10 -10
- package/dist/server.js +1155 -158
- package/dist/workflow-runner-bundle.js +5052 -4591
- package/dist/workflow-runner.js +269 -67
- package/package.json +5 -5
- package/sbom.spdx.json +69 -64
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* Extracted from workflow-loader.ts to avoid duplication across
|
|
5
5
|
* dep-restore.ts and workflow-loader.ts.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Retries a pre-signed upload makes after its first attempt, matching the
|
|
9
|
+
* dep-tarball download's ceiling (`MAX_RETRIES` in `dep-restore.ts`) so both
|
|
10
|
+
* halves of the agent's object-storage traffic give up at the same point.
|
|
11
|
+
*/
|
|
12
|
+
export declare const UPLOAD_MAX_RETRIES = 2;
|
|
7
13
|
/**
|
|
8
14
|
* Download content from an HTTP/HTTPS URL.
|
|
9
15
|
*
|
|
@@ -15,15 +21,34 @@
|
|
|
15
21
|
*/
|
|
16
22
|
export declare function downloadUrl(url: string): Promise<Buffer>;
|
|
17
23
|
/**
|
|
18
|
-
* Upload a buffer to a pre-signed S3 URL via HTTP PUT
|
|
24
|
+
* Upload a buffer to a pre-signed S3 URL via HTTP PUT, retrying a transient
|
|
25
|
+
* failure.
|
|
19
26
|
*
|
|
20
27
|
* Used for direct-to-S3 uploads of bundles and dep tarballs. Localhost /
|
|
21
28
|
* 127.0.0.1 URLs are rewritten via `resolveOrchestratorUrl` so the
|
|
22
29
|
* filesystem cache backend's signed URLs work from container agents that
|
|
23
30
|
* can't reach the orchestrator's host loopback directly.
|
|
24
31
|
*
|
|
32
|
+
* **Why retrying is safe here.** A pre-signed PUT writes one whole object at a
|
|
33
|
+
* single key: there is no multipart session, no append, and no
|
|
34
|
+
* server-generated identity, so a repeat attempt writes the same bytes to the
|
|
35
|
+
* same key and the last write wins. S3 also only makes an object visible once
|
|
36
|
+
* the body has been received in full, so an attempt that died mid-body left
|
|
37
|
+
* nothing behind. A retry therefore cannot double-write or produce a torn
|
|
38
|
+
* object — which is why every AWS SDK retries PUTs by default.
|
|
39
|
+
*
|
|
40
|
+
* Only a failure that can plausibly differ next time is repeated — see
|
|
41
|
+
* {@link isRetryableUploadFailure}.
|
|
42
|
+
*
|
|
25
43
|
* @param url - The pre-signed URL to upload to
|
|
26
44
|
* @param data - The buffer to upload
|
|
45
|
+
* @param opts.baseDelayMs - Backoff before the first retry (doubles thereafter)
|
|
46
|
+
* @param opts.timeoutMs - Per-attempt socket-inactivity timeout (see
|
|
47
|
+
* {@link UPLOAD_TIMEOUT_MS}); an override exists so a test can drive the
|
|
48
|
+
* stall path without waiting out the production budget.
|
|
27
49
|
*/
|
|
28
|
-
export declare function uploadToPresignedUrl(url: string, data: Buffer
|
|
50
|
+
export declare function uploadToPresignedUrl(url: string, data: Buffer, opts?: {
|
|
51
|
+
baseDelayMs?: number;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
}): Promise<void>;
|
|
29
54
|
//# sourceMappingURL=download.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single builder for a `DynamicJobFn`'s context.
|
|
3
|
+
*
|
|
4
|
+
* A generator is evaluated twice — once pre-dispatch to produce the job list,
|
|
5
|
+
* and once inside the sandbox to recover the step closures a `LockJob` cannot
|
|
6
|
+
* carry. `extractStepsFromDynamicJob` throws when the target job is missing on
|
|
7
|
+
* re-evaluation, so a generator whose two calls see different contexts is
|
|
8
|
+
* unsound by construction. Every call site builds its context here so the two
|
|
9
|
+
* cannot drift apart.
|
|
10
|
+
*/
|
|
11
|
+
import type { DynamicJobContext, RepoInfo } from '@kici-dev/sdk';
|
|
12
|
+
/**
|
|
13
|
+
* The source / workflow repo pair a global workflow's generator sees.
|
|
14
|
+
*
|
|
15
|
+
* `sourceRepo.path` is an absolute path into THIS evaluation's work directory:
|
|
16
|
+
* it is NOT stable across the two evaluations (a different `workDir`, possibly
|
|
17
|
+
* a different machine). Its *contents* are stable; the path is not. So read
|
|
18
|
+
* through the path — never embed it in a generated job name, a job output, or
|
|
19
|
+
* anything else the two evaluations are compared on.
|
|
20
|
+
*
|
|
21
|
+
* `RepoInfo.ref` and `.sha` are optional: an evaluation that has no checkout
|
|
22
|
+
* metadata still supplies the pair. Never assume either is present.
|
|
23
|
+
*/
|
|
24
|
+
export interface GeneratorRepoPair {
|
|
25
|
+
sourceRepo: RepoInfo;
|
|
26
|
+
workflowRepo: RepoInfo;
|
|
27
|
+
}
|
|
28
|
+
/** Input for {@link buildGeneratorContext}. */
|
|
29
|
+
export interface GeneratorContextInput {
|
|
30
|
+
workflowName: string;
|
|
31
|
+
/**
|
|
32
|
+
* The raw wire event. Untyped JSON that, per the unified event protocol,
|
|
33
|
+
* always carries the normalized event envelope.
|
|
34
|
+
*/
|
|
35
|
+
event: Record<string, unknown>;
|
|
36
|
+
env: Record<string, string | undefined>;
|
|
37
|
+
/** Present for a global workflow; omitted entirely otherwise. */
|
|
38
|
+
repos?: GeneratorRepoPair;
|
|
39
|
+
/** Frozen upstream outputs for a result-aware generator. */
|
|
40
|
+
needs?: NonNullable<DynamicJobContext['ctx']['needs']>;
|
|
41
|
+
$: DynamicJobContext['$'];
|
|
42
|
+
log: DynamicJobContext['log'];
|
|
43
|
+
kici: DynamicJobContext['kici'];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the context handed to a `DynamicJobFn`.
|
|
47
|
+
*
|
|
48
|
+
* Optional members are spread conditionally rather than assigned `undefined`,
|
|
49
|
+
* so an absent `needs` / repo pair leaves no key behind — a present-but-
|
|
50
|
+
* undefined key reads as "declared" to a generator and serializes differently
|
|
51
|
+
* between the two evaluations.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildGeneratorContext(input: GeneratorContextInput): DynamicJobContext;
|
|
54
|
+
//# sourceMappingURL=generator-context.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-run global eval round.
|
|
3
|
+
*
|
|
4
|
+
* One round runs per (event × workflow repo), on an agent that already holds
|
|
5
|
+
* both trees on disk. For each candidate global workflow it runs the workflow's
|
|
6
|
+
* `filter` — deciding whether the workflow applies to the source repo at all —
|
|
7
|
+
* and, for a survivor, its `DynamicJobFn`s, so the orchestrator can dispatch
|
|
8
|
+
* generated jobs it could not otherwise see. The round precedes every run row:
|
|
9
|
+
* a `filter` returning `false` means no run is created.
|
|
10
|
+
*
|
|
11
|
+
* Shape is modelled on `init-runner.ts` — load the module, resolve the target,
|
|
12
|
+
* evaluate under `withTimeout`, return a structured result. As there, author
|
|
13
|
+
* code runs in the agent's ordinary module loader: no `vm`, no `new Function`,
|
|
14
|
+
* no dynamic `eval`.
|
|
15
|
+
*
|
|
16
|
+
* Two properties are load-bearing and easy to lose:
|
|
17
|
+
*
|
|
18
|
+
* - **One bad candidate never sinks the round.** A candidate whose filter or
|
|
19
|
+
* generator throws, or that blows its budget, comes back
|
|
20
|
+
* `{ run: false, indeterminate: true, reason }` while its siblings carry real
|
|
21
|
+
* verdicts. Several unrelated org-wide workflows share a round; a single
|
|
22
|
+
* broken filter suppressing all of them would be a silent outage.
|
|
23
|
+
* - **Candidates are STARTED sequentially.** They share one checkout and one
|
|
24
|
+
* working directory, so a parallel `$` would race on cwd. This is a strict
|
|
25
|
+
* guarantee on the happy path and **best-effort past a timeout**: `withTimeout`
|
|
26
|
+
* races rather than cancels, so a candidate that blew its own budget is still
|
|
27
|
+
* running when the next one starts. Nothing can preempt user code mid-`await`,
|
|
28
|
+
* so the round bounds the damage rather than eliminating it — the loop stops
|
|
29
|
+
* at the round deadline (and on the caller's abort signal), which caps the
|
|
30
|
+
* overlap at one orphaned candidate instead of the whole remaining queue.
|
|
31
|
+
*/
|
|
32
|
+
import type { RepoInfo, DynamicJobContext, Logger } from '@kici-dev/sdk';
|
|
33
|
+
import { type $ as Shell } from 'zx';
|
|
34
|
+
import type { ChangedFilesStatus, GlobalEvalRoundResult } from '@kici-dev/engine';
|
|
35
|
+
/** One workflow the round must decide on, as the lock file describes it. */
|
|
36
|
+
export interface GlobalEvalCandidate {
|
|
37
|
+
workflowName: string;
|
|
38
|
+
/** Repo-relative path of the workflow module inside the workflow checkout. */
|
|
39
|
+
sourceFile: string;
|
|
40
|
+
/** From `LockWorkflow.hasFilter` — skip the filter call entirely when false. */
|
|
41
|
+
hasFilter: boolean;
|
|
42
|
+
}
|
|
43
|
+
/** Arguments for {@link runGlobalEvalRound}. */
|
|
44
|
+
export interface GlobalEvalRoundArgs {
|
|
45
|
+
/** Absolute path of the workflow-repo checkout (modules are loaded from here). */
|
|
46
|
+
workflowDir: string;
|
|
47
|
+
/** Absolute path of the source-repo checkout. */
|
|
48
|
+
sourceDir: string;
|
|
49
|
+
repos: {
|
|
50
|
+
sourceRepo: RepoInfo;
|
|
51
|
+
workflowRepo: RepoInfo;
|
|
52
|
+
};
|
|
53
|
+
candidates: GlobalEvalCandidate[];
|
|
54
|
+
/** The raw wire event, carrying the normalized event envelope. */
|
|
55
|
+
event: Record<string, unknown>;
|
|
56
|
+
changedFiles: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Availability of `changedFiles`. Required, not defaulted: it feeds the
|
|
59
|
+
* throwing accessor on `FilterContext`, and defaulting it to `'fetched'`
|
|
60
|
+
* would let a diff-less event read as an empty diff and silently suppress
|
|
61
|
+
* every path-gated workflow in the round.
|
|
62
|
+
*/
|
|
63
|
+
changedFilesStatus: ChangedFilesStatus;
|
|
64
|
+
/** Wall-clock budget for the whole round. */
|
|
65
|
+
roundTimeoutMs: number;
|
|
66
|
+
/** Wall-clock budget for one candidate (its filter plus its generators). */
|
|
67
|
+
candidateTimeoutMs: number;
|
|
68
|
+
/**
|
|
69
|
+
* Caller's cancellation signal. The round stops starting new candidates once
|
|
70
|
+
* it fires; the one already in flight cannot be preempted (see the module
|
|
71
|
+
* doc comment) but its verdict is discarded.
|
|
72
|
+
*/
|
|
73
|
+
signal?: AbortSignal;
|
|
74
|
+
/** zx shell handed to filters and generators. Defaults to the ambient `$`. */
|
|
75
|
+
$?: typeof Shell;
|
|
76
|
+
/** Logger handed to generators. Defaults to a no-op. */
|
|
77
|
+
log?: Logger;
|
|
78
|
+
/** KiCI API handed to generators. Defaults to one that rejects every call. */
|
|
79
|
+
kici?: DynamicJobContext['kici'];
|
|
80
|
+
/**
|
|
81
|
+
* Module loader seam. Defaults to `loadWorkflowSource` against `workflowDir`;
|
|
82
|
+
* a caller that already holds the modules (or a unit test) supplies its own.
|
|
83
|
+
*/
|
|
84
|
+
loadModule?: (sourceFile: string) => Promise<Record<string, unknown>>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run one global eval round and return every candidate's verdict, in candidate
|
|
88
|
+
* order. Never throws: a round that exceeds `roundTimeoutMs` reports whatever
|
|
89
|
+
* it established and marks the rest indeterminate.
|
|
90
|
+
*/
|
|
91
|
+
export declare function runGlobalEvalRound(args: GlobalEvalRoundArgs): Promise<GlobalEvalRoundResult>;
|
|
92
|
+
//# sourceMappingURL=global-eval-runner.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single writer of the seven `KICI_*` ambient env keys a global workflow's
|
|
3
|
+
* user code sees.
|
|
4
|
+
*
|
|
5
|
+
* It lives here rather than in `sandbox/workflow-runner.ts` because that module
|
|
6
|
+
* calls `main()` at import time — it is the sandbox process entry point — so the
|
|
7
|
+
* agent's own process cannot import it. The pre-dispatch global eval round runs
|
|
8
|
+
* in the agent process and must set exactly the same keys the sandbox does.
|
|
9
|
+
*
|
|
10
|
+
* Why exactly the same keys: `extractAndNormalizeSteps` hands a `DynamicJobFn`
|
|
11
|
+
* `process.env` as its `env`, and a generator is evaluated twice — once
|
|
12
|
+
* pre-dispatch, once inside the sandbox to recover the step closures a `LockJob`
|
|
13
|
+
* cannot carry. A generator that reads `process.env.KICI_SOURCE_REPO_PATH` and
|
|
14
|
+
* gets a value on one call and `undefined` on the other has seen two different
|
|
15
|
+
* worlds, which `extractStepsFromDynamicJob` turns into a hard determinism
|
|
16
|
+
* failure. `buildGeneratorContext` keeps the two contexts' *shape* identical; it
|
|
17
|
+
* cannot keep the ambient env identical, which is what this does.
|
|
18
|
+
*/
|
|
19
|
+
import type { RepoInfo } from '@kici-dev/sdk';
|
|
20
|
+
/** The source / workflow repo pair a global-workflow evaluation runs against. */
|
|
21
|
+
export interface GlobalWorkflowRepos {
|
|
22
|
+
sourceRepo: RepoInfo;
|
|
23
|
+
workflowRepo: RepoInfo;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Derive an `owner/repo` identifier from a clone URL, stripping the trailing
|
|
27
|
+
* `.git` and any `http(s)://host/` prefix.
|
|
28
|
+
*/
|
|
29
|
+
export declare function repoIdentifierFromUrl(repoUrl: string): string;
|
|
30
|
+
/** Every env key {@link applyGlobalWorkflowEnv} writes, in one place. */
|
|
31
|
+
export declare const GLOBAL_WORKFLOW_ENV_KEYS: readonly ['KICI_IS_GLOBAL_WORKFLOW', 'KICI_WORKFLOW_REPO_PATH', 'KICI_SOURCE_REPO_PATH', 'KICI_SOURCE_REPO', 'KICI_SOURCE_BRANCH', 'KICI_SOURCE_SHA', 'KICI_WORKFLOW_REPO'];
|
|
32
|
+
/**
|
|
33
|
+
* Inject the seven global-workflow env keys and return a restorer that puts
|
|
34
|
+
* `process.env` back exactly as it was — each key reset to its prior value, or
|
|
35
|
+
* deleted if it had none.
|
|
36
|
+
*
|
|
37
|
+
* **The restorer is mandatory for any caller in a long-lived process.** The
|
|
38
|
+
* sandbox may ignore it: it runs one job per forked child, which exits. The
|
|
39
|
+
* pre-dispatch global eval round may NOT: it runs in the agent process, which
|
|
40
|
+
* serves many dispatches from one `JobRunner`. Leaving the keys set there is
|
|
41
|
+
* this module's own hazard running backwards — a later NON-global
|
|
42
|
+
* `DynamicJobFn` evaluation builds its generator context with
|
|
43
|
+
* `env: process.env` still carrying `KICI_IS_GLOBAL_WORKFLOW=true` and a
|
|
44
|
+
* `KICI_SOURCE_REPO_PATH` pointing at a deleted work directory, while that
|
|
45
|
+
* job's own sandbox re-evaluation sees neither (`buildSanitizedEnv` scrubs the
|
|
46
|
+
* whole `KICI_*` namespace on the trusted profile, and the default profile is
|
|
47
|
+
* allowlist-only). That is the same two-worlds determinism failure, injected
|
|
48
|
+
* into an unrelated job.
|
|
49
|
+
*
|
|
50
|
+
* `RepoInfo.ref` / `.sha` are optional, so an evaluation with no checkout
|
|
51
|
+
* metadata writes an empty string rather than leaving the key unset — matching
|
|
52
|
+
* how `KICI_WORKFLOW_REPO` already handles a missing identifier. Assigning
|
|
53
|
+
* `undefined` to a `process.env` key would stringify to `"undefined"`, which is
|
|
54
|
+
* worse than either.
|
|
55
|
+
*/
|
|
56
|
+
export declare function applyGlobalWorkflowEnv(repos: GlobalWorkflowRepos): () => void;
|
|
57
|
+
//# sourceMappingURL=global-workflow-env.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { $ as Shell } from 'zx';
|
|
2
|
+
import type { Workflow, RepoInfo } from '@kici-dev/sdk';
|
|
3
|
+
import { type ChangedFilesStatus, type MatrixValues } from '@kici-dev/engine';
|
|
3
4
|
/**
|
|
4
5
|
* Result of evaluating dynamic fields on a job.
|
|
5
6
|
* Only fields that were flagged as dynamic and successfully resolved are set.
|
|
@@ -14,7 +15,56 @@ export interface InitResult {
|
|
|
14
15
|
* The orchestrator re-materializes these into N execution jobs at dispatch.
|
|
15
16
|
*/
|
|
16
17
|
matrixValues?: MatrixValues[];
|
|
18
|
+
/**
|
|
19
|
+
* Verdict of the workflow-level `filter`, set only when the init job was asked
|
|
20
|
+
* to evaluate one (`flags.hasFilter`). `false` means the workflow does not
|
|
21
|
+
* apply and its job must not be dispatched.
|
|
22
|
+
*
|
|
23
|
+
* Optional on purpose: an agent that predates the filter never sets it, so the
|
|
24
|
+
* orchestrator reads absence as "no verdict was reported", never as "suppress".
|
|
25
|
+
*/
|
|
26
|
+
filterPassed?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Everything a workflow-level `filter` needs beyond the workflow module and the
|
|
30
|
+
* event. Supplied by the caller because none of it is derivable here: the source
|
|
31
|
+
* tree lives wherever the init job materialized it, and the diff is ground truth
|
|
32
|
+
* from that same clone.
|
|
33
|
+
*/
|
|
34
|
+
export interface FilterEvalInput {
|
|
35
|
+
/** The repo whose event triggered this evaluation. */
|
|
36
|
+
sourceRepo: RepoInfo;
|
|
37
|
+
/** The repo that registered the workflow — identical to `sourceRepo` here. */
|
|
38
|
+
workflowRepo: RepoInfo;
|
|
39
|
+
changedFiles: string[];
|
|
40
|
+
changedFilesStatus: ChangedFilesStatus;
|
|
41
|
+
env?: Record<string, string | undefined>;
|
|
42
|
+
/** zx shell handed to the filter. Defaults to the ambient `$`. */
|
|
43
|
+
$?: typeof Shell;
|
|
17
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Run a workflow's `filter` and report whether the workflow applies.
|
|
47
|
+
*
|
|
48
|
+
* Shared by both agent-side evaluation sites for a same-repo workflow: the init
|
|
49
|
+
* job that gates each static job's dispatch, and the dynamic-eval job that gates
|
|
50
|
+
* whether a generator runs at all. Both must reach the same verdict from the same
|
|
51
|
+
* inputs, so neither builds the context itself.
|
|
52
|
+
*
|
|
53
|
+
* The context is built through `createFilterContext` rather than as an object
|
|
54
|
+
* literal: the factory installs `changedFiles` as a throwing getter, so a filter
|
|
55
|
+
* that reads the diff on an event that has none fails loudly instead of seeing an
|
|
56
|
+
* empty list. A `false` verdict dispatches none of the workflow's own jobs, so a
|
|
57
|
+
* silently-empty diff would suppress it on a mistake. On this same-repo path the
|
|
58
|
+
* verdict is at least recoverable — the run row exists, carrying the `__init__*`
|
|
59
|
+
* jobs, and this evaluation's own step log records the verdict; it is the
|
|
60
|
+
* organization-wide path, which runs elsewhere, that leaves nothing behind.
|
|
61
|
+
*
|
|
62
|
+
* A throwing filter propagates: the evaluating job fails, which surfaces as a
|
|
63
|
+
* failed run. "Could not decide" is never treated as "do not run" — that would
|
|
64
|
+
* be a false green, the same reasoning `buildJobRuleCompletion` applies to a rule
|
|
65
|
+
* whose `check()` threw.
|
|
66
|
+
*/
|
|
67
|
+
export declare function evaluateWorkflowFilter(workflow: Workflow, event: Record<string, unknown>, input: FilterEvalInput | undefined, timeoutMs: number): Promise<boolean>;
|
|
18
68
|
/**
|
|
19
69
|
* Evaluate dynamic fields (context, env, concurrencyGroup) on a job.
|
|
20
70
|
*
|
|
@@ -26,16 +76,23 @@ export interface InitResult {
|
|
|
26
76
|
* -: If a dynamic function returns undefined/null, the field is left undefined.
|
|
27
77
|
* -: Each dynamic function call is wrapped in a timeout (default 60s).
|
|
28
78
|
*
|
|
79
|
+
* A workflow-level `filter` is evaluated FIRST when `flags.hasFilter` is set. A
|
|
80
|
+
* `false` verdict returns immediately: no job of that workflow will be
|
|
81
|
+
* dispatched, so evaluating this one's dynamic fields would run customer code
|
|
82
|
+
* whose result nothing can consume.
|
|
83
|
+
*
|
|
29
84
|
* @param workflow - The extracted Workflow object
|
|
30
85
|
* @param jobName - Name of the job whose dynamic fields to evaluate
|
|
31
86
|
* @param event - Normalized event envelope — same shape every dynamic-function call site receives.
|
|
32
87
|
* @param flags - Which fields are dynamic and need evaluation
|
|
33
88
|
* @param timeoutMs - Timeout per dynamic function call (default 60_000ms)
|
|
89
|
+
* @param filterInput - Source tree + diff the workflow's `filter` reads. Required when `flags.hasFilter`.
|
|
34
90
|
*/
|
|
35
91
|
export declare function evaluateDynamicFields(workflow: Workflow, jobName: string, event: Record<string, unknown>, flags: {
|
|
36
92
|
dynamicContext: boolean;
|
|
37
93
|
dynamicEnv: boolean;
|
|
38
94
|
dynamicConcurrencyGroup: boolean;
|
|
39
95
|
dynamicMatrix?: boolean;
|
|
40
|
-
|
|
96
|
+
hasFilter?: boolean;
|
|
97
|
+
}, timeoutMs?: number, filterInput?: FilterEvalInput): Promise<InitResult>;
|
|
41
98
|
//# sourceMappingURL=init-runner.d.ts.map
|
|
@@ -1,7 +1,69 @@
|
|
|
1
1
|
import type { AgentToOrchestratorMessage, JobDispatch } from '@kici-dev/engine';
|
|
2
|
+
import type { LogStream } from '@kici-dev/engine';
|
|
2
3
|
import type { AppConfig } from '../config.js';
|
|
4
|
+
import { type FilterEvalInput } from './init-runner.js';
|
|
3
5
|
import { buildNeedsContext } from '@kici-dev/sdk';
|
|
6
|
+
import type { $ as Shell } from 'zx';
|
|
4
7
|
import type { CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, ArtifactRequestIpc, ArtifactResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './sandbox/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Materialize the source tree a non-global workflow's `filter` reads through
|
|
10
|
+
* `ctx.sourceRepo.path`.
|
|
11
|
+
*
|
|
12
|
+
* An init or dynamic-eval job normally restores only `.kici/` from the cached
|
|
13
|
+
* source tarball — enough to import the workflow module, but a directory with no
|
|
14
|
+
* repo in it. A filter that reads a file or shells out against that path would
|
|
15
|
+
* get a confidently wrong answer, and `changedFiles` could not be computed at
|
|
16
|
+
* all, so a filter-bearing job clones the source repo into a sibling directory.
|
|
17
|
+
*
|
|
18
|
+
* When no tarball was attached the job already cloned the whole repo into
|
|
19
|
+
* `workDir`, and that clone is reused rather than duplicated — including the
|
|
20
|
+
* local working-tree case, where there is no repo url and `workDir` IS the tree.
|
|
21
|
+
*
|
|
22
|
+
* A tarball with no repo url is the one combination that cannot be honoured:
|
|
23
|
+
* `workDir` holds `.kici/` alone and there is nothing to clone from. Returning it
|
|
24
|
+
* would hand the filter a directory in which every path test answers "absent" —
|
|
25
|
+
* the exact silent lie this function exists to prevent — so it throws instead.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ensureFilterSourceDir(dispatch: JobDispatch, workDir: string): Promise<string>;
|
|
28
|
+
/**
|
|
29
|
+
* Build the context a non-global workflow's `filter` is evaluated against.
|
|
30
|
+
*
|
|
31
|
+
* `sourceRepo` and `workflowRepo` are the same repo — that is what "non-global"
|
|
32
|
+
* means — so both carry the same identifier, path, ref, and sha. The zx shell is
|
|
33
|
+
* rooted at the source tree and streams into the evaluating step's log, matching
|
|
34
|
+
* what the global eval round hands its own filters.
|
|
35
|
+
*
|
|
36
|
+
* They are two distinct objects all the same. Being the same repo is a fact
|
|
37
|
+
* about their VALUES, not a licence to hand the author one object under two
|
|
38
|
+
* names: a filter that mutated `ctx.sourceRepo` would silently see
|
|
39
|
+
* `ctx.workflowRepo` change with it, which happens on no other path.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildInitFilterInput(dispatch: JobDispatch, event: Record<string, unknown>, workDir: string, emit: (line: string, stream: LogStream) => void): Promise<FilterEvalInput>;
|
|
42
|
+
/**
|
|
43
|
+
* Build the per-invocation zx `$` a global eval round hands to filters and
|
|
44
|
+
* generators, so a `await $\`…\`` inside one is visible in the eval step's log.
|
|
45
|
+
*
|
|
46
|
+
* **`env` is the LIVE `process.env` reference, never a spread.** A spread is a
|
|
47
|
+
* snapshot taken when the shell is built, which is before the round applies the
|
|
48
|
+
* seven `KICI_*` keys — so a filter that shells out (`$\`printenv
|
|
49
|
+
* KICI_SOURCE_REPO_PATH\``, or any subprocess inheriting env) would see nothing
|
|
50
|
+
* here while the sandbox re-evaluation's ambient `$` resolves `process.env`
|
|
51
|
+
* after `setupGlobalWorkflowEnv` has run and does see them. That is the same
|
|
52
|
+
* two-worlds determinism failure the cwd choice below exists to prevent, one
|
|
53
|
+
* layer down. Passing the live reference reproduces the ambient `$`'s own
|
|
54
|
+
* behaviour, which is what the sandbox uses.
|
|
55
|
+
*
|
|
56
|
+
* `verbose: true` + `makeStreamingZxLog` honors a per-call `quiet: true`, so a
|
|
57
|
+
* decrypted secret never leaks into the log.
|
|
58
|
+
*
|
|
59
|
+
* `emit` is a callback rather than the `LogStreamer` itself so the caller can
|
|
60
|
+
* route it through its own closed-guard: `LogStreamer.destroy()` sets no closed
|
|
61
|
+
* flag and `addLine` buffers unconditionally, so a subprocess line arriving
|
|
62
|
+
* after the step was reported would otherwise emit a `log.chunk` for a terminal
|
|
63
|
+
* step. That is the likeliest path for it — an orphaned candidate is usually
|
|
64
|
+
* orphaned *because* it is waiting on a subprocess.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildEvalShell(cwd: string, emit: (line: string, stream: LogStream) => void): Promise<typeof Shell>;
|
|
5
67
|
/**
|
|
6
68
|
* Dependencies injected into JobRunner.
|
|
7
69
|
*/
|
|
@@ -276,6 +338,18 @@ export declare class JobRunner {
|
|
|
276
338
|
* uncommitted changes).
|
|
277
339
|
*/
|
|
278
340
|
private cloneAndApplyOverlay;
|
|
341
|
+
/**
|
|
342
|
+
* Materialize the init job's workflow source into `workDir`. A test run ships
|
|
343
|
+
* its full working tree as an encrypted overlay tarball (`fullRepo`) rather
|
|
344
|
+
* than a git repo, so skip the clone and let the overlay populate the
|
|
345
|
+
* workspace — the same handling the normal execution-job path uses. Otherwise
|
|
346
|
+
* restore from the cached tarball if present, else clone. In every case, apply
|
|
347
|
+
* an attached overlay tarball afterward (test runs with uncommitted changes;
|
|
348
|
+
* for a fullRepo run this is what actually populates the workspace, so the
|
|
349
|
+
* init job resolves a dynamic context against the real source tree instead of
|
|
350
|
+
* an empty directory).
|
|
351
|
+
*/
|
|
352
|
+
private materializeInitJobSource;
|
|
279
353
|
/**
|
|
280
354
|
* Phase 2 of build: install dependencies locally if needed for the build,
|
|
281
355
|
* and (when the orchestrator has flagged the dep cache as stale) pack
|
|
@@ -288,6 +362,30 @@ export declare class JobRunner {
|
|
|
288
362
|
* it to the source cache.
|
|
289
363
|
*/
|
|
290
364
|
private packAndUploadSource;
|
|
365
|
+
/**
|
|
366
|
+
* Clone both repos for a global eval round and materialize the workflow
|
|
367
|
+
* repo's dependencies, mirroring the sandbox's own dual-clone: the workflow
|
|
368
|
+
* repo under `<workDir>/workflow`, the source repo under `<workDir>/source`.
|
|
369
|
+
*
|
|
370
|
+
* `.kici/` lives in the WORKFLOW repo for a global workflow, so deps and the
|
|
371
|
+
* scratch-dir git exclude both apply to that checkout, never the source one.
|
|
372
|
+
*/
|
|
373
|
+
private checkoutForGlobalEvalRound;
|
|
374
|
+
/**
|
|
375
|
+
* Handle a pre-run global eval round.
|
|
376
|
+
*
|
|
377
|
+
* The round runs once per (event × workflow repo) BEFORE any run row exists:
|
|
378
|
+
* it checks out the workflow repo and the source repo, then runs each
|
|
379
|
+
* candidate global workflow's `filter` and — for a survivor — its
|
|
380
|
+
* `DynamicJobFn`s, so the orchestrator learns which workflows apply to this
|
|
381
|
+
* source repo and which jobs each one generates.
|
|
382
|
+
*
|
|
383
|
+
* A candidate that fails is reported indeterminate inside the result, not as
|
|
384
|
+
* a job failure: the round carries several unrelated org-wide workflows, and
|
|
385
|
+
* one broken filter must not suppress the rest. The job itself fails only
|
|
386
|
+
* when the checkout or the round machinery breaks.
|
|
387
|
+
*/
|
|
388
|
+
private handleGlobalEvalRound;
|
|
291
389
|
/**
|
|
292
390
|
* Handle an init-only job.
|
|
293
391
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Extracted for testability: the workflow-runner's main() handles IPC, clone, deps,
|
|
6
6
|
* module loading, and calls this loop for step execution with hooks.
|
|
7
7
|
*/
|
|
8
|
-
import type { Step, StepContext, HookInput, OutputsMap, StepSecretMountRecord, FanoutPosition } from '@kici-dev/sdk';
|
|
8
|
+
import type { Step, StepContext, HookInput, OutputsMap, StepSecretMountRecord, FanoutPosition, RepoInfo } from '@kici-dev/sdk';
|
|
9
9
|
import { CheckMode } from '@kici-dev/engine';
|
|
10
10
|
import type { RunnerToAgentMessage } from './ipc-protocol.js';
|
|
11
11
|
import type { SandboxStepResult } from './types.js';
|
|
@@ -101,6 +101,15 @@ export interface StepLoopOptions {
|
|
|
101
101
|
dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>;
|
|
102
102
|
/** Fan-out position for the rule context (`ctx.fanout`); undefined on a non-fan-out job. */
|
|
103
103
|
fanout?: FanoutPosition;
|
|
104
|
+
/**
|
|
105
|
+
* The repo whose event triggered this run, for the step rule context
|
|
106
|
+
* (`ctx.sourceRepo`). Present for a global workflow, absent otherwise — a
|
|
107
|
+
* step rule reads the source tree through `.path` the same way a job rule
|
|
108
|
+
* and a generator do.
|
|
109
|
+
*/
|
|
110
|
+
sourceRepo?: RepoInfo;
|
|
111
|
+
/** The repo that registered the workflow, for the step rule context (`ctx.workflowRepo`). */
|
|
112
|
+
workflowRepo?: RepoInfo;
|
|
104
113
|
/** Job-level hooks. */
|
|
105
114
|
jobHooks?: JobHooks;
|
|
106
115
|
/**
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
import { $ } from 'zx';
|
|
17
17
|
import { type TempScope } from '@kici-dev/core/tmp';
|
|
18
18
|
import { ExecutionJobStatus } from '@kici-dev/engine';
|
|
19
|
-
import type { Step, StepInput, StepContext } from '@kici-dev/sdk';
|
|
19
|
+
import type { Step, StepInput, StepContext, RepoInfo, EventPayload } from '@kici-dev/sdk';
|
|
20
20
|
import type { NeedsContext, FanoutPosition, EventDefinition } from '@kici-dev/sdk';
|
|
21
21
|
import type { OutputsMap, StepRefMap, TrackedStepSecrets } from '@kici-dev/sdk';
|
|
22
22
|
import type { RunnerToAgentMessage, JobExecutionRequest } from './ipc-protocol.js';
|
|
23
23
|
import { LogMasker } from './log-masker.js';
|
|
24
|
+
import type { StepLoopOptions } from './step-loop.js';
|
|
25
|
+
import type { GeneratorRepoPair } from '../generator-context.js';
|
|
24
26
|
import { type RuleEvaluationResult } from '../rule-evaluator.js';
|
|
25
27
|
/**
|
|
26
28
|
* Build `ctx.needs` for a job's steps from the dispatch envelope. Reconstructs
|
|
@@ -103,6 +105,42 @@ export declare function createSandboxStepContext(workDir: string, stepIndex: num
|
|
|
103
105
|
export declare function deriveFanout(request: JobExecutionRequest): FanoutPosition | undefined;
|
|
104
106
|
/** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
|
|
105
107
|
export declare function rawPayloadFromEvent(event: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* Build the argument the user's `concurrency.group(...)` function receives.
|
|
110
|
+
*
|
|
111
|
+
* The only inputs an author can scope a group by. `branch` alone does not
|
|
112
|
+
* separate one repository from another — an organization-wide workflow runs on
|
|
113
|
+
* events from many repositories, and their default branches share a name — so
|
|
114
|
+
* `event.sourceRepo` is what makes a per-source-repository group expressible.
|
|
115
|
+
* That is why the orchestrator writes the whole normalized envelope into every
|
|
116
|
+
* global job config: an empty `event` here silently collapses every repository
|
|
117
|
+
* into one group, and with `cancelInProgress` (the default) one repository's
|
|
118
|
+
* push then cancels another's in-flight run.
|
|
119
|
+
*
|
|
120
|
+
* Boundary cast: the wire `request.event` is untyped JSON that, per the unified
|
|
121
|
+
* event protocol, always carries the normalized event envelope.
|
|
122
|
+
*/
|
|
123
|
+
export declare function buildConcurrencyGroupContext(request: JobExecutionRequest): {
|
|
124
|
+
branch: string;
|
|
125
|
+
event: EventPayload;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Phase 5 — Inject env vars and build the `RepoInfo` pair that the generator,
|
|
129
|
+
* the job rules, and step contexts receive when the job is a global workflow.
|
|
130
|
+
* No-op for normal jobs.
|
|
131
|
+
*
|
|
132
|
+
* Runs at the head of phase 5, before anything that may read the source tree:
|
|
133
|
+
* the generator's re-evaluation (phase 5) and the job rules (phase 7) both take
|
|
134
|
+
* the returned pair, and both must see what the pre-dispatch evaluation saw.
|
|
135
|
+
*
|
|
136
|
+
* `sourceRepo.path` is this sandbox's own absolute path — the same repo lives at
|
|
137
|
+
* a different path in the evaluation that produced the job list. Read through
|
|
138
|
+
* it; never compare it or embed it in a job name.
|
|
139
|
+
*/
|
|
140
|
+
export declare function setupGlobalWorkflowEnv(request: JobExecutionRequest, isGlobal: boolean, workflowDir: string, sourceDir: string): {
|
|
141
|
+
workflowRepo: RepoInfo;
|
|
142
|
+
sourceRepo: RepoInfo;
|
|
143
|
+
} | undefined;
|
|
106
144
|
/**
|
|
107
145
|
* Mutable state threaded through {@link coerceStep} for one job normalization
|
|
108
146
|
* pass: the shared `step-N` counter, the bare-function → name ref map, and the
|
|
@@ -150,6 +188,19 @@ export declare function resolveChangedFilesForRules(request: JobExecutionRequest
|
|
|
150
188
|
export declare function buildJobRuleCompletion(ruleResult: RuleEvaluationResult, normalizedSteps: Step[]): (RunnerToAgentMessage & {
|
|
151
189
|
type: 'job.complete';
|
|
152
190
|
}) | null;
|
|
191
|
+
/**
|
|
192
|
+
* Build the inputs the step loop turns into every step rule's `RuleContext`.
|
|
193
|
+
*
|
|
194
|
+
* Shares its source with `maybeSkipJobOnRules` so a step rule and a job rule
|
|
195
|
+
* see the same world: the same event, env, dispatch inputs, fan-out position,
|
|
196
|
+
* and — for a global workflow — the same source / workflow repo pair. A step
|
|
197
|
+
* rule that received the pair as `undefined` while the job rule beside it
|
|
198
|
+
* received the real thing would read the same `RuleContext` type two ways.
|
|
199
|
+
*
|
|
200
|
+
* The pair is spread conditionally: a present-but-undefined `sourceRepo` reads
|
|
201
|
+
* as "declared" to a rule that guards on the key rather than the value.
|
|
202
|
+
*/
|
|
203
|
+
export declare function buildStepLoopRuleInputs(request: JobExecutionRequest, repos: GeneratorRepoPair | undefined): Pick<StepLoopOptions, 'event' | 'env' | 'dispatchInputs' | 'fanout' | 'sourceRepo' | 'workflowRepo'>;
|
|
153
204
|
/**
|
|
154
205
|
* Build the step loop's KICI_ENV/KICI_PATH callbacks with a per-step delta-file
|
|
155
206
|
* pair (keyed by step index). `beforeStepEnvFiles(stepIndex)` lazily creates the
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Node's normal ESM lookup against `.kici/node_modules/`.
|
|
8
8
|
*/
|
|
9
9
|
import type { Workflow, StepInput, DynamicJobFn, OutputsMap, StepRefMap } from '@kici-dev/sdk';
|
|
10
|
+
import { type GeneratorRepoPair } from './generator-context.js';
|
|
10
11
|
/**
|
|
11
12
|
* SDK output-map setters resolved from a specific `@kici-dev/sdk` instance.
|
|
12
13
|
* The agent uses these to wire the workflow module's OWN SDK copy (a different
|
|
@@ -99,7 +100,13 @@ export declare function extractStepsFromDynamicJob(workflow: Workflow, dynamicIn
|
|
|
99
100
|
/** Frozen upstream snapshot for a result-aware generator (rebuilds ctx.needs). */
|
|
100
101
|
upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot,
|
|
101
102
|
/** Declared upstream needs that shape ctx.needs. */
|
|
102
|
-
declaredNeeds?: readonly unknown[]
|
|
103
|
+
declaredNeeds?: readonly unknown[],
|
|
104
|
+
/**
|
|
105
|
+
* The source / workflow repo pair for a global workflow. Must match what the
|
|
106
|
+
* first evaluation saw, or a generator that reads the source tree produces a
|
|
107
|
+
* different job list here and the determinism check below fails the job.
|
|
108
|
+
*/
|
|
109
|
+
repos?: GeneratorRepoPair): Promise<{
|
|
103
110
|
steps: readonly StepInput[];
|
|
104
111
|
droppedJobs: string[];
|
|
105
112
|
}>;
|