@kici-dev/compiler 0.5.0 → 0.6.1
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.js +46 -8
- package/dist/commands/approve.d.ts +12 -0
- package/dist/commands/approve.js +5 -2
- package/dist/commands/compile.js +4 -2
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/endpoints.js +4 -6
- package/dist/commands/feedback.d.ts +53 -0
- package/dist/commands/feedback.js +142 -0
- package/dist/commands/held-run-client.d.ts +21 -1
- package/dist/commands/held-run-client.js +34 -15
- package/dist/commands/hook.js +22 -20
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +3 -1
- package/dist/commands/init.d.ts +9 -2
- package/dist/commands/init.js +43 -16
- package/dist/commands/login.js +1 -1
- package/dist/commands/orchestrators.js +3 -2
- package/dist/commands/reject.d.ts +12 -0
- package/dist/commands/reject.js +5 -2
- package/dist/commands/report/collect.d.ts +82 -0
- package/dist/commands/report/collect.js +234 -0
- package/dist/commands/report/identity.d.ts +48 -0
- package/dist/commands/report/identity.js +49 -0
- package/dist/commands/report/index.d.ts +63 -0
- package/dist/commands/report/index.js +119 -0
- package/dist/commands/report/upload.d.ts +38 -0
- package/dist/commands/report/upload.js +64 -0
- package/dist/commands/run-hold-watch.js +2 -2
- package/dist/commands/run.js +6 -3
- package/dist/commands/runs/show.js +80 -1
- package/dist/commands/types.js +51 -8
- package/dist/execution/sdk-alias.js +4 -2
- package/dist/fixtures/compiler.js +2 -1
- package/dist/format.js +3 -3
- package/dist/generators/secrets-dts.d.ts +8 -2
- package/dist/generators/secrets-dts.js +3 -2
- package/dist/hooks/installer.js +2 -1
- package/dist/llm-context/llms-architecture.txt +35 -13
- package/dist/llm-context/llms-cli-remote.txt +2347 -0
- package/dist/llm-context/llms-cli.txt +284 -2470
- package/dist/llm-context/llms-features-execution.txt +2028 -0
- package/dist/llm-context/llms-features.txt +298 -1483
- package/dist/llm-context/llms-full.txt +6153 -4639
- package/dist/llm-context/llms-getting-started.txt +292 -12
- package/dist/llm-context/llms-patterns.txt +176 -1
- package/dist/llm-context/llms-providers.txt +11 -27
- package/dist/llm-context/llms-sdk-runtime.txt +25 -4
- package/dist/llm-context/llms-sdk.txt +31 -1
- package/dist/llm-context/llms.txt +33 -18
- package/dist/local-plane/paths.d.ts +15 -0
- package/dist/local-plane/paths.js +22 -1
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/local-plane/port-holder.js +1 -1
- package/dist/local-plane/postgres.d.ts +3 -16
- package/dist/local-plane/postgres.js +10 -15
- package/dist/lockfile/generator.d.ts +12 -0
- package/dist/lockfile/generator.js +47 -14
- package/dist/postinstall.js +2 -1
- package/dist/remote/config.d.ts +2 -15
- package/dist/remote/config.js +2 -16
- package/dist/remote/dashboard-client.d.ts +39 -0
- package/dist/remote/dashboard-client.js +41 -0
- package/dist/remote/oauth.js +7 -5
- package/dist/remote/uploader.js +2 -2
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/dry-run.js +4 -2
- package/dist/test-runner/git-detector.js +2 -1
- package/dist/test-runner/job-executor.js +2 -1
- package/dist/test-runner/payload-builder.js +11 -17
- package/dist/types.d.ts +33 -3
- package/dist/validation/validator.js +23 -6
- package/package.json +16 -11
- package/sbom.spdx.json +953 -901
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# KiCI Workflow features
|
|
1
|
+
# KiCI Workflow features: access and approval
|
|
2
2
|
|
|
3
|
-
This bundle covers:
|
|
3
|
+
This bundle covers: Deployment contexts, scoped secrets, private registries, approvals, provenance, dashboard and account access.
|
|
4
4
|
|
|
5
5
|
## Account and sign-in
|
|
6
6
|
|
|
@@ -196,6 +196,10 @@ Here `build-plan` runs, then the job pauses for a `dba` approval. On approval, `
|
|
|
196
196
|
|
|
197
197
|
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](https://docs.kici.dev/operator/approvals/#agent-occupancy-during-step-level-holds).
|
|
198
198
|
|
|
199
|
+
### Not available on organization-wide workflows
|
|
200
|
+
|
|
201
|
+
An `approval` gate applies to per-repository workflows only. A workflow whose trigger carries `repos:` — an [organization-wide workflow](https://docs.kici.dev/user/global-workflows/) — is dispatched by a path that never consults the gate, so the gate would not hold anything. `kici compile` refuses it with `error [E124]` at the workflow level and on any static job, rather than accepting a security control the workflow does not have. Drop the `approval`, or move the gated jobs into a workflow whose triggers carry no `repos:`. A job produced by a `dynamicJob` generator never passes through the compiler, so that one is caught at dispatch instead: the orchestrator logs an error naming the workflow and job, and runs it ungated.
|
|
202
|
+
|
|
199
203
|
## Drift gates (`when: 'drift'`)
|
|
200
204
|
|
|
201
205
|
A `when: 'drift'` gate is **step-scope only** and requires a [check/apply step](https://docs.kici.dev/user/idempotent-steps/). 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.
|
|
@@ -228,7 +232,7 @@ A `when: 'drift'` gate on a step without a `check` facet, or at job/workflow sco
|
|
|
228
232
|
|
|
229
233
|
## Mandatory vs. explicit gates
|
|
230
234
|
|
|
231
|
-
`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 context via required reviewers (see [Contexts](https://docs.kici.dev/user/contexts/#required-reviewers)). When both apply to the same job, all clauses from both sources must be satisfied before the job is released. The
|
|
235
|
+
`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 context via required reviewers (see [Contexts](https://docs.kici.dev/user/contexts/#required-reviewers)). When both apply to the same job, all clauses from both sources must be satisfied before the job is released. A workflow-level `approval` is one of those sources. It gates every root job, so a root job that also binds a context with required reviewers must satisfy the workflow clauses, its own job clauses, and the context's reviewers. The sources funnel into one held-element mechanism, so the dashboard queue and `kici approve` work the same way regardless of which source held the element.
|
|
232
236
|
|
|
233
237
|
## Approving from the CLI
|
|
234
238
|
|
|
@@ -250,1629 +254,437 @@ kici reject <run-id> --job deploy-production --reason "Wrong release branch"
|
|
|
250
254
|
|
|
251
255
|
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`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-approve) for the full command reference.
|
|
252
256
|
|
|
253
|
-
###
|
|
254
|
-
|
|
255
|
-
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.
|
|
256
|
-
|
|
257
|
-
To auto-approve **every gate of the run you just dispatched**, pass `--approve-all` (alias `--yes`):
|
|
258
|
-
|
|
259
|
-
```bash
|
|
260
|
-
kici run remote deploy-prod --approve-all
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
`--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`).
|
|
264
|
-
|
|
265
|
-
`--approve-all` is honored in non-interactive runs too: with `--json` or `--quiet` (no TTY), the flag still auto-approves each eligible gate as it appears instead of hanging or printing out-of-band instructions. Hold notices are routed to stderr so `--json` stdout stays pure machine-readable output.
|
|
266
|
-
|
|
267
|
-
You can also approve from the dashboard approval queue. See [Dashboard](https://docs.kici.dev/user/dashboard/contexts-and-secrets/#approval-queue).
|
|
268
|
-
|
|
269
|
-
## See also
|
|
270
|
-
|
|
271
|
-
- [Idempotent steps](https://docs.kici.dev/user/idempotent-steps/) — the check/apply step facet that drift gates build on.
|
|
272
|
-
- [Contexts](https://docs.kici.dev/user/contexts/) — operator-required reviewers on protected contexts.
|
|
273
|
-
- [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) — teams, the approval queue, expiry, and self-approval.
|
|
274
|
-
- [Approval gates (architecture)](https://docs.kici.dev/architecture/approvals/) — the unified hold model and the step-level round-trip.
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## Concurrency groups
|
|
279
|
-
|
|
280
|
-
Source: https://docs.kici.dev/user/concurrency/
|
|
281
|
-
|
|
282
|
-
Concurrency groups prevent multiple workflow runs from executing in parallel when they target the same resource. Common use cases include preventing parallel deploys to the same environment or serializing database migrations.
|
|
283
|
-
|
|
284
|
-
## Basic usage
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
288
|
-
|
|
289
|
-
export default workflow('deploy', {
|
|
290
|
-
on: push({ branches: ['main', 'staging'] }),
|
|
291
|
-
concurrency: {
|
|
292
|
-
group: (ctx) => `deploy-${ctx.branch}`,
|
|
293
|
-
cancelInProgress: true,
|
|
294
|
-
max: 1,
|
|
295
|
-
},
|
|
296
|
-
jobs: [
|
|
297
|
-
job('deploy', {
|
|
298
|
-
runsOn: 'linux',
|
|
299
|
-
steps: [
|
|
300
|
-
step('deploy', async ({ $ }) => {
|
|
301
|
-
await $`./deploy.sh`;
|
|
302
|
-
}),
|
|
303
|
-
],
|
|
304
|
-
}),
|
|
305
|
-
],
|
|
306
|
-
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
## Configuration
|
|
310
|
-
|
|
311
|
-
The `concurrency` option on a workflow accepts:
|
|
312
|
-
|
|
313
|
-
| Field | Type | Default | Description |
|
|
314
|
-
| ------------------ | -------- | -------- | ------------------------------------------ |
|
|
315
|
-
| `group` | Function | Required | Returns the concurrency group key string |
|
|
316
|
-
| `cancelInProgress` | boolean | `true` | Cancel older runs when a newer run arrives |
|
|
317
|
-
| `max` | number | `1` | Maximum concurrent runs in the same group |
|
|
318
|
-
|
|
319
|
-
### Group key function
|
|
320
|
-
|
|
321
|
-
The group key function receives a context with the branch name and event payload. Runs with the same group key are subject to concurrency limits.
|
|
322
|
-
|
|
323
|
-
```typescript
|
|
324
|
-
// Per-branch concurrency (most common)
|
|
325
|
-
group: (ctx) => `deploy-${ctx.branch}`;
|
|
326
|
-
|
|
327
|
-
// Global concurrency (across all branches)
|
|
328
|
-
group: () => 'deploy';
|
|
329
|
-
|
|
330
|
-
// Per-target-branch concurrency
|
|
331
|
-
group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. Job-level `concurrencyGroup` functions (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)) resolve the same way — on the agent's init step, never in the orchestrator.
|
|
335
|
-
|
|
336
|
-
## cancelInProgress mode
|
|
337
|
-
|
|
338
|
-
When `cancelInProgress: true`, a newer run supersedes older runs in the same group:
|
|
339
|
-
|
|
340
|
-
```
|
|
341
|
-
Run #1 starts deploying to main -> running
|
|
342
|
-
Run #2 arrives for deploy-main group -> Run #1 cancelled ("Superseded by run in concurrency group 'deploy-main'")
|
|
343
|
-
Run #2 continues -> running
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
This is the most common mode for deploy workflows -- you want the latest code deployed, not an outdated version.
|
|
347
|
-
|
|
348
|
-
The cancelled run:
|
|
349
|
-
|
|
350
|
-
- Receives a cancellation with reason "Superseded by run in concurrency group 'deploy-main'"
|
|
351
|
-
- Goes through the normal cancel flow (grace period, hooks if graceful)
|
|
352
|
-
- GitHub Check status updated to `cancelled` with the superseded reason
|
|
353
|
-
|
|
354
|
-
```typescript
|
|
355
|
-
workflow('deploy', {
|
|
356
|
-
concurrency: {
|
|
357
|
-
group: (ctx) => `deploy-${ctx.branch}`,
|
|
358
|
-
cancelInProgress: true,
|
|
359
|
-
},
|
|
360
|
-
jobs: [/* ... */],
|
|
361
|
-
});
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
## Queue mode
|
|
365
|
-
|
|
366
|
-
When `cancelInProgress: false`, newer runs will wait until older runs complete:
|
|
367
|
-
|
|
368
|
-
```
|
|
369
|
-
Run #1 starts deploying -> running
|
|
370
|
-
Run #2 arrives for same group -> queued ("Waiting for deploy-main (1 ahead)")
|
|
371
|
-
Run #1 completes -> success
|
|
372
|
-
Run #2 starts -> running
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
In queue mode, the agent that picked up the queued run **stays connected** to the orchestrator and parks on a long-poll wait. When the holder finishes (success, failure, or cancel), the orchestrator dequeues the FIFO-next entry and pushes a `proceed` notification over the same WebSocket; the queued agent then continues with normal step execution against the workspace it already has. The agent's slot is therefore held for the duration of the queue wait — bound by `KICI_CONCURRENCY_WAIT_TIMEOUT_MS` (default 1 hour).
|
|
376
|
-
|
|
377
|
-
```typescript
|
|
378
|
-
workflow('migrate-db', {
|
|
379
|
-
concurrency: {
|
|
380
|
-
group: () => 'migrations',
|
|
381
|
-
cancelInProgress: false,
|
|
382
|
-
max: 1,
|
|
383
|
-
},
|
|
384
|
-
jobs: [/* ... */],
|
|
385
|
-
});
|
|
386
|
-
```
|
|
387
|
-
|
|
388
|
-
The dashboard will show a "Queued" badge with the reason: "Waiting for deploy-main (1 ahead)".
|
|
389
|
-
|
|
390
|
-
## Max concurrent runs
|
|
391
|
-
|
|
392
|
-
The `max` field controls how many runs can execute simultaneously in the same group:
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
// Allow up to 3 parallel test runs per branch
|
|
396
|
-
workflow('test', {
|
|
397
|
-
concurrency: {
|
|
398
|
-
group: (ctx) => `test-${ctx.branch}`,
|
|
399
|
-
cancelInProgress: false,
|
|
400
|
-
max: 3,
|
|
401
|
-
},
|
|
402
|
-
jobs: [/* ... */],
|
|
403
|
-
});
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
When `max: 1` (default), runs are fully serialized within the group.
|
|
407
|
-
|
|
408
|
-
## Group key examples
|
|
409
|
-
|
|
410
|
-
### Deploy per environment
|
|
411
|
-
|
|
412
|
-
```typescript
|
|
413
|
-
workflow('deploy', {
|
|
414
|
-
concurrency: {
|
|
415
|
-
group: (ctx) => `deploy-${ctx.branch}`,
|
|
416
|
-
cancelInProgress: true,
|
|
417
|
-
},
|
|
418
|
-
jobs: [
|
|
419
|
-
job('deploy-staging', {
|
|
420
|
-
runsOn: 'linux',
|
|
421
|
-
context: 'staging',
|
|
422
|
-
steps: [/* ... */],
|
|
423
|
-
}),
|
|
424
|
-
],
|
|
425
|
-
});
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
### Global singleton
|
|
429
|
-
|
|
430
|
-
```typescript
|
|
431
|
-
// Only one migration can run at a time, regardless of branch
|
|
432
|
-
workflow('migrate', {
|
|
433
|
-
concurrency: {
|
|
434
|
-
group: () => 'db-migration',
|
|
435
|
-
cancelInProgress: false,
|
|
436
|
-
},
|
|
437
|
-
jobs: [/* ... */],
|
|
438
|
-
});
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### Environment-aware groups
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
// Serialize deploys per environment
|
|
445
|
-
workflow('deploy', {
|
|
446
|
-
concurrency: {
|
|
447
|
-
group: (ctx) => {
|
|
448
|
-
const env = ctx.branch === 'main' ? 'production' : 'staging';
|
|
449
|
-
return `deploy-${env}`;
|
|
450
|
-
},
|
|
451
|
-
cancelInProgress: true,
|
|
452
|
-
},
|
|
453
|
-
jobs: [/* ... */],
|
|
454
|
-
});
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
## Interaction with context protection
|
|
458
|
-
|
|
459
|
-
When a workflow has both `concurrency` and `context` protection rules:
|
|
460
|
-
|
|
461
|
-
1. Context protection gates (required reviewers, wait timer) apply first
|
|
462
|
-
2. Concurrency group check happens after protection gates pass
|
|
463
|
-
3. If the run is queued by concurrency, it keeps its protection approval
|
|
464
|
-
|
|
465
|
-
This means a run that passed approval won't need re-approval if it gets queued by concurrency.
|
|
466
|
-
|
|
467
|
-
## Cancelling queued runs
|
|
468
|
-
|
|
469
|
-
Queued runs can be cancelled before they start executing. The cancel request removes them from the queue immediately -- they don't go through the grace period since no step is running.
|
|
470
|
-
|
|
471
|
-
## Job-level concurrency groups
|
|
472
|
-
|
|
473
|
-
In addition to workflow-level concurrency, individual jobs can define their own concurrency group via the `concurrencyGroup` property. This controls concurrent execution at the job level rather than the workflow level. See [Contexts — concurrency groups](https://docs.kici.dev/user/contexts/#concurrency-groups) for details.
|
|
474
|
-
|
|
475
|
-
## Local execution
|
|
476
|
-
|
|
477
|
-
`kici run <event> --local` is a real routed dispatch: your machine becomes an ephemeral agent behind the local dev plane, whose own orchestrator applies the same concurrency machinery described above. The `group` callback is evaluated agent-side against the simulated event, and `cancelInProgress` carries its usual semantics — `true` supersedes the older run in the group, `false` queues the newer one behind it.
|
|
478
|
-
|
|
479
|
-
Coordination is scoped to that plane. The plane's state (including its database) lives under `~/.kici/local/`, so enforcement is per-machine and per-user: running the same workflow on two different machines does not serialize across them. For cross-host enforcement (queueing across agents, dashboard visibility), use `kici run remote` against a deployed orchestrator.
|
|
480
|
-
|
|
481
|
-
See [`kici run <event> --local`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-run-event---local) for the rest of the local-run behavior, and [the local dev plane](https://docs.kici.dev/operator/orchestrator/local-dev-plane/) for the plane's state directory and lifecycle.
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
_Source: `packages/sdk/src/types.ts` (WorkflowOptions.concurrency, JobOptions.concurrencyGroup)_
|
|
486
|
-
|
|
487
|
-
---
|
|
488
|
-
|
|
489
|
-
## Contexts
|
|
490
|
-
|
|
491
|
-
Source: https://docs.kici.dev/user/contexts/
|
|
492
|
-
|
|
493
|
-
<!-- help:contexts-list#overview -->
|
|
494
|
-
|
|
495
|
-
Contexts are named deployment targets (like staging or production) that control where your workflow jobs run. Each context can have its own variables, secrets, and protection rules to gate deployments.
|
|
496
|
-
|
|
497
|
-
<!-- /help:contexts-list -->
|
|
498
|
-
|
|
499
|
-
<!-- help:contexts-protection#protection-rules -->
|
|
500
|
-
|
|
501
|
-
Protection rules control when jobs targeting a context can execute.
|
|
502
|
-
|
|
503
|
-
Available rules:
|
|
504
|
-
|
|
505
|
-
- **Branch restrictions** — only allow specific branches to deploy.
|
|
506
|
-
- **Required reviewer approvals** — gate the run on human sign-off.
|
|
507
|
-
- **Wait timers** — delay execution for a fixed period.
|
|
508
|
-
- **Concurrency limits** — prevent collisions between parallel deployments.
|
|
509
|
-
|
|
510
|
-
<!-- /help:contexts-protection -->
|
|
511
|
-
|
|
512
|
-
Contexts represent deployment targets like `staging`, `production`, or `review/PR-*`. Each context can have its own variables, bound secrets, and protection rules that control when and how jobs targeting that context can execute.
|
|
513
|
-
|
|
514
|
-
## Overview
|
|
515
|
-
|
|
516
|
-
A context in KiCI provides:
|
|
517
|
-
|
|
518
|
-
- **Variables** -- non-secret key-value configuration (e.g., `API_URL`, `CLUSTER_NAME`)
|
|
519
|
-
- **Scoped secrets** -- encrypted values bound to the context via scope bindings
|
|
520
|
-
- **Protection rules** -- branch restrictions, required reviewers, wait timers, and concurrency limits
|
|
521
|
-
- **Per-source overrides** -- repositories can override unlocked variables for their own deployments
|
|
522
|
-
|
|
523
|
-
## SDK API
|
|
524
|
-
|
|
525
|
-
### Job-level context property
|
|
526
|
-
|
|
527
|
-
The `context` property is set on a job, not a workflow or step:
|
|
528
|
-
|
|
529
|
-
```typescript
|
|
530
|
-
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
531
|
-
|
|
532
|
-
export default workflow('deploy', {
|
|
533
|
-
on: [push({ branches: ['main'] })],
|
|
534
|
-
jobs: [
|
|
535
|
-
job('deploy-staging', {
|
|
536
|
-
runsOn: 'default',
|
|
537
|
-
context: 'staging',
|
|
538
|
-
steps: [
|
|
539
|
-
step('deploy', async (ctx) => {
|
|
540
|
-
// ctx.context is the resolved context name
|
|
541
|
-
console.log(`Deploying to ${ctx.context}`);
|
|
542
|
-
// ctx.secrets provides async get/expose/has methods for context-bound secrets
|
|
543
|
-
const dbPassword = await ctx.secrets.get('DB_PASSWORD');
|
|
544
|
-
// Environment variables are in ctx.env
|
|
545
|
-
const apiUrl = ctx.env.API_URL;
|
|
546
|
-
await ctx.$`deploy --target ${ctx.context}`;
|
|
547
|
-
}),
|
|
548
|
-
],
|
|
549
|
-
}),
|
|
550
|
-
],
|
|
551
|
-
});
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
### Dynamic contexts
|
|
555
|
-
|
|
556
|
-
The context name can be a string or a function (sync or async) for dynamic contexts (e.g., per-PR review contexts). The function receives the normalized event envelope, with the raw provider body nested at `event.payload`:
|
|
557
|
-
|
|
558
|
-
```typescript
|
|
559
|
-
job('deploy-review', {
|
|
560
|
-
runsOn: 'default',
|
|
561
|
-
context: (event) => `review/PR-${event.payload.pull_request.number}`,
|
|
562
|
-
steps: [
|
|
563
|
-
step('deploy', async (ctx) => {
|
|
564
|
-
// ctx.context is 'review/PR-123' (resolved at runtime)
|
|
565
|
-
await ctx.$`deploy-preview --env ${ctx.context}`;
|
|
566
|
-
}),
|
|
567
|
-
],
|
|
568
|
-
});
|
|
569
|
-
```
|
|
257
|
+
### When a job is held twice
|
|
570
258
|
|
|
571
|
-
A
|
|
259
|
+
A job can carry an `approval` gate **and** a [security hold](https://docs.kici.dev/user/contexts/#security-approval-queue) at the same time. Two setups reach it: a fork pull request on an organization whose fork policy is `hold`, and a context that sets both required reviewers and `minimumTrust`.
|
|
572
260
|
|
|
573
|
-
|
|
261
|
+
The two are separate holds and **both** must be released before the job runs. They take different permissions: the approval hold needs `contexts:write` plus clause eligibility, the security hold needs `ci_trust:write`.
|
|
574
262
|
|
|
575
|
-
|
|
263
|
+
`--job` names both at once, so pick one with `--hold-type`:
|
|
576
264
|
|
|
577
|
-
```
|
|
578
|
-
job
|
|
579
|
-
|
|
580
|
-
contexts: ['staging', 'my-testing'],
|
|
581
|
-
steps: [
|
|
582
|
-
step('deploy', async (ctx) => {
|
|
583
|
-
// ctx.secrets and ctx.env carry the merged set from both contexts
|
|
584
|
-
const dbUrl = await ctx.secrets.get('DB_URL');
|
|
585
|
-
}),
|
|
586
|
-
],
|
|
587
|
-
});
|
|
265
|
+
```bash
|
|
266
|
+
kici approve <run-id> --job deploy --hold-type reviewer
|
|
267
|
+
kici approve <run-id> --job deploy --hold-type security
|
|
588
268
|
```
|
|
589
269
|
|
|
590
|
-
-
|
|
591
|
-
- Each array entry is a static name or a function of the event, resolved per element exactly like a single dynamic context.
|
|
592
|
-
|
|
593
|
-
**Merge order — last wins.** All bound contexts are resolved on every dispatch (webhook, scheduled, and test runs alike) and merged in array order. When the same secret or variable key is defined in more than one context, the later entry in the array wins. With `contexts: ['staging', 'my-testing']`, a key defined in both resolves to `my-testing`'s value; keys defined in only one are preserved. The longest-scope-path-wins rule still applies _within_ each context.
|
|
594
|
-
|
|
595
|
-
**Protection rules combine all-must-pass.** A job must satisfy **every** bound context's gates — adding a context can never loosen access. Branch restrictions, trigger-type filters, and repo patterns must pass for all contexts; the minimum trust tier is the most restrictive across them; required reviewers are the union of all contexts' reviewers; the wait timer is the longest; and the hold expiry is the shortest. If a run is gated out, the rejection names which context and which rule rejected it (visible via `kici runs show <run-id>` and the run's rejection reason), so a mutually-exclusive set of rules surfaces as a clear failure rather than a silent perpetual rejection.
|
|
270
|
+
Pass `--hold <id>` when the command's error lists two holds that `--hold-type` still cannot separate. The pull request's `KiCI Security` check stays pending until both holds have ended, and its description names the second gate and the permission that clears it.
|
|
596
271
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
**Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
|
|
272
|
+
Each hold carries its own expiry, so the job is cancelled when the **first** one runs out.
|
|
600
273
|
|
|
601
|
-
|
|
274
|
+
### Inline approval and `--approve-all` in `kici run remote`
|
|
602
275
|
|
|
603
|
-
|
|
276
|
+
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.
|
|
604
277
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
job('deploy', {
|
|
609
|
-
runsOn: 'default',
|
|
610
|
-
context: 'production',
|
|
611
|
-
env: { DEPLOY_TARGET: 'us-east-1' },
|
|
612
|
-
// Or dynamic:
|
|
613
|
-
// env: (event) => ({ DEPLOY_SHA: event.payload.after?.slice(0, 7) }),
|
|
614
|
-
steps: [
|
|
615
|
-
step('deploy', async (ctx) => {
|
|
616
|
-
// DEPLOY_TARGET is available in ctx.env
|
|
617
|
-
await ctx.$`deploy --region ${ctx.env.DEPLOY_TARGET}`;
|
|
618
|
-
}),
|
|
619
|
-
],
|
|
620
|
-
});
|
|
621
|
-
```
|
|
622
|
-
|
|
623
|
-
### Concurrency groups
|
|
624
|
-
|
|
625
|
-
Jobs can define their own concurrency groups to control concurrent execution within a context. For workflow-level concurrency (which applies to all jobs in a workflow), see [Concurrency groups](https://docs.kici.dev/user/concurrency/).
|
|
626
|
-
|
|
627
|
-
Control concurrent deployments to the same context:
|
|
628
|
-
|
|
629
|
-
```typescript
|
|
630
|
-
job('deploy', {
|
|
631
|
-
runsOn: 'default',
|
|
632
|
-
context: 'production',
|
|
633
|
-
concurrencyGroup: 'production-api',
|
|
634
|
-
// Or dynamic:
|
|
635
|
-
// concurrencyGroup: (event) => `review-${event.payload.pull_request.number}`,
|
|
636
|
-
steps: [/* ... */],
|
|
637
|
-
});
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
If no `concurrencyGroup` is specified, the context name is used as the default concurrency group. For a job bound to multiple contexts, the default is the **first** bound context's name.
|
|
641
|
-
|
|
642
|
-
### Step context
|
|
643
|
-
|
|
644
|
-
Inside a step, the `ctx` object provides:
|
|
645
|
-
|
|
646
|
-
| Property | Type | Description |
|
|
647
|
-
| ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
648
|
-
| `ctx.context` | `string \| undefined` | Resolved context name (undefined for jobs without context) |
|
|
649
|
-
| `ctx.env` | `Record<string, string \| undefined>` | Environment variables (merged from system, org, source, and job-level `env`) |
|
|
650
|
-
| `ctx.secrets` | `StepSecretsTyped` | Async accessor for bound secrets (get, expose, has, getMeta, list, mountFile, exposeFile) |
|
|
651
|
-
|
|
652
|
-
| Method | Returns | Description |
|
|
653
|
-
| -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
654
|
-
| `await ctx.secrets.get(key)` | `string` | Retrieve a secret value. Throws `SecretNotFoundError` if not found. |
|
|
655
|
-
| `await ctx.secrets.expose(key)` | `void` | Set the secret as an environment variable for this step — visible via `ctx.env` and to child processes (`process.env`). Throws `SecretNotFoundError` if not found. |
|
|
656
|
-
| `ctx.secrets.has(key)` | `boolean` | Check if a secret key exists. Synchronous, never throws. |
|
|
657
|
-
| `ctx.secrets.getMeta(key)` | `SecretMeta \| undefined` | Retrieve metadata (value, backend name, scope path) for a resolved secret. Returns `undefined` if not found. |
|
|
658
|
-
| `ctx.secrets.list()` | `string[]` | Every secret key available to the step, sorted alphabetically. Synchronous, never throws. |
|
|
659
|
-
| `await ctx.secrets.mountFile(opts)` | `{ path }` | Materialise one or more secrets to a per-step tmpfile (auto-removed at step end). See [Secrets → Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files). |
|
|
660
|
-
| `await ctx.secrets.exposeFile(envVar, opts)` | `{ path }` | `mountFile` plus `process.env[envVar] = path`; the env var is unset at step end. |
|
|
661
|
-
| `ctx.setSecretOutput(key, val)` | `void` | Publish an encrypted secret output from this job, consumable by downstream jobs via `needs`. Never logged or stored in plaintext. |
|
|
662
|
-
|
|
663
|
-
The full secrets API — including `SecretFileOptions`, log masking, and the canonical `sops` example — is documented in [Secrets](https://docs.kici.dev/user/secrets/).
|
|
664
|
-
|
|
665
|
-
## Environment variable merge precedence
|
|
666
|
-
|
|
667
|
-
When a job targets a context, variables are merged in this order (last wins):
|
|
668
|
-
|
|
669
|
-
1. **Allowed system vars** -- `PATH`, `HOME`, etc. from the agent process
|
|
670
|
-
2. **Sandbox defaults** -- `FORCE_COLOR=1`
|
|
671
|
-
3. **KICI\_\* system vars** -- orchestrator-generated metadata
|
|
672
|
-
4. **Org-level context vars** -- from the dashboard, managed per-context
|
|
673
|
-
5. **Source-level overrides** -- per-repository overrides (skips locked vars)
|
|
674
|
-
6. **Job env** -- from the `env` property in the SDK
|
|
675
|
-
7. **`setEnv()` calls** -- runtime modifications within steps
|
|
676
|
-
|
|
677
|
-
> **Note:** Secrets are NOT part of the environment variable merge. They are delivered to the step context via IPC and accessed through `ctx.secrets`, not through `process.env`. See the [step context](https://docs.kici.dev/user/contexts/#step-context) section above.
|
|
678
|
-
|
|
679
|
-
## Protection rules
|
|
680
|
-
|
|
681
|
-
Contexts can have protection rules that gate job execution:
|
|
682
|
-
|
|
683
|
-
### Branch restrictions
|
|
684
|
-
|
|
685
|
-
Limit which branches can deploy to a context:
|
|
686
|
-
|
|
687
|
-
```
|
|
688
|
-
Allowed branches: main, release/*
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
Jobs from other branches are rejected immediately with an error message.
|
|
692
|
-
|
|
693
|
-
### Required reviewers
|
|
694
|
-
|
|
695
|
-
Require manual approval before a job can proceed:
|
|
696
|
-
|
|
697
|
-
```
|
|
698
|
-
Required reviewers: alice, bob
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-approve) command, or the API. Held runs expire after a configurable timeout.
|
|
702
|
-
|
|
703
|
-
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](https://docs.kici.dev/user/approvals/). Both forms use the same held-element mechanism and the same queue.
|
|
704
|
-
|
|
705
|
-
### Wait timer
|
|
706
|
-
|
|
707
|
-
Add a mandatory delay before deployment starts:
|
|
708
|
-
|
|
709
|
-
```
|
|
710
|
-
Wait timer: 300 seconds
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
The job waits for the specified duration before proceeding. Useful for staged rollouts.
|
|
714
|
-
|
|
715
|
-
### Minimum trust
|
|
716
|
-
|
|
717
|
-
Gate job execution based on the contributor's trust tier for PR-triggered runs:
|
|
718
|
-
|
|
719
|
-
```
|
|
720
|
-
Minimum trust: known
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
| Value | Effect |
|
|
724
|
-
| --------- | --------------------------------------------------- |
|
|
725
|
-
| `known` | Blocks unknown contributors; allows known + trusted |
|
|
726
|
-
| `trusted` | Blocks unknown + known; allows only trusted |
|
|
727
|
-
|
|
728
|
-
When a contributor does not meet the minimum trust level, the job is held in the security approval queue. Someone with `ci_trust:write` or higher must approve it before execution proceeds.
|
|
729
|
-
|
|
730
|
-
Trust tier is determined by the contributor's identity link and CI trust RBAC level:
|
|
731
|
-
|
|
732
|
-
- **Trusted** -- identity-linked org member with `ci_trust:write+` AND provider write access
|
|
733
|
-
- **Known** -- identity-linked member or verified collaborator via provider API
|
|
734
|
-
- **Unknown** -- no identity link and no provider access, fork PRs
|
|
735
|
-
|
|
736
|
-
The trust tier also affects which lock file is used for PR-triggered runs: trusted contributors use the PR head lock file, while known and unknown contributors use the base branch lock file. This prevents untrusted workflow modifications from affecting execution.
|
|
737
|
-
|
|
738
|
-
See the [CI security architecture docs](https://docs.kici.dev/architecture/security/ci-security/) for the full trust resolution flow.
|
|
739
|
-
|
|
740
|
-
### Security approval queue
|
|
741
|
-
|
|
742
|
-
When a PR is held for security review — the org trust policy held it (a non-trusted contributor modified `.kici/` files, the PR came from a fork, or the contributor could not be resolved to a known identity), or a `minimumTrust` gate blocked the contributor — it enters the security approval queue. This is separate from the context approval queue: a security hold asks "is it safe to run this contributor's code at all?", while a context approval hold asks "should this job be promoted?". The two never cross — releasing a security hold needs `ci_trust:write` or higher, releasing a context approval hold needs `contexts:write` plus eligibility for one of the gate's clauses. See [Approval holds vs security holds](https://docs.kici.dev/architecture/approvals/#approval-holds-vs-security-holds) for the full comparison.
|
|
743
|
-
|
|
744
|
-
Held runs can be approved:
|
|
745
|
-
|
|
746
|
-
- Via the **dashboard** in Settings > CI trust > Approval queue
|
|
747
|
-
- Via a PR comment: `/kici approve` (commenter must have `ci_trust:write+`)
|
|
748
|
-
|
|
749
|
-
A hold raised by the org trust policy covers the whole PR and uses the org's approval expiry (default 72 hours). A `minimumTrust` hold is raised by a context rather than by the org policy, so it uses that context's own hold expiry (default one hour).
|
|
750
|
-
|
|
751
|
-
While the org trust policy is holding a pull request, your organization's global workflows do not run for it. Approving the hold releases that pull request's own workflows; it does not retroactively run the organization's global workflows for the event.
|
|
752
|
-
|
|
753
|
-
### Concurrency limits
|
|
754
|
-
|
|
755
|
-
Control how many jobs can run simultaneously in a context:
|
|
756
|
-
|
|
757
|
-
```
|
|
758
|
-
Concurrency limit: 1
|
|
759
|
-
Strategy: queue (or cancel-pending)
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
The concurrency limit is a positive integer; leave it unset for unlimited concurrency.
|
|
763
|
-
|
|
764
|
-
- **queue** -- new jobs wait in a FIFO queue (with configurable timeout, default 1 hour)
|
|
765
|
-
- **cancel-pending** -- pending (queued) jobs are cancelled when the limit is reached
|
|
766
|
-
|
|
767
|
-
## Dashboard management
|
|
768
|
-
|
|
769
|
-
### Creating contexts
|
|
770
|
-
|
|
771
|
-
Navigate to **Settings > Contexts** in the dashboard. Click **New context** to choose the context name and type (Fixed or Glob).
|
|
772
|
-
|
|
773
|
-
- **Fixed** -- applies to jobs that declare exactly this context name, like `staging` or `production`
|
|
774
|
-
- **Glob** -- applies to any context name a job declares that matches the pattern, e.g. `review/*` matches a job with `context: 'review/PR-123'`
|
|
775
|
-
|
|
776
|
-
The contexts list shows each context's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](https://docs.kici.dev/user/testing-guide/)), and whether it is enabled.
|
|
777
|
-
|
|
778
|
-
### Context detail page
|
|
779
|
-
|
|
780
|
-
Each context has four tabs:
|
|
781
|
-
|
|
782
|
-
1. **Variables** -- manage key-value pairs with lock toggles. Locked variables cannot be overridden by source-level overrides. Source overrides are managed in a sub-tab.
|
|
783
|
-
|
|
784
|
-
2. **Secrets** -- view bound secret scopes and their resolved secret count. Add bindings by specifying scope glob patterns (e.g., `aws/prod/**`).
|
|
785
|
-
|
|
786
|
-
3. **Protection** -- configure branch restrictions, required reviewers, wait timers, and concurrency limits with enable toggles for each section. Turning a section's toggle off and saving clears that rule on the context, so the gate stops applying to new runs. Emptying the hold expiry field clears it too, and held runs fall back to the default one-hour hold window.
|
|
787
|
-
|
|
788
|
-
4. **History** -- view filtered runs targeting this context.
|
|
789
|
-
|
|
790
|
-
### Bound contexts on runs
|
|
791
|
-
|
|
792
|
-
A job's bound deployment contexts are shown as chips on the run detail page (in the job metadata panel) in the order the job declared them, and the distinct set across a run's jobs appears as compact chips on the run list. For a multi-context job the chips read left-to-right in merge order — later contexts override earlier ones on key collisions. A `(dynamic)` chip marks a context whose name is computed at runtime; it resolves to the real name once the run starts. A job that binds a single context shows one chip; a job that binds none shows no chip.
|
|
793
|
-
|
|
794
|
-
If a multi-context binding is gated out, the run's failure banner names which context and which rule rejected it (the same all-must-pass detail surfaced by `kici runs show <run-id>`).
|
|
795
|
-
|
|
796
|
-
### Secrets management
|
|
797
|
-
|
|
798
|
-
Secrets are individual encrypted values organized by scope paths (e.g., `aws/prod`, `databases/postgres`). Scopes are bound to contexts via bindings:
|
|
799
|
-
|
|
800
|
-
- **Scope-centric view** (Secrets page): tree view of scopes with per-scope context binding checkboxes
|
|
801
|
-
- **Context-centric view** (inside context detail): bound scopes, resolved secrets, add binding
|
|
802
|
-
|
|
803
|
-
When scope paths collide on the same key name, the longer (more specific) path wins.
|
|
804
|
-
|
|
805
|
-
## Type generation
|
|
806
|
-
|
|
807
|
-
Running `kici types` generates two augmented interfaces: `KnownSecretKeys` (union of all secret keys across all contexts) and `ContextSecrets` (per-context key unions):
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
interface KnownSecretKeys {
|
|
811
|
-
DB_PASSWORD: string;
|
|
812
|
-
API_KEY: string;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
interface ContextSecrets {
|
|
816
|
-
production: 'DB_PASSWORD' | 'API_KEY';
|
|
817
|
-
staging: 'DB_PASSWORD';
|
|
818
|
-
}
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
`KnownSecretKeys` narrows `ctx.secrets.get()` and `ctx.secrets.expose()` key parameters to valid key names. `ContextSecrets` maps each context to its available secret key names as a string union. Dynamic contexts fall back to the full `KnownSecretKeys` union.
|
|
822
|
-
|
|
823
|
-
---
|
|
824
|
-
|
|
825
|
-
## Dashboard
|
|
826
|
-
|
|
827
|
-
Source: https://docs.kici.dev/user/dashboard/
|
|
828
|
-
|
|
829
|
-
The KiCI dashboard is the browser interface for monitoring workflow runs, inspecting jobs and logs, and managing your organization. It signs in via OIDC and talks to KiCI over its API.
|
|
830
|
-
|
|
831
|
-
This guide is split across the following pages:
|
|
832
|
-
|
|
833
|
-
| Page | Covers |
|
|
834
|
-
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
835
|
-
| [Getting started](https://docs.kici.dev/user/dashboard/getting-started/) | Onboarding checklist and your organizations list |
|
|
836
|
-
| [Navigation and layout](https://docs.kici.dev/user/dashboard/navigation/) | Sidebar, mobile nav, theme, time display, shortcuts, error pages |
|
|
837
|
-
| [Runs and logs](https://docs.kici.dev/user/dashboard/runs/) | Run list, run detail, the log viewer |
|
|
838
|
-
| [Settings](https://docs.kici.dev/user/dashboard/settings/) | Members, roles, teams, keys, sources, billing, security, support access |
|
|
839
|
-
| [Workflows, diagnostics, and orchestrators](https://docs.kici.dev/user/dashboard/workflows-and-diagnostics/) | Registered workflows, infra health, per-cluster views |
|
|
840
|
-
| [Contexts, secrets, and approvals](https://docs.kici.dev/user/dashboard/contexts-and-secrets/) | Contexts, secret scopes, approval queue |
|
|
841
|
-
| [Activity and DLQ](https://docs.kici.dev/user/dashboard/activity-and-dlq/) | Forensic activity log and dead-letter queue |
|
|
842
|
-
| [Notifications](https://docs.kici.dev/user/dashboard/notifications/) | Personal run notifications and org-wide channels, subscriptions, delivery log |
|
|
843
|
-
| [Account](https://docs.kici.dev/user/dashboard/account/) | Personal account settings |
|
|
844
|
-
|
|
845
|
-
---
|
|
846
|
-
|
|
847
|
-
## Dynamic values
|
|
848
|
-
|
|
849
|
-
Source: https://docs.kici.dev/user/dynamic-values/
|
|
850
|
-
|
|
851
|
-
Dynamic values let you compute `context`, `env`, and `concurrencyGroup` at runtime based on the incoming event. Instead of hardcoding static strings, you pass a function that receives the normalized event envelope and returns the resolved value.
|
|
852
|
-
|
|
853
|
-
```typescript
|
|
854
|
-
job('deploy', {
|
|
855
|
-
runsOn: ['default'],
|
|
856
|
-
context: (event) => event.targetBranch,
|
|
857
|
-
env: (event) => ({ BRANCH: event.targetBranch }),
|
|
858
|
-
concurrencyGroup: (event) => `deploy-${event.targetBranch}`,
|
|
859
|
-
steps: [/* ... */],
|
|
860
|
-
});
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
```typescript
|
|
864
|
-
job('deploy', {
|
|
865
|
-
runsOn: 'default',
|
|
866
|
-
// One shape everywhere: branch on the normalized event type.
|
|
867
|
-
context: (event) => (event.type === 'pull_request' ? 'preview' : 'production'),
|
|
868
|
-
steps: [/* ... */],
|
|
869
|
-
});
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
## How it works
|
|
873
|
-
|
|
874
|
-
When you define a dynamic value as a function, it is resolved on the eval agent as a short **init** step that runs before the job:
|
|
875
|
-
|
|
876
|
-
1. The orchestrator dispatches a lightweight `__init__` job to an agent.
|
|
877
|
-
2. The agent loads the compiled workflow bundle and calls your function with the normalized event.
|
|
878
|
-
3. The agent reports the resolved values back to the orchestrator, which dispatches the real execution job with them applied.
|
|
879
|
-
|
|
880
|
-
This resolution appears in the run timeline as an `Init:` entry. The orchestrator never evaluates workflow code — every dynamic `context`, `env`, and `concurrencyGroup` function runs agent-side, whatever it references.
|
|
881
|
-
|
|
882
|
-
`kici preview` lists the injected `__init__` job under each affected job, so you can spot it before the first run.
|
|
883
|
-
|
|
884
|
-
**Examples:**
|
|
885
|
-
|
|
886
|
-
```typescript
|
|
887
|
-
// Simple branch extraction
|
|
888
|
-
context: (event) => event.targetBranch;
|
|
889
|
-
|
|
890
|
-
// Object literal with string operations
|
|
891
|
-
env: (event) => ({ BRANCH: event.targetBranch });
|
|
892
|
-
|
|
893
|
-
// Concatenation with event data
|
|
894
|
-
concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
|
|
895
|
-
|
|
896
|
-
// Local variables and safe globals
|
|
897
|
-
context: (event) => {
|
|
898
|
-
const parts = event.targetBranch.split('/');
|
|
899
|
-
return parts[parts.length - 1];
|
|
900
|
-
};
|
|
901
|
-
|
|
902
|
-
// Async lookups, module access, and process/global reads all work
|
|
903
|
-
context: async (event) => await lookupEnv(event.targetBranch);
|
|
904
|
-
env: (event) => ({ DEFAULT_ENV: process.env.DEFAULT_ENV ?? 'staging' });
|
|
905
|
-
```
|
|
906
|
-
|
|
907
|
-
## Performance
|
|
908
|
-
|
|
909
|
-
| Value | Overhead | Example |
|
|
910
|
-
| ------------------------ | --------- | ---------------------------------------- |
|
|
911
|
-
| Static value | None | `context: 'staging'` |
|
|
912
|
-
| Dynamic value (function) | Init step | `context: (event) => event.targetBranch` |
|
|
913
|
-
|
|
914
|
-
A static value is baked into the lock file and needs no init step. A dynamic value always resolves through the agent's init step, so reach for a function only when the value genuinely depends on the event.
|
|
915
|
-
|
|
916
|
-
## Tips
|
|
917
|
-
|
|
918
|
-
- **Prefer static values when you can.** Most context and env values are the same on every event; only make them dynamic when they truly depend on the event payload.
|
|
919
|
-
- **Run `kici preview`** to see the injected `__init__` job listed under each affected job before your first run.
|
|
920
|
-
- **A runtime error in a dynamic function fails the job.** If your function throws when the init step runs it (e.g., accessing a property on `undefined`), the job fails immediately.
|
|
921
|
-
- **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where dynamic values run relative to rules, hooks, and step bodies.
|
|
922
|
-
- **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
|
|
923
|
-
|
|
924
|
-
---
|
|
925
|
-
|
|
926
|
-
## Environment variables
|
|
927
|
-
|
|
928
|
-
Source: https://docs.kici.dev/user/env-vars/
|
|
929
|
-
|
|
930
|
-
The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`) defaults `KICI_PLATFORM_URL`, `KICI_OIDC_ISSUER`, and `KICI_OIDC_CLIENT_ID` to the hosted KiCI Platform, so `kici login` works with no configuration. Set them only to target another KiCI environment (e.g. a testing instance) or a custom OIDC provider.
|
|
931
|
-
|
|
932
|
-
## Authentication
|
|
933
|
-
|
|
934
|
-
| Variable | Description | Default |
|
|
935
|
-
| --------------------- | -------------------------------------- | -------------------------------------------- |
|
|
936
|
-
| `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication | `https://auth.kici.dev/realms/kici-internal` |
|
|
937
|
-
| `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application | `kici-cli` |
|
|
938
|
-
| `KICI_PLATFORM_URL` | Platform API base URL | `https://api.kici.dev` |
|
|
939
|
-
| `KICI_CONFIG_DIR` | Override the KiCI config directory | `~/.kici` |
|
|
940
|
-
|
|
941
|
-
## Browser behavior
|
|
942
|
-
|
|
943
|
-
| Variable | Description | Default |
|
|
944
|
-
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
945
|
-
| `KICI_BROWSER_CMD` | Custom browser command for OAuth login. Supports `{url}` placeholder. Set to `none` to suppress browser opening and print the URL to stdout instead. | Uses the system default browser |
|
|
946
|
-
| `KICI_CALLBACK_PORT` | Fixed port for the OAuth PKCE callback server. Useful when firewall rules require a known port. | Random available port |
|
|
947
|
-
|
|
948
|
-
## Development
|
|
949
|
-
|
|
950
|
-
| Variable | Description | Default |
|
|
951
|
-
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
|
952
|
-
| `KICI_DEV` | Enable development mode. When `true`, uses prerelease-compatible version ranges (`>=0.0.1-0`) for dev dependencies and skips npm version resolution. | unset |
|
|
953
|
-
| `KICI_DEBUG` | Enable debug logging. When `true`, prints verbose diagnostics (SDK alias resolution, step-level debug logs, stack traces on errors). Equivalent to the `--debug` CLI flag. | unset |
|
|
954
|
-
|
|
955
|
-
## Local dev plane
|
|
956
|
-
|
|
957
|
-
Read by the [local dev plane](https://docs.kici.dev/user/cli/authoring-and-local/#kici-local) that `kici run <event> --local` dispatches through.
|
|
958
|
-
|
|
959
|
-
| Variable | Description | Default |
|
|
960
|
-
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
|
961
|
-
| `KICI_LOCAL_ORCH_PORT` | Port the plane orchestrator listens on (HTTP + WebSocket). Change it when another process already holds the default. | `4319` |
|
|
962
|
-
| `KICI_LOCAL_PG_PORT` | Port the plane's PostgreSQL listens on. | `45432` |
|
|
963
|
-
| `KICI_LOCAL_PG_MODE` | Set to `podman` to force the container PostgreSQL fallback instead of the embedded binary. | Embedded PostgreSQL |
|
|
964
|
-
| `KICI_LOCAL_ACCEPTANCE_TIMEOUT_MS` | How long a local run waits for an agent to claim its first job before failing fast. Raise it on a slow host; the run still fails quickly when no scaler label set matches `runsOn`. | `120000` (2 minutes) |
|
|
965
|
-
|
|
966
|
-
## CI detection
|
|
967
|
-
|
|
968
|
-
The CLI also reads the conventional CI markers your CI provider sets. They are not KiCI variables — KiCI only consumes them.
|
|
969
|
-
|
|
970
|
-
| Variable | Description | Default |
|
|
971
|
-
| ---------------- | -------------------------------------------- | ------- |
|
|
972
|
-
| `CI` | Generic CI marker. Set by most CI providers. | unset |
|
|
973
|
-
| `GITHUB_ACTIONS` | Set to `true` by GitHub Actions. | unset |
|
|
974
|
-
| `GITLAB_CI` | Set to `true` by GitLab CI/CD. | unset |
|
|
975
|
-
|
|
976
|
-
### How `CI` is interpreted
|
|
977
|
-
|
|
978
|
-
`kici` treats the environment as CI when `CI`, `GITHUB_ACTIONS`, or `GITLAB_CI` is set to any value other than an explicit opt-out. `0` and `false` are the opt-outs, compared case-insensitively, so `CI=0`, `CI=false`, and `CI=False` all mean "not CI". Surrounding whitespace is ignored, and a value that is empty or only whitespace (`CI=`) is treated as unset.
|
|
979
|
-
|
|
980
|
-
A vendor marker outranks the generic opt-out: `CI=false GITHUB_ACTIONS=true` is still CI, because an explicit vendor marker names a real runner rather than a preference.
|
|
981
|
-
|
|
982
|
-
This affects which login flow `kici login` chooses (browser vs device) and whether interactive commands such as `kici init` prompt.
|
|
983
|
-
|
|
984
|
-
## Usage examples
|
|
985
|
-
|
|
986
|
-
### CI/CD environment
|
|
987
|
-
|
|
988
|
-
Authenticate with a pre-existing API key (no browser needed):
|
|
278
|
+
To auto-approve **every gate of the run you just dispatched**, pass `--approve-all` (alias `--yes`):
|
|
989
279
|
|
|
990
280
|
```bash
|
|
991
|
-
kici
|
|
992
|
-
```
|
|
993
|
-
|
|
994
|
-
### Targeting another environment or custom OIDC provider
|
|
995
|
-
|
|
996
|
-
`kici login` targets the hosted KiCI Platform by default. To point the CLI at another KiCI environment (e.g. a testing instance) or a custom OIDC provider, override the defaults:
|
|
997
|
-
|
|
998
|
-
```bash
|
|
999
|
-
export KICI_OIDC_ISSUER=https://your-idp.example.com
|
|
1000
|
-
export KICI_OIDC_CLIENT_ID=your-client-id
|
|
1001
|
-
export KICI_PLATFORM_URL=https://your-platform.example.com
|
|
1002
|
-
kici login
|
|
1003
|
-
```
|
|
1004
|
-
|
|
1005
|
-
### Headless SSH session
|
|
1006
|
-
|
|
1007
|
-
The CLI auto-detects headless environments and uses the device flow. To force PKCE with URL output instead:
|
|
1008
|
-
|
|
1009
|
-
```bash
|
|
1010
|
-
export KICI_BROWSER_CMD=none
|
|
1011
|
-
kici login
|
|
1012
|
-
```
|
|
1013
|
-
|
|
1014
|
-
This prints the authorization URL to stdout as `KICI_AUTH_URL=<url>`. Open the URL in any browser to complete authentication.
|
|
1015
|
-
|
|
1016
|
-
### Fixed callback port
|
|
1017
|
-
|
|
1018
|
-
When behind a firewall or using port forwarding:
|
|
1019
|
-
|
|
1020
|
-
```bash
|
|
1021
|
-
export KICI_CALLBACK_PORT=19876
|
|
1022
|
-
kici login
|
|
1023
|
-
```
|
|
1024
|
-
|
|
1025
|
-
### Custom config location
|
|
1026
|
-
|
|
1027
|
-
Store the KiCI config in a non-default location:
|
|
1028
|
-
|
|
1029
|
-
```bash
|
|
1030
|
-
export KICI_CONFIG_DIR=/tmp/kici-test
|
|
1031
|
-
kici login
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
---
|
|
1035
|
-
|
|
1036
|
-
## Event system
|
|
1037
|
-
|
|
1038
|
-
Source: https://docs.kici.dev/user/events/
|
|
1039
|
-
|
|
1040
|
-
KiCI supports two broad categories of workflow triggers: **git-based triggers** that work immediately, and **event-based triggers** that use a registration model. Understanding this distinction is key to working effectively with non-git triggers like schedules, custom events, and generic webhooks.
|
|
1041
|
-
|
|
1042
|
-
## Overview
|
|
1043
|
-
|
|
1044
|
-
Git-based triggers (`push()`, `pr()`, `tag()`, `comment()`, `review()`, `release()`, etc.) work immediately after you commit your lock file. When a GitHub webhook arrives, the orchestrator fetches your lock file and evaluates triggers on the spot -- no advance setup needed.
|
|
1045
|
-
|
|
1046
|
-
Event-based triggers work differently. The orchestrator needs to know about them _before_ the event arrives. This is because event-based triggers are matched against a pre-built registration index rather than being evaluated per-event from a lock file fetch. The six event-based trigger types are:
|
|
1047
|
-
|
|
1048
|
-
- `kiciEvent()` -- custom events emitted from workflow steps
|
|
1049
|
-
- `workflowComplete()` -- fires when a workflow finishes
|
|
1050
|
-
- `jobComplete()` -- fires when a specific job finishes
|
|
1051
|
-
- `genericWebhook()` -- HTTP webhooks from external services
|
|
1052
|
-
- `schedule()` -- cron-based time triggers
|
|
1053
|
-
- `lifecycle()` -- orchestrator lifecycle events (workflow completion, job failure, registration updates)
|
|
1054
|
-
|
|
1055
|
-
All six require the **registration model** to function -- covered in detail below.
|
|
1056
|
-
|
|
1057
|
-
## Event types
|
|
1058
|
-
|
|
1059
|
-
### Custom events
|
|
1060
|
-
|
|
1061
|
-
Custom events are user-defined events emitted from workflow steps using `ctx.emit()`. Use `kiciEvent()` to listen for them.
|
|
1062
|
-
|
|
1063
|
-
```typescript
|
|
1064
|
-
import { kiciEvent } from '@kici-dev/sdk';
|
|
1065
|
-
|
|
1066
|
-
// Listen for a custom event by name
|
|
1067
|
-
kiciEvent({ name: 'deploy-complete' });
|
|
1068
|
-
|
|
1069
|
-
// With payload matching (JSONPath)
|
|
1070
|
-
kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } });
|
|
1071
|
-
|
|
1072
|
-
// With negative filter
|
|
1073
|
-
kiciEvent({ name: 'deploy-complete', not: { '$.env': 'staging' } });
|
|
1074
|
-
|
|
1075
|
-
// From a specific repository
|
|
1076
|
-
kiciEvent({ name: 'deploy-complete', source: 'org/infra-repo' });
|
|
1077
|
-
```
|
|
1078
|
-
|
|
1079
|
-
**Config options:** `name` (required), `match`, `not`, `source`, `description`.
|
|
1080
|
-
|
|
1081
|
-
### System events
|
|
1082
|
-
|
|
1083
|
-
The orchestrator automatically emits completion events when workflows and jobs finish. No manual emission needed -- these fire automatically.
|
|
1084
|
-
|
|
1085
|
-
**Workflow completion:**
|
|
1086
|
-
|
|
1087
|
-
```typescript
|
|
1088
|
-
import { workflowComplete } from '@kici-dev/sdk';
|
|
1089
|
-
|
|
1090
|
-
// Any workflow completion
|
|
1091
|
-
workflowComplete();
|
|
1092
|
-
|
|
1093
|
-
// Specific workflow by name
|
|
1094
|
-
workflowComplete({ name: 'build' });
|
|
1095
|
-
|
|
1096
|
-
// Only successful completions
|
|
1097
|
-
workflowComplete({ name: 'build', status: ['success'] });
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
**Config options:** `name`, `status` (`'success'`, `'failed'`, `'cancelled'`), `source`, `description`.
|
|
1101
|
-
|
|
1102
|
-
**Job completion:**
|
|
1103
|
-
|
|
1104
|
-
```typescript
|
|
1105
|
-
import { jobComplete } from '@kici-dev/sdk';
|
|
1106
|
-
|
|
1107
|
-
// Any job completion
|
|
1108
|
-
jobComplete();
|
|
1109
|
-
|
|
1110
|
-
// Specific workflow + job
|
|
1111
|
-
jobComplete({ workflow: 'build', job: 'test' });
|
|
1112
|
-
|
|
1113
|
-
// Only failures
|
|
1114
|
-
jobComplete({ workflow: 'build', job: 'test', status: ['failed'] });
|
|
1115
|
-
```
|
|
1116
|
-
|
|
1117
|
-
**Config options:** `workflow`, `job`, `status` (`'success'`, `'failed'`, `'cancelled'`, `'skipped'`), `source`, `description`.
|
|
1118
|
-
|
|
1119
|
-
### External events
|
|
1120
|
-
|
|
1121
|
-
Generic webhooks let you trigger workflows from any HTTP service -- Stripe, ArgoCD, Slack, Grafana, or your own internal services.
|
|
1122
|
-
|
|
1123
|
-
```typescript
|
|
1124
|
-
import { genericWebhook } from '@kici-dev/sdk';
|
|
1125
|
-
|
|
1126
|
-
// Match any event from a source
|
|
1127
|
-
genericWebhook({ source: 'stripe' });
|
|
1128
|
-
|
|
1129
|
-
// Match specific event types
|
|
1130
|
-
genericWebhook({ source: 'stripe', events: ['invoice.paid'] });
|
|
1131
|
-
|
|
1132
|
-
// With HMAC-SHA256 signature verification
|
|
1133
|
-
genericWebhook({
|
|
1134
|
-
source: 'stripe',
|
|
1135
|
-
events: ['invoice.paid'],
|
|
1136
|
-
auth: {
|
|
1137
|
-
method: 'hmac-sha256',
|
|
1138
|
-
secret: 'stripe-signing-key',
|
|
1139
|
-
signatureHeader: 'stripe-signature',
|
|
1140
|
-
},
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
// With API key auth
|
|
1144
|
-
genericWebhook({
|
|
1145
|
-
source: 'slack',
|
|
1146
|
-
auth: { method: 'api-key', secret: 'slack-token' },
|
|
1147
|
-
});
|
|
1148
|
-
```
|
|
1149
|
-
|
|
1150
|
-
**Config options:** `source` (required), `events`, `match`, `not`, `auth`, `path`, `description`.
|
|
1151
|
-
|
|
1152
|
-
The `source` field MUST match the `--name` that an operator passed to `kici-admin source add generic --name <name>` when the source was created — that string is the source's identifier in the orchestrator. Generic webhook sources must be created by an operator before events can be received; see [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) for setup instructions.
|
|
1153
|
-
|
|
1154
|
-
### Schedule events
|
|
1155
|
-
|
|
1156
|
-
Cron-based triggers evaluated by the orchestrator on a periodic interval. Only the Raft leader evaluates schedules in a clustered deployment.
|
|
1157
|
-
|
|
1158
|
-
```typescript
|
|
1159
|
-
import { schedule } from '@kici-dev/sdk';
|
|
1160
|
-
|
|
1161
|
-
// Run every hour
|
|
1162
|
-
schedule({ cron: '0 * * * *' });
|
|
1163
|
-
|
|
1164
|
-
// Run daily at 2 AM UTC
|
|
1165
|
-
schedule({ cron: '0 2 * * *' });
|
|
1166
|
-
|
|
1167
|
-
// Run weekly on Mondays at 9 AM Eastern
|
|
1168
|
-
schedule({ cron: '0 9 * * 1', timezone: 'America/New_York' });
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
**Config options:** `cron` (required), `timezone` (defaults to `'UTC'`), `description`.
|
|
1172
|
-
|
|
1173
|
-
### Lifecycle events
|
|
1174
|
-
|
|
1175
|
-
Lifecycle triggers listen for orchestrator-level events related to workflow execution and system state changes.
|
|
1176
|
-
|
|
1177
|
-
```typescript
|
|
1178
|
-
import { lifecycle } from '@kici-dev/sdk';
|
|
1179
|
-
|
|
1180
|
-
// Trigger when any workflow completes
|
|
1181
|
-
lifecycle({ events: ['workflow_complete'] });
|
|
1182
|
-
|
|
1183
|
-
// Trigger on job failures from a specific repo
|
|
1184
|
-
lifecycle({ events: ['job_failed'], sources: ['org/deploy-repo'] });
|
|
1185
|
-
|
|
1186
|
-
// Trigger when registrations are updated
|
|
1187
|
-
lifecycle({ events: ['registration_updated'] });
|
|
1188
|
-
```
|
|
1189
|
-
|
|
1190
|
-
**Available events:** `'workflow_complete'`, `'job_complete'`, `'job_failed'`, `'registration_updated'`.
|
|
1191
|
-
|
|
1192
|
-
**Config options:** `events` (required), `sources`, `description`.
|
|
1193
|
-
|
|
1194
|
-
## The registration model
|
|
1195
|
-
|
|
1196
|
-
This is the most important concept for understanding event-based triggers.
|
|
1197
|
-
|
|
1198
|
-
### Why registrations exist
|
|
1199
|
-
|
|
1200
|
-
When a GitHub webhook arrives (push, PR, etc.), the orchestrator fetches your lock file from the repository and evaluates triggers on the spot. This works because the event itself tells the orchestrator _which repository_ to look at.
|
|
1201
|
-
|
|
1202
|
-
Event-based triggers are different. When a cron timer fires or a custom event is emitted, there is no incoming webhook pointing to a specific repository. The orchestrator needs to know _in advance_ which workflows care about which events. That is what the registration model provides: a pre-built index of event-based workflows.
|
|
1203
|
-
|
|
1204
|
-
### How registration works
|
|
1205
|
-
|
|
1206
|
-
1. You define a workflow with an event-based trigger (e.g., `schedule()`, `kiciEvent()`, `genericWebhook()`)
|
|
1207
|
-
2. You compile the workflow (`kici compile`), which produces a lock file
|
|
1208
|
-
3. You push the lock file to your repository's **default branch** (e.g., `main` or `master`)
|
|
1209
|
-
4. The orchestrator receives the push webhook, detects it targets the default branch, and extracts all workflows with event-based triggers from the lock file
|
|
1210
|
-
5. Those workflows are stored in the orchestrator's registration database
|
|
1211
|
-
6. From that point on, matching events will trigger those workflows
|
|
1212
|
-
|
|
1213
|
-
### Key implications
|
|
1214
|
-
|
|
1215
|
-
- **Event-based workflows do not trigger until you push to the default branch.** If you add a new `schedule()` workflow, it will not start running until you merge to your default branch. This is by design -- the orchestrator cannot match events to workflows it does not know about.
|
|
1216
|
-
|
|
1217
|
-
- **Registration is automatic.** There is no manual setup. Push your code, and the orchestrator handles the rest.
|
|
1218
|
-
|
|
1219
|
-
- **Registrations refresh on every default-branch push.** If you add, remove, or modify event-based workflows and push to the default branch, the orchestrator updates its registration index automatically. Removed workflows stop triggering. New workflows start triggering.
|
|
1220
|
-
|
|
1221
|
-
- **Git-based triggers are unaffected.** Triggers like `push()`, `pr()`, and `tag()` do not use registrations. They work immediately from any branch because the orchestrator evaluates them per-event from the lock file.
|
|
1222
|
-
|
|
1223
|
-
### Practical example
|
|
1224
|
-
|
|
1225
|
-
You create a nightly build workflow:
|
|
1226
|
-
|
|
1227
|
-
```typescript
|
|
1228
|
-
import { workflow, job, step, schedule } from '@kici-dev/sdk';
|
|
1229
|
-
|
|
1230
|
-
export default workflow('nightly-build', {
|
|
1231
|
-
on: schedule({ cron: '0 2 * * *' }),
|
|
1232
|
-
jobs: [
|
|
1233
|
-
job('build', {
|
|
1234
|
-
runsOn: 'linux',
|
|
1235
|
-
steps: [
|
|
1236
|
-
step('build', async ({ $ }) => {
|
|
1237
|
-
await $`pnpm build`;
|
|
1238
|
-
}),
|
|
1239
|
-
],
|
|
1240
|
-
}),
|
|
1241
|
-
],
|
|
1242
|
-
});
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
You compile it, commit the lock file, and push to a feature branch. **Nothing happens** -- the cron will not fire because the orchestrator has not registered this workflow yet.
|
|
1246
|
-
|
|
1247
|
-
You merge the feature branch into `main`. On the merge push, the orchestrator extracts the `nightly-build` workflow (it has a `ScheduleTrigger`) and registers it. Starting at the next 2 AM UTC, the workflow will trigger.
|
|
1248
|
-
|
|
1249
|
-
## How events are matched
|
|
1250
|
-
|
|
1251
|
-
When an event arrives, the orchestrator follows this flow:
|
|
1252
|
-
|
|
1253
|
-
1. **Event received** -- a custom event is emitted by a step, a cron timer fires, or a generic webhook arrives
|
|
1254
|
-
2. **Registration lookup** -- the orchestrator queries its registration index for workflows matching the event type (e.g., all workflows with `ScheduleTrigger` for a cron fire, or all workflows with `KiciEventTrigger` for a custom event)
|
|
1255
|
-
3. **Trigger evaluation** -- for each candidate workflow, the orchestrator evaluates the trigger conditions: event name patterns, payload matching, status filters, source filters
|
|
1256
|
-
4. **Dispatch** -- matched workflows are dispatched to agents for execution, following the same job queue and agent routing as git-triggered workflows
|
|
1257
|
-
|
|
1258
|
-
This lookup is fast because the registration index is held in memory and refreshed only when the registry version changes (on default-branch pushes).
|
|
1259
|
-
|
|
1260
|
-
### Cross-source webhook delivery
|
|
1261
|
-
|
|
1262
|
-
The catch-all `webhook()` trigger (see [SDK reference: webhook()](https://docs.kici.dev/user/sdk/triggers/#webhook)) participates in this same registration lookup, but with one twist: it fires for matching events arriving via **any** inbound webhook source in the same org, not just the source the workflow's repo is bound to. The orchestrator maintains a `(customerId, eventName)` index over webhook trigger registrations and consults it on every inbound generic webhook.
|
|
1263
|
-
|
|
1264
|
-
The lookup is structurally org-isolated — a generic webhook delivered to org A can never reach a workflow registered against org B, because foreign-org rows live in a different bucket of the index. When a webhook fires across sources, the runtime clone token, repo URL, and check-status posting all come from the **registration's** source bundle, not the inbound source. The inbound source contributes only the event payload.
|
|
1265
|
-
|
|
1266
|
-
## Circuit breaker
|
|
1267
|
-
|
|
1268
|
-
Events can trigger workflows that emit more events, creating chains. The circuit breaker prevents runaway event storms.
|
|
1269
|
-
|
|
1270
|
-
### Chain depth limit
|
|
1271
|
-
|
|
1272
|
-
Each event carries a `chainDepth` counter. When a workflow triggered by an event emits a new event, the new event's chain depth increments. The orchestrator rejects events that exceed the maximum chain depth.
|
|
1273
|
-
|
|
1274
|
-
- **Default limit:** 10 levels deep
|
|
1275
|
-
- **What happens when hit:** the event is dropped and logged. It is not queued for later delivery.
|
|
1276
|
-
|
|
1277
|
-
For example: Workflow A emits event X (depth 0) -> Workflow B triggers, emits event Y (depth 1) -> ... -> at depth 10, any further emitted events are dropped.
|
|
1278
|
-
|
|
1279
|
-
### Rate limiting
|
|
1280
|
-
|
|
1281
|
-
Each workflow is rate-limited on how many events it can process per minute, using a sliding window.
|
|
1282
|
-
|
|
1283
|
-
- **Default limit:** 100 events per workflow per minute
|
|
1284
|
-
- **What happens when hit:** additional events for that workflow are dropped and logged until the window clears.
|
|
1285
|
-
|
|
1286
|
-
These defaults are hardcoded in the orchestrator and are not currently configurable via environment variables.
|
|
1287
|
-
|
|
1288
|
-
## Delivery guarantees
|
|
1289
|
-
|
|
1290
|
-
KiCI's event router delivers every accepted event with **at-least-once** semantics:
|
|
1291
|
-
|
|
1292
|
-
- An event that passes the circuit breaker (chain depth + rate limit) and commits
|
|
1293
|
-
to the `kici_events` table is guaranteed to dispatch to all matching workflows
|
|
1294
|
-
at least once.
|
|
1295
|
-
- Each dispatch attempt acquires a short-lived lease (default 60 s) on the row.
|
|
1296
|
-
If the dispatching node crashes or the handler throws, the lease expires (or
|
|
1297
|
-
is released on failure) and the event is automatically retried.
|
|
1298
|
-
- The retry policy is exponential backoff with full jitter: base 5 s, cap 5 min,
|
|
1299
|
-
up to 5 attempts before the event lands in the **DLQ** (dead-letter queue).
|
|
1300
|
-
Operators triage DLQ entries via `kici-admin event-dlq list / count / retry / discard`.
|
|
1301
|
-
|
|
1302
|
-
**What this means for workflow authors:**
|
|
1303
|
-
|
|
1304
|
-
- **Make event handlers idempotent.** A retried dispatch may run a handler more
|
|
1305
|
-
than once (e.g. if the first attempt threw after a partial side-effect).
|
|
1306
|
-
Workflows that mutate external state should use idempotency keys, conditional
|
|
1307
|
-
writes, or other deduplication patterns — same advice as for any distributed
|
|
1308
|
-
CI system.
|
|
1309
|
-
- **Schedule fires are at-least-once too.** A cron schedule that fires while a
|
|
1310
|
-
leader is being killed will commit (atomically with `cron_last_fired`) or roll
|
|
1311
|
-
back together — never half. Recovery on the new leader does not backfill
|
|
1312
|
-
multiple missed instants; if your workflow needs at-least-N guarantees across
|
|
1313
|
-
outages, drive it from a different mechanism (e.g. a workflow that runs more
|
|
1314
|
-
frequently and emits its own custom event).
|
|
1315
|
-
- **Drops are still possible — and visible.** Events rejected by the circuit
|
|
1316
|
-
breaker (chain depth or rate limit exceeded) are dropped and logged, not
|
|
1317
|
-
retried. That's a deliberate safety mechanism; the metric to watch is
|
|
1318
|
-
`kici_orch_events_dropped_total{reason}`.
|
|
1319
|
-
|
|
1320
|
-
## Emitting custom events
|
|
1321
|
-
|
|
1322
|
-
Custom events are emitted from workflow steps using `ctx.emit()`. You can optionally define typed event schemas using `defineEvent()`.
|
|
1323
|
-
|
|
1324
|
-
### Basic emission
|
|
1325
|
-
|
|
1326
|
-
```typescript
|
|
1327
|
-
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
1328
|
-
|
|
1329
|
-
export default workflow('build', {
|
|
1330
|
-
on: push({ branches: 'main' }),
|
|
1331
|
-
jobs: [
|
|
1332
|
-
job('build', {
|
|
1333
|
-
runsOn: 'linux',
|
|
1334
|
-
steps: [
|
|
1335
|
-
step('build', async ({ $ }) => {
|
|
1336
|
-
await $`pnpm build`;
|
|
1337
|
-
}),
|
|
1338
|
-
step('notify', async (ctx) => {
|
|
1339
|
-
await ctx.emit('build-complete', {
|
|
1340
|
-
version: '1.0.0',
|
|
1341
|
-
success: true,
|
|
1342
|
-
});
|
|
1343
|
-
}),
|
|
1344
|
-
],
|
|
1345
|
-
}),
|
|
1346
|
-
],
|
|
1347
|
-
});
|
|
1348
|
-
```
|
|
1349
|
-
|
|
1350
|
-
### Typed event definitions
|
|
1351
|
-
|
|
1352
|
-
Use `defineEvent()` with Zod schemas to create a typed contract for event payloads:
|
|
1353
|
-
|
|
1354
|
-
```typescript
|
|
1355
|
-
import { defineEvent, z } from '@kici-dev/sdk';
|
|
1356
|
-
|
|
1357
|
-
export const deployComplete = defineEvent(
|
|
1358
|
-
'deploy-complete',
|
|
1359
|
-
z.object({
|
|
1360
|
-
env: z.string(),
|
|
1361
|
-
version: z.string(),
|
|
1362
|
-
services: z.array(z.string()),
|
|
1363
|
-
}),
|
|
1364
|
-
);
|
|
1365
|
-
```
|
|
1366
|
-
|
|
1367
|
-
Then emit using the definition — the payload is checked against the schema:
|
|
1368
|
-
|
|
1369
|
-
```typescript
|
|
1370
|
-
step('emit', async (ctx) => {
|
|
1371
|
-
await ctx.emit(deployComplete, {
|
|
1372
|
-
env: 'prod',
|
|
1373
|
-
version: '1.2.3',
|
|
1374
|
-
services: ['api', 'web'],
|
|
1375
|
-
});
|
|
1376
|
-
});
|
|
1377
|
-
```
|
|
1378
|
-
|
|
1379
|
-
And consume in another workflow:
|
|
1380
|
-
|
|
1381
|
-
```typescript
|
|
1382
|
-
import { workflow, job, step, kiciEvent } from '@kici-dev/sdk';
|
|
1383
|
-
|
|
1384
|
-
export default workflow('post-deploy', {
|
|
1385
|
-
on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
|
|
1386
|
-
jobs: [
|
|
1387
|
-
job('smoke-test', {
|
|
1388
|
-
runsOn: 'linux',
|
|
1389
|
-
steps: [
|
|
1390
|
-
step('test', async ({ $ }) => {
|
|
1391
|
-
await $`./scripts/smoke-test.sh`;
|
|
1392
|
-
}),
|
|
1393
|
-
],
|
|
1394
|
-
}),
|
|
1395
|
-
],
|
|
1396
|
-
});
|
|
1397
|
-
```
|
|
1398
|
-
|
|
1399
|
-
Custom events are delivered immediately when emitted (mid-workflow, not queued until workflow completion). See the [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) section for the full `ctx.emit()` API.
|
|
1400
|
-
|
|
1401
|
-
## See also
|
|
1402
|
-
|
|
1403
|
-
- [SDK reference: event triggers](https://docs.kici.dev/user/sdk/triggers/#event-triggers) -- complete API signatures for all trigger builders
|
|
1404
|
-
- [SDK reference: emitting events](https://docs.kici.dev/user/sdk/validation-events/#emitting-events) -- `ctx.emit()` and `defineEvent()` API
|
|
1405
|
-
- [Workflow patterns: workflow chaining](https://docs.kici.dev/user/patterns/integrations/#workflow-chaining) -- examples of event-driven workflow chains
|
|
1406
|
-
- [Operator guide: event routing](https://docs.kici.dev/operator/event-routing/) -- configuring generic webhook sources, trust relationships, and event routing
|
|
1407
|
-
- [Architecture: event system](https://docs.kici.dev/architecture/webhooks/event-system/) -- internal event routing design, registration model, cluster synchronization
|
|
1408
|
-
|
|
1409
|
-
---
|
|
1410
|
-
|
|
1411
|
-
## Global workflows
|
|
1412
|
-
|
|
1413
|
-
Source: https://docs.kici.dev/user/global-workflows/
|
|
1414
|
-
|
|
1415
|
-
Global workflows let one **workflow repo** define jobs that run on events from many **source repos** in the same org. They're the answer to "I want one CI policy / release pipeline / security scan to fire on every repo without copy-pasting `.kici/` folders everywhere."
|
|
1416
|
-
|
|
1417
|
-
If you've only ever used per-repo workflows so far, start with the mental model section — global workflows add two new concepts (workflow repo vs. source repo, and authoring vs. source axes) that show up everywhere from SDK syntax to dashboard settings.
|
|
1418
|
-
|
|
1419
|
-
## Mental model
|
|
1420
|
-
|
|
1421
|
-
| Term | Meaning |
|
|
1422
|
-
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1423
|
-
| Workflow repo | The repo whose `.kici/workflows/*.ts` file **declares** the global workflow. Holds the steps. Also known as the _authoring_ repo. |
|
|
1424
|
-
| Source repo | The repo that **emits** the event (push / PR / tag / ...) that causes the global workflow to fire. The agent checks out this repo as the working copy. |
|
|
1425
|
-
| Global | A workflow whose trigger carries one or more `repos:` glob patterns. The presence of `repos:` is what classifies a workflow as global. |
|
|
1426
|
-
| Authoring axis | Policy that answers "which repos may **author** global workflows?" Controlled by the allow-list in the dashboard's _Allowed author repos_ setting. |
|
|
1427
|
-
| Source axis | Policy that answers "which **source** repos' events are allowed to trigger global workflows?" Controlled by the deny-list in _Blocked source repos_. |
|
|
281
|
+
kici run remote deploy-prod --approve-all
|
|
282
|
+
```
|
|
1428
283
|
|
|
1429
|
-
|
|
284
|
+
`--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`).
|
|
1430
285
|
|
|
1431
|
-
|
|
286
|
+
`--approve-all` is honored in non-interactive runs too: with `--json` or `--quiet` (no TTY), the flag still auto-approves each eligible gate as it appears instead of hanging or printing out-of-band instructions. Hold notices are routed to stderr so `--json` stdout stays pure machine-readable output.
|
|
1432
287
|
|
|
1433
|
-
|
|
288
|
+
You can also approve from the dashboard approval queue. See [Dashboard](https://docs.kici.dev/user/dashboard/contexts-and-secrets/#approval-queue).
|
|
1434
289
|
|
|
1435
|
-
|
|
1436
|
-
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
290
|
+
## See also
|
|
1437
291
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
}),
|
|
1444
|
-
],
|
|
1445
|
-
jobs: [
|
|
1446
|
-
job('lint', {
|
|
1447
|
-
steps: [
|
|
1448
|
-
step('lint-all', async ({ $, env }) => {
|
|
1449
|
-
await $`echo source=${env.KICI_SOURCE_REPO_PATH ?? 'unknown'}`;
|
|
1450
|
-
await $`npm run lint`;
|
|
1451
|
-
}),
|
|
1452
|
-
],
|
|
1453
|
-
}),
|
|
1454
|
-
],
|
|
1455
|
-
});
|
|
1456
|
-
```
|
|
292
|
+
- [Idempotent steps](https://docs.kici.dev/user/idempotent-steps/) — the check/apply step facet that drift gates build on.
|
|
293
|
+
- [Contexts](https://docs.kici.dev/user/contexts/) — operator-required reviewers on protected contexts.
|
|
294
|
+
- [Approval gates (operator guide)](https://docs.kici.dev/operator/approvals/) — teams, the approval queue, expiry, and self-approval.
|
|
295
|
+
- [Approval gates (architecture)](https://docs.kici.dev/architecture/approvals/) — the unified hold model and the step-level round-trip.
|
|
296
|
+
- [Organization-wide workflows](https://docs.kici.dev/user/global-workflows/) — why an approval gate is refused there.
|
|
1457
297
|
|
|
1458
|
-
|
|
298
|
+
---
|
|
1459
299
|
|
|
1460
|
-
|
|
300
|
+
## Contexts
|
|
1461
301
|
|
|
1462
|
-
|
|
302
|
+
Source: https://docs.kici.dev/user/contexts/
|
|
1463
303
|
|
|
1464
|
-
|
|
1465
|
-
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1466
|
-
| `KICI_SOURCE_REPO_PATH` | The **source** repo's working tree (the repo that emitted the event). This is the repo the job's `$` / `git` commands operate on by default. |
|
|
1467
|
-
| `KICI_WORKFLOW_REPO_PATH` | The **workflow** repo's working tree (the repo that authored the workflow). Useful for reading shared scripts or config from your CI repo. |
|
|
304
|
+
<!-- help:contexts-list#overview -->
|
|
1468
305
|
|
|
1469
|
-
|
|
306
|
+
Contexts are named deployment targets (like staging or production) that control where your workflow jobs run. Each context can have its own variables, secrets, and protection rules to gate deployments.
|
|
1470
307
|
|
|
1471
|
-
|
|
308
|
+
<!-- /help:contexts-list -->
|
|
1472
309
|
|
|
1473
|
-
|
|
310
|
+
<!-- help:contexts-protection#protection-rules -->
|
|
1474
311
|
|
|
1475
|
-
|
|
312
|
+
Protection rules control when jobs targeting a context can execute.
|
|
1476
313
|
|
|
1477
|
-
|
|
314
|
+
Available rules:
|
|
1478
315
|
|
|
1479
|
-
|
|
316
|
+
- **Branch restrictions** — only allow specific branches to deploy.
|
|
317
|
+
- **Required reviewer approvals** — gate the run on human sign-off.
|
|
318
|
+
- **Wait timers** — delay execution for a fixed period.
|
|
319
|
+
- **Concurrency limits** — prevent collisions between parallel deployments.
|
|
1480
320
|
|
|
1481
|
-
|
|
1482
|
-
concurrency: {
|
|
1483
|
-
group: ({ branch, event }) => `${event.sourceRepo}:${branch}`,
|
|
1484
|
-
cancelInProgress: true,
|
|
1485
|
-
},
|
|
1486
|
-
```
|
|
321
|
+
<!-- /help:contexts-protection -->
|
|
1487
322
|
|
|
1488
|
-
|
|
323
|
+
## Overview
|
|
1489
324
|
|
|
1490
|
-
A
|
|
325
|
+
A context in KiCI provides:
|
|
1491
326
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
327
|
+
- **Variables** -- non-secret key-value configuration (e.g., `API_URL`, `CLUSTER_NAME`)
|
|
328
|
+
- **Scoped secrets** -- encrypted values bound to the context via scope bindings
|
|
329
|
+
- **Protection rules** -- branch restrictions, required reviewers, wait timers, and concurrency limits
|
|
330
|
+
- **Per-source overrides** -- repositories can override unlocked variables for their own deployments
|
|
1495
331
|
|
|
1496
|
-
|
|
332
|
+
## SDK API
|
|
1497
333
|
|
|
1498
|
-
|
|
334
|
+
### Job-level context property
|
|
1499
335
|
|
|
1500
|
-
|
|
336
|
+
The `context` property is set on a job, not a workflow or step:
|
|
1501
337
|
|
|
1502
|
-
```
|
|
338
|
+
```typescript
|
|
1503
339
|
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
1504
340
|
|
|
1505
|
-
export default workflow('
|
|
1506
|
-
on: [push({
|
|
1507
|
-
filter: async ({ sourceRepo, changedFilesStatus, $ }) => {
|
|
1508
|
-
// `changedFiles` throws when the diff is unavailable, so guard first.
|
|
1509
|
-
if (changedFilesStatus !== 'fetched') return true;
|
|
1510
|
-
const found = await $`ls ${sourceRepo.path}`;
|
|
1511
|
-
return found.stdout.includes('Dockerfile');
|
|
1512
|
-
},
|
|
341
|
+
export default workflow('deploy', {
|
|
342
|
+
on: [push({ branches: ['main'] })],
|
|
1513
343
|
jobs: [
|
|
1514
|
-
job('
|
|
1515
|
-
runsOn:
|
|
344
|
+
job('deploy-staging', {
|
|
345
|
+
runsOn: 'default',
|
|
346
|
+
context: 'staging',
|
|
1516
347
|
steps: [
|
|
1517
|
-
step('
|
|
348
|
+
step('deploy', async (ctx) => {
|
|
349
|
+
// ctx.context is the resolved context name
|
|
350
|
+
console.log(`Deploying to ${ctx.context}`);
|
|
351
|
+
// ctx.secrets provides async get/expose/has methods for context-bound secrets
|
|
352
|
+
const dbPassword = await ctx.secrets.get('DB_PASSWORD');
|
|
353
|
+
// Environment variables are in ctx.env
|
|
354
|
+
const apiUrl = ctx.env.API_URL;
|
|
355
|
+
await ctx.$`deploy --target ${ctx.context}`;
|
|
356
|
+
}),
|
|
1518
357
|
],
|
|
1519
358
|
}),
|
|
1520
359
|
],
|
|
1521
360
|
});
|
|
1522
361
|
```
|
|
1523
362
|
|
|
1524
|
-
|
|
363
|
+
### Dynamic contexts
|
|
364
|
+
|
|
365
|
+
The context name can be a string or a function (sync or async) for dynamic contexts (e.g., per-PR review contexts). The function receives the normalized event envelope, with the raw provider body nested at `event.payload`:
|
|
1525
366
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
367
|
+
```typescript
|
|
368
|
+
job('deploy-review', {
|
|
369
|
+
runsOn: 'default',
|
|
370
|
+
context: (event) => `review/PR-${event.payload.pull_request.number}`,
|
|
371
|
+
steps: [
|
|
372
|
+
step('deploy', async (ctx) => {
|
|
373
|
+
// ctx.context is 'review/PR-123' (resolved at runtime)
|
|
374
|
+
await ctx.$`deploy-preview --env ${ctx.context}`;
|
|
375
|
+
}),
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
```
|
|
1535
379
|
|
|
1536
|
-
|
|
380
|
+
A dynamic context function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is resolved on the eval agent's init step before the job runs. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
|
|
1537
381
|
|
|
1538
|
-
|
|
382
|
+
### Multiple contexts per job
|
|
1539
383
|
|
|
1540
|
-
|
|
384
|
+
A job can bind more than one context with `contexts`, an ordered array. This lets a single job draw secrets and variables from several contexts at once — for example a shared `staging` context plus a `my-testing` context that carries test-only variables:
|
|
1541
385
|
|
|
1542
|
-
|
|
386
|
+
```typescript
|
|
387
|
+
job('deploy', {
|
|
388
|
+
runsOn: 'default',
|
|
389
|
+
contexts: ['staging', 'my-testing'],
|
|
390
|
+
steps: [
|
|
391
|
+
step('deploy', async (ctx) => {
|
|
392
|
+
// ctx.secrets and ctx.env carry the merged set from both contexts
|
|
393
|
+
const dbUrl = await ctx.secrets.get('DB_URL');
|
|
394
|
+
}),
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
```
|
|
1543
398
|
|
|
1544
|
-
|
|
399
|
+
- `context` (singular) and `contexts` (array) are mutually exclusive — setting both is a compile error. `context: 'staging'` is exactly equivalent to `contexts: ['staging']`.
|
|
400
|
+
- Each array entry is a static name or a function of the event, resolved per element exactly like a single dynamic context.
|
|
1545
401
|
|
|
1546
|
-
|
|
1547
|
-
| ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
1548
|
-
| Evaluated | once per (event × workflow repo) | once per job that reaches dispatch, and once per job generator |
|
|
1549
|
-
| Evaluated relative to the run | **before** any run row exists | **after** the run row exists |
|
|
1550
|
-
| A `false` verdict leaves | no run at all — nothing appears in the dashboard | a run whose only entries are the evaluation jobs, rolling up to `success` |
|
|
1551
|
-
| `sourceRepo` vs `workflowRepo` | two different repos | the same repo |
|
|
402
|
+
**Merge order — last wins.** All bound contexts are resolved on every dispatch (webhook, scheduled, and test runs alike) and merged in array order. When the same secret or variable key is defined in more than one context, the later entry in the array wins. With `contexts: ['staging', 'my-testing']`, a key defined in both resolves to `my-testing`'s value; keys defined in only one are preserved. The longest-scope-path-wins rule still applies _within_ each context.
|
|
1552
403
|
|
|
1553
|
-
|
|
404
|
+
**Protection rules combine all-must-pass.** A job must satisfy **every** bound context's gates — adding a context can never loosen access. Branch restrictions, trigger-type filters, and repo patterns must pass for all contexts. The minimum trust tier is the most restrictive across them, and required reviewers are the union of all contexts' reviewers. The wait timer is the longest, and the hold expiry is the shortest. If a rule rejects a job, that job is not dispatched. It still appears on the run, as a failed job whose reason names the context and the rule that rejected it. Read the reason with `kici runs show <run-id>`, or on the run detail page.
|
|
1554
405
|
|
|
1555
|
-
**
|
|
406
|
+
**Skip-on-test (allow-and-warn).** On a test or local run (`kici run remote`, `kici run <event> --local`), a bound context never rejects the run. Any bound context that disallows local execution (`allowLocalExecution: false`) — or that is not configured — is **skipped**: its variables and secrets are omitted from the merge and its gates are not evaluated. The run proceeds, and a user-visible warning naming the skipped context(s) is shown both on the `kici run remote` CLI output and on the dashboard run view. This makes the test-only-variables pattern work: with `contexts: ['staging', 'my-testing']` where only `my-testing` allows local execution, a test run resolves just `my-testing`'s variables and warns that `staging` was skipped. If every bound context is skipped, the job runs with no environment variables. This is intentionally different from a fixture `secrets:` mapping, which is fail-closed — see the [testing guide](https://docs.kici.dev/user/testing-guide/).
|
|
1556
407
|
|
|
1557
|
-
|
|
408
|
+
**Unconfigured contexts contribute nothing at dispatch.** At dispatch time a bound context name with no matching configured context (and no matching glob context) simply adds no variables, secrets, or protection rules — the job still runs, exactly as a single dynamic context resolving to an as-yet-unconfigured name does today.
|
|
1558
409
|
|
|
1559
|
-
|
|
410
|
+
**Registration rejects a provably-unsatisfiable binding.** When a workflow is registered, KiCI statically checks every multi-context binding: a bound context that does not exist, a disabled one, or two contexts with mutually-exclusive fixed branch / trigger-type / repository restrictions (no value can satisfy both) makes the binding provably unsatisfiable, and the registration is rejected with a precise message naming the job, the contexts, and the rule — for example `unsatisfiable context binding: job 'deploy' binds contexts [staging, my-testing] with mutually exclusive branch restrictions (no value satisfies all bound contexts)`. Bindings whose restrictions use globs are undecidable at registration and fall through to the dispatch-time gate check instead.
|
|
1560
411
|
|
|
1561
|
-
|
|
1562
|
-
import { job, step, workflow, push, type DynamicJobFn } from '@kici-dev/sdk';
|
|
1563
|
-
import { readFile } from 'node:fs/promises';
|
|
412
|
+
### Job-level environment variables
|
|
1564
413
|
|
|
1565
|
-
|
|
1566
|
-
if (!sourceRepo) return [];
|
|
1567
|
-
const pkg = JSON.parse(await readFile(`${sourceRepo.path}/package.json`, 'utf8'));
|
|
1568
|
-
return Object.keys(pkg.scripts ?? {})
|
|
1569
|
-
.filter((s) => s.startsWith('ci:'))
|
|
1570
|
-
.map((s) =>
|
|
1571
|
-
job(s.replace(':', '-'), {
|
|
1572
|
-
runsOn: ['kici:os:linux'],
|
|
1573
|
-
steps: [step('run', async ({ $ }) => $`pnpm ${s}`)],
|
|
1574
|
-
}),
|
|
1575
|
-
);
|
|
1576
|
-
};
|
|
414
|
+
The `env` property on a job provides static or dynamic environment variables:
|
|
1577
415
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
416
|
+
```typescript
|
|
417
|
+
job('deploy', {
|
|
418
|
+
runsOn: 'default',
|
|
419
|
+
context: 'production',
|
|
420
|
+
env: { DEPLOY_TARGET: 'us-east-1' },
|
|
421
|
+
// Or dynamic:
|
|
422
|
+
// env: (event) => ({ DEPLOY_SHA: event.payload.after?.slice(0, 7) }),
|
|
423
|
+
steps: [
|
|
424
|
+
step('deploy', async (ctx) => {
|
|
425
|
+
// DEPLOY_TARGET is available in ctx.env
|
|
426
|
+
await ctx.$`deploy --region ${ctx.env.DEPLOY_TARGET}`;
|
|
427
|
+
}),
|
|
428
|
+
],
|
|
1581
429
|
});
|
|
1582
430
|
```
|
|
1583
431
|
|
|
1584
|
-
|
|
432
|
+
### Concurrency groups
|
|
1585
433
|
|
|
1586
|
-
|
|
434
|
+
Jobs can define their own concurrency groups to control concurrent execution within a context. For workflow-level concurrency (which applies to all jobs in a workflow), see [Concurrency groups](https://docs.kici.dev/user/concurrency/).
|
|
1587
435
|
|
|
1588
|
-
|
|
436
|
+
Control concurrent deployments to the same context:
|
|
1589
437
|
|
|
1590
|
-
|
|
1591
|
-
|
|
438
|
+
```typescript
|
|
439
|
+
job('deploy', {
|
|
440
|
+
runsOn: 'default',
|
|
441
|
+
context: 'production',
|
|
442
|
+
concurrencyGroup: 'production-api',
|
|
443
|
+
// Or dynamic:
|
|
444
|
+
// concurrencyGroup: (event) => `review-${event.payload.pull_request.number}`,
|
|
445
|
+
steps: [/* ... */],
|
|
446
|
+
});
|
|
447
|
+
```
|
|
1592
448
|
|
|
1593
|
-
|
|
1594
|
-
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
1595
|
-
| Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
|
|
1596
|
-
| Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
|
|
1597
|
-
| Elevated access | **Deprecated and not enforced.** Stored and echoed back, but nothing reads it — a global workflow's job receives no secrets, so there is no access for it to grant. See _Secrets are not available_. | None. Clear the list so it does not imply a grant that is not in force. |
|
|
449
|
+
If no `concurrencyGroup` is specified, the context name is used as the default concurrency group. For a job bound to multiple contexts, the default is the **first** bound context's name.
|
|
1598
450
|
|
|
1599
|
-
|
|
451
|
+
### Step context
|
|
1600
452
|
|
|
1601
|
-
|
|
453
|
+
Inside a step, the `ctx` object provides:
|
|
1602
454
|
|
|
1603
|
-
|
|
455
|
+
| Property | Type | Description |
|
|
456
|
+
| ------------- | ------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
457
|
+
| `ctx.context` | `string \| undefined` | Resolved context name (undefined for jobs without context) |
|
|
458
|
+
| `ctx.env` | `Record<string, string \| undefined>` | Environment variables (merged from system, org, source, and job-level `env`) |
|
|
459
|
+
| `ctx.secrets` | `StepSecretsTyped` | Async accessor for bound secrets (get, expose, has, getMeta, list, mountFile, exposeFile) |
|
|
1604
460
|
|
|
1605
|
-
|
|
461
|
+
| Method | Returns | Description |
|
|
462
|
+
| -------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
463
|
+
| `await ctx.secrets.get(key)` | `string` | Retrieve a secret value. Throws `SecretNotFoundError` if not found. |
|
|
464
|
+
| `await ctx.secrets.expose(key)` | `void` | Set the secret as an environment variable for this step — visible via `ctx.env` and to child processes (`process.env`). Throws `SecretNotFoundError` if not found. |
|
|
465
|
+
| `ctx.secrets.has(key)` | `boolean` | Check if a secret key exists. Synchronous, never throws. |
|
|
466
|
+
| `ctx.secrets.getMeta(key)` | `SecretMeta \| undefined` | Retrieve metadata (value, backend name, scope path) for a resolved secret. Returns `undefined` if not found. |
|
|
467
|
+
| `ctx.secrets.list()` | `string[]` | Every secret key available to the step, sorted alphabetically. Synchronous, never throws. |
|
|
468
|
+
| `await ctx.secrets.mountFile(opts)` | `{ path }` | Materialise one or more secrets to a per-step tmpfile (auto-removed at step end). See [Secrets → Mounting secrets as files](https://docs.kici.dev/user/secrets/#mounting-secrets-as-files). |
|
|
469
|
+
| `await ctx.secrets.exposeFile(envVar, opts)` | `{ path }` | `mountFile` plus `process.env[envVar] = path`; the env var is unset at step end. |
|
|
470
|
+
| `ctx.setSecretOutput(key, val)` | `void` | Publish an encrypted secret output from this job, consumable by downstream jobs via `needs`. Never logged or stored in plaintext. |
|
|
1606
471
|
|
|
1607
|
-
|
|
472
|
+
The full secrets API — including `SecretFileOptions`, log masking, and the canonical `sops` example — is documented in [Secrets](https://docs.kici.dev/user/secrets/).
|
|
1608
473
|
|
|
1609
|
-
|
|
474
|
+
## Environment variable merge precedence
|
|
1610
475
|
|
|
1611
|
-
|
|
476
|
+
When a job targets a context, variables are merged in this order (last wins):
|
|
1612
477
|
|
|
1613
|
-
1. **
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
478
|
+
1. **Allowed system vars** -- `PATH`, `HOME`, etc. from the agent process
|
|
479
|
+
2. **Sandbox defaults** -- `FORCE_COLOR=1`
|
|
480
|
+
3. **KICI\_\* system vars** -- orchestrator-generated metadata
|
|
481
|
+
4. **Org-level context vars** -- from the dashboard, managed per-context
|
|
482
|
+
5. **Source-level overrides** -- per-repository overrides (skips locked vars)
|
|
483
|
+
6. **Job env** -- from the `env` property in the SDK
|
|
484
|
+
7. **`setEnv()` calls** -- runtime modifications within steps
|
|
1617
485
|
|
|
1618
|
-
|
|
486
|
+
> **Note:** Secrets are NOT part of the environment variable merge. They are delivered to the step context via IPC and accessed through `ctx.secrets`, not through `process.env`. See the [step context](https://docs.kici.dev/user/contexts/#step-context) section above.
|
|
1619
487
|
|
|
1620
|
-
|
|
488
|
+
## Protection rules
|
|
1621
489
|
|
|
1622
|
-
|
|
490
|
+
Contexts can have protection rules that gate job execution:
|
|
1623
491
|
|
|
1624
|
-
|
|
492
|
+
### Branch restrictions
|
|
1625
493
|
|
|
1626
|
-
|
|
494
|
+
Limit which branches can deploy to a context:
|
|
1627
495
|
|
|
1628
|
-
|
|
496
|
+
```
|
|
497
|
+
Allowed branches: main, release/*
|
|
498
|
+
```
|
|
1629
499
|
|
|
1630
|
-
|
|
500
|
+
Jobs from other branches are rejected immediately with an error message.
|
|
1631
501
|
|
|
1632
|
-
|
|
502
|
+
**Most internally-triggered runs carry a branch.** KiCI starts these runs for itself: a [schedule](https://docs.kici.dev/user/sdk/triggers/) fire, a [custom event](https://docs.kici.dev/user/events/), a completion trigger, a failure batch, or an [invoke gate](https://docs.kici.dev/user/global-workflows/) summon. None comes from a branch push. A run with one source branch presents it:
|
|
1633
503
|
|
|
1634
|
-
|
|
504
|
+
- A **schedule** fire executes the default branch's workflow, so it presents the repository's default branch.
|
|
505
|
+
- A **`kiciEvent()` subscriber**, a **completion trigger** (`workflowComplete`, `jobComplete`) and an **invoke-gate summon** present the branch of the run behind them. That is the same branch the run behind them presented. For a pull-request run it is the branch the PR targets, not the contributor's own branch — so a subscriber of an event emitted by a PR against `main` presents `main`.
|
|
1635
506
|
|
|
1636
|
-
|
|
507
|
+
A branch restriction compares that branch against your patterns like any other run. So a nightly deploy bound to a `production` context restricted to `main` runs.
|
|
1637
508
|
|
|
1638
|
-
A
|
|
509
|
+
A branch restriction is not a trust control. It checks the branch a run presents, never where the code came from. A pull request against `main` presents `main`, whoever opened it. To gate on that, set a [minimum trust tier](https://docs.kici.dev/user/contexts/#minimum-trust) on the same context.
|
|
1639
510
|
|
|
1640
|
-
|
|
511
|
+
A run with no single source branch presents no branch, and a branch restriction then rejects it:
|
|
1641
512
|
|
|
1642
|
-
-
|
|
1643
|
-
-
|
|
1644
|
-
- **
|
|
513
|
+
- A **failure batch** (`workflowsFailedBatch`). One accumulation window of failed runs causes it, on as many branches, so no one branch is its own.
|
|
514
|
+
- A **scaler event** (`kici.scaler.scale-up`, `kici.scaler.scale-down`). The orchestrator mints these itself, with no run behind them. See [autoscaling workflows](https://docs.kici.dev/user/workflows/autoscaling-workflows/).
|
|
515
|
+
- A **schedule** fire in a repository that has not pushed to its default branch since you upgraded KiCI. The default branch is captured when a push re-registers the workflows, so the first such push after the upgrade fixes it.
|
|
516
|
+
- Any event whose emitting run is no longer on record.
|
|
1645
517
|
|
|
1646
|
-
|
|
518
|
+
The rejection reason says so:
|
|
1647
519
|
|
|
1648
|
-
|
|
520
|
+
```
|
|
521
|
+
Context 'production' restricts branches: this internally-triggered run carries no
|
|
522
|
+
branch, so no branch restriction can be satisfied - a scheduled run gains its
|
|
523
|
+
branch after the next push to the default branch re-registers the workflow;
|
|
524
|
+
alternatively bind a context without a branch restriction, or restrict by trigger
|
|
525
|
+
type instead
|
|
526
|
+
```
|
|
1649
527
|
|
|
1650
|
-
To
|
|
528
|
+
To limit a context by how the run started instead of by branch, use a **trigger-type filter**: the trigger type is a real name (`schedule`, `kici_event`, `workflow_complete`, `job_complete`), so a filter that allows it works on these runs.
|
|
1651
529
|
|
|
1652
|
-
|
|
530
|
+
### Required reviewers
|
|
1653
531
|
|
|
1654
|
-
|
|
532
|
+
Require manual approval before a job can proceed:
|
|
1655
533
|
|
|
1656
|
-
|
|
534
|
+
```
|
|
535
|
+
Required reviewers: alice, bob
|
|
536
|
+
```
|
|
1657
537
|
|
|
1658
|
-
|
|
538
|
+
When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](https://docs.kici.dev/user/cli/runs-and-approvals/#kici-approve) command, or the API. Held runs expire after a configurable timeout.
|
|
1659
539
|
|
|
1660
|
-
|
|
540
|
+
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](https://docs.kici.dev/user/approvals/). Both forms use the same held-element mechanism and the same queue.
|
|
1661
541
|
|
|
1662
|
-
|
|
542
|
+
### Wait timer
|
|
1663
543
|
|
|
1664
|
-
|
|
1665
|
-
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1666
|
-
| Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` (dispatch time) / `Global workflows excluded from registration` (registration time) |
|
|
1667
|
-
| A global workflow is never registered at all — it is absent from `kici-admin registration list` | The push that should have registered it resolved to the `__default__` organization anchor, because no webhook source maps its routing key to an organization, or the fleet-wide master switch is off. | Orchestrator log: `Global workflows excluded from registration` with `"orgId": "__default__"`. Its `remedy` field names both fixes — map the source (`kici-admin source update <routingKey> --customer-id <org>`) and enable global workflows cluster-wide if it is not already (`kici-admin cluster-settings set --global-workflows-enabled true`). |
|
|
1668
|
-
| `repos:` has no effect — workflow only fires on its own repo | The fleet-wide master switch is off. Without it, the orchestrator treats the workflow as per-repo-only. | Check the fleet-wide switch with `kici-admin cluster-settings show`. The dashboard → Settings → Global workflows tab shows it as a read-only badge. |
|
|
1669
|
-
| Secrets unavailable in a global job | Expected — a global workflow's job receives no secrets at all, and the _Elevated access_ list is not enforced. | Move the jobs that need credentials into a per-repository workflow in the repo that owns the secrets |
|
|
1670
|
-
| Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
|
|
1671
|
-
| Global workflow registered, enabled, allowed — and still no run appears | Its `filter` returned `false`. A global filter runs before the run is created, so a suppressed workflow leaves nothing behind at all. | [Reading a global workflow's filter output](https://docs.kici.dev/user/global-workflows/#reading-a-global-workflows-filter-output) — the evaluation round's own log. The orchestrator also logs `Global workflow skipped by eval round`, naming the workflow and the reason. |
|
|
1672
|
-
| Global workflow never fires for one particular source repo | Its `repos:` patterns do not match that repo's identifier. | Orchestrator log: `Global workflows dropped by their repos filter` — one line per delivery, naming each dropped workflow, its repo and its patterns. |
|
|
1673
|
-
| A `failure` check named `KiCI: Organization workflow evaluation` on a commit | The pre-run evaluation failed or timed out, so the global workflows from that repo were not run. | Orchestrator log for the evaluation job; push a new commit to re-evaluate (a redelivery is dropped as a duplicate). |
|
|
1674
|
-
| Same-repo workflow shows a `success` run with no jobs in it | Its `filter` returned `false`. A same-repo filter runs after the run exists, so the run remains, carrying only the evaluation jobs. | The run detail page — the evaluation job's log records the filter verdict. |
|
|
1675
|
-
| Re-run is refused with "Cannot re-run an organization-wide workflow" | Expected — the run executed against a source repo that does not declare the workflow. | [Re-running is not supported](https://docs.kici.dev/user/global-workflows/#re-running-is-not-supported) — push a new commit to the source repo instead. |
|
|
1676
|
-
| Every global workflow stopped running right after an orchestrator upgrade | The agents were not upgraded first. An agent older than v0.5.0 cannot evaluate a global workflow, and one containing a `dynamicJob` now needs an evaluation even without a `filter` — so its **static** jobs stop too. | The `KiCI: Organization workflow evaluation` check names the agent versions it found. Upgrade every `kici:role:init-runner` agent to v0.5.0 or newer. |
|
|
544
|
+
Add a mandatory delay before deployment starts:
|
|
1677
545
|
|
|
1678
|
-
|
|
546
|
+
```
|
|
547
|
+
Wait timer: 300 seconds
|
|
548
|
+
```
|
|
1679
549
|
|
|
1680
|
-
|
|
1681
|
-
triggered it, and whose code the jobs check out. Its run detail page names both
|
|
1682
|
-
repos, so you can tell it apart from an ordinary per-repo run:
|
|
550
|
+
The job waits for the specified duration before proceeding. Useful for staged rollouts.
|
|
1683
551
|
|
|
1684
|
-
|
|
1685
|
-
| ------------ | ------------------------------------------------------------------------ |
|
|
1686
|
-
| `Repository` | the source repo — the one the run acted on |
|
|
1687
|
-
| `Defined in` | the workflow repo, tagged `Organization-wide`. Absent on an ordinary run |
|
|
1688
|
-
| `Workflow` | links into the **workflow** repo, on its default branch |
|
|
552
|
+
### Minimum trust
|
|
1689
553
|
|
|
1690
|
-
|
|
1691
|
-
a commit: the run's own commit belongs to the source repo, and nothing records
|
|
1692
|
-
which commit of the workflow repo a given run used. So the link always shows the
|
|
1693
|
-
file as it stands now, which may have changed since the run.
|
|
554
|
+
Hold a job whose run came from a fork:
|
|
1694
555
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
path has none, and its tab reports that it could not load one.
|
|
556
|
+
```
|
|
557
|
+
Minimum trust: trusted
|
|
558
|
+
```
|
|
1699
559
|
|
|
1700
|
-
|
|
560
|
+
| Value | Effect |
|
|
561
|
+
| --------- | ---------------------------------------------------------- |
|
|
562
|
+
| `trusted` | Holds a run whose ref came from a fork |
|
|
563
|
+
| `known` | Same effect; the value is deprecated and removed at v1.0.0 |
|
|
564
|
+
| (unset) | No trust-based gating |
|
|
1701
565
|
|
|
1702
|
-
|
|
1703
|
-
either one reaches it — the team whose push triggered it, and the team that
|
|
1704
|
-
authored the workflow. Both see it in the run list, in the repository filter
|
|
1705
|
-
(which offers both names), and on the run detail page. Cancelling follows the
|
|
1706
|
-
same rule, so the team whose workflow is running can always stop it.
|
|
566
|
+
Both values block the same thing. Trust comes from the git ref, and that judgement has two answers: a ref in your repository is `trusted`, a ref from a fork is `unknown`. The value you declare still decides the wording of the hold reason.
|
|
1707
567
|
|
|
1708
|
-
|
|
1709
|
-
run against the source repo, so it stays with a member scoped to that repo. A
|
|
1710
|
-
member scoped only to the workflow repo sees the run but not its hold.
|
|
568
|
+
When the gate holds a job, it enters the security approval queue. Someone with `ci_trust:write` or higher must approve it before execution proceeds.
|
|
1711
569
|
|
|
1712
|
-
|
|
1713
|
-
records no separate workflow repo and is scoped to its own repo exactly as
|
|
1714
|
-
before, and a member scoped to neither repo sees nothing in either case.
|
|
570
|
+
A run that resolved **no** tier passes the gate. A pull request from a source with no fork model resolves none, and so does an internal run whose inheritance lookup failed. See [trust tiers on internal triggers](https://docs.kici.dev/user/events/#trust-tiers-on-internal-triggers) for the full table.
|
|
1715
571
|
|
|
1716
|
-
|
|
572
|
+
The trust tier also affects which lock file a pull-request run uses: a trusted ref evaluates the head lock file, a fork ref evaluates the base branch's. A fork run additionally carries no install or registry secrets, and its build-cache writes are confined to that run. So a fork pull request cannot change what CI does, and cannot read a private-registry token, whether or not you set this gate.
|
|
1717
573
|
|
|
1718
|
-
|
|
1719
|
-
decides whether a run exists at all — so on the path where it suppresses a
|
|
1720
|
-
workflow there is no run, and nothing appears in the dashboard. The round's own
|
|
1721
|
-
log is still recorded. Read it with the orchestrator admin CLI, in two steps:
|
|
574
|
+
Set the gate on any context that carries a credential a fork run must not reach. See the [deployment checklist](https://docs.kici.dev/operator/security/security/#deployment-checklist-which-contexts-need-it).
|
|
1722
575
|
|
|
1723
|
-
|
|
1724
|
-
# 1. Find the round. Its workflow name is __globaleval__<owner>/<repo> of the
|
|
1725
|
-
# WORKFLOW repo. In the JSON rows, `id` is the job id and `run_id` is the
|
|
1726
|
-
# run id.
|
|
1727
|
-
kici-admin queue list --workflow-name '__globaleval__myorg/ci-pipelines' --limit 5 --json
|
|
576
|
+
An internally-triggered run carries a tier too. A schedule fire and the orchestrator's own lifecycle events are trusted. A `kiciEvent()` subscriber inherits the tier of the run that emitted the event, so a `minimumTrust` gate on the subscriber's context reads the **emitting** run's tier. See [trust tiers on internal triggers](https://docs.kici.dev/user/events/#trust-tiers-on-internal-triggers).
|
|
1728
577
|
|
|
1729
|
-
|
|
1730
|
-
kici-admin runs logs <run_id> --job <id>
|
|
1731
|
-
```
|
|
578
|
+
See the [CI security architecture docs](https://docs.kici.dev/architecture/security/ci-security/) for the full trust resolution flow.
|
|
1732
579
|
|
|
1733
|
-
|
|
1734
|
-
first eight characters, and the second command needs them in full.
|
|
580
|
+
### Security approval queue
|
|
1735
581
|
|
|
1736
|
-
The two
|
|
1737
|
-
token. Step 1 reads the dispatch queue, which requires `secret.read` — an auditor
|
|
1738
|
-
token is refused with a 403 and never reaches step 2. Step 2 requires only
|
|
1739
|
-
`run.read`, which every role carries.
|
|
582
|
+
A pull request held for security review enters the security approval queue. Two things put it there: the organization's fork switch set to `hold`, and a `minimumTrust` gate blocking a fork run. This is separate from the context approval queue: a security hold asks "is it safe to run this contributor's code at all?", while a context approval hold asks "should this job be promoted?". The two never cross — releasing a security hold needs `ci_trust:write` or higher, releasing a context approval hold needs `contexts:write` plus eligibility for one of the gate's clauses. See [Approval holds vs security holds](https://docs.kici.dev/architecture/approvals/#approval-holds-vs-security-holds) for the full comparison.
|
|
1740
583
|
|
|
1741
|
-
|
|
1742
|
-
per-candidate verdicts the round recorded.
|
|
584
|
+
Held runs can be approved:
|
|
1743
585
|
|
|
1744
|
-
|
|
586
|
+
- Via the **dashboard** on the [Approval queue](https://docs.kici.dev/user/dashboard/contexts-and-secrets/#approval-queue) page, which lists security and context holds together
|
|
587
|
+
- Via a PR comment: `/kici approve` (commenter must have `ci_trust:write+`)
|
|
1745
588
|
|
|
1746
|
-
|
|
1747
|
-
- [Universal-git provider](https://docs.kici.dev/user/providers/universal-git/#global-workflows) — how global workflows interact with `generic:<orgId>:<sourceId>` routing keys.
|
|
1748
|
-
- [SDK reference](https://docs.kici.dev/user/sdk-reference/) — the full set of triggers that accept `repos:`.
|
|
589
|
+
A hold raised by the fork switch covers the whole pull request and uses the org's approval expiry (default 72 hours). A `minimumTrust` hold is raised by a context, so it uses that context's own hold expiry (default one hour). A job carrying both a reviewer approval hold and a security hold carries both expiries, and whichever comes first cancels the run.
|
|
1749
590
|
|
|
1750
|
-
|
|
591
|
+
While the fork switch is holding a pull request, your organization's global workflows do not run for it. Approving the hold releases that pull request's own workflows; it does not retroactively run the organization's global workflows for the event.
|
|
1751
592
|
|
|
1752
|
-
|
|
593
|
+
### Concurrency limits
|
|
1753
594
|
|
|
1754
|
-
|
|
595
|
+
Control how many jobs can run simultaneously in a context:
|
|
1755
596
|
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
597
|
+
```
|
|
598
|
+
Concurrency limit: 1
|
|
599
|
+
Strategy: queue (or cancel-pending)
|
|
600
|
+
```
|
|
1760
601
|
|
|
1761
|
-
|
|
1762
|
-
step applies the change; when already in sync the step is skipped.
|
|
1763
|
-
- **Check mode** (`--check`): for each step, `check()` runs and KiCI reports what
|
|
1764
|
-
_would_ change — **without changing anything**. This is the same model as a
|
|
1765
|
-
dry-run plan: you see the drift before any side effect happens.
|
|
602
|
+
The concurrency limit is a positive integer; leave it unset for unlimited concurrency.
|
|
1766
603
|
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
preview you can gate a build on.
|
|
604
|
+
- **queue** -- new jobs wait in a FIFO queue (with configurable timeout, default 1 hour)
|
|
605
|
+
- **cancel-pending** -- pending (queued) jobs are cancelled when the limit is reached
|
|
1770
606
|
|
|
1771
|
-
|
|
607
|
+
The children of a matrix job count individually against the limit. A three-child
|
|
608
|
+
matrix bound to a context with a limit of two dispatches two children and applies
|
|
609
|
+
the strategy above to the third.
|
|
1772
610
|
|
|
1773
|
-
|
|
1774
|
-
`run` becomes the _apply_ function and receives the drift value `check`
|
|
1775
|
-
returned:
|
|
611
|
+
## Dashboard management
|
|
1776
612
|
|
|
1777
|
-
|
|
1778
|
-
import { step, z } from '@kici-dev/sdk';
|
|
613
|
+
### Creating contexts
|
|
1779
614
|
|
|
1780
|
-
|
|
1781
|
-
// optional schema for the drift value — gives the dashboard a typed shape
|
|
1782
|
-
drift: z.object({ want: z.string() }),
|
|
615
|
+
Navigate to **Settings > Contexts** in the dashboard. Click **New context** to choose the context name and type (Fixed or Glob).
|
|
1783
616
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const current = await ctx.$`nginx -T`;
|
|
1787
|
-
return current.stdout.includes(DESIRED) ? null : { want: DESIRED };
|
|
1788
|
-
},
|
|
617
|
+
- **Fixed** -- applies to jobs that declare exactly this context name, like `staging` or `production`
|
|
618
|
+
- **Glob** -- applies to any context name a job declares that matches the pattern, e.g. `review/*` matches a job with `context: 'review/PR-123'`
|
|
1789
619
|
|
|
1790
|
-
|
|
1791
|
-
// serializable face: it streams to the logs and persists for the dashboard.
|
|
1792
|
-
summarize: (drift) => `would rewrite nginx.conf (${drift.want.length} bytes)`,
|
|
620
|
+
The contexts list shows each context's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](https://docs.kici.dev/user/testing-guide/)), and whether it is enabled.
|
|
1793
621
|
|
|
1794
|
-
|
|
1795
|
-
run: async (ctx, drift) => {
|
|
1796
|
-
await writeConfig(drift.want);
|
|
1797
|
-
return { reloaded: true };
|
|
1798
|
-
},
|
|
622
|
+
### Context detail page
|
|
1799
623
|
|
|
1800
|
-
|
|
1801
|
-
whenInSync: async () => ({ reloaded: false }),
|
|
1802
|
-
});
|
|
1803
|
-
```
|
|
624
|
+
Each context has four tabs:
|
|
1804
625
|
|
|
1805
|
-
|
|
626
|
+
1. **Variables** -- manage key-value pairs with lock toggles. Locked variables cannot be overridden by source-level overrides. Source overrides are managed in a sub-tab.
|
|
1806
627
|
|
|
1807
|
-
|
|
1808
|
-
| ------------ | ---------------- | ---------------------------------------------------------------------- |
|
|
1809
|
-
| `check` | to opt in | Read-only inspection. Return a drift value, or `null` when in sync. |
|
|
1810
|
-
| `summarize` | when `check` set | Human-readable, serializable preview of the drift. Streams + persists. |
|
|
1811
|
-
| `run` | always | Apply function. With `check`, it receives the drift as its second arg. |
|
|
1812
|
-
| `whenInSync` | optional | Produces the step's outputs when `check` returned `null`. |
|
|
1813
|
-
| `drift` | optional | Schema that validates / shapes the drift value. |
|
|
628
|
+
2. **Secrets** -- view bound secret scopes and their resolved secret count. Add bindings by specifying scope glob patterns (e.g., `aws/prod/**`).
|
|
1814
629
|
|
|
1815
|
-
|
|
1816
|
-
both produce the same output type — one output shape per step, whichever path
|
|
1817
|
-
runs. Every other step facet (`cache`, `rules`, `continueOnError`, `timeout`,
|
|
1818
|
-
`retry`, `approval`, `onCancel`, `cleanup`, `outputs`) composes unchanged.
|
|
630
|
+
3. **Protection** -- configure branch restrictions, required reviewers, wait timers, and concurrency limits with enable toggles for each section. Turning a section's toggle off and saving clears that rule on the context, so the gate stops applying to new runs. Emptying the hold expiry field clears it too, and held runs fall back to the default one-hour hold window.
|
|
1819
631
|
|
|
1820
|
-
|
|
1821
|
-
facet is fully optional.
|
|
632
|
+
4. **History** -- view filtered runs targeting this context.
|
|
1822
633
|
|
|
1823
|
-
|
|
634
|
+
### Bound contexts on runs
|
|
1824
635
|
|
|
1825
|
-
A run
|
|
636
|
+
A job's bound deployment contexts are shown as chips on the run detail page (in the job metadata panel) in the order the job declared them, and the distinct set across a run's jobs appears as compact chips on the run list. For a multi-context job the chips read left-to-right in merge order — later contexts override earlier ones on key collisions. A `(dynamic)` chip marks a context whose name is computed at runtime; it resolves to the real name once the run starts. A job that binds a single context shows one chip; a job that binds none shows no chip.
|
|
1826
637
|
|
|
1827
|
-
|
|
1828
|
-
| --------------------- | ------------------------- | ------------------------------------------------------------------------------------------ |
|
|
1829
|
-
| `apply` | (default, no flags) | Converge: drift ⇒ apply ⇒ **applied**; null ⇒ **in sync** (skipped). |
|
|
1830
|
-
| `check` | `--check` | Preview only: drift ⇒ **would change**; null ⇒ **in sync**. Never applies. Always exits 0. |
|
|
1831
|
-
| `check-fail-on-drift` | `--check --fail-on-drift` | Same as check, but the run **fails** if any step reports drift. |
|
|
638
|
+
A job a bound context rejects keeps its chips, and shows as failed because it never ran. Its failure reason names the context and the rule that rejected it. `kici runs show <run-id>` prints the same reason.
|
|
1832
639
|
|
|
1833
|
-
|
|
640
|
+
### Secrets management
|
|
1834
641
|
|
|
1835
|
-
|
|
1836
|
-
- **in sync** — `check` returned `null`; nothing to do.
|
|
1837
|
-
- **would change** — drift was found in check mode; the change was previewed, not applied.
|
|
1838
|
-
- **no check** — a plain step (no `check`) reached under check mode. A
|
|
1839
|
-
side-effecting step can't be safely previewed, so it is skipped.
|
|
642
|
+
Secrets are individual encrypted values organized by scope paths (e.g., `aws/prod`, `databases/postgres`). Scopes are bound to contexts via bindings:
|
|
1840
643
|
|
|
1841
|
-
|
|
1842
|
-
|
|
644
|
+
- **Scope-centric view** (Secrets page): tree view of scopes with per-scope context binding checkboxes
|
|
645
|
+
- **Context-centric view** (inside context detail): bound scopes, resolved secrets, add binding
|
|
1843
646
|
|
|
1844
|
-
|
|
647
|
+
When scope paths collide on the same key name, the longer (more specific) path wins.
|
|
1845
648
|
|
|
1846
|
-
|
|
649
|
+
## Type generation
|
|
1847
650
|
|
|
1848
|
-
|
|
1849
|
-
# Apply (default): converge the workflow.
|
|
1850
|
-
kici run push --local
|
|
1851
|
-
kici run remote my-fixture
|
|
651
|
+
Running `kici types` generates two augmented interfaces: `KnownSecretKeys` (union of all secret keys across all contexts) and `ContextSecrets` (per-context key unions):
|
|
1852
652
|
|
|
1853
|
-
|
|
1854
|
-
|
|
653
|
+
```typescript
|
|
654
|
+
interface KnownSecretKeys {
|
|
655
|
+
DB_PASSWORD: string;
|
|
656
|
+
API_KEY: string;
|
|
657
|
+
}
|
|
1855
658
|
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
659
|
+
interface ContextSecrets {
|
|
660
|
+
production: 'DB_PASSWORD' | 'API_KEY';
|
|
661
|
+
staging: 'DB_PASSWORD';
|
|
662
|
+
}
|
|
1859
663
|
```
|
|
1860
664
|
|
|
1861
|
-
|
|
1862
|
-
error.
|
|
665
|
+
`KnownSecretKeys` narrows `ctx.secrets.get()` and `ctx.secrets.expose()` key parameters to valid key names. `ContextSecrets` maps each context to its available secret key names as a string union. Dynamic contexts fall back to the full `KnownSecretKeys` union.
|
|
1863
666
|
|
|
1864
|
-
|
|
667
|
+
---
|
|
1865
668
|
|
|
1866
|
-
|
|
1867
|
-
badge on the run header. Each step shows its outcome chip — applied / in sync /
|
|
1868
|
-
would change / no check — and, when drift was detected, the `summarize` line
|
|
1869
|
-
describing what would change. The rendering is read-only.
|
|
669
|
+
## Dashboard
|
|
1870
670
|
|
|
1871
|
-
|
|
671
|
+
Source: https://docs.kici.dev/user/dashboard/
|
|
1872
672
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
673
|
+
The KiCI dashboard is the browser interface for monitoring workflow runs, inspecting jobs and logs, and managing your organization. It signs in via OIDC and talks to KiCI over its API.
|
|
674
|
+
|
|
675
|
+
This guide is split across the following pages:
|
|
676
|
+
|
|
677
|
+
| Page | Covers |
|
|
678
|
+
| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
679
|
+
| [Getting started](https://docs.kici.dev/user/dashboard/getting-started/) | Onboarding checklist and your organizations list |
|
|
680
|
+
| [Navigation and layout](https://docs.kici.dev/user/dashboard/navigation/) | Sidebar, mobile nav, theme, time display, shortcuts, error pages |
|
|
681
|
+
| [Runs and logs](https://docs.kici.dev/user/dashboard/runs/) | Run list, run detail, the log viewer |
|
|
682
|
+
| [Settings](https://docs.kici.dev/user/dashboard/settings/) | Members, roles, teams, keys, sources, billing, security, support access |
|
|
683
|
+
| [Workflows, diagnostics, and orchestrators](https://docs.kici.dev/user/dashboard/workflows-and-diagnostics/) | Registered workflows, infra health, per-cluster views |
|
|
684
|
+
| [Contexts, secrets, and approvals](https://docs.kici.dev/user/dashboard/contexts-and-secrets/) | Contexts, secret scopes, approval queue |
|
|
685
|
+
| [Activity and DLQ](https://docs.kici.dev/user/dashboard/activity-and-dlq/) | Forensic activity log and dead-letter queue |
|
|
686
|
+
| [Notifications](https://docs.kici.dev/user/dashboard/notifications/) | Personal run notifications and org-wide channels, subscriptions, delivery log |
|
|
687
|
+
| [Account](https://docs.kici.dev/user/dashboard/account/) | Personal account settings |
|
|
1876
688
|
|
|
1877
689
|
---
|
|
1878
690
|
|
|
@@ -2102,7 +914,7 @@ registries: [
|
|
|
2102
914
|
## Security model
|
|
2103
915
|
|
|
2104
916
|
- **Per-context scoping.** Every `tokenSecret` and `installEnv` entry is qualified with a context name. The orchestrator runs the same protection-rule pipeline (branch / trust / concurrency / reviewer / wait-timer) against each named context **before** resolving any secret, so a workflow that wants a `production` token from a feature branch is rejected exactly like a job that tries to deploy to `production` from a feature branch. A reviewer-gated install context **pauses** the whole workflow dispatch as a workflow-scoped held run instead of resolving the token — see [Reviewer-gated installs](https://docs.kici.dev/user/private-registries/#reviewer-gated-installs) below.
|
|
2105
|
-
- **Untrusted
|
|
917
|
+
- **Untrusted refs get no tokens.** When the trust resolution returns anything other than `trusted` — every fork pull request does — the orchestrator strips both `npmRegistries` and `installEnvSecrets` out of the dispatch. The install runs without auth and fails naturally on the first private dep. A fork pull request cannot observe a registry token, even if a context lacks an explicit [minimum trust](https://docs.kici.dev/user/contexts/#minimum-trust) rule.
|
|
2106
918
|
- **Lifecycle scripts disabled.** Whenever a private registry is in scope, the agent runs the install with `--ignore-scripts` (npm, pnpm, and yarn classic alike; yarn berry gets the equivalent `enableScripts: false`). A malicious `preinstall` / `postinstall` hook in committed `package.json` cannot read the synthesized token env vars, even though they exist in the install subprocess. For a pnpm or yarn workspace, the agent builds your in-repo dependency closure as a separate step **after** the install's auth is torn down, so build scripts never see the tokens either.
|
|
2107
919
|
- **Stderr is redacted.** If the install fails, the agent masks every token literal out of the surfaced stderr / stdout chunks before logging.
|
|
2108
920
|
- **Job-scoped env-var names.** The synthesized auth env var is `KICI_NPM_TOKEN_<jobIdShort>_<i>` where `jobIdShort` is the first 8 chars of the dispatched job id. The name is unguessable from outside the install subprocess and not reused across jobs.
|
|
@@ -2294,11 +1106,12 @@ commonly, an offline `{ issuer, jwks }` file for air-gapped checks.
|
|
|
2294
1106
|
|
|
2295
1107
|
### Why you supply it out-of-band
|
|
2296
1108
|
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
`iss: https://attacker.example`
|
|
2301
|
-
|
|
1109
|
+
The verifier already resolves a sensible default, so why is naming the trust
|
|
1110
|
+
root a supported step at all -- why not let the verifier read the issuer from
|
|
1111
|
+
the token? Because the issuer named **inside** a token cannot be trusted. A
|
|
1112
|
+
forged bundle could carry a token that names `iss: https://attacker.example`
|
|
1113
|
+
_and_ bundle a key set that "verifies" it. That makes the whole signature chain
|
|
1114
|
+
circular and self-attesting. The verifier
|
|
2302
1115
|
therefore pins to an issuer you supply out-of-band and checks the token against
|
|
2303
1116
|
_that_ — the bundle is verified against a key set you trust, not one it shipped
|
|
2304
1117
|
with. Naming the trust root is a security requirement, not a multiple-choice
|
|
@@ -2740,7 +1553,9 @@ Note that `get()` is async -- you must `await` the result.
|
|
|
2740
1553
|
|
|
2741
1554
|
## Typed secrets
|
|
2742
1555
|
|
|
2743
|
-
When you run `kici types`, the compiler generates a `.kici/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
|
|
1556
|
+
When you run `kici types`, the compiler generates a `.kici/types/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
|
|
1557
|
+
|
|
1558
|
+
This file is a local development aid, not source: `kici init` gitignores `.kici/types/`, so each team member generates their own copy with `kici types` rather than committing it. When the Platform is unreachable, `kici types` keeps any existing copy untouched, or writes an empty stub if none exists, so type checking degrades to "no known keys" instead of failing.
|
|
2744
1559
|
|
|
2745
1560
|
See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
|
|
2746
1561
|
|