@kici-dev/compiler 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/commands/compile.js +1 -13
- package/dist/commands/preview.js +1 -8
- package/dist/commands/types.js +1 -2
- package/dist/errors/formatter.d.ts +2 -4
- package/dist/errors/formatter.js +1 -3
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +2 -2
- package/dist/generators/secrets-dts.d.ts +0 -1
- package/dist/generators/secrets-dts.js +1 -2
- package/dist/llm-context/llms-architecture.txt +27 -13
- package/dist/llm-context/llms-cli.txt +26 -6
- package/dist/llm-context/llms-features.txt +272 -91
- package/dist/llm-context/llms-full.txt +649 -235
- package/dist/llm-context/llms-getting-started.txt +20 -20
- package/dist/llm-context/llms-patterns.txt +10 -6
- package/dist/llm-context/llms-providers.txt +4 -6
- package/dist/llm-context/llms-sdk-runtime.txt +37 -36
- package/dist/llm-context/llms-sdk.txt +253 -57
- package/dist/llm-context/llms.txt +6 -6
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/lockfile/generator.js +137 -42
- package/dist/lockfile/index.d.ts +0 -2
- package/dist/lockfile/index.js +1 -2
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/dry-run.d.ts +1 -2
- package/dist/test-runner/dry-run.js +1 -18
- package/dist/types.d.ts +32 -8
- package/dist/types.js +7 -1
- package/dist/validation/validator.js +40 -0
- package/package.json +6 -6
- package/sbom.spdx.json +126 -121
- package/dist/lockfile/purity-analyzer.d.ts +0 -25
- package/dist/lockfile/purity-analyzer.js +0 -204
- package/dist/lockfile/purity-diagnostics.d.ts +0 -31
- package/dist/lockfile/purity-diagnostics.js +0 -52
|
@@ -36,19 +36,19 @@ Your workflow is plain TypeScript, but different parts of it run at three distin
|
|
|
36
36
|
|
|
37
37
|
## The three phases
|
|
38
38
|
|
|
39
|
-
| Phase | Where it runs | What runs
|
|
40
|
-
| ---------------- | ---------------------------------------- |
|
|
41
|
-
| **Compile** | Your dev machine or CI (`kici compile`) | Load your workflow modules, validate the DAG, assign step IDs,
|
|
42
|
-
| **Orchestrator** | Your orchestrator (no repo clone) | Match triggers against the lock
|
|
43
|
-
| **Agent** | An ephemeral agent (fresh clone per job) | Load the workflow module, evaluate job and step rules, run step bodies and hooks, run
|
|
39
|
+
| Phase | Where it runs | What runs | When |
|
|
40
|
+
| ---------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
|
|
41
|
+
| **Compile** | Your dev machine or CI (`kici compile`) | Load your workflow modules, validate the DAG, assign step IDs, emit `kici.lock.json` | Before anything is pushed |
|
|
42
|
+
| **Orchestrator** | Your orchestrator (no repo clone) | Match triggers against the lock and dispatch jobs; it never evaluates workflow code | On each incoming event |
|
|
43
|
+
| **Agent** | An ephemeral agent (fresh clone per job) | Load the workflow module, evaluate job and step rules, run step bodies and hooks, run dynamic-value init steps and `dynamicJob` generators (both forms) | After dispatch |
|
|
44
44
|
|
|
45
45
|
The lock file is the seam. Everything left of it is decided once at compile time and frozen into JSON; everything right of it reads that JSON. See [the lock file and workflow drift](https://docs.kici.dev/user/lock-file-and-drift/) and [the three-tier architecture](https://docs.kici.dev/architecture/overview/) for the wider picture.
|
|
46
46
|
|
|
47
47
|
## Compile time
|
|
48
48
|
|
|
49
|
-
`kici compile` loads your `.kici/workflows/*.ts`, validates dependencies (no cycles, no missing `needs`), assigns compile-time step IDs (unnamed steps become `step-1`, `step-2`, …),
|
|
49
|
+
`kici compile` loads your `.kici/workflows/*.ts`, validates dependencies (no cycles, no missing `needs`), assigns compile-time step IDs (unnamed steps become `step-1`, `step-2`, …), and writes `kici.lock.json`.
|
|
50
50
|
|
|
51
|
-
The compiler runs your module's **top-level code** to build the workflow object — but that execution's side effects and in-memory state do not travel. Only the resulting workflow structure
|
|
51
|
+
The compiler runs your module's **top-level code** to build the workflow object — but that execution's side effects and in-memory state do not travel. Only the resulting workflow structure lands in the lock. Anything your top-level code computes that isn't part of the returned workflow object simply doesn't exist past this point.
|
|
52
52
|
|
|
53
53
|
See [compile the workflow](https://docs.kici.dev/user/getting-started/#compile-the-workflow) for the command in context.
|
|
54
54
|
|
|
@@ -59,7 +59,7 @@ The lock is portable JSON. It carries:
|
|
|
59
59
|
- Workflow and trigger metadata.
|
|
60
60
|
- The job and step DAG, with compile-time step IDs.
|
|
61
61
|
- Static values, verbatim.
|
|
62
|
-
-
|
|
62
|
+
- Markers noting which fields are dynamic, so the orchestrator knows to resolve them on the agent's init step.
|
|
63
63
|
|
|
64
64
|
It does **not** carry:
|
|
65
65
|
|
|
@@ -72,11 +72,13 @@ The consequence is blunt: if a value isn't in the lock, the orchestrator can't s
|
|
|
72
72
|
|
|
73
73
|
## Orchestrator time
|
|
74
74
|
|
|
75
|
-
On each event the orchestrator matches triggers using only the lock — it never clones your repository.
|
|
75
|
+
On each event the orchestrator matches triggers using only the lock — it never clones your repository and never evaluates workflow code. Dynamic `context`, `env`, and `concurrencyGroup` functions are not run here: the orchestrator dispatches a short init step to an agent to resolve them (see below).
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
Trigger matching can query the **contents** of individual source files, not just their paths: a `pr()`, `push()`, or `tag()` trigger with a [`requires`](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires) filter is matched by reading the named files at the event's commit and evaluating the filter as declarative data — still with no repository clone and no workflow code executed. A `requires` regex is checked for catastrophic (ReDoS) shapes at `kici compile` time and rejected there, so only safe patterns reach the orchestrator.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
The orchestrator also does **not** run `dynamicJob` generator bodies itself: for the event-only (function) form it dispatches a dedicated dynamic-evaluation job to an agent at event time; the generator function then runs agent-side (see below).
|
|
80
|
+
|
|
81
|
+
See [dynamic values](https://docs.kici.dev/user/dynamic-values/) for how dynamic `context`, `env`, and `concurrencyGroup` functions resolve.
|
|
80
82
|
|
|
81
83
|
## Agent time
|
|
82
84
|
|
|
@@ -84,7 +86,7 @@ After dispatch, each job runs in its own ephemeral agent sandbox: a shallow clon
|
|
|
84
86
|
|
|
85
87
|
1. **Job-level rules** are evaluated. By this point the agent has already spawned and the source has already been restored, so a job that its rules skip has **still** paid for that spawn and clone; only its steps are avoided.
|
|
86
88
|
2. **Step-level rules**, then each step's `run()` body and its hooks.
|
|
87
|
-
3. **
|
|
89
|
+
3. **Dynamic values** (`context`, `env`, `concurrencyGroup` functions) are resolved here, via a short `__init__` job that runs the function before the real job runs; this shows in the run timeline as an `Init:` entry.
|
|
88
90
|
4. **`dynamicJob` generators run here — both forms.** The event-only (function) form runs in a dedicated evaluation job dispatched at event time; the result-aware (options) form is deferred until its declared `needs` complete, then run with the upstream outputs frozen as `ctx.needs`.
|
|
89
91
|
|
|
90
92
|
See [job execution](https://docs.kici.dev/architecture/execution/job-execution/) and [hooks and rules](https://docs.kici.dev/user/hooks/) for the details.
|
|
@@ -94,8 +96,7 @@ See [job execution](https://docs.kici.dev/architecture/execution/job-execution/)
|
|
|
94
96
|
| Construct | Runs on | When |
|
|
95
97
|
| ---------------------------- | ---------------- | ------------------------------- |
|
|
96
98
|
| Static value | Compile → lock | Never re-evaluated |
|
|
97
|
-
|
|
|
98
|
-
| Impure dynamic value | Agent init job | Per event |
|
|
99
|
+
| Dynamic value | Agent init step | Per event |
|
|
99
100
|
| Job-level rules | Agent | After clone |
|
|
100
101
|
| Step-level rules | Agent | Per step |
|
|
101
102
|
| `dynamicJob` (function form) | Agent (eval job) | Dispatched at event time |
|
|
@@ -134,12 +135,11 @@ Outputs are typed across the job boundary too: reading `jobRef.result.…` or `c
|
|
|
134
135
|
|
|
135
136
|
## Common footguns
|
|
136
137
|
|
|
137
|
-
| Symptom
|
|
138
|
-
|
|
|
139
|
-
| A top-level `let seen = 0` (or a cache filled in job A) is empty in job B
|
|
140
|
-
|
|
|
141
|
-
|
|
|
142
|
-
| A rule-skipped job still spawned an agent and cloned | Job-level rules evaluate agent-side, after dispatch and clone — not on the orchestrator | This is by design: rules can read true runtime context (`$`, `changedFiles`, `env`). See [step-level rules](https://docs.kici.dev/user/hooks/#step-level-rules) |
|
|
138
|
+
| Symptom | Why | Fix |
|
|
139
|
+
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| A top-level `let seen = 0` (or a cache filled in job A) is empty in job B | Each job loads the workflow module fresh in its own agent process after its own clone — there is no shared memory between jobs | Pass data through step/job **outputs** (`OutputProxy` / `needs`), not module variables |
|
|
141
|
+
| Fan-out job identities shift between re-evaluations | `ctx.event` / `ctx.needs` are frozen and replayed, but `Date.now()` / `Math.random()` are not | Derive job identity only from the frozen event/needs snapshot |
|
|
142
|
+
| A rule-skipped job still spawned an agent and cloned | Job-level rules evaluate agent-side, after dispatch and clone — not on the orchestrator | This is by design: rules can read true runtime context (`$`, `changedFiles`, `env`). See [step-level rules](https://docs.kici.dev/user/hooks/#step-level-rules) |
|
|
143
143
|
|
|
144
144
|
## See also
|
|
145
145
|
|
|
@@ -1080,12 +1080,16 @@ export default workflow('ci', {
|
|
|
1080
1080
|
|
|
1081
1081
|
Rule check functions receive a `RuleContext` with:
|
|
1082
1082
|
|
|
1083
|
-
| Property | Type | Description
|
|
1084
|
-
| -------------- | ----------------------------------- |
|
|
1085
|
-
| `event` | `EventPayload` | The triggering event data
|
|
1086
|
-
| `changedFiles` | `string[]` | Files changed in this event
|
|
1087
|
-
| `
|
|
1088
|
-
|
|
|
1083
|
+
| Property | Type | Description |
|
|
1084
|
+
| -------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
1085
|
+
| `event` | `EventPayload` | The triggering event data |
|
|
1086
|
+
| `changedFiles` | `string[]` | Files changed in this event |
|
|
1087
|
+
| `sourceRepo` | `RepoInfo \| undefined` | The repo whose event triggered the run, when the evaluation has a checkout |
|
|
1088
|
+
| `workflowRepo` | `RepoInfo \| undefined` | The repo that registered the workflow. The same repo as `sourceRepo` outside a global workflow |
|
|
1089
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
1090
|
+
| `$` | zx shell | Shell executor for running commands |
|
|
1091
|
+
|
|
1092
|
+
`RepoInfo` carries `path` — an absolute path to that repo's checkout — plus optional `ref` and `sha`. In a [global workflow](https://docs.kici.dev/user/global-workflows/) the two are different repos, which is what lets one rule read the source repo's tree while the workflow lives elsewhere. Read _through_ `path`: its contents are stable, but the path itself differs between the evaluation and the later run.
|
|
1089
1093
|
|
|
1090
1094
|
### Marker rules
|
|
1091
1095
|
|
|
@@ -2105,22 +2109,24 @@ function workflow(name: string, options: WorkflowOptions): Workflow;
|
|
|
2105
2109
|
|
|
2106
2110
|
**Parameters:**
|
|
2107
2111
|
|
|
2108
|
-
| Parameter | Type | Required | Description
|
|
2109
|
-
| --------------------- | ---------------------------------------------------------------------- | -------- |
|
|
2110
|
-
| `name` | `string` | yes | Unique workflow name
|
|
2111
|
-
| `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators
|
|
2112
|
-
| `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger
|
|
2113
|
-
| `options.rules` | `Rule[]` | no | Conditions that must pass for execution
|
|
2114
|
-
| `options.
|
|
2115
|
-
| `options.
|
|
2116
|
-
| `options.
|
|
2117
|
-
| `options.
|
|
2118
|
-
| `options.
|
|
2119
|
-
| `options.
|
|
2120
|
-
| `options.
|
|
2121
|
-
| `options.
|
|
2122
|
-
| `options.
|
|
2123
|
-
| `options.
|
|
2112
|
+
| Parameter | Type | Required | Description |
|
|
2113
|
+
| --------------------- | ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2114
|
+
| `name` | `string` | yes | Unique workflow name |
|
|
2115
|
+
| `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators |
|
|
2116
|
+
| `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger |
|
|
2117
|
+
| `options.rules` | `Rule[]` | no | Conditions that must pass for execution |
|
|
2118
|
+
| `options.filter` | `FilterFn` | no | Pre-dispatch predicate deciding whether the workflow applies to the event's source repo. A `false` result suppresses the workflow's jobs -- none is dispatched and none is reported as skipped. See [Global workflows](https://docs.kici.dev/user/global-workflows/). |
|
|
2119
|
+
| `options.description` | `string` | no | Human-readable description |
|
|
2120
|
+
| `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache. |
|
|
2121
|
+
| `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<context>:<secret>` syntax. |
|
|
2122
|
+
| `options.installEnv` | `string[]` | no | Qualified `<context>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`). |
|
|
2123
|
+
| `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled |
|
|
2124
|
+
| `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
|
|
2125
|
+
| `options.onSuccess` | `HookInput` | no | Runs on workflow success |
|
|
2126
|
+
| `options.onFailure` | `HookInput` | no | Runs on workflow failure |
|
|
2127
|
+
| `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](https://docs.kici.dev/user/concurrency/). |
|
|
2128
|
+
| `options.timeout` | `number` | no | Whole-run wall-clock timeout in milliseconds across all jobs. On breach the orchestrator cancels the run and marks it timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
|
|
2129
|
+
| `options.approval` | `ApprovalConfig` | no | Pause for a manual human approval before the whole workflow dispatches. See [Approval gates](https://docs.kici.dev/user/approvals/). |
|
|
2124
2130
|
|
|
2125
2131
|
**Returns:** `Workflow` -- an immutable workflow definition.
|
|
2126
2132
|
|
|
@@ -2146,34 +2152,42 @@ function job(options: JobOptions): Job;
|
|
|
2146
2152
|
|
|
2147
2153
|
**Parameters:**
|
|
2148
2154
|
|
|
2149
|
-
| Parameter
|
|
2150
|
-
|
|
|
2151
|
-
| `name`
|
|
2152
|
-
| `options.runsOn`
|
|
2153
|
-
| `options.
|
|
2154
|
-
| `options.
|
|
2155
|
-
| `options.
|
|
2156
|
-
| `options.
|
|
2157
|
-
| `options.
|
|
2158
|
-
| `options.
|
|
2159
|
-
| `options.
|
|
2160
|
-
| `options.
|
|
2161
|
-
| `options.
|
|
2162
|
-
| `options.
|
|
2163
|
-
| `options.
|
|
2164
|
-
| `options.
|
|
2165
|
-
| `options.
|
|
2166
|
-
| `options.
|
|
2167
|
-
| `options.
|
|
2168
|
-
| `options.
|
|
2169
|
-
| `options.
|
|
2170
|
-
| `options.
|
|
2171
|
-
| `options.
|
|
2172
|
-
| `options.
|
|
2173
|
-
| `options.
|
|
2174
|
-
| `options.
|
|
2175
|
-
| `options.
|
|
2176
|
-
| `options.
|
|
2155
|
+
| Parameter | Type | Required | Description |
|
|
2156
|
+
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
2157
|
+
| `name` | `string` | no | Job name (auto-generated UUID if omitted) |
|
|
2158
|
+
| `options.runsOn` | `RunsOn` | yes (or `runsOnAll`) | Single-agent targeting -- runner label(s) and optional exclusions (see below). Mutually exclusive with `runsOnAll`. |
|
|
2159
|
+
| `options.runsOnAll` | `RunsOnAllInput` | yes (or `runsOn`) | Host fan-out -- one pinned execution per roster host matching the predicate. Mutually exclusive with `runsOn`. See [runsOnAll host fan-out](https://docs.kici.dev/user/sdk/runs-on-all/). |
|
|
2160
|
+
| `options.onUnreachable` | `'skip' \| 'fail' \| 'hold'` | no (default: `hold`) | Failure policy for unreachable durable hosts. `skip` omits them, `fail` fails the run, `hold` queues a pinned child and waits. Only meaningful alongside `runsOnAll`. |
|
|
2161
|
+
| `options.includeUninitialized` | `boolean` | no (default: `false`) | Widen a `runsOnAll` fan-out to declared-but-un-agented hosts -- a matching host with no live agent gets a temporary init-runner brought up over SSH. Only meaningful alongside `runsOnAll`. |
|
|
2162
|
+
| `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
|
|
2163
|
+
| `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](https://docs.kici.dev/user/sdk/core/#single-step-job-shorthand). Mutually exclusive with `steps`. |
|
|
2164
|
+
| `options.needs` | `Array<Job \| string \| DynamicGroupRef \| { name; when? } \| { group; when? }>` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs) |
|
|
2165
|
+
| `options.rules` | `Rule[]` | no | Conditions for conditional execution |
|
|
2166
|
+
| `options.description` | `string` | no | Human-readable description |
|
|
2167
|
+
| `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
|
|
2168
|
+
| `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
|
|
2169
|
+
| `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
|
|
2170
|
+
| `options.maxParallel` | `number` | no | Fan-out concurrency width -- the maximum number of fan-out children (matrix combinations or `runsOnAll` hosts) running at once. A sliding window; `1` is strictly serial. Must be `>= 1`. |
|
|
2171
|
+
| `options.failFast` | `boolean` | no (default: `false`) | Halt the fan-out on the first child failure: stop releasing new children and skip the ones still held. Applies to both matrix and `runsOnAll` fan-out. |
|
|
2172
|
+
| `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
|
|
2173
|
+
| `options.container` | `string \| ContainerConfig` | no | Docker image for job execution. String form is the image name; object form adds `env`. All steps run inside the container. |
|
|
2174
|
+
| `options.sandbox` | `{ capabilities?: string[]; network?: 'default' \| 'none' \| 'host' }` | no | Per-job container sandbox escape hatch (container jobs only). Request extra Linux capabilities / host networking; granted only within your operator's allow-list, else the run fails at dispatch. See below. |
|
|
2175
|
+
| `options.context` | `string \| ((event) => string \| Promise<string>)` | no | Bound context for this job -- the secret / variable scope it resolves against. Static string or async/dynamic function -- see [Contexts](https://docs.kici.dev/user/contexts/) and [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
|
|
2176
|
+
| `options.contexts` | `(string \| ((event) => string \| Promise<string>))[]` | no | Bound contexts in merge order (later entries override earlier on name collisions). Mutually exclusive with `context`. |
|
|
2177
|
+
| `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](https://docs.kici.dev/user/dynamic-values/). |
|
|
2178
|
+
| `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name. Defaults to the first bound context's name -- see [Concurrency](https://docs.kici.dev/user/concurrency/). |
|
|
2179
|
+
| `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
|
|
2180
|
+
| `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
|
|
2181
|
+
| `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
|
|
2182
|
+
| `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
|
|
2183
|
+
| `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
|
|
2184
|
+
| `options.afterStep` | `HookInput` | no | Hook that runs after each step |
|
|
2185
|
+
| `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](https://docs.kici.dev/user/hooks/#hook-timeout). |
|
|
2186
|
+
| `options.timeout` | `number` | no | Total job wall-clock timeout in milliseconds (init + all steps + hooks). On breach the job is aborted and reported timed out. See [Timeouts](https://docs.kici.dev/user/sdk/core/#timeouts). |
|
|
2187
|
+
| `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](https://docs.kici.dev/user/sdk/core/#per-job-resources) below. |
|
|
2188
|
+
| `options.init` | `InitConfig` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. A generic config, a typed preset (`'mise'` / `{ mise }`), `'auto'`, or `false`. See [Per-job init](https://docs.kici.dev/user/sdk/core/#per-job-init) below. |
|
|
2189
|
+
| `options.cache` | `CacheInput` | no | Declarative cache: restored before steps, saved after the job on a key miss. See [Caching](https://docs.kici.dev/user/sdk/caching/). |
|
|
2190
|
+
| `options.approval` | `ApprovalConfig` | no | Pause for a manual human approval before this job dispatches. See [Approval gates](https://docs.kici.dev/user/approvals/). |
|
|
2177
2191
|
|
|
2178
2192
|
**Returns:** `Job` -- an immutable job definition.
|
|
2179
2193
|
|
|
@@ -3089,13 +3103,15 @@ type RuleCheckFn = (ctx: RuleContext) => Promise<boolean> | boolean;
|
|
|
3089
3103
|
|
|
3090
3104
|
Can be sync or async. Receives a `RuleContext`:
|
|
3091
3105
|
|
|
3092
|
-
| Property | Type | Description
|
|
3093
|
-
| -------------------- | ----------------------------------------- |
|
|
3094
|
-
| `event` | `EventPayload` | The triggering event payload (discriminated union — narrow on `type`)
|
|
3095
|
-
| `changedFiles` | `string[]` | Files changed in this event (see availability note below)
|
|
3096
|
-
| `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` is available
|
|
3097
|
-
| `
|
|
3098
|
-
|
|
|
3106
|
+
| Property | Type | Description |
|
|
3107
|
+
| -------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
|
|
3108
|
+
| `event` | `EventPayload` | The triggering event payload (discriminated union — narrow on `type`) |
|
|
3109
|
+
| `changedFiles` | `string[]` | Files changed in this event (see availability note below) |
|
|
3110
|
+
| `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` is available |
|
|
3111
|
+
| `sourceRepo` | `RepoInfo \| undefined` | Repo whose event triggered the run, when the evaluation has a checkout |
|
|
3112
|
+
| `workflowRepo` | `RepoInfo \| undefined` | Repo that registered the workflow (same repo outside a global workflow) |
|
|
3113
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
3114
|
+
| `$` | zx shell | Shell executor for running commands |
|
|
3099
3115
|
|
|
3100
3116
|
`changedFiles` is available on `push` and `pull_request` events — the agent computes the diff from its checkout, so no `paths:` trigger is required. It is `unavailable` for events with no diff (`schedule`, `tag`, `manual_schedule`), and in the rare case where the diff cannot be computed (e.g. a history deeper than the agent's bounded fetch). Reading `changedFiles` when it is unavailable throws and fails the job, so guard with `changedFilesStatus` first when a rule can run on such events:
|
|
3101
3117
|
|
|
@@ -3106,6 +3122,8 @@ rule('has source changes', (ctx) => {
|
|
|
3106
3122
|
});
|
|
3107
3123
|
```
|
|
3108
3124
|
|
|
3125
|
+
The throw is a `ChangedFilesUnavailableError` (exported from `@kici-dev/sdk`, carrying the `changedFilesStatus` and `eventType` that produced it). `evaluateRules()` deliberately re-throws it rather than folding it into a `passed=false` skip, so a path-based gate fails loudly instead of silently mis-evaluating.
|
|
3126
|
+
|
|
3109
3127
|
### evaluateRules(rules, context, label, onRuleResult?)
|
|
3110
3128
|
|
|
3111
3129
|
Evaluate an array of rules sequentially with fail-fast behavior. Stops on the first failure.
|
|
@@ -3378,12 +3396,18 @@ type DynamicJobFn = (context: DynamicJobContext) => Promise<Job[]>;
|
|
|
3378
3396
|
|
|
3379
3397
|
Receives a `DynamicJobContext`:
|
|
3380
3398
|
|
|
3381
|
-
| Property
|
|
3382
|
-
|
|
|
3383
|
-
| `$`
|
|
3384
|
-
| `ctx`
|
|
3385
|
-
| `log`
|
|
3386
|
-
| `env`
|
|
3399
|
+
| Property | Type | Description |
|
|
3400
|
+
| -------------- | ----------------------------------- | ----------------------------------------------------------------------- |
|
|
3401
|
+
| `$` | zx shell | Shell executor |
|
|
3402
|
+
| `ctx` | `{ workflow, event? }` | Workflow metadata and event |
|
|
3403
|
+
| `log` | `Logger` | Structured logger |
|
|
3404
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables |
|
|
3405
|
+
| `sourceRepo` | `RepoInfo \| undefined` | Repo whose event triggered the run, when the evaluation has a checkout |
|
|
3406
|
+
| `workflowRepo` | `RepoInfo \| undefined` | Repo that registered the workflow (same repo outside a global workflow) |
|
|
3407
|
+
|
|
3408
|
+
`RepoInfo` carries `path` — an absolute path to that repo's checkout — plus optional `ref` and `sha`; guard before reading either, since an event that carries no single ref leaves them undefined. In a [global workflow](https://docs.kici.dev/user/global-workflows/) `sourceRepo` and `workflowRepo` are different repos, so one generator can produce a different job set per source repo.
|
|
3409
|
+
|
|
3410
|
+
**`sourceRepo.path` is not stable across calls.** A generator is invoked once to discover the job set and again to extract the step closures of the job being run; both see the same tree at the same commit, but not necessarily the same path or even the same machine. Read _through_ it, and derive job names from what the tree contains — never from the path itself, or the second call produces different names and the run fails the determinism check.
|
|
3387
3411
|
|
|
3388
3412
|
```typescript
|
|
3389
3413
|
const discoverJobs: DynamicJobFn = async ({ $ }) => {
|
|
@@ -3576,6 +3600,182 @@ workflow runs rather than being silently dropped, and the delivery is recorded
|
|
|
3576
3600
|
as degraded. GitHub always provides an exact list; a transient API error fails
|
|
3577
3601
|
loudly, not as empty.
|
|
3578
3602
|
|
|
3603
|
+
### Content requirements (`requires`)
|
|
3604
|
+
|
|
3605
|
+
Where `paths` filters on **which files changed**, `requires` filters on **what
|
|
3606
|
+
those files contain**. It is a declarative filter on the `pr()`, `push()`, and
|
|
3607
|
+
`tag()` triggers: a list of queries over the bytes of named source files, read
|
|
3608
|
+
at the event's commit. The orchestrator evaluates it as pure data before
|
|
3609
|
+
dispatching — it reads only the referenced files, never clones the whole
|
|
3610
|
+
repository, and never runs any of your workflow code. A workflow whose `requires`
|
|
3611
|
+
does not pass is simply not dispatched.
|
|
3612
|
+
|
|
3613
|
+
Each entry is a `ContentRequirement`:
|
|
3614
|
+
|
|
3615
|
+
```typescript
|
|
3616
|
+
interface ContentRequirement {
|
|
3617
|
+
file: string; // repo-relative path to query
|
|
3618
|
+
format?: 'auto' | 'json' | 'yaml' | 'text'; // how to parse the file (default: 'auto')
|
|
3619
|
+
exists?: string[]; // JSONPath expressions that must each resolve to ≥1 node (json/yaml)
|
|
3620
|
+
match?: Record<string, unknown>; // JSONPath → expected value; every one must match (json/yaml)
|
|
3621
|
+
not?: Record<string, unknown>; // JSONPath → value; passes only when NONE match (json/yaml)
|
|
3622
|
+
contains?: string | string[]; // literal substrings, all of which must appear (text only)
|
|
3623
|
+
notContains?: string | string[]; // literal substrings, none of which may appear (text only)
|
|
3624
|
+
matches?: string | RegExp | (string | RegExp)[]; // regexes, all of which must match (text only)
|
|
3625
|
+
notMatches?: string | RegExp | (string | RegExp)[]; // regexes, none of which may match (text only)
|
|
3626
|
+
ignoreCase?: boolean; // applies to contains/notContains only (default: false)
|
|
3627
|
+
absent?: boolean; // passes only when the file does NOT exist
|
|
3628
|
+
}
|
|
3629
|
+
```
|
|
3630
|
+
|
|
3631
|
+
**Format.** `format: 'auto'` (the default) picks the parser by extension:
|
|
3632
|
+
`.json` → JSON, `.yaml` / `.yml` → YAML, everything else → text. Set `format`
|
|
3633
|
+
explicitly to override — e.g. treat an extensionless file as JSON, or read a
|
|
3634
|
+
`.json` file as raw text. JSON and YAML both parse to an object, so the JSONPath
|
|
3635
|
+
keys (`exists` / `match` / `not`) work identically over either; `text` files are
|
|
3636
|
+
queried by `contains`, `notContains`, `matches`, and `notMatches` over the raw
|
|
3637
|
+
bytes.
|
|
3638
|
+
|
|
3639
|
+
**Query keys.**
|
|
3640
|
+
|
|
3641
|
+
- **`exists`** — an array of JSONPath expressions; each must resolve to at least
|
|
3642
|
+
one node in the parsed document.
|
|
3643
|
+
- **`match`** — a JSONPath → expected-value map; every expression must match. An
|
|
3644
|
+
expected value is an exact value, a regex string in `/pattern/flags` form
|
|
3645
|
+
(against a string node), or an array of acceptable values (any one matches).
|
|
3646
|
+
- **`not`** — the same map shape, inverted: the entry passes only when **none** of
|
|
3647
|
+
the expressions match.
|
|
3648
|
+
- **`contains` / `notContains`** — literal substrings tested against the raw file
|
|
3649
|
+
text. Every entry must be present (`contains`) or absent (`notContains`). No
|
|
3650
|
+
escaping needed (text format only).
|
|
3651
|
+
- **`matches`** — one or several regexes (a `RegExp` or `/pattern/flags` string),
|
|
3652
|
+
each of which must match the raw file text (text format only).
|
|
3653
|
+
- **`notMatches`** — the inverse of `matches`: the entry passes only when none of
|
|
3654
|
+
the regexes match (text format only).
|
|
3655
|
+
- **`ignoreCase`** — case-insensitive `contains` / `notContains` only; a regex
|
|
3656
|
+
carries its own flags. Default false.
|
|
3657
|
+
- **`absent: true`** — passes only when the file does **not** exist at the event's
|
|
3658
|
+
commit. It is mutually exclusive with the query keys above.
|
|
3659
|
+
- A bare `{ file }` with no query key requires the file to **exist**.
|
|
3660
|
+
|
|
3661
|
+
The keys inside one entry are AND-ed, and the entries in a `requires` list are
|
|
3662
|
+
AND-ed with each other. An empty or absent `requires` matches everything, exactly
|
|
3663
|
+
like `paths`.
|
|
3664
|
+
|
|
3665
|
+
**Examples:**
|
|
3666
|
+
|
|
3667
|
+
```typescript
|
|
3668
|
+
// Only run CI when package.json declares a `ci` script.
|
|
3669
|
+
push({ branches: 'main', requires: [{ file: 'package.json', exists: ['$.scripts.ci'] }] });
|
|
3670
|
+
|
|
3671
|
+
// Deploy only when the service config enables it (YAML, matched by value).
|
|
3672
|
+
push({
|
|
3673
|
+
branches: 'main',
|
|
3674
|
+
requires: [{ file: 'service.yaml', match: { '$.deploy.enabled': true } }],
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3677
|
+
// Only run when the Dockerfile builds from a Node base image (raw-text regex).
|
|
3678
|
+
pr({ requires: [{ file: 'Dockerfile', format: 'text', matches: '/^FROM node:/m' }] });
|
|
3679
|
+
|
|
3680
|
+
// Skip the workflow whenever a repo carries an opt-out marker file.
|
|
3681
|
+
push({ requires: [{ file: '.skip-ci', absent: true }] });
|
|
3682
|
+
|
|
3683
|
+
// Combine filters: a tag build that requires a version file AND forbids a draft flag.
|
|
3684
|
+
tag({
|
|
3685
|
+
patterns: ['v*'],
|
|
3686
|
+
requires: [
|
|
3687
|
+
{ file: 'VERSION', matches: '/^\\d+\\.\\d+\\.\\d+$/' },
|
|
3688
|
+
{ file: 'release.json', not: { '$.draft': true } },
|
|
3689
|
+
],
|
|
3690
|
+
});
|
|
3691
|
+
```
|
|
3692
|
+
|
|
3693
|
+
**Fail-visible evaluation.** Files are read at the event's commit. If a
|
|
3694
|
+
referenced file is larger than **1 MiB**, or fails to parse for its format, the
|
|
3695
|
+
requirement is **indeterminate** — the candidate workflow is dropped and does
|
|
3696
|
+
**not** run. A `requires` that cannot be evaluated never silently passes.
|
|
3697
|
+
|
|
3698
|
+
**Compile-time validation.** `kici compile` rejects a malformed requirement before
|
|
3699
|
+
it ever reaches the orchestrator: a raw-text key that is invalid or catastrophic
|
|
3700
|
+
(ReDoS-prone) is rejected by a safe-regex check; a text file cannot carry a
|
|
3701
|
+
JSON/YAML query key (`exists` / `match` / `not`) and a JSON/YAML file cannot carry a
|
|
3702
|
+
raw-text key; `absent` cannot be combined with a query key; and an explicit
|
|
3703
|
+
`format` with no query key is rejected as having nothing to check.
|
|
3704
|
+
|
|
3705
|
+
### Commit-message filters (`commitMessage`)
|
|
3706
|
+
|
|
3707
|
+
Where `requires` filters on what the repository's **files** contain,
|
|
3708
|
+
`commitMessage` filters on what the **event** says. It is a declarative filter on
|
|
3709
|
+
the `pr()`, `push()`, and `tag()` triggers. The orchestrator evaluates it
|
|
3710
|
+
directly from the webhook payload: no file is fetched, and no repository is
|
|
3711
|
+
cloned. For an organization-wide workflow it dispatches no evaluation job. It is
|
|
3712
|
+
the cheapest gate available.
|
|
3713
|
+
|
|
3714
|
+
The text it tests is the **full head-commit message** — subject and body — for
|
|
3715
|
+
`push` and `tag`, and the **title plus body** for pull-request events.
|
|
3716
|
+
|
|
3717
|
+
```typescript
|
|
3718
|
+
interface TextMatch {
|
|
3719
|
+
contains?: string | string[]; // every needle must be present
|
|
3720
|
+
notContains?: string | string[]; // no needle may be present
|
|
3721
|
+
matches?: string | RegExp | (string | RegExp)[]; // every regex must match
|
|
3722
|
+
notMatches?: string | RegExp | (string | RegExp)[]; // no regex may match
|
|
3723
|
+
ignoreCase?: boolean; // applies to contains/notContains only (default: false)
|
|
3724
|
+
}
|
|
3725
|
+
```
|
|
3726
|
+
|
|
3727
|
+
**Every entry in a list is a conjunct.** `contains: ['a', 'b']` passes only when
|
|
3728
|
+
the text contains both, and the keys AND together. To express OR, declare two
|
|
3729
|
+
triggers — a workflow's trigger list already matches on the first one that fits:
|
|
3730
|
+
|
|
3731
|
+
```typescript
|
|
3732
|
+
// AND — one trigger.
|
|
3733
|
+
push({ commitMessage: { contains: ['release:', 'approved'] } });
|
|
3734
|
+
|
|
3735
|
+
// OR — two triggers.
|
|
3736
|
+
on: [
|
|
3737
|
+
push({ branches: 'main', commitMessage: { contains: 'deploy:' } }),
|
|
3738
|
+
push({ branches: 'main', commitMessage: { contains: 'release:' } }),
|
|
3739
|
+
];
|
|
3740
|
+
```
|
|
3741
|
+
|
|
3742
|
+
Needles are **literal substrings** — no glob, no regex, no escaping, so a needle
|
|
3743
|
+
containing `.*` matches only the literal `.*`. Use `matches` / `notMatches` for a
|
|
3744
|
+
pattern; both accept a `RegExp` literal, and the `m` flag reaches the body:
|
|
3745
|
+
|
|
3746
|
+
```typescript
|
|
3747
|
+
// The single most common use: skip marker commits.
|
|
3748
|
+
push({ branches: 'main', commitMessage: { notContains: ['[skip ci]', '[ci skip]'] } });
|
|
3749
|
+
|
|
3750
|
+
// Ignore dependency-bump noise across an organization.
|
|
3751
|
+
push({ commitMessage: { notMatches: /^chore\(deps\):/ } });
|
|
3752
|
+
|
|
3753
|
+
// Require a conventional-commit prefix and forbid a WIP marker.
|
|
3754
|
+
pr({ target: 'main', commitMessage: { matches: /^(feat|fix)\(/, notContains: 'WIP' } });
|
|
3755
|
+
|
|
3756
|
+
// Match a trailer in the commit body.
|
|
3757
|
+
push({ commitMessage: { matches: /^Fixes: #\d+$/m } });
|
|
3758
|
+
```
|
|
3759
|
+
|
|
3760
|
+
`ignoreCase` affects `contains` and `notContains` only — a regex already carries
|
|
3761
|
+
its own flags, so write `/^feat:/i` rather than expecting `ignoreCase` to reach
|
|
3762
|
+
it.
|
|
3763
|
+
|
|
3764
|
+
**Fail-visible evaluation.** Some events carry no message at all. A
|
|
3765
|
+
branch-deletion push has no head commit, and a self-hosted forge may publish
|
|
3766
|
+
none. The trigger then does **not** match, and the decision trace records it as
|
|
3767
|
+
`indeterminate` rather than as an exclusion. A `commitMessage` filter that cannot
|
|
3768
|
+
be evaluated never silently passes.
|
|
3769
|
+
|
|
3770
|
+
**Compile-time validation.** `kici compile` rejects a malformed matcher. It
|
|
3771
|
+
refuses:
|
|
3772
|
+
|
|
3773
|
+
- a matcher with no query key;
|
|
3774
|
+
- an `ignoreCase` that would affect nothing;
|
|
3775
|
+
- an empty needle list;
|
|
3776
|
+
- an empty-string needle (it would match every text);
|
|
3777
|
+
- a regex that is invalid or catastrophic (ReDoS-prone).
|
|
3778
|
+
|
|
3579
3779
|
### tag()
|
|
3580
3780
|
|
|
3581
3781
|
Create a tag trigger. Returns a frozen `TagTriggerConfig`.
|
|
@@ -5499,7 +5699,7 @@ When `--target` narrows a `runsOnAll` job to zero hosts, the run **fails** by de
|
|
|
5499
5699
|
(a mistyped selector should be loud, not silently no-op). Pass `--target-allow-empty`
|
|
5500
5700
|
to **skip** the zeroed job instead — it records a `skipped` status, and downstream jobs
|
|
5501
5701
|
gated with `when: 'on-skip'` (or `when: 'always'`) still run, exactly as for an
|
|
5502
|
-
`onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](https://docs.kici.dev/user/cli-
|
|
5702
|
+
`onUnreachable: 'skip'` zero-host fan-out. See the [CLI reference](https://docs.kici.dev/user/cli/runs-and-approvals/#host-narrowing-with---target)
|
|
5503
5703
|
for the full flag behavior and the [`needs` gating model](https://docs.kici.dev/user/sdk/core/#job-dependencies-needs)
|
|
5504
5704
|
for how a skipped upstream propagates.
|
|
5505
5705
|
|
|
@@ -5543,41 +5743,42 @@ All types are exported from `@kici-dev/sdk` as type-only imports.
|
|
|
5543
5743
|
|
|
5544
5744
|
### Trigger types
|
|
5545
5745
|
|
|
5546
|
-
| Type
|
|
5547
|
-
|
|
|
5548
|
-
| `Trigger`
|
|
5549
|
-
| `TriggerConfig`
|
|
5550
|
-
| `PrTriggerConfig`
|
|
5551
|
-
| `PushTriggerConfig`
|
|
5552
|
-
| `TagTriggerConfig`
|
|
5553
|
-
| `CommentTriggerConfig`
|
|
5554
|
-
| `ReviewTriggerConfig`
|
|
5555
|
-
| `ReviewCommentTriggerConfig`
|
|
5556
|
-
| `ReleaseTriggerConfig`
|
|
5557
|
-
| `DispatchTriggerConfig`
|
|
5558
|
-
| `CreateTriggerConfig`
|
|
5559
|
-
| `DeleteTriggerConfig`
|
|
5560
|
-
| `StatusTriggerConfig`
|
|
5561
|
-
| `WorkflowRunTriggerConfig`
|
|
5562
|
-
| `ForkTriggerConfig`
|
|
5563
|
-
| `StarTriggerConfig`
|
|
5564
|
-
| `WatchTriggerConfig`
|
|
5565
|
-
| `WebhookTriggerConfig`
|
|
5566
|
-
| `KiciEventTriggerConfig`
|
|
5567
|
-
| `WorkflowCompleteTriggerConfig`
|
|
5568
|
-
| `
|
|
5569
|
-
| `
|
|
5570
|
-
| `
|
|
5571
|
-
| `
|
|
5572
|
-
| `
|
|
5573
|
-
| `
|
|
5574
|
-
| `
|
|
5575
|
-
| `
|
|
5576
|
-
| `
|
|
5577
|
-
| `
|
|
5578
|
-
| `
|
|
5579
|
-
| `
|
|
5580
|
-
| `
|
|
5746
|
+
| Type | Description |
|
|
5747
|
+
| ----------------------------------- | ------------------------------------------------------------------------------ |
|
|
5748
|
+
| `Trigger` | Trigger definition (trigger config + source location) |
|
|
5749
|
+
| `TriggerConfig` | Union of all 23 trigger config types |
|
|
5750
|
+
| `PrTriggerConfig` | PR trigger configuration (from `pr()`) |
|
|
5751
|
+
| `PushTriggerConfig` | Push trigger configuration (from `push()`) |
|
|
5752
|
+
| `TagTriggerConfig` | Tag trigger configuration (from `tag()`) |
|
|
5753
|
+
| `CommentTriggerConfig` | Comment trigger configuration (from `comment()`) |
|
|
5754
|
+
| `ReviewTriggerConfig` | Review trigger configuration (from `review()`) |
|
|
5755
|
+
| `ReviewCommentTriggerConfig` | Review comment trigger configuration (from `reviewComment()`) |
|
|
5756
|
+
| `ReleaseTriggerConfig` | Release trigger configuration (from `release()`) |
|
|
5757
|
+
| `DispatchTriggerConfig` | Repository dispatch trigger configuration (from `dispatch()`) |
|
|
5758
|
+
| `CreateTriggerConfig` | Ref creation trigger configuration (from `create()`) |
|
|
5759
|
+
| `DeleteTriggerConfig` | Ref deletion trigger configuration (from `delete()`) |
|
|
5760
|
+
| `StatusTriggerConfig` | Commit status trigger configuration (from `status()`) |
|
|
5761
|
+
| `WorkflowRunTriggerConfig` | Workflow run trigger configuration (from `workflowRun()`) |
|
|
5762
|
+
| `ForkTriggerConfig` | Fork trigger configuration (from `fork()`) |
|
|
5763
|
+
| `StarTriggerConfig` | Star trigger configuration (from `star()`) |
|
|
5764
|
+
| `WatchTriggerConfig` | Watch trigger configuration (from `watch()`) |
|
|
5765
|
+
| `WebhookTriggerConfig` | Catch-all webhook trigger configuration (from `webhook()`) |
|
|
5766
|
+
| `KiciEventTriggerConfig` | Custom event trigger configuration (from `kiciEvent()`) |
|
|
5767
|
+
| `WorkflowCompleteTriggerConfig` | Workflow completion trigger configuration (from `workflowComplete()`) |
|
|
5768
|
+
| `WorkflowsFailedBatchTriggerConfig` | Batched workflow-failure trigger configuration (from `workflowsFailedBatch()`) |
|
|
5769
|
+
| `JobCompleteTriggerConfig` | Job completion trigger configuration (from `jobComplete()`) |
|
|
5770
|
+
| `GenericWebhookTriggerConfig` | Generic webhook trigger configuration (from `genericWebhook()`) |
|
|
5771
|
+
| `ScheduleTriggerConfig` | Schedule trigger configuration (from `schedule()`) |
|
|
5772
|
+
| `LifecycleTriggerConfig` | Lifecycle trigger configuration (from `lifecycle()`) |
|
|
5773
|
+
| `PrConfigInput` | Config object for `pr()` factory |
|
|
5774
|
+
| `PushConfigInput` | Config object for `push()` factory |
|
|
5775
|
+
| `BranchPattern` | `{ type: 'glob', pattern } \| { type: 'regex', pattern, flags? }` |
|
|
5776
|
+
| `PrEvent` | PR event string literal union (17 event types) |
|
|
5777
|
+
| `GenericWebhookConfigInput` | Config object for `genericWebhook()` factory |
|
|
5778
|
+
| `GenericWebhookAuth` | Union of generic webhook auth types (HMAC or API key) |
|
|
5779
|
+
| `GenericWebhookHmacAuth` | HMAC-SHA256 auth configuration for generic webhooks |
|
|
5780
|
+
| `GenericWebhookApiKeyAuth` | API key auth configuration for generic webhooks |
|
|
5781
|
+
| `GenericWebhookAuthMethod` | Auth method string literal (`'hmac-sha256' \| 'api-key'`) |
|
|
5581
5782
|
|
|
5582
5783
|
### Rule types
|
|
5583
5784
|
|
|
@@ -7083,12 +7284,13 @@ companion at [Operator troubleshooting](https://docs.kici.dev/operator/troublesh
|
|
|
7083
7284
|
|
|
7084
7285
|
## Fast triage
|
|
7085
7286
|
|
|
7086
|
-
| You see... | Jump to
|
|
7087
|
-
| -------------------------------------------------------------- |
|
|
7088
|
-
| A run finishes with `No jobs dispatched` | [No jobs dispatched](https://docs.kici.dev/user/common-failures/#no-jobs-dispatched)
|
|
7089
|
-
| A run fails complaining the lock file is stale or incompatible | [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift)
|
|
7090
|
-
| You pushed but no run ever appears | [The webhook never arrives](https://docs.kici.dev/user/common-failures/#the-webhook-never-arrives)
|
|
7091
|
-
| A run is stuck "queued" and no agent ever picks it up | [The agent won't connect](https://docs.kici.dev/user/common-failures/#the-agent-wont-connect)
|
|
7287
|
+
| You see... | Jump to |
|
|
7288
|
+
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
7289
|
+
| A run finishes with `No jobs dispatched` | [No jobs dispatched](https://docs.kici.dev/user/common-failures/#no-jobs-dispatched) |
|
|
7290
|
+
| A run fails complaining the lock file is stale or incompatible | [Lock-file drift](https://docs.kici.dev/user/common-failures/#lock-file-drift) |
|
|
7291
|
+
| You pushed but no run ever appears | [The webhook never arrives](https://docs.kici.dev/user/common-failures/#the-webhook-never-arrives) |
|
|
7292
|
+
| A run is stuck "queued" and no agent ever picks it up | [The agent won't connect](https://docs.kici.dev/user/common-failures/#the-agent-wont-connect) |
|
|
7293
|
+
| A `commitMessage`-gated workflow stops running for some events | [A `commitMessage` filter never evaluates](https://docs.kici.dev/user/common-failures/#a-commitmessage-filter-never-evaluates) |
|
|
7092
7294
|
|
|
7093
7295
|
## No jobs dispatched
|
|
7094
7296
|
|
|
@@ -7228,6 +7430,24 @@ the unpullable image) — hand this to your operator with the run's failure reas
|
|
|
7228
7430
|
The full provisioning-failure playbook is in
|
|
7229
7431
|
[Operator troubleshooting](https://docs.kici.dev/operator/troubleshooting/).
|
|
7230
7432
|
|
|
7433
|
+
## A `commitMessage` filter never evaluates
|
|
7434
|
+
|
|
7435
|
+
**Symptom.** A workflow gated on a `commitMessage` trigger filter stops running
|
|
7436
|
+
for some events, even though the message looks like it should match.
|
|
7437
|
+
|
|
7438
|
+
**Cause.** The event carries no commit message. A branch-deletion push has no
|
|
7439
|
+
head commit, and a self-hosted forge (Gogs, or a GitLab source) may publish none
|
|
7440
|
+
at the configured path. The filter is **fail-visible**: when it cannot read a
|
|
7441
|
+
message, the workflow does not run rather than running ungated.
|
|
7442
|
+
|
|
7443
|
+
**Diagnose.** The decision trace records the `commitMessage` check with the
|
|
7444
|
+
verdict `indeterminate` and the reason `no commit message in payload`. That is
|
|
7445
|
+
distinct from an `excluded` verdict, which the message itself caused.
|
|
7446
|
+
|
|
7447
|
+
**Fix.** For a self-hosted forge, set the source's `commitMessage` payload path so
|
|
7448
|
+
the orchestrator can read the head commit's message. A branch-deletion push
|
|
7449
|
+
genuinely carries no message and is expected not to match.
|
|
7450
|
+
|
|
7231
7451
|
## When to escalate to your operator
|
|
7232
7452
|
|
|
7233
7453
|
The failures above are ones you can resolve from your workflow repo and the
|
|
@@ -7521,6 +7741,7 @@ Each workflow entry includes:
|
|
|
7521
7741
|
| `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](https://docs.kici.dev/user/concurrency/). |
|
|
7522
7742
|
| `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
|
|
7523
7743
|
| `approval` | Normalized approval gate (optional): `clauses`, `reason`, `timeoutSeconds`, `when`. When present the whole run is held before any job is dispatched. Job and step entries carry the same normalized block for job- and step-level gates. See [approval gates](https://docs.kici.dev/user/approvals/). |
|
|
7744
|
+
| `hasFilter` | `true` when the workflow declares a workflow-level `filter` predicate (optional; omitted rather than `false`). The predicate itself is never serialized — the flag tells the orchestrator an agent must evaluate the workflow before any of its jobs is dispatched. See [global workflows](https://docs.kici.dev/user/global-workflows/#narrowing-with-a-filter). |
|
|
7524
7745
|
| Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
|
|
7525
7746
|
|
|
7526
7747
|
Step entries carry their own capability flags, so the orchestrator can reason about a step without loading your TypeScript:
|
|
@@ -10623,7 +10844,7 @@ group: () => 'deploy';
|
|
|
10623
10844
|
group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
|
|
10624
10845
|
```
|
|
10625
10846
|
|
|
10626
|
-
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.
|
|
10847
|
+
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.
|
|
10627
10848
|
|
|
10628
10849
|
## cancelInProgress mode
|
|
10629
10850
|
|
|
@@ -10860,7 +11081,7 @@ job('deploy-review', {
|
|
|
10860
11081
|
});
|
|
10861
11082
|
```
|
|
10862
11083
|
|
|
10863
|
-
A
|
|
11084
|
+
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.
|
|
10864
11085
|
|
|
10865
11086
|
### Multiple contexts per job
|
|
10866
11087
|
|
|
@@ -11163,22 +11384,17 @@ job('deploy', {
|
|
|
11163
11384
|
|
|
11164
11385
|
## How it works
|
|
11165
11386
|
|
|
11166
|
-
When you define a dynamic value as a function,
|
|
11387
|
+
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:
|
|
11167
11388
|
|
|
11168
|
-
|
|
11389
|
+
1. The orchestrator dispatches a lightweight `__init__` job to an agent.
|
|
11390
|
+
2. The agent loads the compiled workflow bundle and calls your function with the normalized event.
|
|
11391
|
+
3. The agent reports the resolved values back to the orchestrator, which dispatches the real execution job with them applied.
|
|
11169
11392
|
|
|
11170
|
-
|
|
11393
|
+
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.
|
|
11171
11394
|
|
|
11172
|
-
|
|
11173
|
-
- Only references its parameters and local variables
|
|
11174
|
-
- Does not import or require external modules
|
|
11175
|
-
- Does not access globals like `process`, `fetch`, `console`, `setTimeout`, etc.
|
|
11176
|
-
- Uses only safe built-in constructors: `String`, `Number`, `Boolean`, `Array`, `Object`, `JSON`, `Math`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI`
|
|
11177
|
-
- Does not use `this`, `new`, `class`, `throw`, `try`/`catch`, `delete`, `var`, `yield`, or mutation operators (`++`, `--`, `+=`, etc.)
|
|
11395
|
+
`kici preview` lists the injected `__init__` job under each affected job, so you can spot it before the first run.
|
|
11178
11396
|
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
**Examples of pure functions:**
|
|
11397
|
+
**Examples:**
|
|
11182
11398
|
|
|
11183
11399
|
```typescript
|
|
11184
11400
|
// Simple branch extraction
|
|
@@ -11190,65 +11406,32 @@ env: (event) => ({ BRANCH: event.targetBranch });
|
|
|
11190
11406
|
// Concatenation with event data
|
|
11191
11407
|
concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
|
|
11192
11408
|
|
|
11193
|
-
//
|
|
11194
|
-
env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
|
|
11195
|
-
|
|
11196
|
-
// Local variables are fine
|
|
11409
|
+
// Local variables and safe globals
|
|
11197
11410
|
context: (event) => {
|
|
11198
11411
|
const parts = event.targetBranch.split('/');
|
|
11199
11412
|
return parts[parts.length - 1];
|
|
11200
11413
|
};
|
|
11201
|
-
```
|
|
11202
|
-
|
|
11203
|
-
### Impure functions (init-job evaluation)
|
|
11204
|
-
|
|
11205
|
-
If the compiler determines a function is impure, it prints a `warning [W101]` naming the affected field (`context`, `env`, or `concurrencyGroup`), the reason the function was judged impure, and the ~5-10 second init-job cost — and compilation still succeeds. The function falls back to the two-phase init model. This means:
|
|
11206
11414
|
|
|
11207
|
-
|
|
11208
|
-
2. The builder agent clones the repository and evaluates the function
|
|
11209
|
-
3. The resolved values are sent back to the orchestrator
|
|
11210
|
-
4. The orchestrator dispatches the real execution job with the resolved values
|
|
11211
|
-
|
|
11212
|
-
This adds approximately 5-10 seconds of overhead for cloning and evaluation.
|
|
11213
|
-
|
|
11214
|
-
`kici preview` lists the injected `__init__` job under each affected job, so you can spot the init-job cost before the first run.
|
|
11215
|
-
|
|
11216
|
-
**Examples of impure functions (will use init job):**
|
|
11217
|
-
|
|
11218
|
-
```typescript
|
|
11219
|
-
// Async functions cannot be inlined
|
|
11415
|
+
// Async lookups, module access, and process/global reads all work
|
|
11220
11416
|
context: async (event) => await lookupEnv(event.targetBranch);
|
|
11221
|
-
|
|
11222
|
-
// External module references
|
|
11223
|
-
env: (event) => {
|
|
11224
|
-
const config = require('./config');
|
|
11225
|
-
return config.env;
|
|
11226
|
-
};
|
|
11227
|
-
|
|
11228
|
-
// Process/global access
|
|
11229
|
-
context: (event) => process.env.DEFAULT_ENV || 'staging';
|
|
11230
|
-
|
|
11231
|
-
// Dynamic imports
|
|
11232
|
-
env: async (event) => {
|
|
11233
|
-
const m = await import('./config.js');
|
|
11234
|
-
return m.default;
|
|
11235
|
-
};
|
|
11417
|
+
env: (event) => ({ DEFAULT_ENV: process.env.DEFAULT_ENV ?? 'staging' });
|
|
11236
11418
|
```
|
|
11237
11419
|
|
|
11238
|
-
## Performance
|
|
11420
|
+
## Performance
|
|
11239
11421
|
|
|
11240
|
-
|
|
|
11241
|
-
|
|
|
11242
|
-
| Static value
|
|
11243
|
-
|
|
|
11244
|
-
|
|
11422
|
+
| Value | Overhead | Example |
|
|
11423
|
+
| ------------------------ | --------- | ---------------------------------------- |
|
|
11424
|
+
| Static value | None | `context: 'staging'` |
|
|
11425
|
+
| Dynamic value (function) | Init step | `context: (event) => event.targetBranch` |
|
|
11426
|
+
|
|
11427
|
+
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.
|
|
11245
11428
|
|
|
11246
11429
|
## Tips
|
|
11247
11430
|
|
|
11248
|
-
- **
|
|
11249
|
-
- **
|
|
11250
|
-
- **
|
|
11251
|
-
- **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where
|
|
11431
|
+
- **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.
|
|
11432
|
+
- **Run `kici preview`** to see the injected `__init__` job listed under each affected job before your first run.
|
|
11433
|
+
- **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.
|
|
11434
|
+
- **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.
|
|
11252
11435
|
- **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`, …).
|
|
11253
11436
|
|
|
11254
11437
|
---
|
|
@@ -11785,35 +11968,151 @@ export default workflow('org-lint', {
|
|
|
11785
11968
|
});
|
|
11786
11969
|
```
|
|
11787
11970
|
|
|
11788
|
-
Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org.
|
|
11971
|
+
Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org, including one whose identifier starts with a dot (`.github/workflows-config`) — a repo identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own. Path globs in `paths:` keep the usual convention and do not match dot-prefixed files unless the pattern spells the dot out.
|
|
11789
11972
|
|
|
11790
11973
|
### At a dual-repo checkout
|
|
11791
11974
|
|
|
11792
|
-
The agent
|
|
11975
|
+
The agent checks out both repos. **Inside a step body**, `env` carries a pointer to each working tree:
|
|
11793
11976
|
|
|
11794
11977
|
| `env` var | Points to |
|
|
11795
11978
|
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11796
11979
|
| `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. |
|
|
11797
11980
|
| `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. |
|
|
11798
11981
|
|
|
11799
|
-
|
|
11982
|
+
Both variables are step-body context. They are **not** projected into the job's process environment, so a job-level `env:` block, a container image's entrypoint, or a shell command outside a step body will not see them. Outside a step body, use the `sourceRepo` / `workflowRepo` pair on the filter, generator, and rule contexts described below.
|
|
11983
|
+
|
|
11984
|
+
They are also set **only when there are two repos to point at**. An event from the workflow's own repo is matched from that repo's lock file rather than as a global candidate, so the workflow runs as an ordinary single-repo workflow: one checkout, and neither variable set. Read them with a fallback, as the example above does.
|
|
11985
|
+
|
|
11986
|
+
A global workflow's job runs with **no secrets at all** — neither the source repo's nor its own. See _Secrets are not available_ below.
|
|
11987
|
+
|
|
11988
|
+
### The triggering event
|
|
11989
|
+
|
|
11990
|
+
`ctx.event` inside a global workflow's job is the **source** repo's normalized event — the push or PR that fired the workflow, from a repo the workflow's own author may not own. `ctx.event.sourceRepo` names that repo.
|
|
11991
|
+
|
|
11992
|
+
That field is what makes a per-source-repo concurrency group expressible — and you have to write it. A global workflow runs on events from many repos, and their default branches share a name, so a group keyed on the branch alone puts every repo in one group, and with `cancelInProgress` (the default) one repo's push cancels another repo's in-flight run. That is still the behaviour of a branch-only group; naming the source repo in the key is what separates them:
|
|
11993
|
+
|
|
11994
|
+
```ts
|
|
11995
|
+
concurrency: {
|
|
11996
|
+
group: ({ branch, event }) => `${event.sourceRepo}:${branch}`,
|
|
11997
|
+
cancelInProgress: true,
|
|
11998
|
+
},
|
|
11999
|
+
```
|
|
12000
|
+
|
|
12001
|
+
### Narrowing to the repos that need it
|
|
12002
|
+
|
|
12003
|
+
A global workflow that matches `myorg/*` will, by default, run on every repo in the org. Three mechanisms narrow it to the repos it actually applies to, in increasing order of power:
|
|
12004
|
+
|
|
12005
|
+
1. **A `requires` content filter on the trigger** — the cheapest gate. The orchestrator checks a file's contents (a JSON-path probe over `package.json`, for example) and drops the workflow **before any agent is dispatched** when the condition is not met. See [`requires` on triggers](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires). This is provider-dependent — it needs a file-contents fetcher, which the GitHub provider supplies.
|
|
12006
|
+
2. **A workflow-level `filter` predicate** — arbitrary TypeScript over the checked-out source tree (below). Works with any provider that clones.
|
|
12007
|
+
3. **A `DynamicJobFn`** — generate the exact job set from the source repo's state ([Generating jobs per source repo](https://docs.kici.dev/user/global-workflows/#generating-jobs-per-source-repo) below).
|
|
12008
|
+
|
|
12009
|
+
### Narrowing with a filter
|
|
12010
|
+
|
|
12011
|
+
Before reaching for a `filter`, check whether a declarative filter answers the question. `commitMessage` (on the trigger) and `requires` (over source files) are evaluated by the orchestrator from data it already has, so they cost no evaluation job at all — while a `filter` predicate dispatches one per (event × workflow repo). Gating on a `[skip ci]` marker, a conventional-commit prefix, or the contents of a named config file needs no predicate.
|
|
12012
|
+
|
|
12013
|
+
A workflow can declare a `filter`: a predicate that decides whether the workflow applies to this event at all.
|
|
12014
|
+
|
|
12015
|
+
```ts
|
|
12016
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
12017
|
+
|
|
12018
|
+
export default workflow('org-container-lint', {
|
|
12019
|
+
on: [push({ repos: ['myorg/*'] })],
|
|
12020
|
+
filter: async ({ sourceRepo, changedFilesStatus, $ }) => {
|
|
12021
|
+
// `changedFiles` throws when the diff is unavailable, so guard first.
|
|
12022
|
+
if (changedFilesStatus !== 'fetched') return true;
|
|
12023
|
+
const found = await $`ls ${sourceRepo.path}`;
|
|
12024
|
+
return found.stdout.includes('Dockerfile');
|
|
12025
|
+
},
|
|
12026
|
+
jobs: [
|
|
12027
|
+
job('lint-dockerfile', {
|
|
12028
|
+
runsOn: ['kici:os:linux'],
|
|
12029
|
+
steps: [
|
|
12030
|
+
step('lint', async ({ $, env }) => $`hadolint ${env.KICI_SOURCE_REPO_PATH}/Dockerfile`),
|
|
12031
|
+
],
|
|
12032
|
+
}),
|
|
12033
|
+
],
|
|
12034
|
+
});
|
|
12035
|
+
```
|
|
12036
|
+
|
|
12037
|
+
The filter receives a `FilterContext`:
|
|
12038
|
+
|
|
12039
|
+
| Property | Type | Description |
|
|
12040
|
+
| -------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
12041
|
+
| `sourceRepo` | `RepoInfo` | The repo whose event triggered this evaluation, checked out on the evaluating agent. |
|
|
12042
|
+
| `workflowRepo` | `RepoInfo` | The repo that registered the workflow. Identical to `sourceRepo` for a same-repo workflow. |
|
|
12043
|
+
| `event` | `EventPayload` | The normalized event envelope. |
|
|
12044
|
+
| `changedFiles` | `string[]` | Files changed in this event. Throws when unavailable — guard with `changedFilesStatus`. |
|
|
12045
|
+
| `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` can be read. |
|
|
12046
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables. |
|
|
12047
|
+
| `$` | zx shell | Shell executor. |
|
|
12048
|
+
|
|
12049
|
+
`RepoInfo` carries `path` (an absolute path to the checkout on the evaluating agent) plus optional `ref` and `sha`. **Both are optional** — an event that carries no single ref leaves them undefined, so guard before reading them.
|
|
12050
|
+
|
|
12051
|
+
**`sourceRepo.path` is not stable across evaluations.** Its _contents_ are: the evaluating agent and the later run see the same tree at the same commit. The path itself is not — a different working directory, and possibly a different machine. Read _through_ it; never embed it in a job name, an output, or anything compared across calls.
|
|
12052
|
+
|
|
12053
|
+
**A `filter` must be pure and deterministic.** Decide from the context alone — the event, the changed files, and the checked-out tree — so the same event always yields the same verdict.
|
|
12054
|
+
|
|
12055
|
+
### Global and same-repo filters differ
|
|
12056
|
+
|
|
12057
|
+
The same `filter` keyword means two different things depending on whether the workflow is global:
|
|
12058
|
+
|
|
12059
|
+
| | Global workflow (`repos:` on a trigger) | Same-repo workflow |
|
|
12060
|
+
| ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
12061
|
+
| Evaluated | once per (event × workflow repo) | once per job that reaches dispatch, and once per job generator |
|
|
12062
|
+
| Evaluated relative to the run | **before** any run row exists | **after** the run row exists |
|
|
12063
|
+
| 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` |
|
|
12064
|
+
| `sourceRepo` vs `workflowRepo` | two different repos | the same repo |
|
|
12065
|
+
|
|
12066
|
+
Two consequences of the same-repo shape are worth designing for. A workflow with ten jobs calls its filter ten times for one event — each on its own agent with its own checkout and its own `$` — so anything the predicate does happens that many times: keep it cheap and side-effect free. And if the predicate can answer differently for the same event, the workflow will _partially_ dispatch, running some jobs and not others.
|
|
12067
|
+
|
|
12068
|
+
**A held or rejected job is not filtered at all.** A job held for approval, or rejected by a context rule, already has a gate — the hold or the rule — so it never takes a filter verdict, and an approved job dispatches without one. Concretely: a path filter cannot stop an approval request for a job the change does not concern.
|
|
12069
|
+
|
|
12070
|
+
### Generating jobs per source repo
|
|
12071
|
+
|
|
12072
|
+
A global workflow's job generators run in the same pre-run evaluation as the filter, with both repos on disk. `sourceRepo` and `workflowRepo` are on the generator context, so one workflow repo can produce a different job set per source repo:
|
|
12073
|
+
|
|
12074
|
+
```ts
|
|
12075
|
+
import { job, step, workflow, push, type DynamicJobFn } from '@kici-dev/sdk';
|
|
12076
|
+
import { readFile } from 'node:fs/promises';
|
|
12077
|
+
|
|
12078
|
+
const perRepoJobs: DynamicJobFn = async ({ sourceRepo }) => {
|
|
12079
|
+
if (!sourceRepo) return [];
|
|
12080
|
+
const pkg = JSON.parse(await readFile(`${sourceRepo.path}/package.json`, 'utf8'));
|
|
12081
|
+
return Object.keys(pkg.scripts ?? {})
|
|
12082
|
+
.filter((s) => s.startsWith('ci:'))
|
|
12083
|
+
.map((s) =>
|
|
12084
|
+
job(s.replace(':', '-'), {
|
|
12085
|
+
runsOn: ['kici:os:linux'],
|
|
12086
|
+
steps: [step('run', async ({ $ }) => $`pnpm ${s}`)],
|
|
12087
|
+
}),
|
|
12088
|
+
);
|
|
12089
|
+
};
|
|
12090
|
+
|
|
12091
|
+
export default workflow('org-ci', {
|
|
12092
|
+
on: [push({ repos: ['myorg/*'] })],
|
|
12093
|
+
jobs: [perRepoJobs],
|
|
12094
|
+
});
|
|
12095
|
+
```
|
|
12096
|
+
|
|
12097
|
+
The same `sourceRepo.path` caution applies: read the tree through it, and derive job names from the repo's _contents_, never from the path.
|
|
11800
12098
|
|
|
11801
12099
|
## Enabling global workflows
|
|
11802
12100
|
|
|
11803
|
-
Global workflows are **
|
|
12101
|
+
Global workflows are gated by a **fleet-wide master switch** held by the orchestrator operator, off by default. Until it is on, `repos:`-bearing workflows are registered but never dispatched.
|
|
11804
12102
|
|
|
11805
|
-
1.
|
|
11806
|
-
2.
|
|
11807
|
-
3. Decide which authoring/source controls you need:
|
|
12103
|
+
1. **The operator enables it cluster-wide** with `kici-admin cluster-settings set --global-workflows-enabled true`. This is the kill-switch — every per-org control below is ignored while it is off, and it cannot be flipped from the dashboard. The dashboard's **Settings → Global workflows** tab shows its current state as a read-only badge.
|
|
12104
|
+
2. In the dashboard → **Settings → Global workflows**, decide which authoring/source controls you need. These per-org lists stay dashboard-editable; an org that has set none means "no per-org restrictions", not a denial.
|
|
11808
12105
|
|
|
11809
|
-
| Setting | What it controls | Typical use
|
|
11810
|
-
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11811
|
-
| 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.
|
|
11812
|
-
| 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-*`.
|
|
11813
|
-
| Elevated access |
|
|
12106
|
+
| Setting | What it controls | Typical use |
|
|
12107
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
12108
|
+
| 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. |
|
|
12109
|
+
| 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-*`. |
|
|
12110
|
+
| 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. |
|
|
11814
12111
|
|
|
11815
12112
|
All three lists accept globs. Leading `!` inside a single pattern is not supported here; negation is via the list-is-implicit-deny semantics, so keep it simple (`myorg/ci-*`, `myorg/platform-*`).
|
|
11816
12113
|
|
|
12114
|
+
Patterns match repo identifiers by the same rule as `repos:` on a trigger: an identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own and a wildcard segment matches one. `myorg/*` covers `myorg/.github`, and `**` covers every repo in the org. Review any existing entry that relies on a wildcard to reach — or to spare — a dot-prefixed repo name.
|
|
12115
|
+
|
|
11817
12116
|
### Saving and reverting
|
|
11818
12117
|
|
|
11819
12118
|
The page is a two-state editor — changes are local until you click **Save changes**, and you can abandon them with **Discard changes**. There is no partial save; the PATCH is all-or-nothing per save click.
|
|
@@ -11825,17 +12124,23 @@ The page is a two-state editor — changes are local until you click **Save chan
|
|
|
11825
12124
|
A global workflow fires only if:
|
|
11826
12125
|
|
|
11827
12126
|
1. **The authoring repo is allowed.** If _Allowed author repos_ is ON, the workflow's authoring repo must match at least one allow-list glob. If OFF, any repo may author. Enforced at two points:
|
|
11828
|
-
- At registration time (extraction from the lock file — non-matching globals are dropped
|
|
12127
|
+
- At registration time (extraction from the lock file — non-matching globals are dropped, and the orchestrator logs `Global workflows excluded from registration` naming each one).
|
|
11829
12128
|
- At dispatch time (defense-in-depth — policy changes after registration still take effect).
|
|
11830
12129
|
2. **The source repo is not denied.** If the event's source repo matches any glob in _Blocked source repos_, the global workflow is skipped. Enforced at dispatch time.
|
|
11831
12130
|
|
|
11832
|
-
Both checks are logged to the orchestrator. Grep
|
|
12131
|
+
Both checks are logged to the orchestrator. Grep for `Global workflows excluded from registration` (registration time) and `Skipping global workflow dispatch` (dispatch time) to see enforcement in action.
|
|
12132
|
+
|
|
12133
|
+
Both checks read the settings of the organization the **event's source** resolves to. If no webhook source maps the event's routing key to an organization, the orchestrator resolves the built-in `__default__` organization anchor instead — and since nobody has enabled global workflows for that anchor, every global workflow is refused. The registration log line carries the organization it decided against plus the remedy, so this case is distinguishable from a real opt-in that is simply switched off. See the troubleshooting table below.
|
|
12134
|
+
|
|
12135
|
+
### Secrets are not available
|
|
11833
12136
|
|
|
11834
|
-
|
|
12137
|
+
A global workflow's job is dispatched with **no secret material** — not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is simply absent. Plan for it: a global workflow is for checks, policy and reporting that need only the two checkouts, not for deploys that need credentials.
|
|
11835
12138
|
|
|
11836
|
-
|
|
12139
|
+
This is about your **stored secrets**, not about repository access: the job is still handed a short-lived clone token for each repo it checks out, which is how the dual checkout works at all. What it does not get is anything from a secret context.
|
|
11837
12140
|
|
|
11838
|
-
|
|
12141
|
+
To run something that needs secrets on a source repo's event, put those jobs in a per-repository workflow in that repo, where the workflow's `contexts:` resolve normally.
|
|
12142
|
+
|
|
12143
|
+
The **Elevated access** setting reads as the way to lift this, and it is not: it is **deprecated and never consulted**. Nothing in the dispatch path reads the list, and adding a repo to it does not make any secret readable. It is kept only so an existing value stays visible and clearable, and is removed at the next major version — see [Deprecations](https://docs.kici.dev/user/deprecations/).
|
|
11839
12144
|
|
|
11840
12145
|
## When does it fire?
|
|
11841
12146
|
|
|
@@ -11843,14 +12148,111 @@ Same-repo globals (a workflow in `myorg/app` with `repos: ['myorg/app']`) fire o
|
|
|
11843
12148
|
|
|
11844
12149
|
Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workflowRun()`, etc. all accept `repos:`. `kiciEvent()` / `schedule()` / cron-like triggers have no source repo, so they're always per-org-registered regardless of `repos:`.
|
|
11845
12150
|
|
|
12151
|
+
A global workflow that declares a `filter` or a job generator is decided by one **evaluation job per (event × workflow repo)**, dispatched before any run exists. That job checks out both repos once and evaluates every candidate workflow from that repo, so ten global workflows in one CI repo cost one evaluation, not ten.
|
|
12152
|
+
|
|
12153
|
+
When that evaluation cannot reach a verdict — it fails, breaches its budget, or never reports — the workflows it was deciding on **do not run**. On a provider that supports commit checks, that posts a `failure` check named **`KiCI: Organization workflow evaluation`** on the source commit, so the outcome is visible instead of silent. Three things to know about it:
|
|
12154
|
+
|
|
12155
|
+
- The check is posted whether the evaluation failed **outright** or only **partly**. A per-workflow budget breach, or a `filter` that throws, leaves that one workflow undecided while its neighbours from the same repo are decided and run normally; the check then names only the undecided ones. So a broken `filter` is reported the same way whether or not other global workflows happen to share its repo.
|
|
12156
|
+
- Branch protection that lists required checks by name is unaffected, because the check is not on that list. Merge automation that requires _every_ check to be green will block on it.
|
|
12157
|
+
- **The check clears only on a new commit.** A provider redelivery of the same event is dropped as a duplicate, so re-delivering the webhook will not re-run the evaluation.
|
|
12158
|
+
|
|
12159
|
+
## Approval gates are not supported
|
|
12160
|
+
|
|
12161
|
+
A global workflow cannot carry an `approval` gate, at the workflow level or on a job. Approval holds are applied by the per-repository dispatch path; the global path dispatches its jobs without consulting one, so a gate declared here would never be enforced. `kici compile` refuses it with `error [E124]` rather than accepting a security control the workflow does not actually have. A job produced by a `dynamicJob` generator never passes through the compiler, so that case is caught at dispatch instead — the orchestrator logs an error naming the workflow and job, and runs it ungated.
|
|
12162
|
+
|
|
12163
|
+
To gate a deployment behind a human, put the gated jobs in a workflow whose triggers carry no `repos:`.
|
|
12164
|
+
|
|
12165
|
+
## Re-running is not supported
|
|
12166
|
+
|
|
12167
|
+
A global workflow's run cannot be re-run. The rerun path resolves a workflow out of the repo the run acted on, and for a global workflow that is the **source** repo — not the workflow repo that declares it. Re-running is refused with an error naming both repos rather than resolving the wrong one, which for a source repo carrying a same-named workflow would silently run that workflow instead, with the source repo's credentials and none of the global job configuration.
|
|
12168
|
+
|
|
12169
|
+
To run it again, push a new commit to the source repo (a provider redelivery of the same event is dropped as a duplicate), or trigger it from the workflow repo.
|
|
12170
|
+
|
|
12171
|
+
## Requirements a filter places on the run
|
|
12172
|
+
|
|
12173
|
+
A `filter` reads the source tree, so the evaluation must be able to obtain one. A job that restores its workflow source from the cache and has no source repository to clone from fails with an explicit error rather than evaluating the filter against an empty tree. This applies to dispatch paths that run without a source repository configured — a filter and such a path are mutually exclusive; drop one or the other.
|
|
12174
|
+
|
|
11846
12175
|
## Troubleshooting
|
|
11847
12176
|
|
|
11848
|
-
| Symptom
|
|
11849
|
-
|
|
|
11850
|
-
| Global workflow registered but never runs
|
|
11851
|
-
|
|
|
11852
|
-
|
|
|
11853
|
-
|
|
|
12177
|
+
| Symptom | Likely cause | Where to look |
|
|
12178
|
+
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
12179
|
+
| 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) |
|
|
12180
|
+
| 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`). |
|
|
12181
|
+
| `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. |
|
|
12182
|
+
| 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 |
|
|
12183
|
+
| 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. |
|
|
12184
|
+
| 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. |
|
|
12185
|
+
| 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. |
|
|
12186
|
+
| 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). |
|
|
12187
|
+
| 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. |
|
|
12188
|
+
| 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. |
|
|
12189
|
+
| 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. |
|
|
12190
|
+
|
|
12191
|
+
### Reading a global run in the dashboard
|
|
12192
|
+
|
|
12193
|
+
A global run is attributed to the **source** repo — the repo whose event
|
|
12194
|
+
triggered it, and whose code the jobs check out. Its run detail page names both
|
|
12195
|
+
repos, so you can tell it apart from an ordinary per-repo run:
|
|
12196
|
+
|
|
12197
|
+
| Row | Shows |
|
|
12198
|
+
| ------------ | ------------------------------------------------------------------------ |
|
|
12199
|
+
| `Repository` | the source repo — the one the run acted on |
|
|
12200
|
+
| `Defined in` | the workflow repo, tagged `Organization-wide`. Absent on an ordinary run |
|
|
12201
|
+
| `Workflow` | links into the **workflow** repo, on its default branch |
|
|
12202
|
+
|
|
12203
|
+
The `Workflow` link points at the workflow repo's default branch rather than at
|
|
12204
|
+
a commit: the run's own commit belongs to the source repo, and nothing records
|
|
12205
|
+
which commit of the workflow repo a given run used. So the link always shows the
|
|
12206
|
+
file as it stands now, which may have changed since the run.
|
|
12207
|
+
|
|
12208
|
+
The `Payload` tab shows the source repo's event — the webhook delivery the
|
|
12209
|
+
workflow reacted to, which for a global workflow comes from a repo you may not
|
|
12210
|
+
own. A global run dispatched before your orchestrator stored payloads for this
|
|
12211
|
+
path has none, and its tab reports that it could not load one.
|
|
12212
|
+
|
|
12213
|
+
#### Who can see it
|
|
12214
|
+
|
|
12215
|
+
A global run belongs to **both** repos, so a member whose role is scoped to
|
|
12216
|
+
either one reaches it — the team whose push triggered it, and the team that
|
|
12217
|
+
authored the workflow. Both see it in the run list, in the repository filter
|
|
12218
|
+
(which offers both names), and on the run detail page. Cancelling follows the
|
|
12219
|
+
same rule, so the team whose workflow is running can always stop it.
|
|
12220
|
+
|
|
12221
|
+
Releasing a **held** run is the one exception: approving a hold permits code to
|
|
12222
|
+
run against the source repo, so it stays with a member scoped to that repo. A
|
|
12223
|
+
member scoped only to the workflow repo sees the run but not its hold.
|
|
12224
|
+
|
|
12225
|
+
This applies only where the two repos genuinely differ. An ordinary per-repo run
|
|
12226
|
+
records no separate workflow repo and is scoped to its own repo exactly as
|
|
12227
|
+
before, and a member scoped to neither repo sees nothing in either case.
|
|
12228
|
+
|
|
12229
|
+
### Reading a global workflow's filter output
|
|
12230
|
+
|
|
12231
|
+
A global workflow's `filter` runs in a pre-run evaluation round, and that round
|
|
12232
|
+
decides whether a run exists at all — so on the path where it suppresses a
|
|
12233
|
+
workflow there is no run, and nothing appears in the dashboard. The round's own
|
|
12234
|
+
log is still recorded. Read it with the orchestrator admin CLI, in two steps:
|
|
12235
|
+
|
|
12236
|
+
```bash
|
|
12237
|
+
# 1. Find the round. Its workflow name is __globaleval__<owner>/<repo> of the
|
|
12238
|
+
# WORKFLOW repo. In the JSON rows, `id` is the job id and `run_id` is the
|
|
12239
|
+
# run id.
|
|
12240
|
+
kici-admin queue list --workflow-name '__globaleval__myorg/ci-pipelines' --limit 5 --json
|
|
12241
|
+
|
|
12242
|
+
# 2. Print the round's log (step 0 is the evaluation itself).
|
|
12243
|
+
kici-admin runs logs <run_id> --job <id>
|
|
12244
|
+
```
|
|
12245
|
+
|
|
12246
|
+
Use `--json` on the first command: the plain table abbreviates both ids to their
|
|
12247
|
+
first eight characters, and the second command needs them in full.
|
|
12248
|
+
|
|
12249
|
+
The two steps need different permissions, so run both with an **owner or admin**
|
|
12250
|
+
token. Step 1 reads the dispatch queue, which requires `secret.read` — an auditor
|
|
12251
|
+
token is refused with a 403 and never reaches step 2. Step 2 requires only
|
|
12252
|
+
`run.read`, which every role carries.
|
|
12253
|
+
|
|
12254
|
+
Anything your `filter` writes with `console.log` appears there, alongside the
|
|
12255
|
+
per-candidate verdicts the round recorded.
|
|
11854
12256
|
|
|
11855
12257
|
## See also
|
|
11856
12258
|
|
|
@@ -12036,7 +12438,7 @@ export default workflow('build', {
|
|
|
12036
12438
|
|
|
12037
12439
|
Per-field rules:
|
|
12038
12440
|
|
|
12039
|
-
- **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/kici-admin-
|
|
12441
|
+
- **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#allow-http-npm--permit-non-https-private-npm-registries)).
|
|
12040
12442
|
- **`scope`** — Optional. When present, the registry serves only that scope (`@my-org`). When absent, this entry becomes the **default** registry — at most one entry may omit `scope`.
|
|
12041
12443
|
- **`tokenSecret`** — Mandatory `<context>:<secret-name>`. The orchestrator looks up the secret in the named context via the per-context secret resolver. The bare name **must not** contain a colon.
|
|
12042
12444
|
- **`alwaysAuth`** — Defaults to `true`. Forces npm to send the token on every request (even GETs), which is what most managed-registry providers require.
|
|
@@ -12250,7 +12652,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
|
|
|
12250
12652
|
|
|
12251
12653
|
- [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
|
|
12252
12654
|
- [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
|
|
12253
|
-
- [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/kici-admin-
|
|
12655
|
+
- [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
|
|
12254
12656
|
|
|
12255
12657
|
---
|
|
12256
12658
|
|
|
@@ -12556,7 +12958,7 @@ KiCI provides an explicit secrets API that gives workflow steps controlled acces
|
|
|
12556
12958
|
|
|
12557
12959
|
## Overview
|
|
12558
12960
|
|
|
12559
|
-
Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
|
|
12961
|
+
Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration/) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
|
|
12560
12962
|
|
|
12561
12963
|
This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
|
|
12562
12964
|
|
|
@@ -12577,13 +12979,13 @@ Use whichever fits the workflow — most small teams stay on the dashboard; ops
|
|
|
12577
12979
|
|
|
12578
12980
|
### When the operator has disabled dashboard writes
|
|
12579
12981
|
|
|
12580
|
-
The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy). When that flip is on:
|
|
12982
|
+
The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy/). When that flip is on:
|
|
12581
12983
|
|
|
12582
12984
|
- The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Hovering or keyboard-focusing the lock shows a tooltip with the exact `kici-admin secret set` invocation needed. The control itself is inert, so there is nothing to click.
|
|
12583
12985
|
- The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
|
|
12584
12986
|
- `kici-admin secret set` becomes the single entry point for new and updated secret values.
|
|
12585
12987
|
|
|
12586
|
-
This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD,
|
|
12988
|
+
This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, context bindings).
|
|
12587
12989
|
|
|
12588
12990
|
### CLI input modes
|
|
12589
12991
|
|
|
@@ -12618,7 +13020,7 @@ Two cross-cutting flags help every mode:
|
|
|
12618
13020
|
|
|
12619
13021
|
`kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
|
|
12620
13022
|
|
|
12621
|
-
A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy
|
|
13023
|
+
A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy/#cli-input-modes-for-the-plaintext-path).
|
|
12622
13024
|
|
|
12623
13025
|
## Accessing secrets
|
|
12624
13026
|
|
|
@@ -12853,7 +13255,7 @@ Note that `get()` is async -- you must `await` the result.
|
|
|
12853
13255
|
|
|
12854
13256
|
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.
|
|
12855
13257
|
|
|
12856
|
-
See [CLI reference](https://docs.kici.dev/user/cli) for the `kici types` command.
|
|
13258
|
+
See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
|
|
12857
13259
|
|
|
12858
13260
|
---
|
|
12859
13261
|
|
|
@@ -13169,9 +13571,8 @@ when you want a list entry pinned to this specific App rather than
|
|
|
13169
13571
|
applying to any source in the org:
|
|
13170
13572
|
|
|
13171
13573
|
```bash
|
|
13172
|
-
# Enable global workflows
|
|
13173
|
-
kici-admin
|
|
13174
|
-
--customer-id <orgId>
|
|
13574
|
+
# Enable global workflows cluster-wide (the master switch is fleet-wide, held by the operator)
|
|
13575
|
+
kici-admin cluster-settings set --global-workflows-enabled true
|
|
13175
13576
|
|
|
13176
13577
|
# Allow the listed repo as an author for any source in the org
|
|
13177
13578
|
kici-admin org-settings global-workflows allow-add 'my-org/ci-workflows/*' \
|
|
@@ -13606,9 +14007,8 @@ org-scoped (one row per `customer_id`); each list entry can optionally
|
|
|
13606
14007
|
pin to a specific source via `--source <routingKey>`:
|
|
13607
14008
|
|
|
13608
14009
|
```bash
|
|
13609
|
-
# Enable global workflows
|
|
13610
|
-
kici-admin
|
|
13611
|
-
--customer-id <orgId>
|
|
14010
|
+
# Enable global workflows cluster-wide (the master switch is fleet-wide, held by the operator)
|
|
14011
|
+
kici-admin cluster-settings set --global-workflows-enabled true
|
|
13612
14012
|
|
|
13613
14013
|
# Allow authors from any source in the org
|
|
13614
14014
|
kici-admin org-settings global-workflows allow-add \
|
|
@@ -13983,12 +14383,13 @@ The `ProviderRegistry` maps routing keys to provider bundles. Each routing key (
|
|
|
13983
14383
|
- `WebhookNormalizer` (required) -- normalizes incoming webhooks to a standard format
|
|
13984
14384
|
- `LockFileFetcher` -- fetches lock files from the repository
|
|
13985
14385
|
- `ChangedFilesFetcher` -- determines which files changed
|
|
14386
|
+
- `FileContentsFetcher` -- reads arbitrary repository files at a ref, for the declarative content-requirements (`requires`) filter
|
|
13986
14387
|
- `CloneTokenProvider` -- generates clone tokens for agents
|
|
13987
14388
|
- `RepoUrlBuilder` -- builds clone URLs and raw file URLs
|
|
13988
14389
|
- `ContributorResolver` -- resolves contributor permissions for trust-tier gating
|
|
13989
14390
|
- `CheckStatusPoster` -- posts check statuses (approval/hold) to the git provider
|
|
13990
14391
|
|
|
13991
|
-
A GitHub App source populates all
|
|
14392
|
+
A GitHub App source populates all eight -- though the file-contents capability arrives as a per-delivery factory rather than a prebuilt instance, because a GitHub client is scoped to one installation and the installation id is only known once the delivery's credentials are resolved. A plain generic webhook source carries only the normalizer -- it has no repository API to fetch a lock file, resolve a contributor, or post a check against -- so the pipeline skips the stages whose interface is absent rather than failing the delivery.
|
|
13992
14393
|
|
|
13993
14394
|
Provider registrations are managed via the `sources` database table, not via `SharedConfig`. When the orchestrator connects to the Platform relay, it reads source records from the DB and sends `source.register` messages. Changes to sources (add/remove) are detected via PostgreSQL LISTEN/NOTIFY on the `sources_change` channel and pushed to the Platform via `source.secrets` and `source.register`/`source.deregister`.
|
|
13994
14395
|
|
|
@@ -14076,10 +14477,11 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
|
|
|
14076
14477
|
12. **Orchestrator extracts registrations** on default-branch pushes: persists registerable workflows (event, schedule, lifecycle triggers) for cluster-wide event matching.
|
|
14077
14478
|
13. **Orchestrator notifies the event router** on default-branch pushes: after the registrations are persisted, emits a `registration.updated` event via `eventRouter.emit()` (if event routing is active). Workflow event subscriptions are the persisted registrations themselves, matched at emit time through the registration index.
|
|
14078
14479
|
14. **Orchestrator fetches changed files** via the provider's `ChangedFilesFetcher` for path-based trigger filtering (skipped when no workflow uses path filters).
|
|
14079
|
-
15. **Orchestrator matches triggers** against lock file using `
|
|
14080
|
-
16. **Orchestrator
|
|
14081
|
-
17. **Orchestrator
|
|
14082
|
-
18. **Orchestrator
|
|
14480
|
+
15. **Orchestrator matches triggers** against the lock file using `matchWorkflowsForEvent()` from `@kici-dev/engine` -- an event-type-bucketed candidate scan that evaluates only the workflows subscribed to this event type. (The single-registration global / cross-source paths evaluate one lock entry at a time via `matchAllWorkflows()`.)
|
|
14481
|
+
16. **Orchestrator applies the content-requirements filter** to the matched candidates: for each trigger that declares `requires`, it reads the named source files at the event's ref through the provider's `FileContentsFetcher` (once per distinct `(repo, sha, path)` via an LRU cache) and evaluates the declarative requirement. Candidates that fail -- or that cannot be evaluated at all (unreadable or oversize content, a fetch error, no fetcher wired) -- are dropped before dispatch with the concrete reason logged. No workflow code runs at this stage. Skipped entirely when no matched trigger declares `requires`.
|
|
14482
|
+
17. **Orchestrator checks caches** for source tarballs and dependency tarballs.
|
|
14483
|
+
18. **Orchestrator dispatches jobs** to agents via the job queue and WebSocket.
|
|
14484
|
+
19. **Orchestrator persists a delivery row** keyed by `(org_id, delivery_id)` to its own `event_log`, including a pointer to the gzipped payload in object storage. The orchestrator's delivery log is surfaced in the dashboard's Settings → Event log tab. See [`webhook-delivery.md`](https://docs.kici.dev/architecture/webhooks/webhook-delivery/#delivery-log).
|
|
14083
14485
|
|
|
14084
14486
|
## Job execution flow
|
|
14085
14487
|
|
|
@@ -14195,7 +14597,7 @@ Execution Job Dispatch --> Execution Agent
|
|
|
14195
14597
|
| |
|
|
14196
14598
|
| |-- Download source tarball (sourceTarUrl) -> extract to workDir/.kici/
|
|
14197
14599
|
| |-- Download deps tarball (depsUrl) -> verify SHA-256 -> extract to .kici/node_modules/
|
|
14198
|
-
| |-- Register @kici-dev/
|
|
14600
|
+
| |-- Register @kici-dev/core/ts-loader-hook
|
|
14199
14601
|
| |-- Verify workflow contentHash against lock file (drift guard)
|
|
14200
14602
|
| |-- Dynamic-import workflow .ts
|
|
14201
14603
|
| |-- Execute steps
|
|
@@ -14254,7 +14656,7 @@ Dep cache misses alone do **not** trigger a build job. Deps are platform-specifi
|
|
|
14254
14656
|
|
|
14255
14657
|
### Cross-source / no-contentHash workflows
|
|
14256
14658
|
|
|
14257
|
-
- **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is
|
|
14659
|
+
- **Lock files without `contentHash`** (schema v1) skip the source cache entirely; agents compile from source. Regenerate lock files with `kici compile` to enable caching. The current lock file schema version is 34.
|
|
14258
14660
|
- **Cross-source / global-workflow dispatch** (a workflow registered against source A fired by a webhook on source B) bypasses both caches. The registration's lock file entry still carries `contentHash`, but the cross-source path always clone-and-installs — the eval temp dir doesn't ship `@kici-dev/sdk`. The execution agent still verifies `contentHash` against the cloned source for drift detection.
|
|
14259
14661
|
|
|
14260
14662
|
### Build deduplication
|
|
@@ -14499,7 +14901,7 @@ Agent Orchestrator
|
|
|
14499
14901
|
|
|
14500
14902
|
### Registration extraction flow
|
|
14501
14903
|
|
|
14502
|
-
When code is pushed to the default branch, the orchestrator extracts
|
|
14904
|
+
When code is pushed to the default branch, the orchestrator extracts registerable workflows from the lock file and stores them as registrations for cluster-wide event matching. Non-Git triggers (`kici_event`, `schedule`, `generic_webhook`, …) live there because they have no per-repo lock-file pipeline to fall back on; Git-provider triggers (`push`, `pr`, `tag`, …) are indexed too, so the cross-source dispatch path can resolve them by `(customer_id, repo_identifier)` when a generic webhook targets an externally-hosted repo. For same-source Git events the per-event lock-file pipeline remains the primary matcher — registration is an additive index.
|
|
14503
14905
|
|
|
14504
14906
|
```
|
|
14505
14907
|
Git Push to Default Branch
|
|
@@ -14512,8 +14914,15 @@ GitHub Webhook -> Platform Relay -> Orchestrator Processor
|
|
|
14512
14914
|
|-- extractRegisterableWorkflows(fullLockFile)
|
|
14513
14915
|
| |-- For each workflow entry in lock file:
|
|
14514
14916
|
| | Check if any trigger type is registerable
|
|
14515
|
-
| | (
|
|
14516
|
-
| |
|
|
14917
|
+
| | (the RegisterableTriggerType enum — the non-Git
|
|
14918
|
+
| | set kici_event, workflow_complete,
|
|
14919
|
+
| | workflows_failed_batch, job_complete,
|
|
14920
|
+
| | generic_webhook, schedule, lifecycle, webhook,
|
|
14921
|
+
| | plus every Git-provider trigger: push, pr, tag,
|
|
14922
|
+
| | comment, review, review_comment, release,
|
|
14923
|
+
| | dispatch, create, delete, status, workflow_run,
|
|
14924
|
+
| | fork, star, watch)
|
|
14925
|
+
| | ... or the workflow has repo patterns (global workflow)
|
|
14517
14926
|
| |-- Return array of registerable workflows
|
|
14518
14927
|
|
|
|
14519
14928
|
|-- globalWorkflowPolicy.isWorkflowRepoAllowed() (if policy configured)
|
|
@@ -14891,7 +15300,7 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
|
|
|
14891
15300
|
- [Architecture overview](https://docs.kici.dev/architecture/overview/) -- three-tier model and component responsibilities
|
|
14892
15301
|
- [Protocol messages](https://docs.kici.dev/architecture/protocol-messages/) -- WebSocket message schemas
|
|
14893
15302
|
- [Event system internals](https://docs.kici.dev/architecture/webhooks/event-system/) -- event router, registration model, cron scheduler
|
|
14894
|
-
- [Execution
|
|
15303
|
+
- [Execution status vocabulary](https://docs.kici.dev/architecture/execution/state-machine/) -- run, job, and step status vocabularies and terminal states
|
|
14895
15304
|
- [Webhook delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) -- detailed webhook processing pipeline
|
|
14896
15305
|
- [Operator: dependency caching](https://docs.kici.dev/operator/dependency-caching/) -- configuration guide
|
|
14897
15306
|
- [Operator: monitoring & tracing](https://docs.kici.dev/operator/observability/monitoring/) -- trace fields and Loki queries
|
|
@@ -14990,7 +15399,7 @@ The agent is the execution worker. It runs on customer infrastructure and has fu
|
|
|
14990
15399
|
Shared business logic used by all three tiers. Single source of truth for cross-tier concerns. Has no internal `@kici-dev/*` dependencies -- only a handful of third-party libraries.
|
|
14991
15400
|
|
|
14992
15401
|
- Protocol message schemas (Zod-based, direction-specific unions including dashboard REST-over-WS, browser live streaming, the test-relay control plane, log pull, run events, peer-to-peer, cluster join, and source registration)
|
|
14993
|
-
- Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster)
|
|
15402
|
+
- Provider interfaces (WebhookNormalizer, LockFileFetcher, ChangedFilesFetcher, FileContentsFetcher, CloneTokenProvider, RepoUrlBuilder, ContributorResolver, CheckStatusPoster)
|
|
14994
15403
|
- Trigger matching engine (branch, path, event evaluation)
|
|
14995
15404
|
- Dispatch inputs (input descriptors, extraction from the trigger event, and coercion to typed values)
|
|
14996
15405
|
- Matrix expansion and fanout (combination expansion with include/exclude, job-name suffix formatting, and materialization of one matrix or multi-host job into N dispatchable children)
|
|
@@ -15006,11 +15415,16 @@ Shared business logic used by all three tiers. Single source of truth for cross-
|
|
|
15006
15415
|
- Build provenance (in-toto statement schema, DSSE envelope, attestation bundle, verification)
|
|
15007
15416
|
- Artifact name contract (the shared filesystem/URL-safe name schema the orchestrator, agent, and SDK all validate against)
|
|
15008
15417
|
- Developer MCP tool schemas (argument schemas for the AI-agent tool surface)
|
|
15418
|
+
- Developer-operations contract (one row per workflow-developer operation declaring which entrypoints expose it -- the shared REST API behind the web UI and the `kici` CLI, the AI-agent tool surface, and a curated UI flag -- asserted against each real surface by congruence tests)
|
|
15009
15419
|
- Label utilities (platform label derivation, runsOn normalization, `kici:*` set-only reserved namespace, role labels)
|
|
15010
15420
|
- Host inventory (the canonical queryable host-roster schema shared by the orchestrator's roster store, the agent-facing inventory API, and the SDK's `ctx.kici.inventory`)
|
|
15011
15421
|
- Audit policy and retention (per-action access-log sampling, warm-retention windows for cold-store eligibility, federated activity row schema)
|
|
15012
15422
|
- Scaler backend type enum (`container`, `bare-metal`, `firecracker`, `kubernetes`)
|
|
15013
15423
|
- Registration trigger type enum (registerable trigger discriminator)
|
|
15424
|
+
- Sandbox capability set (the Linux capability names a container sandbox may add or drop, shared by the SDK validator, the compiler, and the dispatch resolver)
|
|
15425
|
+
- Plan tier vocabulary (the hosted plan tiers and the purchasable subset, shared by the Platform and the browser dashboard)
|
|
15426
|
+
- Infrastructure alert vocabulary (the diagnostics alert types and severities the Platform mints and the dashboard and `kici` CLI render)
|
|
15427
|
+
- Metric catalog (the generated Prometheus metric inventory, its naming policy, and metric-kind compatibility checks)
|
|
15014
15428
|
- Bundler config (shared bundler configuration consumed by `e2e/helpers/service-deploy.ts`; the agent runtime uses the `@kici-dev/core/ts-loader-hook` to transform TypeScript on import, with no runtime bundler step)
|
|
15015
15429
|
|
|
15016
15430
|
> Source: `packages/engine/src/`
|
|
@@ -15134,7 +15548,7 @@ KiCI uses application-level tenant isolation. The Platform dashboard API accepts
|
|
|
15134
15548
|
## See also
|
|
15135
15549
|
|
|
15136
15550
|
- [Multi-Orchestrator Architecture](https://docs.kici.dev/architecture/clustering/multi-orchestrator/) -- P2P clustering, Raft consensus, job rerouting
|
|
15137
|
-
- [Execution
|
|
15551
|
+
- [Execution status vocabulary](https://docs.kici.dev/architecture/execution/state-machine/) -- run, job, and step status vocabularies and the tracker that owns lifecycle state
|
|
15138
15552
|
- [Protocol Messages](https://docs.kici.dev/architecture/protocol-messages/) -- WebSocket message schemas for all three layers
|
|
15139
15553
|
- [Webhook Delivery](https://docs.kici.dev/architecture/webhooks/webhook-delivery/) -- end-to-end trace of a webhook through all three tiers
|
|
15140
15554
|
|