@kici-dev/sdk 0.5.0 → 0.6.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/invoke.js ADDED
@@ -0,0 +1,27 @@
1
+ import "./rolldown-runtime-ClRpJifh.js";
2
+ import { reservedEventNamePrefix } from "@kici-dev/engine";
3
+ //#region src/invoke.ts
4
+ /**
5
+ * Invoke the source repo's opt-in workflows and gate on them.
6
+ *
7
+ * Targets exactly `ctx.sourceRepo`, so a global workflow can hand control back to
8
+ * the repo whose event triggered it without any org-wide fan-out. Subscribers opt
9
+ * in with `kiciEvent({ name })`. Required by default: if no workflow subscribes,
10
+ * the gate fails unless `optional: true` is set.
11
+ */
12
+ function invokeSource(event, opts) {
13
+ if (typeof event !== "string" || event.trim().length === 0) throw new Error("invokeSource: event name must be a non-empty string");
14
+ const reservedPrefix = reservedEventNamePrefix(event);
15
+ if (reservedPrefix) throw new Error(`invokeSource: event name prefix "${reservedPrefix}" is reserved for KiCI internal events (got "${event}")`);
16
+ return Object.freeze({
17
+ _tag: "InvokeSource",
18
+ event,
19
+ scope: "source",
20
+ ...opts?.payload !== void 0 && { payload: Object.freeze({ ...opts.payload }) },
21
+ ...opts?.optional === true && { optional: true }
22
+ });
23
+ }
24
+ //#endregion
25
+ export { invokeSource };
26
+
27
+ //# sourceMappingURL=invoke.js.map
@@ -1,30 +1,34 @@
1
1
  import "./rolldown-runtime-ClRpJifh.js";
2
2
  import { isHostJobOutputs, isMatrixJobOutputs } from "./context.js";
3
3
  import { dynamicGroup } from "./dynamic-group.js";
4
- import { step } from "./step.js";
5
- import { job } from "./job.js";
6
4
  import { workflow } from "./workflow.js";
5
+ import { job } from "./job.js";
6
+ import { step } from "./step.js";
7
7
  import { describe, expectTypeOf, it } from "vitest";
8
8
  //#region src/job-outputs.test-d.ts
