@kici-dev/compiler 0.1.17 → 0.1.18

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.
@@ -25,6 +25,7 @@ The full markdown bundle of every page indexed here is available at https://docs
25
25
  - [Event payload reference](https://docs.kici.dev/user/sdk/event-payloads/): Generated schema of the normalized event envelope passed to rules and dynamic functions.
26
26
  - [SDK reference: idempotent](https://docs.kici.dev/user/sdk/idempotent/): Idempotent helpers for declarative check / apply patterns inside workflow steps
27
27
  - [SDK reference: rules, matrix, dynamic jobs](https://docs.kici.dev/user/sdk/rules-matrix-dynamic/): rule(), skip(), matrix builds (static + dynamic), and dynamicJob / dynamicGroup
28
+ - [SDK reference: runsOnAll host fan-out](https://docs.kici.dev/user/sdk/runs-on-all/): Fan one job out to every matching connected host, one pinned execution per host
28
29
  - [SDK reference: runtime](https://docs.kici.dev/user/sdk/runtime/): Types index, StepContext, secrets, fixtures
29
30
  - [SDK reference: triggers](https://docs.kici.dev/user/sdk/triggers/): GitHub event triggers, kiciEvent, workflowComplete, jobComplete, genericWebhook, schedule, lifecycle
30
31
  - [SDK reference: validation & events](https://docs.kici.dev/user/sdk/validation-events/): validateDag(), event definitions, and event emission
@@ -45,7 +46,7 @@ The full markdown bundle of every page indexed here is available at https://docs
45
46
  - [Account and sign-in](https://docs.kici.dev/user/account-and-login/): How your KiCI account relates to sign-in methods, and how to change the way you sign in.
46
47
  - [Approval gates](https://docs.kici.dev/user/approvals/): Pause a workflow for human sign-off at step, job, or workflow granularity with requireApproval
47
48
  - [Concurrency groups](https://docs.kici.dev/user/concurrency/): Control parallel execution with auto-cancel and queue modes
48
- - [Dashboard](https://docs.kici.dev/user/dashboard/)
49
+ - [Dashboard](https://docs.kici.dev/user/dashboard/): Web UI for monitoring workflow runs, managing sources, secrets, and organization settings.
49
50
  - [Dynamic values](https://docs.kici.dev/user/dynamic-values/)
50
51
  - [Environment variables](https://docs.kici.dev/user/env-vars/): KICI_* environment variable reference for the CLI
51
52
  - [Environments](https://docs.kici.dev/user/environments/): Configure deployment environments with variables, secrets, and protection rules
@@ -3,6 +3,7 @@ import { ensureTsLoaderHook } from "../execution/ts-loader.js";
3
3
  import { formatter } from "../test-runner/output-formatter.js";
4
4
  import { createStepContext } from "../test-runner/step-context.js";
5
5
  import { createRuleContext, evaluateRules as evaluateRulesWithFormatting } from "../test-runner/rule-evaluator.js";
6
+ import { localRunsOnString } from "./runs-on-display.js";
6
7
  import { toEventPayload } from "./to-event-payload.js";
7
8
  import { pathToFileURL } from "node:url";
8
9
  import path from "node:path";
@@ -135,7 +136,7 @@ async function resolveStaticJob(job, expansionMap) {
135
136
  workflow: { name: "" },
136
137
  job: {
137
138
  name: job.name,
138
- runsOn: job.runsOn
139
+ runsOn: localRunsOnString(job.runsOn)
139
140
  }
140
141
  },
141
142
  log: {
@@ -235,7 +236,7 @@ async function executeResolvedJobInner(resolvedJob, context, startTime) {
235
236
  const repoRoot = context.execDir;
236
237
  const stepCtx = createStepContext({ name: context.workflowName }, {
237
238
  name: expandedName,
238
- runsOn: typeof job.runsOn === "string" ? job.runsOn : Array.isArray(job.runsOn) ? job.runsOn.join(",") : (Array.isArray(job.runsOn.labels) ? job.runsOn.labels : [job.runsOn.labels]).join(",")
239
+ runsOn: localRunsOnString(job.runsOn)
239
240
  }, repoRoot, void 0, hasMatrix ? matrixValues : void 0, context.secrets, void 0, context.event.payload, context.event.provider);
240
241
  const stepResults = [];
241
242
  let stepCounter = 0;
@@ -0,0 +1,9 @@
1
+ import type { RunsOn } from '@kici-dev/sdk';
2
+ /**
3
+ * Render a job's `runsOn` to the comma-joined label string the local/test
4
+ * runners surface as `ctx.job.runsOn`. A `runsOnAll` job has no `runsOn` —
5
+ * locally it runs once on this machine, so we surface the marker `'runsOnAll'`.
6
+ * RegExp selectors render via their `/source/flags` literal.
7
+ */
8
+ export declare function localRunsOnString(runsOn: RunsOn | undefined): string;
9
+ //# sourceMappingURL=runs-on-display.d.ts.map
@@ -0,0 +1,23 @@
1
+ import "../chunk-BTugEXQM.js";
2
+ //#region src/local-executor/runs-on-display.ts
3
+ /** Render one selector element (string label or RegExp) for the local display string. */
4
+ function runsOnElementString(el) {
5
+ return el instanceof RegExp ? el.toString() : el;
6
+ }
7
+ /**
8
+ * Render a job's `runsOn` to the comma-joined label string the local/test
9
+ * runners surface as `ctx.job.runsOn`. A `runsOnAll` job has no `runsOn` —
10
+ * locally it runs once on this machine, so we surface the marker `'runsOnAll'`.
11
+ * RegExp selectors render via their `/source/flags` literal.
12
+ */
13
+ function localRunsOnString(runsOn) {
14
+ if (runsOn === void 0) return "runsOnAll";
15
+ if (typeof runsOn === "string") return runsOn;
16
+ if (runsOn instanceof RegExp) return runsOn.toString();
17
+ if (Array.isArray(runsOn)) return runsOn.map(runsOnElementString).join(",");
18
+ return (Array.isArray(runsOn.labels) ? runsOn.labels : [runsOn.labels]).map(runsOnElementString).join(",");
19
+ }
20
+ //#endregion
21
+ export { localRunsOnString };
22
+
23
+ //# sourceMappingURL=runs-on-display.js.map
@@ -20,6 +20,8 @@ export declare function detectGitRoot(): string;
20
20
  * `.kici/` and keyed on `.kici/yarn.lock`. The hash input is prefixed with the
21
21
  * manager name so two managers can never collide on identical lockfile bytes
22
22
  * (and a restored tree can never be the wrong layout for the agent's manager).
23
+ * For yarn the prefix also carries the flavor (`yarn-classic` / `yarn-berry`),
24
+ * so a classic-layout dep-cache tarball is never restored into a berry install.
23
25
  *
24
26
  * @param gitRoot - Absolute path to git repository root
25
27
  * @returns Hex SHA-256 hash string, or null if no lockfile is found
@@ -5,10 +5,11 @@ import { resolveHashFiles } from "./hash-files.js";
5
5
  import { analyzePurity } from "./purity-analyzer.js";
6
6
  import path from "node:path";
7
7
  import { readFileSync } from "node:fs";
8
- import { getDynamicJobGroup, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject, normalizeCacheSpecs, normalizeRequireApproval } from "@kici-dev/sdk";
8
+ import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject, normalizeCacheSpecs, normalizeRequireApproval } from "@kici-dev/sdk";
9
9
  import { sha256 } from "@kici-dev/core";
10
- import { PackageManager, detectPackageManagerSync } from "@kici-dev/core/package-manager";
10
+ import { PackageManager, detectPackageManagerSync, detectYarnFlavorSync } from "@kici-dev/core/package-manager";
11
11
  import { validateResourceRequest } from "@kici-dev/engine";
12
+ import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
12
13
  import { execSync } from "node:child_process";
13
14
  //#region src/lockfile/generator.ts
14
15
  /**
@@ -44,15 +45,18 @@ function detectGitRoot() {
44
45
  * `.kici/` and keyed on `.kici/yarn.lock`. The hash input is prefixed with the
45
46
  * manager name so two managers can never collide on identical lockfile bytes
46
47
  * (and a restored tree can never be the wrong layout for the agent's manager).
48
+ * For yarn the prefix also carries the flavor (`yarn-classic` / `yarn-berry`),
49
+ * so a classic-layout dep-cache tarball is never restored into a berry install.
47
50
  *
48
51
  * @param gitRoot - Absolute path to git repository root
49
52
  * @returns Hex SHA-256 hash string, or null if no lockfile is found
50
53
  */
51
54
  function computeLockfileHash(gitRoot) {
52
55
  const pm = detectPackageManagerSync(gitRoot) === PackageManager.Npm ? detectPackageManagerSync(path.join(gitRoot, ".kici")) : detectPackageManagerSync(gitRoot);
53
- const candidates = pm === PackageManager.Pnpm ? [path.join(gitRoot, "pnpm-lock.yaml")] : pm === PackageManager.Yarn ? [path.join(gitRoot, "yarn.lock"), path.join(gitRoot, ".kici", "yarn.lock")] : [path.join(gitRoot, ".kici", "package-lock.json")];
56
+ const candidates = pm === PackageManager.Pnpm ? [path.join(gitRoot, "pnpm-lock.yaml"), path.join(gitRoot, ".kici", "pnpm-lock.yaml")] : pm === PackageManager.Yarn ? [path.join(gitRoot, "yarn.lock"), path.join(gitRoot, ".kici", "yarn.lock")] : [path.join(gitRoot, ".kici", "package-lock.json")];
57
+ const prefix = pm === PackageManager.Yarn ? `${pm}-${detectYarnFlavorSync(detectPackageManagerSync(gitRoot) === PackageManager.Yarn ? gitRoot : path.join(gitRoot, ".kici"))}` : pm;
54
58
  for (const lockfilePath of candidates) try {
55
- return sha256(`${pm}\n${readFileSync(lockfilePath, "utf-8")}`);
59
+ return sha256(`${prefix}\n${readFileSync(lockfilePath, "utf-8")}`);
56
60
  } catch {}
57
61
  return null;
58
62
  }
@@ -422,13 +426,19 @@ function transformJobs(jobs, configPath, gitRoot) {
422
426
  return jobs.map((jobOrFactory, index) => {
423
427
  if (isDynamicJobFn(jobOrFactory)) {
424
428
  const groupName = getDynamicJobGroup(jobOrFactory);
429
+ const declaredNeeds = getDynamicJobNeeds(jobOrFactory);
430
+ const resolvedNeeds = declaredNeeds ? resolveNeedsForLock(declaredNeeds, uuidToName) : void 0;
425
431
  return {
426
432
  _type: "dynamic",
427
433
  source: {
428
434
  file: configPath,
429
435
  index
430
436
  },
431
- ...groupName && { group: groupName }
437
+ ...groupName && { group: groupName },
438
+ ...resolvedNeeds && {
439
+ needs: resolvedNeeds.needs,
440
+ resultAware: true
441
+ }
432
442
  };
433
443
  }
434
444
  const j = jobOrFactory;
@@ -440,19 +450,15 @@ function transformJobs(jobs, configPath, gitRoot) {
440
450
  });
441
451
  }
442
452
  /**
443
- * Normalize SDK runsOn into lock file representation.
444
- * - String passthrough: `'linux'` -> `{ runsOn: 'linux' }`
445
- * - Array passthrough: `['linux', 'docker']` -> `{ runsOn: ['linux', 'docker'] }`
446
- * - Object form: `{ labels: ['linux'], exclude: ['gpu'] }` -> `{ runsOn: ['linux'], excludeLabels: ['gpu'] }`
453
+ * Normalize SDK runsOn into lock file matchers.
454
+ * Plain strings stay exact; globs convert to regex; RegExp literals become regex.
455
+ * Throws (via the engine ReDoS gate) on a ReDoS-prone author regex.
447
456
  */
448
- function normalizeRunsOnForLock(runsOn) {
449
- if (typeof runsOn === "string") return { runsOn };
450
- if (Array.isArray(runsOn)) return { runsOn };
451
- const labels = Array.isArray(runsOn.labels) ? runsOn.labels : [runsOn.labels];
452
- const exclude = runsOn.exclude ? Array.isArray(runsOn.exclude) ? runsOn.exclude : [runsOn.exclude] : void 0;
457
+ function normalizeRunsOnForLock(runsOn, jobName) {
458
+ const { include, exclude } = normalizeRunsOnToMatchers(runsOn, `job '${jobName}' runsOn`);
453
459
  return {
454
- runsOn: labels.length === 1 ? labels[0] : labels,
455
- ...exclude && exclude.length > 0 ? { excludeLabels: exclude } : {}
460
+ runsOn: include,
461
+ ...exclude.length > 0 ? { excludeLabels: exclude } : {}
456
462
  };
457
463
  }
458
464
  /**
@@ -463,19 +469,26 @@ function normalizeRunsOnForLock(runsOn) {
463
469
  * targeting a `kici:os:linux` / `kici:role:builder` label is benign. The forgery
464
470
  * boundary is label *setting* (scaler labelSet validation + the agent
465
471
  * register-time scope gate), which is enforced elsewhere and unaffected here.
472
+ *
473
+ * The overlap check compares exact matchers only — a glob/regex include and an
474
+ * exact exclude (or vice versa) cannot be statically known to overlap.
466
475
  */
467
476
  function validateRunsOn(runsOn, jobName) {
468
- if (typeof runsOn === "string" || Array.isArray(runsOn)) return;
469
- const labels = Array.isArray(runsOn.labels) ? runsOn.labels : [runsOn.labels];
470
- const exclude = runsOn.exclude ? Array.isArray(runsOn.exclude) ? runsOn.exclude : [runsOn.exclude] : [];
471
- const overlap = labels.filter((l) => exclude.includes(l));
477
+ const { include, exclude } = normalizeRunsOnToMatchers(runsOn, `job '${jobName}' runsOn`);
478
+ const includeExact = new Set(include.filter((m) => m.kind === "exact").map((m) => m.value));
479
+ const overlap = exclude.filter((m) => m.kind === "exact").map((m) => m.value).filter((v) => includeExact.has(v));
472
480
  if (overlap.length > 0) throw new Error(`Job "${jobName}": labels and exclude overlap on [${overlap.join(", ")}]. A label cannot be both required and excluded.`);
473
481
  }
474
482
  /**
475
483
  * Transform a static job to lock file format.
476
484
  */
477
485
  function transformJob(job, configPath, index, gitRoot, uuidToName) {
478
- validateRunsOn(job.runsOn, job.name);
486
+ if (job.runsOn !== void 0 && job.runsOnAll !== void 0) throw new Error(`job '${job.name}': runsOn and runsOnAll are mutually exclusive`);
487
+ if (job.runsOn === void 0 && job.runsOnAll === void 0) throw new Error(`job '${job.name}': one of runsOn or runsOnAll is required`);
488
+ if (job.onUnreachable !== void 0 && job.runsOnAll === void 0) console.warn(`[kici] job '${job.name}': onUnreachable is ignored without runsOnAll`);
489
+ if (job.maxParallel !== void 0 && job.maxParallel < 1) throw new Error(`job '${job.name}': maxParallel must be >= 1`);
490
+ if (!(job.matrix !== void 0 || job.runsOnAll !== void 0) && (job.maxParallel !== void 0 || job.failFast !== void 0)) console.warn(`[kici] job '${job.name}': maxParallel/failFast are ignored without matrix or runsOnAll (no fan-out to bound)`);
491
+ if (job.runsOn !== void 0) validateRunsOn(job.runsOn, job.name);
479
492
  const environmentFields = {};
480
493
  if (job.environment !== void 0) {
481
494
  if (typeof job.environment === "function") {
@@ -524,7 +537,11 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
524
537
  return {
525
538
  _type: "static",
526
539
  name: job.name,
527
- ...normalizeRunsOnForLock(job.runsOn),
540
+ ...job.runsOn !== void 0 ? normalizeRunsOnForLock(job.runsOn, job.name) : {},
541
+ ...job.runsOnAll !== void 0 && { runsOnAll: normalizeRunsOnAllToMatchers(job.runsOnAll, `job '${job.name}' runsOnAll`) },
542
+ ...job.onUnreachable !== void 0 && { onUnreachable: job.onUnreachable },
543
+ ...job.maxParallel !== void 0 && { maxParallel: job.maxParallel },
544
+ ...job.failFast !== void 0 && { failFast: job.failFast },
528
545
  ...resolveNeedsForLock(job.needs, uuidToName),
529
546
  steps: transformSteps(job.steps, gitRoot),
530
547
  matrix: job.matrix ? transformMatrix(job.matrix, job.name, configPath) : void 0,
@@ -1,6 +1,6 @@
1
1
  import "../chunk-BTugEXQM.js";
2
2
  //#region src/templates/package-json.ts
3
- const sdkVersion = "0.1.17";
3
+ const sdkVersion = "0.1.18";
4
4
  /**
5
5
  * Generate package.json content for .kici/ directory
6
6
  *
@@ -2,6 +2,7 @@ import "../chunk-BTugEXQM.js";
2
2
  import { formatter } from "./output-formatter.js";
3
3
  import { createStepContext } from "./step-context.js";
4
4
  import { createRuleContext, evaluateRules as evaluateRulesWithFormatting } from "./rule-evaluator.js";
5
+ import { localRunsOnString } from "../local-executor/runs-on-display.js";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import path from "node:path";
7
8
  import pc from "picocolors";
@@ -94,7 +95,7 @@ async function executeJob(job, workflowName, event, repoRoot, testSecrets, sdkSe
94
95
  setStepRefMap(refMap);
95
96
  const ctx = createStepContext({ name: workflowName }, {
96
97
  name: job.name,
97
- runsOn: typeof job.runsOn === "string" ? job.runsOn : Array.isArray(job.runsOn) ? job.runsOn.join(",") : (Array.isArray(job.runsOn.labels) ? job.runsOn.labels : [job.runsOn.labels]).join(",")
98
+ runsOn: localRunsOnString(job.runsOn)
98
99
  }, repoRoot, void 0, void 0, testSecrets, void 0, event.payload, event.provider);
99
100
  let stepCounter = 0;
100
101
  for (const stepOrFn of job.steps) {
package/dist/types.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * v15 adds per-job init config(s).
13
13
  * v17 widens per-job init to typed presets ('mise' / { mise }) and 'auto' detection.
14
14
  */
15
- import type { ResourceRequest, ApproverClause } from '@kici-dev/engine';
15
+ import type { ResourceRequest, ApproverClause, RunsOnAllPredicate, OnUnreachableMode, LabelMatcher } from '@kici-dev/engine';
16
16
  /**
17
17
  * Normalized approval config carried in the lock file. Mirrors the engine
18
18
  * `LockApproval` type. Produced by the compiler from an SDK `requireApproval`.
@@ -23,7 +23,7 @@ export interface LockApproval {
23
23
  readonly timeoutSeconds?: number;
24
24
  }
25
25
  /** Schema version - re-exported from engine as single source of truth */
26
- export declare const SCHEMA_VERSION: 17;
26
+ export declare const SCHEMA_VERSION: 20;
27
27
  /**
28
28
  * Source file reference with meaningful path.
29
29
  * Format: file is relative path from git root, export uses hash syntax.
@@ -371,8 +371,17 @@ export interface LockNeedsGroupEntry {
371
371
  export interface LockJob {
372
372
  readonly _type: 'static';
373
373
  readonly name: string;
374
- readonly runsOn: string | readonly string[];
375
- readonly excludeLabels?: readonly string[];
374
+ /** Single-agent targeting matchers. Absent when the job uses runsOnAll instead. */
375
+ readonly runsOn?: readonly LabelMatcher[];
376
+ readonly excludeLabels?: readonly LabelMatcher[];
377
+ /** Host fan-out predicate (mutually exclusive with runsOn). */
378
+ readonly runsOnAll?: RunsOnAllPredicate;
379
+ /** Failure policy for unreachable durable hosts in a runsOnAll fan-out. */
380
+ readonly onUnreachable?: OnUnreachableMode;
381
+ /** Fan-out concurrency width (sliding window; 1 = serial). Applies to matrix and runsOnAll. */
382
+ readonly maxParallel?: number;
383
+ /** Halt the fan-out on first child failure, skipping the held remainder. Default false. */
384
+ readonly failFast?: boolean;
376
385
  readonly needs: readonly (string | LockNeedsEntry | LockNeedsGroupEntry)[];
377
386
  /** Group names this job depends on (populated from dynamicGroup refs). */
378
387
  readonly dependsOnGroups?: readonly string[];
@@ -448,6 +457,15 @@ export interface LockDynamicJobFn {
448
457
  };
449
458
  /** Group name for cross-domain needs (set when dynamicJob('name', fn) is used). */
450
459
  readonly group?: string;
460
+ /**
461
+ * Declared upstream needs for a result-aware generator. When present the
462
+ * generator's eval job is deferred until these upstreams complete, then run
463
+ * with their frozen outputs available as ctx.needs. Same normalized shape as
464
+ * a static job's `needs`.
465
+ */
466
+ readonly needs?: readonly (string | LockNeedsEntry | LockNeedsGroupEntry)[];
467
+ /** True when this dynamic entry was authored as dynamicJob(group, { needs, generate }). */
468
+ readonly resultAware?: boolean;
451
469
  }
452
470
  /** Job or dynamic job generator */
453
471
  export type LockJobOrFactory = LockJob | LockDynamicJobFn;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/compiler",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Compiler and CLI for KiCI workflows. Compiles `.kici/workflows/*.ts` to a `kici.lock.json` file consumed by the orchestrator and agents, and runs workflows locally or against a remote orchestrator.",
5
5
  "keywords": [
6
6
  "ci",
@@ -49,25 +49,25 @@
49
49
  "./cli": "./dist/cli.js"
50
50
  },
51
51
  "dependencies": {
52
- "@inquirer/prompts": "^8.4.2",
52
+ "@inquirer/prompts": "^8.5.2",
53
53
  "chokidar": "^5.0.0",
54
- "commander": "^14.0.3",
54
+ "commander": "^15.0.0",
55
55
  "fast-glob": "^3.3.3",
56
56
  "open": "^11.0.0",
57
57
  "picocolors": "^1.1.1",
58
58
  "picomatch": "^4.0.4",
59
59
  "proper-lockfile": "^4.1.2",
60
- "tar": "^7.5.13",
60
+ "tar": "^7.5.16",
61
61
  "typescript": "^6.0.3",
62
- "ws": "^8.20.0",
63
- "yaml": "^2.8.3",
64
- "zod": "^4.3.6",
62
+ "ws": "^8.21.0",
63
+ "yaml": "^2.9.0",
64
+ "zod": "^4.4.3",
65
65
  "zx": "^8.8.5",
66
- "@kici-dev/core": "0.1.17",
67
- "@kici-dev/engine": "0.1.17"
66
+ "@kici-dev/core": "0.1.18",
67
+ "@kici-dev/engine": "0.1.18"
68
68
  },
69
69
  "peerDependencies": {
70
- "@kici-dev/sdk": "0.1.17"
70
+ "@kici-dev/sdk": "0.1.18"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@types/proper-lockfile": "^4.1.4"