@kici-dev/compiler 0.1.27 → 0.2.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/auth/headless-detect.d.ts +14 -5
- package/dist/auth/headless-detect.js +50 -9
- package/dist/cli.js +120 -11
- package/dist/commands/compile.js +28 -4
- package/dist/commands/diagnostics.js +14 -2
- package/dist/commands/doctor.d.ts +64 -0
- package/dist/commands/doctor.js +413 -0
- package/dist/commands/endpoints.js +6 -0
- package/dist/commands/fixture.js +13 -0
- package/dist/commands/hook.js +3 -2
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.js +5 -1
- package/dist/commands/init-host-os.d.ts +34 -0
- package/dist/commands/init-host-os.js +44 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +159 -36
- package/dist/commands/local.d.ts +28 -2
- package/dist/commands/local.js +74 -5
- package/dist/commands/login.js +1 -1
- package/dist/commands/notifications.d.ts +61 -0
- package/dist/commands/notifications.js +274 -0
- package/dist/commands/preview.js +8 -1
- package/dist/commands/run-routed.js +10 -1
- package/dist/commands/run.js +41 -12
- package/dist/commands/runs/artifacts/download.d.ts +24 -0
- package/dist/commands/runs/artifacts/download.js +154 -0
- package/dist/commands/runs/artifacts/list.d.ts +5 -0
- package/dist/commands/runs/artifacts/list.js +47 -0
- package/dist/commands/runs/cancel.js +2 -2
- package/dist/commands/runs/list.js +8 -2
- package/dist/commands/verify-attestation.d.ts +3 -1
- package/dist/commands/verify-attestation.js +18 -7
- package/dist/errors/formatter.d.ts +14 -1
- package/dist/errors/formatter.js +13 -3
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +3 -2
- package/dist/errors/source-location.d.ts +20 -0
- package/dist/errors/source-location.js +45 -0
- package/dist/execution/executor.js +25 -5
- package/dist/fixtures/defaults/index.js +2 -1
- package/dist/llm-context/llms-architecture.txt +48 -19
- package/dist/llm-context/llms-cli.txt +2461 -1582
- package/dist/llm-context/llms-features.txt +178 -106
- package/dist/llm-context/llms-full.txt +6124 -4423
- package/dist/llm-context/llms-getting-started.txt +306 -42
- package/dist/llm-context/llms-patterns.txt +13 -7
- package/dist/llm-context/llms-sdk-runtime.txt +2013 -0
- package/dist/llm-context/llms-sdk.txt +591 -2152
- package/dist/llm-context/llms.txt +30 -16
- package/dist/local-plane/orchestrator-process.d.ts +12 -7
- package/dist/local-plane/orchestrator-process.js +28 -14
- package/dist/local-plane/os-mismatch-hint.d.ts +16 -0
- package/dist/local-plane/os-mismatch-hint.js +34 -0
- package/dist/local-plane/plane-liveness.d.ts +68 -0
- package/dist/local-plane/plane-liveness.js +145 -0
- package/dist/local-plane/plane-manager.d.ts +60 -18
- package/dist/local-plane/plane-manager.js +210 -70
- package/dist/local-plane/plane-trigger.d.ts +13 -4
- package/dist/local-plane/plane-trigger.js +27 -14
- package/dist/local-plane/platform-attach.js +1 -5
- package/dist/local-plane/port-holder.d.ts +78 -0
- package/dist/local-plane/port-holder.js +227 -0
- package/dist/local-plane/postgres.d.ts +15 -3
- package/dist/local-plane/postgres.js +34 -12
- package/dist/local-plane/run-follow.d.ts +20 -0
- package/dist/local-plane/run-follow.js +51 -4
- package/dist/local-plane/scaler-config.d.ts +28 -14
- package/dist/local-plane/scaler-config.js +59 -41
- package/dist/local-plane/source-provider.js +4 -10
- package/dist/lockfile/generator.d.ts +14 -1
- package/dist/lockfile/generator.js +99 -33
- package/dist/lockfile/index.d.ts +3 -1
- package/dist/lockfile/index.js +3 -2
- package/dist/lockfile/purity-diagnostics.d.ts +31 -0
- package/dist/lockfile/purity-diagnostics.js +52 -0
- package/dist/postinstall.js +2 -1
- package/dist/remote/artifact-extract.d.ts +8 -0
- package/dist/remote/artifact-extract.js +58 -0
- package/dist/remote/config.d.ts +21 -3
- package/dist/remote/config.js +24 -5
- package/dist/remote/dashboard-client.d.ts +39 -2
- package/dist/remote/dashboard-client.js +47 -4
- package/dist/remote/fs-case.d.ts +21 -0
- package/dist/remote/fs-case.js +45 -0
- package/dist/remote/history.d.ts +9 -3
- package/dist/remote/history.js +79 -10
- package/dist/remote/local-repo-identity.d.ts +1 -1
- package/dist/remote/local-repo-identity.js +2 -1
- package/dist/remote/notifications-client.d.ts +149 -0
- package/dist/remote/notifications-client.js +103 -0
- package/dist/remote/oauth.d.ts +13 -0
- package/dist/remote/oauth.js +31 -5
- package/dist/remote/output/summary.js +76 -14
- package/dist/remote/render.js +30 -9
- package/dist/remote/uploader.js +5 -4
- package/dist/templates/agents-md.d.ts +1 -1
- package/dist/templates/agents-md.js +6 -6
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.js +2 -2
- package/dist/templates/package-json.d.ts +15 -0
- package/dist/templates/package-json.js +26 -5
- package/dist/test-runner/dry-run.d.ts +2 -1
- package/dist/test-runner/dry-run.js +18 -1
- package/dist/test-runner/event-types.d.ts +2 -0
- package/dist/test-runner/event-types.js +3 -0
- package/dist/test-runner/job-executor.d.ts +1 -0
- package/dist/test-runner/job-executor.js +81 -43
- package/dist/test-runner/output-formatter.d.ts +5 -2
- package/dist/test-runner/output-formatter.js +6 -4
- package/dist/test-runner/payload-builder.d.ts +7 -1
- package/dist/test-runner/payload-builder.js +11 -2
- package/dist/test-runner/rule-evaluator.d.ts +7 -3
- package/dist/test-runner/rule-evaluator.js +13 -12
- package/dist/test-runner/step-context.d.ts +23 -5
- package/dist/test-runner/step-context.js +28 -118
- package/dist/types.d.ts +21 -2
- package/dist/types.js +4 -2
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.js +2 -1
- package/dist/validation/typecheck.d.ts +28 -0
- package/dist/validation/typecheck.js +110 -0
- package/dist/validation/validator.d.ts +9 -4
- package/dist/validation/validator.js +38 -44
- package/package.json +12 -15
- package/sbom.spdx.json +1322 -1964
- package/dist/assets/api-TJJVHrjC.json +0 -118
- package/dist/assets/descriptor-BTtjzN9L.json +0 -1382
- package/dist/assets/package-BpQF9kR8.json +0 -74
- package/dist/assets/package-Ceo2h27X.json +0 -89
- package/dist/assets/source_context-D0atuL28.json +0 -20
- package/dist/assets/type-BFqO8SCZ.json +0 -202
- package/dist/commands/cancel.d.ts +0 -22
- package/dist/commands/detect-package-manager.d.ts +0 -42
- package/dist/commands/held-run-resolve.d.ts +0 -50
- package/dist/commands/status.d.ts +0 -34
- package/dist/commands/test.d.ts +0 -88
- package/dist/errors/codes.d.ts +0 -25
- package/dist/local-executor/dag-scheduler.d.ts +0 -44
- package/dist/local-executor/index.d.ts +0 -23
- package/dist/local-executor/job-runner.d.ts +0 -47
- package/dist/local-executor/materializer.d.ts +0 -44
- package/dist/local-executor/output-streamer.d.ts +0 -31
- package/dist/local-executor/payload-generator.d.ts +0 -16
- package/dist/local-executor/picker.d.ts +0 -33
- package/dist/local-executor/runs-on-display.d.ts +0 -9
- package/dist/local-executor/secret-loader.d.ts +0 -18
- package/dist/local-executor/to-event-payload.d.ts +0 -16
- package/dist/local-executor/types.d.ts +0 -93
- package/dist/local-executor/workflow-lock.d.ts +0 -82
- package/dist/package-F7UXSDHW.json +0 -74
- package/dist/remote/client.d.ts +0 -210
- package/dist/remote/observer.d.ts +0 -81
- package/dist/test-runner/summary.d.ts +0 -7
|
@@ -1,149 +1,6 @@
|
|
|
1
|
-
# KiCI SDK reference
|
|
1
|
+
# KiCI SDK reference: core
|
|
2
2
|
|
|
3
|
-
This bundle covers:
|
|
4
|
-
|
|
5
|
-
## Caching
|
|
6
|
-
|
|
7
|
-
Source: https://docs.kici.dev/user/sdk/caching/
|
|
8
|
-
|
|
9
|
-
KiCI ships a general-purpose cache for any files or directories your workflow produces — compiled artifacts, downloaded toolchains, package manager stores, build outputs. A cache entry is keyed, immutable once written, and shared across runs of the same repository so a later run can restore what an earlier run produced instead of recomputing it.
|
|
10
|
-
|
|
11
|
-
Two surfaces drive the same cache:
|
|
12
|
-
|
|
13
|
-
- **Declarative** — a `cache` field on a job or a step. The runtime restores before the work runs and saves after it succeeds, with no code in your step body.
|
|
14
|
-
- **Imperative** — `ctx.cache.restore(spec)` / `ctx.cache.save(spec)` inside a step body, for fine-grained control over when restore and save happen.
|
|
15
|
-
|
|
16
|
-
The cache is backed by the orchestrator's object storage. Entries are isolated per organization and per ref scope (see [Isolation](https://docs.kici.dev/user/sdk/caching/#isolation)); no other tenant can read your cache, and an untrusted/fork ref can never poison the cache a trusted branch reads.
|
|
17
|
-
|
|
18
|
-
## CacheSpec
|
|
19
|
-
|
|
20
|
-
Both surfaces take the same shape:
|
|
21
|
-
|
|
22
|
-
```typescript
|
|
23
|
-
interface CacheSpec {
|
|
24
|
-
/** Exact cache key. First save wins; re-saving an existing key is a no-op. */
|
|
25
|
-
key: string;
|
|
26
|
-
/** Files/directories to cache. Repo-root-relative or `~`-prefixed. */
|
|
27
|
-
paths: string[];
|
|
28
|
-
/** Ordered prefix fallbacks for partial restore; newest matching entry wins. */
|
|
29
|
-
restoreKeys?: string[];
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
- **`key`** is the exact cache key. It is **immutable** — the first save under a given key wins, and any later save under the same exact key is a no-op (the existing entry is never overwritten). Build keys from inputs that change when the cached content should change, e.g. a hash of your lockfile: `` key: `deps-${await ctx.$`sha256sum pnpm-lock.yaml`}` ``.
|
|
34
|
-
- **`paths`** are the files and directories to archive, repo-root-relative or `~`-prefixed (the agent expands `~` to the workspace home). At least one path is required.
|
|
35
|
-
- **`restoreKeys`** are ordered **prefix** fallbacks tried only when the exact `key` misses on restore. Each prefix is matched against existing entries; the **newest** matching entry wins. This lets a run that changed its lockfile still restore the closest previous cache and rebuild incrementally.
|
|
36
|
-
|
|
37
|
-
## Declarative cache
|
|
38
|
-
|
|
39
|
-
Add a `cache` field to a job or a step. It accepts one `CacheSpec` or an array of them. The runtime restores every spec before the job/step runs (surfaced as a `cache:restore` pseudo-step) and saves every spec after it completes successfully (surfaced as a `cache:save` pseudo-step):
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import { job } from '@kici-dev/sdk';
|
|
43
|
-
|
|
44
|
-
job('build', {
|
|
45
|
-
runsOn: 'linux-x64',
|
|
46
|
-
cache: {
|
|
47
|
-
key: 'mise-tools-v1',
|
|
48
|
-
paths: ['~/.local/share/mise'],
|
|
49
|
-
},
|
|
50
|
-
steps: [
|
|
51
|
-
step('install-tools', async (ctx) => {
|
|
52
|
-
await ctx.$`mise install`;
|
|
53
|
-
}),
|
|
54
|
-
step('build', async (ctx) => {
|
|
55
|
-
await ctx.$`mise exec -- pnpm build`;
|
|
56
|
-
}),
|
|
57
|
-
],
|
|
58
|
-
});
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
Step-level cache scopes the restore/save to a single step:
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
step('deps', {
|
|
65
|
-
cache: { key: `npm-${lockfileHash}`, paths: ['node_modules'], restoreKeys: ['npm-'] },
|
|
66
|
-
run: async (ctx) => {
|
|
67
|
-
await ctx.$`pnpm install --frozen-lockfile`;
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
On a cache **hit**, the archived paths are restored before the step body runs, so `pnpm install` sees a warm `node_modules`. On a **miss**, the step runs cold and the resulting paths are saved under the exact key for the next run.
|
|
73
|
-
|
|
74
|
-
## Imperative cache (`ctx.cache`)
|
|
75
|
-
|
|
76
|
-
When you need to decide at runtime whether to restore or save — for example, save only when a build actually changed something — use the imperative API on the step context:
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
step('build', async (ctx) => {
|
|
80
|
-
const result = await ctx.cache.restore({
|
|
81
|
-
key: `build-${sourceHash}`,
|
|
82
|
-
paths: ['dist'],
|
|
83
|
-
restoreKeys: ['build-'],
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
if (result.hit) {
|
|
87
|
-
ctx.log.info(`restored cache (matched ${result.matchedKey})`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await ctx.$`pnpm build`;
|
|
91
|
-
|
|
92
|
-
await ctx.cache.save({ key: `build-${sourceHash}`, paths: ['dist'] });
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
`restore(spec)` returns `{ hit, matchedKey? }`:
|
|
97
|
-
|
|
98
|
-
- `hit` is `true` when the exact `key` matched **or** a `restoreKeys` prefix matched.
|
|
99
|
-
- `matchedKey` is the full key that actually matched — the exact key on a direct hit, or the full key of the matched prefix entry on a fallback hit.
|
|
100
|
-
|
|
101
|
-
`save(spec)` archives `spec.paths` under `spec.key`. Like the declarative surface, it is immutable: the first save under an exact key wins, and re-saving the same key is a no-op.
|
|
102
|
-
|
|
103
|
-
## Restore semantics
|
|
104
|
-
|
|
105
|
-
A restore resolves in this order:
|
|
106
|
-
|
|
107
|
-
1. **Exact key.** If an entry exists under the exact `key`, it is restored and `matchedKey === key`.
|
|
108
|
-
2. **restoreKeys prefix fallback.** Each `restoreKeys` prefix is tried in order. Within a prefix, the **newest** matching entry wins; `matchedKey` is that entry's full key.
|
|
109
|
-
3. **Miss.** If nothing matches, `hit` is `false` and no paths are restored.
|
|
110
|
-
|
|
111
|
-
This mirrors the familiar lockfile-hash pattern: key the entry on the exact lockfile hash, and add a `restoreKeys` prefix so a changed lockfile still restores the most recent prior cache to rebuild from.
|
|
112
|
-
|
|
113
|
-
## Immutability
|
|
114
|
-
|
|
115
|
-
Cache keys are write-once. The **first** save under an exact key wins; every subsequent save under that same exact key is a no-op and the original bytes are preserved. To publish new content, use a new key (typically by including a content hash in the key). Immutability is what makes a cache hit safe to trust — the bytes behind a given key never change after they are first written.
|
|
116
|
-
|
|
117
|
-
## Isolation
|
|
118
|
-
|
|
119
|
-
Each cache entry is scoped to your organization and to the ref's trust level:
|
|
120
|
-
|
|
121
|
-
- **Trusted refs** (your repository's own branches, default branch) read and write a **shared** scope visible to the whole org for that repository.
|
|
122
|
-
- **Untrusted / fork refs** read the shared scope as a fallback but write to an **isolated** per-run scope. A fork build can therefore benefit from a warm cache the trusted branch produced, but can never write into the shared scope — so a malicious fork cannot poison the cache a trusted branch later restores.
|
|
123
|
-
|
|
124
|
-
No tenant can read another tenant's cache; the org boundary is enforced in the cache key namespace.
|
|
125
|
-
|
|
126
|
-
## Eviction
|
|
127
|
-
|
|
128
|
-
Cache storage is bounded per organization. Two mechanisms keep it bounded:
|
|
129
|
-
|
|
130
|
-
- **Quota** — when a save pushes the org over its byte quota (`KICI_USER_CACHE_QUOTA_BYTES`, default 5 GiB), the oldest entries are evicted until the org is back under quota.
|
|
131
|
-
- **TTL** — entries unused for `KICI_USER_CACHE_TTL_MS` (default 7 days) expire. The TTL refreshes on read (touch-on-read), so an actively used cache stays warm.
|
|
132
|
-
|
|
133
|
-
Both knobs are operator-configured on the orchestrator — see [orchestrator storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/).
|
|
134
|
-
|
|
135
|
-
## Observability
|
|
136
|
-
|
|
137
|
-
Each cache restore and save surfaces in the run timeline as a `cache:restore` / `cache:save` pseudo-step, reporting the outcome (hit/miss/saved, the matched key, bytes). The same outcomes are recorded as `cache.restore` / `cache.save` run events. See [data flows](https://docs.kici.dev/architecture/data-flows/#user-facing-cache-flow) for the restore/save protocol.
|
|
138
|
-
|
|
139
|
-
## See also
|
|
140
|
-
|
|
141
|
-
- [Core](https://docs.kici.dev/user/sdk/core/) -- `job()` / `step()` factories the `cache` field attaches to
|
|
142
|
-
- [Runtime](https://docs.kici.dev/user/sdk/runtime/) -- `StepContext`, where `ctx.cache` lives
|
|
143
|
-
- [Orchestrator storage layout](https://docs.kici.dev/operator/orchestrator/storage-layout/) -- cache prefix, quota, TTL, and eviction
|
|
144
|
-
- [Data flows](https://docs.kici.dev/architecture/data-flows/#user-facing-cache-flow) -- restore/save protocol and trust→scope mapping
|
|
145
|
-
|
|
146
|
-
---
|
|
3
|
+
This bundle covers: Core authoring API: workflow/job/step factories, triggers, rules, matrix, dynamic jobs, cross-job outputs.
|
|
147
4
|
|
|
148
5
|
## SDK reference: core
|
|
149
6
|
|
|
@@ -189,7 +46,7 @@ export default workflow('ci', {
|
|
|
189
46
|
});
|
|
190
47
|
```
|
|
191
48
|
|
|
192
|
-
Secret scoping happens at the job level via `
|
|
49
|
+
Secret scoping happens at the job level via `context` (see [job options](https://docs.kici.dev/user/sdk/core/#jobname-options--joboptions) and [Secrets](https://docs.kici.dev/user/secrets/)) — the workflow itself does not declare which secret contexts it can read.
|
|
193
50
|
|
|
194
51
|
### job(name, options) / job(options)
|
|
195
52
|
|
|
@@ -202,33 +59,34 @@ function job(options: JobOptions): Job;
|
|
|
202
59
|
|
|
203
60
|
**Parameters:**
|
|
204
61
|
|
|
205
|
-
| Parameter | Type
|
|
206
|
-
| -------------------------- |
|
|
207
|
-
| `name` | `string`
|
|
208
|
-
| `options.runsOn` | `RunsOn`
|
|
209
|
-
| `options.steps` | `StepInput[]`
|
|
210
|
-
| `options.run` | `(ctx) => Promise<unknown>`
|
|
211
|
-
| `options.needs` | `NeedsEntry[]`
|
|
212
|
-
| `options.rules` | `Rule[]`
|
|
213
|
-
| `options.description` | `string`
|
|
214
|
-
| `options.matrix` | `Matrix`
|
|
215
|
-
| `options.include` | `MatrixInclude[]`
|
|
216
|
-
| `options.exclude` | `MatrixExclude[]`
|
|
217
|
-
| `options.checkout` | `boolean`
|
|
218
|
-
| `options.container` | `string \| ContainerConfig`
|
|
219
|
-
| `options.
|
|
220
|
-
| `options.
|
|
221
|
-
| `options.
|
|
222
|
-
| `options.
|
|
223
|
-
| `options.
|
|
224
|
-
| `options.
|
|
225
|
-
| `options.
|
|
226
|
-
| `options.
|
|
227
|
-
| `options.
|
|
228
|
-
| `options.
|
|
229
|
-
| `options.
|
|
230
|
-
| `options.
|
|
231
|
-
| `options.
|
|
62
|
+
| Parameter | Type | Required | Description |
|
|
63
|
+
| -------------------------- | ---------------------------------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
64
|
+
| `name` | `string` | no | Job name (auto-generated UUID if omitted) |
|
|
65
|
+
| `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
|
|
66
|
+
| `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
|
|
67
|
+
| `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand). Mutually exclusive with `steps`. |
|
|
68
|
+
| `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs) |
|
|
69
|
+
| `options.rules` | `Rule[]` | no | Conditions for conditional execution |
|
|
70
|
+
| `options.description` | `string` | no | Human-readable description |
|
|
71
|
+
| `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
|
|
72
|
+
| `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
|
|
73
|
+
| `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
|
|
74
|
+
| `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
|
|
75
|
+
| `options.container` | `string \| ContainerConfig` | no | Docker image for job execution. String form is the image name; object form adds `env`. All steps run inside the container. |
|
|
76
|
+
| `options.sandbox` | `{ capabilities?: string[]; network?: 'default' \| 'none' \| 'host' }` | no | Per-job container sandbox escape hatch (container jobs only). Request extra Linux capabilities / host networking; granted only within your operator's allow-list, else the run fails at dispatch. See below. |
|
|
77
|
+
| `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
|
|
78
|
+
| `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
|
|
79
|
+
| `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](https://docs.kici.dev/user/concurrency/). |
|
|
80
|
+
| `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
|
|
81
|
+
| `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
|
|
82
|
+
| `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
|
|
83
|
+
| `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
|
|
84
|
+
| `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
|
|
85
|
+
| `options.afterStep` | `HookInput` | no | Hook that runs after each step |
|
|
86
|
+
| `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](https://docs.kici.dev/user/hooks/#hook-timeout). |
|
|
87
|
+
| `options.timeout` | `number` | no | Total job wall-clock timeout in milliseconds (init + all steps + hooks). On breach the job is aborted and reported timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
|
|
88
|
+
| `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](https://docs.kici.dev/user/sdk/core/#per-job-resources) below. |
|
|
89
|
+
| `options.init` | `InitConfig` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. A generic config, a typed preset (`'mise'` / `{ mise }`), `'auto'`, or `false`. See [Per-job init](https://docs.kici.dev/user/sdk/core/#per-job-init) below. |
|
|
232
90
|
|
|
233
91
|
**Returns:** `Job` -- an immutable job definition.
|
|
234
92
|
|
|
@@ -463,6 +321,29 @@ Per-backend kernel enforcement of `limits`:
|
|
|
463
321
|
- **Firecracker backend:** always enforced. Fractional CPU rounds up to the nearest integer vCPU.
|
|
464
322
|
- **Bare-metal backend:** advisory by default — the scaler caps still apply, but no cgroup is created. Operators can opt in to kernel enforcement via `enforceCgroups: true` on the scaler entry.
|
|
465
323
|
|
|
324
|
+
### Per-job sandbox escape hatch
|
|
325
|
+
|
|
326
|
+
Container-sandbox jobs (a job with a `container:` image) run with all Linux capabilities dropped by default. When a job genuinely needs one extra capability or the host network namespace, request it with `sandbox:`:
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
job('probe', {
|
|
330
|
+
runsOn: 'kici:os:linux',
|
|
331
|
+
container: 'node:20',
|
|
332
|
+
sandbox: {
|
|
333
|
+
capabilities: ['NET_ADMIN'], // added back on top of the dropped-all default
|
|
334
|
+
network: 'host', // share the host network namespace
|
|
335
|
+
},
|
|
336
|
+
steps: [/* ... */],
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Two levers are exposed, and only these two are ever grantable:
|
|
341
|
+
|
|
342
|
+
- **`capabilities`** — extra Linux capabilities (bare or `CAP_`-prefixed; both accepted). An unknown capability name is rejected when you compile.
|
|
343
|
+
- **`network`** — `'default'` (bridge, the default), `'none'` (loopback only), or `'host'`.
|
|
344
|
+
|
|
345
|
+
Every request is checked at dispatch against an allow-list your orchestrator operator controls. A capability or `network: 'host'` that the operator has not allow-listed **fails the whole run** with a reason naming what was blocked — the request is never silently ignored. `'none'` and the default bridge never need approval. Ask your operator to allow-list a capability (`kici-admin org-settings sandbox-allowlist`) before relying on it. A job with no `sandbox:` field keeps the fully hardened default.
|
|
346
|
+
|
|
466
347
|
### Per-job init
|
|
467
348
|
|
|
468
349
|
`options.init` declares a hand-written command that runs **after the repo is cloned and before the job's steps execute**. Its purpose is to provision a repo-declared toolchain (a `mise` toolchain, a custom setup script, a language runtime) and put it on the environment every subsequent step sees.
|
|
@@ -757,6 +638,25 @@ const pipeline = job('pipeline', {
|
|
|
757
638
|
});
|
|
758
639
|
```
|
|
759
640
|
|
|
641
|
+
The `.result` proxy works for id-less steps too. An id-less step (`step(fn)` or `step({ run, outputs })` without a name) receives a deterministic `step-N` name when the job's steps are enumerated at execution start — before any step runs — and its `.result` resolves under that assigned name from any later step's `run` function:
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
const build = step({ outputs: { version: z.string() }, run: async () => ({ version: '2.0.0' }) });
|
|
645
|
+
|
|
646
|
+
const pipeline = job('pipeline', {
|
|
647
|
+
runsOn: 'default',
|
|
648
|
+
steps: [
|
|
649
|
+
build,
|
|
650
|
+
step(async () => {
|
|
651
|
+
const version = build.result.version; // resolves under the assigned step-N name
|
|
652
|
+
console.log(version); // '2.0.0'
|
|
653
|
+
}),
|
|
654
|
+
],
|
|
655
|
+
});
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Reference `.result` only from inside another step's `run` function. Accessing it outside execution (for example at module top level, before names are assigned) raises "this step has no name yet".
|
|
659
|
+
|
|
760
660
|
**Cross-job output chaining:**
|
|
761
661
|
|
|
762
662
|
```typescript
|
|
@@ -771,8 +671,13 @@ const build = job('build', {
|
|
|
771
671
|
runsOn: 'default',
|
|
772
672
|
needs: [setup],
|
|
773
673
|
steps: [
|
|
774
|
-
step(
|
|
775
|
-
|
|
674
|
+
// The options form `step(name, { run })` carries the return type through, so
|
|
675
|
+
// `build.result.compile.version` is typed `string`. A bare `step(name, fn)`
|
|
676
|
+
// is a void step and contributes no typed key.
|
|
677
|
+
step('compile', {
|
|
678
|
+
run: async (ctx) => {
|
|
679
|
+
return { version: '2.0.0' };
|
|
680
|
+
},
|
|
776
681
|
}),
|
|
777
682
|
],
|
|
778
683
|
});
|
|
@@ -782,19 +687,43 @@ const deploy = job('deploy', {
|
|
|
782
687
|
needs: [build],
|
|
783
688
|
steps: [
|
|
784
689
|
step(async (ctx) => {
|
|
785
|
-
// Multi-step job: jobRef.result.stepName.field
|
|
690
|
+
// Multi-step job: jobRef.result.stepName.field — typed `string`, a typo on
|
|
691
|
+
// `.version` or `.compile` is a compile error.
|
|
786
692
|
const version = build.result.compile.version;
|
|
787
693
|
|
|
788
|
-
// Single-step job (run shorthand): jobRef.result.field
|
|
694
|
+
// Single-step job (run shorthand): jobRef.result.field — typed `string`.
|
|
789
695
|
const env = setup.result.env;
|
|
790
696
|
|
|
791
|
-
// Explicit context method
|
|
697
|
+
// Explicit context method — typed to the job's output shape for a Job ref.
|
|
792
698
|
const buildOutputs = ctx.jobOutputs(build);
|
|
793
699
|
}),
|
|
794
700
|
],
|
|
795
701
|
});
|
|
796
702
|
```
|
|
797
703
|
|
|
704
|
+
**Cross-job outputs are typed.** When you pass a **job reference** (not a string)
|
|
705
|
+
in `needs` and read `jobRef.result.…` or `ctx.jobOutputs(jobRef)`, the output
|
|
706
|
+
types thread across the job boundary — a typo on an output field or a renamed
|
|
707
|
+
step is a compile error, the same guarantee you get within a job. Two authoring
|
|
708
|
+
rules unlock it:
|
|
709
|
+
|
|
710
|
+
- **Name your steps and use the options form** — `step('name', { run })` carries
|
|
711
|
+
the return type into `jobRef.result.name.field` (nested by step name for
|
|
712
|
+
multi-step jobs; flat `jobRef.result.field` for the `run:` shorthand). A bare
|
|
713
|
+
`step('name', fn)` or an id-less `step(fn)` is a void step and contributes no
|
|
714
|
+
typed key.
|
|
715
|
+
- **Pass references, not strings** — reading `buildJob.result.…` on the job
|
|
716
|
+
reference is typed from any job. In a `run:` shorthand job, a referenced
|
|
717
|
+
`needs: [buildJob]` also types `ctx.needs.buildJob.result.…` (the run
|
|
718
|
+
function's `ctx` derives from the job's `needs` tuple). A string
|
|
719
|
+
`needs: ['build']` still works but stays loosely typed
|
|
720
|
+
(`Record<string, unknown>`).
|
|
721
|
+
|
|
722
|
+
For a dynamically-shaped job whose outputs the inference can't reproduce, supply
|
|
723
|
+
the shape explicitly: `job<{ url: string }>('deploy', { … })`. A **matrix** or
|
|
724
|
+
`runsOnAll` upstream returns a typed envelope — discriminate it with
|
|
725
|
+
`isMatrixJobOutputs` / `isHostJobOutputs`.
|
|
726
|
+
|
|
798
727
|
**Access patterns summary:**
|
|
799
728
|
|
|
800
729
|
| Pattern | Scope | Notes |
|
|
@@ -835,14 +764,20 @@ const deploy = job('deploy', {
|
|
|
835
764
|
|
|
836
765
|
**Run condition (`when`):** controls when a downstream edge is satisfied, based on the upstream's terminal status. `when` is keyword sugar (or a raw status-set) that resolves at compile time to the set of upstream terminal statuses that satisfy the edge. The downstream edge is satisfied when the upstream's terminal status is a member of that set.
|
|
837
766
|
|
|
838
|
-
| Keyword | Satisfied when the upstream is…
|
|
839
|
-
| ------------------------ |
|
|
840
|
-
| `'on-success'` (default) | `success`
|
|
841
|
-
| `'always'` | any terminal status
|
|
842
|
-
| `'on-skip'` | `success` or `skipped`
|
|
843
|
-
| `'on-failure'` | `failed` or `
|
|
767
|
+
| Keyword | Satisfied when the upstream is… | Use for |
|
|
768
|
+
| ------------------------ | ------------------------------------------------------------- | ------------------------------------------- |
|
|
769
|
+
| `'on-success'` (default) | `success` | normal dependencies |
|
|
770
|
+
| `'always'` | any terminal status | cleanup / notification / teardown jobs |
|
|
771
|
+
| `'on-skip'` | `success` or `skipped` | continue when an upstream was narrowed out |
|
|
772
|
+
| `'on-failure'` | `failed`, `timed_out_stale`, `drift_dropped`, or `unroutable` | error-handler jobs that run only on failure |
|
|
844
773
|
|
|
845
|
-
|
|
774
|
+
`'on-failure'` covers every terminal status that means the job did not do what
|
|
775
|
+
the workflow declared, which includes a job dropped by determinism drift and a
|
|
776
|
+
job whose `runsOn` matched no agent. It
|
|
777
|
+
does **not** cover `cancelled` (deliberately stopped) or `skipped` (never ran) —
|
|
778
|
+
use a raw status set if you need those.
|
|
779
|
+
|
|
780
|
+
For full control, pass a raw status-set instead of a keyword: `when: ['skipped', 'failed', 'timed_out_stale']`. The valid members are the terminal job statuses: `success`, `failed`, `cancelled`, `skipped`, `timed_out_stale`, `drift_dropped`, `unroutable`.
|
|
846
781
|
|
|
847
782
|
String and `Job`-reference entries default to `when: 'on-success'`. To override, use the object form (`{ name, when }` for static upstreams, `{ group, when }` for dynamic groups -- `dynamicGroup(name, { when: 'always' })` produces the latter).
|
|
848
783
|
|
|
@@ -884,10 +819,11 @@ function dynamicGroup(
|
|
|
884
819
|
Use when a static downstream must wait for every generated job tagged with a given group name to complete. If the dynamic group produces zero jobs, the downstream dispatches immediately (empty group satisfies all upstreams).
|
|
885
820
|
|
|
886
821
|
```typescript
|
|
887
|
-
const shardedTests = dynamicJob('test-shards', async (
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
822
|
+
const shardedTests = dynamicJob('test-shards', async () => {
|
|
823
|
+
// Decide the shard set however you like — a constant, the event payload, or
|
|
824
|
+
// an upstream job's outputs via the result-aware `{ needs, generate }` form.
|
|
825
|
+
const shards = [0, 1, 2, 3];
|
|
826
|
+
return shards.map((i) => job(`test-shard-${i}`, { runsOn: 'linux', run: async () => {} }));
|
|
891
827
|
});
|
|
892
828
|
|
|
893
829
|
const deploy = job('deploy', {
|
|
@@ -899,14 +835,19 @@ const deploy = job('deploy', {
|
|
|
899
835
|
});
|
|
900
836
|
```
|
|
901
837
|
|
|
902
|
-
### dynamicJob(groupName,
|
|
838
|
+
### dynamicJob(groupName, fnOrConfig)
|
|
903
839
|
|
|
904
|
-
Tag a dynamic job generator
|
|
840
|
+
Tag a dynamic job generator with a group name so other jobs can reference it via `dynamicGroup()`.
|
|
905
841
|
|
|
906
842
|
```typescript
|
|
907
|
-
function dynamicJob(
|
|
843
|
+
function dynamicJob(
|
|
844
|
+
groupName: string,
|
|
845
|
+
fnOrConfig: DynamicJobFn | { needs: DynamicJobNeed[]; generate: DynamicJobFn },
|
|
846
|
+
): TaggedDynamicJobFn;
|
|
908
847
|
```
|
|
909
848
|
|
|
849
|
+
The second argument is either a plain generator (event-only, evaluated at webhook time) or a result-aware `{ needs, generate }` config, which defers the generator until its declared upstreams complete and exposes their frozen outputs as `ctx.needs`. See [Rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/#dynamicjob--result-aware-generation) for the result-aware form.
|
|
850
|
+
|
|
910
851
|
The generator runs twice: once in the init phase (to register expected job names) and once inside the executing agent (to produce the actual jobs). Mismatches between the two evaluations are detected as determinism drift -- see [dynamic-jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/).
|
|
911
852
|
|
|
912
853
|
### Auto-generated IDs
|
|
@@ -920,2012 +861,537 @@ These IDs are stable as long as the order of unnamed entries does not change. Ad
|
|
|
920
861
|
|
|
921
862
|
---
|
|
922
863
|
|
|
923
|
-
##
|
|
864
|
+
## SDK reference: parallel
|
|
924
865
|
|
|
925
|
-
Source: https://docs.kici.dev/user/sdk/
|
|
866
|
+
Source: https://docs.kici.dev/user/sdk/parallel/
|
|
926
867
|
|
|
927
|
-
|
|
868
|
+
`parallel([...steps], opts?)` runs a group of independent steps **concurrently**
|
|
869
|
+
within one job, behind a join barrier: execution continues past the group only
|
|
870
|
+
once every child has settled. Each child is its own observable step — it gets its
|
|
871
|
+
own logs, status, timing, and retry — instead of being hidden inside one step's
|
|
872
|
+
`Promise.all`.
|
|
928
873
|
|
|
929
|
-
|
|
874
|
+
```ts
|
|
875
|
+
import { workflow, job, step, parallel, push } from '@kici-dev/sdk';
|
|
930
876
|
|
|
931
|
-
|
|
877
|
+
export default workflow('ci', {
|
|
878
|
+
on: push(),
|
|
879
|
+
jobs: [
|
|
880
|
+
job('checks', {
|
|
881
|
+
runsOn: 'kici:os:linux',
|
|
882
|
+
steps: [
|
|
883
|
+
checkout,
|
|
884
|
+
// lint, typecheck, and the unit tests have no ordering between them,
|
|
885
|
+
// so they run together — the job's wall-clock is the slowest child,
|
|
886
|
+
// not the sum of all three.
|
|
887
|
+
parallel([lint, typecheck, unitTests], { failFast: true }),
|
|
888
|
+
deploy,
|
|
889
|
+
],
|
|
890
|
+
}),
|
|
891
|
+
],
|
|
892
|
+
});
|
|
893
|
+
```
|
|
932
894
|
|
|
933
|
-
|
|
895
|
+
`parallel(...)` returns a `ParallelGroup` that sits in the ordinary flat
|
|
896
|
+
`steps: [...]` array — there is no new `job` field. A group's children are
|
|
897
|
+
**sequential steps only**; groups cannot be nested.
|
|
934
898
|
|
|
935
|
-
|
|
899
|
+
## Options
|
|
936
900
|
|
|
937
|
-
|
|
938
|
-
| ----------------- | ------------------------- | ------------------------------------------------------------------------------- |
|
|
939
|
-
| `type` | `string` | Normalized event type discriminant. |
|
|
940
|
-
| `action?` | `string` | Sub-action (e.g. 'opened', 'created', 'submitted'). |
|
|
941
|
-
| `targetBranch?` | `string` | Target branch (push target, PR base, or default branch). |
|
|
942
|
-
| `sourceBranch?` | `string` | Source branch (PR head branch). Only set for PR-like events. |
|
|
943
|
-
| `provider?` | `string` | Provider that originated this event. |
|
|
944
|
-
| `isForkPR?` | `boolean` | Whether this PR comes from a fork. Only set for PR-like events. |
|
|
945
|
-
| `baseBranch?` | `string` | Base branch ref for PR events. |
|
|
946
|
-
| `senderUsername?` | `string` | Sender username from the webhook payload. |
|
|
947
|
-
| `sourceRepo?` | `string` | Repository identifier (e.g. "owner/repo"). |
|
|
948
|
-
| `changedFiles?` | `string[]` | Files changed in this event (for path filtering). |
|
|
949
|
-
| `payload?` | `Record<string, unknown>` | Raw webhook payload from the provider. May be absent in flattened event forms. |
|
|
950
|
-
| `[key: string]` | `unknown` | Index signature for backward compatibility — untyped fields resolve to unknown. |
|
|
901
|
+
`parallel(steps, opts?)` accepts:
|
|
951
902
|
|
|
952
|
-
|
|
903
|
+
- **`failFast?: boolean`** — default `true`. When a child fails, the in-flight
|
|
904
|
+
siblings are cancelled immediately and the job fails. With `failFast: false`
|
|
905
|
+
every child runs to completion first, then the job fails if any child failed.
|
|
906
|
+
- **`maxParallel?: number`** — default unlimited. Caps how many children run at
|
|
907
|
+
once; children waiting for a slot report a `pending` status until they launch.
|
|
908
|
+
- **`name?: string`** — a label for the group's dashboard band.
|
|
953
909
|
|
|
954
|
-
|
|
910
|
+
A child marked `continueOnError: true` never trips fail-fast and never fails the
|
|
911
|
+
job — it still shows a `failed` status badge, but the group treats it as
|
|
912
|
+
non-fatal.
|
|
955
913
|
|
|
956
|
-
|
|
914
|
+
## Statuses
|
|
957
915
|
|
|
958
|
-
|
|
916
|
+
Parallel steps introduce two step statuses:
|
|
959
917
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
| `pull_request` | `GitHubPullRequest` | |
|
|
965
|
-
| `repository` | `GitHubRepository` | |
|
|
966
|
-
| `sender` | `GitHubUser` | |
|
|
967
|
-
| `[key: string]` | `unknown` | |
|
|
918
|
+
- **`pending`** — a child queued behind `maxParallel`, not yet launched.
|
|
919
|
+
- **`cancelled`** — a sibling aborted by fail-fast. A cancelled step is **not** a
|
|
920
|
+
failure: only the child that actually failed fails the job; the cancelled
|
|
921
|
+
siblings render in gray (distinct from the red failing step) on the dashboard.
|
|
968
922
|
|
|
969
|
-
|
|
923
|
+
Children may also complete **out of order** — the fastest child finishes first
|
|
924
|
+
regardless of array position. A later sequential step can read a parallel child's
|
|
925
|
+
`.result` after the barrier; children within a group cannot read each other's
|
|
926
|
+
results (there is no ordering inside the group).
|
|
970
927
|
|
|
971
|
-
|
|
928
|
+
## Scope: nests inside job-level fan-out
|
|
972
929
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
| `head_commit?` | `GitHubCommit` | |
|
|
979
|
-
| `commits?` | `GitHubCommit[]` | |
|
|
980
|
-
| `repository` | `GitHubRepository` | |
|
|
981
|
-
| `sender?` | `GitHubUser` | |
|
|
982
|
-
| `forced?` | `boolean` | |
|
|
983
|
-
| `[key: string]` | `unknown` | |
|
|
930
|
+
`parallel()`'s `failFast` / `maxParallel` are **step-group** scopes — they govern
|
|
931
|
+
only the steps inside the group. They are a different layer from the **job-level**
|
|
932
|
+
`failFast` / `maxParallel` on a matrix / `runsOnAll` fan-out, which govern how a
|
|
933
|
+
job's child _jobs_ spread across the matrix or host roster. A `parallel()` group
|
|
934
|
+
inside a fan-out job nests its concurrency inside each fan-out child.
|
|
984
935
|
|
|
985
|
-
|
|
936
|
+
## Local vs remote execution
|
|
986
937
|
|
|
987
|
-
|
|
938
|
+
Run remotely (the orchestrator + agent), parallel children execute concurrently
|
|
939
|
+
and each surfaces as its own dashboard step. `kici run <event> --local` executes the same
|
|
940
|
+
children in array order in its single-process model — the results are identical,
|
|
941
|
+
only the wall-clock and the live fail-fast cancellation differ. Use a remote run
|
|
942
|
+
to observe the concurrent timeline.
|
|
988
943
|
|
|
989
|
-
|
|
990
|
-
| --------------- | ------------------ | ----------- |
|
|
991
|
-
| `ref` | `string` | |
|
|
992
|
-
| `after` | `string` | |
|
|
993
|
-
| `repository` | `GitHubRepository` | |
|
|
994
|
-
| `sender?` | `GitHubUser` | |
|
|
995
|
-
| `[key: string]` | `unknown` | |
|
|
944
|
+
---
|
|
996
945
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
Carried by `CommentEventPayload`. The `payload` property has the following shape:
|
|
946
|
+
## SDK reference: rules, matrix, dynamic jobs
|
|
1000
947
|
|
|
1001
|
-
|
|
1002
|
-
| --------------- | ------------------------------------------------------------------------------------ | ----------- |
|
|
1003
|
-
| `action` | `string` | |
|
|
1004
|
-
| `comment` | `GitHubComment` | |
|
|
1005
|
-
| `issue?` | `{ number: number; title?: string; pull_request?: unknown; [key: string]: unknown }` | |
|
|
1006
|
-
| `repository` | `GitHubRepository` | |
|
|
1007
|
-
| `sender` | `GitHubUser` | |
|
|
1008
|
-
| `[key: string]` | `unknown` | |
|
|
1009
|
-
|
|
1010
|
-
### `review`
|
|
948
|
+
Source: https://docs.kici.dev/user/sdk/rules-matrix-dynamic/
|
|
1011
949
|
|
|
1012
|
-
|
|
950
|
+
## Rules
|
|
1013
951
|
|
|
1014
|
-
|
|
1015
|
-
| --------------- | ------------------- | ----------- |
|
|
1016
|
-
| `action` | `string` | |
|
|
1017
|
-
| `review` | `GitHubReview` | |
|
|
1018
|
-
| `pull_request` | `GitHubPullRequest` | |
|
|
1019
|
-
| `repository` | `GitHubRepository` | |
|
|
1020
|
-
| `sender` | `GitHubUser` | |
|
|
1021
|
-
| `[key: string]` | `unknown` | |
|
|
952
|
+
Rules control conditional execution of workflows and jobs. A rule that returns `false` (or whose check function returns `false`) prevents execution.
|
|
1022
953
|
|
|
1023
|
-
|
|
954
|
+
A rule's check function that **returns `false`** cleanly skips the job or step. A check function that **throws** is treated as an evaluation failure, not a skip: the job or step **fails** with the error surfaced (both on a remote run and when running locally with `kici run --local`), so a broken rule can never silently pass as a green run. For example, `rule('main only', (ctx) => ctx.event.ref.endsWith('main'))` throws on an event whose `ref` is undefined — that run fails with the error instead of quietly skipping every step. Fix the thrown error (guard the access) rather than relying on the skip.
|
|
1024
955
|
|
|
1025
|
-
|
|
956
|
+
### rule(label) / rule(label, check)
|
|
1026
957
|
|
|
1027
|
-
|
|
1028
|
-
| --------------- | ------------------- | ----------- |
|
|
1029
|
-
| `action` | `string` | |
|
|
1030
|
-
| `comment` | `GitHubComment` | |
|
|
1031
|
-
| `pull_request` | `GitHubPullRequest` | |
|
|
1032
|
-
| `repository` | `GitHubRepository` | |
|
|
1033
|
-
| `sender` | `GitHubUser` | |
|
|
1034
|
-
| `[key: string]` | `unknown` | |
|
|
1035
|
-
|
|
1036
|
-
### `release`
|
|
1037
|
-
|
|
1038
|
-
Carried by `ReleaseEventPayload`. The `payload` property has the following shape:
|
|
1039
|
-
|
|
1040
|
-
| Field | Type | Description |
|
|
1041
|
-
| --------------- | ------------------ | ----------- |
|
|
1042
|
-
| `action` | `string` | |
|
|
1043
|
-
| `release` | `GitHubRelease` | |
|
|
1044
|
-
| `repository` | `GitHubRepository` | |
|
|
1045
|
-
| `sender` | `GitHubUser` | |
|
|
1046
|
-
| `[key: string]` | `unknown` | |
|
|
1047
|
-
|
|
1048
|
-
### `dispatch`
|
|
958
|
+
Create a rule.
|
|
1049
959
|
|
|
1050
|
-
|
|
960
|
+
```typescript
|
|
961
|
+
function rule(label: string): Rule;
|
|
962
|
+
function rule(label: string, check: RuleCheckFn): Rule;
|
|
963
|
+
```
|
|
1051
964
|
|
|
1052
|
-
|
|
1053
|
-
| ----------------- | ------------------------- | ----------- |
|
|
1054
|
-
| `action` | `string` | |
|
|
1055
|
-
| `client_payload?` | `Record<string, unknown>` | |
|
|
1056
|
-
| `repository` | `GitHubRepository` | |
|
|
1057
|
-
| `sender?` | `GitHubUser` | |
|
|
1058
|
-
| `[key: string]` | `unknown` | |
|
|
965
|
+
**Without check function:** Always passes. Useful as a marker in the decision trace.
|
|
1059
966
|
|
|
1060
|
-
|
|
967
|
+
```typescript
|
|
968
|
+
rule('ci: required check');
|
|
969
|
+
```
|
|
1061
970
|
|
|
1062
|
-
|
|
971
|
+
**With check function:** Passes when the function returns `true`.
|
|
1063
972
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
| `sender` | `GitHubUser` | |
|
|
1070
|
-
| `[key: string]` | `unknown` | |
|
|
973
|
+
```typescript
|
|
974
|
+
rule('has source changes', async (ctx) => {
|
|
975
|
+
return ctx.changedFiles.some((f) => f.startsWith('src/'));
|
|
976
|
+
});
|
|
977
|
+
```
|
|
1071
978
|
|
|
1072
|
-
###
|
|
979
|
+
### skip(label, check)
|
|
1073
980
|
|
|
1074
|
-
|
|
981
|
+
Create a rule that skips when the condition is met. Inverts the check function.
|
|
1075
982
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
| `ref_type` | `string` | |
|
|
1080
|
-
| `repository` | `GitHubRepository` | |
|
|
1081
|
-
| `sender` | `GitHubUser` | |
|
|
1082
|
-
| `[key: string]` | `unknown` | |
|
|
983
|
+
```typescript
|
|
984
|
+
function skip(label: string, check: RuleCheckFn): Rule;
|
|
985
|
+
```
|
|
1083
986
|
|
|
1084
|
-
|
|
987
|
+
When the check returns `true` (condition met), the rule returns `false` (skip execution).
|
|
988
|
+
When the check returns `false` (condition not met), the rule returns `true` (allow execution).
|
|
1085
989
|
|
|
1086
|
-
|
|
990
|
+
```typescript
|
|
991
|
+
// Skip when only docs changed
|
|
992
|
+
skip('docs only PR', async (ctx) => {
|
|
993
|
+
return ctx.changedFiles.every((f) => f.endsWith('.md'));
|
|
994
|
+
});
|
|
995
|
+
```
|
|
1087
996
|
|
|
1088
|
-
|
|
1089
|
-
| --------------- | ------------------------------------------------- | ----------- |
|
|
1090
|
-
| `state` | `string` | |
|
|
1091
|
-
| `sha` | `string` | |
|
|
1092
|
-
| `context` | `string` | |
|
|
1093
|
-
| `description?` | `string` | |
|
|
1094
|
-
| `target_url?` | `string` | |
|
|
1095
|
-
| `branches?` | `Array<{ name: string; [key: string]: unknown }>` | |
|
|
1096
|
-
| `repository` | `GitHubRepository` | |
|
|
1097
|
-
| `sender` | `GitHubUser` | |
|
|
1098
|
-
| `[key: string]` | `unknown` | |
|
|
997
|
+
### RuleCheckFn
|
|
1099
998
|
|
|
1100
|
-
|
|
999
|
+
```typescript
|
|
1000
|
+
type RuleCheckFn = (ctx: RuleContext) => Promise<boolean> | boolean;
|
|
1001
|
+
```
|
|
1101
1002
|
|
|
1102
|
-
|
|
1003
|
+
Can be sync or async. Receives a `RuleContext`:
|
|
1103
1004
|
|
|
1104
|
-
|
|
|
1105
|
-
|
|
|
1106
|
-
| `
|
|
1107
|
-
| `
|
|
1108
|
-
| `
|
|
1109
|
-
| `
|
|
1110
|
-
|
|
|
1005
|
+
| Property | Type | Description |
|
|
1006
|
+
| -------------------- | ----------------------------------------- | --------------------------------------------------------------------- |
|
|
1007
|
+
| `event` | `EventPayload` | The triggering event payload (discriminated union — narrow on `type`) |
|
|
1008
|
+
| `changedFiles` | `string[]` | Files changed in this event (see availability note below) |
|
|
1009
|
+
| `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` is available |
|
|
1010
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
1011
|
+
| `$` | zx shell | Shell executor for running commands |
|
|
1111
1012
|
|
|
1112
|
-
|
|
1013
|
+
`changedFiles` is available on `push` and `pull_request` events — the agent computes the diff from its checkout, so no `paths:` trigger is required. It is `unavailable` for events with no diff (`schedule`, `tag`, `manual_schedule`), and in the rare case where the diff cannot be computed (e.g. a history deeper than the agent's bounded fetch). Reading `changedFiles` when it is unavailable throws and fails the job, so guard with `changedFilesStatus` first when a rule can run on such events:
|
|
1113
1014
|
|
|
1114
|
-
|
|
1015
|
+
```typescript
|
|
1016
|
+
rule('has source changes', (ctx) => {
|
|
1017
|
+
if (ctx.changedFilesStatus !== 'fetched') return true; // no diff — don't gate on files
|
|
1018
|
+
return ctx.changedFiles.some((f) => f.startsWith('src/'));
|
|
1019
|
+
});
|
|
1020
|
+
```
|
|
1115
1021
|
|
|
1116
|
-
|
|
1117
|
-
| --------------- | ----------------------------------------------- | ----------- |
|
|
1118
|
-
| `forkee` | `{ full_name: string; [key: string]: unknown }` | |
|
|
1119
|
-
| `repository` | `GitHubRepository` | |
|
|
1120
|
-
| `sender` | `GitHubUser` | |
|
|
1121
|
-
| `[key: string]` | `unknown` | |
|
|
1022
|
+
### evaluateRules(rules, context, label, onRuleResult?)
|
|
1122
1023
|
|
|
1123
|
-
|
|
1024
|
+
Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
|
|
1124
1025
|
|
|
1125
|
-
|
|
1026
|
+
```typescript
|
|
1027
|
+
function evaluateRules(
|
|
1028
|
+
rules: Rule[],
|
|
1029
|
+
context: RuleContext,
|
|
1030
|
+
label: string,
|
|
1031
|
+
onRuleResult?: (result: RuleResult) => void,
|
|
1032
|
+
): Promise<RuleEvaluationResult>;
|
|
1033
|
+
```
|
|
1126
1034
|
|
|
1127
|
-
|
|
1128
|
-
| --------------- | ------------------ | ----------- |
|
|
1129
|
-
| `action` | `string` | |
|
|
1130
|
-
| `repository` | `GitHubRepository` | |
|
|
1131
|
-
| `sender` | `GitHubUser` | |
|
|
1132
|
-
| `[key: string]` | `unknown` | |
|
|
1035
|
+
Returns a `RuleEvaluationResult`:
|
|
1133
1036
|
|
|
1134
|
-
|
|
1037
|
+
```typescript
|
|
1038
|
+
interface RuleEvaluationResult {
|
|
1039
|
+
allPassed: boolean;
|
|
1040
|
+
results: RuleResult[];
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1135
1043
|
|
|
1136
|
-
|
|
1044
|
+
### isEventType(event, type)
|
|
1137
1045
|
|
|
1138
|
-
|
|
1139
|
-
| --------------- | ------------------ | ----------- |
|
|
1140
|
-
| `action` | `string` | |
|
|
1141
|
-
| `repository` | `GitHubRepository` | |
|
|
1142
|
-
| `sender` | `GitHubUser` | |
|
|
1143
|
-
| `[key: string]` | `unknown` | |
|
|
1046
|
+
Type guard that narrows an `EventPayload` to a specific event type variant. Use this in rule check functions to get autocomplete on provider-specific fields.
|
|
1144
1047
|
|
|
1145
|
-
|
|
1048
|
+
```typescript
|
|
1049
|
+
function isEventType<T extends EventPayload['type']>(
|
|
1050
|
+
event: EventPayload,
|
|
1051
|
+
type: T,
|
|
1052
|
+
): event is Extract<EventPayload, { type: T }>;
|
|
1053
|
+
```
|
|
1146
1054
|
|
|
1147
|
-
|
|
1055
|
+
**Example — skip draft PRs:**
|
|
1148
1056
|
|
|
1149
|
-
|
|
1057
|
+
```typescript
|
|
1058
|
+
rule('skip-draft-prs', (ctx) => {
|
|
1059
|
+
if (!isEventType(ctx.event, 'pull_request')) return true;
|
|
1060
|
+
// ctx.event is now PullRequestEventPayload — full autocomplete
|
|
1061
|
+
return !ctx.event.payload.pull_request.draft;
|
|
1062
|
+
});
|
|
1063
|
+
```
|
|
1150
1064
|
|
|
1151
|
-
|
|
1065
|
+
**Example — branch-based rule with push narrowing:**
|
|
1152
1066
|
|
|
1153
|
-
|
|
1067
|
+
```typescript
|
|
1068
|
+
rule('only-main-pushes', (ctx) => {
|
|
1069
|
+
if (!isEventType(ctx.event, 'push')) return false;
|
|
1070
|
+
// ctx.event.payload.ref is typed as string
|
|
1071
|
+
return ctx.event.payload.ref === 'refs/heads/main';
|
|
1072
|
+
});
|
|
1073
|
+
```
|
|
1154
1074
|
|
|
1155
|
-
|
|
1075
|
+
You can also narrow directly with `if (ctx.event.type === 'pull_request')` — TypeScript's discriminated union narrowing works on the `type` field.
|
|
1156
1076
|
|
|
1157
|
-
###
|
|
1077
|
+
### EventPayload
|
|
1158
1078
|
|
|
1159
|
-
|
|
1079
|
+
`EventPayload` is a discriminated union over the `type` field. Each variant provides typed access to the normalized event fields and the raw webhook payload.
|
|
1160
1080
|
|
|
1161
|
-
|
|
1081
|
+
Every variant carries the shared `EventBase` fields — `type`, `action`, `targetBranch`, `sourceBranch`, `provider`, `isForkPR`, `baseBranch`, `senderUsername`, `sourceRepo`, `changedFiles`, and the raw `payload` — plus a per-type `payload` shape for the typed variants. The complete field-by-field schema, including every typed `payload` shape and the shared GitHub object types, is in the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/).
|
|
1162
1082
|
|
|
1163
|
-
|
|
1083
|
+
**Typed variants** (with GitHub-specific payload fields): `pull_request`, `push`, `tag`, `comment`, `review`, `review_comment`, `release`, `dispatch`, `create`, `delete`, `status`, `workflow_run`, `fork`, `star`, `watch`.
|
|
1164
1084
|
|
|
1165
|
-
|
|
1085
|
+
**Generic variants** (payload is `Record<string, unknown>`): `webhook`, `kici_event`, `workflow_complete`, `job_complete`, `generic_webhook`, `schedule`, `lifecycle`.
|
|
1166
1086
|
|
|
1167
|
-
|
|
1087
|
+
## Matrix
|
|
1168
1088
|
|
|
1169
|
-
|
|
1089
|
+
Matrix configurations expand a single job into multiple instances, one per parameter combination. Maximum 256 combinations.
|
|
1170
1090
|
|
|
1171
|
-
|
|
1091
|
+
Combinations must be **unique**. Two combinations that would produce the same instance name — most simply, the same value listed twice — fail the job instead of quietly running it twice.
|
|
1172
1092
|
|
|
1173
|
-
|
|
1093
|
+
Expansion happens at **dispatch time**: the orchestrator materializes the matrix into N execution jobs — one per combination, each dispatched to its own agent — before any job runs. Each instance receives its combination as `ctx.matrix`. This is identical whether the workflow runs via `kici run <event> --local` or remotely through a webhook trigger, and the dashboard groups the N instances under one parent node.
|
|
1174
1094
|
|
|
1175
|
-
|
|
1095
|
+
### Static array (single dimension)
|
|
1176
1096
|
|
|
1177
|
-
|
|
1097
|
+
```typescript
|
|
1098
|
+
matrix: ['18', '20', '22'];
|
|
1099
|
+
```
|
|
1178
1100
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
### `unknown`
|
|
1182
|
-
|
|
1183
|
-
Carried by `UnknownEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
1184
|
-
|
|
1185
|
-
## Shared GitHub object shapes
|
|
1186
|
-
|
|
1187
|
-
The typed `payload` shapes above reference these partial GitHub object types. Each lists only the commonly accessed fields; the index signature on every shape resolves any other field to `unknown`.
|
|
1188
|
-
|
|
1189
|
-
### `GitHubRepository`
|
|
1190
|
-
|
|
1191
|
-
| Field | Type | Description |
|
|
1192
|
-
| ---------------- | ------------------------------------------- | ----------- |
|
|
1193
|
-
| `full_name` | `string` | |
|
|
1194
|
-
| `default_branch` | `string` | |
|
|
1195
|
-
| `name?` | `string` | |
|
|
1196
|
-
| `owner?` | `{ login: string; [key: string]: unknown }` | |
|
|
1197
|
-
| `private?` | `boolean` | |
|
|
1198
|
-
| `[key: string]` | `unknown` | |
|
|
1199
|
-
|
|
1200
|
-
### `GitHubUser`
|
|
1201
|
-
|
|
1202
|
-
| Field | Type | Description |
|
|
1203
|
-
| --------------- | --------- | ----------- |
|
|
1204
|
-
| `login` | `string` | |
|
|
1205
|
-
| `id?` | `number` | |
|
|
1206
|
-
| `[key: string]` | `unknown` | |
|
|
1207
|
-
|
|
1208
|
-
### `GitHubPullRequest`
|
|
1209
|
-
|
|
1210
|
-
| Field | Type | Description |
|
|
1211
|
-
| --------------- | ------------------------------------------------------------------------------------------------------------- | ----------- |
|
|
1212
|
-
| `number` | `number` | |
|
|
1213
|
-
| `draft?` | `boolean` | |
|
|
1214
|
-
| `title?` | `string` | |
|
|
1215
|
-
| `body?` | `string` | |
|
|
1216
|
-
| `state?` | `string` | |
|
|
1217
|
-
| `merged?` | `boolean` | |
|
|
1218
|
-
| `head` | `{ ref: string; sha: string; repo?: { full_name: string; [key: string]: unknown }; [key: string]: unknown; }` | |
|
|
1219
|
-
| `base` | `{ ref: string; repo?: { full_name: string; [key: string]: unknown }; [key: string]: unknown; }` | |
|
|
1220
|
-
| `user?` | `GitHubUser` | |
|
|
1221
|
-
| `labels?` | `Array<{ name: string; [key: string]: unknown }>` | |
|
|
1222
|
-
| `[key: string]` | `unknown` | |
|
|
1223
|
-
|
|
1224
|
-
### `GitHubCommit`
|
|
1225
|
-
|
|
1226
|
-
| Field | Type | Description |
|
|
1227
|
-
| --------------- | ------------------------------------------------------------------------------ | ----------- |
|
|
1228
|
-
| `id` | `string` | |
|
|
1229
|
-
| `message` | `string` | |
|
|
1230
|
-
| `author?` | `{ name?: string; email?: string; username?: string; [key: string]: unknown }` | |
|
|
1231
|
-
| `timestamp?` | `string` | |
|
|
1232
|
-
| `added?` | `string[]` | |
|
|
1233
|
-
| `removed?` | `string[]` | |
|
|
1234
|
-
| `modified?` | `string[]` | |
|
|
1235
|
-
| `[key: string]` | `unknown` | |
|
|
1236
|
-
|
|
1237
|
-
### `GitHubComment`
|
|
1238
|
-
|
|
1239
|
-
| Field | Type | Description |
|
|
1240
|
-
| --------------- | ------------ | ----------- |
|
|
1241
|
-
| `id` | `number` | |
|
|
1242
|
-
| `body` | `string` | |
|
|
1243
|
-
| `user` | `GitHubUser` | |
|
|
1244
|
-
| `[key: string]` | `unknown` | |
|
|
1245
|
-
|
|
1246
|
-
### `GitHubReview`
|
|
1247
|
-
|
|
1248
|
-
| Field | Type | Description |
|
|
1249
|
-
| --------------- | ------------ | ----------- |
|
|
1250
|
-
| `id` | `number` | |
|
|
1251
|
-
| `state` | `string` | |
|
|
1252
|
-
| `body?` | `string` | |
|
|
1253
|
-
| `user` | `GitHubUser` | |
|
|
1254
|
-
| `[key: string]` | `unknown` | |
|
|
1255
|
-
|
|
1256
|
-
### `GitHubRelease`
|
|
1257
|
-
|
|
1258
|
-
| Field | Type | Description |
|
|
1259
|
-
| ------------------- | --------- | ----------- |
|
|
1260
|
-
| `id` | `number` | |
|
|
1261
|
-
| `tag_name` | `string` | |
|
|
1262
|
-
| `name?` | `string` | |
|
|
1263
|
-
| `body?` | `string` | |
|
|
1264
|
-
| `draft?` | `boolean` | |
|
|
1265
|
-
| `prerelease?` | `boolean` | |
|
|
1266
|
-
| `target_commitish?` | `string` | |
|
|
1267
|
-
| `[key: string]` | `unknown` | |
|
|
1268
|
-
|
|
1269
|
-
---
|
|
1270
|
-
|
|
1271
|
-
## SDK reference: idempotent
|
|
1272
|
-
|
|
1273
|
-
Source: https://docs.kici.dev/user/sdk/idempotent/
|
|
1274
|
-
|
|
1275
|
-
The SDK exposes three idempotency helpers — a generic function `idempotent()`, the step factory `idempotentStep()`, and its check-mode-aware sibling `checkStep()` — for the common case where a workflow step should:
|
|
1276
|
-
|
|
1277
|
-
1. **Check** whether the desired state is already in place.
|
|
1278
|
-
2. **Apply** the change only when drift is detected.
|
|
1279
|
-
3. **Surface** the resource (or its identifier) on both branches, so downstream steps don't need to know whether work happened or was skipped.
|
|
1280
|
-
|
|
1281
|
-
`idempotent()` and `idempotentStep()` wrap the same underlying runner and always apply on drift. Pick `idempotentStep()` when the operation is the whole job of a step; use `idempotent()` from anywhere — inside a multi-action step, a hook, or a bare async function. Pick `checkStep()` when the step should respect the run-level check mode — `kici run --check` previews the drift without applying it.
|
|
1282
|
-
|
|
1283
|
-
## `idempotent(options)`
|
|
1284
|
-
|
|
1285
|
-
Run a single check / apply cycle and return a discriminated result describing the outcome.
|
|
1286
|
-
|
|
1287
|
-
### Parameters
|
|
1288
|
-
|
|
1289
|
-
| Name | Type | Required | Description |
|
|
1290
|
-
| ------------ | -------------------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
|
|
1291
|
-
| `name` | `string` | No | Label that appears in log lines. Defaults to `'idempotent'`. |
|
|
1292
|
-
| `check` | `() => Promise<TDrift \| null>` | Yes | Read-only inspection. Return `null` when the system is already in the desired state. |
|
|
1293
|
-
| `apply` | `(drift: TDrift) => Promise<TApplied>` | Yes | Brings the system to the desired state when `check()` returned a non-null drift value. |
|
|
1294
|
-
| `whenInSync` | `() => Promise<TInSync>` | No | Runs when `check()` returned `null`. Use it to fetch the already-satisfied resource. |
|
|
1295
|
-
| `summarize` | `(drift: TDrift) => string` | No | Human-readable, multi-line summary of what `apply()` would do. Defaults to a JSON dump of drift. |
|
|
1296
|
-
| `log` | `(line: string) => void` | No | Sink for status lines. Defaults to `console.log`. |
|
|
1297
|
-
|
|
1298
|
-
### Result
|
|
1299
|
-
|
|
1300
|
-
`idempotent()` resolves to a discriminated `IdempotentResult` union:
|
|
1301
|
-
|
|
1302
|
-
| Outcome | `drift` | `result` |
|
|
1303
|
-
| ----------- | -------- | ------------------------------------------------ |
|
|
1304
|
-
| `'skipped'` | `null` | The `whenInSync()` return value, or `undefined`. |
|
|
1305
|
-
| `'applied'` | `TDrift` | The `apply()` return value. |
|
|
1306
|
-
|
|
1307
|
-
Narrow on `result.outcome` before reading `result.result` to get the correct typed shape.
|
|
1308
|
-
|
|
1309
|
-
### Example
|
|
1310
|
-
|
|
1311
|
-
```typescript
|
|
1312
|
-
import { idempotent } from '@kici-dev/sdk';
|
|
1313
|
-
|
|
1314
|
-
const result = await idempotent({
|
|
1315
|
-
name: 'create-dns-record',
|
|
1316
|
-
check: async () => {
|
|
1317
|
-
const existing = await dns.getRecord('api.example.com');
|
|
1318
|
-
return existing ? null : { fqdn: 'api.example.com', target: '203.0.113.10' };
|
|
1319
|
-
},
|
|
1320
|
-
whenInSync: async () => {
|
|
1321
|
-
const existing = await dns.getRecord('api.example.com');
|
|
1322
|
-
return { id: existing.id };
|
|
1323
|
-
},
|
|
1324
|
-
apply: async (drift) => {
|
|
1325
|
-
const created = await dns.createRecord(drift.fqdn, drift.target);
|
|
1326
|
-
return { id: created.id };
|
|
1327
|
-
},
|
|
1328
|
-
summarize: (drift) => `Create A record ${drift.fqdn} → ${drift.target}`,
|
|
1329
|
-
});
|
|
1330
|
-
|
|
1331
|
-
// Both branches surface the record id.
|
|
1332
|
-
const recordId = result.result.id;
|
|
1333
|
-
```
|
|
1334
|
-
|
|
1335
|
-
## `idempotentStep(name, options)`
|
|
1336
|
-
|
|
1337
|
-
A factory returning an SDK `Step` whose `run` body executes `idempotent(...)` and routes status lines through the step's structured logger.
|
|
1338
|
-
|
|
1339
|
-
### Parameters
|
|
1340
|
-
|
|
1341
|
-
| Name | Type | Required | Description |
|
|
1342
|
-
| --------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------ |
|
|
1343
|
-
| `name` | `string` | Yes | Step name. Appears in the run timeline and in log lines. |
|
|
1344
|
-
| `options` | `Omit<IdempotentOptions, 'name' \| 'log'>` | Yes | Same shape as `idempotent()` minus `name` (already provided) and `log` (provided by the step context). |
|
|
1345
|
-
|
|
1346
|
-
### Result
|
|
1347
|
-
|
|
1348
|
-
`idempotentStep(...)` returns `Step<IdempotentResult<TDrift, TInSync, TApplied>>`. Other steps can consume the result through the standard step output mechanisms.
|
|
1349
|
-
|
|
1350
|
-
### Example
|
|
1351
|
-
|
|
1352
|
-
```typescript
|
|
1353
|
-
import { idempotentStep, job } from '@kici-dev/sdk';
|
|
1354
|
-
|
|
1355
|
-
const ensureBucket = idempotentStep('ensure-bucket', {
|
|
1356
|
-
check: async () => {
|
|
1357
|
-
const exists = await s3.bucketExists('app-cache');
|
|
1358
|
-
return exists ? null : { bucket: 'app-cache', region: 'eu-central-1' };
|
|
1359
|
-
},
|
|
1360
|
-
whenInSync: async () => ({ arn: 'arn:aws:s3:::app-cache' }),
|
|
1361
|
-
apply: async (drift) => {
|
|
1362
|
-
const created = await s3.createBucket(drift.bucket, drift.region);
|
|
1363
|
-
return { arn: created.arn };
|
|
1364
|
-
},
|
|
1365
|
-
summarize: (drift) => `Create S3 bucket ${drift.bucket} in ${drift.region}`,
|
|
1366
|
-
});
|
|
1367
|
-
|
|
1368
|
-
export const setup = job('setup', {
|
|
1369
|
-
runsOn: 'linux',
|
|
1370
|
-
steps: [ensureBucket],
|
|
1371
|
-
});
|
|
1372
|
-
```
|
|
1373
|
-
|
|
1374
|
-
## `checkStep(name, options)`
|
|
1375
|
-
|
|
1376
|
-
The check-mode-aware sibling of `idempotentStep()`. It takes the **same option shape**, but behaves differently when a run is started in check mode (`kici run --check`):
|
|
1377
|
-
|
|
1378
|
-
| Factory | Behavior under `kici run --check` |
|
|
1379
|
-
| ---------------- | ----------------------------------------- |
|
|
1380
|
-
| `idempotentStep` | always applies on drift |
|
|
1381
|
-
| `checkStep` | reports drift, applies only in apply mode |
|
|
1382
|
-
|
|
1383
|
-
Use `checkStep()` for deploy-style steps where you want a dry-run preview of pending changes before committing them, and `idempotentStep()` for steps that must always converge (for example inside a hook). A `checkStep()` desugars to the first-class step check facet (`check` / `summarize` / `run(ctx, drift)` / `whenInSync`), so it participates in run-level check mode automatically: `kici run --check` reports the drift and skips `apply`, `kici run --check --fail-on-drift` exits non-zero when drift is detected, and apply mode applies the change.
|
|
1384
|
-
|
|
1385
|
-
### Parameters
|
|
1386
|
-
|
|
1387
|
-
| Name | Type | Required | Description |
|
|
1388
|
-
| ----------------- | ------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
|
|
1389
|
-
| `name` | `string` | Yes | Step name. Appears in the run timeline and in log lines. |
|
|
1390
|
-
| `check` | `(ctx) => Promise<TDrift \| null>` | Yes | Read-only inspection. Return `null` when the system is already in the desired state. |
|
|
1391
|
-
| `apply` | `(ctx, drift: TDrift) => Promise<TApplied>` | Yes | Brings the system to the desired state. Runs only in apply mode (skipped under `kici run --check`). |
|
|
1392
|
-
| `summarize` | `(drift: TDrift) => string` | Yes | Human-readable summary of what `apply()` would do; shown in check-mode drift output. |
|
|
1393
|
-
| `whenInSync` | `(ctx) => Promise<TInSync>` | No | Runs when `check()` returned `null` (already in sync). |
|
|
1394
|
-
| `continueOnError` | `boolean` | No | When true, the job proceeds even if this step fails. |
|
|
1395
|
-
| `timeout` | `number` | No | Step-level timeout in milliseconds. |
|
|
1396
|
-
|
|
1397
|
-
The one signature difference from `idempotentStep`: `apply` and `whenInSync` receive `ctx` as their first argument, so the apply logic has access to `ctx.$`, `ctx.log`, and `ctx.secrets`.
|
|
1398
|
-
|
|
1399
|
-
### Result
|
|
1400
|
-
|
|
1401
|
-
`checkStep(...)` returns `Step<TApplied | TInSync>` — the output is whichever of `apply` / `whenInSync` ran.
|
|
1402
|
-
|
|
1403
|
-
### Example
|
|
1404
|
-
|
|
1405
|
-
```typescript
|
|
1406
|
-
import { checkStep, job } from '@kici-dev/sdk';
|
|
1407
|
-
|
|
1408
|
-
const ensureDnsRecord = checkStep('ensure-dns-record', {
|
|
1409
|
-
check: async (ctx) => {
|
|
1410
|
-
const existing = await ctx.$`dig +short api.example.com`;
|
|
1411
|
-
return existing.stdout.trim() ? null : { fqdn: 'api.example.com', target: '203.0.113.10' };
|
|
1412
|
-
},
|
|
1413
|
-
summarize: (drift) => `Create A record ${drift.fqdn} → ${drift.target}`,
|
|
1414
|
-
apply: async (ctx, drift) => {
|
|
1415
|
-
await ctx.$`dns-cli create ${drift.fqdn} ${drift.target}`;
|
|
1416
|
-
return { created: true };
|
|
1417
|
-
},
|
|
1418
|
-
whenInSync: async () => ({ created: false }),
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
export const deploy = job('deploy', {
|
|
1422
|
-
runsOn: 'linux',
|
|
1423
|
-
steps: [ensureDnsRecord],
|
|
1424
|
-
});
|
|
1425
|
-
```
|
|
1426
|
-
|
|
1427
|
-
Run `kici run --check` against this workflow to see the drift summary without touching DNS; run it without `--check` to apply.
|
|
1428
|
-
|
|
1429
|
-
## Worked example: create-if-missing returning a resource id
|
|
1430
|
-
|
|
1431
|
-
The typical use case is **resource provisioning that should be safe to re-run**. The helper guarantees the same downstream typed shape whether the resource already existed or was just created:
|
|
1432
|
-
|
|
1433
|
-
```typescript
|
|
1434
|
-
import { idempotent } from '@kici-dev/sdk';
|
|
1435
|
-
|
|
1436
|
-
interface BucketDrift {
|
|
1437
|
-
bucket: string;
|
|
1438
|
-
region: string;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
interface BucketHandle {
|
|
1442
|
-
arn: string;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
async function ensureBucket(bucket: string, region: string): Promise<BucketHandle> {
|
|
1446
|
-
const result = await idempotent<BucketDrift, BucketHandle, BucketHandle>({
|
|
1447
|
-
name: `ensure-${bucket}`,
|
|
1448
|
-
check: async () => {
|
|
1449
|
-
const existing = await s3.describeBucket(bucket);
|
|
1450
|
-
return existing ? null : { bucket, region };
|
|
1451
|
-
},
|
|
1452
|
-
whenInSync: async () => {
|
|
1453
|
-
const existing = await s3.describeBucket(bucket);
|
|
1454
|
-
return { arn: existing.arn };
|
|
1455
|
-
},
|
|
1456
|
-
apply: async (drift) => {
|
|
1457
|
-
const created = await s3.createBucket(drift.bucket, drift.region);
|
|
1458
|
-
return { arn: created.arn };
|
|
1459
|
-
},
|
|
1460
|
-
summarize: (drift) => `Create S3 bucket ${drift.bucket} in ${drift.region}`,
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
return result.result;
|
|
1464
|
-
}
|
|
1465
|
-
```
|
|
1466
|
-
|
|
1467
|
-
The caller never has to branch on outcome — `result.result` is always a `BucketHandle`. A second invocation against the same bucket logs a single "in sync, skipping" line and returns the same ARN.
|
|
1468
|
-
|
|
1469
|
-
## See also
|
|
1470
|
-
|
|
1471
|
-
- [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories that `idempotentStep()` builds on.
|
|
1472
|
-
- [Runtime types](https://docs.kici.dev/user/sdk/runtime/) — `StepContext`, `Logger`, and other surface used inside the helpers.
|
|
1473
|
-
|
|
1474
|
-
---
|
|
1475
|
-
|
|
1476
|
-
## SDK reference: parallel
|
|
1477
|
-
|
|
1478
|
-
Source: https://docs.kici.dev/user/sdk/parallel/
|
|
1479
|
-
|
|
1480
|
-
`parallel([...steps], opts?)` runs a group of independent steps **concurrently**
|
|
1481
|
-
within one job, behind a join barrier: execution continues past the group only
|
|
1482
|
-
once every child has settled. Each child is its own observable step — it gets its
|
|
1483
|
-
own logs, status, timing, and retry — instead of being hidden inside one step's
|
|
1484
|
-
`Promise.all`.
|
|
1485
|
-
|
|
1486
|
-
```ts
|
|
1487
|
-
import { workflow, job, step, parallel, push } from '@kici-dev/sdk';
|
|
1488
|
-
|
|
1489
|
-
export default workflow('ci', {
|
|
1490
|
-
on: push(),
|
|
1491
|
-
jobs: [
|
|
1492
|
-
job('checks', {
|
|
1493
|
-
runsOn: 'kici:os:linux',
|
|
1494
|
-
steps: [
|
|
1495
|
-
checkout,
|
|
1496
|
-
// lint, typecheck, and the unit tests have no ordering between them,
|
|
1497
|
-
// so they run together — the job's wall-clock is the slowest child,
|
|
1498
|
-
// not the sum of all three.
|
|
1499
|
-
parallel([lint, typecheck, unitTests], { failFast: true }),
|
|
1500
|
-
deploy,
|
|
1501
|
-
],
|
|
1502
|
-
}),
|
|
1503
|
-
],
|
|
1504
|
-
});
|
|
1505
|
-
```
|
|
1506
|
-
|
|
1507
|
-
`parallel(...)` returns a `ParallelGroup` that sits in the ordinary flat
|
|
1508
|
-
`steps: [...]` array — there is no new `job` field. A group's children are
|
|
1509
|
-
**sequential steps only**; groups cannot be nested.
|
|
1510
|
-
|
|
1511
|
-
## Options
|
|
1512
|
-
|
|
1513
|
-
`parallel(steps, opts?)` accepts:
|
|
1514
|
-
|
|
1515
|
-
- **`failFast?: boolean`** — default `true`. When a child fails, the in-flight
|
|
1516
|
-
siblings are cancelled immediately and the job fails. With `failFast: false`
|
|
1517
|
-
every child runs to completion first, then the job fails if any child failed.
|
|
1518
|
-
- **`maxParallel?: number`** — default unlimited. Caps how many children run at
|
|
1519
|
-
once; children waiting for a slot report a `pending` status until they launch.
|
|
1520
|
-
- **`name?: string`** — a label for the group's dashboard band.
|
|
1521
|
-
|
|
1522
|
-
A child marked `continueOnError: true` never trips fail-fast and never fails the
|
|
1523
|
-
job — it still shows a `failed` status badge, but the group treats it as
|
|
1524
|
-
non-fatal.
|
|
1525
|
-
|
|
1526
|
-
## Statuses
|
|
1527
|
-
|
|
1528
|
-
Parallel steps introduce two step statuses:
|
|
1529
|
-
|
|
1530
|
-
- **`pending`** — a child queued behind `maxParallel`, not yet launched.
|
|
1531
|
-
- **`cancelled`** — a sibling aborted by fail-fast. A cancelled step is **not** a
|
|
1532
|
-
failure: only the child that actually failed fails the job; the cancelled
|
|
1533
|
-
siblings render in gray (distinct from the red failing step) on the dashboard.
|
|
1534
|
-
|
|
1535
|
-
Children may also complete **out of order** — the fastest child finishes first
|
|
1536
|
-
regardless of array position. A later sequential step can read a parallel child's
|
|
1537
|
-
`.result` after the barrier; children within a group cannot read each other's
|
|
1538
|
-
results (there is no ordering inside the group).
|
|
1539
|
-
|
|
1540
|
-
## Scope: nests inside job-level fan-out
|
|
1541
|
-
|
|
1542
|
-
`parallel()`'s `failFast` / `maxParallel` are **step-group** scopes — they govern
|
|
1543
|
-
only the steps inside the group. They are a different layer from the **job-level**
|
|
1544
|
-
`failFast` / `maxParallel` on a matrix / `runsOnAll` fan-out, which govern how a
|
|
1545
|
-
job's child _jobs_ spread across the matrix or host roster. A `parallel()` group
|
|
1546
|
-
inside a fan-out job nests its concurrency inside each fan-out child.
|
|
1547
|
-
|
|
1548
|
-
## Local vs remote execution
|
|
1549
|
-
|
|
1550
|
-
Run remotely (the orchestrator + agent), parallel children execute concurrently
|
|
1551
|
-
and each surfaces as its own dashboard step. `kici run <event> --local` executes the same
|
|
1552
|
-
children in array order in its single-process model — the results are identical,
|
|
1553
|
-
only the wall-clock and the live fail-fast cancellation differ. Use a remote run
|
|
1554
|
-
to observe the concurrent timeline.
|
|
1555
|
-
|
|
1556
|
-
---
|
|
1557
|
-
|
|
1558
|
-
## SDK reference: rules, matrix, dynamic jobs
|
|
1559
|
-
|
|
1560
|
-
Source: https://docs.kici.dev/user/sdk/rules-matrix-dynamic/
|
|
1561
|
-
|
|
1562
|
-
## Rules
|
|
1563
|
-
|
|
1564
|
-
Rules control conditional execution of workflows and jobs. A rule that returns `false` (or whose check function returns `false`) prevents execution.
|
|
1565
|
-
|
|
1566
|
-
### rule(label) / rule(label, check)
|
|
1567
|
-
|
|
1568
|
-
Create a rule.
|
|
1569
|
-
|
|
1570
|
-
```typescript
|
|
1571
|
-
function rule(label: string): Rule;
|
|
1572
|
-
function rule(label: string, check: RuleCheckFn): Rule;
|
|
1573
|
-
```
|
|
1574
|
-
|
|
1575
|
-
**Without check function:** Always passes. Useful as a marker in the decision trace.
|
|
1576
|
-
|
|
1577
|
-
```typescript
|
|
1578
|
-
rule('ci: required check');
|
|
1579
|
-
```
|
|
1580
|
-
|
|
1581
|
-
**With check function:** Passes when the function returns `true`.
|
|
1582
|
-
|
|
1583
|
-
```typescript
|
|
1584
|
-
rule('has source changes', async (ctx) => {
|
|
1585
|
-
return ctx.changedFiles.some((f) => f.startsWith('src/'));
|
|
1586
|
-
});
|
|
1587
|
-
```
|
|
1588
|
-
|
|
1589
|
-
### skip(label, check)
|
|
1590
|
-
|
|
1591
|
-
Create a rule that skips when the condition is met. Inverts the check function.
|
|
1592
|
-
|
|
1593
|
-
```typescript
|
|
1594
|
-
function skip(label: string, check: RuleCheckFn): Rule;
|
|
1595
|
-
```
|
|
1596
|
-
|
|
1597
|
-
When the check returns `true` (condition met), the rule returns `false` (skip execution).
|
|
1598
|
-
When the check returns `false` (condition not met), the rule returns `true` (allow execution).
|
|
1599
|
-
|
|
1600
|
-
```typescript
|
|
1601
|
-
// Skip when only docs changed
|
|
1602
|
-
skip('docs only PR', async (ctx) => {
|
|
1603
|
-
return ctx.changedFiles.every((f) => f.endsWith('.md'));
|
|
1604
|
-
});
|
|
1605
|
-
```
|
|
1606
|
-
|
|
1607
|
-
### RuleCheckFn
|
|
1608
|
-
|
|
1609
|
-
```typescript
|
|
1610
|
-
type RuleCheckFn = (ctx: RuleContext) => Promise<boolean> | boolean;
|
|
1611
|
-
```
|
|
1612
|
-
|
|
1613
|
-
Can be sync or async. Receives a `RuleContext`:
|
|
1614
|
-
|
|
1615
|
-
| Property | Type | Description |
|
|
1616
|
-
| -------------- | ----------------------------------- | --------------------------------------------------------------------- |
|
|
1617
|
-
| `event` | `EventPayload` | The triggering event payload (discriminated union — narrow on `type`) |
|
|
1618
|
-
| `changedFiles` | `string[]` | Files changed in this event |
|
|
1619
|
-
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
1620
|
-
| `$` | zx shell | Shell executor for running commands |
|
|
1621
|
-
|
|
1622
|
-
### evaluateRules(rules, context, label, onRuleResult?)
|
|
1623
|
-
|
|
1624
|
-
Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
|
|
1625
|
-
|
|
1626
|
-
```typescript
|
|
1627
|
-
function evaluateRules(
|
|
1628
|
-
rules: Rule[],
|
|
1629
|
-
context: RuleContext,
|
|
1630
|
-
label: string,
|
|
1631
|
-
onRuleResult?: (result: RuleResult) => void,
|
|
1632
|
-
): Promise<RuleEvaluationResult>;
|
|
1633
|
-
```
|
|
1634
|
-
|
|
1635
|
-
Returns a `RuleEvaluationResult`:
|
|
1636
|
-
|
|
1637
|
-
```typescript
|
|
1638
|
-
interface RuleEvaluationResult {
|
|
1639
|
-
allPassed: boolean;
|
|
1640
|
-
results: RuleResult[];
|
|
1641
|
-
}
|
|
1642
|
-
```
|
|
1643
|
-
|
|
1644
|
-
### isEventType(event, type)
|
|
1645
|
-
|
|
1646
|
-
Type guard that narrows an `EventPayload` to a specific event type variant. Use this in rule check functions to get autocomplete on provider-specific fields.
|
|
1647
|
-
|
|
1648
|
-
```typescript
|
|
1649
|
-
function isEventType<T extends EventPayload['type']>(
|
|
1650
|
-
event: EventPayload,
|
|
1651
|
-
type: T,
|
|
1652
|
-
): event is Extract<EventPayload, { type: T }>;
|
|
1653
|
-
```
|
|
1654
|
-
|
|
1655
|
-
**Example — skip draft PRs:**
|
|
1656
|
-
|
|
1657
|
-
```typescript
|
|
1658
|
-
rule('skip-draft-prs', (ctx) => {
|
|
1659
|
-
if (!isEventType(ctx.event, 'pull_request')) return true;
|
|
1660
|
-
// ctx.event is now PullRequestEventPayload — full autocomplete
|
|
1661
|
-
return !ctx.event.payload.pull_request.draft;
|
|
1662
|
-
});
|
|
1663
|
-
```
|
|
1664
|
-
|
|
1665
|
-
**Example — branch-based rule with push narrowing:**
|
|
1666
|
-
|
|
1667
|
-
```typescript
|
|
1668
|
-
rule('only-main-pushes', (ctx) => {
|
|
1669
|
-
if (!isEventType(ctx.event, 'push')) return false;
|
|
1670
|
-
// ctx.event.payload.ref is typed as string
|
|
1671
|
-
return ctx.event.payload.ref === 'refs/heads/main';
|
|
1672
|
-
});
|
|
1673
|
-
```
|
|
1674
|
-
|
|
1675
|
-
You can also narrow directly with `if (ctx.event.type === 'pull_request')` — TypeScript's discriminated union narrowing works on the `type` field.
|
|
1676
|
-
|
|
1677
|
-
### EventPayload
|
|
1678
|
-
|
|
1679
|
-
`EventPayload` is a discriminated union over the `type` field. Each variant provides typed access to the normalized event fields and the raw webhook payload.
|
|
1680
|
-
|
|
1681
|
-
Every variant carries the shared `EventBase` fields — `type`, `action`, `targetBranch`, `sourceBranch`, `provider`, `isForkPR`, `baseBranch`, `senderUsername`, `sourceRepo`, `changedFiles`, and the raw `payload` — plus a per-type `payload` shape for the typed variants. The complete field-by-field schema, including every typed `payload` shape and the shared GitHub object types, is in the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/).
|
|
1682
|
-
|
|
1683
|
-
**Typed variants** (with GitHub-specific payload fields): `pull_request`, `push`, `tag`, `comment`, `review`, `review_comment`, `release`, `dispatch`, `create`, `delete`, `status`, `workflow_run`, `fork`, `star`, `watch`.
|
|
1684
|
-
|
|
1685
|
-
**Generic variants** (payload is `Record<string, unknown>`): `webhook`, `kici_event`, `workflow_complete`, `job_complete`, `generic_webhook`, `schedule`, `lifecycle`.
|
|
1686
|
-
|
|
1687
|
-
## Matrix
|
|
1688
|
-
|
|
1689
|
-
Matrix configurations expand a single job into multiple instances, one per parameter combination. Maximum 256 combinations.
|
|
1690
|
-
|
|
1691
|
-
Expansion happens at **dispatch time**: the orchestrator materializes the matrix into N execution jobs — one per combination, each dispatched to its own agent — before any job runs. Each instance receives its combination as `ctx.matrix`. This is identical whether the workflow runs via `kici run <event> --local` or remotely through a webhook trigger, and the dashboard groups the N instances under one parent node.
|
|
1692
|
-
|
|
1693
|
-
### Static array (single dimension)
|
|
1694
|
-
|
|
1695
|
-
```typescript
|
|
1696
|
-
matrix: ['18', '20', '22'];
|
|
1697
|
-
```
|
|
1698
|
-
|
|
1699
|
-
Creates 3 job instances. In steps, the current value is `matrix.value`:
|
|
1700
|
-
|
|
1701
|
-
```typescript
|
|
1702
|
-
step('test', async ({ $, matrix }) => {
|
|
1703
|
-
console.log(matrix!.value); // '18', '20', or '22'
|
|
1704
|
-
});
|
|
1705
|
-
```
|
|
1706
|
-
|
|
1707
|
-
### Static object (multi-dimensional)
|
|
1708
|
-
|
|
1709
|
-
```typescript
|
|
1710
|
-
matrix: {
|
|
1711
|
-
os: ['linux', 'arm64'],
|
|
1712
|
-
node: ['18', '20'],
|
|
1713
|
-
}
|
|
1714
|
-
```
|
|
1715
|
-
|
|
1716
|
-
Creates 4 job instances (2 x 2). The `os` values (`linux`, `arm64`) are **customer-defined scaler labels** matched by subset semantics against the labels your orchestrator advertises in its scaler `labelSets` — not hosted-runner names. In steps, values are named properties:
|
|
1717
|
-
|
|
1718
|
-
```typescript
|
|
1719
|
-
step('test', async ({ $, matrix }) => {
|
|
1720
|
-
console.log(matrix!.os); // 'linux' or 'arm64'
|
|
1721
|
-
console.log(matrix!.node); // '18' or '20'
|
|
1722
|
-
});
|
|
1723
|
-
```
|
|
1724
|
-
|
|
1725
|
-
### Dynamic function
|
|
1726
|
-
|
|
1727
|
-
Compute matrix values at runtime:
|
|
1728
|
-
|
|
1729
|
-
```typescript
|
|
1730
|
-
matrix: async ({ $ }) => {
|
|
1731
|
-
const result = await $`ls packages/`;
|
|
1732
|
-
return result.stdout.trim().split('\n');
|
|
1733
|
-
};
|
|
1734
|
-
```
|
|
1735
|
-
|
|
1736
|
-
The function receives a `DynamicMatrixContext`:
|
|
1737
|
-
|
|
1738
|
-
| Property | Type | Description |
|
|
1739
|
-
| -------- | ----------------------------------- | ------------------------- |
|
|
1740
|
-
| `$` | zx shell | Shell executor |
|
|
1741
|
-
| `ctx` | `{ workflow, job }` | Workflow and job metadata |
|
|
1742
|
-
| `log` | `Logger` | Structured logger |
|
|
1743
|
-
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
1744
|
-
|
|
1745
|
-
Must return `string[]` (single dimension) or `Record<string, string[]>` (multi-dimensional).
|
|
1746
|
-
|
|
1747
|
-
A dynamic matrix is resolved at runtime, then materialized into N instances exactly like a static matrix. Because the combinations are not known until the function runs, the 256-combination cap (and the "zero combinations" guard) is enforced at that point: a dynamic matrix that resolves to more than 256 combinations, or to none, fails the job with a matrix-expansion error rather than dispatching.
|
|
1748
|
-
|
|
1749
|
-
### Include and exclude
|
|
1750
|
-
|
|
1751
|
-
Fine-tune matrix combinations on multi-dimensional matrices:
|
|
1752
|
-
|
|
1753
|
-
```typescript
|
|
1754
|
-
matrix: {
|
|
1755
|
-
os: ['linux', 'arm64', 'windows'],
|
|
1756
|
-
node: ['18', '20', '22'],
|
|
1757
|
-
},
|
|
1758
|
-
exclude: [
|
|
1759
|
-
{ os: 'windows', node: '18' },
|
|
1760
|
-
],
|
|
1761
|
-
include: [
|
|
1762
|
-
{ os: 'linux', node: '23' },
|
|
1763
|
-
],
|
|
1764
|
-
```
|
|
1765
|
-
|
|
1766
|
-
**Exclude** removes combinations matching all specified keys. Applied first.
|
|
1767
|
-
**Include** adds exact combinations. Applied after exclude.
|
|
1768
|
-
|
|
1769
|
-
Types:
|
|
1770
|
-
|
|
1771
|
-
```typescript
|
|
1772
|
-
type MatrixInclude = Record<string, string>;
|
|
1773
|
-
type MatrixExclude = Record<string, string>;
|
|
1774
|
-
```
|
|
1775
|
-
|
|
1776
|
-
### MatrixValues
|
|
1777
|
-
|
|
1778
|
-
The shape of `matrix` in `StepContext`:
|
|
1779
|
-
|
|
1780
|
-
```typescript
|
|
1781
|
-
interface MatrixValues {
|
|
1782
|
-
value?: string; // Single-dimension value
|
|
1783
|
-
[dimension: string]: string | undefined; // Named dimensions
|
|
1784
|
-
}
|
|
1785
|
-
```
|
|
1786
|
-
|
|
1787
|
-
### Bounding matrix concurrency (maxParallel / failFast)
|
|
1788
|
-
|
|
1789
|
-
A matrix fan-out runs every combination at once by default. The fan-out-generic
|
|
1790
|
-
`maxParallel` and `failFast` job options bound it the same way they bound a
|
|
1791
|
-
[`runsOnAll`](https://docs.kici.dev/user/sdk/runs-on-all/#rolling-rollout-maxparallel--failfast) host fan-out:
|
|
1792
|
-
|
|
1793
|
-
```typescript
|
|
1794
|
-
const test = job('test', {
|
|
1795
|
-
runsOn: 'linux',
|
|
1796
|
-
matrix: { os: ['ubuntu', 'macos', 'windows'] },
|
|
1797
|
-
maxParallel: 1, // run one combination at a time (sliding window)
|
|
1798
|
-
failFast: true, // stop launching combinations after the first failure
|
|
1799
|
-
run: async (ctx) => {
|
|
1800
|
-
/* ctx.matrix.os */
|
|
1801
|
-
},
|
|
1802
|
-
});
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
`maxParallel` is a sliding window (each combination that finishes releases the next;
|
|
1806
|
-
`1` = serial; must be `>= 1`); `failFast` halts the fan-out on the first failure and
|
|
1807
|
-
skips the held remainder (default `false`). They are ignored on a job with no `matrix`
|
|
1808
|
-
or `runsOnAll`.
|
|
1809
|
-
|
|
1810
|
-
### Consuming matrix outputs downstream
|
|
1811
|
-
|
|
1812
|
-
A downstream job that lists a matrix job in its `needs` receives a **keyed envelope** instead of a flat outputs object, because the upstream produced N sets of outputs (one per combination). `ctx.jobOutputs(matrixJob)` returns a `MatrixJobOutputs`:
|
|
1813
|
-
|
|
1814
|
-
```typescript
|
|
1815
|
-
interface MatrixJobOutputs<T = Record<string, unknown>> {
|
|
1816
|
-
/** Keyed by the combination suffix — the text inside `(...)` of the child name. */
|
|
1817
|
-
byMatrix: Record<string, T>;
|
|
1818
|
-
/** Last-write-wins flat merge across children, in child (name) order. */
|
|
1819
|
-
merged: T;
|
|
1820
|
-
}
|
|
1821
|
-
```
|
|
1822
|
-
|
|
1823
|
-
The suffix key matches the child job's display name: `byMatrix['a']` for a single-dimension `['a', 'b']` matrix, `byMatrix['linux, arm64']` for a multi-dimension combination. Use `isMatrixJobOutputs` (or `'byMatrix' in result`) to discriminate:
|
|
1824
|
-
|
|
1825
|
-
```typescript
|
|
1826
|
-
import { isMatrixJobOutputs } from '@kici-dev/sdk';
|
|
1827
|
-
|
|
1828
|
-
step('collect', async ({ jobOutputs }) => {
|
|
1829
|
-
const out = jobOutputs(buildMatrixJob);
|
|
1830
|
-
if (isMatrixJobOutputs(out)) {
|
|
1831
|
-
console.log(out.byMatrix['a']); // outputs of the `a` combination
|
|
1832
|
-
console.log(out.merged); // last-write-wins across all combinations
|
|
1833
|
-
}
|
|
1834
|
-
});
|
|
1835
|
-
```
|
|
1836
|
-
|
|
1837
|
-
The downstream job waits for **all** matrix combinations to terminate before it dispatches. A non-matrix upstream keeps the flat outputs shape. The envelope is identical under `kici run <event> --local` and the remote path.
|
|
1838
|
-
|
|
1839
|
-
### Matrix type guards
|
|
1840
|
-
|
|
1841
|
-
```typescript
|
|
1842
|
-
import { isStaticArray, isStaticObject, isDynamicFunction } from '@kici-dev/sdk';
|
|
1843
|
-
|
|
1844
|
-
isStaticArray(matrix); // true if string[]
|
|
1845
|
-
isStaticObject(matrix); // true if Record<string, string[]>
|
|
1846
|
-
isDynamicFunction(matrix); // true if async function
|
|
1847
|
-
```
|
|
1848
|
-
|
|
1849
|
-
### Matrix expansion utilities
|
|
1850
|
-
|
|
1851
|
-
```typescript
|
|
1852
|
-
import { expandMatrix, applyIncludeExclude } from '@kici-dev/sdk';
|
|
1853
|
-
```
|
|
1854
|
-
|
|
1855
|
-
`expandMatrix(matrix)` takes a `StaticMatrixArray` or `StaticMatrixObject` and returns all combinations as `MatrixValues[]`. For a single-dimension array, each value becomes `{ value: '...' }`. For multi-dimensional objects, it produces the Cartesian product.
|
|
1856
|
-
|
|
1857
|
-
`applyIncludeExclude(values, include?, exclude?)` filters an expanded matrix: removes combinations matching any exclude entry, then appends include entries. Returns the filtered `MatrixValues[]`.
|
|
1858
|
-
|
|
1859
|
-
## Dynamic jobs
|
|
1860
|
-
|
|
1861
|
-
Generate jobs at runtime using async factory functions.
|
|
1862
|
-
|
|
1863
|
-
### DynamicJobFn
|
|
1864
|
-
|
|
1865
|
-
```typescript
|
|
1866
|
-
type DynamicJobFn = (context: DynamicJobContext) => Promise<Job[]>;
|
|
1867
|
-
```
|
|
1868
|
-
|
|
1869
|
-
Receives a `DynamicJobContext`:
|
|
1870
|
-
|
|
1871
|
-
| Property | Type | Description |
|
|
1872
|
-
| -------- | ----------------------------------- | --------------------------- |
|
|
1873
|
-
| `$` | zx shell | Shell executor |
|
|
1874
|
-
| `ctx` | `{ workflow, event? }` | Workflow metadata and event |
|
|
1875
|
-
| `log` | `Logger` | Structured logger |
|
|
1876
|
-
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
1877
|
-
|
|
1878
|
-
```typescript
|
|
1879
|
-
const discoverJobs: DynamicJobFn = async ({ $ }) => {
|
|
1880
|
-
const result = await $`ls packages/`;
|
|
1881
|
-
const packages = result.stdout.trim().split('\n');
|
|
1882
|
-
return packages.map((pkg) =>
|
|
1883
|
-
job(`test-${pkg}`, {
|
|
1884
|
-
runsOn: 'linux',
|
|
1885
|
-
steps: [
|
|
1886
|
-
step('test', async ({ $ }) => {
|
|
1887
|
-
await $`cd packages/${pkg} && pnpm test`;
|
|
1888
|
-
}),
|
|
1889
|
-
],
|
|
1890
|
-
}),
|
|
1891
|
-
);
|
|
1892
|
-
};
|
|
1893
|
-
|
|
1894
|
-
export default workflow('ci', {
|
|
1895
|
-
jobs: [discoverJobs],
|
|
1896
|
-
});
|
|
1897
|
-
```
|
|
1898
|
-
|
|
1899
|
-
### dynamicJob — result-aware generation
|
|
1900
|
-
|
|
1901
|
-
`dynamicJob(group, fnOrConfig)` tags a generator with a group name (so static jobs can depend on it via `needs: [dynamicGroup('group')]`). It is polymorphic:
|
|
1902
|
-
|
|
1903
|
-
- **Function form** — event-only, dispatched at webhook time: `dynamicJob('shards', async ({ ctx }) => [...])`.
|
|
1904
|
-
- **Options-object form** — result-aware, deferred until its declared `needs` complete, then run with the upstreams' frozen outputs as `ctx.needs`: `dynamicJob('reports', { needs, generate })`.
|
|
1905
|
-
|
|
1906
|
-
```typescript
|
|
1907
|
-
import { workflow, job, step, dynamicJob, dynamicGroup, z } from '@kici-dev/sdk';
|
|
1908
|
-
|
|
1909
|
-
// Upstream job A discovers a list of targets at runtime.
|
|
1910
|
-
const discover = job('discover', {
|
|
1911
|
-
runsOn: 'linux',
|
|
1912
|
-
steps: [
|
|
1913
|
-
step('emit', {
|
|
1914
|
-
outputs: { targets: z.array(z.string()) },
|
|
1915
|
-
run: async () => ({ targets: ['api', 'web'] }),
|
|
1916
|
-
}),
|
|
1917
|
-
],
|
|
1918
|
-
});
|
|
1919
|
-
|
|
1920
|
-
// Result-aware generator fans out one report job per discovered target.
|
|
1921
|
-
const reports = dynamicJob('reports', {
|
|
1922
|
-
needs: ['discover'],
|
|
1923
|
-
generate: async ({ ctx }) => {
|
|
1924
|
-
const targets = ctx.needs.discover.result.targets; // OutputProxy over discover's outputs
|
|
1925
|
-
return targets.map((target) =>
|
|
1926
|
-
job(`report-${target}`, {
|
|
1927
|
-
runsOn: 'linux',
|
|
1928
|
-
run: async ({ log }) => log.info(`reporting on ${target}`),
|
|
1929
|
-
}),
|
|
1930
|
-
);
|
|
1931
|
-
},
|
|
1932
|
-
});
|
|
1933
|
-
|
|
1934
|
-
export default workflow('discovery-fan-out', { jobs: [discover, reports] });
|
|
1935
|
-
```
|
|
1936
|
-
|
|
1937
|
-
`ctx.needs` shape:
|
|
1938
|
-
|
|
1939
|
-
| Need form | `ctx.needs[...]` value |
|
|
1940
|
-
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1941
|
-
| `'jobName'` / `{ name, when }` | `{ result, status }` — `result` is an `OutputProxy` (`ctx.needs.<job>.result.<step>.<field>`; single-step `run` jobs flatten to `ctx.needs.<job>.result.<field>`); `status` is the upstream's terminal status |
|
|
1942
|
-
| `dynamicGroup('g')` / `dynamicGroup('g', { when })` | ordered array of `{ name, result, status }`, one per group member |
|
|
1943
|
-
|
|
1944
|
-
`ctx.needs` is deterministic — a snapshot of upstream outputs frozen at first eval and replayed unchanged on re-eval, like `ctx.event`. Use result-aware generation for same-run fan-out from a prior job's result; use [`jobComplete()`](https://docs.kici.dev/user/sdk/triggers/) for cross-workflow reactions to a job finishing. See the architecture deep-dive in [dynamic jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/#result-aware-generation).
|
|
1945
|
-
|
|
1946
|
-
### JobOrFactory
|
|
1947
|
-
|
|
1948
|
-
The `jobs` array in `WorkflowOptions` accepts both static jobs and dynamic generators:
|
|
1101
|
+
Creates 3 job instances. In steps, the current value is `matrix.value`:
|
|
1949
1102
|
|
|
1950
1103
|
```typescript
|
|
1951
|
-
|
|
1104
|
+
step('test', async ({ $, matrix }) => {
|
|
1105
|
+
console.log(matrix!.value); // '18', '20', or '22'
|
|
1106
|
+
});
|
|
1952
1107
|
```
|
|
1953
1108
|
|
|
1954
|
-
###
|
|
1955
|
-
|
|
1956
|
-
Type guard to distinguish static jobs from dynamic generators:
|
|
1957
|
-
|
|
1958
|
-
```typescript
|
|
1959
|
-
function isDynamicJobFn(item: JobOrFactory): item is DynamicJobFn;
|
|
1960
|
-
```
|
|
1109
|
+
### Static object (multi-dimensional)
|
|
1961
1110
|
|
|
1962
1111
|
```typescript
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
} else {
|
|
1967
|
-
// item is Job
|
|
1968
|
-
}
|
|
1112
|
+
matrix: {
|
|
1113
|
+
os: ['linux', 'arm64'],
|
|
1114
|
+
node: ['18', '20'],
|
|
1969
1115
|
}
|
|
1970
1116
|
```
|
|
1971
1117
|
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
## SDK reference: runsOnAll host fan-out
|
|
1975
|
-
|
|
1976
|
-
Source: https://docs.kici.dev/user/sdk/runs-on-all/
|
|
1977
|
-
|
|
1978
|
-
## runsOnAll
|
|
1979
|
-
|
|
1980
|
-
`runsOnAll` fans a single job out to **every** host in the orchestrator's declared
|
|
1981
|
-
roster that matches a label predicate — one pinned execution per host. Use it for
|
|
1982
|
-
fleet-wide operations: patch every web tier, smoke-test every node, collect uptime
|
|
1983
|
-
from the fleet.
|
|
1984
|
-
|
|
1985
|
-
`runsOnAll` is mutually exclusive with [`runsOn`](https://docs.kici.dev/user/sdk/core/): a job declares one
|
|
1986
|
-
or the other. Where `runsOn` picks a **single** agent that satisfies the labels,
|
|
1987
|
-
`runsOnAll` targets **all** matching hosts and runs the job once on each, pinned to
|
|
1988
|
-
that specific host.
|
|
1989
|
-
|
|
1990
|
-
```typescript
|
|
1991
|
-
import { job } from '@kici-dev/sdk';
|
|
1992
|
-
|
|
1993
|
-
// Run on every host labelled role:web.
|
|
1994
|
-
const patch = job('patch', {
|
|
1995
|
-
runsOnAll: 'role:web',
|
|
1996
|
-
run: async (ctx) => {
|
|
1997
|
-
await ctx.$`sudo apt-get update && sudo apt-get upgrade -y`;
|
|
1998
|
-
ctx.log.info(`patched ${ctx.host}`);
|
|
1999
|
-
},
|
|
2000
|
-
});
|
|
2001
|
-
```
|
|
2002
|
-
|
|
2003
|
-
### Input forms
|
|
2004
|
-
|
|
2005
|
-
`runsOnAll` accepts three shapes:
|
|
2006
|
-
|
|
2007
|
-
- **Bare string** — one required label.
|
|
2008
|
-
|
|
2009
|
-
```typescript
|
|
2010
|
-
runsOnAll: 'role:web';
|
|
2011
|
-
```
|
|
2012
|
-
|
|
2013
|
-
- **Array** — every positive entry is required (AND); a `!`-prefixed entry excludes a host.
|
|
2014
|
-
|
|
2015
|
-
```typescript
|
|
2016
|
-
runsOnAll: ['kici:os:linux', 'role:db', '!kici:host:db-01'];
|
|
2017
|
-
```
|
|
2018
|
-
|
|
2019
|
-
- **Structured** — explicit OR-of-AND include groups plus excludes.
|
|
2020
|
-
|
|
2021
|
-
```typescript
|
|
2022
|
-
runsOnAll: {
|
|
2023
|
-
include: [{ all: ['kici:os:linux', 'role:db'] }, { all: ['role:replica'] }],
|
|
2024
|
-
exclude: ['kici:host:db-01'],
|
|
2025
|
-
};
|
|
2026
|
-
```
|
|
2027
|
-
|
|
2028
|
-
A host matches when it satisfies **any** include group (all labels in that group)
|
|
2029
|
-
and carries **none** of the exclude labels.
|
|
2030
|
-
|
|
2031
|
-
#### Targeting by pattern
|
|
2032
|
-
|
|
2033
|
-
Every entry in any of these forms — include or exclude — can be an exact string, a
|
|
2034
|
-
glob, or a regular expression, exactly like [`runsOn`](https://docs.kici.dev/user/sdk/core/#targeting-by-pattern):
|
|
2035
|
-
|
|
2036
|
-
- **Plain string → exact match** (`'role:web'`).
|
|
2037
|
-
- **String with glob metacharacters (`*`, `?`, `[]`, `{}`) → glob** (`'kici:host:web-*'`).
|
|
2038
|
-
- **`RegExp` literal → regular expression** (`/.*-canary$/`).
|
|
2039
|
-
|
|
2040
|
-
In the array form, a leading `!` routes an entry to the exclude side and is stripped
|
|
2041
|
-
**before** the matching mode is decided, so `'!kici:host:box-*'` is an exclude **glob**
|
|
2042
|
-
and `'!box-01'` an exclude **exact** match. A regular-expression exclusion uses the
|
|
2043
|
-
structured `exclude: [/…/]` form (a `RegExp` cannot carry a `!` prefix):
|
|
1118
|
+
Creates 4 job instances (2 x 2). The `os` values (`linux`, `arm64`) are **customer-defined scaler labels** matched by subset semantics against the labels your orchestrator advertises in its scaler `labelSets` — not hosted-runner names. In steps, values are named properties:
|
|
2044
1119
|
|
|
2045
1120
|
```typescript
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
exclude: [/.*-canary$/],
|
|
2050
|
-
},
|
|
2051
|
-
run: async (ctx) => {
|
|
2052
|
-
/* runs once per matched host */
|
|
2053
|
-
},
|
|
1121
|
+
step('test', async ({ $, matrix }) => {
|
|
1122
|
+
console.log(matrix!.os); // 'linux' or 'arm64'
|
|
1123
|
+
console.log(matrix!.node); // '18' or '20'
|
|
2054
1124
|
});
|
|
2055
1125
|
```
|
|
2056
1126
|
|
|
2057
|
-
|
|
2058
|
-
and can no longer be matched exactly. A regular expression you supply is validated for
|
|
2059
|
-
catastrophic-backtracking (ReDoS) when you run `kici compile` and rejected if it could
|
|
2060
|
-
hang on a crafted input.
|
|
2061
|
-
|
|
2062
|
-
### Per-host execution model
|
|
2063
|
-
|
|
2064
|
-
Each matching host runs the job as its own pinned child, named `<job> (<hostname>)`
|
|
2065
|
-
(e.g. `patch (web-01)`). The children fan in for downstream `needs:` exactly like a
|
|
2066
|
-
matrix job — a downstream that needs the base job waits for every host child.
|
|
2067
|
-
|
|
2068
|
-
The job runs once per host with concurrency `unlimited` (all hosts in parallel).
|
|
2069
|
-
|
|
2070
|
-
### ctx.host and ctx.agent
|
|
2071
|
-
|
|
2072
|
-
Inside a `runsOnAll` step, two extra context fields identify the host the child is
|
|
2073
|
-
running on:
|
|
2074
|
-
|
|
2075
|
-
- `ctx.host` — the hostname (string).
|
|
2076
|
-
- `ctx.agent` — the resolved agent facts: `{ host, labels, platform?, arch? }`.
|
|
2077
|
-
|
|
2078
|
-
```typescript
|
|
2079
|
-
run: async (ctx) => {
|
|
2080
|
-
ctx.log.info(`running on ${ctx.host} (${ctx.agent?.platform}/${ctx.agent?.arch})`);
|
|
2081
|
-
};
|
|
2082
|
-
```
|
|
2083
|
-
|
|
2084
|
-
Both are `undefined` for jobs that do not use `runsOnAll`.
|
|
2085
|
-
|
|
2086
|
-
### ctx.fanout — fan-out position
|
|
1127
|
+
### Dynamic function
|
|
2087
1128
|
|
|
2088
|
-
|
|
2089
|
-
carries its **position** within the fan-out:
|
|
1129
|
+
Compute matrix values at runtime:
|
|
2090
1130
|
|
|
2091
1131
|
```typescript
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
first: boolean; // index === 0
|
|
2096
|
-
last: boolean; // index === total - 1
|
|
1132
|
+
matrix: async ({ $ }) => {
|
|
1133
|
+
const result = await $`ls packages/`;
|
|
1134
|
+
return result.stdout.trim().split('\n');
|
|
2097
1135
|
};
|
|
2098
1136
|
```
|
|
2099
1137
|
|
|
2100
|
-
The
|
|
2101
|
-
matrix fan-out by its combination label. So `ctx.fanout.first` is always the
|
|
2102
|
-
same (lowest-agent-id) host across re-runs, and `ctx.fanout.last` the same final
|
|
2103
|
-
one. `ctx.fanout` is `undefined` on a job that is not fanned out.
|
|
2104
|
-
|
|
2105
|
-
### Run-once steps: onlyOnFirstHost / onlyOnLastHost / onlyOnFanoutIndex
|
|
2106
|
-
|
|
2107
|
-
For ordered, stateful rollouts you often need a step that runs on exactly **one**
|
|
2108
|
-
host — enable a leader before the rest join, run a one-time migration, take a
|
|
2109
|
-
single dump. Three rule helpers express this by reading `ctx.fanout`:
|
|
2110
|
-
|
|
2111
|
-
```typescript
|
|
2112
|
-
import { job, step, onlyOnFirstHost, onlyOnLastHost, onlyOnFanoutIndex } from '@kici-dev/sdk';
|
|
2113
|
-
|
|
2114
|
-
const rollout = job('rollout', {
|
|
2115
|
-
runsOnAll: 'role:db',
|
|
2116
|
-
maxParallel: 1, // serial, so "first" runs before the rest
|
|
2117
|
-
steps: [
|
|
2118
|
-
// Runs only on the first (lowest-agent-id) host — KiCI's run-once primitive.
|
|
2119
|
-
step('enable-sync-mode', { rules: [onlyOnFirstHost()] }, async (ctx) => {
|
|
2120
|
-
/* configure the leader before standbys join */
|
|
2121
|
-
}),
|
|
2122
|
-
// Runs on every host.
|
|
2123
|
-
step('apply', async (ctx) => {
|
|
2124
|
-
/* ... */
|
|
2125
|
-
}),
|
|
2126
|
-
// Runs only on the last host.
|
|
2127
|
-
step('finalize', { rules: [onlyOnLastHost()] }, async (ctx) => {
|
|
2128
|
-
/* ... */
|
|
2129
|
-
}),
|
|
2130
|
-
],
|
|
2131
|
-
});
|
|
2132
|
-
```
|
|
2133
|
-
|
|
2134
|
-
- A step gated this way is **skipped** (not failed) on non-matching hosts — its
|
|
2135
|
-
outputs exist only on the host where it ran.
|
|
2136
|
-
- `onlyOnFanoutIndex(n)` targets the host at a specific position.
|
|
2137
|
-
- **Non-fan-out safety:** on a job that is not fanned out, `ctx.fanout` is
|
|
2138
|
-
`undefined` and these helpers treat the job as a single implicit child at
|
|
2139
|
-
index 0 — so `onlyOnFirstHost()` runs normally there (there is one host, which
|
|
2140
|
-
is the first). This means you can author a step with `onlyOnFirstHost()` and it
|
|
2141
|
-
behaves correctly whether or not the job ends up fanning out.
|
|
2142
|
-
- The helpers are host-flavored by name (the dominant use case) but read
|
|
2143
|
-
`ctx.fanout`, so they work for matrix fan-out too — `onlyOnFirstHost()` runs on
|
|
2144
|
-
the first combination.
|
|
2145
|
-
|
|
2146
|
-
### byHost outputs
|
|
2147
|
-
|
|
2148
|
-
A downstream that `needs:` a `runsOnAll` job receives a **byHost** envelope instead
|
|
2149
|
-
of a flat outputs object — keyed by hostname, with a per-host summary:
|
|
2150
|
-
|
|
2151
|
-
```typescript
|
|
2152
|
-
import { isHostJobOutputs } from '@kici-dev/sdk';
|
|
2153
|
-
|
|
2154
|
-
const report = job('report', {
|
|
2155
|
-
runsOn: 'role:control',
|
|
2156
|
-
needs: [patch],
|
|
2157
|
-
run: async (ctx) => {
|
|
2158
|
-
const outputs = ctx.jobOutputs(patch);
|
|
2159
|
-
if (isHostJobOutputs(outputs)) {
|
|
2160
|
-
ctx.log.info(`succeeded: ${outputs.summary.succeededHosts.join(', ')}`);
|
|
2161
|
-
ctx.log.info(`failed: ${outputs.summary.failedHosts.join(', ')}`);
|
|
2162
|
-
// Per-host outputs, keyed by hostname:
|
|
2163
|
-
const version = outputs.byHost['web-01']?.version;
|
|
2164
|
-
// Array view of one output key across every host:
|
|
2165
|
-
const allVersions = outputs.summary.outputs.version;
|
|
2166
|
-
}
|
|
2167
|
-
},
|
|
2168
|
-
});
|
|
2169
|
-
```
|
|
1138
|
+
The function receives a `DynamicMatrixContext`:
|
|
2170
1139
|
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
1140
|
+
| Property | Type | Description |
|
|
1141
|
+
| -------- | ----------------------------------- | ------------------------- |
|
|
1142
|
+
| `$` | zx shell | Shell executor |
|
|
1143
|
+
| `ctx` | `{ workflow, job }` | Workflow and job metadata |
|
|
1144
|
+
| `log` | `Logger` | Structured logger |
|
|
1145
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
2174
1146
|
|
|
2175
|
-
|
|
1147
|
+
Must return `string[]` (single dimension) or `Record<string, string[]>` (multi-dimensional).
|
|
2176
1148
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
(static) host in the roster is matched but not currently connected:
|
|
1149
|
+
The contract is enforced at runtime. A function that returns anything else — `undefined` (a
|
|
1150
|
+
missing `return`), a bare string, or an object whose values are not arrays — fails the job with
|
|
1151
|
+
an error naming the job and the expected shape. Numbers and booleans are accepted and converted
|
|
1152
|
+
to strings, since matrix values appear in job names as text.
|
|
2182
1153
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
- **`skip`** — omit the absent durable host and run only on the reachable hosts.
|
|
2187
|
-
- **`fail`** — fail the run init if any expected durable host is unreachable.
|
|
1154
|
+
Values must also be unique. A matrix containing the same value twice would produce two children
|
|
1155
|
+
with the same name, so it fails the job instead — de-duplicate the values your function returns
|
|
1156
|
+
(for example, `[...new Set(values)]`). The same rule applies to a static matrix.
|
|
2188
1157
|
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
onUnreachable: 'skip',
|
|
2193
|
-
run: async (ctx) => {
|
|
2194
|
-
/* ... */
|
|
2195
|
-
},
|
|
2196
|
-
});
|
|
2197
|
-
```
|
|
1158
|
+
A dynamic matrix that would expand to an unreasonable number of raw combinations is refused
|
|
1159
|
+
before it is built, so a runaway discovery command fails the job with an error rather than
|
|
1160
|
+
exhausting the agent.
|
|
2198
1161
|
|
|
2199
|
-
|
|
2200
|
-
independent of `onUnreachable` — a scaled-down node may never return. A `runsOnAll`
|
|
2201
|
-
that matches zero usable hosts fails the run rather than reporting a silent zero-child
|
|
2202
|
-
success.
|
|
1162
|
+
A dynamic matrix is resolved at runtime, then materialized into N instances exactly like a static matrix. Because the combinations are not known until the function runs, the 256-combination cap (and the "zero combinations" guard) is enforced at that point: a dynamic matrix that resolves to more than 256 combinations, or to none, fails the job with a matrix-expansion error rather than dispatching.
|
|
2203
1163
|
|
|
2204
|
-
###
|
|
1164
|
+
### Include and exclude
|
|
2205
1165
|
|
|
2206
|
-
|
|
2207
|
-
A **never-initialized** host — a freshly-provisioned box reachable over SSH but with no
|
|
2208
|
-
agent yet — is a different case: there is nothing to run on. Set
|
|
2209
|
-
`includeUninitialized: true` to widen the fan-out to those hosts and bring them up:
|
|
1166
|
+
Fine-tune matrix combinations on multi-dimensional matrices:
|
|
2210
1167
|
|
|
2211
1168
|
```typescript
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
1169
|
+
matrix: {
|
|
1170
|
+
os: ['linux', 'arm64', 'windows'],
|
|
1171
|
+
node: ['18', '20', '22'],
|
|
1172
|
+
},
|
|
1173
|
+
exclude: [
|
|
1174
|
+
{ os: 'windows', node: '18' },
|
|
1175
|
+
],
|
|
1176
|
+
include: [
|
|
1177
|
+
{ os: 'linux', node: '23' },
|
|
1178
|
+
],
|
|
2217
1179
|
```
|
|
2218
1180
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
have a live agent run the steps on their own agent. One workflow converges the whole
|
|
2222
|
-
fleet — fresh boxes get built, live boxes run the same phases.
|
|
2223
|
-
|
|
2224
|
-
Because the steps run on already-initialized hosts too, the bootstrap phases **must be
|
|
2225
|
-
idempotent [check-steps](https://docs.kici.dev/user/sdk/core/)**: each step's `check()` reports in-sync on a
|
|
2226
|
-
live box so the partition / format / install steps **skip** there and run only on fresh
|
|
2227
|
-
boxes. This is the safety guard — an OS or disk-format step must never re-run on a host
|
|
2228
|
-
that is already built. Re-running the workflow is a no-op everywhere. See the operator
|
|
2229
|
-
[fresh-box bootstrap](https://docs.kici.dev/operator/orchestrator/host-roster/) doc for the bring-up,
|
|
2230
|
-
capability gating, and lifecycle details.
|
|
2231
|
-
|
|
2232
|
-
`includeUninitialized` is only meaningful alongside `runsOnAll`; it is ignored on a
|
|
2233
|
-
single-agent `runsOn` job.
|
|
2234
|
-
|
|
2235
|
-
### Rolling rollout: maxParallel + failFast
|
|
1181
|
+
**Exclude** removes combinations matching all specified keys. Applied first.
|
|
1182
|
+
**Include** adds exact combinations. Applied after exclude.
|
|
2236
1183
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
1184
|
+
Values appear in the expanded job name ordered by their dimension name, alphabetically, whichever
|
|
1185
|
+
order you write the keys in. The include entry above therefore becomes `test (23, linux)` — the
|
|
1186
|
+
`node` value then the `os` value, the same order as its expanded siblings `test (18, linux)` and
|
|
1187
|
+
`test (20, linux)`. That order is also the `byMatrix` key a downstream job reads its outputs under
|
|
1188
|
+
(see [Consuming matrix outputs downstream](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/#consuming-matrix-outputs-downstream)), so an include
|
|
1189
|
+
entry whose keys you wrote out of
|
|
1190
|
+
alphabetical order is keyed alphabetically too.
|
|
2240
1191
|
|
|
2241
|
-
|
|
2242
|
-
sliding window — each host that finishes (success or failure) releases the next held
|
|
2243
|
-
host. `maxParallel: 1` is a strictly serial, one-host-at-a-time rolling deploy. Must
|
|
2244
|
-
be `>= 1`.
|
|
2245
|
-
- **`failFast`** — when `true`, the first host failure halts the rollout: no further
|
|
2246
|
-
held hosts are started, and the remaining ones are marked skipped. Default `false`
|
|
2247
|
-
(every host runs regardless of sibling outcomes — the same as the unbounded fan-out).
|
|
1192
|
+
Types:
|
|
2248
1193
|
|
|
2249
1194
|
```typescript
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
onUnreachable: 'skip', // see the caveat below
|
|
2253
|
-
maxParallel: 1, // strictly one host at a time
|
|
2254
|
-
failFast: true, // stop the roll on the first failure
|
|
2255
|
-
run: async (ctx) => {
|
|
2256
|
-
/* patch ctx.host */
|
|
2257
|
-
},
|
|
2258
|
-
});
|
|
2259
|
-
```
|
|
2260
|
-
|
|
2261
|
-
Both options are **fan-out-generic** — they bound a `matrix` fan-out exactly the same
|
|
2262
|
-
way (the children are matrix combinations instead of hosts). They are ignored on a job
|
|
2263
|
-
with neither `matrix` nor `runsOnAll` (there is no fan-out to bound).
|
|
2264
|
-
|
|
2265
|
-
**Caveat — use `onUnreachable: 'skip'` or `'fail'` for rolling deploys, not `'hold'`.**
|
|
2266
|
-
A held host occupies a wave slot indefinitely while it waits to reconnect, stalling the
|
|
2267
|
-
roll behind an absent box. `skip` (run only reachable hosts) or `fail` (refuse the roll
|
|
2268
|
-
if any expected host is down) keep the window moving.
|
|
2269
|
-
|
|
2270
|
-
### Narrowing the roster at run time with `--target`
|
|
2271
|
-
|
|
2272
|
-
A `runsOnAll` predicate is authored once in the workflow, but you can narrow it for a
|
|
2273
|
-
single run with `kici run --target <selector>` — an Ansible-`--limit`-style runtime
|
|
2274
|
-
filter. The effective host set becomes `runsOnAll ∩ target`: the selector can only
|
|
2275
|
-
_remove_ hosts from the matched roster, never add them. The narrowing is **run-global**
|
|
2276
|
-
(it applies to every `runsOnAll` job) and **`runsOnAll`-only** (single `runsOn`-pinned
|
|
2277
|
-
jobs are untouched). Repeated `--target` values AND-combine — a host must satisfy every
|
|
2278
|
-
selector to survive.
|
|
2279
|
-
|
|
2280
|
-
```bash
|
|
2281
|
-
# Patch only the role:web subset of whatever role:* hosts the job would match
|
|
2282
|
-
kici run remote deploy --target role:web
|
|
2283
|
-
|
|
2284
|
-
# Intersect two selectors: hosts must be BOTH role:web AND dc:eu
|
|
2285
|
-
kici run remote deploy --target role:web --target dc:eu
|
|
1195
|
+
type MatrixInclude = Record<string, string>;
|
|
1196
|
+
type MatrixExclude = Record<string, string>;
|
|
2286
1197
|
```
|
|
2287
1198
|
|
|
2288
|
-
|
|
2289
|
-
(a mistyped selector should be loud, not silently no-op). Pass `--target-allow-empty`
|
|
2290
|
-
to **skip** the zeroed job instead — it records a `skipped` status, and downstream jobs
|
|
2291
|
-
gated with `when: 'on-skip'` (or `when: 'always'`) still run, exactly as for an
|
|
2292
|
-
`onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](https://docs.kici.dev/user/cli-reference/#host-narrowing-with---target)
|
|
2293
|
-
for the full flag behavior and the [`needs` gating model](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs)
|
|
2294
|
-
for how a skipped upstream propagates.
|
|
2295
|
-
|
|
2296
|
-
### Limits (v0)
|
|
2297
|
-
|
|
2298
|
-
- Per-host secret scoping is not yet available — all hosts receive the job's resolved
|
|
2299
|
-
secrets.
|
|
2300
|
-
|
|
2301
|
-
---
|
|
2302
|
-
|
|
2303
|
-
## SDK reference: runtime
|
|
2304
|
-
|
|
2305
|
-
Source: https://docs.kici.dev/user/sdk/runtime/
|
|
2306
|
-
|
|
2307
|
-
## Types
|
|
2308
|
-
|
|
2309
|
-
All types are exported from `@kici-dev/sdk` as type-only imports.
|
|
2310
|
-
|
|
2311
|
-
### Core types
|
|
2312
|
-
|
|
2313
|
-
| Type | Description |
|
|
2314
|
-
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2315
|
-
| `Workflow` | Workflow definition returned by `workflow()` |
|
|
2316
|
-
| `WorkflowOptions` | Options for `workflow()` factory |
|
|
2317
|
-
| `Job` | Job definition returned by `job()` |
|
|
2318
|
-
| `JobOptions` | Options for `job()` factory |
|
|
2319
|
-
| `Step<TOutputs>` | Step definition returned by `step()` |
|
|
2320
|
-
| `StepOptions<T>` | Options for `step()` factory (full form with outputs) |
|
|
2321
|
-
| `StepRunFn` | Simple step function type: `(ctx) => Promise<void>` |
|
|
2322
|
-
| `BareStepFn` | Bare step function (no options, just `(ctx) => ...`) |
|
|
2323
|
-
| `StepInput` | Union of step input forms accepted by `job()` |
|
|
2324
|
-
| `OutputSchema` | Record of Zod types for step outputs |
|
|
2325
|
-
| `InferOutputs<T>` | Infer output type from output schema |
|
|
2326
|
-
| `ContainerConfig` | Container config for job execution (`image`, `env?`) |
|
|
2327
|
-
| `RunsOn` | Union of `runsOn` forms: `string \| RegExp \| (string \| RegExp)[] \| RunsOnSelector`. A plain string matches exactly, a string with glob metacharacters (`*?[]{}`) is a glob, and a `RegExp` is a regular expression. See [Targeting by pattern](https://docs.kici.dev/user/sdk/core/#targeting-by-pattern). |
|
|
2328
|
-
| `RunsOnSelector` | Object form for `runsOn` with `labels` (required) and `exclude` (optional) properties. Each element accepts the exact / glob / regex forms on both sides. |
|
|
2329
|
-
| `RunsOnAllInput` | Union of `runsOnAll` host fan-out forms: `string \| RegExp \| (string \| RegExp)[] \| { include: { all: (string \| RegExp)[] }[]; exclude?: (string \| RegExp)[] }`. Same exact / glob / regex semantics per element. See [runsOnAll](https://docs.kici.dev/user/sdk/runs-on-all/#targeting-by-pattern). |
|
|
2330
|
-
| `Fixture` | Test fixture definition returned by `fixture()` |
|
|
2331
|
-
| `FixtureOptions` | Options for `fixture()` factory |
|
|
2332
|
-
| `Registry` | Private npm registry declaration used in `WorkflowOptions.registries` |
|
|
2333
|
-
|
|
2334
|
-
### Trigger types
|
|
2335
|
-
|
|
2336
|
-
| Type | Description |
|
|
2337
|
-
| ------------------------------- | --------------------------------------------------------------------- |
|
|
2338
|
-
| `Trigger` | Trigger definition (trigger config + source location) |
|
|
2339
|
-
| `TriggerConfig` | Union of all 22 trigger config types |
|
|
2340
|
-
| `PrTriggerConfig` | PR trigger configuration (from `pr()`) |
|
|
2341
|
-
| `PushTriggerConfig` | Push trigger configuration (from `push()`) |
|
|
2342
|
-
| `TagTriggerConfig` | Tag trigger configuration (from `tag()`) |
|
|
2343
|
-
| `CommentTriggerConfig` | Comment trigger configuration (from `comment()`) |
|
|
2344
|
-
| `ReviewTriggerConfig` | Review trigger configuration (from `review()`) |
|
|
2345
|
-
| `ReviewCommentTriggerConfig` | Review comment trigger configuration (from `reviewComment()`) |
|
|
2346
|
-
| `ReleaseTriggerConfig` | Release trigger configuration (from `release()`) |
|
|
2347
|
-
| `DispatchTriggerConfig` | Repository dispatch trigger configuration (from `dispatch()`) |
|
|
2348
|
-
| `CreateTriggerConfig` | Ref creation trigger configuration (from `create()`) |
|
|
2349
|
-
| `DeleteTriggerConfig` | Ref deletion trigger configuration (from `delete()`) |
|
|
2350
|
-
| `StatusTriggerConfig` | Commit status trigger configuration (from `status()`) |
|
|
2351
|
-
| `WorkflowRunTriggerConfig` | Workflow run trigger configuration (from `workflowRun()`) |
|
|
2352
|
-
| `ForkTriggerConfig` | Fork trigger configuration (from `fork()`) |
|
|
2353
|
-
| `StarTriggerConfig` | Star trigger configuration (from `star()`) |
|
|
2354
|
-
| `WatchTriggerConfig` | Watch trigger configuration (from `watch()`) |
|
|
2355
|
-
| `WebhookTriggerConfig` | Catch-all webhook trigger configuration (from `webhook()`) |
|
|
2356
|
-
| `KiciEventTriggerConfig` | Custom event trigger configuration (from `kiciEvent()`) |
|
|
2357
|
-
| `WorkflowCompleteTriggerConfig` | Workflow completion trigger configuration (from `workflowComplete()`) |
|
|
2358
|
-
| `JobCompleteTriggerConfig` | Job completion trigger configuration (from `jobComplete()`) |
|
|
2359
|
-
| `GenericWebhookTriggerConfig` | Generic webhook trigger configuration (from `genericWebhook()`) |
|
|
2360
|
-
| `ScheduleTriggerConfig` | Schedule trigger configuration (from `schedule()`) |
|
|
2361
|
-
| `LifecycleTriggerConfig` | Lifecycle trigger configuration (from `lifecycle()`) |
|
|
2362
|
-
| `PrConfigInput` | Config object for `pr()` factory |
|
|
2363
|
-
| `PushConfigInput` | Config object for `push()` factory |
|
|
2364
|
-
| `BranchPattern` | `{ type: 'glob', pattern } \| { type: 'regex', pattern, flags? }` |
|
|
2365
|
-
| `PrEvent` | PR event string literal union (17 event types) |
|
|
2366
|
-
| `GenericWebhookConfigInput` | Config object for `genericWebhook()` factory |
|
|
2367
|
-
| `GenericWebhookAuth` | Union of generic webhook auth types (HMAC or API key) |
|
|
2368
|
-
| `GenericWebhookHmacAuth` | HMAC-SHA256 auth configuration for generic webhooks |
|
|
2369
|
-
| `GenericWebhookApiKeyAuth` | API key auth configuration for generic webhooks |
|
|
2370
|
-
| `GenericWebhookAuthMethod` | Auth method string literal (`'hmac-sha256' \| 'api-key'`) |
|
|
2371
|
-
|
|
2372
|
-
### Rule types
|
|
2373
|
-
|
|
2374
|
-
| Type | Description |
|
|
2375
|
-
| ---------------------- | ----------------------------------------------------------------------- |
|
|
2376
|
-
| `Rule` | Rule definition returned by `rule()` / `skip()` |
|
|
2377
|
-
| `RuleCheckFn` | `(ctx: RuleContext) => Promise<boolean> \| boolean` |
|
|
2378
|
-
| `RuleContext` | Context passed to rule check functions |
|
|
2379
|
-
| `RuleResult` | Result of rule evaluation (label, passed, duration) |
|
|
2380
|
-
| `EventPayload` | Discriminated union over event type (narrow on `type` for autocomplete) |
|
|
2381
|
-
| `RuleEvaluationResult` | Result of `evaluateRules()` (allPassed + results) |
|
|
2382
|
-
|
|
2383
|
-
### Matrix types
|
|
2384
|
-
|
|
2385
|
-
| Type | Description |
|
|
2386
|
-
| ---------------------- | ------------------------------------------------------------------- |
|
|
2387
|
-
| `Matrix` | Union: `StaticMatrixArray \| StaticMatrixObject \| DynamicMatrixFn` |
|
|
2388
|
-
| `StaticMatrixArray` | `string[]` |
|
|
2389
|
-
| `StaticMatrixObject` | `Record<string, string[]>` |
|
|
2390
|
-
| `DynamicMatrixFn` | `(ctx) => Promise<StaticMatrixArray \| StaticMatrixObject>` |
|
|
2391
|
-
| `DynamicMatrixContext` | Context passed to dynamic matrix functions |
|
|
2392
|
-
| `MatrixValues` | Values exposed to steps (`value?` + named dimensions) |
|
|
2393
|
-
| `MatrixInclude` | `Record<string, string>` -- additional combinations |
|
|
2394
|
-
| `MatrixExclude` | `Record<string, string>` -- removed combinations |
|
|
2395
|
-
|
|
2396
|
-
### Hook types
|
|
2397
|
-
|
|
2398
|
-
| Type | Description |
|
|
2399
|
-
| ----------------- | --------------------------------------------------------------- |
|
|
2400
|
-
| `HookConfig` | Hook definition returned by hook factories (`onCancel()`, etc.) |
|
|
2401
|
-
| `HookFn` | Hook function type: `(ctx: HookContext) => Promise<void>` |
|
|
2402
|
-
| `HookInput` | Hook input: `HookFn \| { run: HookFn; timeout?: number }` |
|
|
2403
|
-
| `HookContext` | Context passed to hook functions |
|
|
2404
|
-
| `OutcomeMetadata` | Metadata about the outcome that triggered the hook |
|
|
2405
|
-
|
|
2406
|
-
### Dynamic job types
|
|
2407
|
-
|
|
2408
|
-
| Type | Description |
|
|
2409
|
-
| ------------------- | ---------------------------------- |
|
|
2410
|
-
| `DynamicJobFn` | `(ctx) => Promise<Job[]>` |
|
|
2411
|
-
| `DynamicJobContext` | Context for dynamic job generators |
|
|
2412
|
-
| `JobOrFactory` | `Job \| DynamicJobFn` |
|
|
2413
|
-
|
|
2414
|
-
### Context types
|
|
2415
|
-
|
|
2416
|
-
| Type | Description |
|
|
2417
|
-
| --------------------- | ------------------------------------------------------------------ |
|
|
2418
|
-
| `StepContext<T>` | Context passed to step run functions |
|
|
2419
|
-
| `Logger` | Logger interface (info, warn, error, debug) |
|
|
2420
|
-
| `WorkflowInfo` | Workflow metadata: `{ name: string }` |
|
|
2421
|
-
| `JobInfo` | Job metadata: `{ name: string, runsOn: string }` |
|
|
2422
|
-
| `RepoInfo` | Repository metadata available in step context |
|
|
2423
|
-
| `StepSecrets` | Async accessor interface for step secrets (`get`, `expose`, `has`) |
|
|
2424
|
-
| `StepSecretsTyped` | Typed step secrets with known key inference |
|
|
2425
|
-
| `KnownSecretKeys` | String literal union of declared secret context keys |
|
|
2426
|
-
| `SecretNotFoundError` | Thrown when accessing a nonexistent key in secrets |
|
|
2427
|
-
|
|
2428
|
-
## StepContext
|
|
2429
|
-
|
|
2430
|
-
The context object passed to every step's `run` function:
|
|
2431
|
-
|
|
2432
|
-
```typescript
|
|
2433
|
-
interface StepContext<TInputs = Record<string, unknown>> {
|
|
2434
|
-
/** zx shell executor for running commands */
|
|
2435
|
-
$: typeof Shell;
|
|
2436
|
-
/** Structured logger */
|
|
2437
|
-
log: Logger;
|
|
2438
|
-
/** Environment variables */
|
|
2439
|
-
env: Record<string, string | undefined>;
|
|
2440
|
-
/** Set an environment variable visible to this step and all subsequent steps */
|
|
2441
|
-
setEnv(key: string, value: string): void;
|
|
2442
|
-
/** Prepend a directory to PATH, visible to this step and all subsequent steps */
|
|
2443
|
-
addPath(dir: string): void;
|
|
2444
|
-
/** Typed inputs from dependency step outputs */
|
|
2445
|
-
inputs: TInputs;
|
|
2446
|
-
/** Current workflow metadata */
|
|
2447
|
-
workflow: WorkflowInfo;
|
|
2448
|
-
/** Current job metadata */
|
|
2449
|
-
job: JobInfo;
|
|
2450
|
-
/** Matrix values for current job instance (undefined without matrix) */
|
|
2451
|
-
matrix?: MatrixValues;
|
|
2452
|
-
/** Raw webhook payload from the git provider */
|
|
2453
|
-
rawPayload?: Record<string, unknown>;
|
|
2454
|
-
/** Which git provider triggered this workflow (e.g. 'github', 'gitlab') */
|
|
2455
|
-
provider?: string;
|
|
2456
|
-
/** Whether this execution was triggered by `kici run remote` (developer-initiated remote run) */
|
|
2457
|
-
isTestRun: boolean;
|
|
2458
|
-
/** The resolved deployment environment name for this job (undefined without environment) */
|
|
2459
|
-
environment?: string;
|
|
2460
|
-
/** Flat secrets resolved for this job. Throws SecretNotFoundError on missing key. */
|
|
2461
|
-
secrets: StepSecrets;
|
|
2462
|
-
/** Emit a custom event that can trigger other workflows */
|
|
2463
|
-
emit(
|
|
2464
|
-
eventName: string,
|
|
2465
|
-
payload?: Record<string, unknown>,
|
|
2466
|
-
options?: EventEmitOptions,
|
|
2467
|
-
): Promise<{ deliveryId: string }>;
|
|
2468
|
-
/** Resolve outputs from a preceding step by reference */
|
|
2469
|
-
outputsOf<T>(ref: { _tag: 'Step'; name: string } | ((...args: any[]) => any)): T;
|
|
2470
|
-
/** Resolve outputs from a preceding job by reference */
|
|
2471
|
-
jobOutputs(ref: { name: string }): Record<string, unknown>;
|
|
2472
|
-
/** Publish a secret output value from this job (encrypted before leaving the agent) */
|
|
2473
|
-
setSecretOutput(key: string, value: string): void;
|
|
2474
|
-
}
|
|
2475
|
-
```
|
|
1199
|
+
### MatrixValues
|
|
2476
1200
|
|
|
2477
|
-
|
|
1201
|
+
The shape of `matrix` in `StepContext`:
|
|
2478
1202
|
|
|
2479
1203
|
```typescript
|
|
2480
|
-
interface
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
error(message: string, ...args: unknown[]): void;
|
|
2484
|
-
debug(message: string, ...args: unknown[]): void;
|
|
1204
|
+
interface MatrixValues {
|
|
1205
|
+
value?: string; // Single-dimension value
|
|
1206
|
+
[dimension: string]: string | undefined; // Named dimensions
|
|
2485
1207
|
}
|
|
2486
|
-
```
|
|
2487
|
-
|
|
2488
|
-
### Usage
|
|
2489
|
-
|
|
2490
|
-
```typescript
|
|
2491
|
-
step('example', async ({ $, log, env, matrix, workflow, job }) => {
|
|
2492
|
-
log.info(`Running in workflow: ${workflow.name}`);
|
|
2493
|
-
log.info(`Job: ${job.name} on ${job.runsOn}`);
|
|
2494
|
-
|
|
2495
|
-
if (matrix) {
|
|
2496
|
-
log.info(`Matrix value: ${matrix.value}`);
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
const token = env.GITHUB_TOKEN;
|
|
2500
|
-
await $`echo "Building..."`;
|
|
2501
|
-
});
|
|
2502
|
-
```
|
|
2503
|
-
|
|
2504
|
-
### `rawPayload` and rule-context parity
|
|
2505
|
-
|
|
2506
|
-
`ctx.rawPayload` carries the same data that rule contexts access via `ctx.event.payload` — the unmodified webhook body from the git provider. A rule that branches on `ctx.event.payload.client_payload.foo` and a step body that reads `ctx.rawPayload.client_payload.foo` see the same value. Use it inside steps when the operator's dispatch payload (or any other provider-specific field) needs to drive runtime behavior — e.g. a `--dry-run` toggle or a deploy target — without bouncing the data through an env var.
|
|
2507
|
-
|
|
2508
|
-
**What's captured in the dashboard log viewer.** KiCI captures user output from every place in a workflow that can run TypeScript:
|
|
2509
|
-
|
|
2510
|
-
- **Inside a step body** — the agent merges three streams into the step's log: `ctx.log.*` structured calls, subprocess stdout/stderr from `ctx.$`, and any direct `console.log` / `.error` / `.warn` / `.info` / `.debug` (or other library that writes to `process.stdout` / `process.stderr`).
|
|
2511
|
-
- **Inside hooks** (`beforeStep`, `afterStep`, `onSuccess`, `onFailure`, `onCancel`, `cleanup`) — the same three streams are captured; per-step hooks share the step's log, post-loop hooks get their own dashboard row.
|
|
2512
|
-
- **At workflow module top-level, in rule `check` functions, and in the workflow `concurrency.group` function** — captured to the workflow-level `prepare` log bucket for the job, alongside KiCI's own setup narration.
|
|
2513
|
-
- **Inside a dynamic `environment` / `env` / `concurrencyGroup` function** on a static job — captured to the `__init__` job's synthetic step-0 log, which appears in the timeline as "Init: _jobname_".
|
|
2514
|
-
- **Inside a `DynamicJobFn` body and the per-generated-job `environment` / `env` / `concurrencyGroup` / `matrix` functions** — captured to the `__dynamic__` job's synthetic step-0 log ("Evaluate: _jobname_" in the timeline). The `$` parameter in that context is a scoped zx shell, so `await $\`...\`` subprocess output is captured too.
|
|
2515
|
-
|
|
2516
|
-
Use whichever style is convenient — you don't have to wrap `console.log` in the provided `log` parameter to make it visible. One limitation applies to in-process contexts only (init, build, dynamic-eval): direct `process.stdout.write` / `printf` is not captured there, because the agent's own logger uses that path and we don't want agent-internal output leaking into your step logs. Use `console.*` or the `log` parameter instead. See [Log streaming](https://docs.kici.dev/architecture/execution/job-execution/#log-streaming) for the full capture surface and limits (default 10 MB per step, backpressure behavior).
|
|
2517
|
-
|
|
2518
|
-
### setEnv(key, value)
|
|
2519
|
-
|
|
2520
|
-
Export an environment variable to later steps in the same job. This is the canonical way to hand a value computed in one step to the steps that follow — the equivalent of `echo "KEY=VALUE" >> $GITHUB_ENV` in GitHub Actions. The value is visible to the current step and all subsequent steps in the job.
|
|
2521
|
-
|
|
2522
|
-
```typescript
|
|
2523
|
-
step('setup', async (ctx) => {
|
|
2524
|
-
// Install a tool and record its version
|
|
2525
|
-
await ctx.$`npm install -g some-tool`;
|
|
2526
|
-
const version = (await ctx.$`some-tool --version`).stdout.trim();
|
|
2527
|
-
ctx.setEnv('TOOL_VERSION', version);
|
|
2528
|
-
});
|
|
2529
|
-
|
|
2530
|
-
step('use', async (ctx) => {
|
|
2531
|
-
// TOOL_VERSION is available here
|
|
2532
|
-
ctx.log.info(`Using tool version: ${ctx.env.TOOL_VERSION}`);
|
|
2533
|
-
});
|
|
2534
|
-
```
|
|
2535
|
-
|
|
2536
|
-
**Behavior:**
|
|
2537
|
-
|
|
2538
|
-
- Last-write-wins -- if multiple steps set the same key, the last value is used
|
|
2539
|
-
- Cannot override operator-injected secrets (the operator value takes precedence)
|
|
2540
|
-
- Changes take effect immediately in the current step and persist for all subsequent steps
|
|
2541
|
-
- Shell commands export the same way by appending to `$KICI_ENV` (see [Exporting env from shell commands](https://docs.kici.dev/user/sdk/runtime/#exporting-env-from-shell-commands-kici_env--kici_path) below)
|
|
2542
|
-
|
|
2543
|
-
### addPath(dir)
|
|
2544
|
-
|
|
2545
|
-
Prepend a directory to `PATH` for the current step and all subsequent steps in the same job. Useful for tools installed to non-standard locations.
|
|
2546
|
-
|
|
2547
|
-
```typescript
|
|
2548
|
-
step('install-go', async (ctx) => {
|
|
2549
|
-
await ctx.$`curl -L https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /tmp -xz`;
|
|
2550
|
-
ctx.addPath('/tmp/go/bin');
|
|
2551
|
-
});
|
|
2552
|
-
|
|
2553
|
-
step('build', async (ctx) => {
|
|
2554
|
-
// `go` is now on PATH
|
|
2555
|
-
await ctx.$`go build ./...`;
|
|
2556
|
-
});
|
|
2557
|
-
```
|
|
2558
|
-
|
|
2559
|
-
### Exporting env from shell commands ($KICI_ENV / $KICI_PATH)
|
|
2560
|
-
|
|
2561
|
-
`setEnv` and `addPath` are the TypeScript form of "export env to later steps". A shell command — including a non-JS toolchain installer — exports env the same way by appending to two files the agent points at before every step:
|
|
2562
|
-
|
|
2563
|
-
- **`$KICI_ENV`** — append `KEY=value` lines. Each becomes an environment variable visible to subsequent steps, exactly like `ctx.setEnv('KEY', 'value')`.
|
|
2564
|
-
- **`$KICI_PATH`** — append one directory per line. Each is prepended to `PATH` for subsequent steps, exactly like `ctx.addPath(dir)`. The first directory appended ends up first on `PATH`.
|
|
2565
|
-
|
|
2566
|
-
```typescript
|
|
2567
|
-
step('install-tool', async (ctx) => {
|
|
2568
|
-
await ctx.$`./install-mytool.sh`; // installs to /opt/mytool
|
|
2569
|
-
// Export from the shell, no JS round-trip needed:
|
|
2570
|
-
await ctx.$`echo "MYTOOL_HOME=/opt/mytool" >> "$KICI_ENV"`;
|
|
2571
|
-
await ctx.$`echo "/opt/mytool/bin" >> "$KICI_PATH"`;
|
|
2572
|
-
});
|
|
2573
|
-
|
|
2574
|
-
step('build', async (ctx) => {
|
|
2575
|
-
// MYTOOL_HOME is set and /opt/mytool/bin is on PATH here.
|
|
2576
|
-
await ctx.$`mytool build`;
|
|
2577
|
-
});
|
|
2578
|
-
```
|
|
2579
|
-
|
|
2580
|
-
**Format (v1):**
|
|
2581
|
-
|
|
2582
|
-
- One `KEY=value` per line in `$KICI_ENV`. The split is on the first `=`, so the value may contain `=`. Blank lines and lines without a `=` are ignored.
|
|
2583
|
-
- One directory per line in `$KICI_PATH`. Blank lines are ignored.
|
|
2584
|
-
- Values must be single-line — embedded newlines are not supported in v1.
|
|
2585
|
-
|
|
2586
|
-
**Behavior (shared with `setEnv` / `addPath`):**
|
|
2587
|
-
|
|
2588
|
-
- Applied after the step completes and visible to every later step in the job.
|
|
2589
|
-
- Last-write-wins on a repeated key.
|
|
2590
|
-
- Cannot override an operator-injected secret — a collision is ignored and logged, and the operator value is preserved.
|
|
2591
|
-
- The files are reset before each step, so each step sees only its own appended lines.
|
|
2592
|
-
|
|
2593
|
-
### setSecretOutput(key, value)
|
|
2594
|
-
|
|
2595
|
-
Publish an encrypted secret output from this job. Downstream jobs that list this job in their `needs` array receive the value merged into `ctx.secrets`.
|
|
2596
|
-
|
|
2597
|
-
```typescript
|
|
2598
|
-
const generateToken = job('generate-token', {
|
|
2599
|
-
steps: [
|
|
2600
|
-
step('create', async (ctx) => {
|
|
2601
|
-
const token = (await ctx.$`vault write -f auth/token/create`).stdout.trim();
|
|
2602
|
-
ctx.setSecretOutput('DEPLOY_TOKEN', token);
|
|
2603
|
-
}),
|
|
2604
|
-
],
|
|
2605
|
-
});
|
|
2606
|
-
|
|
2607
|
-
const deploy = job('deploy', {
|
|
2608
|
-
needs: [generateToken],
|
|
2609
|
-
steps: [
|
|
2610
|
-
step('deploy', async (ctx) => {
|
|
2611
|
-
// DEPLOY_TOKEN is available as a secret (decrypted by the orchestrator)
|
|
2612
|
-
const token = await ctx.secrets.get('DEPLOY_TOKEN');
|
|
2613
|
-
await ctx.$`DEPLOY_TOKEN=${token} ./deploy.sh`;
|
|
2614
|
-
}),
|
|
2615
|
-
],
|
|
2616
|
-
});
|
|
2617
|
-
```
|
|
2618
|
-
|
|
2619
|
-
**Security model:**
|
|
2620
|
-
|
|
2621
|
-
- The value is encrypted on the agent before leaving the machine (X25519 ECDH + AES-256-GCM)
|
|
2622
|
-
- The orchestrator decrypts and re-encrypts with its own key before storing
|
|
2623
|
-
- The ephemeral key pair is deleted when the run completes (forward secrecy)
|
|
2624
|
-
- Downstream agents never see the plaintext -- they receive it as part of their injected secrets
|
|
2625
|
-
|
|
2626
|
-
**Limits:**
|
|
2627
|
-
|
|
2628
|
-
- Maximum 20 secret outputs per job
|
|
2629
|
-
- Maximum 64 KB per value
|
|
1208
|
+
```
|
|
2630
1209
|
|
|
2631
|
-
###
|
|
1210
|
+
### Bounding matrix concurrency (maxParallel / failFast)
|
|
2632
1211
|
|
|
2633
|
-
|
|
1212
|
+
A matrix fan-out runs every combination at once by default. The fan-out-generic
|
|
1213
|
+
`maxParallel` and `failFast` job options bound it the same way they bound a
|
|
1214
|
+
[`runsOnAll`](https://docs.kici.dev/user/sdk/runs-on-all/#rolling-rollout-maxparallel--failfast) host fan-out:
|
|
2634
1215
|
|
|
2635
1216
|
```typescript
|
|
2636
|
-
const
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
1217
|
+
const test = job('test', {
|
|
1218
|
+
runsOn: 'linux',
|
|
1219
|
+
matrix: { os: ['ubuntu', 'macos', 'windows'] },
|
|
1220
|
+
maxParallel: 1, // run one combination at a time (sliding window)
|
|
1221
|
+
failFast: true, // stop launching combinations after the first failure
|
|
1222
|
+
run: async (ctx) => {
|
|
1223
|
+
/* ctx.matrix.os */
|
|
1224
|
+
},
|
|
2644
1225
|
});
|
|
2645
1226
|
```
|
|
2646
1227
|
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
- The step never holds platform credentials — the request is relayed through the orchestrator, which mints the token on the step's behalf.
|
|
2652
|
-
- Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
|
|
1228
|
+
`maxParallel` is a sliding window (each combination that finishes releases the next;
|
|
1229
|
+
`1` = serial; must be `>= 1`); `failFast` halts the fan-out on the first failure and
|
|
1230
|
+
skips the held remainder (default `false`). They are ignored on a job with no `matrix`
|
|
1231
|
+
or `runsOnAll`.
|
|
2653
1232
|
|
|
2654
|
-
###
|
|
1233
|
+
### Consuming matrix outputs downstream
|
|
2655
1234
|
|
|
2656
|
-
|
|
1235
|
+
A downstream job that lists a matrix job in its `needs` receives a **keyed envelope** instead of a flat outputs object, because the upstream produced N sets of outputs (one per combination). `ctx.jobOutputs(matrixJob)` returns a `MatrixJobOutputs`:
|
|
2657
1236
|
|
|
2658
1237
|
```typescript
|
|
2659
|
-
interface
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
platform: string | null;
|
|
2665
|
-
arch: string | null;
|
|
2666
|
-
lifecycleClass: 'static' | 'ephemeral';
|
|
2667
|
-
status: 'ready' | 'unreachable' | 'stale';
|
|
2668
|
-
lastSeen: string; // ISO timestamp
|
|
1238
|
+
interface MatrixJobOutputs<T = Record<string, unknown>> {
|
|
1239
|
+
/** Keyed by the combination suffix — the text inside `(...)` of the child name. */
|
|
1240
|
+
byMatrix: Record<string, T>;
|
|
1241
|
+
/** Last-write-wins flat merge across children, in child (name) order. */
|
|
1242
|
+
merged: T;
|
|
2669
1243
|
}
|
|
2670
1244
|
```
|
|
2671
1245
|
|
|
2672
|
-
|
|
1246
|
+
The suffix key matches the child job's display name: `byMatrix['a']` for a single-dimension `['a', 'b']` matrix, and for a multi-dimension combination the values ordered by dimension name — `byMatrix['arm64, linux']` for `{ arch: 'arm64', os: 'linux' }`. Use `isMatrixJobOutputs` (or `'byMatrix' in result`) to discriminate:
|
|
2673
1247
|
|
|
2674
1248
|
```typescript
|
|
2675
|
-
|
|
2676
|
-
const all = await ctx.kici.inventory.query();
|
|
1249
|
+
import { isMatrixJobOutputs } from '@kici-dev/sdk';
|
|
2677
1250
|
|
|
2678
|
-
|
|
2679
|
-
const
|
|
2680
|
-
|
|
1251
|
+
step('collect', async ({ jobOutputs }) => {
|
|
1252
|
+
const out = jobOutputs(buildMatrixJob);
|
|
1253
|
+
if (isMatrixJobOutputs(out)) {
|
|
1254
|
+
console.log(out.byMatrix['a']); // outputs of the `a` combination
|
|
1255
|
+
console.log(out.merged); // last-write-wins across all combinations
|
|
1256
|
+
}
|
|
2681
1257
|
});
|
|
2682
|
-
|
|
2683
|
-
// Property filtering is client-side — plain JS in the workflow:
|
|
2684
|
-
const euDbHosts = dbHosts.filter((h) => h.properties.region === 'eu');
|
|
2685
|
-
|
|
2686
|
-
// One host by id:
|
|
2687
|
-
const host = await ctx.kici.inventory.get('box-1'); // HostInventoryEntry | null
|
|
2688
1258
|
```
|
|
2689
1259
|
|
|
2690
|
-
|
|
1260
|
+
The downstream job waits for **all** matrix combinations to terminate before it dispatches. A non-matrix upstream keeps the flat outputs shape. The envelope is identical under `kici run <event> --local` and the remote path.
|
|
2691
1261
|
|
|
2692
|
-
|
|
1262
|
+
### Matrix type guards
|
|
2693
1263
|
|
|
2694
1264
|
```typescript
|
|
2695
|
-
|
|
2696
|
-
const hosts = await ctx.kici.inventory.query({
|
|
2697
|
-
include: [[{ kind: 'exact', value: 'role:db' }]],
|
|
2698
|
-
});
|
|
2699
|
-
return hosts
|
|
2700
|
-
.filter((h) => h.properties.region === 'eu')
|
|
2701
|
-
.map((h) =>
|
|
2702
|
-
job(`migrate-${h.agentId}`, {
|
|
2703
|
-
runsOn: [h.agentId],
|
|
2704
|
-
run: async (c) => {
|
|
2705
|
-
await c.$`./migrate.sh`;
|
|
2706
|
-
},
|
|
2707
|
-
}),
|
|
2708
|
-
);
|
|
2709
|
-
});
|
|
2710
|
-
```
|
|
2711
|
-
|
|
2712
|
-
A `runsOn` of a single host's `agentId` (as in `runsOn: [h.agentId]` above) **pins the job to that host**: the orchestrator routes it to that agent only, and queues it with the pin if the host is momentarily offline — the same host-pin path `runsOnAll` uses. A `runsOn` with multiple labels or a glob/regex pattern stays ordinary label routing.
|
|
2713
|
-
|
|
2714
|
-
`ctx.kici.inventory` is available to **both** steps and dynamic-job generators (unlike `ctx.kici.oidc.token`, which is job-bound — the inventory is cluster-scoped, not job-bound).
|
|
2715
|
-
|
|
2716
|
-
**Determinism caveat.** The inventory is **live**: it can change between when a dynamic-job generator first runs (at dispatch) and when it re-evaluates (at agent time). Generating jobs from `inventory.query()` therefore inherits the same non-determinism contract as `infrastructure.list()` — KiCI warns when the re-evaluated job set drifts (a sibling job name changed) and hard-errors when a targeted job vanishes. Prefer stable inputs where you can, and treat a fanned-out job set as a snapshot of the roster at generation time.
|
|
2717
|
-
|
|
2718
|
-
### ctx.attestProvenance({ subject })
|
|
1265
|
+
import { isStaticArray, isStaticObject, isDynamicFunction } from '@kici-dev/sdk';
|
|
2719
1266
|
|
|
2720
|
-
|
|
1267
|
+
isStaticArray(matrix); // true if string[]
|
|
1268
|
+
isStaticObject(matrix); // true if Record<string, string[]>
|
|
1269
|
+
isDynamicFunction(matrix); // true if async function
|
|
1270
|
+
```
|
|
2721
1271
|
|
|
2722
|
-
|
|
1272
|
+
### Matrix expansion utilities
|
|
2723
1273
|
|
|
2724
1274
|
```typescript
|
|
2725
|
-
|
|
2726
|
-
steps: [
|
|
2727
|
-
step('build', async (ctx) => {
|
|
2728
|
-
await ctx.$`npm pack`;
|
|
2729
|
-
}),
|
|
2730
|
-
step('attest', async (ctx) => {
|
|
2731
|
-
// Digest a file KiCI hashes for you:
|
|
2732
|
-
const result = await ctx.attestProvenance({
|
|
2733
|
-
subject: { name: 'my-pkg-1.2.3.tgz', path: 'my-pkg-1.2.3.tgz' },
|
|
2734
|
-
});
|
|
2735
|
-
ctx.log.info(`Attestation stored at ${result.storageKey}`);
|
|
2736
|
-
|
|
2737
|
-
// Or supply a precomputed digest (e.g. a container manifest digest):
|
|
2738
|
-
await ctx.attestProvenance({
|
|
2739
|
-
subject: { name: 'ghcr.io/acme/app', digest: { sha256: '<manifest-digest>' } },
|
|
2740
|
-
});
|
|
2741
|
-
}),
|
|
2742
|
-
],
|
|
2743
|
-
});
|
|
1275
|
+
import { expandMatrix, applyIncludeExclude } from '@kici-dev/sdk';
|
|
2744
1276
|
```
|
|
2745
1277
|
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
- The attestation is a signed [DSSE](https://github.com/secure-systems-lab/dsse) envelope over an [in-toto](https://in-toto.io) statement carrying the [SLSA v1.0](https://slsa.dev/spec/v1.0/provenance) provenance predicate.
|
|
2749
|
-
- It is signed with an ephemeral key bound to a platform-minted identity token, so it is **offline-verifiable** against the platform's published signing keys — no online lookup needed at verify time.
|
|
2750
|
-
- The bundle is persisted to object storage and recorded so the dashboard can show it and `kici verify-attestation` can retrieve it.
|
|
2751
|
-
- The returned `{ storageKey, subjectDigest, bundleMediaType }` identifies the stored bundle.
|
|
2752
|
-
- Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
|
|
1278
|
+
`expandMatrix(matrix)` takes a string array or an object of string arrays and returns all combinations as `MatrixValues[]`. For a single-dimension array, each value becomes `{ value: '...' }`. For multi-dimensional objects, it produces the Cartesian product. Anything else throws a `MatrixShapeError` naming the expected shape; numbers and booleans inside the values are accepted and converted to strings.
|
|
2753
1279
|
|
|
2754
|
-
|
|
2755
|
-
verify → view journey, including how to verify a bundle with `kici verify-attestation`.
|
|
1280
|
+
`applyIncludeExclude(values, include?, exclude?)` filters an expanded matrix: removes combinations matching any exclude entry, then appends a key-sorted copy of each include entry that is not already present. Returns the filtered `MatrixValues[]`.
|
|
2756
1281
|
|
|
2757
|
-
##
|
|
2758
|
-
|
|
2759
|
-
Workflows access secrets through `ctx.secrets` on `StepContext`. Use `await ctx.secrets.get('KEY')` to retrieve a value (rejects with `SecretNotFoundError` if the key is missing, fail-fast on typos), `ctx.secrets.has('KEY')` for a synchronous existence check, and `await ctx.secrets.expose('KEY')` when you need the value as a `process.env` entry for a child process.
|
|
1282
|
+
## Dynamic jobs
|
|
2760
1283
|
|
|
2761
|
-
|
|
1284
|
+
Generate jobs at runtime using async factory functions.
|
|
2762
1285
|
|
|
2763
|
-
|
|
1286
|
+
### DynamicJobFn
|
|
2764
1287
|
|
|
2765
1288
|
```typescript
|
|
2766
|
-
|
|
2767
|
-
runsOn: 'linux',
|
|
2768
|
-
context: 'production',
|
|
2769
|
-
steps: [
|
|
2770
|
-
/* ... */
|
|
2771
|
-
],
|
|
2772
|
-
});
|
|
2773
|
-
|
|
2774
|
-
export default workflow('deploy', {
|
|
2775
|
-
on: push({ branches: 'main' }),
|
|
2776
|
-
jobs: [deploy],
|
|
2777
|
-
});
|
|
1289
|
+
type DynamicJobFn = (context: DynamicJobContext) => Promise<Job[]>;
|
|
2778
1290
|
```
|
|
2779
1291
|
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
### Accessing secrets (ctx.secrets)
|
|
1292
|
+
Receives a `DynamicJobContext`:
|
|
2783
1293
|
|
|
2784
|
-
|
|
1294
|
+
| Property | Type | Description |
|
|
1295
|
+
| -------- | ----------------------------------- | --------------------------- |
|
|
1296
|
+
| `$` | zx shell | Shell executor |
|
|
1297
|
+
| `ctx` | `{ workflow, event? }` | Workflow metadata and event |
|
|
1298
|
+
| `log` | `Logger` | Structured logger |
|
|
1299
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
2785
1300
|
|
|
2786
1301
|
```typescript
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
const
|
|
1302
|
+
const discoverJobs: DynamicJobFn = async ({ $ }) => {
|
|
1303
|
+
const result = await $`ls packages/`;
|
|
1304
|
+
const packages = result.stdout.trim().split('\n');
|
|
1305
|
+
return packages.map((pkg) =>
|
|
1306
|
+
job(`test-${pkg}`, {
|
|
1307
|
+
runsOn: 'linux',
|
|
1308
|
+
steps: [
|
|
1309
|
+
step('test', async ({ $ }) => {
|
|
1310
|
+
await $`cd packages/${pkg} && pnpm test`;
|
|
1311
|
+
}),
|
|
1312
|
+
],
|
|
1313
|
+
}),
|
|
1314
|
+
);
|
|
1315
|
+
};
|
|
2790
1316
|
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
const optional = await secrets.get('OPTIONAL_KEY');
|
|
2794
|
-
}
|
|
1317
|
+
export default workflow('ci', {
|
|
1318
|
+
jobs: [discoverJobs],
|
|
2795
1319
|
});
|
|
2796
1320
|
```
|
|
2797
1321
|
|
|
2798
|
-
|
|
1322
|
+
### dynamicJob — result-aware generation
|
|
1323
|
+
|
|
1324
|
+
`dynamicJob(group, fnOrConfig)` tags a generator with a group name (so static jobs can depend on it via `needs: [dynamicGroup('group')]`). It is polymorphic:
|
|
2799
1325
|
|
|
2800
|
-
|
|
1326
|
+
- **Function form** — event-only, dispatched at webhook time: `dynamicJob('shards', async ({ ctx }) => [...])`.
|
|
1327
|
+
- **Options-object form** — result-aware, deferred until its declared `needs` complete, then run with the upstreams' frozen outputs as `ctx.needs`: `dynamicJob('reports', { needs, generate })`.
|
|
2801
1328
|
|
|
2802
1329
|
```typescript
|
|
2803
|
-
import { workflow, job, step,
|
|
1330
|
+
import { workflow, job, step, dynamicJob, dynamicGroup, z } from '@kici-dev/sdk';
|
|
2804
1331
|
|
|
2805
|
-
|
|
1332
|
+
// Upstream job A discovers a list of targets at runtime.
|
|
1333
|
+
const discover = job('discover', {
|
|
2806
1334
|
runsOn: 'linux',
|
|
2807
|
-
environment: 'production',
|
|
2808
1335
|
steps: [
|
|
2809
|
-
step('
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
// Safe check before access
|
|
2813
|
-
if (ctx.secrets.has('OPTIONAL_NOTIFICATION_URL')) {
|
|
2814
|
-
const url = await ctx.secrets.get('OPTIONAL_NOTIFICATION_URL');
|
|
2815
|
-
ctx.log.info('Sending notification...');
|
|
2816
|
-
}
|
|
2817
|
-
|
|
2818
|
-
// Pass to subprocess explicitly (secrets are NOT auto-injected as env vars)
|
|
2819
|
-
await ctx.$`DEPLOY_TOKEN=${token} ./scripts/deploy.sh`;
|
|
1336
|
+
step('emit', {
|
|
1337
|
+
outputs: { targets: z.array(z.string()) },
|
|
1338
|
+
run: async () => ({ targets: ['api', 'web'] }),
|
|
2820
1339
|
}),
|
|
2821
1340
|
],
|
|
2822
1341
|
});
|
|
2823
1342
|
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
1343
|
+
// Result-aware generator fans out one report job per discovered target.
|
|
1344
|
+
const reports = dynamicJob('reports', {
|
|
1345
|
+
needs: ['discover'],
|
|
1346
|
+
generate: async ({ ctx }) => {
|
|
1347
|
+
const targets = ctx.needs.discover.result.targets; // OutputProxy over discover's outputs
|
|
1348
|
+
return targets.map((target) =>
|
|
1349
|
+
job(`report-${target}`, {
|
|
1350
|
+
runsOn: 'linux',
|
|
1351
|
+
run: async ({ log }) => log.info(`reporting on ${target}`),
|
|
1352
|
+
}),
|
|
1353
|
+
);
|
|
1354
|
+
},
|
|
2827
1355
|
});
|
|
2828
|
-
```
|
|
2829
|
-
|
|
2830
|
-
### Security notes
|
|
2831
|
-
|
|
2832
|
-
- Secrets are **not** automatically injected as environment variables. You must explicitly pass them to subprocesses.
|
|
2833
|
-
- All secret values are automatically **masked** in log output. If a step logs a string containing a secret value, the value is replaced with `***`.
|
|
2834
|
-
- Secrets flow from the orchestrator to the agent via the authenticated WebSocket channel. The Platform tier never handles secret material.
|
|
2835
|
-
|
|
2836
|
-
### Enumerating available keys (ctx.secrets.list)
|
|
2837
1356
|
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
```typescript
|
|
2841
|
-
step('discover', async (ctx) => {
|
|
2842
|
-
const ageKeys = ctx.secrets.list().filter((k) => k.startsWith('AGE_KEY_'));
|
|
2843
|
-
ctx.log.info(`Found ${ageKeys.length} age keys`);
|
|
2844
|
-
});
|
|
1357
|
+
export default workflow('discovery-fan-out', { jobs: [discover, reports] });
|
|
2845
1358
|
```
|
|
2846
1359
|
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
Tools that require a file path on disk (sops `SOPS_AGE_KEY_FILE`, kubectl `KUBECONFIG`, gcloud `GOOGLE_APPLICATION_CREDENTIALS`) get a typed step-side API: `ctx.secrets.mountFile(opts)` writes the concatenation of one or more existing secrets to a per-step tmpfile and returns the path; `ctx.secrets.exposeFile(envVar, opts)` additionally sets `process.env[envVar] = path`. Files are removed and env vars are unset automatically when the step completes (success, failure, or timeout) — no manual cleanup. See [Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files) for the full options table, lifecycle details, and the canonical sops example.
|
|
2850
|
-
|
|
2851
|
-
### Local test mode secrets
|
|
2852
|
-
|
|
2853
|
-
When running `kici preview`, you can provide secrets locally without an orchestrator.
|
|
2854
|
-
|
|
2855
|
-
#### .kici/.secrets file
|
|
2856
|
-
|
|
2857
|
-
Create a `.kici/.secrets` file in your project (auto-gitignored by `kici init`):
|
|
2858
|
-
|
|
2859
|
-
```ini
|
|
2860
|
-
# Flat secrets (before any section)
|
|
2861
|
-
DEPLOY_TOKEN=my-deploy-token
|
|
2862
|
-
API_KEY=my-api-key
|
|
2863
|
-
|
|
2864
|
-
# Context-scoped secrets
|
|
2865
|
-
[production]
|
|
2866
|
-
DB_PASSWORD=prod-secret
|
|
2867
|
-
API_KEY=prod-key
|
|
2868
|
-
|
|
2869
|
-
[npm-publish]
|
|
2870
|
-
NPM_TOKEN=npm-abc123
|
|
2871
|
-
```
|
|
1360
|
+
`ctx.needs` shape:
|
|
2872
1361
|
|
|
2873
|
-
|
|
1362
|
+
| Need form | `ctx.needs[...]` value |
|
|
1363
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1364
|
+
| `'jobName'` / `{ name, when }` | `{ result, status }` — `result` is an `OutputProxy` (`ctx.needs.<job>.result.<step>.<field>`; single-step `run` jobs flatten to `ctx.needs.<job>.result.<field>`); `status` is the upstream's terminal status |
|
|
1365
|
+
| `dynamicGroup('g')` / `dynamicGroup('g', { when })` | ordered array of `{ name, result, status }`, one per group member |
|
|
2874
1366
|
|
|
2875
|
-
|
|
1367
|
+
`ctx.needs` is deterministic — a snapshot of upstream outputs frozen at first eval and replayed unchanged on re-eval, like `ctx.event`. Use result-aware generation for same-run fan-out from a prior job's result; use [`jobComplete()`](https://docs.kici.dev/user/sdk/triggers/) for cross-workflow reactions to a job finishing. See the architecture deep-dive in [dynamic jobs](https://docs.kici.dev/architecture/execution/dynamic-jobs/#result-aware-generation).
|
|
2876
1368
|
|
|
2877
|
-
|
|
1369
|
+
### JobOrFactory
|
|
2878
1370
|
|
|
2879
|
-
|
|
2880
|
-
# Inject flat secrets (repeatable)
|
|
2881
|
-
kici preview push --secret DEPLOY_TOKEN=my-token --secret API_KEY=my-key
|
|
1371
|
+
The `jobs` array in `WorkflowOptions` accepts both static jobs and dynamic generators:
|
|
2882
1372
|
|
|
2883
|
-
|
|
2884
|
-
|
|
1373
|
+
```typescript
|
|
1374
|
+
type JobOrFactory = Job | DynamicJobFn;
|
|
2885
1375
|
```
|
|
2886
1376
|
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
## Fixtures
|
|
2890
|
-
|
|
2891
|
-
Test fixtures define event replicas for `kici run remote`. They simulate trigger events without requiring real webhooks.
|
|
1377
|
+
### isDynamicJobFn(item)
|
|
2892
1378
|
|
|
2893
|
-
|
|
1379
|
+
Type guard to distinguish static jobs from dynamic generators:
|
|
2894
1380
|
|
|
2895
1381
|
```typescript
|
|
2896
|
-
function
|
|
2897
|
-
id: string,
|
|
2898
|
-
options: FixtureOptions | (() => FixtureOptions | Promise<FixtureOptions>),
|
|
2899
|
-
): Fixture;
|
|
1382
|
+
function isDynamicJobFn(item: JobOrFactory): item is DynamicJobFn;
|
|
2900
1383
|
```
|
|
2901
1384
|
|
|
2902
|
-
**Parameters:**
|
|
2903
|
-
|
|
2904
|
-
- `id` — unique fixture name (no whitespace). Used in `kici run remote <id>`.
|
|
2905
|
-
- `options` — a `FixtureOptions` object, or an async factory function returning one.
|
|
2906
|
-
|
|
2907
1385
|
```typescript
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
1386
|
+
for (const item of workflow.jobs) {
|
|
1387
|
+
if (isDynamicJobFn(item)) {
|
|
1388
|
+
const generatedJobs = await item(context);
|
|
1389
|
+
} else {
|
|
1390
|
+
// item is Job
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
2913
1393
|
```
|
|
2914
1394
|
|
|
2915
|
-
### FixtureOptions
|
|
2916
|
-
|
|
2917
|
-
| Property | Type | Description |
|
|
2918
|
-
| -------------- | ------------------------ | ---------------------------------------------------------- |
|
|
2919
|
-
| `event` | `TriggerConfig` | The trigger event to simulate (required) |
|
|
2920
|
-
| `branch` | `string` | Override branch name (defaults to git-detected) |
|
|
2921
|
-
| `sha` | `string` | Override commit SHA (defaults to HEAD) |
|
|
2922
|
-
| `repo` | `string` | Override repository (defaults to git-detected) |
|
|
2923
|
-
| `pr` | `number` | For PR events, override PR number |
|
|
2924
|
-
| `secrets` | `Record<string, string>` | Secret context mappings: `{ localName: 'remote-context' }` |
|
|
2925
|
-
| `workflowName` | `string` | Bypass trigger matching and run this workflow directly |
|
|
2926
|
-
|
|
2927
|
-
Options can also be provided as an async factory function for dynamic fixture generation.
|
|
2928
|
-
|
|
2929
1395
|
---
|
|
2930
1396
|
|
|
2931
1397
|
## SDK reference: triggers
|
|
@@ -2934,7 +1400,7 @@ Source: https://docs.kici.dev/user/sdk/triggers/
|
|
|
2934
1400
|
|
|
2935
1401
|
## Triggers
|
|
2936
1402
|
|
|
2937
|
-
Triggers define when a workflow runs. KiCI provides
|
|
1403
|
+
Triggers define when a workflow runs. KiCI provides 23 trigger types: 16 GitHub webhook triggers and 7 internal/generic triggers for event routing, scheduling, and non-GitHub sources. Each trigger returns a frozen config object with a unique `_tag` discriminator.
|
|
2938
1404
|
|
|
2939
1405
|
All triggers use a config object form -- pass an options object to configure the trigger.
|
|
2940
1406
|
|
|
@@ -3012,6 +1478,17 @@ push({ branches: ['main', 'develop'], paths: ['src/**'] });
|
|
|
3012
1478
|
push({ tags: ['v*'] });
|
|
3013
1479
|
```
|
|
3014
1480
|
|
|
1481
|
+
### Path filter behavior
|
|
1482
|
+
|
|
1483
|
+
A `pr()` or `push()` trigger with `paths` matches the event's changed files
|
|
1484
|
+
against your patterns. An available list is matched exactly (an event with no
|
|
1485
|
+
matching change — including a diff-less branch create or delete — does not run).
|
|
1486
|
+
When the diff is **unavailable** (chiefly a universal-git pull-request event,
|
|
1487
|
+
whose webhook carries no diff), path filters match **conservatively** so the
|
|
1488
|
+
workflow runs rather than being silently dropped, and the delivery is recorded
|
|
1489
|
+
as degraded. GitHub always provides an exact list; a transient API error fails
|
|
1490
|
+
loudly, not as empty.
|
|
1491
|
+
|
|
3015
1492
|
### tag()
|
|
3016
1493
|
|
|
3017
1494
|
Create a tag trigger. Returns a frozen `TagTriggerConfig`.
|
|
@@ -3300,7 +1777,7 @@ The orchestrator emits `kici_cross_source_fanout_size` (histogram) per inbound w
|
|
|
3300
1777
|
|
|
3301
1778
|
### Event triggers
|
|
3302
1779
|
|
|
3303
|
-
The following
|
|
1780
|
+
The following 7 trigger types support internal event routing, scheduling, lifecycle orchestration, and non-GitHub webhook sources.
|
|
3304
1781
|
|
|
3305
1782
|
### kiciEvent()
|
|
3306
1783
|
|
|
@@ -3356,6 +1833,51 @@ workflowComplete({ name: 'CI', status: ['success'] }); // Success only
|
|
|
3356
1833
|
workflowComplete({ name: 'CI', status: ['success'], source: 'org/repo' }); // Cross-repo
|
|
3357
1834
|
```
|
|
3358
1835
|
|
|
1836
|
+
### workflowsFailedBatch()
|
|
1837
|
+
|
|
1838
|
+
Create a batched failure trigger. Instead of firing once per failed workflow, it accumulates every failed workflow completion over a time window and fires the subscribing workflow **once** with the whole list — so a mass incident (a bad deploy failing hundreds of runs at once) notifies a single time, not once per failure. Returns a frozen `WorkflowsFailedBatchTriggerConfig`.
|
|
1839
|
+
|
|
1840
|
+
```typescript
|
|
1841
|
+
function workflowsFailedBatch(
|
|
1842
|
+
config: WorkflowsFailedBatchConfigInput,
|
|
1843
|
+
): WorkflowsFailedBatchTriggerConfig;
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
**Config options:**
|
|
1847
|
+
|
|
1848
|
+
```typescript
|
|
1849
|
+
interface WorkflowsFailedBatchConfigInput {
|
|
1850
|
+
accumulateFor: number; // Accumulation window in milliseconds (opens on the first failure)
|
|
1851
|
+
name?: string; // Filter by failed workflow name
|
|
1852
|
+
source?: string; // Cross-repo source filter
|
|
1853
|
+
description?: string;
|
|
1854
|
+
}
|
|
1855
|
+
```
|
|
1856
|
+
|
|
1857
|
+
The first failure inside the window opens it; when the window closes, the subscribing workflow is dispatched once. The batch is delivered on `ctx.event.payload`:
|
|
1858
|
+
|
|
1859
|
+
```typescript
|
|
1860
|
+
// ctx.event.payload for a workflowsFailedBatch dispatch:
|
|
1861
|
+
// {
|
|
1862
|
+
// total: number, // total failures in the window
|
|
1863
|
+
// runs: Array<{ // the failed runs (bounded — the first 200)
|
|
1864
|
+
// runId: string;
|
|
1865
|
+
// repo: string;
|
|
1866
|
+
// workflowName: string;
|
|
1867
|
+
// failureClass?: string; // why the run failed
|
|
1868
|
+
// senderUsername?: string; // triggering actor, when known
|
|
1869
|
+
// }>,
|
|
1870
|
+
// }
|
|
1871
|
+
```
|
|
1872
|
+
|
|
1873
|
+
```typescript
|
|
1874
|
+
workflowsFailedBatch({ accumulateFor: 10000 }); // One notification per 10s burst of failures
|
|
1875
|
+
workflowsFailedBatch({ accumulateFor: 30000, name: 'CI' }); // Only CI failures
|
|
1876
|
+
workflowsFailedBatch({ accumulateFor: 30000, source: 'org/repo' }); // Cross-repo source filter
|
|
1877
|
+
```
|
|
1878
|
+
|
|
1879
|
+
A workflow dispatched by a failure trigger (`workflowsFailedBatch`, or `workflowComplete({ status: ['failed'] })`) never re-triggers the same batch on its own failure — a notifier that itself fails cannot loop.
|
|
1880
|
+
|
|
3359
1881
|
### jobComplete()
|
|
3360
1882
|
|
|
3361
1883
|
Create a job completion trigger. Fires automatically when a specific job within a workflow finishes. Returns a frozen `JobCompleteTriggerConfig`.
|
|
@@ -3447,6 +1969,14 @@ schedule({ cron: '0 9 * * 1', timezone: 'America/New_York' }); // Monday 9am ET
|
|
|
3447
1969
|
schedule({ cron: '*/15 * * * *', description: 'health check every 15 min' });
|
|
3448
1970
|
```
|
|
3449
1971
|
|
|
1972
|
+
A workflow may declare **multiple** `schedule()` triggers. Each schedule is
|
|
1973
|
+
evaluated and fired independently — a `Monday 9am` schedule and a `Friday 6pm`
|
|
1974
|
+
schedule on the same workflow both run at their own times:
|
|
1975
|
+
|
|
1976
|
+
```typescript
|
|
1977
|
+
on: [schedule({ cron: '0 9 * * 1' }), schedule({ cron: '0 18 * * 5' })];
|
|
1978
|
+
```
|
|
1979
|
+
|
|
3450
1980
|
#### Schedule inputs (defaults-only)
|
|
3451
1981
|
|
|
3452
1982
|
A `schedule()` trigger may declare typed `inputs`. A cron or dashboard
|
|
@@ -3621,6 +2151,14 @@ Workflow steps can emit custom events via `ctx.emit()`. Emitted events are deliv
|
|
|
3621
2151
|
### ctx.emit(eventName, payload?, options?)
|
|
3622
2152
|
|
|
3623
2153
|
```typescript
|
|
2154
|
+
// Typed — a defineEvent() definition drives payload type-checking
|
|
2155
|
+
emit<T extends z.ZodTypeAny>(
|
|
2156
|
+
definition: EventDefinition<T>,
|
|
2157
|
+
payload: z.infer<T>,
|
|
2158
|
+
options?: EventEmitOptions,
|
|
2159
|
+
): Promise<{ deliveryId: string }>;
|
|
2160
|
+
|
|
2161
|
+
// Ad-hoc — event name string, payload typed as Record<string, unknown>
|
|
3624
2162
|
emit(
|
|
3625
2163
|
eventName: string,
|
|
3626
2164
|
payload?: Record<string, unknown>,
|
|
@@ -3630,17 +2168,30 @@ emit(
|
|
|
3630
2168
|
|
|
3631
2169
|
**Parameters:**
|
|
3632
2170
|
|
|
3633
|
-
| Parameter | Type | Required | Description
|
|
3634
|
-
| ---------------- | ------------------------- | -------- |
|
|
3635
|
-
| `
|
|
3636
|
-
| `
|
|
3637
|
-
| `
|
|
2171
|
+
| Parameter | Type | Required | Description |
|
|
2172
|
+
| ---------------- | ------------------------- | -------- | ---------------------------------------------------------------------------- |
|
|
2173
|
+
| `definition` | `EventDefinition<T>` | — | A `defineEvent()` definition; its Zod schema types `payload` as `z.infer<T>` |
|
|
2174
|
+
| `eventName` | `string` | yes | Name of the event to emit |
|
|
2175
|
+
| `payload` | `Record<string, unknown>` | no | Event payload data |
|
|
2176
|
+
| `options.target` | `{ repos?: string[] }` | no | Target specific repos for cross-repo delivery |
|
|
3638
2177
|
|
|
3639
2178
|
**Returns:** `Promise<{ deliveryId: string }>` -- a delivery receipt after the event is persisted and routed.
|
|
3640
2179
|
|
|
3641
2180
|
**Examples:**
|
|
3642
2181
|
|
|
3643
2182
|
```typescript
|
|
2183
|
+
// Typed emit — payload is checked against the deploy-complete schema
|
|
2184
|
+
import { defineEvent, z } from '@kici-dev/sdk';
|
|
2185
|
+
|
|
2186
|
+
const deployComplete = defineEvent(
|
|
2187
|
+
'deploy-complete',
|
|
2188
|
+
z.object({ env: z.string(), version: z.string() }),
|
|
2189
|
+
);
|
|
2190
|
+
|
|
2191
|
+
step('notify-typed', async (ctx) => {
|
|
2192
|
+
await ctx.emit(deployComplete, { env: 'prod', version: '1.2.3' });
|
|
2193
|
+
});
|
|
2194
|
+
|
|
3644
2195
|
// Emit a simple event
|
|
3645
2196
|
step('notify', async (ctx) => {
|
|
3646
2197
|
await ctx.emit('deploy-complete', { env: 'prod', version: '1.2.3' });
|
|
@@ -3690,139 +2241,27 @@ The orchestrator automatically emits system events for workflow and job completi
|
|
|
3690
2241
|
|
|
3691
2242
|
---
|
|
3692
2243
|
|
|
3693
|
-
## SDK reference: waitFor
|
|
3694
|
-
|
|
3695
|
-
Source: https://docs.kici.dev/user/sdk/wait-for/
|
|
3696
|
-
|
|
3697
|
-
The SDK exposes two wait-for helpers — a generic function `waitFor()` and a step factory `waitForStep()` — for the common case where a workflow step should:
|
|
3698
|
-
|
|
3699
|
-
1. **Poll** a condition on a fixed interval.
|
|
3700
|
-
2. **Proceed** as soon as the condition is met, optionally running a success action.
|
|
3701
|
-
3. **Fail or recover** gracefully when the deadline is exceeded, with an optional timeout action.
|
|
3702
|
-
|
|
3703
|
-
Both helpers wrap the same polling loop, so they share semantics and return shape. Pick `waitForStep()` when the wait is the whole job of a step; use `waitFor()` from anywhere — inside a multi-action step, a hook, or a bare async function.
|
|
3704
|
-
|
|
3705
|
-
## `waitFor(options)`
|
|
3706
|
-
|
|
3707
|
-
Poll `check()` on a fixed interval until it returns a non-null value or the deadline is exceeded. Resolves to a discriminated result describing which outcome occurred.
|
|
3708
|
-
|
|
3709
|
-
### Parameters
|
|
3710
|
-
|
|
3711
|
-
| Name | Type | Required | Description |
|
|
3712
|
-
| ---------------- | ---------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
|
|
3713
|
-
| `name` | `string` | No | Label that appears in log lines and in the timeout error. Defaults to `'waitFor'`. |
|
|
3714
|
-
| `check` | `() => Promise<TValue \| null>` | Yes | Polled inspection. Return the resolved value when the condition is met, or `null` to keep polling. |
|
|
3715
|
-
| `intervalMs` | `number` | No | Time between successive `check()` invocations. Defaults to `2000` milliseconds. |
|
|
3716
|
-
| `timeoutMs` | `number` | No | Total time budget for the wait. Defaults to `60000` milliseconds. |
|
|
3717
|
-
| `initialDelayMs` | `number` | No | Time to wait before the first `check()` invocation. Defaults to `0`. |
|
|
3718
|
-
| `onSuccess` | `(value: TValue) => Promise<TSuccess>` | No | Runs once after `check()` returns a non-null value. Its return value is surfaced as `result` on success. |
|
|
3719
|
-
| `onTimeout` | `(info: { elapsedMs: number; attempts: number }) => Promise<TTimeout>` | No | Runs when the deadline is exceeded. Its return value is surfaced as `result` on the `'timed-out'` outcome. |
|
|
3720
|
-
| `swallowErrors` | `boolean` | No | When `true` (default), errors thrown by `check()` are logged and polling continues. |
|
|
3721
|
-
| `log` | `(line: string) => void` | No | Sink for status lines. Defaults to `console.log`. |
|
|
3722
|
-
|
|
3723
|
-
### Result
|
|
3724
|
-
|
|
3725
|
-
`waitFor()` resolves to a discriminated `WaitForResult` union:
|
|
3726
|
-
|
|
3727
|
-
| Outcome | Branch fields |
|
|
3728
|
-
| ------------- | ----------------------------------------------------------------------------------------------------- |
|
|
3729
|
-
| `'succeeded'` | `value: TValue`, `elapsedMs`, `attempts`, `result: TSuccess` (the `onSuccess` return or `undefined`). |
|
|
3730
|
-
| `'timed-out'` | `elapsedMs`, `attempts`, `result: TTimeout` (the `onTimeout` return). |
|
|
3731
|
-
|
|
3732
|
-
Narrow on `result.outcome` before reading the branch-specific fields.
|
|
3733
|
-
|
|
3734
|
-
When `onTimeout` is **not** supplied, the helper throws a `WaitForTimeoutError` instead of returning a `'timed-out'` result. The error exposes `stepName`, `elapsedMs`, and `attempts` as instance fields so a catch block can branch on them.
|
|
3735
|
-
|
|
3736
|
-
### Cancellation and the deadline check
|
|
3737
|
-
|
|
3738
|
-
The loop inspects the deadline at the top of each iteration. A `check()` that takes longer than `intervalMs` is not aborted mid-flight; the helper has no `AbortSignal` plumbing. The step's own `timeout` field is the hard kill if the step needs to be interrupted unconditionally.
|
|
3739
|
-
|
|
3740
|
-
### Example
|
|
3741
|
-
|
|
3742
|
-
```typescript
|
|
3743
|
-
import { waitFor } from '@kici-dev/sdk';
|
|
3744
|
-
|
|
3745
|
-
const result = await waitFor({
|
|
3746
|
-
name: 'await-build-artifact',
|
|
3747
|
-
check: async () => {
|
|
3748
|
-
const artifact = await registry.findArtifact('myapp', 'v1.2.3');
|
|
3749
|
-
return artifact ?? null;
|
|
3750
|
-
},
|
|
3751
|
-
onSuccess: async (artifact) => ({ digest: artifact.digest }),
|
|
3752
|
-
intervalMs: 5000,
|
|
3753
|
-
timeoutMs: 5 * 60 * 1000,
|
|
3754
|
-
});
|
|
3755
|
-
|
|
3756
|
-
if (result.outcome === 'succeeded') {
|
|
3757
|
-
console.log(`Artifact ready: ${result.result.digest} (${result.attempts} polls)`);
|
|
3758
|
-
} else {
|
|
3759
|
-
console.log(`Gave up after ${result.elapsedMs} ms`);
|
|
3760
|
-
}
|
|
3761
|
-
```
|
|
3762
|
-
|
|
3763
|
-
## `waitForStep(name, options)`
|
|
3764
|
-
|
|
3765
|
-
A factory returning an SDK `Step` whose `run` body executes `waitFor(...)` and routes status lines through the step's structured logger.
|
|
3766
|
-
|
|
3767
|
-
### Parameters
|
|
3768
|
-
|
|
3769
|
-
| Name | Type | Required | Description |
|
|
3770
|
-
| --------- | --------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
|
|
3771
|
-
| `name` | `string` | Yes | Step name. Appears in the run timeline and in log lines. |
|
|
3772
|
-
| `options` | `Omit<WaitForOptions, 'name' \| 'log'>` | Yes | Same shape as `waitFor()` minus `name` (already provided) and `log` (provided by the step context). |
|
|
3773
|
-
|
|
3774
|
-
### Result
|
|
3775
|
-
|
|
3776
|
-
`waitForStep(...)` returns `Step<WaitForResult<TValue, TSuccess, TTimeout>>`. Other steps can consume the result through the standard step output mechanisms.
|
|
3777
|
-
|
|
3778
|
-
### Example
|
|
3779
|
-
|
|
3780
|
-
```typescript
|
|
3781
|
-
import { waitForStep, job } from '@kici-dev/sdk';
|
|
3782
|
-
|
|
3783
|
-
const awaitMarker = waitForStep('await-marker', {
|
|
3784
|
-
check: async () => {
|
|
3785
|
-
const stat = await tryStatMarker('/tmp/build-ready');
|
|
3786
|
-
return stat ? { path: '/tmp/build-ready' } : null;
|
|
3787
|
-
},
|
|
3788
|
-
intervalMs: 1000,
|
|
3789
|
-
timeoutMs: 60_000,
|
|
3790
|
-
onTimeout: async ({ attempts }) => ({ aborted: true, attempts }),
|
|
3791
|
-
});
|
|
3792
|
-
|
|
3793
|
-
export const release = job('release', {
|
|
3794
|
-
runsOn: 'linux',
|
|
3795
|
-
steps: [awaitMarker],
|
|
3796
|
-
});
|
|
3797
|
-
```
|
|
3798
|
-
|
|
3799
|
-
If `check()` throws while polling, the error is logged and polling continues — the default `swallowErrors: true` matches the "poll until healthy" pattern. Pass `swallowErrors: false` to fail fast on the first error instead.
|
|
3800
|
-
|
|
3801
|
-
## See also
|
|
3802
|
-
|
|
3803
|
-
- [Core SDK reference](https://docs.kici.dev/user/sdk/core/) — the `step()`, `job()`, and `workflow()` factories that `waitForStep()` builds on.
|
|
3804
|
-
- [Idempotent helpers](https://docs.kici.dev/user/sdk/idempotent/) — `idempotent()` and `idempotentStep()` for check / apply patterns.
|
|
3805
|
-
- [Runtime types](https://docs.kici.dev/user/sdk/runtime/) — `StepContext`, `Logger`, and other surface used inside the helpers.
|
|
3806
|
-
|
|
3807
|
-
---
|
|
3808
|
-
|
|
3809
2244
|
## SDK reference
|
|
3810
2245
|
|
|
3811
2246
|
Source: https://docs.kici.dev/user/sdk-reference/
|
|
3812
2247
|
|
|
3813
2248
|
Reference documentation for `@kici-dev/sdk`. The reference is split across the per-topic pages below.
|
|
3814
2249
|
|
|
3815
|
-
| Page | Covers
|
|
3816
|
-
| ------------------------------------------------------------ |
|
|
3817
|
-
| [Core](https://docs.kici.dev/user/sdk/core/) | `workflow()`, `job()`, `step()` factory functions and step / job authoring patterns (bare functions, output chaining, `needs`, dynamic groups).
|
|
3818
|
-
| [Triggers](https://docs.kici.dev/user/sdk/triggers/) | All
|
|
3819
|
-
| [Rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/) | `rule()`, `skip()`, matrix builds (static + dynamic), and `dynamicJob()` / `dynamicGroup()`.
|
|
3820
|
-
| [
|
|
3821
|
-
| [
|
|
3822
|
-
| [
|
|
3823
|
-
| [
|
|
3824
|
-
| [
|
|
3825
|
-
| [
|
|
2250
|
+
| Page | Covers |
|
|
2251
|
+
| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2252
|
+
| [Core](https://docs.kici.dev/user/sdk/core/) | `workflow()`, `job()`, `step()` factory functions and step / job authoring patterns (bare functions, output chaining, `needs`, dynamic groups). |
|
|
2253
|
+
| [Triggers](https://docs.kici.dev/user/sdk/triggers/) | All 23 trigger factories -- GitHub events (`pr`, `push`, `tag`, `comment`, ...), event triggers (`kiciEvent`, `workflowComplete`, `workflowsFailedBatch`, `jobComplete`), `genericWebhook`, `schedule`, `lifecycle`, plus branch-pattern semantics. |
|
|
2254
|
+
| [Rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/) | `rule()`, `skip()`, matrix builds (static + dynamic), and `dynamicJob()` / `dynamicGroup()`. |
|
|
2255
|
+
| [runsOnAll host fan-out](https://docs.kici.dev/user/sdk/runs-on-all/) | `runsOnAll` — fan one job out to every matching connected host, one pinned execution per host, with exact / glob / regex host selectors. |
|
|
2256
|
+
| [Caching](https://docs.kici.dev/user/sdk/caching/) | `CacheSpec`, declarative `cache` on jobs/steps, imperative `ctx.cache.restore()` / `ctx.cache.save()`, immutable keys, `restoreKeys` prefix fallback, per-org + per-ref isolation. |
|
|
2257
|
+
| [Artifacts](https://docs.kici.dev/user/sdk/artifacts/) | `ctx.artifacts` — share named, durable build outputs between jobs of a run and download them from the run page. |
|
|
2258
|
+
| [Validation & events](https://docs.kici.dev/user/sdk/validation-events/) | `validateDag()`, `defineEvent()`, event emission patterns. |
|
|
2259
|
+
| [Runtime](https://docs.kici.dev/user/sdk/runtime/) | Types index, `StepContext`, secrets, and fixtures. |
|
|
2260
|
+
| [Temp directories](https://docs.kici.dev/user/sdk/temp-directories/) | `ctx.mktemp()` / `ctx.mktempFile()` — allocate job-scoped scratch dirs and files that are cleaned up automatically when the job ends. |
|
|
2261
|
+
| [Event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) | Generated schema of the normalized event envelope passed to rules and dynamic functions. |
|
|
2262
|
+
| [Idempotent helpers](https://docs.kici.dev/user/sdk/idempotent/) | `idempotent()`, `idempotentStep()`, and the check-mode-aware `checkStep()` — check / apply pattern with typed results on both the skipped and applied branches. |
|
|
2263
|
+
| [Wait-for helpers](https://docs.kici.dev/user/sdk/wait-for/) | `waitFor()` and `waitForStep()` — poll a condition on an interval, run an optional success action, recover gracefully on timeout. |
|
|
2264
|
+
| [Parallel steps](https://docs.kici.dev/user/sdk/parallel/) | `parallel()` — run independent steps concurrently within one job behind a join barrier, each as its own observable step, with `failFast` and `maxParallel` controls. |
|
|
3826
2265
|
|
|
3827
2266
|
The `@kici-dev/sdk` package re-exports the entire surface from a single entry point. Pick what you need:
|
|
3828
2267
|
|
|
@@ -3839,6 +2278,6 @@ For the complete list of every named export (factory functions, triggers, rules,
|
|
|
3839
2278
|
- [Workflow patterns](https://docs.kici.dev/user/workflow-patterns/) -- common patterns using the SDK features documented above
|
|
3840
2279
|
- [Secrets management (operator)](https://docs.kici.dev/operator/security/secrets/) -- configure encrypted secret storage and admin API
|
|
3841
2280
|
- [Secrets architecture](https://docs.kici.dev/architecture/security/secrets/) -- encryption model, multi-backend, and data flow
|
|
3842
|
-
- [
|
|
2281
|
+
- [Execution status vocabulary](https://docs.kici.dev/architecture/execution/state-machine/) -- run/job/step statuses and terminal states
|
|
3843
2282
|
|
|
3844
2283
|
---
|