@kici-dev/compiler 0.1.14 → 0.1.16
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/README.md +21 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +213 -179
- package/dist/commands/approve.d.ts +23 -0
- package/dist/commands/approve.js +46 -0
- package/dist/commands/compile.js +1 -1
- package/dist/commands/docs.js +2 -2
- package/dist/commands/endpoints.js +1 -1
- package/dist/commands/held-run-client.d.ts +26 -0
- package/dist/commands/held-run-client.js +98 -0
- package/dist/commands/held-run-resolve.d.ts +45 -0
- package/dist/commands/held-run-resolve.js +53 -0
- package/dist/commands/hook.js +1 -1
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/index.js +3 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/login.js +4 -15
- package/dist/commands/reject.d.ts +25 -0
- package/dist/commands/reject.js +49 -0
- package/dist/commands/run.js +13 -2
- package/dist/commands/secrets-list.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +1 -1
- package/dist/commands/types.js +1 -1
- package/dist/commands/watch.js +1 -1
- package/dist/execution/executor.js +2 -2
- package/dist/execution/sdk-alias.js +1 -1
- package/dist/fixtures/compiler.js +2 -2
- package/dist/fixtures/defaults/index.js +2 -2
- package/dist/hooks/detector.js +1 -1
- package/dist/hooks/installer.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/llm-context/llms-full.txt +1141 -349
- package/dist/llm-context/llms.txt +4 -2
- package/dist/local-executor/index.js +5 -4
- package/dist/local-executor/job-runner.js +3 -5
- package/dist/local-executor/materializer.d.ts +5 -0
- package/dist/local-executor/materializer.js +23 -2
- package/dist/local-executor/secret-loader.js +1 -1
- package/dist/local-executor/to-event-payload.d.ts +16 -0
- package/dist/local-executor/to-event-payload.js +21 -0
- package/dist/local-executor/workflow-lock.d.ts +4 -3
- package/dist/local-executor/workflow-lock.js +0 -0
- package/dist/lockfile/generator.js +22 -5
- package/dist/lockfile/hash-files.js +1 -1
- package/dist/postinstall.js +1 -1
- package/dist/remote/client.d.ts +4 -0
- package/dist/remote/config.js +1 -1
- package/dist/remote/history.js +1 -1
- package/dist/remote/prod-defaults.d.ts +18 -0
- package/dist/remote/prod-defaults.js +23 -0
- package/dist/remote/secret-upload.d.ts +20 -0
- package/dist/remote/secret-upload.js +58 -0
- package/dist/remote/uploader.js +1 -1
- package/dist/templates/index.js +1 -1
- package/dist/templates/package-json.js +1 -1
- package/dist/templates/workflows/pr-checks.js +3 -2
- package/dist/templates/workflows/pr-checks.ts +4 -2
- package/dist/test-runner/git-detector.js +1 -1
- package/dist/test-runner/job-executor.js +2 -2
- package/dist/test-runner/secrets-file.js +1 -1
- package/dist/test-runner/step-context.js +6 -2
- package/dist/types.d.ts +33 -2
- package/dist/types.js +1 -0
- package/dist/workflows/pr-checks.ts +4 -2
- package/package.json +13 -9
- package/sbom.spdx.json +34 -34
|
@@ -12,7 +12,7 @@ Documentation for workflow authors -- people writing CI/CD pipelines in TypeScri
|
|
|
12
12
|
|
|
13
13
|
## Pages
|
|
14
14
|
|
|
15
|
-
### [Getting started](getting-started.md)
|
|
15
|
+
### [Getting started with KiCI](getting-started.md)
|
|
16
16
|
|
|
17
17
|
Install the SDK and compiler, write your first workflow, compile it to a lock file, and test it locally with simulated events. Covers prerequisites (Node.js 24+, pnpm), the `kici init` command for scaffolding, and the relationship between workflows, the lock file, and the three-tier runtime.
|
|
18
18
|
|
|
@@ -86,7 +86,7 @@ Cross-repo workflows that let a single workflow repo define jobs which run on ev
|
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
-
## Getting started with
|
|
89
|
+
## Getting started with workflows
|
|
90
90
|
|
|
91
91
|
Source: https://docs.kici.dev/user/getting-started/
|
|
92
92
|
|
|
@@ -179,7 +179,7 @@ Create `.kici/workflows/ci.ts`:
|
|
|
179
179
|
import { workflow, job, step, pr } from '@kici-dev/sdk';
|
|
180
180
|
|
|
181
181
|
const lint = job('lint', {
|
|
182
|
-
runsOn: '
|
|
182
|
+
runsOn: 'linux',
|
|
183
183
|
steps: [
|
|
184
184
|
step('install', async ({ $ }) => {
|
|
185
185
|
await $`pnpm install --frozen-lockfile`;
|
|
@@ -191,7 +191,7 @@ const lint = job('lint', {
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
const test = job('test', {
|
|
194
|
-
runsOn: '
|
|
194
|
+
runsOn: 'linux',
|
|
195
195
|
needs: [lint],
|
|
196
196
|
steps: [
|
|
197
197
|
step('install', async ({ $ }) => {
|
|
@@ -264,9 +264,9 @@ Workflow: ci
|
|
|
264
264
|
✓ Matched trigger 1
|
|
265
265
|
Jobs (2):
|
|
266
266
|
lint
|
|
267
|
-
runs-on:
|
|
267
|
+
runs-on: linux
|
|
268
268
|
test
|
|
269
|
-
runs-on:
|
|
269
|
+
runs-on: linux
|
|
270
270
|
|
|
271
271
|
Decision Summary:
|
|
272
272
|
|
|
@@ -310,7 +310,7 @@ This updates `.kici/package.json` and generates (or updates) `package-lock.json`
|
|
|
310
310
|
|
|
311
311
|
### Dependency resolution contract
|
|
312
312
|
|
|
313
|
-
Every `.kici/` dependency must be resolvable from the **single cloned repository**. When a job runs, the agent clones only this repository and installs `.kici/` dependencies with your repo's package manager (npm or pnpm
|
|
313
|
+
Every `.kici/` dependency must be resolvable from the **single cloned repository**. When a job runs, the agent clones only this repository and installs `.kici/` dependencies with your repo's package manager (npm or pnpm; yarn is not supported — the agent rejects it with an actionable error). A dependency that points outside the cloned repo cannot be resolved.
|
|
314
314
|
|
|
315
315
|
In practice:
|
|
316
316
|
|
|
@@ -525,7 +525,7 @@ A standard lint-then-test pipeline using job dependencies (`needs`):
|
|
|
525
525
|
import { workflow, job, step, pr } from '@kici-dev/sdk';
|
|
526
526
|
|
|
527
527
|
const lint = job('lint', {
|
|
528
|
-
runsOn: '
|
|
528
|
+
runsOn: 'linux',
|
|
529
529
|
steps: [
|
|
530
530
|
step('install', async ({ $ }) => {
|
|
531
531
|
await $`pnpm install --frozen-lockfile`;
|
|
@@ -538,7 +538,7 @@ const lint = job('lint', {
|
|
|
538
538
|
});
|
|
539
539
|
|
|
540
540
|
const test = job('test', {
|
|
541
|
-
runsOn: '
|
|
541
|
+
runsOn: 'linux',
|
|
542
542
|
needs: [lint],
|
|
543
543
|
steps: [
|
|
544
544
|
step('install', async ({ $ }) => {
|
|
@@ -551,7 +551,7 @@ const test = job('test', {
|
|
|
551
551
|
});
|
|
552
552
|
|
|
553
553
|
const typecheck = job('typecheck', {
|
|
554
|
-
runsOn: '
|
|
554
|
+
runsOn: 'linux',
|
|
555
555
|
needs: [lint],
|
|
556
556
|
steps: [
|
|
557
557
|
step('install', async ({ $ }) => {
|
|
@@ -608,7 +608,7 @@ const trigger = pr({
|
|
|
608
608
|
});
|
|
609
609
|
|
|
610
610
|
const build = job('build', {
|
|
611
|
-
runsOn: '
|
|
611
|
+
runsOn: 'linux',
|
|
612
612
|
steps: [
|
|
613
613
|
step('build', async ({ $ }) => {
|
|
614
614
|
await $`pnpm build`;
|
|
@@ -643,7 +643,7 @@ import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
|
643
643
|
|
|
644
644
|
// Deploy on pushes to main
|
|
645
645
|
const deploy = job('deploy', {
|
|
646
|
-
runsOn: '
|
|
646
|
+
runsOn: 'linux',
|
|
647
647
|
steps: [
|
|
648
648
|
step('deploy', async ({ $ }) => {
|
|
649
649
|
await $`pnpm build`;
|
|
@@ -687,7 +687,7 @@ A workflow can respond to multiple trigger types:
|
|
|
687
687
|
import { workflow, job, step, pr, push } from '@kici-dev/sdk';
|
|
688
688
|
|
|
689
689
|
const test = job('test', {
|
|
690
|
-
runsOn: '
|
|
690
|
+
runsOn: 'linux',
|
|
691
691
|
steps: [
|
|
692
692
|
step('test', async ({ $ }) => {
|
|
693
693
|
await $`pnpm test`;
|
|
@@ -719,7 +719,7 @@ export default workflow('hello-world', {
|
|
|
719
719
|
on: dispatch(),
|
|
720
720
|
jobs: [
|
|
721
721
|
job('greet', {
|
|
722
|
-
runsOn: '
|
|
722
|
+
runsOn: 'linux',
|
|
723
723
|
steps: [
|
|
724
724
|
step('say-hello', async ({ $ }) => {
|
|
725
725
|
await $`echo "Hello, World!"`;
|
|
@@ -777,7 +777,7 @@ Rules control whether a workflow or job runs. Use `rule()` for conditions that m
|
|
|
777
777
|
import { workflow, job, step, pr, rule } from '@kici-dev/sdk';
|
|
778
778
|
|
|
779
779
|
const test = job('test', {
|
|
780
|
-
runsOn: '
|
|
780
|
+
runsOn: 'linux',
|
|
781
781
|
steps: [
|
|
782
782
|
step('test', async ({ $ }) => {
|
|
783
783
|
await $`pnpm test`;
|
|
@@ -802,7 +802,7 @@ export default workflow('ci', {
|
|
|
802
802
|
import { workflow, job, step, pr, rule, skip } from '@kici-dev/sdk';
|
|
803
803
|
|
|
804
804
|
const unitTests = job('unit-tests', {
|
|
805
|
-
runsOn: '
|
|
805
|
+
runsOn: 'linux',
|
|
806
806
|
steps: [
|
|
807
807
|
step('test', async ({ $ }) => {
|
|
808
808
|
await $`pnpm test:unit`;
|
|
@@ -811,7 +811,7 @@ const unitTests = job('unit-tests', {
|
|
|
811
811
|
});
|
|
812
812
|
|
|
813
813
|
const e2eTests = job('e2e-tests', {
|
|
814
|
-
runsOn: '
|
|
814
|
+
runsOn: 'linux',
|
|
815
815
|
rules: [
|
|
816
816
|
// Skip E2E when only docs change
|
|
817
817
|
skip('docs only', async (ctx) => {
|
|
@@ -862,7 +862,7 @@ Run a job for each value in an array:
|
|
|
862
862
|
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
863
863
|
|
|
864
864
|
const test = job('test', {
|
|
865
|
-
runsOn: '
|
|
865
|
+
runsOn: 'linux',
|
|
866
866
|
matrix: ['18', '20', '22'],
|
|
867
867
|
steps: [
|
|
868
868
|
step('test', async ({ $, matrix }) => {
|
|
@@ -886,14 +886,14 @@ Use an object to define multiple dimensions. KiCI expands all combinations (capp
|
|
|
886
886
|
|
|
887
887
|
```typescript
|
|
888
888
|
const test = job('test', {
|
|
889
|
-
runsOn: '
|
|
889
|
+
runsOn: ['linux', 'kici:agent:container'],
|
|
890
890
|
matrix: {
|
|
891
|
-
os: ['
|
|
891
|
+
os: ['linux', 'arm64'],
|
|
892
892
|
node: ['18', '20', '22'],
|
|
893
893
|
},
|
|
894
894
|
steps: [
|
|
895
895
|
step('test', async ({ $, matrix }) => {
|
|
896
|
-
// matrix.os = '
|
|
896
|
+
// matrix.os = 'linux' | 'arm64'
|
|
897
897
|
// matrix.node = '18' | '20' | '22'
|
|
898
898
|
await $`echo "Testing on ${matrix!.os} with Node ${matrix!.node}"`;
|
|
899
899
|
await $`pnpm test`;
|
|
@@ -904,21 +904,27 @@ const test = job('test', {
|
|
|
904
904
|
|
|
905
905
|
This creates 6 job instances (2 OS x 3 Node versions).
|
|
906
906
|
|
|
907
|
+
> **Labels are customer-defined.** `runsOn` values such as `linux` or `arm64` are scaler
|
|
908
|
+
> labels **you** define in your orchestrator's `labelSets` — they are matched by subset
|
|
909
|
+
> semantics, not by a hosted-runner name. You can also target reserved auto-injected labels
|
|
910
|
+
> in the `kici:` namespace (e.g. `kici:agent:firecracker`, `kici:agent:container`) to pin a
|
|
911
|
+
> job to a specific backend type.
|
|
912
|
+
|
|
907
913
|
### Include and exclude
|
|
908
914
|
|
|
909
915
|
Fine-tune matrix combinations:
|
|
910
916
|
|
|
911
917
|
```typescript
|
|
912
918
|
const test = job('test', {
|
|
913
|
-
runsOn: '
|
|
919
|
+
runsOn: 'linux',
|
|
914
920
|
matrix: {
|
|
915
|
-
os: ['
|
|
921
|
+
os: ['linux', 'arm64', 'windows'],
|
|
916
922
|
node: ['18', '20', '22'],
|
|
917
923
|
},
|
|
918
924
|
// Remove specific combination
|
|
919
|
-
exclude: [{ os: 'windows
|
|
925
|
+
exclude: [{ os: 'windows', node: '18' }],
|
|
920
926
|
// Add specific combination not in the matrix
|
|
921
|
-
include: [{ os: '
|
|
927
|
+
include: [{ os: 'linux', node: '23' }],
|
|
922
928
|
steps: [
|
|
923
929
|
step('test', async ({ $ }) => {
|
|
924
930
|
await $`pnpm test`;
|
|
@@ -935,7 +941,7 @@ Compute matrix values at runtime using an async function:
|
|
|
935
941
|
|
|
936
942
|
```typescript
|
|
937
943
|
const test = job('test', {
|
|
938
|
-
runsOn: '
|
|
944
|
+
runsOn: 'linux',
|
|
939
945
|
matrix: async ({ $ }) => {
|
|
940
946
|
// Discover packages in a monorepo
|
|
941
947
|
const result = await $`ls packages/`;
|
|
@@ -984,7 +990,7 @@ const discoverAndTest: DynamicJobFn = async ({ $ }) => {
|
|
|
984
990
|
|
|
985
991
|
return packages.map((pkg) =>
|
|
986
992
|
job(`test-${pkg}`, {
|
|
987
|
-
runsOn: '
|
|
993
|
+
runsOn: 'linux',
|
|
988
994
|
steps: [
|
|
989
995
|
step('test', async ({ $ }) => {
|
|
990
996
|
await $`cd packages/${pkg} && pnpm test`;
|
|
@@ -1006,7 +1012,7 @@ The `jobs` array accepts both static `Job` objects and `DynamicJobFn` functions:
|
|
|
1006
1012
|
|
|
1007
1013
|
```typescript
|
|
1008
1014
|
const lint = job('lint', {
|
|
1009
|
-
runsOn: '
|
|
1015
|
+
runsOn: 'linux',
|
|
1010
1016
|
steps: [
|
|
1011
1017
|
step('lint', async ({ $ }) => {
|
|
1012
1018
|
await $`pnpm lint`;
|
|
@@ -1036,7 +1042,7 @@ const prTrigger = pr({ target: 'main', paths: ['src/**', 'packages/**', '!**/*.m
|
|
|
1036
1042
|
const pushTrigger = push({ branches: 'main' });
|
|
1037
1043
|
|
|
1038
1044
|
const lint = job('lint', {
|
|
1039
|
-
runsOn: '
|
|
1045
|
+
runsOn: 'linux',
|
|
1040
1046
|
steps: [
|
|
1041
1047
|
step('install', async ({ $ }) => {
|
|
1042
1048
|
await $`pnpm install --frozen-lockfile`;
|
|
@@ -1048,7 +1054,7 @@ const lint = job('lint', {
|
|
|
1048
1054
|
});
|
|
1049
1055
|
|
|
1050
1056
|
const test = job('test', {
|
|
1051
|
-
runsOn: '
|
|
1057
|
+
runsOn: 'linux',
|
|
1052
1058
|
needs: [lint],
|
|
1053
1059
|
matrix: { node: ['18', '20', '22'] },
|
|
1054
1060
|
steps: [
|
|
@@ -1059,7 +1065,7 @@ const test = job('test', {
|
|
|
1059
1065
|
});
|
|
1060
1066
|
|
|
1061
1067
|
const deploy = job('deploy', {
|
|
1062
|
-
runsOn: '
|
|
1068
|
+
runsOn: 'linux',
|
|
1063
1069
|
needs: [test],
|
|
1064
1070
|
rules: [
|
|
1065
1071
|
// Only deploy from push events (not PRs)
|
|
@@ -1114,7 +1120,7 @@ export const deploy = workflow('deploy', {
|
|
|
1114
1120
|
on: push({ branches: 'main' }),
|
|
1115
1121
|
jobs: [
|
|
1116
1122
|
job('deploy', {
|
|
1117
|
-
runsOn: '
|
|
1123
|
+
runsOn: 'linux',
|
|
1118
1124
|
steps: [
|
|
1119
1125
|
step('deploy', async ({ $ }) => {
|
|
1120
1126
|
await $`./scripts/deploy.sh`;
|
|
@@ -1129,7 +1135,7 @@ export const postDeploy = workflow('post-deploy', {
|
|
|
1129
1135
|
on: workflowComplete({ name: 'deploy', status: ['success'] }),
|
|
1130
1136
|
jobs: [
|
|
1131
1137
|
job('notify', {
|
|
1132
|
-
runsOn: '
|
|
1138
|
+
runsOn: 'linux',
|
|
1133
1139
|
steps: [
|
|
1134
1140
|
step('slack', async ({ $ }) => {
|
|
1135
1141
|
await $`./scripts/notify-slack.sh "Deploy succeeded"`;
|
|
@@ -1152,7 +1158,7 @@ export const deploy = workflow('deploy', {
|
|
|
1152
1158
|
on: push({ branches: 'main' }),
|
|
1153
1159
|
jobs: [
|
|
1154
1160
|
job('deploy', {
|
|
1155
|
-
runsOn: '
|
|
1161
|
+
runsOn: 'linux',
|
|
1156
1162
|
steps: [
|
|
1157
1163
|
step('deploy', async ({ $ }) => {
|
|
1158
1164
|
await $`./scripts/deploy.sh`;
|
|
@@ -1173,7 +1179,7 @@ export const postDeploy = workflow('post-deploy', {
|
|
|
1173
1179
|
on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
|
|
1174
1180
|
jobs: [
|
|
1175
1181
|
job('smoke-test', {
|
|
1176
|
-
runsOn: '
|
|
1182
|
+
runsOn: 'linux',
|
|
1177
1183
|
steps: [
|
|
1178
1184
|
step('test', async ({ $ }) => {
|
|
1179
1185
|
await $`./scripts/smoke-test.sh`;
|
|
@@ -1198,7 +1204,7 @@ export default workflow('on-argocd-deploy', {
|
|
|
1198
1204
|
on: genericWebhook({ source: 'argocd', events: ['deploy.success'] }),
|
|
1199
1205
|
jobs: [
|
|
1200
1206
|
job('post-deploy', {
|
|
1201
|
-
runsOn: '
|
|
1207
|
+
runsOn: 'linux',
|
|
1202
1208
|
steps: [
|
|
1203
1209
|
step('verify', async ({ $, rawPayload }) => {
|
|
1204
1210
|
// rawPayload contains the full webhook body from ArgoCD
|
|
@@ -1232,7 +1238,7 @@ export default workflow('stripe-invoice-handler', {
|
|
|
1232
1238
|
}),
|
|
1233
1239
|
jobs: [
|
|
1234
1240
|
job('process-invoice', {
|
|
1235
|
-
runsOn: '
|
|
1241
|
+
runsOn: 'linux',
|
|
1236
1242
|
steps: [
|
|
1237
1243
|
step('extract-customer', async ({ $, log }) => {
|
|
1238
1244
|
log.info('Processing paid invoice from Stripe');
|
|
@@ -1289,7 +1295,7 @@ export default workflow('on-forgejo-push', {
|
|
|
1289
1295
|
}),
|
|
1290
1296
|
jobs: [
|
|
1291
1297
|
job('react-to-push', {
|
|
1292
|
-
runsOn: '
|
|
1298
|
+
runsOn: 'linux',
|
|
1293
1299
|
steps: [
|
|
1294
1300
|
step('log', async ({ rawPayload, log }) => {
|
|
1295
1301
|
const ref = (rawPayload as { ref?: string }).ref;
|
|
@@ -1311,7 +1317,7 @@ Manual-clone example (pattern 2) using an SSH deploy key:
|
|
|
1311
1317
|
|
|
1312
1318
|
```typescript
|
|
1313
1319
|
job('forgejo-ci', {
|
|
1314
|
-
runsOn: '
|
|
1320
|
+
runsOn: 'linux',
|
|
1315
1321
|
checkout: false, // skip framework clone
|
|
1316
1322
|
steps: [
|
|
1317
1323
|
step('clone', async ({ $, ctx, rawPayload }) => {
|
|
@@ -1356,7 +1362,7 @@ kici-admin source add generic \
|
|
|
1356
1362
|
--rate-limit 120
|
|
1357
1363
|
|
|
1358
1364
|
# Patch the verificationConfig to use GitHub's signature header
|
|
1359
|
-
# (the CLI
|
|
1365
|
+
# (the CLI has no --signature-header flag; use the admin REST API):
|
|
1360
1366
|
curl -X PATCH https://<orchestrator>/api/v1/admin/generic-sources/<sourceId> \
|
|
1361
1367
|
-H "Authorization: Bearer <admin-token>" \
|
|
1362
1368
|
-H "Content-Type: application/json" \
|
|
@@ -1383,7 +1389,7 @@ export default workflow('on-github-repo-push', {
|
|
|
1383
1389
|
}),
|
|
1384
1390
|
jobs: [
|
|
1385
1391
|
job('notify', {
|
|
1386
|
-
runsOn: '
|
|
1392
|
+
runsOn: 'linux',
|
|
1387
1393
|
checkout: false, // no App token -> skip auto-clone
|
|
1388
1394
|
steps: [
|
|
1389
1395
|
step('log', async ({ rawPayload, log }) => {
|
|
@@ -1502,7 +1508,7 @@ const install = step('install', async ({ $ }) => {
|
|
|
1502
1508
|
});
|
|
1503
1509
|
|
|
1504
1510
|
const fullTest = job('full-test', {
|
|
1505
|
-
runsOn: '
|
|
1511
|
+
runsOn: 'linux',
|
|
1506
1512
|
steps: [
|
|
1507
1513
|
install,
|
|
1508
1514
|
step('test', async ({ $ }) => {
|
|
@@ -1515,7 +1521,7 @@ const fullTest = job('full-test', {
|
|
|
1515
1521
|
});
|
|
1516
1522
|
|
|
1517
1523
|
const publish = job('publish-nightly', {
|
|
1518
|
-
runsOn: '
|
|
1524
|
+
runsOn: 'linux',
|
|
1519
1525
|
needs: [fullTest],
|
|
1520
1526
|
steps: [
|
|
1521
1527
|
install,
|
|
@@ -1559,7 +1565,7 @@ export const build = workflow('build', {
|
|
|
1559
1565
|
on: push({ branches: 'main' }),
|
|
1560
1566
|
jobs: [
|
|
1561
1567
|
job('test', {
|
|
1562
|
-
runsOn: '
|
|
1568
|
+
runsOn: 'linux',
|
|
1563
1569
|
steps: [
|
|
1564
1570
|
step('install', async ({ $ }) => {
|
|
1565
1571
|
await $`pnpm install --frozen-lockfile`;
|
|
@@ -1580,7 +1586,7 @@ export const deploy = workflow('deploy-on-success', {
|
|
|
1580
1586
|
on: workflowComplete({ name: 'build', status: ['success'] }),
|
|
1581
1587
|
jobs: [
|
|
1582
1588
|
job('deploy', {
|
|
1583
|
-
runsOn: '
|
|
1589
|
+
runsOn: 'linux',
|
|
1584
1590
|
steps: [
|
|
1585
1591
|
step('deploy-staging', async ({ $ }) => {
|
|
1586
1592
|
await $`./scripts/deploy.sh staging`;
|
|
@@ -1627,7 +1633,7 @@ export const testSuite = workflow('test-suite', {
|
|
|
1627
1633
|
on: push({ branches: 'main' }),
|
|
1628
1634
|
jobs: [
|
|
1629
1635
|
job('test', {
|
|
1630
|
-
runsOn: '
|
|
1636
|
+
runsOn: 'linux',
|
|
1631
1637
|
steps: [
|
|
1632
1638
|
step('install', async ({ $ }) => {
|
|
1633
1639
|
await $`pnpm install --frozen-lockfile`;
|
|
@@ -1653,7 +1659,7 @@ export const autoDeploy = workflow('auto-deploy', {
|
|
|
1653
1659
|
on: kiciEvent({ name: 'tests-passed' }),
|
|
1654
1660
|
jobs: [
|
|
1655
1661
|
job('deploy', {
|
|
1656
|
-
runsOn: '
|
|
1662
|
+
runsOn: 'linux',
|
|
1657
1663
|
steps: [
|
|
1658
1664
|
step('deploy', async ({ $ }) => {
|
|
1659
1665
|
await $`./scripts/deploy.sh`;
|
|
@@ -1682,6 +1688,149 @@ export const autoDeploy = workflow('auto-deploy', {
|
|
|
1682
1688
|
|
|
1683
1689
|
# SDK reference
|
|
1684
1690
|
|
|
1691
|
+
## Caching
|
|
1692
|
+
|
|
1693
|
+
Source: https://docs.kici.dev/user/sdk/caching/
|
|
1694
|
+
|
|
1695
|
+
KiCI ships a general-purpose cache for any files or directories your workflow produces — compiled artifacts, downloaded toolchains, package manager stores, build outputs. A cache entry is keyed, immutable once written, and shared across runs of the same repository so a later run can restore what an earlier run produced instead of recomputing it.
|
|
1696
|
+
|
|
1697
|
+
Two surfaces drive the same cache:
|
|
1698
|
+
|
|
1699
|
+
- **Declarative** — a `cache` field on a job or a step. The runtime restores before the work runs and saves after it succeeds, with no code in your step body.
|
|
1700
|
+
- **Imperative** — `ctx.cache.restore(spec)` / `ctx.cache.save(spec)` inside a step body, for fine-grained control over when restore and save happen.
|
|
1701
|
+
|
|
1702
|
+
The cache is backed by the orchestrator's object storage. Entries are isolated per organization and per ref scope (see [Isolation](#isolation)); no other tenant can read your cache, and an untrusted/fork ref can never poison the cache a trusted branch reads.
|
|
1703
|
+
|
|
1704
|
+
## CacheSpec
|
|
1705
|
+
|
|
1706
|
+
Both surfaces take the same shape:
|
|
1707
|
+
|
|
1708
|
+
```typescript
|
|
1709
|
+
interface CacheSpec {
|
|
1710
|
+
/** Exact cache key. First save wins; re-saving an existing key is a no-op. */
|
|
1711
|
+
key: string;
|
|
1712
|
+
/** Files/directories to cache. Repo-root-relative or `~`-prefixed. */
|
|
1713
|
+
paths: string[];
|
|
1714
|
+
/** Ordered prefix fallbacks for partial restore; newest matching entry wins. */
|
|
1715
|
+
restoreKeys?: string[];
|
|
1716
|
+
}
|
|
1717
|
+
```
|
|
1718
|
+
|
|
1719
|
+
- **`key`** is the exact cache key. It is **immutable** — the first save under a given key wins, and any later save under the same exact key is a no-op (the existing entry is never overwritten). Build keys from inputs that change when the cached content should change, e.g. a hash of your lockfile: `` key: `deps-${await ctx.$`sha256sum pnpm-lock.yaml`}` ``.
|
|
1720
|
+
- **`paths`** are the files and directories to archive, repo-root-relative or `~`-prefixed (the agent expands `~` to the workspace home). At least one path is required.
|
|
1721
|
+
- **`restoreKeys`** are ordered **prefix** fallbacks tried only when the exact `key` misses on restore. Each prefix is matched against existing entries; the **newest** matching entry wins. This lets a run that changed its lockfile still restore the closest previous cache and rebuild incrementally.
|
|
1722
|
+
|
|
1723
|
+
## Declarative cache
|
|
1724
|
+
|
|
1725
|
+
Add a `cache` field to a job or a step. It accepts one `CacheSpec` or an array of them. The runtime restores every spec before the job/step runs (surfaced as a `cache:restore` pseudo-step) and saves every spec after it completes successfully (surfaced as a `cache:save` pseudo-step):
|
|
1726
|
+
|
|
1727
|
+
```typescript
|
|
1728
|
+
import { job } from '@kici-dev/sdk';
|
|
1729
|
+
|
|
1730
|
+
job('build', {
|
|
1731
|
+
runsOn: 'linux-x64',
|
|
1732
|
+
cache: {
|
|
1733
|
+
key: 'mise-tools-v1',
|
|
1734
|
+
paths: ['~/.local/share/mise'],
|
|
1735
|
+
},
|
|
1736
|
+
steps: [
|
|
1737
|
+
step('install-tools', async (ctx) => {
|
|
1738
|
+
await ctx.$`mise install`;
|
|
1739
|
+
}),
|
|
1740
|
+
step('build', async (ctx) => {
|
|
1741
|
+
await ctx.$`mise exec -- pnpm build`;
|
|
1742
|
+
}),
|
|
1743
|
+
],
|
|
1744
|
+
});
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
Step-level cache scopes the restore/save to a single step:
|
|
1748
|
+
|
|
1749
|
+
```typescript
|
|
1750
|
+
step('deps', {
|
|
1751
|
+
cache: { key: `npm-${lockfileHash}`, paths: ['node_modules'], restoreKeys: ['npm-'] },
|
|
1752
|
+
run: async (ctx) => {
|
|
1753
|
+
await ctx.$`pnpm install --frozen-lockfile`;
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
On a cache **hit**, the archived paths are restored before the step body runs, so `pnpm install` sees a warm `node_modules`. On a **miss**, the step runs cold and the resulting paths are saved under the exact key for the next run.
|
|
1759
|
+
|
|
1760
|
+
## Imperative cache (`ctx.cache`)
|
|
1761
|
+
|
|
1762
|
+
When you need to decide at runtime whether to restore or save — for example, save only when a build actually changed something — use the imperative API on the step context:
|
|
1763
|
+
|
|
1764
|
+
```typescript
|
|
1765
|
+
step('build', async (ctx) => {
|
|
1766
|
+
const result = await ctx.cache.restore({
|
|
1767
|
+
key: `build-${sourceHash}`,
|
|
1768
|
+
paths: ['dist'],
|
|
1769
|
+
restoreKeys: ['build-'],
|
|
1770
|
+
});
|
|
1771
|
+
|
|
1772
|
+
if (result.hit) {
|
|
1773
|
+
ctx.log.info(`restored cache (matched ${result.matchedKey})`);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
await ctx.$`pnpm build`;
|
|
1777
|
+
|
|
1778
|
+
await ctx.cache.save({ key: `build-${sourceHash}`, paths: ['dist'] });
|
|
1779
|
+
});
|
|
1780
|
+
```
|
|
1781
|
+
|
|
1782
|
+
`restore(spec)` returns `{ hit, matchedKey? }`:
|
|
1783
|
+
|
|
1784
|
+
- `hit` is `true` when the exact `key` matched **or** a `restoreKeys` prefix matched.
|
|
1785
|
+
- `matchedKey` is the full key that actually matched — the exact key on a direct hit, or the full key of the matched prefix entry on a fallback hit.
|
|
1786
|
+
|
|
1787
|
+
`save(spec)` archives `spec.paths` under `spec.key`. Like the declarative surface, it is immutable: the first save under an exact key wins, and re-saving the same key is a no-op.
|
|
1788
|
+
|
|
1789
|
+
## Restore semantics
|
|
1790
|
+
|
|
1791
|
+
A restore resolves in this order:
|
|
1792
|
+
|
|
1793
|
+
1. **Exact key.** If an entry exists under the exact `key`, it is restored and `matchedKey === key`.
|
|
1794
|
+
2. **restoreKeys prefix fallback.** Each `restoreKeys` prefix is tried in order. Within a prefix, the **newest** matching entry wins; `matchedKey` is that entry's full key.
|
|
1795
|
+
3. **Miss.** If nothing matches, `hit` is `false` and no paths are restored.
|
|
1796
|
+
|
|
1797
|
+
This mirrors the familiar lockfile-hash pattern: key the entry on the exact lockfile hash, and add a `restoreKeys` prefix so a changed lockfile still restores the most recent prior cache to rebuild from.
|
|
1798
|
+
|
|
1799
|
+
## Immutability
|
|
1800
|
+
|
|
1801
|
+
Cache keys are write-once. The **first** save under an exact key wins; every subsequent save under that same exact key is a no-op and the original bytes are preserved. To publish new content, use a new key (typically by including a content hash in the key). Immutability is what makes a cache hit safe to trust — the bytes behind a given key never change after they are first written.
|
|
1802
|
+
|
|
1803
|
+
## Isolation
|
|
1804
|
+
|
|
1805
|
+
Each cache entry is scoped to your organization and to the ref's trust level:
|
|
1806
|
+
|
|
1807
|
+
- **Trusted refs** (your repository's own branches, default branch) read and write a **shared** scope visible to the whole org for that repository.
|
|
1808
|
+
- **Untrusted / fork refs** read the shared scope as a fallback but write to an **isolated** per-run scope. A fork build can therefore benefit from a warm cache the trusted branch produced, but can never write into the shared scope — so a malicious fork cannot poison the cache a trusted branch later restores.
|
|
1809
|
+
|
|
1810
|
+
No tenant can read another tenant's cache; the org boundary is enforced in the cache key namespace.
|
|
1811
|
+
|
|
1812
|
+
## Eviction
|
|
1813
|
+
|
|
1814
|
+
Cache storage is bounded per organization. Two mechanisms keep it bounded:
|
|
1815
|
+
|
|
1816
|
+
- **Quota** — when a save pushes the org over its byte quota (`KICI_USER_CACHE_QUOTA_BYTES`, default 5 GiB), the oldest entries are evicted until the org is back under quota.
|
|
1817
|
+
- **TTL** — entries unused for `KICI_USER_CACHE_TTL_MS` (default 7 days) expire. The TTL refreshes on read (touch-on-read), so an actively used cache stays warm.
|
|
1818
|
+
|
|
1819
|
+
Both knobs are operator-configured on the orchestrator — see [orchestrator storage layout](../../operator/orchestrator/storage-layout.md).
|
|
1820
|
+
|
|
1821
|
+
## Observability
|
|
1822
|
+
|
|
1823
|
+
Each cache restore and save surfaces in the run timeline as a `cache:restore` / `cache:save` pseudo-step, reporting the outcome (hit/miss/saved, the matched key, bytes). The same outcomes are recorded as `cache.restore` / `cache.save` run events. See [data flows](../../architecture/data-flows.md#user-facing-cache-flow) for the restore/save protocol.
|
|
1824
|
+
|
|
1825
|
+
## See also
|
|
1826
|
+
|
|
1827
|
+
- [Core](./core.md) -- `job()` / `step()` factories the `cache` field attaches to
|
|
1828
|
+
- [Runtime](./runtime.md) -- `StepContext`, where `ctx.cache` lives
|
|
1829
|
+
- [Orchestrator storage layout](../../operator/orchestrator/storage-layout.md) -- cache prefix, quota, TTL, and eviction
|
|
1830
|
+
- [Data flows](../../architecture/data-flows.md#user-facing-cache-flow) -- restore/save protocol and trust→scope mapping
|
|
1831
|
+
|
|
1832
|
+
---
|
|
1833
|
+
|
|
1685
1834
|
## SDK reference: core
|
|
1686
1835
|
|
|
1687
1836
|
Source: https://docs.kici.dev/user/sdk/core/
|
|
@@ -1698,21 +1847,22 @@ function workflow(name: string, options: WorkflowOptions): Workflow;
|
|
|
1698
1847
|
|
|
1699
1848
|
**Parameters:**
|
|
1700
1849
|
|
|
1701
|
-
| Parameter | Type | Required | Description
|
|
1702
|
-
| --------------------- | ---------------------------------------------------------------------- | -------- |
|
|
1703
|
-
| `name` | `string` | yes | Unique workflow name
|
|
1704
|
-
| `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators
|
|
1705
|
-
| `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger
|
|
1706
|
-
| `options.rules` | `Rule[]` | no | Conditions that must pass for execution
|
|
1707
|
-
| `options.description` | `string` | no | Human-readable description
|
|
1708
|
-
| `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache.
|
|
1709
|
-
| `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<environment>:<secret>` syntax.
|
|
1710
|
-
| `options.installEnv` | `string[]` | no | Qualified `<environment>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`).
|
|
1711
|
-
| `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled
|
|
1712
|
-
| `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel)
|
|
1713
|
-
| `options.onSuccess` | `HookInput` | no | Runs on workflow success
|
|
1714
|
-
| `options.onFailure` | `HookInput` | no | Runs on workflow failure
|
|
1715
|
-
| `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](../concurrency.md).
|
|
1850
|
+
| Parameter | Type | Required | Description |
|
|
1851
|
+
| --------------------- | ---------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1852
|
+
| `name` | `string` | yes | Unique workflow name |
|
|
1853
|
+
| `options.jobs` | `JobOrFactory[]` | yes | Static jobs and/or dynamic job generators |
|
|
1854
|
+
| `options.on` | `Trigger \| Trigger[]` | no | When the workflow should trigger |
|
|
1855
|
+
| `options.rules` | `Rule[]` | no | Conditions that must pass for execution |
|
|
1856
|
+
| `options.description` | `string` | no | Human-readable description |
|
|
1857
|
+
| `options.hashFiles` | `string[]` | no | Extra repo-relative paths or globs mixed into the workflow content hash. Changes invalidate the source cache. |
|
|
1858
|
+
| `options.registries` | `Registry[]` | no | Private npm registries the agent authenticates against before `npm install`. Each `tokenSecret` uses qualified `<environment>:<secret>` syntax. |
|
|
1859
|
+
| `options.installEnv` | `string[]` | no | Qualified `<environment>:<secret>` refs projected as env vars onto the install subprocess (used with a customer-committed `.kici/.npmrc`). |
|
|
1860
|
+
| `options.onCancel` | `HookInput` | no | Runs when the workflow is cancelled |
|
|
1861
|
+
| `options.cleanup` | `HookInput` | no | Always runs after the workflow (success, failure, or cancel) |
|
|
1862
|
+
| `options.onSuccess` | `HookInput` | no | Runs on workflow success |
|
|
1863
|
+
| `options.onFailure` | `HookInput` | no | Runs on workflow failure |
|
|
1864
|
+
| `options.concurrency` | `{ group: (ctx) => string; cancelInProgress?: boolean; max?: number }` | no | Workflow-scoped concurrency. See [Concurrency](../concurrency.md). |
|
|
1865
|
+
| `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](#timeouts). |
|
|
1716
1866
|
|
|
1717
1867
|
**Returns:** `Workflow` -- an immutable workflow definition.
|
|
1718
1868
|
|
|
@@ -1738,45 +1888,47 @@ function job(options: JobOptions): Job;
|
|
|
1738
1888
|
|
|
1739
1889
|
**Parameters:**
|
|
1740
1890
|
|
|
1741
|
-
| Parameter | Type | Required | Description
|
|
1742
|
-
| -------------------------- | --------------------------------------------------------------- | -------------------- |
|
|
1743
|
-
| `name` | `string` | no | Job name (auto-generated UUID if omitted)
|
|
1744
|
-
| `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below)
|
|
1745
|
-
| `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`.
|
|
1746
|
-
| `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](#single-step-job-shorthand). Mutually exclusive with `steps`.
|
|
1747
|
-
| `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs)
|
|
1748
|
-
| `options.rules` | `Rule[]` | no | Conditions for conditional execution
|
|
1749
|
-
| `options.description` | `string` | no | Human-readable description
|
|
1750
|
-
| `options.matrix` | `Matrix` | no | Matrix configuration for job expansion
|
|
1751
|
-
| `options.include` | `MatrixInclude[]` | no | Additional matrix combinations
|
|
1752
|
-
| `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove
|
|
1753
|
-
| `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs.
|
|
1754
|
-
| `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.
|
|
1755
|
-
| `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](../dynamic-values.md).
|
|
1756
|
-
| `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md).
|
|
1757
|
-
| `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md).
|
|
1758
|
-
| `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled
|
|
1759
|
-
| `options.cleanup` | `HookInput` | no | Hook that always runs after completion
|
|
1760
|
-
| `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds
|
|
1761
|
-
| `options.onFailure` | `HookInput` | no | Hook that runs when the job fails
|
|
1762
|
-
| `options.beforeStep` | `HookInput` | no | Hook that runs before each step
|
|
1763
|
-
| `options.afterStep` | `HookInput` | no | Hook that runs after each step
|
|
1764
|
-
| `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout).
|
|
1765
|
-
| `options.
|
|
1891
|
+
| Parameter | Type | Required | Description |
|
|
1892
|
+
| -------------------------- | --------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1893
|
+
| `name` | `string` | no | Job name (auto-generated UUID if omitted) |
|
|
1894
|
+
| `options.runsOn` | `RunsOn` | yes | Runner label(s) and optional exclusions (see below) |
|
|
1895
|
+
| `options.steps` | `StepInput[]` | yes (or use `run`) | Steps to execute in order. Mutually exclusive with `run`. |
|
|
1896
|
+
| `options.run` | `(ctx) => Promise<unknown>` | yes (or use `steps`) | Single-step shorthand -- see [Single-step job shorthand](#single-step-job-shorthand). Mutually exclusive with `steps`. |
|
|
1897
|
+
| `options.needs` | `NeedsEntry[]` | no | Job dependencies (must complete first) -- see [Job dependencies (`needs`)](#job-dependencies-needs) |
|
|
1898
|
+
| `options.rules` | `Rule[]` | no | Conditions for conditional execution |
|
|
1899
|
+
| `options.description` | `string` | no | Human-readable description |
|
|
1900
|
+
| `options.matrix` | `Matrix` | no | Matrix configuration for job expansion |
|
|
1901
|
+
| `options.include` | `MatrixInclude[]` | no | Additional matrix combinations |
|
|
1902
|
+
| `options.exclude` | `MatrixExclude[]` | no | Matrix combinations to remove |
|
|
1903
|
+
| `options.checkout` | `boolean` | no (default: `true`) | When `false`, agent skips git clone. Useful for deploy/notify jobs. |
|
|
1904
|
+
| `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. |
|
|
1905
|
+
| `options.environment` | `string \| ((event) => string \| Promise<string>)` | no | Deployment environment for this job. Static string or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
|
|
1906
|
+
| `options.env` | `Record<string, string> \| ((event) => Record<string, string>)` | no | Environment variables. Static object or async/dynamic function -- see [Dynamic values](../dynamic-values.md). |
|
|
1907
|
+
| `options.concurrencyGroup` | `string \| ((event) => string \| Promise<string>)` | no | Concurrency group name (defaults to environment name) -- see [Concurrency](../concurrency.md). |
|
|
1908
|
+
| `options.onCancel` | `HookInput` | no | Hook that runs when the job is cancelled |
|
|
1909
|
+
| `options.cleanup` | `HookInput` | no | Hook that always runs after completion |
|
|
1910
|
+
| `options.onSuccess` | `HookInput` | no | Hook that runs when the job succeeds |
|
|
1911
|
+
| `options.onFailure` | `HookInput` | no | Hook that runs when the job fails |
|
|
1912
|
+
| `options.beforeStep` | `HookInput` | no | Hook that runs before each step |
|
|
1913
|
+
| `options.afterStep` | `HookInput` | no | Hook that runs after each step |
|
|
1914
|
+
| `options.gracePeriod` | `number` | no | Seconds before SIGKILL after SIGTERM during cancellation -- see [Hooks](../hooks.md#hook-timeout). |
|
|
1915
|
+
| `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](#timeouts). |
|
|
1916
|
+
| `options.resources` | `ResourceRequest` | no | Per-job CPU / memory request and limit. See [Per-job resources](#per-job-resources) below. |
|
|
1917
|
+
| `options.init` | `GenericInitConfig \| GenericInitConfig[] \| false` | no | Per-job initialization run after clone, before steps -- provisions a toolchain. See [Per-job init](#per-job-init) below. |
|
|
1766
1918
|
|
|
1767
1919
|
**Returns:** `Job` -- an immutable job definition.
|
|
1768
1920
|
|
|
1769
1921
|
```typescript
|
|
1770
1922
|
// Named job
|
|
1771
1923
|
const build = job('build', {
|
|
1772
|
-
runsOn: '
|
|
1924
|
+
runsOn: 'linux',
|
|
1773
1925
|
steps: [checkout, install, compile],
|
|
1774
1926
|
needs: [lint],
|
|
1775
1927
|
});
|
|
1776
1928
|
|
|
1777
1929
|
// Anonymous job (auto-generated UUID name)
|
|
1778
1930
|
const build = job({
|
|
1779
|
-
runsOn: '
|
|
1931
|
+
runsOn: 'linux',
|
|
1780
1932
|
steps: [checkout, install],
|
|
1781
1933
|
});
|
|
1782
1934
|
```
|
|
@@ -1802,7 +1954,7 @@ runsOn: { labels: ['linux', 'docker'], exclude: ['gpu'] }
|
|
|
1802
1954
|
- **Required labels:** The agent must have every label in the `labels` array (or the string/array form).
|
|
1803
1955
|
- **Excluded labels:** The agent must NOT have any label in the `exclude` array. This includes auto-derived labels like `kici:arch:arm64`, `kici:os:linux`, etc.
|
|
1804
1956
|
- **Compile-time validation:** The compiler will error if any label appears in both `labels` and `exclude` (overlap detection).
|
|
1805
|
-
- **Operator-declared mandatory labels:** Operators may mark a scaler with `mandatoryLabels` (Kubernetes-taint-style opt-in). When a scaler declares a mandatory label, a job is only allowed to land on it if `runsOn.labels` includes that label. A workflow targeting such a scaler must explicitly list the mandatory label in `runsOn`. See the [auto-scaler
|
|
1957
|
+
- **Operator-declared mandatory labels:** Operators may mark a scaler with `mandatoryLabels` (Kubernetes-taint-style opt-in). When a scaler declares a mandatory label, a job is only allowed to land on it if `runsOn.labels` includes that label. A workflow targeting such a scaler must explicitly list the mandatory label in `runsOn`. See the [auto-scaler mandatory labels](../../operator/orchestrator/auto-scaler/common-config.md#mandatory--exclude-labels) for details.
|
|
1806
1958
|
|
|
1807
1959
|
```typescript
|
|
1808
1960
|
// Route to any Linux agent that does NOT have the 'gpu' label
|
|
@@ -1899,6 +2051,65 @@ Per-backend kernel enforcement of `limits`:
|
|
|
1899
2051
|
- **Firecracker backend:** always enforced. Fractional CPU rounds up to the nearest integer vCPU.
|
|
1900
2052
|
- **Bare-metal backend:** advisory by default — the scaler caps still apply, but no cgroup is created. Operators can opt in to kernel enforcement via `enforceCgroups: true` on the scaler entry.
|
|
1901
2053
|
|
|
2054
|
+
### Per-job init
|
|
2055
|
+
|
|
2056
|
+
`options.init` declares a hand-written command that runs **after the repo is cloned and before the job's steps execute**. Its purpose is to provision a repo-declared toolchain (a `mise` toolchain, a custom setup script, a language runtime) and put it on the environment every subsequent step sees.
|
|
2057
|
+
|
|
2058
|
+
```typescript
|
|
2059
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
2060
|
+
|
|
2061
|
+
export const build = workflow('build', {
|
|
2062
|
+
on: [push()],
|
|
2063
|
+
jobs: [
|
|
2064
|
+
job('build', {
|
|
2065
|
+
runsOn: 'linux',
|
|
2066
|
+
init: {
|
|
2067
|
+
run: `
|
|
2068
|
+
set -euo pipefail
|
|
2069
|
+
command -v mise >/dev/null || curl -fsSL https://mise.run | sh
|
|
2070
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
2071
|
+
mise install
|
|
2072
|
+
mise env -s bash | sed -n 's/^export //p' >> "$KICI_ENV"
|
|
2073
|
+
echo "$HOME/.local/share/mise/shims" >> "$KICI_PATH"
|
|
2074
|
+
`,
|
|
2075
|
+
cache: { key: 'mise-jq-1.7.1', paths: ['~/.local/share/mise'] },
|
|
2076
|
+
timeout: 600_000,
|
|
2077
|
+
},
|
|
2078
|
+
steps: [
|
|
2079
|
+
step('show-jq-version', async (ctx) => {
|
|
2080
|
+
// jq is on PATH because the init phase appended the mise shims dir to $KICI_PATH.
|
|
2081
|
+
const { stdout } = await ctx.$`jq --version`;
|
|
2082
|
+
ctx.log.info(`jq version: ${stdout.trim()}`);
|
|
2083
|
+
}),
|
|
2084
|
+
],
|
|
2085
|
+
}),
|
|
2086
|
+
],
|
|
2087
|
+
});
|
|
2088
|
+
```
|
|
2089
|
+
|
|
2090
|
+
**`GenericInitConfig` shape:**
|
|
2091
|
+
|
|
2092
|
+
| Field | Type | Required | Description |
|
|
2093
|
+
| --------- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2094
|
+
| `run` | `string` | yes | Command run after clone, before steps. Runs in the job's sandbox at the clone root. Must be a non-empty command. |
|
|
2095
|
+
| `shell` | `string` | no | Shell used to run `run`. Defaults to `bash`. |
|
|
2096
|
+
| `cache` | `CacheSpec` | no | Cache spec for binaries the command installs -- restored before the command, saved after on a key miss. See [Caching](./caching.md). |
|
|
2097
|
+
| `timeout` | `number` | no | Max wall-clock for this init command in milliseconds. Defaults to 10 minutes. On breach the init is aborted and the job is reported timed out. |
|
|
2098
|
+
| `env` | `Record<string,string>` | no | Static environment variables available to the command. |
|
|
2099
|
+
|
|
2100
|
+
**The `$KICI_ENV` / `$KICI_PATH` handoff.** The init command does not mutate the step environment directly. Instead it writes what it wants visible to later steps to two files the agent allocates and exposes as environment variables:
|
|
2101
|
+
|
|
2102
|
+
- **`$KICI_ENV`** -- append one `KEY=value` line per environment variable. The agent reads the file after the command and makes each variable available to every subsequent step.
|
|
2103
|
+
- **`$KICI_PATH`** -- append one directory per line. The agent prepends each directory to `PATH` for every subsequent step.
|
|
2104
|
+
|
|
2105
|
+
The agent reads both files after the command succeeds, applies the delta, and the resulting environment is visible to all steps that follow (and to any later init command).
|
|
2106
|
+
|
|
2107
|
+
**Failure before steps.** If the init command exits non-zero or exceeds its `timeout`, the job **fails before any step runs** -- the init surfaces as a failed `init:<n>` pseudo-step in the run timeline (alongside the step list), its logs are attached, and the step loop never executes. This makes a broken toolchain a clear, early failure rather than a confusing mid-run error.
|
|
2108
|
+
|
|
2109
|
+
**Arrays run in order.** Passing `GenericInitConfig[]` runs the inits sequentially; each one's `$KICI_ENV` / `$KICI_PATH` delta is applied before the next runs, so a later init sees an earlier init's tools on `PATH`. The first init to fail stops the sequence and fails the job.
|
|
2110
|
+
|
|
2111
|
+
**`init: false`** is an explicit opt-out (reserved for a future auto-detect layer); it behaves the same as omitting `init`.
|
|
2112
|
+
|
|
1902
2113
|
## Step & job authoring patterns
|
|
1903
2114
|
|
|
1904
2115
|
KiCI supports several authoring patterns for steps and jobs to reduce boilerplate and improve developer experience.
|
|
@@ -1997,6 +2208,44 @@ const deploy = job('deploy', {
|
|
|
1997
2208
|
|
|
1998
2209
|
The `run` function is stored as the job's only step with an auto-generated name (`step-1`). `run` and `steps` are mutually exclusive -- providing both throws an error.
|
|
1999
2210
|
|
|
2211
|
+
### Timeouts
|
|
2212
|
+
|
|
2213
|
+
`timeout` (milliseconds) can be set at three levels. Each level caps **its own scope** independently — a workflow or job timeout is a separate wall-clock cap, **not** a default that flows down to steps.
|
|
2214
|
+
|
|
2215
|
+
| Level | Field | Caps | Enforced by | On breach |
|
|
2216
|
+
| ------------ | ---------------------------- | ------------------------------------------------------ | ---------------- | ----------------------------------------------------------------- |
|
|
2217
|
+
| **step** | `step(..., { timeout })` | A single step's wall-clock. | the agent | The step fails; falls back to the 30-minute default when unset. |
|
|
2218
|
+
| **job** | `job(..., { timeout })` | The job's total wall-clock (init + all steps + hooks). | the agent | The job is aborted and reported failed with a "timed out" reason. |
|
|
2219
|
+
| **workflow** | `workflow(..., { timeout })` | The whole run's wall-clock across all jobs. | the orchestrator | The run is cancelled with a "timed out" reason. |
|
|
2220
|
+
|
|
2221
|
+
```typescript
|
|
2222
|
+
export default workflow('ci', {
|
|
2223
|
+
timeout: 1_800_000, // whole run must finish within 30 minutes
|
|
2224
|
+
jobs: [
|
|
2225
|
+
job('build', {
|
|
2226
|
+
runsOn: 'linux',
|
|
2227
|
+
timeout: 600_000, // this job (init + steps + hooks) within 10 minutes
|
|
2228
|
+
steps: [
|
|
2229
|
+
step('compile', {
|
|
2230
|
+
timeout: 120_000, // this single step within 2 minutes
|
|
2231
|
+
run: async (ctx) => {
|
|
2232
|
+
await ctx.$`make build`;
|
|
2233
|
+
},
|
|
2234
|
+
}),
|
|
2235
|
+
],
|
|
2236
|
+
}),
|
|
2237
|
+
],
|
|
2238
|
+
});
|
|
2239
|
+
```
|
|
2240
|
+
|
|
2241
|
+
**Precedence — each scope caps its own scope.** The three timeouts are independent caps, not a fallback chain:
|
|
2242
|
+
|
|
2243
|
+
- A **step** with no `timeout` falls back to the 30-minute agent default, regardless of the job or workflow timeout. A job timeout never becomes a step's default.
|
|
2244
|
+
- A **job** `timeout` bounds the job's total wall-clock (its init, every step including their own per-step timeouts, and its hooks). It does not change any step's individual cap.
|
|
2245
|
+
- A **workflow** `timeout` is a run-level deadline. The orchestrator records it when the run starts and cancels the run if its wall-clock exceeds the timeout, even when individual jobs and steps are still within their own caps.
|
|
2246
|
+
|
|
2247
|
+
Workflow and job timeouts surface with a distinct "timed out" reason so the dashboard labels the run or job as timed out rather than a generic failure or cancel.
|
|
2248
|
+
|
|
2000
2249
|
### Output chaining
|
|
2001
2250
|
|
|
2002
2251
|
Steps and jobs can access outputs from preceding steps/jobs using two patterns.
|
|
@@ -2169,6 +2418,354 @@ These IDs are stable as long as the order of unnamed entries does not change. Ad
|
|
|
2169
2418
|
|
|
2170
2419
|
---
|
|
2171
2420
|
|
|
2421
|
+
## Event payload reference
|
|
2422
|
+
|
|
2423
|
+
Source: https://docs.kici.dev/user/sdk/event-payloads/
|
|
2424
|
+
|
|
2425
|
+
<!-- Generated by scripts/docs-gen-event-payloads.ts — do not edit by hand. Regenerate: pnpm docs:gen:events -->
|
|
2426
|
+
|
|
2427
|
+
## The envelope
|
|
2428
|
+
|
|
2429
|
+
The normalized event envelope is the single event contract in KiCI. Rules receive it as `ctx.event`, and every dynamic function — `environment:`, `env:`, and `concurrencyGroup:` resolvers, generated jobs, and a workflow's `concurrency.group` — receives the same envelope as its argument.
|
|
2430
|
+
|
|
2431
|
+
Narrow on the `type` field to branch per trigger kind (`if (event.type === 'push')`). The raw provider webhook body is nested at `payload`; the typed variants below describe its shape per event type.
|
|
2432
|
+
|
|
2433
|
+
These fields are present on every envelope (the `EventBase` shape):
|
|
2434
|
+
|
|
2435
|
+
| Field | Type | Description |
|
|
2436
|
+
| ----------------- | ------------------------- | ------------------------------------------------------------------------------- |
|
|
2437
|
+
| `type` | `string` | Normalized event type discriminant. |
|
|
2438
|
+
| `action?` | `string` | Sub-action (e.g. 'opened', 'created', 'submitted'). |
|
|
2439
|
+
| `targetBranch?` | `string` | Target branch (push target, PR base, or default branch). |
|
|
2440
|
+
| `sourceBranch?` | `string` | Source branch (PR head branch). Only set for PR-like events. |
|
|
2441
|
+
| `provider?` | `string` | Provider that originated this event. |
|
|
2442
|
+
| `isForkPR?` | `boolean` | Whether this PR comes from a fork. Only set for PR-like events. |
|
|
2443
|
+
| `baseBranch?` | `string` | Base branch ref for PR events. |
|
|
2444
|
+
| `senderUsername?` | `string` | Sender username from the webhook payload. |
|
|
2445
|
+
| `sourceRepo?` | `string` | Repository identifier (e.g. "owner/repo"). |
|
|
2446
|
+
| `changedFiles?` | `string[]` | Files changed in this event (for path filtering). |
|
|
2447
|
+
| `payload?` | `Record<string, unknown>` | Raw webhook payload from the provider. May be absent in flattened event forms. |
|
|
2448
|
+
| `[key: string]` | `unknown` | Index signature for backward compatibility — untyped fields resolve to unknown. |
|
|
2449
|
+
|
|
2450
|
+
## Event types
|
|
2451
|
+
|
|
2452
|
+
One section per member of the `EventPayload` union. The heading is the `type` literal; the table lists the fields of that event's `payload` property when it declares a typed shape.
|
|
2453
|
+
|
|
2454
|
+
### `pull_request`
|
|
2455
|
+
|
|
2456
|
+
Carried by `PullRequestEventPayload`. The `payload` property has the following shape:
|
|
2457
|
+
|
|
2458
|
+
| Field | Type | Description |
|
|
2459
|
+
| --------------- | ------------------- | ----------- |
|
|
2460
|
+
| `action` | `string` | |
|
|
2461
|
+
| `number` | `number` | |
|
|
2462
|
+
| `pull_request` | `GitHubPullRequest` | |
|
|
2463
|
+
| `repository` | `GitHubRepository` | |
|
|
2464
|
+
| `sender` | `GitHubUser` | |
|
|
2465
|
+
| `[key: string]` | `unknown` | |
|
|
2466
|
+
|
|
2467
|
+
### `push`
|
|
2468
|
+
|
|
2469
|
+
Carried by `PushEventPayload`. The `payload` property has the following shape:
|
|
2470
|
+
|
|
2471
|
+
| Field | Type | Description |
|
|
2472
|
+
| --------------- | ------------------ | ----------- |
|
|
2473
|
+
| `ref` | `string` | |
|
|
2474
|
+
| `after` | `string` | |
|
|
2475
|
+
| `before` | `string` | |
|
|
2476
|
+
| `head_commit?` | `GitHubCommit` | |
|
|
2477
|
+
| `commits?` | `GitHubCommit[]` | |
|
|
2478
|
+
| `repository` | `GitHubRepository` | |
|
|
2479
|
+
| `sender?` | `GitHubUser` | |
|
|
2480
|
+
| `forced?` | `boolean` | |
|
|
2481
|
+
| `[key: string]` | `unknown` | |
|
|
2482
|
+
|
|
2483
|
+
### `tag`
|
|
2484
|
+
|
|
2485
|
+
Carried by `TagEventPayload`. The `payload` property has the following shape:
|
|
2486
|
+
|
|
2487
|
+
| Field | Type | Description |
|
|
2488
|
+
| --------------- | ------------------ | ----------- |
|
|
2489
|
+
| `ref` | `string` | |
|
|
2490
|
+
| `after` | `string` | |
|
|
2491
|
+
| `repository` | `GitHubRepository` | |
|
|
2492
|
+
| `sender?` | `GitHubUser` | |
|
|
2493
|
+
| `[key: string]` | `unknown` | |
|
|
2494
|
+
|
|
2495
|
+
### `comment`
|
|
2496
|
+
|
|
2497
|
+
Carried by `CommentEventPayload`. The `payload` property has the following shape:
|
|
2498
|
+
|
|
2499
|
+
| Field | Type | Description |
|
|
2500
|
+
| --------------- | ------------------------------------------------------------------------------------ | ----------- |
|
|
2501
|
+
| `action` | `string` | |
|
|
2502
|
+
| `comment` | `GitHubComment` | |
|
|
2503
|
+
| `issue?` | `{ number: number; title?: string; pull_request?: unknown; [key: string]: unknown }` | |
|
|
2504
|
+
| `repository` | `GitHubRepository` | |
|
|
2505
|
+
| `sender` | `GitHubUser` | |
|
|
2506
|
+
| `[key: string]` | `unknown` | |
|
|
2507
|
+
|
|
2508
|
+
### `review`
|
|
2509
|
+
|
|
2510
|
+
Carried by `ReviewEventPayload`. The `payload` property has the following shape:
|
|
2511
|
+
|
|
2512
|
+
| Field | Type | Description |
|
|
2513
|
+
| --------------- | ------------------- | ----------- |
|
|
2514
|
+
| `action` | `string` | |
|
|
2515
|
+
| `review` | `GitHubReview` | |
|
|
2516
|
+
| `pull_request` | `GitHubPullRequest` | |
|
|
2517
|
+
| `repository` | `GitHubRepository` | |
|
|
2518
|
+
| `sender` | `GitHubUser` | |
|
|
2519
|
+
| `[key: string]` | `unknown` | |
|
|
2520
|
+
|
|
2521
|
+
### `review_comment`
|
|
2522
|
+
|
|
2523
|
+
Carried by `ReviewCommentEventPayload`. The `payload` property has the following shape:
|
|
2524
|
+
|
|
2525
|
+
| Field | Type | Description |
|
|
2526
|
+
| --------------- | ------------------- | ----------- |
|
|
2527
|
+
| `action` | `string` | |
|
|
2528
|
+
| `comment` | `GitHubComment` | |
|
|
2529
|
+
| `pull_request` | `GitHubPullRequest` | |
|
|
2530
|
+
| `repository` | `GitHubRepository` | |
|
|
2531
|
+
| `sender` | `GitHubUser` | |
|
|
2532
|
+
| `[key: string]` | `unknown` | |
|
|
2533
|
+
|
|
2534
|
+
### `release`
|
|
2535
|
+
|
|
2536
|
+
Carried by `ReleaseEventPayload`. The `payload` property has the following shape:
|
|
2537
|
+
|
|
2538
|
+
| Field | Type | Description |
|
|
2539
|
+
| --------------- | ------------------ | ----------- |
|
|
2540
|
+
| `action` | `string` | |
|
|
2541
|
+
| `release` | `GitHubRelease` | |
|
|
2542
|
+
| `repository` | `GitHubRepository` | |
|
|
2543
|
+
| `sender` | `GitHubUser` | |
|
|
2544
|
+
| `[key: string]` | `unknown` | |
|
|
2545
|
+
|
|
2546
|
+
### `dispatch`
|
|
2547
|
+
|
|
2548
|
+
Carried by `DispatchEventPayload`. The `payload` property has the following shape:
|
|
2549
|
+
|
|
2550
|
+
| Field | Type | Description |
|
|
2551
|
+
| ----------------- | ------------------------- | ----------- |
|
|
2552
|
+
| `action` | `string` | |
|
|
2553
|
+
| `client_payload?` | `Record<string, unknown>` | |
|
|
2554
|
+
| `repository` | `GitHubRepository` | |
|
|
2555
|
+
| `sender?` | `GitHubUser` | |
|
|
2556
|
+
| `[key: string]` | `unknown` | |
|
|
2557
|
+
|
|
2558
|
+
### `create`
|
|
2559
|
+
|
|
2560
|
+
Carried by `CreateEventPayload`. The `payload` property has the following shape:
|
|
2561
|
+
|
|
2562
|
+
| Field | Type | Description |
|
|
2563
|
+
| --------------- | ------------------ | ----------- |
|
|
2564
|
+
| `ref` | `string` | |
|
|
2565
|
+
| `ref_type` | `string` | |
|
|
2566
|
+
| `repository` | `GitHubRepository` | |
|
|
2567
|
+
| `sender` | `GitHubUser` | |
|
|
2568
|
+
| `[key: string]` | `unknown` | |
|
|
2569
|
+
|
|
2570
|
+
### `delete`
|
|
2571
|
+
|
|
2572
|
+
Carried by `DeleteEventPayload`. The `payload` property has the following shape:
|
|
2573
|
+
|
|
2574
|
+
| Field | Type | Description |
|
|
2575
|
+
| --------------- | ------------------ | ----------- |
|
|
2576
|
+
| `ref` | `string` | |
|
|
2577
|
+
| `ref_type` | `string` | |
|
|
2578
|
+
| `repository` | `GitHubRepository` | |
|
|
2579
|
+
| `sender` | `GitHubUser` | |
|
|
2580
|
+
| `[key: string]` | `unknown` | |
|
|
2581
|
+
|
|
2582
|
+
### `status`
|
|
2583
|
+
|
|
2584
|
+
Carried by `StatusEventPayload`. The `payload` property has the following shape:
|
|
2585
|
+
|
|
2586
|
+
| Field | Type | Description |
|
|
2587
|
+
| --------------- | ------------------------------------------------- | ----------- |
|
|
2588
|
+
| `state` | `string` | |
|
|
2589
|
+
| `sha` | `string` | |
|
|
2590
|
+
| `context` | `string` | |
|
|
2591
|
+
| `description?` | `string` | |
|
|
2592
|
+
| `target_url?` | `string` | |
|
|
2593
|
+
| `branches?` | `Array<{ name: string; [key: string]: unknown }>` | |
|
|
2594
|
+
| `repository` | `GitHubRepository` | |
|
|
2595
|
+
| `sender` | `GitHubUser` | |
|
|
2596
|
+
| `[key: string]` | `unknown` | |
|
|
2597
|
+
|
|
2598
|
+
### `workflow_run`
|
|
2599
|
+
|
|
2600
|
+
Carried by `WorkflowRunEventPayload`. The `payload` property has the following shape:
|
|
2601
|
+
|
|
2602
|
+
| Field | Type | Description |
|
|
2603
|
+
| --------------- | ------------------------------------------------------------------------------------------------------ | ----------- |
|
|
2604
|
+
| `action` | `string` | |
|
|
2605
|
+
| `workflow_run` | `{ head_branch: string; name: string; conclusion?: string; status?: string; [key: string]: unknown; }` | |
|
|
2606
|
+
| `repository` | `GitHubRepository` | |
|
|
2607
|
+
| `sender` | `GitHubUser` | |
|
|
2608
|
+
| `[key: string]` | `unknown` | |
|
|
2609
|
+
|
|
2610
|
+
### `fork`
|
|
2611
|
+
|
|
2612
|
+
Carried by `ForkEventPayload`. The `payload` property has the following shape:
|
|
2613
|
+
|
|
2614
|
+
| Field | Type | Description |
|
|
2615
|
+
| --------------- | ----------------------------------------------- | ----------- |
|
|
2616
|
+
| `forkee` | `{ full_name: string; [key: string]: unknown }` | |
|
|
2617
|
+
| `repository` | `GitHubRepository` | |
|
|
2618
|
+
| `sender` | `GitHubUser` | |
|
|
2619
|
+
| `[key: string]` | `unknown` | |
|
|
2620
|
+
|
|
2621
|
+
### `star`
|
|
2622
|
+
|
|
2623
|
+
Carried by `StarEventPayload`. The `payload` property has the following shape:
|
|
2624
|
+
|
|
2625
|
+
| Field | Type | Description |
|
|
2626
|
+
| --------------- | ------------------ | ----------- |
|
|
2627
|
+
| `action` | `string` | |
|
|
2628
|
+
| `repository` | `GitHubRepository` | |
|
|
2629
|
+
| `sender` | `GitHubUser` | |
|
|
2630
|
+
| `[key: string]` | `unknown` | |
|
|
2631
|
+
|
|
2632
|
+
### `watch`
|
|
2633
|
+
|
|
2634
|
+
Carried by `WatchEventPayload`. The `payload` property has the following shape:
|
|
2635
|
+
|
|
2636
|
+
| Field | Type | Description |
|
|
2637
|
+
| --------------- | ------------------ | ----------- |
|
|
2638
|
+
| `action` | `string` | |
|
|
2639
|
+
| `repository` | `GitHubRepository` | |
|
|
2640
|
+
| `sender` | `GitHubUser` | |
|
|
2641
|
+
| `[key: string]` | `unknown` | |
|
|
2642
|
+
|
|
2643
|
+
### `webhook`
|
|
2644
|
+
|
|
2645
|
+
Carried by `WebhookEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2646
|
+
|
|
2647
|
+
### `kici_event`
|
|
2648
|
+
|
|
2649
|
+
Carried by `KiciEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2650
|
+
|
|
2651
|
+
### `workflow_complete`
|
|
2652
|
+
|
|
2653
|
+
Carried by `WorkflowCompleteEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2654
|
+
|
|
2655
|
+
### `job_complete`
|
|
2656
|
+
|
|
2657
|
+
Carried by `JobCompleteEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2658
|
+
|
|
2659
|
+
### `generic_webhook`
|
|
2660
|
+
|
|
2661
|
+
Carried by `GenericWebhookEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2662
|
+
|
|
2663
|
+
### `schedule`
|
|
2664
|
+
|
|
2665
|
+
Carried by `ScheduleEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2666
|
+
|
|
2667
|
+
### `lifecycle`
|
|
2668
|
+
|
|
2669
|
+
Carried by `LifecycleEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2670
|
+
|
|
2671
|
+
### `rerun`
|
|
2672
|
+
|
|
2673
|
+
Carried by `RerunEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2674
|
+
|
|
2675
|
+
### `manual_schedule`
|
|
2676
|
+
|
|
2677
|
+
Carried by `ManualScheduleEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2678
|
+
|
|
2679
|
+
### `unknown`
|
|
2680
|
+
|
|
2681
|
+
Carried by `UnknownEventPayload`. Adds no typed fields beyond the shared envelope; `payload` is the raw provider body (`Record<string, unknown>`).
|
|
2682
|
+
|
|
2683
|
+
## Shared GitHub object shapes
|
|
2684
|
+
|
|
2685
|
+
The typed `payload` shapes above reference these partial GitHub object types. Each lists only the commonly accessed fields; the index signature on every shape resolves any other field to `unknown`.
|
|
2686
|
+
|
|
2687
|
+
### `GitHubRepository`
|
|
2688
|
+
|
|
2689
|
+
| Field | Type | Description |
|
|
2690
|
+
| ---------------- | ------------------------------------------- | ----------- |
|
|
2691
|
+
| `full_name` | `string` | |
|
|
2692
|
+
| `default_branch` | `string` | |
|
|
2693
|
+
| `name?` | `string` | |
|
|
2694
|
+
| `owner?` | `{ login: string; [key: string]: unknown }` | |
|
|
2695
|
+
| `private?` | `boolean` | |
|
|
2696
|
+
| `[key: string]` | `unknown` | |
|
|
2697
|
+
|
|
2698
|
+
### `GitHubUser`
|
|
2699
|
+
|
|
2700
|
+
| Field | Type | Description |
|
|
2701
|
+
| --------------- | --------- | ----------- |
|
|
2702
|
+
| `login` | `string` | |
|
|
2703
|
+
| `id?` | `number` | |
|
|
2704
|
+
| `[key: string]` | `unknown` | |
|
|
2705
|
+
|
|
2706
|
+
### `GitHubPullRequest`
|
|
2707
|
+
|
|
2708
|
+
| Field | Type | Description |
|
|
2709
|
+
| --------------- | ------------------------------------------------------------------------------------------------------------- | ----------- |
|
|
2710
|
+
| `number` | `number` | |
|
|
2711
|
+
| `draft?` | `boolean` | |
|
|
2712
|
+
| `title?` | `string` | |
|
|
2713
|
+
| `body?` | `string` | |
|
|
2714
|
+
| `state?` | `string` | |
|
|
2715
|
+
| `merged?` | `boolean` | |
|
|
2716
|
+
| `head` | `{ ref: string; sha: string; repo?: { full_name: string; [key: string]: unknown }; [key: string]: unknown; }` | |
|
|
2717
|
+
| `base` | `{ ref: string; repo?: { full_name: string; [key: string]: unknown }; [key: string]: unknown; }` | |
|
|
2718
|
+
| `user?` | `GitHubUser` | |
|
|
2719
|
+
| `labels?` | `Array<{ name: string; [key: string]: unknown }>` | |
|
|
2720
|
+
| `[key: string]` | `unknown` | |
|
|
2721
|
+
|
|
2722
|
+
### `GitHubCommit`
|
|
2723
|
+
|
|
2724
|
+
| Field | Type | Description |
|
|
2725
|
+
| --------------- | ------------------------------------------------------------------------------ | ----------- |
|
|
2726
|
+
| `id` | `string` | |
|
|
2727
|
+
| `message` | `string` | |
|
|
2728
|
+
| `author?` | `{ name?: string; email?: string; username?: string; [key: string]: unknown }` | |
|
|
2729
|
+
| `timestamp?` | `string` | |
|
|
2730
|
+
| `added?` | `string[]` | |
|
|
2731
|
+
| `removed?` | `string[]` | |
|
|
2732
|
+
| `modified?` | `string[]` | |
|
|
2733
|
+
| `[key: string]` | `unknown` | |
|
|
2734
|
+
|
|
2735
|
+
### `GitHubComment`
|
|
2736
|
+
|
|
2737
|
+
| Field | Type | Description |
|
|
2738
|
+
| --------------- | ------------ | ----------- |
|
|
2739
|
+
| `id` | `number` | |
|
|
2740
|
+
| `body` | `string` | |
|
|
2741
|
+
| `user` | `GitHubUser` | |
|
|
2742
|
+
| `[key: string]` | `unknown` | |
|
|
2743
|
+
|
|
2744
|
+
### `GitHubReview`
|
|
2745
|
+
|
|
2746
|
+
| Field | Type | Description |
|
|
2747
|
+
| --------------- | ------------ | ----------- |
|
|
2748
|
+
| `id` | `number` | |
|
|
2749
|
+
| `state` | `string` | |
|
|
2750
|
+
| `body?` | `string` | |
|
|
2751
|
+
| `user` | `GitHubUser` | |
|
|
2752
|
+
| `[key: string]` | `unknown` | |
|
|
2753
|
+
|
|
2754
|
+
### `GitHubRelease`
|
|
2755
|
+
|
|
2756
|
+
| Field | Type | Description |
|
|
2757
|
+
| ------------------- | --------- | ----------- |
|
|
2758
|
+
| `id` | `number` | |
|
|
2759
|
+
| `tag_name` | `string` | |
|
|
2760
|
+
| `name?` | `string` | |
|
|
2761
|
+
| `body?` | `string` | |
|
|
2762
|
+
| `draft?` | `boolean` | |
|
|
2763
|
+
| `prerelease?` | `boolean` | |
|
|
2764
|
+
| `target_commitish?` | `string` | |
|
|
2765
|
+
| `[key: string]` | `unknown` | |
|
|
2766
|
+
|
|
2767
|
+
---
|
|
2768
|
+
|
|
2172
2769
|
## SDK reference: idempotent
|
|
2173
2770
|
|
|
2174
2771
|
Source: https://docs.kici.dev/user/sdk/idempotent/
|
|
@@ -2442,17 +3039,7 @@ You can also narrow directly with `if (ctx.event.type === 'pull_request')` — T
|
|
|
2442
3039
|
|
|
2443
3040
|
`EventPayload` is a discriminated union over the `type` field. Each variant provides typed access to the normalized event fields and the raw webhook payload.
|
|
2444
3041
|
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
| Field | Type | Description |
|
|
2448
|
-
| ---------------- | ------------------------------ | --------------------------------------- |
|
|
2449
|
-
| `type` | `string` (literal per variant) | Event type discriminant |
|
|
2450
|
-
| `action` | `string \| undefined` | Sub-action (e.g. 'opened', 'created') |
|
|
2451
|
-
| `targetBranch` | `string \| undefined` | Target branch (push target, PR base) |
|
|
2452
|
-
| `sourceBranch` | `string \| undefined` | Source branch (PR head) |
|
|
2453
|
-
| `provider` | `string \| undefined` | Provider name (e.g. 'github') |
|
|
2454
|
-
| `senderUsername` | `string \| undefined` | Webhook sender username |
|
|
2455
|
-
| `payload` | `Record<string, unknown>` | Raw webhook payload (typed per variant) |
|
|
3042
|
+
Every variant carries the shared `EventBase` fields — `type`, `action`, `targetBranch`, `sourceBranch`, `provider`, `isForkPR`, `baseBranch`, `senderUsername`, `sourceRepo`, `changedFiles`, and the raw `payload` — plus a per-type `payload` shape for the typed variants. The complete field-by-field schema, including every typed `payload` shape and the shared GitHub object types, is in the [event payload reference](./event-payloads.md).
|
|
2456
3043
|
|
|
2457
3044
|
**Typed variants** (with GitHub-specific payload fields): `pull_request`, `push`, `tag`, `comment`, `review`, `review_comment`, `release`, `dispatch`, `create`, `delete`, `status`, `workflow_run`, `fork`, `star`, `watch`.
|
|
2458
3045
|
|
|
@@ -2480,16 +3067,16 @@ step('test', async ({ $, matrix }) => {
|
|
|
2480
3067
|
|
|
2481
3068
|
```typescript
|
|
2482
3069
|
matrix: {
|
|
2483
|
-
os: ['
|
|
3070
|
+
os: ['linux', 'arm64'],
|
|
2484
3071
|
node: ['18', '20'],
|
|
2485
3072
|
}
|
|
2486
3073
|
```
|
|
2487
3074
|
|
|
2488
|
-
Creates 4 job instances (2 x 2). In steps, values are named properties:
|
|
3075
|
+
Creates 4 job instances (2 x 2). The `os` values (`linux`, `arm64`) are **customer-defined scaler labels** matched by subset semantics against the labels your orchestrator advertises in its scaler `labelSets` — not hosted-runner names. In steps, values are named properties:
|
|
2489
3076
|
|
|
2490
3077
|
```typescript
|
|
2491
3078
|
step('test', async ({ $, matrix }) => {
|
|
2492
|
-
console.log(matrix!.os); // '
|
|
3079
|
+
console.log(matrix!.os); // 'linux' or 'arm64'
|
|
2493
3080
|
console.log(matrix!.node); // '18' or '20'
|
|
2494
3081
|
});
|
|
2495
3082
|
```
|
|
@@ -2522,14 +3109,14 @@ Fine-tune matrix combinations on multi-dimensional matrices:
|
|
|
2522
3109
|
|
|
2523
3110
|
```typescript
|
|
2524
3111
|
matrix: {
|
|
2525
|
-
os: ['
|
|
3112
|
+
os: ['linux', 'arm64', 'windows'],
|
|
2526
3113
|
node: ['18', '20', '22'],
|
|
2527
3114
|
},
|
|
2528
3115
|
exclude: [
|
|
2529
|
-
{ os: 'windows
|
|
3116
|
+
{ os: 'windows', node: '18' },
|
|
2530
3117
|
],
|
|
2531
3118
|
include: [
|
|
2532
|
-
{ os: '
|
|
3119
|
+
{ os: 'linux', node: '23' },
|
|
2533
3120
|
],
|
|
2534
3121
|
```
|
|
2535
3122
|
|
|
@@ -2599,7 +3186,7 @@ const discoverJobs: DynamicJobFn = async ({ $ }) => {
|
|
|
2599
3186
|
const packages = result.stdout.trim().split('\n');
|
|
2600
3187
|
return packages.map((pkg) =>
|
|
2601
3188
|
job(`test-${pkg}`, {
|
|
2602
|
-
runsOn: '
|
|
3189
|
+
runsOn: 'linux',
|
|
2603
3190
|
steps: [
|
|
2604
3191
|
step('test', async ({ $ }) => {
|
|
2605
3192
|
await $`cd packages/${pkg} && pnpm test`;
|
|
@@ -2858,7 +3445,7 @@ Use whichever style is convenient — you don't have to wrap `console.log` in th
|
|
|
2858
3445
|
|
|
2859
3446
|
### setEnv(key, value)
|
|
2860
3447
|
|
|
2861
|
-
|
|
3448
|
+
Export an environment variable to later steps in the same job. This is the canonical way to hand a value computed in one step to the steps that follow — the equivalent of `echo "KEY=VALUE" >> $GITHUB_ENV` in GitHub Actions. The value is visible to the current step and all subsequent steps in the job.
|
|
2862
3449
|
|
|
2863
3450
|
```typescript
|
|
2864
3451
|
step('setup', async (ctx) => {
|
|
@@ -2879,6 +3466,7 @@ step('use', async (ctx) => {
|
|
|
2879
3466
|
- Last-write-wins -- if multiple steps set the same key, the last value is used
|
|
2880
3467
|
- Cannot override operator-injected secrets (the operator value takes precedence)
|
|
2881
3468
|
- Changes take effect immediately in the current step and persist for all subsequent steps
|
|
3469
|
+
- Shell commands export the same way by appending to `$KICI_ENV` (see [Exporting env from shell commands](#exporting-env-from-shell-commands-kici_env--kici_path) below)
|
|
2882
3470
|
|
|
2883
3471
|
### addPath(dir)
|
|
2884
3472
|
|
|
@@ -2896,6 +3484,40 @@ step('build', async (ctx) => {
|
|
|
2896
3484
|
});
|
|
2897
3485
|
```
|
|
2898
3486
|
|
|
3487
|
+
### Exporting env from shell commands ($KICI_ENV / $KICI_PATH)
|
|
3488
|
+
|
|
3489
|
+
`setEnv` and `addPath` are the TypeScript form of "export env to later steps". A shell command — including a non-JS toolchain installer — exports env the same way by appending to two files the agent points at before every step:
|
|
3490
|
+
|
|
3491
|
+
- **`$KICI_ENV`** — append `KEY=value` lines. Each becomes an environment variable visible to subsequent steps, exactly like `ctx.setEnv('KEY', 'value')`.
|
|
3492
|
+
- **`$KICI_PATH`** — append one directory per line. Each is prepended to `PATH` for subsequent steps, exactly like `ctx.addPath(dir)`. The first directory appended ends up first on `PATH`.
|
|
3493
|
+
|
|
3494
|
+
```typescript
|
|
3495
|
+
step('install-tool', async (ctx) => {
|
|
3496
|
+
await ctx.$`./install-mytool.sh`; // installs to /opt/mytool
|
|
3497
|
+
// Export from the shell, no JS round-trip needed:
|
|
3498
|
+
await ctx.$`echo "MYTOOL_HOME=/opt/mytool" >> "$KICI_ENV"`;
|
|
3499
|
+
await ctx.$`echo "/opt/mytool/bin" >> "$KICI_PATH"`;
|
|
3500
|
+
});
|
|
3501
|
+
|
|
3502
|
+
step('build', async (ctx) => {
|
|
3503
|
+
// MYTOOL_HOME is set and /opt/mytool/bin is on PATH here.
|
|
3504
|
+
await ctx.$`mytool build`;
|
|
3505
|
+
});
|
|
3506
|
+
```
|
|
3507
|
+
|
|
3508
|
+
**Format (v1):**
|
|
3509
|
+
|
|
3510
|
+
- One `KEY=value` per line in `$KICI_ENV`. The split is on the first `=`, so the value may contain `=`. Blank lines and lines without a `=` are ignored.
|
|
3511
|
+
- One directory per line in `$KICI_PATH`. Blank lines are ignored.
|
|
3512
|
+
- Values must be single-line — embedded newlines are not supported in v1.
|
|
3513
|
+
|
|
3514
|
+
**Behavior (shared with `setEnv` / `addPath`):**
|
|
3515
|
+
|
|
3516
|
+
- Applied after the step completes and visible to every later step in the job.
|
|
3517
|
+
- Last-write-wins on a repeated key.
|
|
3518
|
+
- Cannot override an operator-injected secret — a collision is ignored and logged, and the operator value is preserved.
|
|
3519
|
+
- The files are reset before each step, so each step sees only its own appended lines.
|
|
3520
|
+
|
|
2899
3521
|
### setSecretOutput(key, value)
|
|
2900
3522
|
|
|
2901
3523
|
Publish an encrypted secret output from this job. Downstream jobs that list this job in their `needs` array receive the value merged into `ctx.secrets`.
|
|
@@ -2944,7 +3566,7 @@ Each job picks its secret environment via the `environment` option on `job()`. T
|
|
|
2944
3566
|
|
|
2945
3567
|
```typescript
|
|
2946
3568
|
const deploy = job('deploy', {
|
|
2947
|
-
runsOn: '
|
|
3569
|
+
runsOn: 'linux',
|
|
2948
3570
|
environment: 'production',
|
|
2949
3571
|
steps: [
|
|
2950
3572
|
/* ... */
|
|
@@ -2983,7 +3605,7 @@ step('deploy', async ({ secrets }) => {
|
|
|
2983
3605
|
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
2984
3606
|
|
|
2985
3607
|
const deploy = job('deploy', {
|
|
2986
|
-
runsOn: '
|
|
3608
|
+
runsOn: 'linux',
|
|
2987
3609
|
environment: 'production',
|
|
2988
3610
|
steps: [
|
|
2989
3611
|
step('deploy', async (ctx) => {
|
|
@@ -3899,6 +4521,7 @@ Reference documentation for `@kici-dev/sdk`. The reference is split across five
|
|
|
3899
4521
|
| [Core](./sdk/core.md) | `workflow()`, `job()`, `step()` factory functions and step / job authoring patterns (bare functions, output chaining, `needs`, dynamic groups). |
|
|
3900
4522
|
| [Triggers](./sdk/triggers.md) | All 22 trigger factories -- GitHub events (`pr`, `push`, `tag`, `comment`, ...), event triggers (`kiciEvent`, `workflowComplete`, `jobComplete`), `genericWebhook`, `schedule`, `lifecycle`, plus branch-pattern semantics. |
|
|
3901
4523
|
| [Rules, matrix, dynamic jobs](./sdk/rules-matrix-dynamic.md) | `rule()`, `skip()`, matrix builds (static + dynamic), and `dynamicJob()` / `dynamicGroup()`. |
|
|
4524
|
+
| [Caching](./sdk/caching.md) | `CacheSpec`, declarative `cache` on jobs/steps, imperative `ctx.cache.restore()` / `ctx.cache.save()`, immutable keys, `restoreKeys` prefix fallback, per-org + per-ref isolation. |
|
|
3902
4525
|
| [Validation & events](./sdk/validation-events.md) | `validateDag()`, `defineEvent()`, event emission patterns. |
|
|
3903
4526
|
| [Runtime](./sdk/runtime.md) | Types index, `StepContext`, secrets, and fixtures. |
|
|
3904
4527
|
| [Idempotent helpers](./sdk/idempotent.md) | `idempotent()` and `idempotentStep()` — check / apply pattern with typed results on both the skipped and applied branches. |
|
|
@@ -4089,7 +4712,7 @@ JWT and opaque OIDC tokens are validated against the configured OIDC issuer (JWK
|
|
|
4089
4712
|
|
|
4090
4713
|
### Permissions
|
|
4091
4714
|
|
|
4092
|
-
Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The
|
|
4715
|
+
Tokens authenticate; RBAC authorizes. Every org-scoped route runs `orgContextMiddleware` (verifies you are a member of the target org) followed by `requirePermission(resource, level)`. The 15 resources and 5 levels are documented in [RBAC](../architecture/security/rbac.md#permission-model). User API keys carry their own permission matrix bounded above by the creator's effective permissions; PATs inherit the user's role permissions (or are capped further by their `scopes` field).
|
|
4093
4716
|
|
|
4094
4717
|
### Configurable surfaces
|
|
4095
4718
|
|
|
@@ -4346,6 +4969,7 @@ Cleanup policy:
|
|
|
4346
4969
|
- On a fully successful run, the isolated checkout is removed.
|
|
4347
4970
|
- On failure, it is retained and its path is logged so you can inspect the failed state.
|
|
4348
4971
|
- `--keep` always retains it, even on success.
|
|
4972
|
+
- Retained checkouts are garbage-collected after 72 hours by the next `kici run local` invocation — copy a checkout elsewhere if you need it longer.
|
|
4349
4973
|
|
|
4350
4974
|
Set the `KICI_RUN_DIR` environment variable to place the isolated checkout under a base directory other than the system temp directory.
|
|
4351
4975
|
|
|
@@ -4402,6 +5026,8 @@ kici run local push --keep-going
|
|
|
4402
5026
|
|
|
4403
5027
|
Execute fixtures remotely through the full CI pipeline. Fixtures are defined in `.kici/tests/*.ts` using the `fixture()` factory function. Without arguments, lists available fixtures.
|
|
4404
5028
|
|
|
5029
|
+
Requires `kici login` (an authenticated session) and a target orchestrator that has **cache storage configured** (`KICI_STORAGE_TYPE` = `s3` or `filesystem`) — the command uploads your working-tree overlay to that storage for the agent to fetch. The quickstart orchestrators do not enable storage by default; see the [testing guide](testing-guide.md) for setup (including non-public / self-hosted S3 endpoints).
|
|
5030
|
+
|
|
4405
5031
|
```bash
|
|
4406
5032
|
kici run remote [fixture] [options]
|
|
4407
5033
|
```
|
|
@@ -4414,21 +5040,21 @@ kici run remote [fixture] [options]
|
|
|
4414
5040
|
|
|
4415
5041
|
**Options:**
|
|
4416
5042
|
|
|
4417
|
-
| Option | Default | Description
|
|
4418
|
-
| --------------------------- | ------- |
|
|
4419
|
-
| `--all` | `false` | Run all fixtures
|
|
4420
|
-
| `--workflow <name>` | none | Run a specific workflow directly (bypass triggers)
|
|
4421
|
-
| `--parallel` | `false` | Run multiple fixtures concurrently
|
|
4422
|
-
| `--no-wait` | - | Fire and forget (print runIds, don't stream)
|
|
4423
|
-
| `--quiet` | `false` | Minimal output (only final result)
|
|
4424
|
-
| `--json` | `false` | Machine-readable JSON output
|
|
4425
|
-
| `--junit <path>` | none | JUnit XML output to file for CI integration
|
|
4426
|
-
| `--history` | `false` | Show table of recent test runs
|
|
4427
|
-
| `--routing-key <key>` | none | Override routing key for this run
|
|
4428
|
-
| `--
|
|
4429
|
-
| `--
|
|
4430
|
-
| `--debug` | `false` | Verbose internals
|
|
4431
|
-
| `--kici-dir <path>` | `.kici` | Path to .kici directory
|
|
5043
|
+
| Option | Default | Description |
|
|
5044
|
+
| --------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
5045
|
+
| `--all` | `false` | Run all fixtures |
|
|
5046
|
+
| `--workflow <name>` | none | Run a specific workflow directly (bypass triggers) |
|
|
5047
|
+
| `--parallel` | `false` | Run multiple fixtures concurrently |
|
|
5048
|
+
| `--no-wait` | - | Fire and forget (print runIds, don't stream) |
|
|
5049
|
+
| `--quiet` | `false` | Minimal output (only final result) |
|
|
5050
|
+
| `--json` | `false` | Machine-readable JSON output |
|
|
5051
|
+
| `--junit <path>` | none | JUnit XML output to file for CI integration |
|
|
5052
|
+
| `--history` | `false` | Show table of recent test runs |
|
|
5053
|
+
| `--routing-key <key>` | none | Override routing key for this run |
|
|
5054
|
+
| `--context <ctx.key=value>` | none | Inject a namespaced context secret, uploaded encrypted to the orchestrator (repeatable) |
|
|
5055
|
+
| `--env <KEY=VALUE>` | none | Provide a per-run secret, uploaded encrypted to the orchestrator (repeatable) — see [testing guide](testing-guide.md) |
|
|
5056
|
+
| `--debug` | `false` | Verbose internals |
|
|
5057
|
+
| `--kici-dir <path>` | `.kici` | Path to .kici directory |
|
|
4432
5058
|
|
|
4433
5059
|
**Examples:**
|
|
4434
5060
|
|
|
@@ -4561,14 +5187,14 @@ kici login [options]
|
|
|
4561
5187
|
|
|
4562
5188
|
**Environment variables:**
|
|
4563
5189
|
|
|
4564
|
-
| Variable | Default
|
|
4565
|
-
| --------------------- |
|
|
4566
|
-
| `KICI_PLATFORM_URL` |
|
|
4567
|
-
| `KICI_OIDC_ISSUER` |
|
|
4568
|
-
| `KICI_OIDC_CLIENT_ID` |
|
|
4569
|
-
| `KICI_BROWSER_CMD` | uses `open` package
|
|
4570
|
-
| `KICI_CALLBACK_PORT` | random
|
|
4571
|
-
| `KICI_CONFIG_DIR` | `~/.kici`
|
|
5190
|
+
| Variable | Default | Description |
|
|
5191
|
+
| --------------------- | -------------------------------------------- | ---------------------------------------------------------------------- |
|
|
5192
|
+
| `KICI_PLATFORM_URL` | `https://api.kici.dev` | Platform API base URL (override for a self-hosted Platform) |
|
|
5193
|
+
| `KICI_OIDC_ISSUER` | `https://auth.kici.dev/realms/kici-internal` | OIDC issuer URL (override for a self-hosted Platform) |
|
|
5194
|
+
| `KICI_OIDC_CLIENT_ID` | `kici-cli` | OIDC client ID (override for a self-hosted Platform) |
|
|
5195
|
+
| `KICI_BROWSER_CMD` | uses `open` package | Custom browser command with `{url}` placeholder, or `none` to suppress |
|
|
5196
|
+
| `KICI_CALLBACK_PORT` | random | Fixed port for OAuth PKCE callback server |
|
|
5197
|
+
| `KICI_CONFIG_DIR` | `~/.kici` | Override config directory |
|
|
4572
5198
|
|
|
4573
5199
|
**Examples:**
|
|
4574
5200
|
|
|
@@ -4751,6 +5377,72 @@ kici cancel abc123 --force
|
|
|
4751
5377
|
kici cancel --branch feature/wip
|
|
4752
5378
|
```
|
|
4753
5379
|
|
|
5380
|
+
### kici approve
|
|
5381
|
+
|
|
5382
|
+
Approve a held [approval gate](approvals.md) so the run resumes. Identify the held element by run ID, optionally narrowed to a job and step.
|
|
5383
|
+
|
|
5384
|
+
```bash
|
|
5385
|
+
kici approve <run-id> [options]
|
|
5386
|
+
```
|
|
5387
|
+
|
|
5388
|
+
**Arguments:**
|
|
5389
|
+
|
|
5390
|
+
| Argument | Required | Description |
|
|
5391
|
+
| -------- | -------- | ---------------------------------- |
|
|
5392
|
+
| `run-id` | yes | Run ID holding the gate to approve |
|
|
5393
|
+
|
|
5394
|
+
**Options:**
|
|
5395
|
+
|
|
5396
|
+
| Option | Default | Description |
|
|
5397
|
+
| ----------------- | ------- | --------------------------------------------------- |
|
|
5398
|
+
| `--job <name>` | none | Approve a held job (omit for a workflow-level hold) |
|
|
5399
|
+
| `--step <name>` | none | Approve a held step (used with `--job`) |
|
|
5400
|
+
| `--reason <text>` | none | Optional note recorded with the approval |
|
|
5401
|
+
|
|
5402
|
+
**Examples:**
|
|
5403
|
+
|
|
5404
|
+
```bash
|
|
5405
|
+
# Approve a workflow-level hold
|
|
5406
|
+
kici approve abc123
|
|
5407
|
+
|
|
5408
|
+
# Approve a held job
|
|
5409
|
+
kici approve abc123 --job deploy-production
|
|
5410
|
+
|
|
5411
|
+
# Approve a held step
|
|
5412
|
+
kici approve abc123 --job migrate-and-deploy --step apply-migration
|
|
5413
|
+
```
|
|
5414
|
+
|
|
5415
|
+
You must be eligible for at least one unsatisfied clause (a member of a named team, or a named user) and hold the `environments:write` or `ci_trust:write` permission. The command reports whether the element was released, how many clauses remain, or that it was rejected.
|
|
5416
|
+
|
|
5417
|
+
### kici reject
|
|
5418
|
+
|
|
5419
|
+
Reject a held [approval gate](approvals.md). A rejection fails the held element and the run. A reason is required.
|
|
5420
|
+
|
|
5421
|
+
```bash
|
|
5422
|
+
kici reject <run-id> --reason <text> [options]
|
|
5423
|
+
```
|
|
5424
|
+
|
|
5425
|
+
**Arguments:**
|
|
5426
|
+
|
|
5427
|
+
| Argument | Required | Description |
|
|
5428
|
+
| -------- | -------- | --------------------------------- |
|
|
5429
|
+
| `run-id` | yes | Run ID holding the gate to reject |
|
|
5430
|
+
|
|
5431
|
+
**Options:**
|
|
5432
|
+
|
|
5433
|
+
| Option | Default | Description |
|
|
5434
|
+
| ----------------- | ------- | -------------------------------------------------- |
|
|
5435
|
+
| `--reason <text>` | none | Required. Reason recorded with the rejection |
|
|
5436
|
+
| `--job <name>` | none | Reject a held job (omit for a workflow-level hold) |
|
|
5437
|
+
| `--step <name>` | none | Reject a held step (used with `--job`) |
|
|
5438
|
+
|
|
5439
|
+
**Examples:**
|
|
5440
|
+
|
|
5441
|
+
```bash
|
|
5442
|
+
# Reject a held job with a reason
|
|
5443
|
+
kici reject abc123 --job deploy-production --reason "Wrong release branch"
|
|
5444
|
+
```
|
|
5445
|
+
|
|
4754
5446
|
### kici secrets list
|
|
4755
5447
|
|
|
4756
5448
|
List secret contexts available for test runs. Shows context names and key names (not values).
|
|
@@ -5416,13 +6108,13 @@ If you change a workflow file (`.ts`) but do **not** regenerate and commit the l
|
|
|
5416
6108
|
|
|
5417
6109
|
The lock file (`kici.lock.json`) is a JSON file with the following top-level fields:
|
|
5418
6110
|
|
|
5419
|
-
| Field | Description
|
|
5420
|
-
| --------------- |
|
|
5421
|
-
| `schemaVersion` | Lock file schema version (currently
|
|
5422
|
-
| `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`).
|
|
5423
|
-
| `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes.
|
|
5424
|
-
| `lockfileHash` | SHA-256 of `.kici/package-lock.json
|
|
5425
|
-
| `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs.
|
|
6111
|
+
| Field | Description |
|
|
6112
|
+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
6113
|
+
| `schemaVersion` | Lock file schema version (currently 15). Incremented on breaking format changes. |
|
|
6114
|
+
| `source` | Reference to the source file and export (e.g., `{ file: “.kici/workflows/ci.ts”, export: “#default” }`). |
|
|
6115
|
+
| `contentHash` | SHA-256 of the serialized lock file content (excluding itself). Changes when any workflow, trigger, or job changes. |
|
|
6116
|
+
| `lockfileHash` | SHA-256 of the detected package manager's lockfile, used as the dependency cache key. The lockfile is `.kici/package-lock.json` for npm, or the repo-root `pnpm-lock.yaml` / `yarn.lock` for a pnpm/yarn workspace; the hash input is prefixed with the manager name so a manager change is a guaranteed cache miss. Omitted when no lockfile exists. |
|
|
6117
|
+
| `workflows` | Array of workflow entries, each with its own `contentHash`, `compileSchemaVersion`, triggers, and jobs. |
|
|
5426
6118
|
|
|
5427
6119
|
Each workflow entry includes:
|
|
5428
6120
|
|
|
@@ -5438,7 +6130,11 @@ Each workflow entry includes:
|
|
|
5438
6130
|
| `description` | Optional workflow description. |
|
|
5439
6131
|
| `hashFiles` | Declared glob patterns for extra files included in the content hash (optional). See [extra files in the content hash](#extra-files-in-the-content-hash-hashfiles). |
|
|
5440
6132
|
| `resolvedHashFiles` | Resolved file paths from `hashFiles` at compile time (optional). Recorded so the agent can verify without re-discovering. |
|
|
6133
|
+
| `contexts` | Secret contexts declared by the workflow (optional). The orchestrator validates access to each context before dispatch. |
|
|
6134
|
+
| `registries` | Private npm registry declarations the agent authenticates against before install (optional): `url`, `scope`, `tokenSecret` reference, `alwaysAuth`. Resolved token bytes never appear in the lock file. See [private registries](private-registries.md). |
|
|
6135
|
+
| `installEnv` | Extra qualified secret refs (`<environment>:<secret-name>`) projected as env vars on the install subprocess for use with a committed `.kici/.npmrc` (optional). See [private registries](private-registries.md). |
|
|
5441
6136
|
| `concurrency` | Workflow-level concurrency config: `hasGroup`, `cancelInProgress`, `max` (optional). See [concurrency groups](concurrency.md). |
|
|
6137
|
+
| `timeout` | Whole-run wall-clock timeout in milliseconds (optional). The orchestrator reads this at run creation to set the run deadline. |
|
|
5442
6138
|
| Hook flags | Boolean flags (`hasOnCancel`, `hasCleanup`, `hasOnSuccess`, `hasOnFailure`) indicating which lifecycle hooks are defined. Job entries additionally have `hasBeforeStep` and `hasAfterStep`. |
|
|
5443
6139
|
|
|
5444
6140
|
## Rule: commit both together
|
|
@@ -5534,11 +6230,22 @@ Test your workflows remotely against the full CI pipeline from your local machin
|
|
|
5534
6230
|
|
|
5535
6231
|
- Run any workflow against your current working tree (including unstaged changes)
|
|
5536
6232
|
- Get real-time log output streamed back to your terminal
|
|
5537
|
-
-
|
|
6233
|
+
- Give test runs test-scoped secrets — your local secret files and `--env` values (uploaded encrypted) plus any environment flagged `allowLocalExecution: true` — while production environments stay unreachable
|
|
5538
6234
|
- Detect test mode in workflow code via `ctx.isTestRun`
|
|
5539
6235
|
|
|
5540
6236
|
The command is remote-only -- all execution happens on the orchestrator and agent. For local-only trigger matching previews, use `kici test <event>`.
|
|
5541
6237
|
|
|
6238
|
+
:::note[Orchestrator prerequisite: cache storage]
|
|
6239
|
+
`kici run remote` uploads your working-tree overlay to the orchestrator's **cache storage** via a pre-signed URL, and the agent fetches it from there (see [Repo state transfer](#repo-state-transfer)). The target orchestrator must therefore have cache storage enabled (`KICI_STORAGE_TYPE` = `s3` or `filesystem`).
|
|
6240
|
+
|
|
6241
|
+
- **The [Docker / Podman quickstart](quickstart/compose.md) wires this up for you** — it ships a SeaweedFS service, so `kici run remote` works there out of the box (see its "run a workflow without pushing" step).
|
|
6242
|
+
- **The [bare-metal quickstart](quickstart/bare-metal.md) does not configure storage by default** — enable a backend before using `kici run remote`:
|
|
6243
|
+
- **`filesystem`** — simplest for a single-host orchestrator: set `KICI_STORAGE_TYPE=filesystem` and `KICI_STORAGE_FS_PATH=/var/lib/kici/cache`. No external service needed; blobs are served through the orchestrator's own HMAC-signed HTTP route.
|
|
6244
|
+
- **`s3`** — any S3-compatible bucket. **A non-public / self-hosted endpoint works**: set `KICI_STORAGE_TYPE=s3`, `KICI_STORAGE_BUCKET`, `KICI_STORAGE_ENDPOINT=https://your-endpoint` and (for most self-hosted services) `KICI_STORAGE_FORCE_PATH_STYLE=true`. If the developer machine running `kici run remote` reaches the bucket at a different address than the orchestrator, set `KICI_STORAGE_UPLOAD_ENDPOINT` to the developer-reachable address; if agents reach it at yet another address (e.g. agents in containers), set `KICI_STORAGE_EXTERNAL_ENDPOINT` to the agent-routable URL.
|
|
6245
|
+
|
|
6246
|
+
See [Storage layout](../operator/orchestrator/storage-layout.md) for the full env-var reference.
|
|
6247
|
+
:::
|
|
6248
|
+
|
|
5542
6249
|
## Getting started
|
|
5543
6250
|
|
|
5544
6251
|
### 1. Authenticate
|
|
@@ -5637,7 +6344,7 @@ When not specified, these default to values detected from your local git repo (c
|
|
|
5637
6344
|
|
|
5638
6345
|
### Secret context mappings
|
|
5639
6346
|
|
|
5640
|
-
Map
|
|
6347
|
+
Map secret contexts to your fixture:
|
|
5641
6348
|
|
|
5642
6349
|
```typescript
|
|
5643
6350
|
export const pushWithSecrets = fixture('push-with-secrets', {
|
|
@@ -5649,7 +6356,12 @@ export const pushWithSecrets = fixture('push-with-secrets', {
|
|
|
5649
6356
|
});
|
|
5650
6357
|
```
|
|
5651
6358
|
|
|
5652
|
-
This maps the `db` secret context to the `test-database` context
|
|
6359
|
+
This maps the `db` secret context to the `test-database` context, and `api` to `test-api-keys`.
|
|
6360
|
+
|
|
6361
|
+
This mapping is honored by **both** `kici run local` and `kici run remote`:
|
|
6362
|
+
|
|
6363
|
+
- For **`kici run local`** (see [`kici run local`](cli-reference.md#kici-run-local)), each named context is resolved from your local secret files (`.kici/.secrets`, `.env.local`, `secrets.yaml`, and `--env` flags).
|
|
6364
|
+
- For **`kici run remote`**, each named context maps to an orchestrator **environment**, and the orchestrator resolves that environment's secrets for the run. The target environment must be flagged `allowLocalExecution: true` — mapping a context to a missing or non-test environment rejects the run (see [Secret contexts for testing](#secret-contexts-for-testing) below).
|
|
5653
6365
|
|
|
5654
6366
|
### Async fixtures
|
|
5655
6367
|
|
|
@@ -5780,39 +6492,76 @@ The CLI always shows a pre-upload summary before transferring:
|
|
|
5780
6492
|
|
|
5781
6493
|
## Secret contexts for testing
|
|
5782
6494
|
|
|
5783
|
-
|
|
6495
|
+
The goal of the test-secret model is to let test runs reach **test-only credentials** while keeping production credentials out of reach. `kici run remote` combines two sources of secrets for a test run, then merges them with a clear precedence and a fail-closed gate.
|
|
6496
|
+
|
|
6497
|
+
### CLI-uploaded local secrets
|
|
6498
|
+
|
|
6499
|
+
`kici run remote` collects the same local secret values that `kici run local` reads — `.kici/.secrets`, `.kici/.env.local`, `.kici/secrets.yaml`, and any `--env KEY=VALUE` flags — and uploads them **encrypted** to the orchestrator alongside the run. The orchestrator decrypts them only to inject them into the agent for that run; the control plane never sees the values.
|
|
6500
|
+
|
|
6501
|
+
```bash
|
|
6502
|
+
# Provide an ad-hoc test value for a single remote run
|
|
6503
|
+
kici run remote push-main --env KICI_DATABASE_URL=postgresql://localhost/test
|
|
6504
|
+
```
|
|
6505
|
+
|
|
6506
|
+
`--env` provides a **flat** per-run override; `--context <ctx>.<KEY>=<value>` is its sibling for a **namespaced** per-run override, placing the value under the named context `ctx`. Both are uploaded **encrypted** and follow the same precedence rule below — a CLI-supplied value wins over the orchestrator test-environment secret on a key collision.
|
|
5784
6507
|
|
|
5785
|
-
|
|
6508
|
+
```bash
|
|
6509
|
+
# Provide a namespaced per-run value under the 'db' context
|
|
6510
|
+
kici run remote push-db --context db.KICI_DATABASE_URL=postgresql://localhost/test
|
|
6511
|
+
```
|
|
5786
6512
|
|
|
5787
|
-
|
|
6513
|
+
Because these values originate on your machine, they are the natural place to put throwaway test credentials without touching any orchestrator-stored secret.
|
|
5788
6514
|
|
|
5789
|
-
|
|
6515
|
+
### Orchestrator test-environment secrets
|
|
5790
6516
|
|
|
5791
|
-
|
|
6517
|
+
In addition to your uploaded values, the orchestrator resolves test-scoped secrets from its own store for a remote test run:
|
|
5792
6518
|
|
|
5793
|
-
|
|
6519
|
+
- The job's own declared `environment` contributes its resolved secrets (flat). Static strings and **pure dynamic functions** both participate: a pure `environment:` function (see [Dynamic values](dynamic-values.md)) is evaluated against the fixture's simulated event, and the resolved name is gated and resolved like a static one. Impure dynamic functions (those requiring an init job) are not evaluated for test runs — use a fixture `secrets:` mapping (or `--context`) to supply such a job's secrets.
|
|
6520
|
+
- Each fixture `secrets: { ctx: envName }` mapping resolves the named environment's secrets under the namespaced context `ctx`.
|
|
6521
|
+
|
|
6522
|
+
Both paths are restricted to environments flagged `allowLocalExecution: true`. A production environment left at the default `false` is never resolvable for a test run.
|
|
5794
6523
|
|
|
5795
6524
|
```typescript
|
|
5796
6525
|
export const pushWithDb = fixture('push-db', {
|
|
5797
6526
|
event: push({ branches: ['main'] }),
|
|
5798
|
-
secrets: { db: 'test-database' },
|
|
6527
|
+
secrets: { db: 'test-database' }, // 'test-database' must be allowLocalExecution: true
|
|
5799
6528
|
});
|
|
5800
6529
|
```
|
|
5801
6530
|
|
|
5802
|
-
In your workflow, access secrets normally via `ctx.secrets` or `ctx.contexts`:
|
|
5803
|
-
|
|
5804
6531
|
```typescript
|
|
5805
6532
|
step('migrate', async (ctx) => {
|
|
5806
|
-
const dbUrl = ctx.secrets.KICI_DATABASE_URL;
|
|
6533
|
+
const dbUrl = await ctx.secrets.get('KICI_DATABASE_URL');
|
|
5807
6534
|
await ctx.$`npx prisma migrate deploy`;
|
|
5808
6535
|
});
|
|
5809
6536
|
```
|
|
5810
6537
|
|
|
5811
|
-
|
|
6538
|
+
### Precedence: CLI values win
|
|
6539
|
+
|
|
6540
|
+
When a key exists in both sources, the **CLI-uploaded local value wins** over the orchestrator test-environment value. This makes a local override a per-run knob: set `--env KICI_DATABASE_URL=...` (or put it in `.kici/.secrets`) to shadow the test environment's value for just that run, without changing anything on the orchestrator.
|
|
6541
|
+
|
|
6542
|
+
### Fail-closed on non-test environments
|
|
6543
|
+
|
|
6544
|
+
Test-run secret resolution is fail-closed:
|
|
6545
|
+
|
|
6546
|
+
- If a fixture maps a context to an environment that does not exist, the run is **rejected**.
|
|
6547
|
+
- If a fixture maps a context to an environment whose `allowLocalExecution` is `false`, the run is **rejected**.
|
|
6548
|
+
- The `allowLocalExecution` gate applies to **all** remote test runs: a run whose matched workflow targets an environment with the flag off is rejected, so a test run can never resolve production secrets.
|
|
6549
|
+
|
|
6550
|
+
### The `allowLocalExecution` environment flag
|
|
6551
|
+
|
|
6552
|
+
Each environment carries an `allowLocalExecution` flag (default `false`) that controls test-run access to that environment and to its secrets. Production environments should leave it at `false`; create a dedicated test environment with `allowLocalExecution: true` that binds only test-only secret scopes for the contexts you want test runs to use.
|
|
6553
|
+
|
|
6554
|
+
The flag is set by the orchestrator operator, either via the CLI:
|
|
6555
|
+
|
|
6556
|
+
```bash
|
|
6557
|
+
kici-admin environment set-policy --env test-database --allow-local-execution true
|
|
6558
|
+
```
|
|
6559
|
+
|
|
6560
|
+
or via the dashboard's "Test runs" toggle on the environment detail page. `kici secrets list` only surfaces contexts whose owning environment has `allowLocalExecution: true`, so production environments are never advertised as test-accessible.
|
|
5812
6561
|
|
|
5813
|
-
###
|
|
6562
|
+
### Local execution as an alternative
|
|
5814
6563
|
|
|
5815
|
-
|
|
6564
|
+
`kici run local` resolves the same local secret files entirely on your machine and honors the fixture `secrets: { ... }` mapping to pick which local context backs each name (see [`kici run local`](cli-reference.md#kici-run-local)). Because the values never leave your machine, it's a good fit when you want to exercise secret-dependent steps without involving the orchestrator at all.
|
|
5816
6565
|
|
|
5817
6566
|
### Discovering available contexts
|
|
5818
6567
|
|
|
@@ -5922,6 +6671,58 @@ Practical patterns for building real-world KiCI workflows in TypeScript. The pat
|
|
|
5922
6671
|
|
|
5923
6672
|
# Workflow features
|
|
5924
6673
|
|
|
6674
|
+
## Account and sign-in
|
|
6675
|
+
|
|
6676
|
+
Source: https://docs.kici.dev/user/account-and-login/
|
|
6677
|
+
|
|
6678
|
+
Your KiCI account is a single identity. It stays the same no matter how you
|
|
6679
|
+
sign in — whether you signed up with GitHub or with an email and password.
|
|
6680
|
+
Changing your sign-in method does not create a new account or move your data;
|
|
6681
|
+
your organizations, roles, and API keys stay attached to the same identity.
|
|
6682
|
+
|
|
6683
|
+
## Where sign-in methods are managed
|
|
6684
|
+
|
|
6685
|
+
Sign-in methods and passwords are managed in your **account console**, provided
|
|
6686
|
+
by the identity provider that handles single sign-on for KiCI. The dashboard's
|
|
6687
|
+
**Linked accounts** page does not control how you sign in — see
|
|
6688
|
+
[Linked accounts vs sign-in methods](#linked-accounts-vs-sign-in-methods) below.
|
|
6689
|
+
|
|
6690
|
+
You can open the account console from the dashboard: go to your personal
|
|
6691
|
+
settings, open **Linked accounts**, and use the **Account console** link.
|
|
6692
|
+
|
|
6693
|
+
## Adding a password to a GitHub-created account
|
|
6694
|
+
|
|
6695
|
+
If you registered by signing in with GitHub and now want to sign in with an
|
|
6696
|
+
email and password as well:
|
|
6697
|
+
|
|
6698
|
+
1. Open your account console.
|
|
6699
|
+
2. Add a password (and, if prompted, confirm your email).
|
|
6700
|
+
|
|
6701
|
+
After this, you can sign in either with GitHub or with your email and password —
|
|
6702
|
+
it is the same account.
|
|
6703
|
+
|
|
6704
|
+
## Removing GitHub as a sign-in method
|
|
6705
|
+
|
|
6706
|
+
To stop using GitHub to sign in:
|
|
6707
|
+
|
|
6708
|
+
1. First add a password (see above). The identity provider will not let you
|
|
6709
|
+
remove your only sign-in method, so you must have another one first.
|
|
6710
|
+
2. In your account console, remove the GitHub sign-in method.
|
|
6711
|
+
|
|
6712
|
+
Your account, organizations, and data are unaffected — you simply sign in a
|
|
6713
|
+
different way afterward.
|
|
6714
|
+
|
|
6715
|
+
## Linked accounts vs sign-in methods
|
|
6716
|
+
|
|
6717
|
+
The dashboard's **Linked accounts** page controls **run-attribution metadata**
|
|
6718
|
+
only — for example, showing your GitHub username on the runs you trigger and
|
|
6719
|
+
determining your contributor trust level. Unlinking a provider there removes
|
|
6720
|
+
that display link; it does **not** remove the provider as a way to sign in.
|
|
6721
|
+
|
|
6722
|
+
To actually change how you sign in, use your account console as described above.
|
|
6723
|
+
|
|
6724
|
+
---
|
|
6725
|
+
|
|
5925
6726
|
## Concurrency groups
|
|
5926
6727
|
|
|
5927
6728
|
Source: https://docs.kici.dev/user/concurrency/
|
|
@@ -5974,8 +6775,8 @@ group: (ctx) => `deploy-${ctx.branch}`;
|
|
|
5974
6775
|
// Global concurrency (across all branches)
|
|
5975
6776
|
group: () => 'deploy';
|
|
5976
6777
|
|
|
5977
|
-
// Per-
|
|
5978
|
-
group: (ctx) => `deploy-${ctx.event.
|
|
6778
|
+
// Per-target-branch concurrency
|
|
6779
|
+
group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
|
|
5979
6780
|
```
|
|
5980
6781
|
|
|
5981
6782
|
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. This differs from job-level `concurrencyGroup` (see [Environments](environments.md#concurrency-groups)), where the compiler performs purity analysis and can inline pure functions for orchestrator-side evaluation.
|
|
@@ -6172,7 +6973,7 @@ The six steps are:
|
|
|
6172
6973
|
|
|
6173
6974
|
1. **Install the kici CLI** -- `npm install -g kici`.
|
|
6174
6975
|
2. **Create a workflow** -- `kici init` scaffolds a `.kici/` directory in your repository.
|
|
6175
|
-
3. **Run a workflow locally** -- `kici
|
|
6976
|
+
3. **Run a workflow locally** -- `kici run local pr:open` executes a workflow on your machine with no orchestrator required.
|
|
6176
6977
|
4. **Connect an orchestrator** -- deploy an orchestrator and connect it with a join token from **Settings → Orchestrator keys**.
|
|
6177
6978
|
5. **Add a webhook source** -- register a source under **Settings → Sources** so pushes and pull requests trigger runs.
|
|
6178
6979
|
6. **Trigger your first run** -- push to your repository to produce your first run through the relay.
|
|
@@ -6462,7 +7263,7 @@ The job tree supports keyboard navigation:
|
|
|
6462
7263
|
The content area has the following tabs:
|
|
6463
7264
|
|
|
6464
7265
|
- **Logs** (default) -- shows log output for the selected job or step
|
|
6465
|
-
- **Payload** -- webhook payload viewer showing the raw event payload that triggered the run
|
|
7266
|
+
- **Payload** -- webhook payload viewer showing the raw event payload that triggered the run. This tab appears only for runs triggered by a webhook event (and re-runs of those, which copy the original payload); runs started by a schedule, manual schedule, lifecycle event, or another run carry no payload, so the tab is hidden for them
|
|
6466
7267
|
- **Timeline** -- CSS Gantt chart showing the execution timeline of all jobs, with percentage-based bars and striped animation for running jobs. A **Provisioning** milestones section between the dispatch and execution phases plots scaler lifecycle events for the run — including a **Provisioning failed** marker when the scaler could not bring an agent up
|
|
6467
7268
|
- **Summary** -- contextual overview scoped to the current selection (run-level trigger/repo/timing info, or job-level execution context with environment variables, runtime info, and sandbox details)
|
|
6468
7269
|
|
|
@@ -6577,18 +7378,30 @@ Each member's linked provider accounts (e.g. GitHub) are also visible here.
|
|
|
6577
7378
|
|
|
6578
7379
|
<!-- help:settings-roles#settings -->
|
|
6579
7380
|
|
|
6580
|
-
Roles define granular permissions across
|
|
7381
|
+
Roles define granular permissions across 15 resource categories (runs, secrets, members, etc.) with 5 access levels: `none`, `read`, `read_payload`, `write`, `admin`.
|
|
6581
7382
|
|
|
6582
7383
|
Create custom roles to restrict what team members can do, or use the built-in **Owner** role for full access.
|
|
6583
7384
|
|
|
6584
7385
|
<!-- /help:settings-roles -->
|
|
6585
7386
|
|
|
7387
|
+
<!-- help:settings-teams#settings -->
|
|
7388
|
+
|
|
7389
|
+
Teams are named groups of organization members. A role granted to a team is inherited by every member, so you can manage permissions for a whole group in one place.
|
|
7390
|
+
|
|
7391
|
+
Team names can also be referenced in workflow approval gates (`requireApproval: [{ team: 'leads' }]`) — any member of the named team can satisfy that gate.
|
|
7392
|
+
|
|
7393
|
+
Managing teams (create / rename / delete, membership, role grants) requires the **Teams** permission at `admin`; `read` shows a view-only list.
|
|
7394
|
+
|
|
7395
|
+
<!-- /help:settings-teams -->
|
|
7396
|
+
|
|
6586
7397
|
<!-- help:settings-api-keys#settings -->
|
|
6587
7398
|
|
|
6588
7399
|
API keys allow programmatic access to the KiCI API for automation, scripts, and CI integrations.
|
|
6589
7400
|
|
|
6590
7401
|
Each key is scoped to this organization with a custom permission matrix and an optional expiry date. Keys can be revoked individually.
|
|
6591
7402
|
|
|
7403
|
+
Use a key's clone button to open the creation modal prefilled with that key's name, expiry, and permissions — handy for recreating an expired key or deriving a new key from an existing one.
|
|
7404
|
+
|
|
6592
7405
|
<!-- /help:settings-api-keys -->
|
|
6593
7406
|
|
|
6594
7407
|
<!-- help:settings-orchestrator-keys#orchestrator-keys -->
|
|
@@ -6597,6 +7410,8 @@ Orchestrator keys authenticate the WebSocket connection between your orchestrato
|
|
|
6597
7410
|
|
|
6598
7411
|
Create a key here and set it as the `KICI_PLATFORM_TOKEN` environment variable in your orchestrator configuration. Keys can optionally be restricted to specific routing patterns.
|
|
6599
7412
|
|
|
7413
|
+
Use a key's clone button to open the creation modal prefilled with that key's name and description.
|
|
7414
|
+
|
|
6600
7415
|
<!-- /help:settings-orchestrator-keys -->
|
|
6601
7416
|
|
|
6602
7417
|
<!-- help:settings-sources#sources -->
|
|
@@ -6649,6 +7464,8 @@ The relayed-webhooks counter only includes webhooks delivered through the KiCI P
|
|
|
6649
7464
|
|
|
6650
7465
|
Webhooks pointed directly at your orchestrator's public ingest endpoint never reach the Platform, so they're invisible to this counter and uncapped on every Hosted tier. If you have a public orchestrator ingress, you can mix-and-match: use the relay for sources you can't expose publicly, and point GitHub (or any provider / generic webhook) straight at your orchestrator for the rest.
|
|
6651
7466
|
|
|
7467
|
+
Every webhook the relay forwards counts — **including ones your workflows ultimately ignore**. Trigger matching runs on your orchestrator, not on the Platform, so the relay forwards each signature-verified webhook before any trigger is evaluated. A source that sends many events you filter down to a handful of runs still consumes one relayed webhook per event. If a high-volume source mostly produces no run, point it directly at your orchestrator (see above) to keep it off this counter entirely.
|
|
7468
|
+
|
|
6652
7469
|
When you hit the cap, new relayed webhooks are rejected with `429 Plan limit reached`. Upgrade in the Stripe Billing Portal to lift the cap immediately; usage resets monthly on your billing anniversary.
|
|
6653
7470
|
|
|
6654
7471
|
<!-- /help:settings-billing-relayed-webhooks -->
|
|
@@ -6818,6 +7635,20 @@ The summary strip at the top shows total / enabled / disabled counts plus whethe
|
|
|
6818
7635
|
|
|
6819
7636
|
<!-- /help:settings-security-dashboard-policy -->
|
|
6820
7637
|
|
|
7638
|
+
<!-- help:settings-support-access#settings -->
|
|
7639
|
+
|
|
7640
|
+
Controls whether KiCI support staff may open read-only support sessions against your organization. Sessions are **off by default** — nobody outside your org can read your data until you opt in here.
|
|
7641
|
+
|
|
7642
|
+
When enabled:
|
|
7643
|
+
|
|
7644
|
+
- KiCI staff can open time-boxed, read-only sessions to investigate an issue.
|
|
7645
|
+
- Every read they perform is recorded in your audit trail with the support reason.
|
|
7646
|
+
- No writes are ever possible during a session.
|
|
7647
|
+
|
|
7648
|
+
Disabling the toggle immediately ends any in-progress support session. Only users with the `support:admin` permission (owners by default) can change this setting.
|
|
7649
|
+
|
|
7650
|
+
<!-- /help:settings-support-access -->
|
|
7651
|
+
|
|
6821
7652
|
<!-- help:settings-webhooks-delivery-log#settings -->
|
|
6822
7653
|
|
|
6823
7654
|
The delivery log shows recent webhook deliveries for an endpoint, including the HTTP status code, number of retry attempts, and the event payload.
|
|
@@ -6874,11 +7705,26 @@ The settings page (`/orgs/:customerId/settings`) uses a tabbed layout:
|
|
|
6874
7705
|
10. **Webhooks** -- outbound webhook endpoint management with delivery logs and test ping
|
|
6875
7706
|
11. **Event log** -- inbound webhook delivery log (visible with `event_log:read` permission)
|
|
6876
7707
|
12. **Security** -- read-only view of the orchestrator's dashboard-write policy matrix (visible with `org_settings:read` permission)
|
|
7708
|
+
13. **Support access** -- opt-in switch that controls whether KiCI support staff may open read-only support sessions against your org (visible with `support:read`; toggled with `support:admin`)
|
|
6877
7709
|
|
|
6878
7710
|
Audit-log-style entries are no longer a settings tab; they live on the dedicated **Activity** page accessible from the sidebar.
|
|
6879
7711
|
|
|
6880
7712
|
Tab selection syncs with the URL path (`/settings/members`, `/settings/api-keys`, etc.), making tabs bookmarkable.
|
|
6881
7713
|
|
|
7714
|
+
### Support access
|
|
7715
|
+
|
|
7716
|
+
The Support access tab controls whether KiCI support staff may open a read-only **support session** against your organization to help diagnose an issue. The setting is **off by default** -- until you opt in here, no one outside your org can read your data.
|
|
7717
|
+
|
|
7718
|
+
When support access is enabled:
|
|
7719
|
+
|
|
7720
|
+
- A KiCI operator can open a time-boxed (30-minute, renewable), read-only support session scoped to a stated reason.
|
|
7721
|
+
- A support session is **runs-only**: the operator can browse your run list and, by confirming each run individually, view that run's detail and step logs. Nothing else is visible, and no write is ever possible.
|
|
7722
|
+
- Every run an operator opens is recorded in your [Activity](#activity) audit trail, attributed to the operator with the support reason -- so you can see exactly what was looked at and why.
|
|
7723
|
+
|
|
7724
|
+
**Disabling immediately ends any active session.** Toggling the switch off closes every in-progress support session for your org at once. Enabling and disabling the setting is itself audited, attributed to the user who changed it.
|
|
7725
|
+
|
|
7726
|
+
Viewing the setting requires the `support:read` permission; changing it requires `support:admin` (granted to owners by default).
|
|
7727
|
+
|
|
6882
7728
|
### Orchestrator keys
|
|
6883
7729
|
|
|
6884
7730
|
The orchestrator keys tab manages API keys used to authenticate orchestrator-to-Platform WebSocket connections. These are separate from user API keys (which grant dashboard/API access).
|
|
@@ -6955,11 +7801,15 @@ Personal access tokens (PATs) are long-lived credentials for programmatic API ac
|
|
|
6955
7801
|
|
|
6956
7802
|
Create a PAT to authenticate CLI tools or scripts without going through the OIDC login flow. Tokens can be revoked at any time.
|
|
6957
7803
|
|
|
7804
|
+
Use a token's clone button to open the creation modal prefilled with that token's name, expiry, and permissions.
|
|
7805
|
+
|
|
6958
7806
|
<!-- /help:personal-pats -->
|
|
6959
7807
|
|
|
6960
7808
|
<!-- help:personal-linked-accounts#account -->
|
|
6961
7809
|
|
|
6962
|
-
Linked accounts connect your external provider identities (like GitHub) to your KiCI account. Linking
|
|
7810
|
+
Linked accounts connect your external provider identities (like GitHub) to your KiCI account. Linking shows your provider username in run metadata and sets your contributor trust level.
|
|
7811
|
+
|
|
7812
|
+
**Unlinking here removes the display link only** — it does not remove a sign-in method. To change how you sign in (add a password, remove GitHub login), use the **Account console** link, or see [Account and sign-in](./account-and-login.md).
|
|
6963
7813
|
|
|
6964
7814
|
<!-- /help:personal-linked-accounts -->
|
|
6965
7815
|
|
|
@@ -7053,7 +7903,7 @@ The policy state is visible at three layers in the UI:
|
|
|
7053
7903
|
- A **lock-icon prefix** on every disabled control, with a per-control CLI hint.
|
|
7054
7904
|
- A **per-page banner** on any page containing at least one disabled operation, listing every disabled op on that page and its CLI equivalent.
|
|
7055
7905
|
|
|
7056
|
-
The Security policy page (Settings → Security → Dashboard policy) renders the full
|
|
7906
|
+
The Security policy page (Settings → Security → Dashboard policy) renders the full 24-row read-only matrix with the current state and the `kici-admin` command for each row. The policy itself cannot be changed from the dashboard — the orchestrator operator manages it via `kici-admin org-settings dashboard-writes`. See [Dashboard-write policy](/operator/security/dashboard-write-policy) for the operator-side details.
|
|
7057
7907
|
|
|
7058
7908
|
## Approval queue
|
|
7059
7909
|
|
|
@@ -7067,6 +7917,8 @@ The standalone account page (`/account`) provides access to personal settings ou
|
|
|
7067
7917
|
- **Personal access tokens** -- create and revoke PATs for programmatic API access
|
|
7068
7918
|
- **Linked accounts** -- connect external provider identities (e.g. GitHub) to your KiCI account
|
|
7069
7919
|
|
|
7920
|
+
Linked accounts control run-attribution metadata only — unlinking a provider here does not remove it as a way to sign in. To change how you sign in, see [Account and sign-in](./account-and-login.md).
|
|
7921
|
+
|
|
7070
7922
|
This page is also accessible within an org context via the user menu in the sidebar (`/orgs/:customerId/account`).
|
|
7071
7923
|
|
|
7072
7924
|
## Admin section
|
|
@@ -7133,14 +7985,25 @@ The dashboard shows informative error pages instead of blank screens:
|
|
|
7133
7985
|
|
|
7134
7986
|
Source: https://docs.kici.dev/user/dynamic-values/
|
|
7135
7987
|
|
|
7136
|
-
Dynamic values let you compute `environment`, `env`, and `concurrencyGroup` at runtime based on the incoming event. Instead of hardcoding static strings, you pass a function that receives the
|
|
7988
|
+
Dynamic values let you compute `environment`, `env`, and `concurrencyGroup` at runtime based on the incoming event. Instead of hardcoding static strings, you pass a function that receives the normalized event envelope and returns the resolved value.
|
|
7137
7989
|
|
|
7138
7990
|
```typescript
|
|
7139
7991
|
job('deploy', {
|
|
7140
7992
|
runsOn: ['default'],
|
|
7141
|
-
environment: (event) => event.
|
|
7142
|
-
env: (event) => ({ BRANCH: event.
|
|
7143
|
-
concurrencyGroup: (event) => `deploy-${event.
|
|
7993
|
+
environment: (event) => event.targetBranch,
|
|
7994
|
+
env: (event) => ({ BRANCH: event.targetBranch }),
|
|
7995
|
+
concurrencyGroup: (event) => `deploy-${event.targetBranch}`,
|
|
7996
|
+
steps: [
|
|
7997
|
+
/* ... */
|
|
7998
|
+
],
|
|
7999
|
+
});
|
|
8000
|
+
```
|
|
8001
|
+
|
|
8002
|
+
```typescript
|
|
8003
|
+
job('deploy', {
|
|
8004
|
+
runsOn: 'default',
|
|
8005
|
+
// One shape everywhere: branch on the normalized event type.
|
|
8006
|
+
environment: (event) => (event.type === 'pull_request' ? 'preview' : 'production'),
|
|
7144
8007
|
steps: [
|
|
7145
8008
|
/* ... */
|
|
7146
8009
|
],
|
|
@@ -7167,21 +8030,21 @@ When the compiler detects a pure function, it serializes the function source dir
|
|
|
7167
8030
|
**Examples of pure functions:**
|
|
7168
8031
|
|
|
7169
8032
|
```typescript
|
|
7170
|
-
// Simple branch extraction
|
|
7171
|
-
environment: (event) => event.
|
|
8033
|
+
// Simple branch extraction
|
|
8034
|
+
environment: (event) => event.targetBranch;
|
|
7172
8035
|
|
|
7173
8036
|
// Object literal with string operations
|
|
7174
|
-
env: (
|
|
8037
|
+
env: (event) => ({ BRANCH: event.targetBranch });
|
|
7175
8038
|
|
|
7176
8039
|
// Concatenation with event data
|
|
7177
|
-
concurrencyGroup: (event) => `deploy-${event.
|
|
8040
|
+
concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
|
|
7178
8041
|
|
|
7179
8042
|
// Using safe globals
|
|
7180
|
-
env: (event) => ({ UPPER: String(event.
|
|
8043
|
+
env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
|
|
7181
8044
|
|
|
7182
8045
|
// Local variables are fine
|
|
7183
8046
|
environment: (event) => {
|
|
7184
|
-
const parts = event.
|
|
8047
|
+
const parts = event.targetBranch.split('/');
|
|
7185
8048
|
return parts[parts.length - 1];
|
|
7186
8049
|
};
|
|
7187
8050
|
```
|
|
@@ -7201,7 +8064,7 @@ This adds approximately 5-10 seconds of overhead for cloning and evaluation.
|
|
|
7201
8064
|
|
|
7202
8065
|
```typescript
|
|
7203
8066
|
// Async functions cannot be inlined
|
|
7204
|
-
environment: async (event) => await lookupEnv(event.
|
|
8067
|
+
environment: async (event) => await lookupEnv(event.targetBranch);
|
|
7205
8068
|
|
|
7206
8069
|
// External module references
|
|
7207
8070
|
env: (event) => {
|
|
@@ -7221,18 +8084,18 @@ env: async (event) => {
|
|
|
7221
8084
|
|
|
7222
8085
|
## Performance comparison
|
|
7223
8086
|
|
|
7224
|
-
| Evaluation path | Overhead | When used
|
|
7225
|
-
| ------------------------------------ | -------- |
|
|
7226
|
-
| Static value (string/object literal) | ~0ms | `environment: 'staging'`
|
|
7227
|
-
| Inline expression (pure function) | ~0ms | `environment: (event) => event.
|
|
7228
|
-
| Init job (impure function) | ~5-10s | `environment: async (event) => await lookupEnv(event.
|
|
8087
|
+
| Evaluation path | Overhead | When used |
|
|
8088
|
+
| ------------------------------------ | -------- | ------------------------------------------------------------------- |
|
|
8089
|
+
| Static value (string/object literal) | ~0ms | `environment: 'staging'` |
|
|
8090
|
+
| Inline expression (pure function) | ~0ms | `environment: (event) => event.targetBranch` |
|
|
8091
|
+
| Init job (impure function) | ~5-10s | `environment: async (event) => await lookupEnv(event.targetBranch)` |
|
|
7229
8092
|
|
|
7230
8093
|
## Tips
|
|
7231
8094
|
|
|
7232
8095
|
- **Write pure functions whenever possible** to avoid the init-job delay. Most environment and env computations only need the event payload data.
|
|
7233
8096
|
- **Check compiler warnings** -- the compiler tells you when a function is classified as impure and explains why.
|
|
7234
8097
|
- **Runtime errors in inline expressions cause immediate job failure.** There is no fallback to the init-job path. If your pure function throws at runtime (e.g., accessing a property on `undefined`), the job fails immediately.
|
|
7235
|
-
- **The event parameter
|
|
8098
|
+
- **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](./sdk/event-payloads.md) 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`, …).
|
|
7236
8099
|
|
|
7237
8100
|
---
|
|
7238
8101
|
|
|
@@ -7240,16 +8103,16 @@ env: async (event) => {
|
|
|
7240
8103
|
|
|
7241
8104
|
Source: https://docs.kici.dev/user/env-vars/
|
|
7242
8105
|
|
|
7243
|
-
The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`)
|
|
8106
|
+
The KiCI CLI reads the following environment variables to customize its behavior. OAuth login (`kici login` without `--token`) defaults `KICI_PLATFORM_URL`, `KICI_OIDC_ISSUER`, and `KICI_OIDC_CLIENT_ID` to the hosted KiCI Platform, so `kici login` works with no configuration. Set them only to target a self-hosted Platform or a testing environment.
|
|
7244
8107
|
|
|
7245
8108
|
## Authentication
|
|
7246
8109
|
|
|
7247
|
-
| Variable | Description
|
|
7248
|
-
| --------------------- |
|
|
7249
|
-
| `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication
|
|
7250
|
-
| `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application
|
|
7251
|
-
| `KICI_PLATFORM_URL` | Platform API base URL
|
|
7252
|
-
| `KICI_CONFIG_DIR` | Override the KiCI config directory
|
|
8110
|
+
| Variable | Description | Default |
|
|
8111
|
+
| --------------------- | -------------------------------------- | -------------------------------------------- |
|
|
8112
|
+
| `KICI_OIDC_ISSUER` | OIDC issuer URL for authentication | `https://auth.kici.dev/realms/kici-internal` |
|
|
8113
|
+
| `KICI_OIDC_CLIENT_ID` | OIDC client ID for the CLI application | `kici-cli` |
|
|
8114
|
+
| `KICI_PLATFORM_URL` | Platform API base URL | `https://api.kici.dev` |
|
|
8115
|
+
| `KICI_CONFIG_DIR` | Override the KiCI config directory | `~/.kici` |
|
|
7253
8116
|
|
|
7254
8117
|
## Browser behavior
|
|
7255
8118
|
|
|
@@ -7275,9 +8138,9 @@ Authenticate with a pre-existing API key (no browser needed):
|
|
|
7275
8138
|
kici login --token <<< "$KICI_API_KEY"
|
|
7276
8139
|
```
|
|
7277
8140
|
|
|
7278
|
-
###
|
|
8141
|
+
### Self-hosted Platform or custom OIDC provider
|
|
7279
8142
|
|
|
7280
|
-
|
|
8143
|
+
`kici login` targets the hosted KiCI Platform by default. To point the CLI at a self-hosted Platform or a testing OIDC provider, override the defaults:
|
|
7281
8144
|
|
|
7282
8145
|
```bash
|
|
7283
8146
|
export KICI_OIDC_ISSUER=https://your-idp.example.com
|
|
@@ -7384,12 +8247,12 @@ export default workflow('deploy', {
|
|
|
7384
8247
|
|
|
7385
8248
|
### Dynamic environments
|
|
7386
8249
|
|
|
7387
|
-
The environment name can be a string or a function (sync or async) for dynamic environments (e.g., per-PR review environments)
|
|
8250
|
+
The environment name can be a string or a function (sync or async) for dynamic environments (e.g., per-PR review environments). The function receives the normalized event envelope, with the raw provider body nested at `event.payload`:
|
|
7388
8251
|
|
|
7389
8252
|
```typescript
|
|
7390
8253
|
job('deploy-review', {
|
|
7391
8254
|
runsOn: 'default',
|
|
7392
|
-
environment:
|
|
8255
|
+
environment: (event) => `review/PR-${event.payload.pull_request.number}`,
|
|
7393
8256
|
steps: [
|
|
7394
8257
|
step('deploy', async (ctx) => {
|
|
7395
8258
|
// ctx.environment is 'review/PR-123' (resolved at runtime)
|
|
@@ -7399,7 +8262,7 @@ job('deploy-review', {
|
|
|
7399
8262
|
});
|
|
7400
8263
|
```
|
|
7401
8264
|
|
|
7402
|
-
Dynamic environments that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
|
|
8265
|
+
A pure function like the one above (see [Dynamic values](dynamic-values.md)) is evaluated inline at dispatch with no init-job overhead. Dynamic environments that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
|
|
7403
8266
|
|
|
7404
8267
|
### Job-level environment variables
|
|
7405
8268
|
|
|
@@ -7411,7 +8274,7 @@ job('deploy', {
|
|
|
7411
8274
|
environment: 'production',
|
|
7412
8275
|
env: { DEPLOY_TARGET: 'us-east-1' },
|
|
7413
8276
|
// Or dynamic:
|
|
7414
|
-
// env:
|
|
8277
|
+
// env: (event) => ({ DEPLOY_SHA: event.payload.after?.slice(0, 7) }),
|
|
7415
8278
|
steps: [
|
|
7416
8279
|
step('deploy', async (ctx) => {
|
|
7417
8280
|
// DEPLOY_TARGET is available in ctx.env
|
|
@@ -7433,7 +8296,7 @@ job('deploy', {
|
|
|
7433
8296
|
environment: 'production',
|
|
7434
8297
|
concurrencyGroup: 'production-api',
|
|
7435
8298
|
// Or dynamic:
|
|
7436
|
-
// concurrencyGroup:
|
|
8299
|
+
// concurrencyGroup: (event) => `review-${event.payload.pull_request.number}`,
|
|
7437
8300
|
steps: [
|
|
7438
8301
|
/* ... */
|
|
7439
8302
|
],
|
|
@@ -7496,7 +8359,9 @@ Require manual approval before a job can proceed:
|
|
|
7496
8359
|
Required reviewers: alice, bob
|
|
7497
8360
|
```
|
|
7498
8361
|
|
|
7499
|
-
When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard or API. Held runs expire after a configurable timeout
|
|
8362
|
+
When reviewers are required, the job enters a "held" state. Reviewers can approve or reject via the dashboard, the [`kici approve`](cli-reference.md#kici-approve) command, or the API. Held runs expire after a configurable timeout.
|
|
8363
|
+
|
|
8364
|
+
This operator-set rule is the **mandatory** form of an approval gate. Workflow authors can also declare gates in code with `requireApproval` at step, job, or workflow level — see [Approval gates](approvals.md). Both forms use the same held-element mechanism and the same queue.
|
|
7500
8365
|
|
|
7501
8366
|
### Wait timer
|
|
7502
8367
|
|
|
@@ -7560,10 +8425,12 @@ Strategy: queue (or cancel-pending)
|
|
|
7560
8425
|
|
|
7561
8426
|
### Creating environments
|
|
7562
8427
|
|
|
7563
|
-
Navigate to **Settings > Environments** in the dashboard. Click
|
|
8428
|
+
Navigate to **Settings > Environments** in the dashboard. Click **New environment** to choose the environment name and type (Fixed or Glob).
|
|
8429
|
+
|
|
8430
|
+
- **Fixed** -- applies to jobs that declare exactly this environment name, like `staging` or `production`
|
|
8431
|
+
- **Glob** -- applies to any environment name a job declares that matches the pattern, e.g. `review/*` matches a job with `environment: 'review/PR-123'`
|
|
7564
8432
|
|
|
7565
|
-
|
|
7566
|
-
- **Glob** -- a pattern like `review/*` that matches dynamic environment names
|
|
8433
|
+
The environments list shows each environment's type, whether test runs may use it (the `allowLocalExecution` flag -- see the [testing guide](./testing-guide.md)), and whether it is enabled.
|
|
7567
8434
|
|
|
7568
8435
|
### Environment detail page
|
|
7569
8436
|
|
|
@@ -7804,7 +8671,7 @@ export default workflow('nightly-build', {
|
|
|
7804
8671
|
on: schedule({ cron: '0 2 * * *' }),
|
|
7805
8672
|
jobs: [
|
|
7806
8673
|
job('build', {
|
|
7807
|
-
runsOn: '
|
|
8674
|
+
runsOn: 'linux',
|
|
7808
8675
|
steps: [
|
|
7809
8676
|
step('build', async ({ $ }) => {
|
|
7810
8677
|
await $`pnpm build`;
|
|
@@ -7903,7 +8770,7 @@ export default workflow('build', {
|
|
|
7903
8770
|
on: push({ branches: 'main' }),
|
|
7904
8771
|
jobs: [
|
|
7905
8772
|
job('build', {
|
|
7906
|
-
runsOn: '
|
|
8773
|
+
runsOn: 'linux',
|
|
7907
8774
|
steps: [
|
|
7908
8775
|
step('build', async ({ $ }) => {
|
|
7909
8776
|
await $`pnpm build`;
|
|
@@ -7958,7 +8825,7 @@ export default workflow('post-deploy', {
|
|
|
7958
8825
|
on: kiciEvent({ name: 'deploy-complete', match: { '$.env': 'prod' } }),
|
|
7959
8826
|
jobs: [
|
|
7960
8827
|
job('smoke-test', {
|
|
7961
|
-
runsOn: '
|
|
8828
|
+
runsOn: 'linux',
|
|
7962
8829
|
steps: [
|
|
7963
8830
|
step('test', async ({ $ }) => {
|
|
7964
8831
|
await $`./scripts/smoke-test.sh`;
|
|
@@ -8246,7 +9113,7 @@ export default workflow('build', {
|
|
|
8246
9113
|
});
|
|
8247
9114
|
```
|
|
8248
9115
|
|
|
8249
|
-
The same pattern works for GCP Artifact Registry — replace the `aws codeartifact` call with `gcloud auth print-access-token`.
|
|
9116
|
+
The same pattern works for GCP Artifact Registry — replace the `aws codeartifact` call with `gcloud auth print-access-token`. The manual setup-step shown here is the supported path for these short-lived flows.
|
|
8250
9117
|
|
|
8251
9118
|
## Provider-specific examples
|
|
8252
9119
|
|
|
@@ -8774,7 +9641,7 @@ kici-admin source get-webhook-secret github:12345 # Fetch the secret (for debug
|
|
|
8774
9641
|
```
|
|
8775
9642
|
|
|
8776
9643
|
For the full CLI reference see the `source` section of the
|
|
8777
|
-
[kici-admin CLI reference](../../operator/kici-admin-cli.md).
|
|
9644
|
+
[kici-admin CLI reference](../../operator/orchestrator/kici-admin-cli.md).
|
|
8778
9645
|
|
|
8779
9646
|
## Routing keys
|
|
8780
9647
|
|
|
@@ -8927,7 +9794,7 @@ and confirm the App is installed on that repo.
|
|
|
8927
9794
|
Gogs / GitLab, and for plain-GitHub repos without an App
|
|
8928
9795
|
- [GitHub checks architecture](../../architecture/webhooks/github-checks.md)
|
|
8929
9796
|
- [Global workflows](../../architecture/global-workflows.md)
|
|
8930
|
-
- [kici-admin CLI reference](../../operator/kici-admin-cli.md)
|
|
9797
|
+
- [kici-admin CLI reference](../../operator/orchestrator/kici-admin-cli.md)
|
|
8931
9798
|
- [Event routing](../../operator/event-routing.md) — operator-level
|
|
8932
9799
|
routing-key mechanics
|
|
8933
9800
|
|
|
@@ -9180,7 +10047,7 @@ Source: https://docs.kici.dev/architecture/data-flows/
|
|
|
9180
10047
|
|
|
9181
10048
|
This document describes the key data flows through the KiCI architecture: webhook delivery, job execution, dependency caching, re-run and cancel, trace ID propagation, internal event routing, and generic webhook ingestion.
|
|
9182
10049
|
|
|
9183
|
-
> **Lock file schema version:** The lock file uses schema version
|
|
10050
|
+
> **Lock file schema version:** The lock file uses schema version 15, which adds per-job init config on top of v14's declarative cache specs, v11's `LockInlineValue` for pure function inline evaluation, v10's simplified negative patterns (! prefix in repos/paths arrays), v9's global workflow repos matching, and v8's runsOn polymorphic type support.
|
|
9184
10051
|
|
|
9185
10052
|
## Webhook delivery flow
|
|
9186
10053
|
|
|
@@ -9206,7 +10073,7 @@ GitHub --> Platform Relay --> Orchestrator --> Agent
|
|
|
9206
10073
|
10. **Orchestrator fetches lock file** via the provider's `LockFileFetcher` (cached with LRU). For untrusted PR events, fetches both base and head lock files in parallel; for trusted PRs and pushes, fetches from head SHA.
|
|
9207
10074
|
11. **Orchestrator detects workflow modifications** for untrusted PR events by comparing base and head lock files via `detectWorkflowModifications()`, applying security holds when non-trusted contributors modify workflow files.
|
|
9208
10075
|
12. **Orchestrator extracts registrations** on default-branch pushes: persists registerable workflows (event, schedule, lifecycle triggers) for cluster-wide event matching.
|
|
9209
|
-
13. **Orchestrator
|
|
10076
|
+
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.
|
|
9210
10077
|
14. **Orchestrator fetches changed files** via the provider's `ChangedFilesFetcher` for path-based trigger filtering (skipped when no workflow uses path filters).
|
|
9211
10078
|
15. **Orchestrator matches triggers** against lock file using `matchAllWorkflows()` from `@kici-dev/engine`.
|
|
9212
10079
|
16. **Orchestrator checks caches** for source tarballs and dependency tarballs.
|
|
@@ -9349,9 +10216,9 @@ The source cache and dep cache are independent. Four combinations are possible:
|
|
|
9349
10216
|
|
|
9350
10217
|
Dep cache misses alone do **not** trigger a build job. Deps are platform-specific (`deps/{platform}-{arch}/{hash}.tar.gz`) so a build job would need a builder agent matching the target platform, which may not exist (e.g., an arm64 builder when only x64 builders are available). When the source cache misses, the dispatched build job piggy-backs dep packing if deps are also missing. A single build job handles both artifacts when both miss, avoiding duplicate builds.
|
|
9351
10218
|
|
|
9352
|
-
###
|
|
10219
|
+
### Cross-source / no-contentHash workflows
|
|
9353
10220
|
|
|
9354
|
-
- **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
|
|
10221
|
+
- **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 15.
|
|
9355
10222
|
- **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.
|
|
9356
10223
|
|
|
9357
10224
|
### Build deduplication
|
|
@@ -9435,6 +10302,64 @@ The two-phase metadata approach (`upload via PUT` then `initMeta via CopyObject`
|
|
|
9435
10302
|
|
|
9436
10303
|
Agents receive pre-signed S3 GET URLs (15-minute expiry) directly in `job.dispatch` messages. Agents download artifacts from S3, bypassing the orchestrator for all data transfer.
|
|
9437
10304
|
|
|
10305
|
+
## User-facing cache flow
|
|
10306
|
+
|
|
10307
|
+
The source/dep cache above is internal: the orchestrator owns its keys and decides when to hit or build. The **user-facing cache** is driven by the workflow author — the declarative `cache: { key, paths, restoreKeys? }` on a job/step, or the imperative `ctx.cache.restore()` / `ctx.cache.save()` API (see [SDK caching reference](../user/sdk/caching.md)). It reuses the same object-storage backend and the same direct-to-storage presigned-URL transport, but the agent — not the orchestrator — initiates each restore and save over WebSocket.
|
|
10308
|
+
|
|
10309
|
+
The agent's cache module archives `paths` into a gzipped tarball (computing a SHA-256 over the bytes) and streams downloads back through a checksum-verified extract pipeline. The orchestrator's `UserCache` owns the `cache/<orgId>/<repoId>/<scope>/<key>` namespacing, the immutable first-save check, the `restoreKeys` prefix scan, the two-phase atomic save, and per-org quota/TTL eviction.
|
|
10310
|
+
|
|
10311
|
+
### Restore flow
|
|
10312
|
+
|
|
10313
|
+
```
|
|
10314
|
+
Agent Orchestrator (UserCache) Object storage
|
|
10315
|
+
| | |
|
|
10316
|
+
|-- cache.user.restore.request ------->| |
|
|
10317
|
+
| { key, restoreKeys? } |-- exact key in read prefixes ->|
|
|
10318
|
+
| | (isolated: iso/<runId>/ |
|
|
10319
|
+
| | then shared/; trusted: |
|
|
10320
|
+
| | shared/ only) |
|
|
10321
|
+
| |-- restoreKeys prefix scan ---->|
|
|
10322
|
+
| | (newest match wins) |
|
|
10323
|
+
| |-- getUrl(matched) + touch ---->|
|
|
10324
|
+
|<-- cache.user.restore.response ------| |
|
|
10325
|
+
| { hit, matchedKey?, | |
|
|
10326
|
+
| downloadUrl?, tarHash? } | |
|
|
10327
|
+
| | |
|
|
10328
|
+
|-- HTTP GET (tarball body) -------------------------------------->|
|
|
10329
|
+
| (direct download; verify tarHash, extract paths) |
|
|
10330
|
+
```
|
|
10331
|
+
|
|
10332
|
+
The restore resolves the exact `key` across the ref's read prefixes first, then each `restoreKeys` prefix in order (newest matching entry wins). A trusted ref reads only `shared/`; an untrusted/fork ref reads its own `iso/<runId>/` scope and then falls back to `shared/`. On a hit the response carries a presigned GET URL plus the tarball's `tarHash`, which the agent verifies before extracting.
|
|
10333
|
+
|
|
10334
|
+
### Save flow (two-phase atomic)
|
|
10335
|
+
|
|
10336
|
+
```
|
|
10337
|
+
Agent Orchestrator (UserCache) Object storage
|
|
10338
|
+
| | |
|
|
10339
|
+
|-- cache.user.save.request --------->| |
|
|
10340
|
+
| { key } |-- has(final key)? ------------>|
|
|
10341
|
+
| | (immutable: skip if exists) |
|
|
10342
|
+
| |-- getUploadUrl(.tmp-<uuid>) -->|
|
|
10343
|
+
|<-- cache.user.save.response ---------| |
|
|
10344
|
+
| { uploadUrl?, skip } | |
|
|
10345
|
+
| | |
|
|
10346
|
+
|-- HTTP PUT (tarball body) ----------------------------------->|
|
|
10347
|
+
| (direct upload to temp object) |
|
|
10348
|
+
| | |
|
|
10349
|
+
|-- cache.user.save.complete -------->| |
|
|
10350
|
+
| { key, tarHash, sizeBytes } |-- copy(temp -> final) -------->|
|
|
10351
|
+
| |-- delete(temp) --------------->|
|
|
10352
|
+
| |-- initMeta(final) ------------>|
|
|
10353
|
+
| |-- put(.hash) + put(.size) ---->|
|
|
10354
|
+
| |-- enforce per-org quota ------>|
|
|
10355
|
+
```
|
|
10356
|
+
|
|
10357
|
+
The save is **immutable** and **atomic**. The orchestrator declines (`skip: true`) up front if the exact key already exists. Otherwise the agent uploads to a `.tmp-<uuid>` object via a presigned PUT, then `cache.user.save.complete` triggers a server-side copy temp→final, a delete of the temp, an `initMeta` to stamp TTL metadata, and `.hash` / `.size` companion writes. Because the final key only appears after the copy, a crashed upload never leaves a corrupt committed entry. The committing save then enforces the per-org byte quota, evicting oldest entries until the org is back under `KICI_USER_CACHE_QUOTA_BYTES`.
|
|
10358
|
+
|
|
10359
|
+
### Trust → scope mapping
|
|
10360
|
+
|
|
10361
|
+
The orchestrator threads a `cacheRefScope` onto each `job.dispatch`. A **trusted** ref (the repo's own branches, default branch) maps to the `shared` write scope; any other ref (a fork PR) maps to `isolated`, writing to a per-run `iso/<runId>/` scope. This is the cache-isolation model: a fork can restore from the trusted `shared/` cache but can never write into it, so it cannot poison the entries a trusted branch later restores. The org segment of the key namespace (`cache/<orgId>/`) is the per-tenant boundary — no tenant can read another tenant's cache. See [orchestrator storage layout](../operator/orchestrator/storage-layout.md#user-cache) for the full prefix map and quota/TTL knobs.
|
|
10362
|
+
|
|
9438
10363
|
## Internal event routing flow
|
|
9439
10364
|
|
|
9440
10365
|
Internal events (custom events from `ctx.emit()` and system events from workflow/job completion) flow through the event router for fan-out delivery to matching workflows.
|
|
@@ -9467,11 +10392,11 @@ EventRouter.onNotification(eventId) [private]
|
|
|
9467
10392
|
| increments attempts and records claimed_at/claimed_by atomically)
|
|
9468
10393
|
|-- If lease acquired:
|
|
9469
10394
|
| |-- processSubscriptions(event):
|
|
9470
|
-
| | |-- If RegistrationIndex available
|
|
10395
|
+
| | |-- If RegistrationIndex available:
|
|
9471
10396
|
| | | Look up registrations by trigger type
|
|
9472
10397
|
| | | TrustStore.isTrusted() (for cross-repo events)
|
|
9473
10398
|
| | | matchAllWorkflows() against registered workflows
|
|
9474
|
-
| | |-- Else (
|
|
10399
|
+
| | |-- Else (no RegistrationIndex):
|
|
9475
10400
|
| | | TrustStore.isTrusted() (for cross-routing-key events)
|
|
9476
10401
|
| | | matchAllWorkflows() against in-memory lock file subscriptions
|
|
9477
10402
|
| | |-- For each match: onEventMatched(event, lockFile, matchedWorkflows)
|
|
@@ -9503,7 +10428,7 @@ Two invariants keep events from being silently lost:
|
|
|
9503
10428
|
event lands in the DLQ (`dlq_at` set, `dlq_reason='exhausted_retries'`)
|
|
9504
10429
|
and is surfaced via Prometheus (`kici_orch_event_dlq_*`), Grafana
|
|
9505
10430
|
(`event-delivery` dashboard), and the kici-admin CLI
|
|
9506
|
-
(`kici-admin event-dlq {list,retry,discard}`).
|
|
10431
|
+
(`kici-admin event-dlq {list,count,retry,discard}`).
|
|
9507
10432
|
- **Crash detection:** when a node crashes mid-dispatch, its lease ages out
|
|
9508
10433
|
after `leaseDurationMs` (default 60 s). The leader's
|
|
9509
10434
|
`EventRetryScanner` releases the expired lease and re-publishes
|
|
@@ -9939,141 +10864,6 @@ The Platform tier exposes a `/ws/browser` WebSocket endpoint for dashboard clien
|
|
|
9939
10864
|
|
|
9940
10865
|
---
|
|
9941
10866
|
|
|
9942
|
-
## Design decisions
|
|
9943
|
-
|
|
9944
|
-
Source: https://docs.kici.dev/architecture/design-decisions/
|
|
9945
|
-
|
|
9946
|
-
This page explains the rationale behind KiCI's major architectural choices. Each section describes the decision, why it was made, and what alternatives were considered.
|
|
9947
|
-
|
|
9948
|
-
## TypeScript over YAML
|
|
9949
|
-
|
|
9950
|
-
**Decision:** Workflows are defined in TypeScript, not YAML.
|
|
9951
|
-
|
|
9952
|
-
TypeScript provides full language power -- type checking, IDE autocomplete, refactoring tools, conditional logic, and composability. Workflows are regular TypeScript code that can be tested, imported, and reused like any other module. YAML-based CI systems (GitHub Actions, CircleCI) limit expressiveness to what the YAML schema supports, requiring workarounds for anything beyond basic conditionals.
|
|
9953
|
-
|
|
9954
|
-
**Alternative:** YAML workflow definitions (industry standard). Rejected because YAML constrains what users can express and makes workflows harder to test and refactor.
|
|
9955
|
-
|
|
9956
|
-
## Lock file approach
|
|
9957
|
-
|
|
9958
|
-
**Decision:** The compiler generates a `kici.lock.json` file that separates workflow definitions from runtime execution.
|
|
9959
|
-
|
|
9960
|
-
The lock file is a JSON snapshot of workflow definitions with static triggers pre-evaluated at compile time. This enables the orchestrator to match triggers without cloning code -- it fetches only the lock file via the GitHub API. Dynamic elements (dynamic jobs, dynamic matrices, rules) are represented as source references in the lock file and evaluated at runtime by the agent.
|
|
9961
|
-
|
|
9962
|
-
**Alternative:** Runtime evaluation of TypeScript at the routing tier. Rejected because it would require the orchestrator to clone repositories for trigger matching, adding latency and complexity to the dispatch path.
|
|
9963
|
-
|
|
9964
|
-
> See `packages/compiler/src/` for the compiler that generates lock files.
|
|
9965
|
-
|
|
9966
|
-
## Three-tier relay model
|
|
9967
|
-
|
|
9968
|
-
**Decision:** The system is split into three deployment tiers: Platform (webhook router), orchestrator (execution brain), and agent (execution worker).
|
|
9969
|
-
|
|
9970
|
-
This architecture separates trust boundaries. Customer code never leaves customer infrastructure -- the Platform tier handles only webhook verification and routing, with no access to customer code or secrets. The orchestrator matches triggers without cloning code. Only the agent, running on customer infrastructure, clones repositories and executes steps. The three-tier model also enables fully self-hosted deployment where all tiers run on customer infrastructure.
|
|
9971
|
-
|
|
9972
|
-
**Alternative:** Monolithic Platform that runs everything (customer code on KiCI servers). Rejected because it requires customers to trust a third party with their source code and secrets.
|
|
9973
|
-
|
|
9974
|
-
> See [Architecture Overview](overview.md) for the full three-tier diagram.
|
|
9975
|
-
|
|
9976
|
-
## Synchronous webhook relay
|
|
9977
|
-
|
|
9978
|
-
**Decision:** The Platform tier relays webhooks synchronously over WebSocket: verify signature, route to orchestrator, wait for ACK, respond to GitHub.
|
|
9979
|
-
|
|
9980
|
-
Synchronous relay is simpler, lower latency, and eliminates infrastructure dependencies. GitHub's built-in webhook retry mechanism handles the failure case -- if the Platform tier cannot relay (no orchestrator connected), it returns an error and GitHub retries automatically. This approach removed the need for a separate broker and an async job queue.
|
|
9981
|
-
|
|
9982
|
-
**Alternative:** Async queue (store webhook in a queue, process later). Rejected because it added infrastructure complexity (a separate broker and queue layer) without meaningful benefit -- GitHub already provides reliable retry behavior.
|
|
9983
|
-
|
|
9984
|
-
## Per-source webhook secrets
|
|
9985
|
-
|
|
9986
|
-
**Decision:** Each webhook source has its own webhook secret stored on the orchestrator side, dynamically registered by orchestrators via the `source.register` protocol message.
|
|
9987
|
-
|
|
9988
|
-
Per-source secrets enable multi-tenant Platform with proper isolation. Each customer's webhook source (e.g., a GitHub App) has a unique webhook secret, and signature verification uses the correct secret for each incoming webhook based on the routing key derived from the `X-GitHub-Hook-Installation-Target-ID` header.
|
|
9989
|
-
|
|
9990
|
-
**Alternative:** A single shared webhook secret via environment variable. Rejected because it cannot support multiple tenants and provides no isolation between customers.
|
|
9991
|
-
|
|
9992
|
-
## Routing key routing
|
|
9993
|
-
|
|
9994
|
-
**Decision:** Webhooks are routed by provider-scoped routing key (e.g., `github:12345`) derived from the `X-GitHub-Hook-Installation-Target-ID` header. One routing key maps to one orchestrator connection.
|
|
9995
|
-
|
|
9996
|
-
App ID routing provides a clean 1:1 mapping between a customer's GitHub App and their orchestrator. The orchestrator extracts the `installation_id` from the webhook payload body for GitHub API calls (fetching lock files, posting check runs).
|
|
9997
|
-
|
|
9998
|
-
**Alternative:** Route by Installation ID (more granular, one App can have many installations). Rejected because it adds complexity without clear benefit -- most customers use one App with one or a few installations, and the orchestrator can handle multiple installations internally.
|
|
9999
|
-
|
|
10000
|
-
## PostgreSQL as the orchestrator's only data store
|
|
10001
|
-
|
|
10002
|
-
**Decision:** The customer-deployable orchestrator depends on a single PostgreSQL database — no separate broker, cache, or pub/sub system. PostgreSQL is the primary data store on the Platform tier as well (the two tiers run independent databases).
|
|
10003
|
-
|
|
10004
|
-
PostgreSQL backs the orchestrator's durable job queue (delayed and recurring jobs, retries, dead-letter handling), workflow runs, dispatched jobs, sources, secrets, and event registrations. All DB access goes through a typed SQL query layer. Cross-instance coordination inside an orchestrator cluster (peer announcements, run cancellation, secret invalidation) uses PostgreSQL LISTEN/NOTIFY, so adding orchestrator replicas does not introduce a new infrastructure dependency.
|
|
10005
|
-
|
|
10006
|
-
**Alternative:** A separate broker / queue / pub-sub system alongside PostgreSQL. Rejected for the customer-deployable orchestrator because PostgreSQL covers durable jobs, transactional state changes, and cross-instance coordination in a single dependency — adding a broker would multiply operator surface area without buying customers a capability they don't already have.
|
|
10007
|
-
|
|
10008
|
-
## Pure-function state machine
|
|
10009
|
-
|
|
10010
|
-
**Decision:** The state machine uses pure functions: `transition(state, event) -> newState` with no internal state, no classes, and no mutation.
|
|
10011
|
-
|
|
10012
|
-
Pure functions are trivially testable (input/output, no setup), serializable (state is a string), and usable across all tiers without instantiation. The transition table is a plain `Record` for type-safe dispatch. The state machine tracks workflow runs, jobs, and steps using 11 states and 16 events.
|
|
10013
|
-
|
|
10014
|
-
**Alternative:** Class-based state machine with internal state (e.g., `machine.apply(event)`). Rejected because it is harder to test (requires instantiation and lifecycle management), harder to serialize (class instances), and adds unnecessary complexity for a simple state transition problem.
|
|
10015
|
-
|
|
10016
|
-
> See `packages/engine/src/state-machine/machine.ts` for the implementation and [State Machine](./execution/state-machine.md) for full documentation.
|
|
10017
|
-
|
|
10018
|
-
## Single agent binary with label-based routing
|
|
10019
|
-
|
|
10020
|
-
**Decision:** One agent binary, configured with labels via the `KICI_LABELS` environment variable (e.g., `linux,docker,gpu`). The orchestrator routes jobs by matching `runsOn` labels against registered agent labels.
|
|
10021
|
-
|
|
10022
|
-
A single binary simplifies deployment and maintenance. Labels provide flexible routing without requiring separate agent builds or deployments per platform. New capabilities are added by deploying the same binary with different labels and environment configuration.
|
|
10023
|
-
|
|
10024
|
-
**Alternative:** Separate agent binaries per platform (e.g., `kici-agent-linux`, `kici-agent-docker`). Rejected because it multiplies build and deployment complexity without meaningful benefit.
|
|
10025
|
-
|
|
10026
|
-
> See `packages/agent/src/config.ts` for label configuration.
|
|
10027
|
-
|
|
10028
|
-
## Lock file schema versioning
|
|
10029
|
-
|
|
10030
|
-
**Decision:** The lock file includes a `schemaVersion` field that tracks breaking changes to the trigger format.
|
|
10031
|
-
|
|
10032
|
-
| Version | Description |
|
|
10033
|
-
| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
10034
|
-
| 12 | Current schema. Covers everything in v11 plus per-workflow `registries` and `installEnv` fields for private-package authentication. Token bytes never appear in the lock file — only qualified `<environment>:<secret-name>` references that the orchestrator resolves at dispatch time through the per-environment secret resolver. |
|
|
10035
|
-
| 11 | Covers the full trigger taxonomy (git events, internal events, schedule, lifecycle, generic webhooks), the `_type` discriminator on each trigger entry, polymorphic `runsOn` matching with `excludeLabels`, hook and concurrency flags on `LockJob`, repo-pattern matching with `!` negation, and `LockInlineValue` for pure-function inline evaluation. |
|
|
10036
|
-
|
|
10037
|
-
The `_type` field on each lock file trigger entry identifies the trigger type for the matcher's exhaustive switch dispatch. Users must recompile workflows with `pnpm kici compile` after upgrading to get the latest trigger types in their lock files.
|
|
10038
|
-
|
|
10039
|
-
**`SimulatedEvent.type`** is an open `string` type, allowing the `kici test` CLI to simulate any event type without requiring engine changes for each new event.
|
|
10040
|
-
|
|
10041
|
-
## Two-phase evaluation
|
|
10042
|
-
|
|
10043
|
-
**Decision:** Trigger matching happens in two phases: fast reject at the orchestrator (no clone needed) and full evaluation at the agent (clone and run TypeScript).
|
|
10044
|
-
|
|
10045
|
-
The orchestrator evaluates static triggers from the lock file -- branch patterns, path patterns, and event types. This is fast and requires only a small JSON file fetched via the GitHub API. The agent handles everything that requires code: rule evaluation (arbitrary TypeScript functions), dynamic job generation, and dynamic matrix expansion. This separation keeps the orchestrator fast and git-agnostic.
|
|
10046
|
-
|
|
10047
|
-
**Alternative:** Full evaluation at the orchestrator (clone repo, run TypeScript, then dispatch). Rejected because it would make the orchestrator slow, require git access at the routing tier, and blur the trust boundary between routing and execution.
|
|
10048
|
-
|
|
10049
|
-
## Registration model for event-triggered workflows
|
|
10050
|
-
|
|
10051
|
-
**Decision:** Workflows with non-Git triggers (schedule, kiciEvent, genericWebhook, workflowComplete, jobComplete, lifecycle) are extracted from the lock file on default-branch push and stored in a registration database. The orchestrator matches incoming events against these registrations without fetching the lock file each time.
|
|
10052
|
-
|
|
10053
|
-
**Why:** Git-based triggers (push, PR) work with per-event lock file fetching because the webhook itself provides the repo and ref. Internal events (cron fires, custom events, workflow completions) do not carry repo/ref information -- the orchestrator needs to know which workflows to evaluate before the event arrives. The registration model solves this by pre-populating the orchestrator's knowledge of event-triggered workflows.
|
|
10054
|
-
|
|
10055
|
-
**Alternative:** Fetch all lock files from all known repos on every internal event. Rejected because it would be O(repos \* events) API calls, adding unacceptable latency and GitHub API rate limit pressure.
|
|
10056
|
-
|
|
10057
|
-
> See `packages/orchestrator/src/registration/` for the implementation.
|
|
10058
|
-
|
|
10059
|
-
## Event trigger matching via registration index
|
|
10060
|
-
|
|
10061
|
-
**Decision:** The event router matches events exclusively against the DB-backed `RegistrationIndex`. A previous legacy in-memory subscription fallback was removed since the registration index is always available and is the sole authoritative source for trigger matching.
|
|
10062
|
-
|
|
10063
|
-
**Why:** A single code path eliminates drift risk between dual paths, halves the test surface, and avoids confusing future contributors about which path is authoritative. The project's pre-release "no backward compatibility" rule means deferred cleanup shouldn't accumulate.
|
|
10064
|
-
|
|
10065
|
-
> See `packages/orchestrator/src/events/event-router.ts` and `packages/orchestrator/src/registration/` for the implementation.
|
|
10066
|
-
|
|
10067
|
-
## See also
|
|
10068
|
-
|
|
10069
|
-
- [Architecture Overview](overview.md) -- the three-tier model and package structure
|
|
10070
|
-
- [State Machine](./execution/state-machine.md) -- details of the pure-function state machine
|
|
10071
|
-
- [Webhook Delivery](./webhooks/webhook-delivery.md) -- synchronous relay in action
|
|
10072
|
-
- [Reconnection](./clustering/reconnection.md) -- WebSocket resilience patterns
|
|
10073
|
-
- [Event System Internals](./webhooks/event-system.md) -- event router, registration model, cron scheduler
|
|
10074
|
-
|
|
10075
|
-
---
|
|
10076
|
-
|
|
10077
10867
|
## Architecture overview
|
|
10078
10868
|
|
|
10079
10869
|
Source: https://docs.kici.dev/architecture/overview/
|
|
@@ -10233,14 +11023,17 @@ flowchart TD
|
|
|
10233
11023
|
PLATFORM --> SHARED
|
|
10234
11024
|
ORCH --> ENGINE
|
|
10235
11025
|
ORCH --> SHARED
|
|
11026
|
+
ORCH -.->|dev| AGENT
|
|
10236
11027
|
AGENT --> ENGINE
|
|
10237
11028
|
AGENT --> SDK
|
|
10238
11029
|
AGENT --> SHARED
|
|
11030
|
+
AGENT --> CORE
|
|
10239
11031
|
KICI["kici (wrapper)"]
|
|
10240
11032
|
KICI --> COMPILER
|
|
10241
11033
|
KICI --> CORE
|
|
10242
11034
|
KICIADMIN["kici-admin (admin CLI)"]
|
|
10243
11035
|
KICIADMIN --> ORCH
|
|
11036
|
+
KICIADMIN --> AGENT
|
|
10244
11037
|
```
|
|
10245
11038
|
|
|
10246
11039
|
**Leaf packages** (no `@kici` dependencies): `@kici-dev/core` and `@kici-dev/engine`. These can be tested and built independently. `@kici-dev/shared` builds on `@kici-dev/core` and re-exports it. The dashboard depends on `@kici-dev/engine` for shared types (protocol schemas, state machine) and imports the Platform's API type definitions as a dev dependency, but communicates with backend services at runtime via HTTP/WebSocket, not at compile time.
|
|
@@ -10277,6 +11070,5 @@ KiCI uses application-level tenant isolation. The Platform dashboard API accepts
|
|
|
10277
11070
|
- [State Machine](./execution/state-machine.md) -- execution lifecycle tracking across all tiers
|
|
10278
11071
|
- [Protocol Messages](protocol-messages.md) -- WebSocket message schemas for all three layers
|
|
10279
11072
|
- [Webhook Delivery](./webhooks/webhook-delivery.md) -- end-to-end trace of a webhook through all three tiers
|
|
10280
|
-
- [Design Decisions](design-decisions.md) -- rationale behind the three-tier model and other architectural choices
|
|
10281
11073
|
|
|
10282
11074
|
---
|