@kici-dev/compiler 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/commands/compile.js +1 -13
- package/dist/commands/preview.js +1 -8
- package/dist/commands/types.js +1 -2
- package/dist/errors/formatter.d.ts +2 -4
- package/dist/errors/formatter.js +1 -3
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +2 -2
- package/dist/generators/secrets-dts.d.ts +0 -1
- package/dist/generators/secrets-dts.js +1 -2
- package/dist/llm-context/llms-architecture.txt +27 -13
- package/dist/llm-context/llms-cli.txt +26 -6
- package/dist/llm-context/llms-features.txt +272 -91
- package/dist/llm-context/llms-full.txt +649 -235
- package/dist/llm-context/llms-getting-started.txt +20 -20
- package/dist/llm-context/llms-patterns.txt +10 -6
- package/dist/llm-context/llms-providers.txt +4 -6
- package/dist/llm-context/llms-sdk-runtime.txt +37 -36
- package/dist/llm-context/llms-sdk.txt +253 -57
- package/dist/llm-context/llms.txt +6 -6
- package/dist/local-plane/plane-manager.js +2 -2
- package/dist/lockfile/generator.js +137 -42
- package/dist/lockfile/index.d.ts +0 -2
- package/dist/lockfile/index.js +1 -2
- package/dist/templates/package-json.js +1 -1
- package/dist/test-runner/dry-run.d.ts +1 -2
- package/dist/test-runner/dry-run.js +1 -18
- package/dist/types.d.ts +32 -8
- package/dist/types.js +7 -1
- package/dist/validation/validator.js +40 -0
- package/package.json +6 -6
- package/sbom.spdx.json +126 -121
- package/dist/lockfile/purity-analyzer.d.ts +0 -25
- package/dist/lockfile/purity-analyzer.js +0 -204
- package/dist/lockfile/purity-diagnostics.d.ts +0 -31
- package/dist/lockfile/purity-diagnostics.js +0 -52
|
@@ -331,7 +331,7 @@ group: () => 'deploy';
|
|
|
331
331
|
group: (ctx) => `deploy-${ctx.event.targetBranch ?? 'default'}`;
|
|
332
332
|
```
|
|
333
333
|
|
|
334
|
-
The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins.
|
|
334
|
+
The workflow-level group function is always evaluated **agent-side** at runtime -- the lock file records only that a group function exists (`hasGroup: true`), not the function itself. The agent loads the workflow source, calls the group function with `{ branch, event }`, and reports the evaluated key back to the orchestrator before step execution begins. Job-level `concurrencyGroup` functions (see [Contexts](https://docs.kici.dev/user/contexts/#concurrency-groups)) resolve the same way — on the agent's init step, never in the orchestrator.
|
|
335
335
|
|
|
336
336
|
## cancelInProgress mode
|
|
337
337
|
|
|
@@ -568,7 +568,7 @@ job('deploy-review', {
|
|
|
568
568
|
});
|
|
569
569
|
```
|
|
570
570
|
|
|
571
|
-
A
|
|
571
|
+
A dynamic context function like the one above (see [Dynamic values](https://docs.kici.dev/user/dynamic-values/)) is resolved on the eval agent's init step before the job runs. Dynamic contexts that match a glob pattern (e.g., `review/*`) inherit the pattern's configuration, variables, and protection rules.
|
|
572
572
|
|
|
573
573
|
### Multiple contexts per job
|
|
574
574
|
|
|
@@ -871,22 +871,17 @@ job('deploy', {
|
|
|
871
871
|
|
|
872
872
|
## How it works
|
|
873
873
|
|
|
874
|
-
When you define a dynamic value as a function,
|
|
874
|
+
When you define a dynamic value as a function, it is resolved on the eval agent as a short **init** step that runs before the job:
|
|
875
875
|
|
|
876
|
-
|
|
876
|
+
1. The orchestrator dispatches a lightweight `__init__` job to an agent.
|
|
877
|
+
2. The agent loads the compiled workflow bundle and calls your function with the normalized event.
|
|
878
|
+
3. The agent reports the resolved values back to the orchestrator, which dispatches the real execution job with them applied.
|
|
877
879
|
|
|
878
|
-
|
|
880
|
+
This resolution appears in the run timeline as an `Init:` entry. The orchestrator never evaluates workflow code — every dynamic `context`, `env`, and `concurrencyGroup` function runs agent-side, whatever it references.
|
|
879
881
|
|
|
880
|
-
|
|
881
|
-
- Only references its parameters and local variables
|
|
882
|
-
- Does not import or require external modules
|
|
883
|
-
- Does not access globals like `process`, `fetch`, `console`, `setTimeout`, etc.
|
|
884
|
-
- Uses only safe built-in constructors: `String`, `Number`, `Boolean`, `Array`, `Object`, `JSON`, `Math`, `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `encodeURIComponent`, `decodeURIComponent`, `encodeURI`, `decodeURI`
|
|
885
|
-
- Does not use `this`, `new`, `class`, `throw`, `try`/`catch`, `delete`, `var`, `yield`, or mutation operators (`++`, `--`, `+=`, etc.)
|
|
882
|
+
`kici preview` lists the injected `__init__` job under each affected job, so you can spot it before the first run.
|
|
886
883
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
**Examples of pure functions:**
|
|
884
|
+
**Examples:**
|
|
890
885
|
|
|
891
886
|
```typescript
|
|
892
887
|
// Simple branch extraction
|
|
@@ -898,65 +893,32 @@ env: (event) => ({ BRANCH: event.targetBranch });
|
|
|
898
893
|
// Concatenation with event data
|
|
899
894
|
concurrencyGroup: (event) => `deploy-${event.targetBranch}`;
|
|
900
895
|
|
|
901
|
-
//
|
|
902
|
-
env: (event) => ({ UPPER: String(event.targetBranch).toUpperCase() });
|
|
903
|
-
|
|
904
|
-
// Local variables are fine
|
|
896
|
+
// Local variables and safe globals
|
|
905
897
|
context: (event) => {
|
|
906
898
|
const parts = event.targetBranch.split('/');
|
|
907
899
|
return parts[parts.length - 1];
|
|
908
900
|
};
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
### Impure functions (init-job evaluation)
|
|
912
|
-
|
|
913
|
-
If the compiler determines a function is impure, it prints a `warning [W101]` naming the affected field (`context`, `env`, or `concurrencyGroup`), the reason the function was judged impure, and the ~5-10 second init-job cost — and compilation still succeeds. The function falls back to the two-phase init model. This means:
|
|
914
|
-
|
|
915
|
-
1. The orchestrator dispatches a special `__init__` job to a builder agent
|
|
916
|
-
2. The builder agent clones the repository and evaluates the function
|
|
917
|
-
3. The resolved values are sent back to the orchestrator
|
|
918
|
-
4. The orchestrator dispatches the real execution job with the resolved values
|
|
919
901
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
`kici preview` lists the injected `__init__` job under each affected job, so you can spot the init-job cost before the first run.
|
|
923
|
-
|
|
924
|
-
**Examples of impure functions (will use init job):**
|
|
925
|
-
|
|
926
|
-
```typescript
|
|
927
|
-
// Async functions cannot be inlined
|
|
902
|
+
// Async lookups, module access, and process/global reads all work
|
|
928
903
|
context: async (event) => await lookupEnv(event.targetBranch);
|
|
929
|
-
|
|
930
|
-
// External module references
|
|
931
|
-
env: (event) => {
|
|
932
|
-
const config = require('./config');
|
|
933
|
-
return config.env;
|
|
934
|
-
};
|
|
935
|
-
|
|
936
|
-
// Process/global access
|
|
937
|
-
context: (event) => process.env.DEFAULT_ENV || 'staging';
|
|
938
|
-
|
|
939
|
-
// Dynamic imports
|
|
940
|
-
env: async (event) => {
|
|
941
|
-
const m = await import('./config.js');
|
|
942
|
-
return m.default;
|
|
943
|
-
};
|
|
904
|
+
env: (event) => ({ DEFAULT_ENV: process.env.DEFAULT_ENV ?? 'staging' });
|
|
944
905
|
```
|
|
945
906
|
|
|
946
|
-
## Performance
|
|
907
|
+
## Performance
|
|
908
|
+
|
|
909
|
+
| Value | Overhead | Example |
|
|
910
|
+
| ------------------------ | --------- | ---------------------------------------- |
|
|
911
|
+
| Static value | None | `context: 'staging'` |
|
|
912
|
+
| Dynamic value (function) | Init step | `context: (event) => event.targetBranch` |
|
|
947
913
|
|
|
948
|
-
|
|
949
|
-
| ------------------------------------ | -------- | --------------------------------------------------------------- |
|
|
950
|
-
| Static value (string/object literal) | ~0ms | `context: 'staging'` |
|
|
951
|
-
| Inline expression (pure function) | ~0ms | `context: (event) => event.targetBranch` |
|
|
952
|
-
| Init job (impure function) | ~5-10s | `context: async (event) => await lookupEnv(event.targetBranch)` |
|
|
914
|
+
A static value is baked into the lock file and needs no init step. A dynamic value always resolves through the agent's init step, so reach for a function only when the value genuinely depends on the event.
|
|
953
915
|
|
|
954
916
|
## Tips
|
|
955
917
|
|
|
956
|
-
- **
|
|
957
|
-
- **
|
|
958
|
-
- **
|
|
959
|
-
- **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where
|
|
918
|
+
- **Prefer static values when you can.** Most context and env values are the same on every event; only make them dynamic when they truly depend on the event payload.
|
|
919
|
+
- **Run `kici preview`** to see the injected `__init__` job listed under each affected job before your first run.
|
|
920
|
+
- **A runtime error in a dynamic function fails the job.** If your function throws when the init step runs it (e.g., accessing a property on `undefined`), the job fails immediately.
|
|
921
|
+
- **See [how your workflow code executes](https://docs.kici.dev/user/execution-model/)** for the full picture of where dynamic values run relative to rules, hooks, and step bodies.
|
|
960
922
|
- **The event parameter is the normalized event envelope** — the same shape rules receive as `ctx.event`: `{ type, action, targetBranch, sourceBranch, changedFiles, payload, … }` (see the [event payload reference](https://docs.kici.dev/user/sdk/event-payloads/) for the complete schema). Narrow on `event.type` (`'push'`, `'pull_request'`, `'tag'`, …) to branch per trigger kind. The raw provider webhook body is nested at `event.payload` (for GitHub pushes: `payload.ref`, `payload.after`, `payload.repository`, …).
|
|
961
923
|
|
|
962
924
|
---
|
|
@@ -1493,35 +1455,151 @@ export default workflow('org-lint', {
|
|
|
1493
1455
|
});
|
|
1494
1456
|
```
|
|
1495
1457
|
|
|
1496
|
-
Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org.
|
|
1458
|
+
Patterns in `repos:` use the same globbing as `branches:` / `paths:` — plain globs (`myorg/*`), a leading `!` for exclusions (`!myorg/fork-*`), and a fully-qualified `owner/repo` identity for exact matches (`myorg/platform`). A bare `**` matches every repo in the org, including one whose identifier starts with a dot (`.github/workflows-config`) — a repo identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own. Path globs in `paths:` keep the usual convention and do not match dot-prefixed files unless the pattern spells the dot out.
|
|
1497
1459
|
|
|
1498
1460
|
### At a dual-repo checkout
|
|
1499
1461
|
|
|
1500
|
-
The agent
|
|
1462
|
+
The agent checks out both repos. **Inside a step body**, `env` carries a pointer to each working tree:
|
|
1501
1463
|
|
|
1502
1464
|
| `env` var | Points to |
|
|
1503
1465
|
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1504
1466
|
| `KICI_SOURCE_REPO_PATH` | The **source** repo's working tree (the repo that emitted the event). This is the repo the job's `$` / `git` commands operate on by default. |
|
|
1505
1467
|
| `KICI_WORKFLOW_REPO_PATH` | The **workflow** repo's working tree (the repo that authored the workflow). Useful for reading shared scripts or config from your CI repo. |
|
|
1506
1468
|
|
|
1507
|
-
|
|
1469
|
+
Both variables are step-body context. They are **not** projected into the job's process environment, so a job-level `env:` block, a container image's entrypoint, or a shell command outside a step body will not see them. Outside a step body, use the `sourceRepo` / `workflowRepo` pair on the filter, generator, and rule contexts described below.
|
|
1470
|
+
|
|
1471
|
+
They are also set **only when there are two repos to point at**. An event from the workflow's own repo is matched from that repo's lock file rather than as a global candidate, so the workflow runs as an ordinary single-repo workflow: one checkout, and neither variable set. Read them with a fallback, as the example above does.
|
|
1472
|
+
|
|
1473
|
+
A global workflow's job runs with **no secrets at all** — neither the source repo's nor its own. See _Secrets are not available_ below.
|
|
1474
|
+
|
|
1475
|
+
### The triggering event
|
|
1476
|
+
|
|
1477
|
+
`ctx.event` inside a global workflow's job is the **source** repo's normalized event — the push or PR that fired the workflow, from a repo the workflow's own author may not own. `ctx.event.sourceRepo` names that repo.
|
|
1478
|
+
|
|
1479
|
+
That field is what makes a per-source-repo concurrency group expressible — and you have to write it. A global workflow runs on events from many repos, and their default branches share a name, so a group keyed on the branch alone puts every repo in one group, and with `cancelInProgress` (the default) one repo's push cancels another repo's in-flight run. That is still the behaviour of a branch-only group; naming the source repo in the key is what separates them:
|
|
1480
|
+
|
|
1481
|
+
```ts
|
|
1482
|
+
concurrency: {
|
|
1483
|
+
group: ({ branch, event }) => `${event.sourceRepo}:${branch}`,
|
|
1484
|
+
cancelInProgress: true,
|
|
1485
|
+
},
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
### Narrowing to the repos that need it
|
|
1489
|
+
|
|
1490
|
+
A global workflow that matches `myorg/*` will, by default, run on every repo in the org. Three mechanisms narrow it to the repos it actually applies to, in increasing order of power:
|
|
1491
|
+
|
|
1492
|
+
1. **A `requires` content filter on the trigger** — the cheapest gate. The orchestrator checks a file's contents (a JSON-path probe over `package.json`, for example) and drops the workflow **before any agent is dispatched** when the condition is not met. See [`requires` on triggers](https://docs.kici.dev/user/sdk/triggers/#content-requirements-requires). This is provider-dependent — it needs a file-contents fetcher, which the GitHub provider supplies.
|
|
1493
|
+
2. **A workflow-level `filter` predicate** — arbitrary TypeScript over the checked-out source tree (below). Works with any provider that clones.
|
|
1494
|
+
3. **A `DynamicJobFn`** — generate the exact job set from the source repo's state ([Generating jobs per source repo](https://docs.kici.dev/user/global-workflows/#generating-jobs-per-source-repo) below).
|
|
1495
|
+
|
|
1496
|
+
### Narrowing with a filter
|
|
1497
|
+
|
|
1498
|
+
Before reaching for a `filter`, check whether a declarative filter answers the question. `commitMessage` (on the trigger) and `requires` (over source files) are evaluated by the orchestrator from data it already has, so they cost no evaluation job at all — while a `filter` predicate dispatches one per (event × workflow repo). Gating on a `[skip ci]` marker, a conventional-commit prefix, or the contents of a named config file needs no predicate.
|
|
1499
|
+
|
|
1500
|
+
A workflow can declare a `filter`: a predicate that decides whether the workflow applies to this event at all.
|
|
1501
|
+
|
|
1502
|
+
```ts
|
|
1503
|
+
import { workflow, job, step, push } from '@kici-dev/sdk';
|
|
1504
|
+
|
|
1505
|
+
export default workflow('org-container-lint', {
|
|
1506
|
+
on: [push({ repos: ['myorg/*'] })],
|
|
1507
|
+
filter: async ({ sourceRepo, changedFilesStatus, $ }) => {
|
|
1508
|
+
// `changedFiles` throws when the diff is unavailable, so guard first.
|
|
1509
|
+
if (changedFilesStatus !== 'fetched') return true;
|
|
1510
|
+
const found = await $`ls ${sourceRepo.path}`;
|
|
1511
|
+
return found.stdout.includes('Dockerfile');
|
|
1512
|
+
},
|
|
1513
|
+
jobs: [
|
|
1514
|
+
job('lint-dockerfile', {
|
|
1515
|
+
runsOn: ['kici:os:linux'],
|
|
1516
|
+
steps: [
|
|
1517
|
+
step('lint', async ({ $, env }) => $`hadolint ${env.KICI_SOURCE_REPO_PATH}/Dockerfile`),
|
|
1518
|
+
],
|
|
1519
|
+
}),
|
|
1520
|
+
],
|
|
1521
|
+
});
|
|
1522
|
+
```
|
|
1523
|
+
|
|
1524
|
+
The filter receives a `FilterContext`:
|
|
1525
|
+
|
|
1526
|
+
| Property | Type | Description |
|
|
1527
|
+
| -------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
1528
|
+
| `sourceRepo` | `RepoInfo` | The repo whose event triggered this evaluation, checked out on the evaluating agent. |
|
|
1529
|
+
| `workflowRepo` | `RepoInfo` | The repo that registered the workflow. Identical to `sourceRepo` for a same-repo workflow. |
|
|
1530
|
+
| `event` | `EventPayload` | The normalized event envelope. |
|
|
1531
|
+
| `changedFiles` | `string[]` | Files changed in this event. Throws when unavailable — guard with `changedFilesStatus`. |
|
|
1532
|
+
| `changedFilesStatus` | `'fetched' \| 'unavailable' \| 'skipped'` | Whether `changedFiles` can be read. |
|
|
1533
|
+
| `env` | `Record<string, string\|undefined>` | Environment variables. |
|
|
1534
|
+
| `$` | zx shell | Shell executor. |
|
|
1535
|
+
|
|
1536
|
+
`RepoInfo` carries `path` (an absolute path to the checkout on the evaluating agent) plus optional `ref` and `sha`. **Both are optional** — an event that carries no single ref leaves them undefined, so guard before reading them.
|
|
1537
|
+
|
|
1538
|
+
**`sourceRepo.path` is not stable across evaluations.** Its _contents_ are: the evaluating agent and the later run see the same tree at the same commit. The path itself is not — a different working directory, and possibly a different machine. Read _through_ it; never embed it in a job name, an output, or anything compared across calls.
|
|
1539
|
+
|
|
1540
|
+
**A `filter` must be pure and deterministic.** Decide from the context alone — the event, the changed files, and the checked-out tree — so the same event always yields the same verdict.
|
|
1541
|
+
|
|
1542
|
+
### Global and same-repo filters differ
|
|
1543
|
+
|
|
1544
|
+
The same `filter` keyword means two different things depending on whether the workflow is global:
|
|
1545
|
+
|
|
1546
|
+
| | Global workflow (`repos:` on a trigger) | Same-repo workflow |
|
|
1547
|
+
| ------------------------------ | ------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
1548
|
+
| Evaluated | once per (event × workflow repo) | once per job that reaches dispatch, and once per job generator |
|
|
1549
|
+
| Evaluated relative to the run | **before** any run row exists | **after** the run row exists |
|
|
1550
|
+
| A `false` verdict leaves | no run at all — nothing appears in the dashboard | a run whose only entries are the evaluation jobs, rolling up to `success` |
|
|
1551
|
+
| `sourceRepo` vs `workflowRepo` | two different repos | the same repo |
|
|
1552
|
+
|
|
1553
|
+
Two consequences of the same-repo shape are worth designing for. A workflow with ten jobs calls its filter ten times for one event — each on its own agent with its own checkout and its own `$` — so anything the predicate does happens that many times: keep it cheap and side-effect free. And if the predicate can answer differently for the same event, the workflow will _partially_ dispatch, running some jobs and not others.
|
|
1554
|
+
|
|
1555
|
+
**A held or rejected job is not filtered at all.** A job held for approval, or rejected by a context rule, already has a gate — the hold or the rule — so it never takes a filter verdict, and an approved job dispatches without one. Concretely: a path filter cannot stop an approval request for a job the change does not concern.
|
|
1556
|
+
|
|
1557
|
+
### Generating jobs per source repo
|
|
1558
|
+
|
|
1559
|
+
A global workflow's job generators run in the same pre-run evaluation as the filter, with both repos on disk. `sourceRepo` and `workflowRepo` are on the generator context, so one workflow repo can produce a different job set per source repo:
|
|
1560
|
+
|
|
1561
|
+
```ts
|
|
1562
|
+
import { job, step, workflow, push, type DynamicJobFn } from '@kici-dev/sdk';
|
|
1563
|
+
import { readFile } from 'node:fs/promises';
|
|
1564
|
+
|
|
1565
|
+
const perRepoJobs: DynamicJobFn = async ({ sourceRepo }) => {
|
|
1566
|
+
if (!sourceRepo) return [];
|
|
1567
|
+
const pkg = JSON.parse(await readFile(`${sourceRepo.path}/package.json`, 'utf8'));
|
|
1568
|
+
return Object.keys(pkg.scripts ?? {})
|
|
1569
|
+
.filter((s) => s.startsWith('ci:'))
|
|
1570
|
+
.map((s) =>
|
|
1571
|
+
job(s.replace(':', '-'), {
|
|
1572
|
+
runsOn: ['kici:os:linux'],
|
|
1573
|
+
steps: [step('run', async ({ $ }) => $`pnpm ${s}`)],
|
|
1574
|
+
}),
|
|
1575
|
+
);
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
export default workflow('org-ci', {
|
|
1579
|
+
on: [push({ repos: ['myorg/*'] })],
|
|
1580
|
+
jobs: [perRepoJobs],
|
|
1581
|
+
});
|
|
1582
|
+
```
|
|
1583
|
+
|
|
1584
|
+
The same `sourceRepo.path` caution applies: read the tree through it, and derive job names from the repo's _contents_, never from the path.
|
|
1508
1585
|
|
|
1509
1586
|
## Enabling global workflows
|
|
1510
1587
|
|
|
1511
|
-
Global workflows are **
|
|
1588
|
+
Global workflows are gated by a **fleet-wide master switch** held by the orchestrator operator, off by default. Until it is on, `repos:`-bearing workflows are registered but never dispatched.
|
|
1512
1589
|
|
|
1513
|
-
1.
|
|
1514
|
-
2.
|
|
1515
|
-
3. Decide which authoring/source controls you need:
|
|
1590
|
+
1. **The operator enables it cluster-wide** with `kici-admin cluster-settings set --global-workflows-enabled true`. This is the kill-switch — every per-org control below is ignored while it is off, and it cannot be flipped from the dashboard. The dashboard's **Settings → Global workflows** tab shows its current state as a read-only badge.
|
|
1591
|
+
2. In the dashboard → **Settings → Global workflows**, decide which authoring/source controls you need. These per-org lists stay dashboard-editable; an org that has set none means "no per-org restrictions", not a denial.
|
|
1516
1592
|
|
|
1517
|
-
| Setting | What it controls | Typical use
|
|
1518
|
-
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1519
|
-
| Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation.
|
|
1520
|
-
| Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`.
|
|
1521
|
-
| Elevated access |
|
|
1593
|
+
| Setting | What it controls | Typical use |
|
|
1594
|
+
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
1595
|
+
| Allowed author repos | Restricts which repos can **author** (register) global workflows. Globs matched against the authoring repo identifier. When OFF, any repo in the org may author globals. | Lock authoring to `myorg/ci-*` so random product repos can't ship org-wide automation. |
|
|
1596
|
+
| Blocked source repos | Blocks dispatch for events emitted from these **source** repos, regardless of authoring. Globs matched against the event source repo identifier. When OFF, events from any repo may trigger globals. | Protect against fork spam — e.g. `!myorg/*` via `myorg/fork-*`. |
|
|
1597
|
+
| Elevated access | **Deprecated and not enforced.** Stored and echoed back, but nothing reads it — a global workflow's job receives no secrets, so there is no access for it to grant. See _Secrets are not available_. | None. Clear the list so it does not imply a grant that is not in force. |
|
|
1522
1598
|
|
|
1523
1599
|
All three lists accept globs. Leading `!` inside a single pattern is not supported here; negation is via the list-is-implicit-deny semantics, so keep it simple (`myorg/ci-*`, `myorg/platform-*`).
|
|
1524
1600
|
|
|
1601
|
+
Patterns match repo identifiers by the same rule as `repos:` on a trigger: an identifier is an owner/name pair, not a file path, so a leading dot carries no meaning of its own and a wildcard segment matches one. `myorg/*` covers `myorg/.github`, and `**` covers every repo in the org. Review any existing entry that relies on a wildcard to reach — or to spare — a dot-prefixed repo name.
|
|
1602
|
+
|
|
1525
1603
|
### Saving and reverting
|
|
1526
1604
|
|
|
1527
1605
|
The page is a two-state editor — changes are local until you click **Save changes**, and you can abandon them with **Discard changes**. There is no partial save; the PATCH is all-or-nothing per save click.
|
|
@@ -1533,17 +1611,23 @@ The page is a two-state editor — changes are local until you click **Save chan
|
|
|
1533
1611
|
A global workflow fires only if:
|
|
1534
1612
|
|
|
1535
1613
|
1. **The authoring repo is allowed.** If _Allowed author repos_ is ON, the workflow's authoring repo must match at least one allow-list glob. If OFF, any repo may author. Enforced at two points:
|
|
1536
|
-
- At registration time (extraction from the lock file — non-matching globals are dropped
|
|
1614
|
+
- At registration time (extraction from the lock file — non-matching globals are dropped, and the orchestrator logs `Global workflows excluded from registration` naming each one).
|
|
1537
1615
|
- At dispatch time (defense-in-depth — policy changes after registration still take effect).
|
|
1538
1616
|
2. **The source repo is not denied.** If the event's source repo matches any glob in _Blocked source repos_, the global workflow is skipped. Enforced at dispatch time.
|
|
1539
1617
|
|
|
1540
|
-
Both checks are logged to the orchestrator. Grep
|
|
1618
|
+
Both checks are logged to the orchestrator. Grep for `Global workflows excluded from registration` (registration time) and `Skipping global workflow dispatch` (dispatch time) to see enforcement in action.
|
|
1541
1619
|
|
|
1542
|
-
|
|
1620
|
+
Both checks read the settings of the organization the **event's source** resolves to. If no webhook source maps the event's routing key to an organization, the orchestrator resolves the built-in `__default__` organization anchor instead — and since nobody has enabled global workflows for that anchor, every global workflow is refused. The registration log line carries the organization it decided against plus the remedy, so this case is distinguishable from a real opt-in that is simply switched off. See the troubleshooting table below.
|
|
1543
1621
|
|
|
1544
|
-
|
|
1622
|
+
### Secrets are not available
|
|
1545
1623
|
|
|
1546
|
-
|
|
1624
|
+
A global workflow's job is dispatched with **no secret material** — not the source repo's, and not the workflow repo's own. The organization-wide dispatch path binds no secret contexts, so a `contexts:` declaration on a global workflow resolves to nothing and any secret the steps expect is simply absent. Plan for it: a global workflow is for checks, policy and reporting that need only the two checkouts, not for deploys that need credentials.
|
|
1625
|
+
|
|
1626
|
+
This is about your **stored secrets**, not about repository access: the job is still handed a short-lived clone token for each repo it checks out, which is how the dual checkout works at all. What it does not get is anything from a secret context.
|
|
1627
|
+
|
|
1628
|
+
To run something that needs secrets on a source repo's event, put those jobs in a per-repository workflow in that repo, where the workflow's `contexts:` resolve normally.
|
|
1629
|
+
|
|
1630
|
+
The **Elevated access** setting reads as the way to lift this, and it is not: it is **deprecated and never consulted**. Nothing in the dispatch path reads the list, and adding a repo to it does not make any secret readable. It is kept only so an existing value stays visible and clearable, and is removed at the next major version — see [Deprecations](https://docs.kici.dev/user/deprecations/).
|
|
1547
1631
|
|
|
1548
1632
|
## When does it fire?
|
|
1549
1633
|
|
|
@@ -1551,14 +1635,111 @@ Same-repo globals (a workflow in `myorg/app` with `repos: ['myorg/app']`) fire o
|
|
|
1551
1635
|
|
|
1552
1636
|
Non-push triggers work too — `pr()`, `tag()`, `comment()`, `release()`, `workflowRun()`, etc. all accept `repos:`. `kiciEvent()` / `schedule()` / cron-like triggers have no source repo, so they're always per-org-registered regardless of `repos:`.
|
|
1553
1637
|
|
|
1638
|
+
A global workflow that declares a `filter` or a job generator is decided by one **evaluation job per (event × workflow repo)**, dispatched before any run exists. That job checks out both repos once and evaluates every candidate workflow from that repo, so ten global workflows in one CI repo cost one evaluation, not ten.
|
|
1639
|
+
|
|
1640
|
+
When that evaluation cannot reach a verdict — it fails, breaches its budget, or never reports — the workflows it was deciding on **do not run**. On a provider that supports commit checks, that posts a `failure` check named **`KiCI: Organization workflow evaluation`** on the source commit, so the outcome is visible instead of silent. Three things to know about it:
|
|
1641
|
+
|
|
1642
|
+
- The check is posted whether the evaluation failed **outright** or only **partly**. A per-workflow budget breach, or a `filter` that throws, leaves that one workflow undecided while its neighbours from the same repo are decided and run normally; the check then names only the undecided ones. So a broken `filter` is reported the same way whether or not other global workflows happen to share its repo.
|
|
1643
|
+
- Branch protection that lists required checks by name is unaffected, because the check is not on that list. Merge automation that requires _every_ check to be green will block on it.
|
|
1644
|
+
- **The check clears only on a new commit.** A provider redelivery of the same event is dropped as a duplicate, so re-delivering the webhook will not re-run the evaluation.
|
|
1645
|
+
|
|
1646
|
+
## Approval gates are not supported
|
|
1647
|
+
|
|
1648
|
+
A global workflow cannot carry an `approval` gate, at the workflow level or on a job. Approval holds are applied by the per-repository dispatch path; the global path dispatches its jobs without consulting one, so a gate declared here would never be enforced. `kici compile` refuses it with `error [E124]` rather than accepting a security control the workflow does not actually have. A job produced by a `dynamicJob` generator never passes through the compiler, so that case is caught at dispatch instead — the orchestrator logs an error naming the workflow and job, and runs it ungated.
|
|
1649
|
+
|
|
1650
|
+
To gate a deployment behind a human, put the gated jobs in a workflow whose triggers carry no `repos:`.
|
|
1651
|
+
|
|
1652
|
+
## Re-running is not supported
|
|
1653
|
+
|
|
1654
|
+
A global workflow's run cannot be re-run. The rerun path resolves a workflow out of the repo the run acted on, and for a global workflow that is the **source** repo — not the workflow repo that declares it. Re-running is refused with an error naming both repos rather than resolving the wrong one, which for a source repo carrying a same-named workflow would silently run that workflow instead, with the source repo's credentials and none of the global job configuration.
|
|
1655
|
+
|
|
1656
|
+
To run it again, push a new commit to the source repo (a provider redelivery of the same event is dropped as a duplicate), or trigger it from the workflow repo.
|
|
1657
|
+
|
|
1658
|
+
## Requirements a filter places on the run
|
|
1659
|
+
|
|
1660
|
+
A `filter` reads the source tree, so the evaluation must be able to obtain one. A job that restores its workflow source from the cache and has no source repository to clone from fails with an explicit error rather than evaluating the filter against an empty tree. This applies to dispatch paths that run without a source repository configured — a filter and such a path are mutually exclusive; drop one or the other.
|
|
1661
|
+
|
|
1554
1662
|
## Troubleshooting
|
|
1555
1663
|
|
|
1556
|
-
| Symptom
|
|
1557
|
-
|
|
|
1558
|
-
| Global workflow registered but never runs
|
|
1559
|
-
|
|
|
1560
|
-
|
|
|
1561
|
-
|
|
|
1664
|
+
| Symptom | Likely cause | Where to look |
|
|
1665
|
+
| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1666
|
+
| Global workflow registered but never runs | Master toggle OFF, or allow-list blocks the authoring repo, or deny-list blocks the source repo | Orchestrator log: `Skipping global workflow dispatch` (dispatch time) / `Global workflows excluded from registration` (registration time) |
|
|
1667
|
+
| A global workflow is never registered at all — it is absent from `kici-admin registration list` | The push that should have registered it resolved to the `__default__` organization anchor, because no webhook source maps its routing key to an organization, or the fleet-wide master switch is off. | Orchestrator log: `Global workflows excluded from registration` with `"orgId": "__default__"`. Its `remedy` field names both fixes — map the source (`kici-admin source update <routingKey> --customer-id <org>`) and enable global workflows cluster-wide if it is not already (`kici-admin cluster-settings set --global-workflows-enabled true`). |
|
|
1668
|
+
| `repos:` has no effect — workflow only fires on its own repo | The fleet-wide master switch is off. Without it, the orchestrator treats the workflow as per-repo-only. | Check the fleet-wide switch with `kici-admin cluster-settings show`. The dashboard → Settings → Global workflows tab shows it as a read-only badge. |
|
|
1669
|
+
| Secrets unavailable in a global job | Expected — a global workflow's job receives no secrets at all, and the _Elevated access_ list is not enforced. | Move the jobs that need credentials into a per-repository workflow in the repo that owns the secrets |
|
|
1670
|
+
| Dashboard shows workflow twice after registering | Both a generic webhook source and a provider source (github, generic) re-registered the same repo. | Check `workflow_registrations` via `kici-admin workflow list` and confirm the right routing key owns the workflow. |
|
|
1671
|
+
| Global workflow registered, enabled, allowed — and still no run appears | Its `filter` returned `false`. A global filter runs before the run is created, so a suppressed workflow leaves nothing behind at all. | [Reading a global workflow's filter output](https://docs.kici.dev/user/global-workflows/#reading-a-global-workflows-filter-output) — the evaluation round's own log. The orchestrator also logs `Global workflow skipped by eval round`, naming the workflow and the reason. |
|
|
1672
|
+
| Global workflow never fires for one particular source repo | Its `repos:` patterns do not match that repo's identifier. | Orchestrator log: `Global workflows dropped by their repos filter` — one line per delivery, naming each dropped workflow, its repo and its patterns. |
|
|
1673
|
+
| A `failure` check named `KiCI: Organization workflow evaluation` on a commit | The pre-run evaluation failed or timed out, so the global workflows from that repo were not run. | Orchestrator log for the evaluation job; push a new commit to re-evaluate (a redelivery is dropped as a duplicate). |
|
|
1674
|
+
| Same-repo workflow shows a `success` run with no jobs in it | Its `filter` returned `false`. A same-repo filter runs after the run exists, so the run remains, carrying only the evaluation jobs. | The run detail page — the evaluation job's log records the filter verdict. |
|
|
1675
|
+
| Re-run is refused with "Cannot re-run an organization-wide workflow" | Expected — the run executed against a source repo that does not declare the workflow. | [Re-running is not supported](https://docs.kici.dev/user/global-workflows/#re-running-is-not-supported) — push a new commit to the source repo instead. |
|
|
1676
|
+
| Every global workflow stopped running right after an orchestrator upgrade | The agents were not upgraded first. An agent older than v0.5.0 cannot evaluate a global workflow, and one containing a `dynamicJob` now needs an evaluation even without a `filter` — so its **static** jobs stop too. | The `KiCI: Organization workflow evaluation` check names the agent versions it found. Upgrade every `kici:role:init-runner` agent to v0.5.0 or newer. |
|
|
1677
|
+
|
|
1678
|
+
### Reading a global run in the dashboard
|
|
1679
|
+
|
|
1680
|
+
A global run is attributed to the **source** repo — the repo whose event
|
|
1681
|
+
triggered it, and whose code the jobs check out. Its run detail page names both
|
|
1682
|
+
repos, so you can tell it apart from an ordinary per-repo run:
|
|
1683
|
+
|
|
1684
|
+
| Row | Shows |
|
|
1685
|
+
| ------------ | ------------------------------------------------------------------------ |
|
|
1686
|
+
| `Repository` | the source repo — the one the run acted on |
|
|
1687
|
+
| `Defined in` | the workflow repo, tagged `Organization-wide`. Absent on an ordinary run |
|
|
1688
|
+
| `Workflow` | links into the **workflow** repo, on its default branch |
|
|
1689
|
+
|
|
1690
|
+
The `Workflow` link points at the workflow repo's default branch rather than at
|
|
1691
|
+
a commit: the run's own commit belongs to the source repo, and nothing records
|
|
1692
|
+
which commit of the workflow repo a given run used. So the link always shows the
|
|
1693
|
+
file as it stands now, which may have changed since the run.
|
|
1694
|
+
|
|
1695
|
+
The `Payload` tab shows the source repo's event — the webhook delivery the
|
|
1696
|
+
workflow reacted to, which for a global workflow comes from a repo you may not
|
|
1697
|
+
own. A global run dispatched before your orchestrator stored payloads for this
|
|
1698
|
+
path has none, and its tab reports that it could not load one.
|
|
1699
|
+
|
|
1700
|
+
#### Who can see it
|
|
1701
|
+
|
|
1702
|
+
A global run belongs to **both** repos, so a member whose role is scoped to
|
|
1703
|
+
either one reaches it — the team whose push triggered it, and the team that
|
|
1704
|
+
authored the workflow. Both see it in the run list, in the repository filter
|
|
1705
|
+
(which offers both names), and on the run detail page. Cancelling follows the
|
|
1706
|
+
same rule, so the team whose workflow is running can always stop it.
|
|
1707
|
+
|
|
1708
|
+
Releasing a **held** run is the one exception: approving a hold permits code to
|
|
1709
|
+
run against the source repo, so it stays with a member scoped to that repo. A
|
|
1710
|
+
member scoped only to the workflow repo sees the run but not its hold.
|
|
1711
|
+
|
|
1712
|
+
This applies only where the two repos genuinely differ. An ordinary per-repo run
|
|
1713
|
+
records no separate workflow repo and is scoped to its own repo exactly as
|
|
1714
|
+
before, and a member scoped to neither repo sees nothing in either case.
|
|
1715
|
+
|
|
1716
|
+
### Reading a global workflow's filter output
|
|
1717
|
+
|
|
1718
|
+
A global workflow's `filter` runs in a pre-run evaluation round, and that round
|
|
1719
|
+
decides whether a run exists at all — so on the path where it suppresses a
|
|
1720
|
+
workflow there is no run, and nothing appears in the dashboard. The round's own
|
|
1721
|
+
log is still recorded. Read it with the orchestrator admin CLI, in two steps:
|
|
1722
|
+
|
|
1723
|
+
```bash
|
|
1724
|
+
# 1. Find the round. Its workflow name is __globaleval__<owner>/<repo> of the
|
|
1725
|
+
# WORKFLOW repo. In the JSON rows, `id` is the job id and `run_id` is the
|
|
1726
|
+
# run id.
|
|
1727
|
+
kici-admin queue list --workflow-name '__globaleval__myorg/ci-pipelines' --limit 5 --json
|
|
1728
|
+
|
|
1729
|
+
# 2. Print the round's log (step 0 is the evaluation itself).
|
|
1730
|
+
kici-admin runs logs <run_id> --job <id>
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
Use `--json` on the first command: the plain table abbreviates both ids to their
|
|
1734
|
+
first eight characters, and the second command needs them in full.
|
|
1735
|
+
|
|
1736
|
+
The two steps need different permissions, so run both with an **owner or admin**
|
|
1737
|
+
token. Step 1 reads the dispatch queue, which requires `secret.read` — an auditor
|
|
1738
|
+
token is refused with a 403 and never reaches step 2. Step 2 requires only
|
|
1739
|
+
`run.read`, which every role carries.
|
|
1740
|
+
|
|
1741
|
+
Anything your `filter` writes with `console.log` appears there, alongside the
|
|
1742
|
+
per-candidate verdicts the round recorded.
|
|
1562
1743
|
|
|
1563
1744
|
## See also
|
|
1564
1745
|
|
|
@@ -1744,7 +1925,7 @@ export default workflow('build', {
|
|
|
1744
1925
|
|
|
1745
1926
|
Per-field rules:
|
|
1746
1927
|
|
|
1747
|
-
- **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/kici-admin-
|
|
1928
|
+
- **`url`** — Must be HTTPS. HTTP is permitted only for `localhost` / `127.0.0.0/8` / `::1` / `*.local` hosts, or when an operator has flipped the org-level `allow_http_npm_registries` toggle (see [`kici-admin org-settings allow-http-npm`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#allow-http-npm--permit-non-https-private-npm-registries)).
|
|
1748
1929
|
- **`scope`** — Optional. When present, the registry serves only that scope (`@my-org`). When absent, this entry becomes the **default** registry — at most one entry may omit `scope`.
|
|
1749
1930
|
- **`tokenSecret`** — Mandatory `<context>:<secret-name>`. The orchestrator looks up the secret in the named context via the per-context secret resolver. The bare name **must not** contain a colon.
|
|
1750
1931
|
- **`alwaysAuth`** — Defaults to `true`. Forces npm to send the token on every request (even GETs), which is what most managed-registry providers require.
|
|
@@ -1958,7 +2139,7 @@ The dashboard JSON lives at `infra/terraform/modules/grafana/dashboards/install-
|
|
|
1958
2139
|
|
|
1959
2140
|
- [Secrets](https://docs.kici.dev/user/secrets/) — how to seed the `<context>:<secret-name>` values referenced by `tokenSecret` / `installEnv`.
|
|
1960
2141
|
- [Contexts](https://docs.kici.dev/user/contexts/) — protection rules (`branch_restrictions`, `requires_review`, `minimum_trust`) that the install gate inherits.
|
|
1961
|
-
- [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/kici-admin-
|
|
2142
|
+
- [Operator: `kici-admin org-settings`](https://docs.kici.dev/operator/orchestrator/kici-admin/org-settings/#org-settings----org-level-security-policy) — the `allow_http_npm_registries` toggle and other org-scoped knobs.
|
|
1962
2143
|
|
|
1963
2144
|
---
|
|
1964
2145
|
|
|
@@ -2264,7 +2445,7 @@ KiCI provides an explicit secrets API that gives workflow steps controlled acces
|
|
|
2264
2445
|
|
|
2265
2446
|
## Overview
|
|
2266
2447
|
|
|
2267
|
-
Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
|
|
2448
|
+
Secrets are managed per-context in the orchestrator (see [operator docs](https://docs.kici.dev/operator/orchestrator/configuration/) for setup). When a job runs with a `context` binding, the agent receives the secret keys available for that context but does **not** inject their values into the step's process environment. Instead, steps access secrets through the `ctx.secrets` API.
|
|
2268
2449
|
|
|
2269
2450
|
This design prevents accidental secret leakage through child processes, log output, or error messages. Only secrets you explicitly request are loaded into memory.
|
|
2270
2451
|
|
|
@@ -2285,13 +2466,13 @@ Use whichever fits the workflow — most small teams stay on the dashboard; ops
|
|
|
2285
2466
|
|
|
2286
2467
|
### When the operator has disabled dashboard writes
|
|
2287
2468
|
|
|
2288
|
-
The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy). When that flip is on:
|
|
2469
|
+
The orchestrator operator can flip `secrets.set` (and `variables.set`) to **CLI-only** as part of the [dashboard-write policy](https://docs.kici.dev/operator/security/dashboard-write-policy/). When that flip is on:
|
|
2289
2470
|
|
|
2290
2471
|
- The dashboard's "Add secret" / "Edit value" controls render with a lock icon. Hovering or keyboard-focusing the lock shows a tooltip with the exact `kici-admin secret set` invocation needed. The control itself is inert, so there is nothing to click.
|
|
2291
2472
|
- The dashboard's secrets page still lists secret **names**, scopes, and bindings — only the value-entry path moves to the CLI.
|
|
2292
2473
|
- `kici-admin secret set` becomes the single entry point for new and updated secret values.
|
|
2293
2474
|
|
|
2294
|
-
This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD,
|
|
2475
|
+
This configuration is common for SOC2-prep and regulated workloads, where the customer requirement is "the SaaS control plane process never receives plaintext customer secret values." The dashboard remains usable for everything else (read paths, name CRUD, context bindings).
|
|
2295
2476
|
|
|
2296
2477
|
### CLI input modes
|
|
2297
2478
|
|
|
@@ -2326,7 +2507,7 @@ Two cross-cutting flags help every mode:
|
|
|
2326
2507
|
|
|
2327
2508
|
`kici-admin variable set` uses the same flags for non-encrypted variables, plus `--locked` to mark a variable as immutable from subsequent dashboard writes.
|
|
2328
2509
|
|
|
2329
|
-
A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy
|
|
2510
|
+
A full reference of input modes — including the default-mode resolution rules and the security trade-offs of each — lives in [Dashboard-write policy → CLI input modes](https://docs.kici.dev/operator/security/dashboard-write-policy/#cli-input-modes-for-the-plaintext-path).
|
|
2330
2511
|
|
|
2331
2512
|
## Accessing secrets
|
|
2332
2513
|
|
|
@@ -2561,6 +2742,6 @@ Note that `get()` is async -- you must `await` the result.
|
|
|
2561
2742
|
|
|
2562
2743
|
When you run `kici types`, the compiler generates a `.kici/secrets.d.ts` file that provides type-safe autocompletion for your secret keys. The generated types augment the `StepSecrets` interface so that `ctx.secrets.get('...')` and `ctx.secrets.has('...')` offer suggestions for known keys.
|
|
2563
2744
|
|
|
2564
|
-
See [CLI reference](https://docs.kici.dev/user/cli) for the `kici types` command.
|
|
2745
|
+
See [CLI reference](https://docs.kici.dev/user/cli/authoring-and-local/#kici-types) for the `kici types` command.
|
|
2565
2746
|
|
|
2566
2747
|
---
|