9
9
  describe("Job<TOutputs> — merged-steps inference", () => {
10
10
  it("infers a nested output map keyed by step name for a multi-step job", () => {
11
+ const build = step("build", { run: async () => ({ version: "1.0" }) });
12
+ const test_ = step("test", { run: async () => ({ passed: true }) });
11
13
  const j = job("ci", {
12
14
  runsOn: "kici:os:linux",
13
- steps: [step("build", { run: async () => ({ version: "1.0" }) }), step("test", { run: async () => ({ passed: true }) })]
15
+ steps: [build, test_]
14
16
  });
15
17
  expectTypeOf(j.result.build.version).toEqualTypeOf();
16
18
  expectTypeOf(j.result.test.passed).toEqualTypeOf();
17
19
  });
18
20
  it("rejects reading an unknown output field of a declared step", () => {
21
+ const build = step("build", { run: async () => ({ version: "1.0" }) });
19
22
  job("ci", {
20
23
  runsOn: "kici:os:linux",
21
- steps: [step("build", { run: async () => ({ version: "1.0" }) })]
24
+ steps: [build]
22
25
  }).result.build.nope;
23
26
  });
24
27
  it("rejects reading an undeclared step name", () => {
28
+ const build = step("build", { run: async () => ({ version: "1.0" }) });
25
29
  job("ci", {
26
30
  runsOn: "kici:os:linux",
27
- steps: [step("build", { run: async () => ({ version: "1.0" }) })]
31
+ steps: [build]
28
32
  }).result.other;
29
33
  });
30
34
  it("infers a flat output shape for the run: shorthand", () => {
@@ -36,22 +40,26 @@ describe("Job<TOutputs> — merged-steps inference", () => {
36
40
  flat.result.nope;
37
41
  });
38
42
  it("honours an explicit output-type override for a dynamically-shaped job", () => {
39
- expectTypeOf(job("dyn", {
43
+ const dyn = job("dyn", {
40
44
  runsOn: "kici:os:linux",
41
45
  steps: [step(async () => ({}))]
42
- }).result.n).toEqualTypeOf();
46
+ });
47
+ expectTypeOf(dyn.result.n).toEqualTypeOf();
43
48
  });
44
49
  it("omits id-less steps and falls back to the loose shape", () => {
45
- expectTypeOf(job("idless", {
50
+ const j = job("idless", {
46
51
  runsOn: "kici:os:linux",
47
52
  steps: [step(async () => ({ x: 1 }))]
48
- }).result.anything).toEqualTypeOf();
53
+ });
54
+ expectTypeOf(j.result.anything).toEqualTypeOf();
49
55
  });
50
56
  it("keeps a typed job assignable to the bare Job type", () => {
51
- expectTypeOf(job("ci", {
57
+ const build = step("build", { run: async () => ({ version: "1.0" }) });
58
+ const anyJob = job("ci", {
52
59
  runsOn: "kici:os:linux",
53
- steps: [step("build", { run: async () => ({ version: "1.0" }) })]
54
- })).toEqualTypeOf();
60
+ steps: [build]
61
+ });
62
+ expectTypeOf(anyJob).toEqualTypeOf();
55
63
  });
56
64
  });
57
65
  describe("Job<void> — void run-shorthand assignability", () => {
@@ -75,10 +83,11 @@ describe("Job<void> — void run-shorthand assignability", () => {
75
83
  workflow("wf", { jobs: [setup] });
76
84
  });
77
85
  it("resolves result to never for a void run-shorthand job", () => {
78
- expectTypeOf(job("setup", {
86
+ const setup = job("setup", {
79
87
  runsOn: "kici:os:linux",
80
88
  run: async () => {}
81
- }).result).toEqualTypeOf();
89
+ });
90
+ expectTypeOf(setup.result).toEqualTypeOf();
82
91
  });
83
92
  it("leaves a value-returning run job untouched (regression guard)", () => {
84
93
  const flat = job("flat", {
@@ -90,9 +99,11 @@ describe("Job<void> — void run-shorthand assignability", () => {
90
99
  });
91
100
  });
92
101
  describe("typed ctx.needs from job references", () => {
102
+ const build = step("build", { run: async () => ({ version: "1.0" }) });
103
+ const test_ = step("test", { run: async () => ({ passed: true }) });
93
104
  const ci = job("ci", {
94
105
  runsOn: "kici:os:linux",
95
- steps: [step("build", { run: async () => ({ version: "1.0" }) }), step("test", { run: async () => ({ passed: true }) })]
106
+ steps: [build, test_]
96
107
  });
97
108
  it("threads a referenced job’s inferred outputs into ctx.needs", () => {
98
109
  job("deploy", {
@@ -135,13 +146,15 @@ describe("typed ctx.needs from job references", () => {
135
146
  });
136
147
  });
137
148
  describe("envelope-generic jobOutputs for typed refs", () => {
149
+ const build = step("build", { run: async () => ({ version: "1.0" }) });
138
150
  const ci = job("ci", {
139
151
  runsOn: "kici:os:linux",
140
- steps: [step("build", { run: async () => ({ version: "1.0" }) })]
152
+ steps: [build]
141
153
  });
142
154
  const ctx = null;
143
155
  it("types a job-ref jobOutputs as the plain | matrix | host union of the inferred shape", () => {
144
- expectTypeOf(ctx.jobOutputs(ci)).toEqualTypeOf();
156
+ const out = ctx.jobOutputs(ci);
157
+ expectTypeOf(out).toEqualTypeOf();
145
158
  });
146
159
  it("narrows to the typed matrix envelope via isMatrixJobOutputs", () => {
147
160
  const out = ctx.jobOutputs(ci);
@@ -155,7 +168,8 @@ describe("envelope-generic jobOutputs for typed refs", () => {
155
168
  if (isHostJobOutputs(out)) expectTypeOf(out.byHost).toEqualTypeOf();
156
169
  });
157
170
  it("keeps a string-name ref on the loose union", () => {
158
- expectTypeOf(ctx.jobOutputs({ name: "ci" })).toEqualTypeOf();
171
+ const out = ctx.jobOutputs({ name: "ci" });
172
+ expectTypeOf(out).toEqualTypeOf();
159
173
  });
160
174
  });
161
175
  //#endregion
package/dist/job.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import "./rolldown-runtime-ClRpJifh.js";
2
2
  import { createJobOutputProxy } from "./outputs.js";
3
+ import { assertSecretName } from "./git-types.js";
3
4
  import { isKnownCapability } from "@kici-dev/engine";
4
5
  import { randomUUID } from "node:crypto";
5
6
  //#region src/job.ts
@@ -38,6 +39,7 @@ function validateSandbox(sandbox, jobName) {
38
39
  function job(nameOrOptions, maybeOptions) {
39
40
  const name = typeof nameOrOptions === "string" ? nameOrOptions : randomUUID();
40
41
  const options = typeof nameOrOptions === "string" ? maybeOptions : nameOrOptions;
42
+ if (options.invoke && ((options.steps?.length ?? 0) > 0 || "run" in options)) throw new Error(`job('${name}'): 'invoke' is mutually exclusive with 'steps'/'run'`);
41
43
  let steps = options.steps ?? [];
42
44
  if (options.run) {
43
45
  if (options.steps && options.steps.length > 0) throw new Error("job() cannot have both \"run\" and \"steps\" -- use one or the other");
@@ -49,7 +51,10 @@ function job(nameOrOptions, maybeOptions) {
49
51
  const concurrencyGroup = options.concurrencyGroup ?? (typeof firstContext === "string" ? firstContext : void 0);
50
52
  if (options.context !== void 0 && options.contexts !== void 0) throw new Error(`job('${name}'): context and contexts are mutually exclusive — use one`);
51
53
  if (options.runsOn !== void 0 && options.runsOnAll !== void 0) throw new Error(`job('${name}'): runsOn and runsOnAll are mutually exclusive`);
52
- if (options.runsOn === void 0 && options.runsOnAll === void 0) throw new Error(`job('${name}'): one of runsOn or runsOnAll is required`);
54
+ if (options.container !== void 0) validateContainerImageSource(name, options.container);
55
+ if (options.container && typeof options.container === "object" && options.container.auth) validateContainerAuth(name, options.container.auth);
56
+ if (options.gitCredentials) validateGitCredentials(name, options.gitCredentials);
57
+ if (options.invoke === void 0 && options.runsOn === void 0 && options.runsOnAll === void 0) throw new Error(`job('${name}'): one of runsOn or runsOnAll is required`);
53
58
  if (options.onUnreachable !== void 0 && options.runsOnAll === void 0) console.warn(`[kici] job('${name}'): onUnreachable is ignored without runsOnAll`);
54
59
  if (options.includeUninitialized !== void 0 && options.runsOnAll === void 0) console.warn(`[kici] job('${name}'): includeUninitialized is ignored without runsOnAll`);
55
60
  return {
@@ -61,6 +66,7 @@ function job(nameOrOptions, maybeOptions) {
61
66
  ...options.includeUninitialized !== void 0 && { includeUninitialized: options.includeUninitialized },
62
67
  ...options.maxParallel !== void 0 && { maxParallel: options.maxParallel },
63
68
  ...options.failFast !== void 0 && { failFast: options.failFast },
69
+ ...options.invoke !== void 0 && { invoke: options.invoke },
64
70
  steps,
65
71
  needs: options.needs,
66
72
  rules: options.rules,
@@ -69,6 +75,7 @@ function job(nameOrOptions, maybeOptions) {
69
75
  include: options.include,
70
76
  exclude: options.exclude,
71
77
  checkout: options.checkout,
78
+ ...options.gitCredentials && { gitCredentials: options.gitCredentials },
72
79
  container: options.container,
73
80
  ...options.sandbox !== void 0 && { sandbox: options.sandbox },
74
81
  context: options.context,
@@ -90,6 +97,91 @@ function job(nameOrOptions, maybeOptions) {
90
97
  result: createJobOutputProxy(name)
91
98
  };
92
99
  }
100
+ /**
101
+ * Validate a job's named git credentials at definition time.
102
+ *
103
+ * Every `*Secret` field is a qualified `<context>:<secret-name>` reference into
104
+ * the secrets backend — the same form `workflow()` enforces for
105
+ * `registries[].tokenSecret`. Catching a pasted credential here matters because
106
+ * the alternative is committing it to a git repository and discovering it later
107
+ * as a confusing auth failure.
108
+ */
109
+ /**
110
+ * Validate a job's container registry auth at definition time.
111
+ *
112
+ * Same contract as `gitCredentials`: every `*Secret` field is a qualified
113
+ * `<context>:<secret-name>` reference, and a pasted credential is refused
114
+ * rather than committed to a git repository. The `*Value` half is material
115
+ * supplied at run time and is deliberately not checked for shape.
116
+ */
117
+ /**
118
+ * Is `p` a repo-relative path that stays inside the repository?
119
+ *
120
+ * Rejects an absolute path and any path whose `..` segments walk out of the
121
+ * tree. Deliberately string-only: the workflow is authored on one machine and
122
+ * the path is resolved on another, so there is no filesystem here to consult.
123
+ */
124
+ function isInsideRepo(p) {
125
+ if (p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)) return false;
126
+ let depth = 0;
127
+ for (const part of p.split(/[\\/]+/)) {
128
+ if (part === "" || part === ".") continue;
129
+ if (part === "..") {
130
+ depth -= 1;
131
+ if (depth < 0) return false;
132
+ continue;
133
+ }
134
+ depth += 1;
135
+ }
136
+ return true;
137
+ }
138
+ /**
139
+ * A container names exactly one image source: a finalized `image`, or a
140
+ * `dockerfile` the agent builds before the job runs.
141
+ *
142
+ * Refused at definition time rather than at dispatch, because the author is the
143
+ * only one who can fix it and the workflow file is where they are looking. The
144
+ * agent re-checks the paths against the real workdir — a lock file is repo
145
+ * content and is not trusted — but that failure reaches a run log, not an
146
+ * editor.
147
+ */
148
+ function validateContainerImageSource(name, container) {
149
+ if (typeof container === "string") return;
150
+ const hasImage = typeof container.image === "string" && container.image.length > 0;
151
+ const hasDockerfile = typeof container.dockerfile === "string" && container.dockerfile.length > 0;
152
+ if (hasImage === hasDockerfile) throw new Error(`job('${name}'): exactly one of container.image or container.dockerfile is required`);
153
+ if (!hasDockerfile) {
154
+ for (const field of [
155
+ "context",
156
+ "target",
157
+ "args"
158
+ ]) if (container[field] !== void 0) throw new Error(`job('${name}'): container.${field} applies only to container.dockerfile`);
159
+ return;
160
+ }
161
+ if (!isInsideRepo(container.dockerfile)) throw new Error(`job('${name}'): container.dockerfile must stay inside the repository (got: ${container.dockerfile})`);
162
+ if (container.context !== void 0 && !isInsideRepo(container.context)) throw new Error(`job('${name}'): container.context must stay inside the repository (got: ${container.context})`);
163
+ if (container.auth && typeof container.auth.registry !== "string") throw new Error(`job('${name}'): container.auth.registry is required with container.dockerfile — the base image is named inside the Dockerfile, so the registry cannot be derived`);
164
+ }
165
+ function validateContainerAuth(name, auth) {
166
+ const bag = auth;
167
+ for (const [field, value] of Object.entries(bag)) {
168
+ if (!field.endsWith("Secret") || typeof value !== "string") continue;
169
+ assertSecretName(value, field, "container registry credential");
170
+ const idx = value.indexOf(":");
171
+ if (idx <= 0 || idx >= value.length - 1 || value.slice(idx + 1).includes(":")) throw new Error(`job('${name}'): container.auth.${field} must use qualified <context>:<secret-name> syntax (got: ${value})`);
172
+ }
173
+ }
174
+ function validateGitCredentials(name, credentials) {
175
+ for (const [alias, ref] of Object.entries(credentials)) {
176
+ const bag = ref;
177
+ for (const [field, value] of Object.entries(bag)) {
178
+ if (!field.endsWith("Secret") || typeof value !== "string") continue;
179
+ assertSecretName(value, `${alias}.${field}`);
180
+ const idx = value.indexOf(":");
181
+ if (idx <= 0 || idx >= value.length - 1 || value.slice(idx + 1).includes(":")) throw new Error(`job('${name}'): gitCredentials.${alias}.${field} must use qualified <context>:<secret-name> syntax (got: ${value})`);
182
+ }
183
+ }
184
+ }
93
185
  //#endregion
94
186
  export { job };
95
187
 
@@ -14,6 +14,23 @@ export interface UpstreamSnapshot {
14
14
  jobs: Record<string, Record<string, unknown>>;
15
15
  groups: Record<string, string[]>;
16
16
  statuses?: Record<string, ExecutionJobStatus>;
17
+ /**
18
+ * Maps an invoke-gate job name to its ordered per-run results, one entry per
19
+ * run the gate triggered. Present only when the upstream is an invoke gate.
20
+ */
21
+ invokeResults?: Record<string, InvokeResult[]>;
22
+ }
23
+ /**
24
+ * One invoked-run result exposed on an invoke-gate upstream's
25
+ * `ctx.needs[gate].result` array — one entry per run the gate triggered.
26
+ * `outputs` carries the invoked run's non-secret declared outputs.
27
+ */
28
+ export interface InvokeResult {
29
+ readonly repo: string;
30
+ readonly workflow: string;
31
+ readonly runId: string;
32
+ readonly status: string;
33
+ readonly outputs: Readonly<Record<string, unknown>>;
17
34
  }
18
35
  /**
19
36
  * A single-job need entry: the upstream's outputs proxy plus its terminal
@@ -31,8 +48,20 @@ export interface GroupNeedEntry<T = Record<string, unknown>> {
31
48
  result: OutputProxy<T>;
32
49
  status: ExecutionJobStatus;
33
50
  }
34
- /** A single-job need exposes `{ result, status }`; a group need exposes an ordered array. */
35
- export type NeedEntry<T = Record<string, unknown>> = SingleNeedEntry<T> | GroupNeedEntry[];
51
+ /**
52
+ * An invoke-gate need entry: `ctx.needs['<gate>'].result` is an ordered array of
53
+ * {@link InvokeResult}, one per run the gate triggered.
54
+ */
55
+ export interface InvokeNeedEntry {
56
+ result: readonly InvokeResult[];
57
+ }
58
+ /**
59
+ * A single-job need exposes `{ result, status }`; a group need exposes an
60
+ * ordered array of `{ name, result, status }`; an invoke-gate need exposes
61
+ * `{ result }` — an ordered array of {@link InvokeResult} on `.result`, one per
62
+ * triggered run.
63
+ */
64
+ export type NeedEntry<T = Record<string, unknown>> = SingleNeedEntry<T> | GroupNeedEntry[] | InvokeNeedEntry;
36
65
  /** The resolved `ctx.needs` map keyed by job name or group name. */
37
66
  export type NeedsContext = Record<string, NeedEntry>;
38
67
  /**
@@ -34,7 +34,9 @@ function buildNeedsContext(snapshot, declaredNeeds) {
34
34
  const out = {};
35
35
  for (const need of declaredNeeds) {
36
36
  const { kind, key } = needKey(need);
37
- if (kind === "group") out[key] = (snapshot.groups[key] ?? []).map((name) => ({
37
+ const invokeResults = snapshot.invokeResults?.[key];
38
+ if (kind === "job" && invokeResults !== void 0) out[key] = { result: invokeResults };
39
+ else if (kind === "group") out[key] = (snapshot.groups[key] ?? []).map((name) => ({
38
40
  name,
39
41
  result: createSnapshotOutputProxy(name, snapshot.jobs[name]),
40
42
  status: statusFor(snapshot, name)
@@ -218,13 +218,15 @@ function createTestStepContext(options = {}) {
218
218
  get: () => Promise.resolve(null)
219
219
  },
220
220
  oidc: { token: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.oidc.token() is not available in a test step context")) },
221
+ git: { github: { getToken: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.git.github.getToken() is not available in a test step context")) } },
221
222
  host: { requestReboot: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.host.requestReboot() is not available in a test step context")) },
222
223
  bootstrap: {
223
224
  ensureInitRunner: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.ensureInitRunner() is not available in a test step context")),
224
225
  preBootSend: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.preBootSend() is not available in a test step context")),
225
226
  agentVersionStatus: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.agentVersionStatus() is not available in a test step context")),
226
227
  restageAgent: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.bootstrap.restageAgent() is not available in a test step context"))
227
- }
228
+ },
229
+ scaler: { claimAgentCredentials: () => Promise.reject(/* @__PURE__ */ new Error("ctx.kici.scaler.claimAgentCredentials() is not available in a test step context")) }
228
230
  },
229
231
  cache: {
230
232
  restore: async () => ({ hit: false }),
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { z } from 'zod';
2
2
  import type { $ as Shell } from 'zx';
3
3
  import type { RetryBackoff } from '@kici-dev/core';
4
+ import type { GitCredentialMap, ContainerRegistryAuth } from './git-types.js';
4
5
  import type { ResourceRequest, RunsOnAllInput, OnUnreachableMode, NeedsWhen, ExecutionJobStatus } from '@kici-dev/engine';
5
6
  import type { StepContext, Logger, RepoInfo } from './context.js';
6
7
  import type { FilterFn } from './filter.js';
@@ -406,10 +407,43 @@ export { DYNAMIC_JOB_NEEDS_TAG };
406
407
  * When set, all steps run inside the specified container.
407
408
  */
408
409
  export interface ContainerConfig {
409
- /** Docker image name (e.g., 'node:20-alpine') */
410
- image: string;
410
+ /**
411
+ * Docker image name (e.g., 'node:20-alpine'). Mutually exclusive with
412
+ * `dockerfile`; exactly one of the two is required.
413
+ */
414
+ image?: string;
415
+ /**
416
+ * Repo-relative path to a Dockerfile the agent builds before the job runs.
417
+ * Mutually exclusive with `image`.
418
+ *
419
+ * The build runs on the agent host, NOT inside the job's hardened sandbox.
420
+ * See the container-jobs documentation for the trust model and the
421
+ * untrusted-ref rule.
422
+ */
423
+ dockerfile?: string;
424
+ /**
425
+ * Repo-relative build context. Defaults to the repository root.
426
+ * Meaningful only with `dockerfile`.
427
+ */
428
+ context?: string;
429
+ /** Build stage to stop at (`--target`). Meaningful only with `dockerfile`. */
430
+ target?: string;
431
+ /**
432
+ * Build arguments. Meaningful only with `dockerfile`.
433
+ *
434
+ * Plain strings only, deliberately: a build argument is recorded in the
435
+ * image's history, so a secret placed here is readable by anyone who can
436
+ * inspect the image. Pass a secret to a step instead.
437
+ */
438
+ args?: Record<string, string>;
411
439
  /** Additional environment variables for the container */
412
440
  env?: Record<string, string>;
441
+ /**
442
+ * Private-registry credentials. With `image` they pull that image; with
443
+ * `dockerfile` they pull the Dockerfile's own `FROM` base — and must name
444
+ * the registry, because there is no image reference to derive it from.
445
+ */
446
+ auth?: ContainerRegistryAuth;
413
447
  }
414
448
  /**
415
449
  * Per-job sandbox escape hatch for container-sandbox jobs (a job with a
@@ -574,6 +608,13 @@ export interface Job<TOutputs = Record<string, unknown>, TName extends string =
574
608
  readonly maxParallel?: number;
575
609
  /** Halt the fan-out on first child failure, skipping the remainder. Default `false`. */
576
610
  readonly failFast?: boolean;
611
+ /**
612
+ * Invoke the source repo's opt-in workflows and gate on them. Mutually
613
+ * exclusive with `steps` / `run` — an invoke job never runs step code on an
614
+ * agent; it fans out into one proxy node per triggered run. Built by
615
+ * `invokeSource()`.
616
+ */
617
+ readonly invoke?: import('./invoke.js').InvokeConfig;
577
618
  readonly steps: readonly StepInput[];
578
619
  readonly needs?: ReadonlyArray<Job | string | DynamicGroupRef | {
579
620
  name: string;
@@ -594,6 +635,17 @@ export interface Job<TOutputs = Record<string, unknown>, TName extends string =
594
635
  readonly exclude?: MatrixExclude[];
595
636
  /** When false, agent skips git clone (default: true). Useful for deploy/notify jobs. */
596
637
  readonly checkout?: boolean;
638
+ /**
639
+ * Named git credentials available to every step in this job.
640
+ *
641
+ * Values are **secret names**, resolved from the secrets backend — never
642
+ * secret material. `default` is used whenever a call names no credential;
643
+ * any other key is referenced by name, e.g.
644
+ * `kici.git.clone({ repo, credential: 'forge' })`.
645
+ *
646
+ * Omit entirely and the source credential applies, which is all a read needs.
647
+ */
648
+ readonly gitCredentials?: GitCredentialMap;
597
649
  /** Docker image for job execution. All steps run inside the container. */
598
650
  readonly container?: string | ContainerConfig;
599
651
  /** Per-job sandbox escape hatch (container jobs only); granted within the operator allow-list. */
@@ -709,6 +761,13 @@ export interface JobOptions {
709
761
  * of sibling outcomes). Applies to both matrix and `runsOnAll` fan-out.
710
762
  */
711
763
  failFast?: boolean;
764
+ /**
765
+ * Invoke the source repo's opt-in workflows and gate on them. Mutually
766
+ * exclusive with `steps` / `run` — an invoke job never runs step code on an
767
+ * agent; it fans out into one proxy node per triggered run. Built by
768
+ * `invokeSource()`.
769
+ */
770
+ invoke?: import('./invoke.js').InvokeConfig;
712
771
  /**
713
772
  * Steps to execute in this job. Accepts Step objects and bare async functions.
714
773
  * Mutually exclusive with `run`.
@@ -749,6 +808,17 @@ export interface JobOptions {
749
808
  exclude?: MatrixExclude[];
750
809
  /** When false, agent skips git clone (default: true). Useful for deploy/notify jobs. */
751
810
  checkout?: boolean;
811
+ /**
812
+ * Named git credentials available to every step in this job.
813
+ *
814
+ * Values are **secret names**, resolved from the secrets backend — never
815
+ * secret material. `default` is used whenever a call names no credential;
816
+ * any other key is referenced by name, e.g.
817
+ * `kici.git.clone({ repo, credential: 'forge' })`.
818
+ *
819
+ * Omit entirely and the source credential applies, which is all a read needs.
820
+ */
821
+ gitCredentials?: GitCredentialMap;
752
822
  /**
753
823
  * Docker image for job execution.
754
824
  * Simple string form for image name, object form for additional config.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/sdk",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript SDK for defining KiCI workflows. Import into `.kici/workflows/*.ts` to declare workflows, jobs, steps, triggers, rules, and matrix configurations.",
5
5
  "keywords": [
6
6
  "ci",
@@ -52,10 +52,11 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "micromatch": "^4.0.8",
55
+ "yaml": "^2.9.0",
55
56
  "zod": "^4.4.3",
56
57
  "zx": "^8.8.5",
57
- "@kici-dev/core": "0.5.0",
58
- "@kici-dev/engine": "0.5.0"
58
+ "@kici-dev/core": "0.6.0",
59
+ "@kici-dev/engine": "0.6.0"
59
60
  },
60
61
  "devDependencies": {
61
62
  "@types/micromatch": "^4.0.10"