@kici-dev/compiler 0.3.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 +30 -14
- 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 +652 -236
- 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
|
@@ -5,13 +5,13 @@ import "../errors/index.js";
|
|
|
5
5
|
import { BREAKING_FLOOR as BREAKING_FLOOR$1, SCHEMA_VERSION as SCHEMA_VERSION$1 } from "../types.js";
|
|
6
6
|
import { computeContentHash } from "./hasher.js";
|
|
7
7
|
import { resolveHashFiles } from "./hash-files.js";
|
|
8
|
-
import { analyzePurity } from "./purity-analyzer.js";
|
|
9
8
|
import path from "node:path";
|
|
10
9
|
import { readFileSync } from "node:fs";
|
|
11
10
|
import { getDynamicJobGroup, getDynamicJobNeeds, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isParallelGroup, isStaticArray, isStaticObject, normalizeApproval, normalizeCacheSpecs } from "@kici-dev/sdk";
|
|
12
11
|
import { sha256 } from "@kici-dev/core";
|
|
13
12
|
import { PackageManager, detectPackageManagerSync, detectYarnFlavorSync } from "@kici-dev/core/package-manager";
|
|
14
|
-
import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
|
|
13
|
+
import { assertScheduleInputsSatisfiable, extractInputsDescriptorMap, resolveContentFormat, resolveWhenToRunOn, validateResourceRequest } from "@kici-dev/engine";
|
|
14
|
+
import { assertSafeRegex } from "@kici-dev/engine/safe-regex";
|
|
15
15
|
import { normalizeRunsOnAllToMatchers, normalizeRunsOnToMatchers, runsOnPickFromInput } from "@kici-dev/engine/labels/compile";
|
|
16
16
|
import { execSync } from "node:child_process";
|
|
17
17
|
//#region src/lockfile/generator.ts
|
|
@@ -178,7 +178,8 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
|
|
|
178
178
|
...workflow.concurrency.max !== void 0 && { max: workflow.concurrency.max }
|
|
179
179
|
} },
|
|
180
180
|
...workflow.timeout !== void 0 && { timeout: workflow.timeout },
|
|
181
|
-
...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow", locationForWorkflow(sourceFile)), toLockApproval(workflow.approval)) }
|
|
181
|
+
...workflow.approval !== void 0 && { approval: (assertNonStepApprovalScope(workflow.approval, "workflow", locationForWorkflow(sourceFile)), toLockApproval(workflow.approval)) },
|
|
182
|
+
...typeof workflow.filter === "function" && { hasFilter: true }
|
|
182
183
|
};
|
|
183
184
|
}
|
|
184
185
|
/**
|
|
@@ -189,6 +190,114 @@ function transformWorkflow(workflow, sourceFile, exportRef, bundleSource, gitRoo
|
|
|
189
190
|
function reposField(trigger) {
|
|
190
191
|
return trigger.repos && trigger.repos.length > 0 ? { repos: transformBranchPatterns(trigger.repos) } : {};
|
|
191
192
|
}
|
|
193
|
+
/** Normalize a scalar-or-array SDK value to an array. */
|
|
194
|
+
function toArray(v) {
|
|
195
|
+
if (v === void 0) return [];
|
|
196
|
+
return Array.isArray(v) ? v : [v];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Normalize one regex entry to the lock's `/pattern/flags` form, rejecting an
|
|
200
|
+
* invalid or ReDoS-prone pattern at compile time so a catastrophic pattern fails
|
|
201
|
+
* `kici compile` with author feedback instead of reaching the orchestrator.
|
|
202
|
+
*/
|
|
203
|
+
function serializeRegexEntry(entry, ctx) {
|
|
204
|
+
const source = entry instanceof RegExp ? `/${entry.source}/${entry.flags}` : entry;
|
|
205
|
+
const wrapped = /^\/(.+)\/([gimsuy]*)$/.exec(source);
|
|
206
|
+
const pattern = wrapped ? wrapped[1] : source;
|
|
207
|
+
const flags = wrapped ? wrapped[2] : "";
|
|
208
|
+
let re;
|
|
209
|
+
try {
|
|
210
|
+
re = new RegExp(pattern, flags);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
throw compilerError("E123", `${ctx}: invalid regex — ${err instanceof Error ? err.message : String(err)}`);
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
assertSafeRegex(re.source, re.flags, ctx);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
throw compilerError("E123", `${ctx}: ${err instanceof Error ? err.message : String(err)}`);
|
|
218
|
+
}
|
|
219
|
+
return `/${re.source}/${re.flags}`;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Serialize a {@link TextMatch} to its lock form: every key normalized to a flat
|
|
223
|
+
* array, every regex to `/pattern/flags`, every degenerate shape rejected.
|
|
224
|
+
*
|
|
225
|
+
* `ctx` is a human label woven into any thrown error (e.g. `push trigger
|
|
226
|
+
* 'commitMessage'`, `requires 'Dockerfile'`).
|
|
227
|
+
*/
|
|
228
|
+
function serializeTextMatch(m, ctx) {
|
|
229
|
+
const contains = toArray(m.contains);
|
|
230
|
+
const notContains = toArray(m.notContains);
|
|
231
|
+
const matches = toArray(m.matches);
|
|
232
|
+
const notMatches = toArray(m.notMatches);
|
|
233
|
+
if (contains.length === 0 && notContains.length === 0 && matches.length === 0 && notMatches.length === 0) throw compilerError("E122", `${ctx}: no query key (contains/notContains/matches/notMatches) — nothing to check`);
|
|
234
|
+
for (const needle of [...contains, ...notContains]) if (needle === "") throw compilerError("E122", `${ctx}: an empty needle matches every text — remove it`);
|
|
235
|
+
if (m.ignoreCase !== void 0 && contains.length === 0 && notContains.length === 0) throw compilerError("E122", `${ctx}: 'ignoreCase' affects only contains/notContains — a regex carries its own flags`);
|
|
236
|
+
return {
|
|
237
|
+
...contains.length > 0 && { contains: [...contains] },
|
|
238
|
+
...notContains.length > 0 && { notContains: [...notContains] },
|
|
239
|
+
...matches.length > 0 && { matches: matches.map((e) => serializeRegexEntry(e, ctx)) },
|
|
240
|
+
...notMatches.length > 0 && { notMatches: notMatches.map((e) => serializeRegexEntry(e, ctx)) },
|
|
241
|
+
...m.ignoreCase !== void 0 && { ignoreCase: m.ignoreCase }
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Emit the optional `commitMessage` field for a git-event lock trigger. */
|
|
245
|
+
function commitMessageField(commitMessage, triggerLabel) {
|
|
246
|
+
if (commitMessage === void 0) return {};
|
|
247
|
+
return { commitMessage: serializeTextMatch(commitMessage, `${triggerLabel} 'commitMessage'`) };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Serialize one SDK content requirement into its lock form, validating and
|
|
251
|
+
* resolving `format: 'auto'` by extension at compile time. A malformed filter
|
|
252
|
+
* throws (fails `kici compile`) rather than reaching the orchestrator.
|
|
253
|
+
*/
|
|
254
|
+
function serializeOneRequirement(req) {
|
|
255
|
+
const hasJsonQuery = req.exists !== void 0 && req.exists.length > 0 || req.match !== void 0 && Object.keys(req.match).length > 0 || req.not !== void 0 && Object.keys(req.not).length > 0;
|
|
256
|
+
const textKeys = {
|
|
257
|
+
...req.contains !== void 0 && { contains: req.contains },
|
|
258
|
+
...req.notContains !== void 0 && { notContains: req.notContains },
|
|
259
|
+
...req.matches !== void 0 && { matches: req.matches },
|
|
260
|
+
...req.notMatches !== void 0 && { notMatches: req.notMatches },
|
|
261
|
+
...req.ignoreCase !== void 0 && { ignoreCase: req.ignoreCase }
|
|
262
|
+
};
|
|
263
|
+
const hasTextQuery = req.contains !== void 0 || req.notContains !== void 0 || req.matches !== void 0 || req.notMatches !== void 0;
|
|
264
|
+
const hasAnyQuery = hasJsonQuery || hasTextQuery || req.ignoreCase !== void 0;
|
|
265
|
+
if (req.absent) {
|
|
266
|
+
if (hasAnyQuery) throw compilerError("E118", `requires '${req.file}': 'absent' is mutually exclusive with query keys (exists/match/not/contains/notContains/matches/notMatches)`);
|
|
267
|
+
return {
|
|
268
|
+
file: req.file,
|
|
269
|
+
absent: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const format = resolveContentFormat(req.file, req.format);
|
|
273
|
+
if (format === "text" && hasJsonQuery) throw compilerError("E119", `requires '${req.file}': text format cannot carry a json/yaml query key (exists/match/not) — name a .json/.yaml file or set format: 'json' | 'yaml'`);
|
|
274
|
+
if (format !== "text" && (hasTextQuery || req.ignoreCase !== void 0)) throw compilerError("E119", `requires '${req.file}': '${format}' format cannot carry a raw-text key (contains/notContains/matches/notMatches/ignoreCase) — use format: 'text'`);
|
|
275
|
+
if (!hasAnyQuery) {
|
|
276
|
+
if (req.format !== void 0) throw compilerError("E122", `requires '${req.file}': format '${req.format}' declared with no query key — nothing to check`);
|
|
277
|
+
return { file: req.file };
|
|
278
|
+
}
|
|
279
|
+
const text = hasTextQuery ? serializeTextMatch(textKeys, `requires '${req.file}'`) : {};
|
|
280
|
+
return {
|
|
281
|
+
file: req.file,
|
|
282
|
+
format,
|
|
283
|
+
...req.exists !== void 0 && { exists: req.exists },
|
|
284
|
+
...req.match !== void 0 && { match: req.match },
|
|
285
|
+
...req.not !== void 0 && { not: req.not },
|
|
286
|
+
...text
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Serialize a trigger's `requires` list, or `undefined` when empty (so the lock
|
|
291
|
+
* field stays absent). Each entry is validated + format-resolved at compile time.
|
|
292
|
+
*/
|
|
293
|
+
function serializeRequires(requires) {
|
|
294
|
+
if (!requires || requires.length === 0) return void 0;
|
|
295
|
+
return requires.map(serializeOneRequirement);
|
|
296
|
+
}
|
|
297
|
+
function requiresField(requires) {
|
|
298
|
+
const serialized = serializeRequires(requires);
|
|
299
|
+
return serialized ? { requires: serialized } : {};
|
|
300
|
+
}
|
|
192
301
|
function toLockPr(t) {
|
|
193
302
|
return {
|
|
194
303
|
_type: "pr",
|
|
@@ -196,19 +305,27 @@ function toLockPr(t) {
|
|
|
196
305
|
targetBranches: transformBranchPatterns(t.targetBranches),
|
|
197
306
|
sourceBranches: transformBranchPatterns(t.sourceBranches),
|
|
198
307
|
paths: t.paths,
|
|
199
|
-
...reposField(t)
|
|
308
|
+
...reposField(t),
|
|
309
|
+
...requiresField(t.requires),
|
|
310
|
+
...commitMessageField(t.commitMessage, "pr trigger")
|
|
200
311
|
};
|
|
201
312
|
}
|
|
202
313
|
function toLockPushAndTag(t) {
|
|
314
|
+
const requires = requiresField(t.requires);
|
|
315
|
+
const commitMessage = commitMessageField(t.commitMessage, "push trigger");
|
|
203
316
|
const results = [{
|
|
204
317
|
_type: "push",
|
|
205
318
|
branches: transformBranchPatterns(t.branches),
|
|
206
319
|
paths: t.paths,
|
|
207
|
-
...reposField(t)
|
|
320
|
+
...reposField(t),
|
|
321
|
+
...requires,
|
|
322
|
+
...commitMessage
|
|
208
323
|
}];
|
|
209
324
|
if (t.tags.length > 0) results.push({
|
|
210
325
|
_type: "tag",
|
|
211
|
-
patterns: transformBranchPatterns(t.tags)
|
|
326
|
+
patterns: transformBranchPatterns(t.tags),
|
|
327
|
+
...requires,
|
|
328
|
+
...commitMessage
|
|
212
329
|
});
|
|
213
330
|
return results;
|
|
214
331
|
}
|
|
@@ -216,7 +333,9 @@ function toLockTag(t) {
|
|
|
216
333
|
return {
|
|
217
334
|
_type: "tag",
|
|
218
335
|
patterns: transformBranchPatterns(t.patterns),
|
|
219
|
-
...reposField(t)
|
|
336
|
+
...reposField(t),
|
|
337
|
+
...requiresField(t.requires),
|
|
338
|
+
...commitMessageField(t.commitMessage, "tag trigger")
|
|
220
339
|
};
|
|
221
340
|
}
|
|
222
341
|
function toLockComment(t) {
|
|
@@ -518,25 +637,15 @@ function validateRunsOn(runsOn, jobName, location) {
|
|
|
518
637
|
}
|
|
519
638
|
/**
|
|
520
639
|
* Transform one context reference (static name or function) into a lock
|
|
521
|
-
* `{ value, dynamic }` entry. A function element
|
|
522
|
-
*
|
|
523
|
-
*
|
|
640
|
+
* `{ value, dynamic }` entry. A function element carries only the `dynamic`
|
|
641
|
+
* flag (the agent resolves it in the init round); a static name carries its
|
|
642
|
+
* literal value.
|
|
524
643
|
*/
|
|
525
644
|
function transformContextRef(ref) {
|
|
526
|
-
if (typeof ref === "function") {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
_type: "inline",
|
|
531
|
-
expression: fnSource
|
|
532
|
-
},
|
|
533
|
-
dynamic: true
|
|
534
|
-
};
|
|
535
|
-
return {
|
|
536
|
-
value: "",
|
|
537
|
-
dynamic: true
|
|
538
|
-
};
|
|
539
|
-
}
|
|
645
|
+
if (typeof ref === "function") return {
|
|
646
|
+
value: "",
|
|
647
|
+
dynamic: true
|
|
648
|
+
};
|
|
540
649
|
return {
|
|
541
650
|
value: ref,
|
|
542
651
|
dynamic: false
|
|
@@ -567,15 +676,8 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
|
|
|
567
676
|
if (contextRefs !== void 0 && contextRefs.length > 0) contextFields.contexts = contextRefs.map((ref) => transformContextRef(ref));
|
|
568
677
|
const envFields = {};
|
|
569
678
|
if (job.env !== void 0) {
|
|
570
|
-
if (typeof job.env === "function")
|
|
571
|
-
|
|
572
|
-
const purity = analyzePurity(fnSource);
|
|
573
|
-
envFields.dynamicEnv = true;
|
|
574
|
-
if (purity.pure) envFields.env = {
|
|
575
|
-
_type: "inline",
|
|
576
|
-
expression: fnSource
|
|
577
|
-
};
|
|
578
|
-
} else if (typeof job.env === "object") envFields.env = { ...job.env };
|
|
679
|
+
if (typeof job.env === "function") envFields.dynamicEnv = true;
|
|
680
|
+
else if (typeof job.env === "object") envFields.env = { ...job.env };
|
|
579
681
|
}
|
|
580
682
|
if (job.resources !== void 0) try {
|
|
581
683
|
validateResourceRequest(job.resources);
|
|
@@ -588,15 +690,8 @@ function transformJob(job, configPath, index, gitRoot, uuidToName) {
|
|
|
588
690
|
}
|
|
589
691
|
const concurrencyFields = {};
|
|
590
692
|
if (job.concurrencyGroup !== void 0) {
|
|
591
|
-
if (typeof job.concurrencyGroup === "function")
|
|
592
|
-
|
|
593
|
-
const purity = analyzePurity(fnSource);
|
|
594
|
-
concurrencyFields.dynamicConcurrencyGroup = true;
|
|
595
|
-
if (purity.pure) concurrencyFields.concurrencyGroup = {
|
|
596
|
-
_type: "inline",
|
|
597
|
-
expression: fnSource
|
|
598
|
-
};
|
|
599
|
-
} else if (typeof job.concurrencyGroup === "string") concurrencyFields.concurrencyGroup = job.concurrencyGroup;
|
|
693
|
+
if (typeof job.concurrencyGroup === "function") concurrencyFields.dynamicConcurrencyGroup = true;
|
|
694
|
+
else if (typeof job.concurrencyGroup === "string") concurrencyFields.concurrencyGroup = job.concurrencyGroup;
|
|
600
695
|
}
|
|
601
696
|
return {
|
|
602
697
|
_type: "static",
|
package/dist/lockfile/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export { generateLockFile, serializeLockFile, detectGitRoot, computeLockfileHash, schemaWindowWarning, } from './generator.js';
|
|
2
2
|
export { computeContentHash, COMPILE_SCHEMA_VERSION } from './hasher.js';
|
|
3
|
-
export { DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings, } from './purity-diagnostics.js';
|
|
4
|
-
export type { JobPurityWarning } from './purity-diagnostics.js';
|
|
5
3
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/lockfile/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import "../rolldown-runtime-ClRpJifh.js";
|
|
2
2
|
import { COMPILE_SCHEMA_VERSION, computeContentHash } from "./hasher.js";
|
|
3
3
|
import { computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile } from "./generator.js";
|
|
4
|
-
|
|
5
|
-
export { COMPILE_SCHEMA_VERSION, DynamicValueField, analyzeJobPurity, collectWorkflowPurityWarnings, computeContentHash, computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile };
|
|
4
|
+
export { COMPILE_SCHEMA_VERSION, computeContentHash, computeLockfileHash, detectGitRoot, generateLockFile, schemaWindowWarning, serializeLockFile };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { LockWorkflow } from '../types.js';
|
|
2
|
-
import type { JobPurityWarning } from '../lockfile/purity-diagnostics.js';
|
|
3
2
|
import type { WorkflowDecision } from '@kici-dev/engine';
|
|
4
3
|
interface DryRunOptions {
|
|
5
4
|
workflow?: string;
|
|
@@ -8,6 +7,6 @@ interface DryRunOptions {
|
|
|
8
7
|
/**
|
|
9
8
|
* Display dry-run output showing what would execute.
|
|
10
9
|
*/
|
|
11
|
-
export declare function displayDryRun(workflows: readonly LockWorkflow[], decisions: WorkflowDecision[], options: DryRunOptions
|
|
10
|
+
export declare function displayDryRun(workflows: readonly LockWorkflow[], decisions: WorkflowDecision[], options: DryRunOptions): void;
|
|
12
11
|
export {};
|
|
13
12
|
//# sourceMappingURL=dry-run.d.ts.map
|
|
@@ -3,26 +3,11 @@ import pc from "picocolors";
|
|
|
3
3
|
import { logger } from "@kici-dev/core";
|
|
4
4
|
//#region src/test-runner/dry-run.ts
|
|
5
5
|
/**
|
|
6
|
-
* Print the injected `__init__` job line for each impure dynamic value on a job,
|
|
7
|
-
* so the ~5-10s init-job cost is visible before the first run. Returns the number
|
|
8
|
-
* of lines rendered so the caller can count the jobs it actually surfaced.
|
|
9
|
-
*/
|
|
10
|
-
function renderJobInitWarnings(purityWarnings, workflowName, jobName) {
|
|
11
|
-
let rendered = 0;
|
|
12
|
-
for (const w of purityWarnings) {
|
|
13
|
-
if (w.workflowName !== workflowName || w.jobName !== jobName) continue;
|
|
14
|
-
logger.info(pc.yellow(` ⚠ __init__ job required (~5-10s): ${w.field} is not pure — ${w.reason}`));
|
|
15
|
-
rendered++;
|
|
16
|
-
}
|
|
17
|
-
return rendered;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
6
|
* Display dry-run output showing what would execute.
|
|
21
7
|
*/
|
|
22
|
-
function displayDryRun(workflows, decisions, options
|
|
8
|
+
function displayDryRun(workflows, decisions, options) {
|
|
23
9
|
logger.info(pc.bold("\n🔍 DRY RUN - No commands will be executed\n"));
|
|
24
10
|
const matchedWorkflows = decisions.filter((d) => d.matched);
|
|
25
|
-
const injectedInitJobs = /* @__PURE__ */ new Set();
|
|
26
11
|
if (matchedWorkflows.length === 0) {
|
|
27
12
|
logger.info(pc.yellow("No workflows matched the event.\n"));
|
|
28
13
|
displayDecisionSummary(decisions);
|
|
@@ -48,7 +33,6 @@ function displayDryRun(workflows, decisions, options, purityWarnings = []) {
|
|
|
48
33
|
else logger.info(pc.gray(` matrix: [dynamic]`));
|
|
49
34
|
logger.info(pc.gray(` steps (${job.steps.length}):`));
|
|
50
35
|
for (const step of job.steps) logger.info(pc.gray(` - ${step.name}`));
|
|
51
|
-
if (renderJobInitWarnings(purityWarnings, decision.workflowName, job.name) > 0) injectedInitJobs.add(`${decision.workflowName} ${job.name}`);
|
|
52
36
|
}
|
|
53
37
|
const dynamicJobs = workflow.jobs.filter((j) => j._type === "dynamic");
|
|
54
38
|
if (dynamicJobs.length > 0) {
|
|
@@ -57,7 +41,6 @@ function displayDryRun(workflows, decisions, options, purityWarnings = []) {
|
|
|
57
41
|
}
|
|
58
42
|
logger.info("");
|
|
59
43
|
}
|
|
60
|
-
if (injectedInitJobs.size > 0) logger.info(pc.yellow(`⚠ ${injectedInitJobs.size} __init__ job(s) will be injected for impure dynamic values (~5-10s each).`));
|
|
61
44
|
displayDecisionSummary(decisions);
|
|
62
45
|
logger.info(pc.green("✓ Dry run complete\n"));
|
|
63
46
|
}
|
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, RunsOnAllPredicate, OnUnreachableMode, LabelMatcher, ExecutionJobStatus, InputsDescriptorMap } from '@kici-dev/engine';
|
|
15
|
+
import type { ResourceRequest, ApproverClause, RunsOnAllPredicate, OnUnreachableMode, LabelMatcher, ExecutionJobStatus, InputsDescriptorMap, LockContentRequirement } 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 `approval` config.
|
|
@@ -28,7 +28,7 @@ export interface LockApproval {
|
|
|
28
28
|
readonly when: 'always' | 'drift';
|
|
29
29
|
}
|
|
30
30
|
/** Schema version - re-exported from engine as single source of truth */
|
|
31
|
-
export declare const SCHEMA_VERSION:
|
|
31
|
+
export declare const SCHEMA_VERSION: 35;
|
|
32
32
|
/** Lock compatibility-window floor - re-exported from engine as single source of truth */
|
|
33
33
|
export declare const BREAKING_FLOOR: 30;
|
|
34
34
|
/**
|
|
@@ -58,6 +58,8 @@ export interface LockPrTrigger {
|
|
|
58
58
|
readonly sourceBranches: readonly LockBranchPattern[];
|
|
59
59
|
readonly paths: readonly string[];
|
|
60
60
|
readonly repos?: readonly LockBranchPattern[];
|
|
61
|
+
/** Declarative static content filter over source files at the event ref (AND-ed). */
|
|
62
|
+
readonly requires?: readonly LockContentRequirement[];
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
65
|
* Push trigger in lock file.
|
|
@@ -68,6 +70,8 @@ export interface LockPushTrigger {
|
|
|
68
70
|
readonly branches: readonly LockBranchPattern[];
|
|
69
71
|
readonly paths: readonly string[];
|
|
70
72
|
readonly repos?: readonly LockBranchPattern[];
|
|
73
|
+
/** Declarative static content filter over source files at the event ref (AND-ed). */
|
|
74
|
+
readonly requires?: readonly LockContentRequirement[];
|
|
71
75
|
}
|
|
72
76
|
/**
|
|
73
77
|
* Tag trigger in lock file.
|
|
@@ -77,6 +81,8 @@ export interface LockTagTrigger {
|
|
|
77
81
|
readonly _type: 'tag';
|
|
78
82
|
readonly patterns: readonly LockBranchPattern[];
|
|
79
83
|
readonly repos?: readonly LockBranchPattern[];
|
|
84
|
+
/** Declarative static content filter over source files at the event ref (AND-ed). */
|
|
85
|
+
readonly requires?: readonly LockContentRequirement[];
|
|
80
86
|
}
|
|
81
87
|
/**
|
|
82
88
|
* Comment trigger in lock file.
|
|
@@ -389,17 +395,27 @@ export type LockStepEntry = LockStep | LockParallelStep;
|
|
|
389
395
|
/** Type guard distinguishing a parallel group from an ordinary lock step. */
|
|
390
396
|
export declare function isLockParallelStep(entry: LockStepEntry): entry is LockParallelStep;
|
|
391
397
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
398
|
+
* Serialized inline expression for a dynamic env/context/concurrencyGroup
|
|
399
|
+
* field, shaped as `{ _type: 'inline', expression: '(event) => ...' }`
|
|
400
|
+
* alongside the existing 'static' and 'dynamic' discriminants.
|
|
401
|
+
*
|
|
402
|
+
* @deprecated Schema v11 inline expressions are no longer evaluated in the
|
|
403
|
+
* orchestrator. Dynamic env/context/concurrencyGroup fields are resolved on the
|
|
404
|
+
* eval agent's init-runner. The compiler no longer emits this type; readers keep
|
|
405
|
+
* recognizing it only to defer an old lock's field to the init round. Removed at
|
|
406
|
+
* the next major (v1.0.0).
|
|
397
407
|
*/
|
|
398
408
|
export interface LockInlineValue {
|
|
399
409
|
readonly _type: 'inline';
|
|
400
410
|
readonly expression: string;
|
|
401
411
|
}
|
|
402
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* Type guard for inline expression values.
|
|
414
|
+
*
|
|
415
|
+
* @deprecated See {@link LockInlineValue}. Retained only so a reader can
|
|
416
|
+
* recognize an old lock's inline field and defer it to the eval agent's
|
|
417
|
+
* init-runner. Removed at the next major (v1.0.0).
|
|
418
|
+
*/
|
|
403
419
|
export declare function isLockInlineValue(value: unknown): value is LockInlineValue;
|
|
404
420
|
/**
|
|
405
421
|
* Static job in lock file.
|
|
@@ -589,6 +605,13 @@ export interface LockWorkflow {
|
|
|
589
605
|
readonly timeout?: number;
|
|
590
606
|
/** Normalized approval gate; when set the whole run is held before any job dispatches. */
|
|
591
607
|
readonly approval?: LockApproval;
|
|
608
|
+
/**
|
|
609
|
+
* True when the workflow declares a `filter` predicate. A bare flag, not a
|
|
610
|
+
* source reference: `LockWorkflow.source` already identifies the module and
|
|
611
|
+
* export, so the eval agent loads it and reads `.filter` off the workflow
|
|
612
|
+
* object. Mirrors the `dynamicEnv` / `dynamicConcurrencyGroup` convention.
|
|
613
|
+
*/
|
|
614
|
+
readonly hasFilter?: boolean;
|
|
592
615
|
}
|
|
593
616
|
/**
|
|
594
617
|
* Complete lock file structure.
|
|
@@ -602,6 +625,7 @@ export interface LockWorkflow {
|
|
|
602
625
|
* v8 adds runsOn polymorphic type (string | string[] | selector) and excludeLabels.
|
|
603
626
|
* v11 adds LockInlineValue type for pure function inline evaluation.
|
|
604
627
|
* v13 adds job-level and workflow-level timeout.
|
|
628
|
+
* v34 adds LockWorkflow.hasFilter (workflow-level pre-dispatch filter predicate).
|
|
605
629
|
*/
|
|
606
630
|
export interface LockFile {
|
|
607
631
|
readonly schemaVersion: typeof SCHEMA_VERSION;
|
package/dist/types.js
CHANGED
|
@@ -23,7 +23,13 @@ const BREAKING_FLOOR = BREAKING_FLOOR$1;
|
|
|
23
23
|
function isLockParallelStep(entry) {
|
|
24
24
|
return entry.kind === "parallel";
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Type guard for inline expression values.
|
|
28
|
+
*
|
|
29
|
+
* @deprecated See {@link LockInlineValue}. Retained only so a reader can
|
|
30
|
+
* recognize an old lock's inline field and defer it to the eval agent's
|
|
31
|
+
* init-runner. Removed at the next major (v1.0.0).
|
|
32
|
+
*/
|
|
27
33
|
function isLockInlineValue(value) {
|
|
28
34
|
return typeof value === "object" && value !== null && value._type === "inline";
|
|
29
35
|
}
|
|
@@ -75,6 +75,46 @@ function validateWorkflow(workflow, workflowFile) {
|
|
|
75
75
|
const matrixErrors = validateStaticMatrix(job, workflow.name, workflowFile);
|
|
76
76
|
errors.push(...matrixErrors);
|
|
77
77
|
}
|
|
78
|
+
errors.push(...validateGlobalApproval(workflow, staticJobs, workflowFile));
|
|
79
|
+
return errors;
|
|
80
|
+
}
|
|
81
|
+
/** True when any trigger carries `repos:`, which is what makes a workflow global. */
|
|
82
|
+
function isGlobalWorkflow(workflow) {
|
|
83
|
+
return (Array.isArray(workflow.on) ? workflow.on : workflow.on ? [workflow.on] : []).some((trigger) => {
|
|
84
|
+
const repos = trigger.repos;
|
|
85
|
+
return Array.isArray(repos) && repos.length > 0;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Refuse an `approval` gate on an organization-wide workflow.
|
|
90
|
+
*
|
|
91
|
+
* The approval hold is applied by the per-repository dispatch path. The global
|
|
92
|
+
* path builds its job inputs and dispatches them without ever consulting the
|
|
93
|
+
* gate, so an `approval` declared on a global workflow is **silently ignored**:
|
|
94
|
+
* the job runs immediately and the author believes a human had to release it.
|
|
95
|
+
*
|
|
96
|
+
* Failing the compile rather than warning is the right direction for a gate an
|
|
97
|
+
* author is relying on. A silently-ignored approval is not a cosmetic problem —
|
|
98
|
+
* it is a security control the workflow claims to have and does not.
|
|
99
|
+
*
|
|
100
|
+
* Only static jobs and the workflow level are reachable here; a generated job
|
|
101
|
+
* carrying `approval` is caught at dispatch, which is the only place it exists.
|
|
102
|
+
*/
|
|
103
|
+
function validateGlobalApproval(workflow, staticJobs, workflowFile) {
|
|
104
|
+
if (!isGlobalWorkflow(workflow)) return [];
|
|
105
|
+
const errors = [];
|
|
106
|
+
const suggestion = "Approval gates are supported on per-repository workflows only. Drop `approval`, or move the gated jobs into a workflow whose triggers carry no `repos:`.";
|
|
107
|
+
if (workflow.approval) errors.push(compilerError("E124", `Workflow "${workflow.name}" declares \`approval\` and is organization-wide (a trigger carries \`repos:\`). The approval would never be enforced.`, {
|
|
108
|
+
location: locationForWorkflow(workflowFile),
|
|
109
|
+
suggestion
|
|
110
|
+
}));
|
|
111
|
+
for (const job of staticJobs) {
|
|
112
|
+
if (!job.approval) continue;
|
|
113
|
+
errors.push(compilerError("E124", `Job "${job.name}" in organization-wide workflow "${workflow.name}" declares \`approval\`. The approval would never be enforced.`, {
|
|
114
|
+
location: locationForJob(job, workflowFile),
|
|
115
|
+
suggestion
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
78
118
|
return errors;
|
|
79
119
|
}
|
|
80
120
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kici-dev/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"yaml": "^2.9.0",
|
|
62
62
|
"zod": "^4.4.3",
|
|
63
63
|
"zx": "^8.8.5",
|
|
64
|
-
"@kici-dev/agent": "0.
|
|
65
|
-
"@kici-dev/
|
|
66
|
-
"@kici-dev/
|
|
67
|
-
"@kici-dev/
|
|
64
|
+
"@kici-dev/agent": "0.5.0",
|
|
65
|
+
"@kici-dev/core": "0.5.0",
|
|
66
|
+
"@kici-dev/engine": "0.5.0",
|
|
67
|
+
"@kici-dev/orchestrator": "0.5.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@kici-dev/sdk": "0.
|
|
70
|
+
"@kici-dev/sdk": "0.5.0"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
73
|
"build": "node ../../scripts/build-ts.mjs && tsgo --emitDeclarationOnly",
|