@kici-dev/compiler 0.1.20 → 0.1.22
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/cli.d.ts +14 -0
- package/dist/cli.js +58 -7
- package/dist/commands/check-mode.d.ts +19 -0
- package/dist/commands/check-mode.js +21 -0
- package/dist/commands/compile.js +1 -1
- package/dist/commands/held-run-client.d.ts +7 -2
- package/dist/commands/held-run-client.js +9 -3
- package/dist/commands/held-run-resolve.d.ts +5 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +15 -7
- package/dist/commands/run-hold-watch.d.ts +57 -0
- package/dist/commands/run-hold-watch.js +87 -0
- package/dist/commands/run.d.ts +8 -0
- package/dist/commands/run.js +67 -4
- package/dist/commands/test.d.ts +17 -0
- package/dist/llm-context/llms-full.txt +643 -53
- package/dist/llm-context/llms.txt +3 -1
- package/dist/local-executor/index.js +15 -2
- package/dist/local-executor/job-runner.d.ts +3 -0
- package/dist/local-executor/job-runner.js +54 -5
- package/dist/local-executor/output-streamer.js +3 -1
- package/dist/local-executor/types.d.ts +7 -0
- package/dist/lockfile/generator.js +36 -17
- package/dist/remote/config.d.ts +2 -0
- package/dist/remote/config.js +1 -0
- package/dist/remote/platform-client.d.ts +12 -0
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/job-executor.d.ts +6 -1
- package/dist/test-runner/step-context.js +6 -1
- package/dist/types.d.ts +20 -6
- package/package.json +4 -4
- package/sbom.spdx.json +35 -35
|
@@ -576,7 +576,7 @@ export default workflow('ci', {
|
|
|
576
576
|
});
|
|
577
577
|
```
|
|
578
578
|
|
|
579
|
-
The `test` and `typecheck` jobs both depend on `lint`, so they run in parallel after lint succeeds. KiCI validates the dependency graph at compile time -- cycles and missing references are caught before you commit. At runtime, jobs are gated on upstream completion: a job only dispatches after every entry in its `needs` array reaches a terminal
|
|
579
|
+
The `test` and `typecheck` jobs both depend on `lint`, so they run in parallel after lint succeeds. KiCI validates the dependency graph at compile time -- cycles and missing references are caught before you commit. At runtime, jobs are gated on upstream completion: a job only dispatches after every entry in its `needs` array reaches a terminal status that satisfies the edge. If an upstream fails, downstream jobs skip by default (override per-edge with `when: 'always'`). See [Job dependencies (`needs`)](../sdk/core.md#job-dependencies-needs) in the SDK reference for the full matrix of `needs` forms (string, `Job` ref, `{ name, when }`, `dynamicGroup()`) and [needs-scheduler](../../architecture/execution/needs-scheduler.md) for the dispatch semantics.
|
|
580
580
|
|
|
581
581
|
**Single-step jobs don't need a `steps` array.** When a job only does one thing, pass `run` to `job()` instead of wrapping it in `steps: [step(...)]`:
|
|
582
582
|
|
|
@@ -1109,6 +1109,116 @@ This workflow:
|
|
|
1109
1109
|
|
|
1110
1110
|
---
|
|
1111
1111
|
|
|
1112
|
+
## Host restart & wait-for-alive
|
|
1113
|
+
|
|
1114
|
+
Source: https://docs.kici.dev/user/patterns/host-restart/
|
|
1115
|
+
|
|
1116
|
+
When a KiCI agent runs on a host you are provisioning, a workflow can reboot
|
|
1117
|
+
that host and resume work once it comes back — the Ansible `reboot` +
|
|
1118
|
+
`wait_for_connection` pattern, expressed as two jobs pinned to the same host.
|
|
1119
|
+
|
|
1120
|
+
## The two-job pattern
|
|
1121
|
+
|
|
1122
|
+
Host restart is a **job-boundary** capability: the reboot is the last step of a
|
|
1123
|
+
"restart" job, and the post-restart work lives in a **separate job** pinned to
|
|
1124
|
+
the same host that `needs` the restart job. The orchestrator holds the
|
|
1125
|
+
post-restart job until the host completes a reboot cycle, then dispatches it.
|
|
1126
|
+
|
|
1127
|
+
```typescript
|
|
1128
|
+
import { workflow, job, step, restartHost, waitForHostAlive } from '@kici-dev/sdk';
|
|
1129
|
+
|
|
1130
|
+
export default workflow('patch-and-verify', {
|
|
1131
|
+
on: [
|
|
1132
|
+
/* ... */
|
|
1133
|
+
],
|
|
1134
|
+
jobs: [
|
|
1135
|
+
// Restart job: apply updates, then reboot. restartHost() MUST be the last step.
|
|
1136
|
+
job('patch', {
|
|
1137
|
+
runsOn: 'kici:host:box-01',
|
|
1138
|
+
steps: [
|
|
1139
|
+
step('upgrade', async (ctx) => {
|
|
1140
|
+
await ctx.$`apt-get upgrade -y`;
|
|
1141
|
+
}),
|
|
1142
|
+
restartHost(),
|
|
1143
|
+
],
|
|
1144
|
+
}),
|
|
1145
|
+
// Post-restart job: pinned to the SAME host, needs the restart job.
|
|
1146
|
+
job('verify', {
|
|
1147
|
+
runsOn: 'kici:host:box-01',
|
|
1148
|
+
needs: ['patch'],
|
|
1149
|
+
steps: [
|
|
1150
|
+
waitForHostAlive(() => fetch('http://localhost:8080/health')),
|
|
1151
|
+
step('check-service', async (ctx) => {
|
|
1152
|
+
await ctx.$`systemctl is-active myservice`;
|
|
1153
|
+
}),
|
|
1154
|
+
],
|
|
1155
|
+
}),
|
|
1156
|
+
],
|
|
1157
|
+
});
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
## `restartHost()`
|
|
1161
|
+
|
|
1162
|
+
`restartHost(opts?)` reboots the host the job runs on. It signals the
|
|
1163
|
+
orchestrator that a reboot is pending (which holds the post-restart job and
|
|
1164
|
+
treats the agent's imminent disconnect as expected, not a failure), reports the
|
|
1165
|
+
step success, and the agent issues the OS reboot once the step completes.
|
|
1166
|
+
|
|
1167
|
+
- **Must be the last step** of its job — the job completes before the box goes
|
|
1168
|
+
down.
|
|
1169
|
+
- `deadlineMs` (optional) overrides how long the orchestrator waits for the host
|
|
1170
|
+
to return after the reboot. The default is the orchestrator's
|
|
1171
|
+
`KICI_HOST_REBOOT_DEADLINE_MS` (15 minutes). If the host does not reconnect by
|
|
1172
|
+
the deadline, the held post-restart job fails with a clear "host did not
|
|
1173
|
+
return after reboot" reason.
|
|
1174
|
+
- The reboot command is chosen per operating system (Linux `systemctl reboot`,
|
|
1175
|
+
macOS `shutdown -r now`, Windows `shutdown /r /t 0`).
|
|
1176
|
+
|
|
1177
|
+
## `waitForHostAlive(probe, opts?)`
|
|
1178
|
+
|
|
1179
|
+
`waitForHostAlive()` is the optional first step of the post-restart job. The
|
|
1180
|
+
baseline "the host is back" guarantee comes for free — the post-restart job only
|
|
1181
|
+
dispatches after the agent reconnects. `waitForHostAlive(probe)` adds a
|
|
1182
|
+
**service-readiness** gate on top: it polls `probe` until it resolves, for hosts
|
|
1183
|
+
where "agent connected" does not yet mean "services ready".
|
|
1184
|
+
|
|
1185
|
+
- The probe can return anything (an HTTP response, an open port check, a marker
|
|
1186
|
+
file). Any non-null resolution means "ready"; a throw or rejection keeps
|
|
1187
|
+
polling.
|
|
1188
|
+
- `intervalMs` (default 3000) and `timeoutMs` (default 300000) tune the poll. If
|
|
1189
|
+
the probe never succeeds within `timeoutMs`, the step fails with "services did
|
|
1190
|
+
not come up".
|
|
1191
|
+
|
|
1192
|
+
## Same-host pinning
|
|
1193
|
+
|
|
1194
|
+
The "same host" relationship is the pin: both jobs target the same host via
|
|
1195
|
+
`runsOn` (a `kici:host:<id>` label or another label the host carries), and the
|
|
1196
|
+
post-restart job `needs` the restart job. Durable provisioning hosts MUST set a
|
|
1197
|
+
stable agent id (`KICI_AGENT_ID`) so the host re-registers under the same
|
|
1198
|
+
identity after the reboot — that stable identity is what lets the orchestrator
|
|
1199
|
+
recognise the down-then-up cycle and release the held job.
|
|
1200
|
+
|
|
1201
|
+
## Failure behavior
|
|
1202
|
+
|
|
1203
|
+
- **Host never returns by the deadline** → the held post-restart job fails.
|
|
1204
|
+
- **Reboot privilege denied** → the restart step fails with a clear privilege
|
|
1205
|
+
error (see the operator note below), and the orchestrator clears the
|
|
1206
|
+
reboot-pending hold.
|
|
1207
|
+
- **`waitForHostAlive` probe never succeeds** → that step fails.
|
|
1208
|
+
|
|
1209
|
+
The orchestrator refuses to reboot the host it runs on, so a co-located agent
|
|
1210
|
+
cannot take down the orchestrator's own box.
|
|
1211
|
+
|
|
1212
|
+
## Operator prerequisite: reboot privilege
|
|
1213
|
+
|
|
1214
|
+
Rebooting needs host privilege. An agent used for host provisioning must be able
|
|
1215
|
+
to run the OS reboot primitive — run the agent service with reboot privilege, or
|
|
1216
|
+
grant a narrow `shutdown` / `systemctl reboot` permission. Agents used for
|
|
1217
|
+
provisioning generally need broad, near-root host privileges; see the operator
|
|
1218
|
+
agent documentation for the full posture.
|
|
1219
|
+
|
|
1220
|
+
---
|
|
1221
|
+
|
|
1112
1222
|
## Integration patterns
|
|
1113
1223
|
|
|
1114
1224
|
Source: https://docs.kici.dev/user/patterns/integrations/
|
|
@@ -2108,6 +2218,28 @@ const build = step('build', {
|
|
|
2108
2218
|
|
|
2109
2219
|
**StepRunFn type:** `(ctx: StepContext) => Promise<void>`
|
|
2110
2220
|
|
|
2221
|
+
**With a check facet (idempotent step):**
|
|
2222
|
+
|
|
2223
|
+
Add a `check` function to describe _desired state_ instead of a fixed action. When
|
|
2224
|
+
`check` is present, `run` becomes the _apply_ function and receives the drift value
|
|
2225
|
+
`check` returned; `summarize` (required) renders that drift for logs and the
|
|
2226
|
+
dashboard; `whenInSync` optionally produces the step's outputs when already in sync.
|
|
2227
|
+
|
|
2228
|
+
```typescript
|
|
2229
|
+
const configureNginx = step('configure-nginx', {
|
|
2230
|
+
check: async (ctx) => ((await inSync(ctx)) ? null : { want: DESIRED }),
|
|
2231
|
+
summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
|
|
2232
|
+
run: async (ctx, drift) => {
|
|
2233
|
+
await writeConfig(drift.want);
|
|
2234
|
+
return { reloaded: true };
|
|
2235
|
+
},
|
|
2236
|
+
whenInSync: async () => ({ reloaded: false }),
|
|
2237
|
+
});
|
|
2238
|
+
```
|
|
2239
|
+
|
|
2240
|
+
A checked step can run in apply mode (converge) or `--check` preview mode (report
|
|
2241
|
+
drift, change nothing). See [Idempotent steps and check mode](../idempotent-steps.md).
|
|
2242
|
+
|
|
2111
2243
|
### Per-job resources
|
|
2112
2244
|
|
|
2113
2245
|
`options.resources` declares the CPU and memory the job needs. The orchestrator's auto-scaler uses these numbers to:
|
|
@@ -2467,9 +2599,9 @@ const test = job('test', { needs: [lint], ... });
|
|
|
2467
2599
|
// 2. Reference by string name
|
|
2468
2600
|
const test = job('test', { needs: ['lint'], ... });
|
|
2469
2601
|
|
|
2470
|
-
// 3. Object form with per-edge
|
|
2602
|
+
// 3. Object form with a per-edge run condition (`when`)
|
|
2471
2603
|
const cleanup = job('cleanup', {
|
|
2472
|
-
needs: [{ name: 'build',
|
|
2604
|
+
needs: [{ name: 'build', when: 'always' }],
|
|
2473
2605
|
...
|
|
2474
2606
|
});
|
|
2475
2607
|
|
|
@@ -2480,16 +2612,36 @@ const deploy = job('deploy', {
|
|
|
2480
2612
|
});
|
|
2481
2613
|
```
|
|
2482
2614
|
|
|
2483
|
-
**
|
|
2615
|
+
**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.
|
|
2616
|
+
|
|
2617
|
+
| Keyword | Satisfied when the upstream is… | Use for |
|
|
2618
|
+
| ------------------------ | ------------------------------- | ------------------------------------------- |
|
|
2619
|
+
| `'on-success'` (default) | `success` | normal dependencies |
|
|
2620
|
+
| `'always'` | any terminal status | cleanup / notification / teardown jobs |
|
|
2621
|
+
| `'on-skip'` | `success` or `skipped` | continue when an upstream was narrowed out |
|
|
2622
|
+
| `'on-failure'` | `failed` or `timed_out_stale` | error-handler jobs that run only on failure |
|
|
2623
|
+
|
|
2624
|
+
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`.
|
|
2484
2625
|
|
|
2485
|
-
|
|
2486
|
-
| ------ | ------------------------------------------------------------------------------------------- |
|
|
2487
|
-
| `skip` | (Default) Downstream transitions directly to `skipped`. Failures cascade through the DAG. |
|
|
2488
|
-
| `run` | Downstream dispatches anyway. Use for cleanup, notification, or "always-run" teardown jobs. |
|
|
2626
|
+
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).
|
|
2489
2627
|
|
|
2490
|
-
|
|
2628
|
+
When an upstream's terminal status is **not** in the edge's set, the downstream transitions directly to `skipped`. Because a skipped job is itself terminal, this propagates transitively: each downstream's `when` set governs whether the skip cascades further.
|
|
2491
2629
|
|
|
2492
|
-
**Dispatch gate:** `needs` is a hard dispatch gate. A job only
|
|
2630
|
+
**Dispatch gate:** `needs` is a hard dispatch gate. A job dispatches only after every upstream in its `needs` array reaches a terminal status that satisfies that edge's `when` set. Root jobs (empty `needs`, no dynamic group refs) dispatch immediately. The scheduler is DB-backed and fully recovers across orchestrator restarts.
|
|
2631
|
+
|
|
2632
|
+
**Reading an upstream's status in a step:** inside a running job, `ctx.needs.<job>.status` exposes each upstream's terminal status (`success`, `failed`, `skipped`, …) and `ctx.needs.<job>.result` its outputs. A group / matrix / `runsOnAll` fan-out upstream is an ordered array of `{ name, result, status }`, one per child. Use this to branch in TypeScript:
|
|
2633
|
+
|
|
2634
|
+
```typescript
|
|
2635
|
+
job('report', {
|
|
2636
|
+
needs: [{ name: 'probe', when: 'always' }],
|
|
2637
|
+
run: async (ctx) => {
|
|
2638
|
+
if (ctx.needs.probe.status === 'failed') await fileIncident(ctx.needs.probe.result);
|
|
2639
|
+
else await publish(ctx.needs.probe.result);
|
|
2640
|
+
},
|
|
2641
|
+
});
|
|
2642
|
+
```
|
|
2643
|
+
|
|
2644
|
+
For an arbitrary outcome-based gate that prevents a job from dispatching at all, use a result-aware `dynamicJob` that returns `[]` or `[job]` based on `ctx.needs.<job>.status` — see [Dynamic jobs](../../architecture/execution/dynamic-jobs.md).
|
|
2493
2645
|
|
|
2494
2646
|
**DAG validation:** three-layer cycle detection.
|
|
2495
2647
|
|
|
@@ -2502,7 +2654,10 @@ String and `Job`-reference entries default to `ifFailed: 'skip'`. To override, u
|
|
|
2502
2654
|
Create a reference to a dynamic job group, for use inside a static job's `needs` array.
|
|
2503
2655
|
|
|
2504
2656
|
```typescript
|
|
2505
|
-
function dynamicGroup(
|
|
2657
|
+
function dynamicGroup(
|
|
2658
|
+
name: string,
|
|
2659
|
+
options?: { when?: 'on-success' | 'always' | 'on-skip' | 'on-failure' | string[] },
|
|
2660
|
+
): DynamicGroupRef;
|
|
2506
2661
|
```
|
|
2507
2662
|
|
|
2508
2663
|
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).
|
|
@@ -3423,10 +3578,10 @@ export default workflow('discovery-fan-out', { jobs: [discover, reports] });
|
|
|
3423
3578
|
|
|
3424
3579
|
`ctx.needs` shape:
|
|
3425
3580
|
|
|
3426
|
-
| Need form
|
|
3427
|
-
|
|
|
3428
|
-
| `'jobName'` / `{ name,
|
|
3429
|
-
| `dynamicGroup('g')` / `dynamicGroup('g', {
|
|
3581
|
+
| Need form | `ctx.needs[...]` value |
|
|
3582
|
+
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3583
|
+
| `'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 |
|
|
3584
|
+
| `dynamicGroup('g')` / `dynamicGroup('g', { when })` | ordered array of `{ name, result, status }`, one per group member |
|
|
3430
3585
|
|
|
3431
3586
|
`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()`](./triggers.md) for cross-workflow reactions to a job finishing. See the architecture deep-dive in [dynamic jobs](../../architecture/execution/dynamic-jobs.md#result-aware-generation).
|
|
3432
3587
|
|
|
@@ -3663,6 +3818,32 @@ A held host occupies a wave slot indefinitely while it waits to reconnect, stall
|
|
|
3663
3818
|
roll behind an absent box. `skip` (run only reachable hosts) or `fail` (refuse the roll
|
|
3664
3819
|
if any expected host is down) keep the window moving.
|
|
3665
3820
|
|
|
3821
|
+
### Narrowing the roster at run time with `--target`
|
|
3822
|
+
|
|
3823
|
+
A `runsOnAll` predicate is authored once in the workflow, but you can narrow it for a
|
|
3824
|
+
single run with `kici run --target <selector>` — an Ansible-`--limit`-style runtime
|
|
3825
|
+
filter. The effective host set becomes `runsOnAll ∩ target`: the selector can only
|
|
3826
|
+
_remove_ hosts from the matched roster, never add them. The narrowing is **run-global**
|
|
3827
|
+
(it applies to every `runsOnAll` job) and **`runsOnAll`-only** (single `runsOn`-pinned
|
|
3828
|
+
jobs are untouched). Repeated `--target` values AND-combine — a host must satisfy every
|
|
3829
|
+
selector to survive.
|
|
3830
|
+
|
|
3831
|
+
```bash
|
|
3832
|
+
# Patch only the role:web subset of whatever role:* hosts the job would match
|
|
3833
|
+
kici run remote deploy --target role:web
|
|
3834
|
+
|
|
3835
|
+
# Intersect two selectors: hosts must be BOTH role:web AND dc:eu
|
|
3836
|
+
kici run remote deploy --target role:web --target dc:eu
|
|
3837
|
+
```
|
|
3838
|
+
|
|
3839
|
+
When `--target` narrows a `runsOnAll` job to zero hosts, the run **fails** by default
|
|
3840
|
+
(a mistyped selector should be loud, not silently no-op). Pass `--target-allow-empty`
|
|
3841
|
+
to **skip** the zeroed job instead — it records a `skipped` status, and downstream jobs
|
|
3842
|
+
gated with `when: 'on-skip'` (or `when: 'always'`) still run, exactly as for an
|
|
3843
|
+
`onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](/user/cli-reference/#host-narrowing-with---target)
|
|
3844
|
+
for the full flag behavior and the [`needs` gating model](./core.md#job-dependencies-needs)
|
|
3845
|
+
for how a skipped upstream propagates.
|
|
3846
|
+
|
|
3666
3847
|
### Limits (v0)
|
|
3667
3848
|
|
|
3668
3849
|
- Per-host secret scoping is not yet available — all hosts receive the job's resolved
|
|
@@ -4021,6 +4202,70 @@ const publish = job('publish', {
|
|
|
4021
4202
|
- The step never holds platform credentials — the request is relayed through the orchestrator, which mints the token on the step's behalf.
|
|
4022
4203
|
- Only available inside a running job step; calling it outside one (for example, during local execution) rejects with a clear error.
|
|
4023
4204
|
|
|
4205
|
+
### ctx.kici.inventory.query(selector?) / .get(agentId)
|
|
4206
|
+
|
|
4207
|
+
Query the **host inventory** — the roster of agents in the caller's orchestrator cluster — from inside a workflow. Each host is a `HostInventoryEntry`:
|
|
4208
|
+
|
|
4209
|
+
```typescript
|
|
4210
|
+
interface HostInventoryEntry {
|
|
4211
|
+
agentId: string;
|
|
4212
|
+
labels: string[]; // flat-string grouping/tags dimension
|
|
4213
|
+
properties: Record<string, string | number | boolean>; // typed host-vars dimension
|
|
4214
|
+
hostname: string | null;
|
|
4215
|
+
platform: string | null;
|
|
4216
|
+
arch: string | null;
|
|
4217
|
+
lifecycleClass: 'static' | 'ephemeral';
|
|
4218
|
+
status: 'ready' | 'unreachable' | 'stale';
|
|
4219
|
+
lastSeen: string; // ISO timestamp
|
|
4220
|
+
}
|
|
4221
|
+
```
|
|
4222
|
+
|
|
4223
|
+
Two dimensions describe a host. **Labels** are flat strings used for grouping and targeting (the same labels `runsOn` / `runsOnAll` match). **Properties** are typed host-vars (`string | number | boolean`) — the place for facts like `region`, `cores`, or `gpu`. A host reports its own properties via the agent's `KICI_PROPERTIES` config, and an operator can pre-declare them with `kici-admin host declare --prop key=value`; the two are shallow-merged (agent-reported keys win).
|
|
4224
|
+
|
|
4225
|
+
```typescript
|
|
4226
|
+
// All hosts:
|
|
4227
|
+
const all = await ctx.kici.inventory.query();
|
|
4228
|
+
|
|
4229
|
+
// Server-side label filter (OR-of-AND include groups, plus exclude):
|
|
4230
|
+
const dbHosts = await ctx.kici.inventory.query({
|
|
4231
|
+
include: [[{ kind: 'exact', value: 'role:db' }]],
|
|
4232
|
+
});
|
|
4233
|
+
|
|
4234
|
+
// Property filtering is client-side — plain JS in the workflow:
|
|
4235
|
+
const euDbHosts = dbHosts.filter((h) => h.properties.region === 'eu');
|
|
4236
|
+
|
|
4237
|
+
// One host by id:
|
|
4238
|
+
const host = await ctx.kici.inventory.get('box-1'); // HostInventoryEntry | null
|
|
4239
|
+
```
|
|
4240
|
+
|
|
4241
|
+
**The label selector is applied server-side** (reusing the same glob/regex matchers as `runsOnAll`). **Property filtering is client-side** — you filter the returned array in plain JavaScript, so there is no query DSL to learn.
|
|
4242
|
+
|
|
4243
|
+
**Headline use — dynamic-job fan-out.** A dynamic-job generator can query the inventory and return one job per matching host, fanning a workflow out across a fleet:
|
|
4244
|
+
|
|
4245
|
+
```typescript
|
|
4246
|
+
const migrate = job('migrate', async (ctx) => {
|
|
4247
|
+
const hosts = await ctx.kici.inventory.query({
|
|
4248
|
+
include: [[{ kind: 'exact', value: 'role:db' }]],
|
|
4249
|
+
});
|
|
4250
|
+
return hosts
|
|
4251
|
+
.filter((h) => h.properties.region === 'eu')
|
|
4252
|
+
.map((h) =>
|
|
4253
|
+
job(`migrate-${h.agentId}`, {
|
|
4254
|
+
runsOn: [h.agentId],
|
|
4255
|
+
run: async (c) => {
|
|
4256
|
+
await c.$`./migrate.sh`;
|
|
4257
|
+
},
|
|
4258
|
+
}),
|
|
4259
|
+
);
|
|
4260
|
+
});
|
|
4261
|
+
```
|
|
4262
|
+
|
|
4263
|
+
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.
|
|
4264
|
+
|
|
4265
|
+
`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).
|
|
4266
|
+
|
|
4267
|
+
**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.
|
|
4268
|
+
|
|
4024
4269
|
### ctx.attestProvenance({ subject })
|
|
4025
4270
|
|
|
4026
4271
|
Build, sign, and persist a build-provenance attestation for an artifact your step produced. KiCI assembles an in-toto SLSA v1.0 provenance statement whose build identity (`repository`, `ref`, `sha`, run/job ids) comes from the platform — not from the step — so it cannot be spoofed, signs it, and stores a verifiable bundle that the dashboard surfaces and the `kici verify-attestation` CLI checks.
|
|
@@ -5563,6 +5808,8 @@ kici run remote [fixture] [options]
|
|
|
5563
5808
|
| `--history` | `false` | Show table of recent test runs |
|
|
5564
5809
|
| `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted (repeatable) |
|
|
5565
5810
|
| `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted (repeatable) — see [testing guide](testing-guide.md) |
|
|
5811
|
+
| `--target <selector>` | none | Narrow `runsOnAll` jobs to hosts matching this label selector (repeatable, AND-combined) |
|
|
5812
|
+
| `--target-allow-empty` | `false` | A `--target` that narrows a `runsOnAll` job to zero hosts skips it instead of failing |
|
|
5566
5813
|
| `--debug` | `false` | Verbose internals |
|
|
5567
5814
|
| `--kici-dir <path>` | `.kici` | Path to .kici directory |
|
|
5568
5815
|
|
|
@@ -5582,7 +5829,7 @@ kici run remote push-main --org xyz789ghi012
|
|
|
5582
5829
|
kici run remote push-main --orchestrator us-east
|
|
5583
5830
|
|
|
5584
5831
|
# Run all push-related fixtures
|
|
5585
|
-
kici run remote push-*
|
|
5832
|
+
kici run remote 'push-*'
|
|
5586
5833
|
|
|
5587
5834
|
# Run everything
|
|
5588
5835
|
kici run remote --all
|
|
@@ -5601,8 +5848,44 @@ kici run remote push-main --no-wait
|
|
|
5601
5848
|
|
|
5602
5849
|
# View recent test run history
|
|
5603
5850
|
kici run remote --history
|
|
5851
|
+
|
|
5852
|
+
# Narrow runsOnAll jobs to a subset of the host roster
|
|
5853
|
+
kici run remote deploy --target role:web
|
|
5854
|
+
|
|
5855
|
+
# AND-combine repeated --target values (hosts must match every selector)
|
|
5856
|
+
kici run remote deploy --target role:web --target dc:eu
|
|
5857
|
+
|
|
5858
|
+
# Skip a runsOnAll job instead of failing it when the target matches no host
|
|
5859
|
+
kici run remote deploy --target role:gpu --target-allow-empty
|
|
5604
5860
|
```
|
|
5605
5861
|
|
|
5862
|
+
#### Host narrowing with `--target`
|
|
5863
|
+
|
|
5864
|
+
`--target <selector>` is a runtime narrowing for `runsOnAll` jobs, analogous to
|
|
5865
|
+
Ansible's `--limit`. A `runsOnAll` job normally fans out to **every** roster host
|
|
5866
|
+
matching its predicate, one pinned execution per host. `--target` intersects that
|
|
5867
|
+
matched roster with a label selector, so the effective host set is
|
|
5868
|
+
`runsOnAll ∩ target`:
|
|
5869
|
+
|
|
5870
|
+
- **Narrow-only.** `--target` can only _remove_ hosts from the matched set, never
|
|
5871
|
+
add them. The widening dimension (OR across host groups) lives in the workflow's
|
|
5872
|
+
`runsOnAll`; `--target` only subtracts.
|
|
5873
|
+
- **Run-global, `runsOnAll`-only.** A single `--target` applies to every
|
|
5874
|
+
`runsOnAll` job in the run. Jobs pinned to a single host with `runsOn` are
|
|
5875
|
+
untouched.
|
|
5876
|
+
- **Repeatable and AND-combined.** Each `--target` value is its own selector; a
|
|
5877
|
+
host must satisfy **all** of them to survive the narrowing. Use a single value
|
|
5878
|
+
for an OR-style match within one selector and repeated values for AND.
|
|
5879
|
+
- **Selector syntax** matches `runsOn`: an exact label (`role:web`), a glob
|
|
5880
|
+
(`role:*`), or a regex (`/^box-0[1-3]$/`).
|
|
5881
|
+
|
|
5882
|
+
When `--target` narrows a `runsOnAll` job to zero hosts, the default is to **fail**
|
|
5883
|
+
the run (fail-loud — a typo in the selector shouldn't silently skip work). Pass
|
|
5884
|
+
`--target-allow-empty` to **skip** the zeroed job instead; the job records a
|
|
5885
|
+
`skipped` status, and any downstream job that needs it with `when: 'on-skip'` (or
|
|
5886
|
+
`when: 'always'`) still runs. See [Job dependencies](./sdk/core.md#job-dependencies-needs)
|
|
5887
|
+
for the `when` gating model.
|
|
5888
|
+
|
|
5606
5889
|
**Exit codes:**
|
|
5607
5890
|
|
|
5608
5891
|
| Code | Meaning |
|
|
@@ -5743,18 +6026,21 @@ By default, `kici login` opens your browser for OIDC authentication using PKCE.
|
|
|
5743
6026
|
|
|
5744
6027
|
After OAuth, the CLI exchanges the OIDC token for a personal access token (PAT) stored in the config directory (`~/.kici/config` by default, overridable with `KICI_CONFIG_DIR`).
|
|
5745
6028
|
|
|
6029
|
+
`kici login` targets the hosted KiCI Platform by default. To authenticate against another environment (a self-hosted Platform, for example), pass `--platform-endpoint` / `--oidc-issuer` or set `KICI_PLATFORM_URL` / `KICI_OIDC_ISSUER`. Login persists the platform endpoint and OIDC issuer it authenticated against alongside the PAT, so a saved PAT always matches its endpoint. Because the config describes one environment at a time, **switching the endpoint resets the active organization and default clusters** — re-run `kici org use <name>` after switching environments.
|
|
6030
|
+
|
|
5746
6031
|
```bash
|
|
5747
6032
|
kici login [options]
|
|
5748
6033
|
```
|
|
5749
6034
|
|
|
5750
6035
|
**Options:**
|
|
5751
6036
|
|
|
5752
|
-
| Option | Default | Description
|
|
5753
|
-
| --------------------------- | ------- |
|
|
5754
|
-
| `--token <key>` | none | API key for direct authentication (legacy)
|
|
5755
|
-
| `--device` | false | Force device authorization flow (headless/SSH)
|
|
5756
|
-
| `--platform-endpoint <url>` | none | Platform relay URL
|
|
5757
|
-
| `--
|
|
6037
|
+
| Option | Default | Description |
|
|
6038
|
+
| --------------------------- | ------- | --------------------------------------------------- |
|
|
6039
|
+
| `--token <key>` | none | API key for direct authentication (legacy) |
|
|
6040
|
+
| `--device` | false | Force device authorization flow (headless/SSH) |
|
|
6041
|
+
| `--platform-endpoint <url>` | none | Platform relay URL |
|
|
6042
|
+
| `--oidc-issuer <url>` | none | OIDC issuer URL (selects a non-default environment) |
|
|
6043
|
+
| `--routing-key <key>` | none | Routing key for webhook source identification |
|
|
5758
6044
|
|
|
5759
6045
|
**Environment variables:**
|
|
5760
6046
|
|
|
@@ -5780,7 +6066,8 @@ kici login --device
|
|
|
5780
6066
|
kici login --token kici_sk_abc123...
|
|
5781
6067
|
|
|
5782
6068
|
# Log in against a self-hosted Platform
|
|
5783
|
-
kici login --platform-endpoint https://platform.example.com
|
|
6069
|
+
kici login --platform-endpoint https://platform.example.com \
|
|
6070
|
+
--oidc-issuer https://auth.example.com/realms/kici-internal
|
|
5784
6071
|
|
|
5785
6072
|
# Suppress browser opening (print authorize URL to stdout)
|
|
5786
6073
|
KICI_BROWSER_CMD=none kici login
|
|
@@ -6813,7 +7100,7 @@ The lock file (`kici.lock.json`) is a JSON file with the following top-level fie
|
|
|
6813
7100
|
|
|
6814
7101
|
| Field | Description |
|
|
6815
7102
|
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
6816
|
-
| `schemaVersion` | Lock file schema version (currently
|
|
7103
|
+
| `schemaVersion` | Lock file schema version (currently 20). Incremented on breaking format changes. |
|
|
6817
7104
|
| `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
|
|
6818
7105
|
| `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
|
|
6819
7106
|
| `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
|
|
@@ -6988,12 +7275,14 @@ kici run remote
|
|
|
6988
7275
|
kici run remote push-main
|
|
6989
7276
|
|
|
6990
7277
|
# Run all fixtures matching a glob
|
|
6991
|
-
kici run remote push-*
|
|
7278
|
+
kici run remote 'push-*'
|
|
6992
7279
|
|
|
6993
7280
|
# Run everything
|
|
6994
7281
|
kici run remote --all
|
|
6995
7282
|
```
|
|
6996
7283
|
|
|
7284
|
+
The single quotes keep your shell from expanding `push-*` against local files, so the pattern reaches KiCI intact for its own fixture-glob matching.
|
|
7285
|
+
|
|
6997
7286
|
## Fixture reference
|
|
6998
7287
|
|
|
6999
7288
|
### Event types
|
|
@@ -7089,7 +7378,7 @@ kici run remote
|
|
|
7089
7378
|
kici run remote push-main
|
|
7090
7379
|
|
|
7091
7380
|
# Glob matching -- run all push-related fixtures
|
|
7092
|
-
kici run remote push-*
|
|
7381
|
+
kici run remote 'push-*'
|
|
7093
7382
|
|
|
7094
7383
|
# Run all fixtures sequentially
|
|
7095
7384
|
kici run remote --all
|
|
@@ -7432,12 +7721,14 @@ Source: https://docs.kici.dev/user/approvals/
|
|
|
7432
7721
|
|
|
7433
7722
|
An **approval gate** pauses execution until an authorized person approves it. Execution resumes from exactly where it paused; a rejection (or an expired hold) fails the run.
|
|
7434
7723
|
|
|
7435
|
-
You declare a gate in your workflow with `
|
|
7724
|
+
You declare a gate in your workflow with `approval`. It is available at three levels of granularity:
|
|
7436
7725
|
|
|
7437
7726
|
- **Step** — pause mid-job, before a specific step runs. The agent holds the live workspace (with all prior-step state intact) for the duration of the wait.
|
|
7438
7727
|
- **Job** — hold the job before any of its steps run.
|
|
7439
7728
|
- **Workflow** — hold the whole run before any job is dispatched.
|
|
7440
7729
|
|
|
7730
|
+
A step-level gate can also fire **only when a check/apply step finds drift** — Terraform's plan→apply, per step. See [Drift gates](#drift-gates-whendrift) below.
|
|
7731
|
+
|
|
7441
7732
|
Approvers are named as **teams** and **users**. A team is an operator-defined group of org members; your workflow code may name a team but can never change its membership, which is what makes a team clause a real gate rather than a suggestion. See [Approval gates (operator guide)](../operator/approvals.md) for how operators define teams, the approval queue, and expiry; see [the architecture overview](../architecture/approvals.md) for how a hold is evaluated and resumed.
|
|
7442
7733
|
|
|
7443
7734
|
## Quick start
|
|
@@ -7452,7 +7743,7 @@ export default workflow('deploy', {
|
|
|
7452
7743
|
jobs: [
|
|
7453
7744
|
job('deploy-production', {
|
|
7454
7745
|
runsOn: 'default',
|
|
7455
|
-
|
|
7746
|
+
approval: [{ team: 'leads' }],
|
|
7456
7747
|
steps: [step('deploy', async (ctx) => ctx.$`deploy --prod`)],
|
|
7457
7748
|
}),
|
|
7458
7749
|
],
|
|
@@ -7461,28 +7752,28 @@ export default workflow('deploy', {
|
|
|
7461
7752
|
|
|
7462
7753
|
When the run reaches this job, it is held instead of dispatched. The held run appears in the dashboard approval queue and can be released from there or with the [`kici approve`](#approving-from-the-cli) command. Once a member of `leads` approves, the job dispatches normally.
|
|
7463
7754
|
|
|
7464
|
-
## The `
|
|
7755
|
+
## The `approval` field
|
|
7465
7756
|
|
|
7466
|
-
`
|
|
7757
|
+
`approval` accepts three forms.
|
|
7467
7758
|
|
|
7468
7759
|
### Shorthand: `true`
|
|
7469
7760
|
|
|
7470
7761
|
```typescript
|
|
7471
7762
|
job('deploy', {
|
|
7472
7763
|
runsOn: 'default',
|
|
7473
|
-
|
|
7764
|
+
approval: true,
|
|
7474
7765
|
steps: [
|
|
7475
7766
|
/* ... */
|
|
7476
7767
|
],
|
|
7477
7768
|
});
|
|
7478
7769
|
```
|
|
7479
7770
|
|
|
7480
|
-
`
|
|
7771
|
+
`approval: true` holds the element until **any** org member who can act on approvals signs off — anyone with the `environments:write` or `ci_trust:write` permission. Use it when you want a manual gate without restricting who may release it.
|
|
7481
7772
|
|
|
7482
7773
|
### Approver list (AND)
|
|
7483
7774
|
|
|
7484
7775
|
```typescript
|
|
7485
|
-
|
|
7776
|
+
approval: [{ team: 'leads' }, { user: 'cto' }],
|
|
7486
7777
|
```
|
|
7487
7778
|
|
|
7488
7779
|
A list of approver clauses is an **AND** list: every clause must be satisfied before the element is released.
|
|
@@ -7494,27 +7785,29 @@ A single approver may satisfy more than one clause. If `cto` is also a member of
|
|
|
7494
7785
|
|
|
7495
7786
|
There is no OR or nested logic — clauses are always a flat AND list.
|
|
7496
7787
|
|
|
7497
|
-
### Object form: reason and timeout
|
|
7788
|
+
### Object form: when, reason, and timeout
|
|
7498
7789
|
|
|
7499
7790
|
```typescript
|
|
7500
|
-
|
|
7791
|
+
approval: {
|
|
7792
|
+
when: 'always',
|
|
7501
7793
|
approvers: [{ team: 'security' }, { team: 'leads' }],
|
|
7502
7794
|
reason: 'Production deploy requires security + leads sign-off',
|
|
7503
7795
|
timeout: 7200, // seconds
|
|
7504
7796
|
},
|
|
7505
7797
|
```
|
|
7506
7798
|
|
|
7507
|
-
| Field | Type
|
|
7508
|
-
| ----------- |
|
|
7509
|
-
| `
|
|
7510
|
-
| `
|
|
7511
|
-
| `
|
|
7799
|
+
| Field | Type | Description |
|
|
7800
|
+
| ----------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7801
|
+
| `when` | `'always' \| 'drift'` | When the gate fires. `'always'` (default) gates before the element; `'drift'` gates a check/apply step only when it finds drift. See [Drift gates](#drift-gates-whendrift). |
|
|
7802
|
+
| `approvers` | `ApproverClause[]` | The AND list of `{ team }` / `{ user }` clauses. An empty list means "any approval-capable member". |
|
|
7803
|
+
| `reason` | `string` | A human-readable label shown in the dashboard queue and the held-for-approval status check. |
|
|
7804
|
+
| `timeout` | `number` | Per-gate expiry in **seconds**, overriding the org default. On expiry the element is rejected. |
|
|
7512
7805
|
|
|
7513
7806
|
When `timeout` is omitted, the gate uses the org's default approval expiry (set by the operator). On expiry, the held element is rejected and the run fails — see [expiry](../operator/approvals.md#expiry).
|
|
7514
7807
|
|
|
7515
7808
|
## Granularity
|
|
7516
7809
|
|
|
7517
|
-
The same `
|
|
7810
|
+
The same `approval` field is accepted on a workflow, a job, and a step.
|
|
7518
7811
|
|
|
7519
7812
|
### Workflow-level
|
|
7520
7813
|
|
|
@@ -7523,7 +7816,7 @@ A workflow-level gate holds the entire run before any job is dispatched:
|
|
|
7523
7816
|
```typescript
|
|
7524
7817
|
export default workflow('release', {
|
|
7525
7818
|
on: [push({ branches: ['main'] })],
|
|
7526
|
-
|
|
7819
|
+
approval: [{ team: 'release-managers' }],
|
|
7527
7820
|
jobs: [buildJob, publishJob],
|
|
7528
7821
|
});
|
|
7529
7822
|
```
|
|
@@ -7535,7 +7828,7 @@ A job-level gate holds just that job; other jobs in the run proceed normally:
|
|
|
7535
7828
|
```typescript
|
|
7536
7829
|
job('publish', {
|
|
7537
7830
|
runsOn: 'default',
|
|
7538
|
-
|
|
7831
|
+
approval: [{ team: 'leads' }],
|
|
7539
7832
|
steps: [
|
|
7540
7833
|
/* ... */
|
|
7541
7834
|
],
|
|
@@ -7552,7 +7845,7 @@ job('migrate-and-deploy', {
|
|
|
7552
7845
|
steps: [
|
|
7553
7846
|
step('build-plan', async (ctx) => ctx.$`./gen-migration-plan.sh`),
|
|
7554
7847
|
step('apply-migration', {
|
|
7555
|
-
|
|
7848
|
+
approval: [{ team: 'dba' }],
|
|
7556
7849
|
run: async (ctx) => ctx.$`./apply-migration.sh`,
|
|
7557
7850
|
}),
|
|
7558
7851
|
step('deploy', async (ctx) => ctx.$`deploy --prod`),
|
|
@@ -7564,9 +7857,39 @@ Here `build-plan` runs, then the job pauses for a `dba` approval. On approval, `
|
|
|
7564
7857
|
|
|
7565
7858
|
Because a step-level hold keeps an agent and its workspace occupied for the whole human wait, prefer job- or workflow-level gates when you do not need prior-step state, and keep step-level timeouts short. See the [operator note on agent occupancy](../operator/approvals.md#agent-occupancy-during-step-level-holds).
|
|
7566
7859
|
|
|
7860
|
+
## Drift gates (`when: 'drift'`)
|
|
7861
|
+
|
|
7862
|
+
A `when: 'drift'` gate is **step-scope only** and requires a [check/apply step](idempotent-steps.md). Instead of pausing unconditionally, it fires **between the step's `check` and `run`, only when `check` finds drift in apply mode** — exactly Terraform's plan→apply, scoped to one step. When the step is already in sync (no drift), nothing pauses and the step skips.
|
|
7863
|
+
|
|
7864
|
+
When the gate fires, the held run carries the **computed drift** as a payload: the rendering your `summarize(drift)` produced (the per-file diff, the commands that would run), plus the structured drift. The dashboard approval queue and the [CLI](#approving-from-the-cli) show the actual diff the operator is approving — not a static reason string.
|
|
7865
|
+
|
|
7866
|
+
```typescript
|
|
7867
|
+
job('patch-prod', {
|
|
7868
|
+
runsOn: 'default',
|
|
7869
|
+
steps: [
|
|
7870
|
+
step('apply-nginx-config', {
|
|
7871
|
+
check: async (ctx) => ((await inSync(ctx)) ? null : { want: DESIRED_CONF }),
|
|
7872
|
+
summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
|
|
7873
|
+
run: async (ctx, drift) => {
|
|
7874
|
+
await writeConfig(drift.want);
|
|
7875
|
+
},
|
|
7876
|
+
approval: { when: 'drift', approvers: [{ team: 'ops' }], reason: 'prod patch' },
|
|
7877
|
+
}),
|
|
7878
|
+
],
|
|
7879
|
+
});
|
|
7880
|
+
```
|
|
7881
|
+
|
|
7882
|
+
Behavior:
|
|
7883
|
+
|
|
7884
|
+
- The gate fires **only in apply mode**. In `--check` mode nothing applies, so nothing gates — drift is just reported.
|
|
7885
|
+
- **Approve** → the step's `run(ctx, drift)` applies the change.
|
|
7886
|
+
- **Reject** → fail-stop: the step fails, the job fails, and the `needs:` skip-cascade aborts everything downstream.
|
|
7887
|
+
|
|
7888
|
+
A `when: 'drift'` gate on a step without a `check` facet, or at job/workflow scope, is a compile error.
|
|
7889
|
+
|
|
7567
7890
|
## Mandatory vs. explicit gates
|
|
7568
7891
|
|
|
7569
|
-
`
|
|
7892
|
+
`approval` is the **explicit** gate — a deliberate "pause for a human here" written by the workflow author. It composes with the **mandatory** gate an operator can attach to a protected environment via required reviewers (see [Environments](environments.md#required-reviewers)). When both apply to the same job, all clauses from both sources must be satisfied before the job is released. The two funnel into one held-element mechanism, so the dashboard queue and `kici approve` work the same way regardless of which source held the element.
|
|
7570
7893
|
|
|
7571
7894
|
## Approving from the CLI
|
|
7572
7895
|
|
|
@@ -7588,10 +7911,23 @@ kici reject <run-id> --job deploy-production --reason "Wrong release branch"
|
|
|
7588
7911
|
|
|
7589
7912
|
You must be eligible for at least one unsatisfied clause — being a member of a named team or being a named user. The orchestrator verifies eligibility against the operator-defined teams, so naming a team in your workflow can never let an ineligible person release the gate. The command reports whether the element was released, how many clauses remain, or that it was rejected. See [`kici approve`](cli-reference.md#kici-approve) for the full command reference.
|
|
7590
7913
|
|
|
7914
|
+
### Inline approval and `--approve-all` in `kici run remote`
|
|
7915
|
+
|
|
7916
|
+
When you trigger a run with `kici run remote` and it pauses on a gate, the CLI prints the gate (and, for a drift gate, the computed drift) and — in an interactive terminal — prompts you to approve or reject right there, without leaving the watch. In a non-interactive shell it prints how to approve out of band and keeps watching.
|
|
7917
|
+
|
|
7918
|
+
To auto-approve **every gate of the run you just dispatched**, pass `--approve-all` (alias `--yes`):
|
|
7919
|
+
|
|
7920
|
+
```bash
|
|
7921
|
+
kici run remote deploy-prod --approve-all
|
|
7922
|
+
```
|
|
7923
|
+
|
|
7924
|
+
`--approve-all` is **run-scoped** — it only auto-approves holds belonging to the run this invocation dispatched; there is no fleet-wide or account-wide auto-approve. Eligibility is still enforced per hold: if you are not eligible for a gate, that gate still blocks. Each auto-approved gate prints its payload before resolving and is recorded distinctly in the audit trail (`held_run.auto_approve`).
|
|
7925
|
+
|
|
7591
7926
|
You can also approve from the dashboard approval queue. See [Dashboard](dashboard/environments-and-secrets.md#approval-queue).
|
|
7592
7927
|
|
|
7593
7928
|
## See also
|
|
7594
7929
|
|
|
7930
|
+
- [Idempotent steps](idempotent-steps.md) — the check/apply step facet that drift gates build on.
|
|
7595
7931
|
- [Environments](environments.md) — operator-required reviewers on protected environments.
|
|
7596
7932
|
- [Approval gates (operator guide)](../operator/approvals.md) — teams, the approval queue, expiry, and self-approval.
|
|
7597
7933
|
- [Approval gates (architecture)](../architecture/approvals.md) — the unified hold model and the step-level round-trip.
|
|
@@ -8224,7 +8560,7 @@ Required reviewers: alice, bob
|
|
|
8224
8560
|
|
|
8225
8561
|
When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](cli-reference.md#kici-approve) command, or the API. Held runs expire after a configurable timeout.
|
|
8226
8562
|
|
|
8227
|
-
This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `
|
|
8563
|
+
This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `approval` at step, job, or workflow level — see [Approval gates](approvals.md). Both forms use the same held-element mechanism and the same queue.
|
|
8228
8564
|
|
|
8229
8565
|
### Wait timer
|
|
8230
8566
|
|
|
@@ -8833,6 +9169,135 @@ Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workf
|
|
|
8833
9169
|
|
|
8834
9170
|
---
|
|
8835
9171
|
|
|
9172
|
+
## Idempotent steps and check mode
|
|
9173
|
+
|
|
9174
|
+
Source: https://docs.kici.dev/user/idempotent-steps/
|
|
9175
|
+
|
|
9176
|
+
An **idempotent step** describes _desired state_ rather than a fixed sequence of
|
|
9177
|
+
commands. You give the step a `check` function that inspects the world and a
|
|
9178
|
+
`run` function that converges it. KiCI then executes the workflow in one of two
|
|
9179
|
+
modes:
|
|
9180
|
+
|
|
9181
|
+
- **Apply mode** (the default): for each step, `check()` runs first; on drift the
|
|
9182
|
+
step applies the change; when already in sync the step is skipped.
|
|
9183
|
+
- **Check mode** (`--check`): for each step, `check()` runs and KiCI reports what
|
|
9184
|
+
_would_ change — **without changing anything**. This is the same model as a
|
|
9185
|
+
dry-run plan: you see the drift before any side effect happens.
|
|
9186
|
+
|
|
9187
|
+
This turns a workflow into convergent configuration management: re-running an
|
|
9188
|
+
apply is safe (in-sync steps do nothing), and a check-mode run is a read-only
|
|
9189
|
+
preview you can gate a build on.
|
|
9190
|
+
|
|
9191
|
+
## Authoring a checked step
|
|
9192
|
+
|
|
9193
|
+
Add a `check` facet to the existing `step()` factory. When `check` is present,
|
|
9194
|
+
`run` becomes the _apply_ function and receives the drift value `check`
|
|
9195
|
+
returned:
|
|
9196
|
+
|
|
9197
|
+
```typescript
|
|
9198
|
+
import { step, z } from '@kici-dev/sdk';
|
|
9199
|
+
|
|
9200
|
+
const configureNginx = step('configure-nginx', {
|
|
9201
|
+
// optional schema for the drift value — gives the dashboard a typed shape
|
|
9202
|
+
drift: z.object({ want: z.string() }),
|
|
9203
|
+
|
|
9204
|
+
// read-only inspection; return null when already in the desired state
|
|
9205
|
+
check: async (ctx) => {
|
|
9206
|
+
const current = await ctx.$`nginx -T`;
|
|
9207
|
+
return current.stdout.includes(DESIRED) ? null : { want: DESIRED };
|
|
9208
|
+
},
|
|
9209
|
+
|
|
9210
|
+
// human-readable preview line — REQUIRED when check is set. It is the drift's
|
|
9211
|
+
// serializable face: it streams to the logs and persists for the dashboard.
|
|
9212
|
+
summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
|
|
9213
|
+
|
|
9214
|
+
// apply — runs only when check returned drift (apply mode); receives that drift
|
|
9215
|
+
run: async (ctx, drift) => {
|
|
9216
|
+
await writeConfig(drift.want);
|
|
9217
|
+
return { reloaded: true };
|
|
9218
|
+
},
|
|
9219
|
+
|
|
9220
|
+
// optional — runs when check returned null, to produce the step's outputs
|
|
9221
|
+
whenInSync: async () => ({ reloaded: false }),
|
|
9222
|
+
});
|
|
9223
|
+
```
|
|
9224
|
+
|
|
9225
|
+
### The facet fields
|
|
9226
|
+
|
|
9227
|
+
| Field | Required | Purpose |
|
|
9228
|
+
| ------------ | ---------------- | ---------------------------------------------------------------------- |
|
|
9229
|
+
| `check` | to opt in | Read-only inspection. Return a drift value, or `null` when in sync. |
|
|
9230
|
+
| `summarize` | when `check` set | Human-readable, serializable preview of the drift. Streams + persists. |
|
|
9231
|
+
| `run` | always | Apply function. With `check`, it receives the drift as its second arg. |
|
|
9232
|
+
| `whenInSync` | optional | Produces the step's outputs when `check` returned `null`. |
|
|
9233
|
+
| `drift` | optional | Schema that validates / shapes the drift value. |
|
|
9234
|
+
|
|
9235
|
+
`summarize` is **required** whenever `check` is declared. `run` and `whenInSync`
|
|
9236
|
+
both produce the same output type — one output shape per step, whichever path
|
|
9237
|
+
runs. Every other step facet (`cache`, `rules`, `continueOnError`, `timeout`,
|
|
9238
|
+
`approval`, `onCancel`, `cleanup`, `outputs`) composes unchanged.
|
|
9239
|
+
|
|
9240
|
+
A plain `step()` without `check` keeps its exact current behavior — the check
|
|
9241
|
+
facet is fully optional.
|
|
9242
|
+
|
|
9243
|
+
## Run modes
|
|
9244
|
+
|
|
9245
|
+
A run carries one of three modes:
|
|
9246
|
+
|
|
9247
|
+
| Mode | CLI flags | Behavior |
|
|
9248
|
+
| --------------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
|
|
9249
|
+
| `apply` | (default, no flags) | Converge: drift ⇒ apply ⇒ **applied**; null ⇒ **in sync** (skipped). |
|
|
9250
|
+
| `check` | `--check` | Preview only: drift ⇒ **would change**; null ⇒ **in sync**. Never applies. Always exits 0. |
|
|
9251
|
+
| `check-fail-on-drift` | `--check --fail-on-drift` | Same as check, but the run **fails** if any step reports drift. |
|
|
9252
|
+
|
|
9253
|
+
Per-step outcomes:
|
|
9254
|
+
|
|
9255
|
+
- **applied** — drift was found and the step applied the change (apply mode).
|
|
9256
|
+
- **in sync** — `check` returned `null`; nothing to do.
|
|
9257
|
+
- **would change** — drift was found in check mode; the change was previewed, not applied.
|
|
9258
|
+
- **no check** — a plain step (no `check`) reached under check mode. A
|
|
9259
|
+
side-effecting step can't be safely previewed, so it is skipped.
|
|
9260
|
+
|
|
9261
|
+
In check mode KiCI never invokes a checked step's `run` (apply) — the preview is
|
|
9262
|
+
guaranteed side-effect-free.
|
|
9263
|
+
|
|
9264
|
+
## Running in check mode
|
|
9265
|
+
|
|
9266
|
+
`--check` and `--fail-on-drift` work on both local and remote runs:
|
|
9267
|
+
|
|
9268
|
+
```bash
|
|
9269
|
+
# Apply (default): converge the workflow.
|
|
9270
|
+
kici run local push
|
|
9271
|
+
kici run remote my-fixture
|
|
9272
|
+
|
|
9273
|
+
# Check: report drift, change nothing. Always exits 0.
|
|
9274
|
+
kici run local push --check
|
|
9275
|
+
kici run remote my-fixture --check
|
|
9276
|
+
|
|
9277
|
+
# Check + fail on drift: exit non-zero (2) locally, or fail the run remotely,
|
|
9278
|
+
# when any step reports drift. Use this as a CI gate ("fail the build if prod
|
|
9279
|
+
# has drifted").
|
|
9280
|
+
kici run local push --check --fail-on-drift
|
|
9281
|
+
```
|
|
9282
|
+
|
|
9283
|
+
`--fail-on-drift` only modifies check mode — passing it without `--check` is an
|
|
9284
|
+
error.
|
|
9285
|
+
|
|
9286
|
+
## Where outcomes show up
|
|
9287
|
+
|
|
9288
|
+
A check-mode run is labeled in the dashboard with a **CHECK MODE — preview**
|
|
9289
|
+
badge on the run header. Each step shows its outcome chip — applied / in sync /
|
|
9290
|
+
would change / no check — and, when drift was detected, the `summarize` line
|
|
9291
|
+
describing what would change. The rendering is read-only.
|
|
9292
|
+
|
|
9293
|
+
## See also
|
|
9294
|
+
|
|
9295
|
+
- [Idempotent SDK helpers](./sdk/idempotent.md) — the `idempotent()` / `idempotentStep()` convenience wrappers, which always apply on drift inside a single step (no run-level check mode).
|
|
9296
|
+
- [Core SDK reference](./sdk/core.md) — the `step()`, `job()`, and `workflow()` factories the check facet extends.
|
|
9297
|
+
- [Lock file and drift](./lock-file-and-drift.md) — how the lock file carries step capability flags.
|
|
9298
|
+
|
|
9299
|
+
---
|
|
9300
|
+
|
|
8836
9301
|
## Private npm registries
|
|
8837
9302
|
|
|
8838
9303
|
Source: https://docs.kici.dev/user/private-registries/
|
|
@@ -9618,7 +10083,83 @@ forge side looks like:
|
|
|
9618
10083
|
Use the App when you can; the `github-repo` preset is a fallback for
|
|
9619
10084
|
repos where you can't install an App.
|
|
9620
10085
|
|
|
9621
|
-
##
|
|
10086
|
+
## One-click setup (recommended)
|
|
10087
|
+
|
|
10088
|
+
`kici-admin source add github --manifest` creates **and** configures the
|
|
10089
|
+
GitHub App for you via GitHub's App Manifest flow. KiCI builds a manifest
|
|
10090
|
+
with the exact permissions, events, webhook URL, and webhook secret baked
|
|
10091
|
+
in, so you never pick permissions, paste a URL, generate a secret, or
|
|
10092
|
+
download a `.pem` by hand — the App is correct by construction.
|
|
10093
|
+
|
|
10094
|
+
```bash
|
|
10095
|
+
kici-admin --url http://<orchestrator-host>:4000 --token $KICI_BOOTSTRAP_ADMIN_TOKEN \
|
|
10096
|
+
source add github --manifest --name my-org --github-org my-org
|
|
10097
|
+
```
|
|
10098
|
+
|
|
10099
|
+
`--github-org <slug>` creates the App under a GitHub **organization** (the
|
|
10100
|
+
`<slug>` is the org's `github.com/<slug>` URL slug, not its display name) — the
|
|
10101
|
+
recommended default, since org-owned Apps can be installed across the org. Drop
|
|
10102
|
+
the flag only when you deliberately want a personal-account App, which can be
|
|
10103
|
+
installed solely on repos you own. You need permission to create Apps in that
|
|
10104
|
+
org (be an org owner, or have the org allow member App creation).
|
|
10105
|
+
|
|
10106
|
+
What happens:
|
|
10107
|
+
|
|
10108
|
+
1. The CLI resolves your org's webhook URL and opens GitHub with a
|
|
10109
|
+
pre-filled App manifest. You click **"Create GitHub App"** once — the
|
|
10110
|
+
only manual step.
|
|
10111
|
+
2. GitHub redirects back to a localhost callback; the CLI exchanges the
|
|
10112
|
+
returned setup code for the App's id, private key, and webhook secret.
|
|
10113
|
+
**The private key is exchanged and stored only on your orchestrator
|
|
10114
|
+
host — it never transits the KiCI Platform.**
|
|
10115
|
+
3. The CLI stores the credentials encrypted under `KICI_SECRET_KEY` and
|
|
10116
|
+
registers the routing key `github:<appId>`, reusing the same storage
|
|
10117
|
+
path as the manual flow.
|
|
10118
|
+
4. It opens the App's install page so you can pick repos, then verifies
|
|
10119
|
+
end-to-end: it waits for the installation, mints an installation token,
|
|
10120
|
+
and confirms repo access before declaring success.
|
|
10121
|
+
|
|
10122
|
+
```
|
|
10123
|
+
$ kici-admin source add github --manifest --name my-org --github-org my-org
|
|
10124
|
+
→ Opening GitHub to create your App…
|
|
10125
|
+
→ ✓ App created (id 12345), credentials captured
|
|
10126
|
+
→ ✓ Stored on orchestrator (encrypted), registered as github:12345
|
|
10127
|
+
→ Install the App on your repos: https://github.com/apps/my-org/installations/new
|
|
10128
|
+
→ ✓ Installation detected (account my-org)
|
|
10129
|
+
→ ✓ Credentials verified (3 repositories reachable)
|
|
10130
|
+
|
|
10131
|
+
GitHub App "my-org" is live.
|
|
10132
|
+
Webhook: https://<platform-host>/webhook/<orgId>/github
|
|
10133
|
+
```
|
|
10134
|
+
|
|
10135
|
+
**Flags:**
|
|
10136
|
+
|
|
10137
|
+
| Flag | Effect |
|
|
10138
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
10139
|
+
| `--name <name>` | The App name you _request_ on GitHub (required). GitHub assigns the final name + slug; the stored, displayed name always comes from GitHub (see [Display name and slug](#display-name-and-slug)). |
|
|
10140
|
+
| `--github-org <slug>` | Create the App under a GitHub organization instead of your personal account. |
|
|
10141
|
+
| `--webhook-url <url>` | **Advanced / self-hosted.** Bake this `https://` URL into the App's webhook verbatim and skip the platform-mode webhook-URL resolution (so it works even where the auto-resolved KiCI Platform URL is unavailable). See [Self-hosted webhook URL](#self-hosted-webhook-url-override). |
|
|
10142
|
+
| `--no-browser` | Headless mode: the CLI prints a `kici.dev` URL to open, then reads the setup code you paste back. The page is pure client-side — it only displays the short-lived code, which is useless once the CLI exchanges it. |
|
|
10143
|
+
|
|
10144
|
+
The manifest flow always creates a **new** App on GitHub. If a source for
|
|
10145
|
+
that App id already exists on the orchestrator, the command refuses — use
|
|
10146
|
+
`source update` to rotate an existing App's credentials.
|
|
10147
|
+
|
|
10148
|
+
If any step after App creation fails (e.g. storage), the CLI prints the
|
|
10149
|
+
captured App id and writes the private key to a `0600` file, then tells
|
|
10150
|
+
you how to finish with the manual `source add github` command — so a
|
|
10151
|
+
created App is never orphaned.
|
|
10152
|
+
|
|
10153
|
+
Independent-mode orchestrators have no GitHub-App ingress (it is
|
|
10154
|
+
Platform-relayed), so the manifest flow is unavailable there; use a
|
|
10155
|
+
generic webhook source instead.
|
|
10156
|
+
|
|
10157
|
+
## Manual setup (fallback)
|
|
10158
|
+
|
|
10159
|
+
When you'd rather create the App by hand — or your environment can't run
|
|
10160
|
+
the manifest flow — follow these steps.
|
|
10161
|
+
|
|
10162
|
+
### Create the GitHub App on GitHub's side
|
|
9622
10163
|
|
|
9623
10164
|
1. **Decide the App scope.** User-owned Apps can only be installed on
|
|
9624
10165
|
repos you own; organization-owned Apps can be installed anywhere in
|
|
@@ -9694,7 +10235,7 @@ repos where you can't install an App.
|
|
|
9694
10235
|
KiCI runs. Re-install to add repos later — this is live and
|
|
9695
10236
|
revocable without redeploying the App.
|
|
9696
10237
|
|
|
9697
|
-
|
|
10238
|
+
### Register the App with the orchestrator
|
|
9698
10239
|
|
|
9699
10240
|
With the App ID, private key `.pem`, and webhook secret in hand:
|
|
9700
10241
|
|
|
@@ -9762,6 +10303,53 @@ its own source record and the orchestrator looks up the right one by
|
|
|
9762
10303
|
combining the URL's `<orgId>` with the App ID from the
|
|
9763
10304
|
`X-GitHub-Hook-Installation-Target-ID` header.
|
|
9764
10305
|
|
|
10306
|
+
## Display name and slug
|
|
10307
|
+
|
|
10308
|
+
For a GitHub App source, **GitHub is the source of truth for the displayed
|
|
10309
|
+
name**. `--name` is only the name you _request_ when the App is created;
|
|
10310
|
+
GitHub assigns the final display name and a URL-safe **slug**
|
|
10311
|
+
(`my-org` → `my-org-1` if the name was taken). KiCI captures both at creation
|
|
10312
|
+
and shows them in the dashboard **Sources** tab — the display name prominently,
|
|
10313
|
+
with the slug as dimmed secondary text.
|
|
10314
|
+
|
|
10315
|
+
If you later rename the App in GitHub's UI, KiCI keeps the displayed name in
|
|
10316
|
+
sync two ways:
|
|
10317
|
+
|
|
10318
|
+
- **Automatically**, on a daily schedule. The orchestrator re-reads each GitHub
|
|
10319
|
+
source's name + slug from GitHub and updates the dashboard if they changed.
|
|
10320
|
+
The interval is configurable via `KICI_GITHUB_APP_NAME_REFRESH_INTERVAL_MS`
|
|
10321
|
+
(default 24h).
|
|
10322
|
+
- **On demand**, with `source refresh`:
|
|
10323
|
+
|
|
10324
|
+
```bash
|
|
10325
|
+
kici-admin source refresh github:<appId> # one source
|
|
10326
|
+
kici-admin source refresh --all # every GitHub source
|
|
10327
|
+
```
|
|
10328
|
+
|
|
10329
|
+
It prints `old → new` for any name or slug that changed, and is a no-op when
|
|
10330
|
+
GitHub already matches what KiCI has stored. Non-GitHub routing keys are
|
|
10331
|
+
rejected — name/slug sync applies only to GitHub App sources.
|
|
10332
|
+
|
|
10333
|
+
## Self-hosted webhook URL override
|
|
10334
|
+
|
|
10335
|
+
By default the manifest flow bakes the KiCI Platform webhook endpoint
|
|
10336
|
+
(`https://<platform-host>/webhook/<orgId>/github`) into the App. If you run
|
|
10337
|
+
your own ingress and want GitHub to deliver events to it instead, pass
|
|
10338
|
+
`--webhook-url` when creating the App:
|
|
10339
|
+
|
|
10340
|
+
```bash
|
|
10341
|
+
kici-admin source add github --manifest --name my-org \
|
|
10342
|
+
--webhook-url https://hooks.my-infra.example/github
|
|
10343
|
+
```
|
|
10344
|
+
|
|
10345
|
+
The supplied URL must be an absolute `https://` URL; it is written into the
|
|
10346
|
+
App's webhook configuration **verbatim**. This is the operator asserting "I own
|
|
10347
|
+
webhook delivery": KiCI adds **no** ingress at this URL and does **not** receive
|
|
10348
|
+
events there — your own infrastructure is responsible for accepting GitHub's
|
|
10349
|
+
deliveries and routing them onward. Supplying the flag also decouples App
|
|
10350
|
+
creation from platform-mode URL resolution, so it works even in a configuration
|
|
10351
|
+
where the auto-resolved KiCI Platform URL is unavailable.
|
|
10352
|
+
|
|
9765
10353
|
## Global workflows
|
|
9766
10354
|
|
|
9767
10355
|
A GitHub App source opts in to org-wide global workflows using the
|
|
@@ -10273,7 +10861,7 @@ Source: https://docs.kici.dev/architecture/data-flows/
|
|
|
10273
10861
|
|
|
10274
10862
|
This document describes the key data flows through the KiCI architecture: webhook delivery, job execution, developer-initiated remote runs, dependency caching, re-run and cancel, trace ID propagation, internal event routing, and generic webhook ingestion.
|
|
10275
10863
|
|
|
10276
|
-
> **Lock file schema version:** The lock file uses schema version
|
|
10864
|
+
> **Lock file schema version:** The lock file uses schema version 21, which adds the `CheckMode` / `CheckStepOutcome` enums for check-mode step execution on top of v20's `LabelMatcher` (exact/regex) selectors for `runsOn`/`runsOnAll`/`excludeLabels`, v19's `maxParallel`/`failFast` fan-out concurrency, v18's `runsOnAll` host fan-out predicate and `onUnreachable` policy, v17's typed init presets (`mise` / `{ mise }`) and `auto` detection, v16's normalized approval config, v15's per-job init config, v14's declarative cache specs, v11's `LockInlineValue` for pure function inline evaluation, v10's simplified negative patterns (! prefix in repos/paths arrays), v9's global workflow repos matching, and v8's runsOn polymorphic type support.
|
|
10277
10865
|
|
|
10278
10866
|
## Webhook delivery flow
|
|
10279
10867
|
|
|
@@ -10479,7 +11067,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
|
|
|
10479
11067
|
|
|
10480
11068
|
### Cross-source / no-contentHash workflows
|
|
10481
11069
|
|
|
10482
|
-
- **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is
|
|
11070
|
+
- **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 21.
|
|
10483
11071
|
- **Cross-source / global-workflow dispatch** (a workflow registered against source A fired by a webhook on source B) bypasses both caches. The registration's lock file entry still carries `contentHash`, but the cross-source path always clone-and-installs — the eval temp dir doesn't ship `@kici-dev/sdk`. The execution agent still verifies `contentHash` against the cloned source for drift detection.
|
|
10484
11072
|
|
|
10485
11073
|
### Build deduplication
|
|
@@ -10518,7 +11106,7 @@ Both source and dep caches use `S3CacheStorage` as the sole backend. The `CacheS
|
|
|
10518
11106
|
|
|
10519
11107
|
Cache keys reflect that source tarballs and deps have different platform characteristics:
|
|
10520
11108
|
|
|
10521
|
-
- **Source:** `source/{contentHash}.tar.gz` — platform-agnostic. Raw TypeScript source is identical regardless of CPU architecture, so one entry is shared across all platforms. `contentHash` is the per-workflow hash from the lock file (`SHA-256(
|
|
11109
|
+
- **Source:** `source/{contentHash}.tar.gz` — platform-agnostic. Raw TypeScript source is identical regardless of CPU architecture, so one entry is shared across all platforms. `contentHash` is the per-workflow hash from the lock file (`SHA-256(COMPILE_SCHEMA_VERSION + ":" + rawSource [+ "\0" + assetDigest])`, where `COMPILE_SCHEMA_VERSION = 5` and line endings are normalized to LF so the hash agrees across platforms).
|
|
10522
11110
|
- **Deps:** `deps/{platform}-{arch}/{lockfileHash}.tar.gz` (e.g., `deps/linux-arm64/def456.tar.gz`) — platform-specific. Native dependencies in `node_modules` differ across architectures, so each platform/arch combination gets its own cache entry.
|
|
10523
11111
|
|
|
10524
11112
|
The orchestrator derives the target platform/arch for dep cache lookups by probing `AgentRegistry.findAvailable()` with the workflow's first job's `runsOn` labels to find a representative matching agent, then using that agent's platform and arch. Falls back to `linux/x64` if no matching agents are registered.
|
|
@@ -11163,7 +11751,9 @@ This model also enables fully self-hosted deployment: all three tiers can run on
|
|
|
11163
11751
|
|
|
11164
11752
|
### Platform
|
|
11165
11753
|
|
|
11166
|
-
The Platform
|
|
11754
|
+
The Platform is KiCI's hosted, multi-tenant control plane. It provides the hosted dashboard (run listing, run detail, live log streaming, settings), identity and authentication (OIDC, personal access tokens, API keys, JWTs), multi-tenant organization / team / role-based access management, billing, and webhook ingestion -- verifying inbound signatures (HMAC-SHA256, timing-safe) and relaying payloads to the correct orchestrator over WebSocket. It aggregates execution telemetry and status forwarded by orchestrators, registers sources, and matchmakes peers for clustering.
|
|
11755
|
+
|
|
11756
|
+
The Platform never processes, stores, or executes customer code, and never sees customer secrets. It routes webhook payloads and aggregates execution status; the code itself only ever lives on the customer's orchestrator and agent tiers. In the execution path the Platform is deliberately thin -- it does not run jobs -- but functionally it is a full platform, not merely a relay. The hosted Platform is EU-sovereign.
|
|
11167
11757
|
|
|
11168
11758
|
### Orchestrator (`@kici-dev/orchestrator`)
|
|
11169
11759
|
|
|
@@ -11198,7 +11788,7 @@ The agent is the execution worker. It runs on customer infrastructure and has fu
|
|
|
11198
11788
|
|
|
11199
11789
|
Shared business logic used by all three tiers. Single source of truth for cross-tier concerns. Has no internal `@kici-dev/*` dependencies -- only a handful of third-party libraries.
|
|
11200
11790
|
|
|
11201
|
-
- Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, test
|
|
11791
|
+
- Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, the test-relay control plane, log pull, run events, peer-to-peer, cluster join, and source registration)
|
|
11202
11792
|
- Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster)
|
|
11203
11793
|
- Trigger matching engine (branch, path, event evaluation)
|
|
11204
11794
|
- Execution state machine (11 states, 16 events, pure functions)
|