@kici-dev/sdk 0.4.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.
@@ -0,0 +1,32 @@
1
+ import "./rolldown-runtime-ClRpJifh.js";
2
+ //#region src/git-types.ts
3
+ /**
4
+ * Resolve a call site's credential.
5
+ *
6
+ * Order: an explicit per-call value wins; otherwise `default` from the job map;
7
+ * otherwise undefined, which means the source credential — all a read needs.
8
+ *
9
+ * An unknown name THROWS rather than falling back to `default`: silently using
10
+ * a different credential than the author named is precisely the confusion this
11
+ * map exists to remove.
12
+ */
13
+ function resolveCredential(perCall, map) {
14
+ if (perCall && typeof perCall === "object") return perCall;
15
+ if (typeof perCall === "string") {
16
+ const found = map?.[perCall];
17
+ if (!found) throw new Error(`Unknown git credential '${perCall}'. Declare it in the job's gitCredentials map. Known: ${Object.keys(map ?? {}).join(", ") || "(none)"}`);
18
+ return found;
19
+ }
20
+ return map?.default;
21
+ }
22
+ /**
23
+ * Every credential field names a secret. Catch the easy, silent mistake of
24
+ * pasting the credential itself — which would commit it to a git repository.
25
+ */
26
+ function assertSecretName(value, field, subject = "git credential") {
27
+ if (value.startsWith("-----BEGIN") || /^gh[pousr]_/.test(value) || value.startsWith("github_pat_")) throw new Error(`${subject} '${field}' looks like the credential itself, not the name of a secret holding it. Store it with \`kici-admin secret set\` and name it here.`);
28
+ }
29
+ //#endregion
30
+ export { assertSecretName, resolveCredential };
31
+
32
+ //# sourceMappingURL=git-types.js.map
@@ -83,7 +83,7 @@ function idempotentStep(name, opts) {
83
83
  * lockfile change.
84
84
  */
85
85
  function checkStep(name, options) {
86
- return step(name, {
86
+ const checkOptions = {
87
87
  check: options.check,
88
88
  summarize: options.summarize,
89
89
  run: (ctx, drift) => options.apply(ctx, drift),
@@ -94,7 +94,8 @@ function checkStep(name, options) {
94
94
  ...options.retry !== void 0 && { retry: options.retry },
95
95
  ...options.cache !== void 0 && { cache: options.cache },
96
96
  ...options.rules !== void 0 && { rules: options.rules }
97
- });
97
+ };
98
+ return step(name, checkOptions);
98
99
  }
99
100
  //#endregion
100
101
  export { checkStep, idempotent, idempotentStep };
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export { job } from './job.js';
3
3
  export { workflow } from './workflow.js';
4
4
  export { parallel, isParallelGroup, flattenStepInputs } from './parallel.js';
5
5
  export type { ParallelGroup, ParallelOptions } from './parallel.js';
6
+ export { invokeSource } from './invoke.js';
7
+ export type { InvokeConfig } from './invoke.js';
6
8
  export { normalizeApproval } from './approval.js';
7
9
  export type { ApprovalConfig, ApprovalWhen, ApproverClause, NormalizedApproval, } from './approval.js';
8
10
  export { pr, push, tag, comment, review, reviewComment, release, dispatch, create, delete as delete, status, workflowRun, fork, star, watch, webhook, kiciEvent, workflowComplete, workflowsFailedBatch, jobComplete, genericWebhook, schedule, lifecycle, defineDispatchInputs, } from './triggers/index.js';
@@ -21,7 +23,7 @@ export type { SourceLocation, OutputProxy, Step, StepOptions, StepOptionsBase, S
21
23
  export { isDynamicJobFn, dynamicJob, getDynamicJobGroup, getDynamicJobNeeds, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, } from './types.js';
22
24
  export type { TaggedDynamicJobFn, ResultAwareDynamicJobConfig, ResultAwareDynamicJobFn, DynamicJobNeed, NeedsWhen, NeedsWhenInput, } from './types.js';
23
25
  export { buildNeedsContext } from './needs-context.js';
24
- export type { UpstreamSnapshot, NeedsContext, NeedEntry, GroupNeedEntry } from './needs-context.js';
26
+ export type { UpstreamSnapshot, NeedsContext, NeedEntry, GroupNeedEntry, InvokeNeedEntry, InvokeResult, } from './needs-context.js';
25
27
  export { CacheSpecSchema, normalizeCacheSpecs } from './cache-types.js';
26
28
  export type { CacheSpec, CacheInput } from './cache-types.js';
27
29
  export type { CacheRestoreResult, CacheApi } from './cache-types.js';
@@ -33,6 +35,9 @@ export { dynamicGroup, isDynamicGroupRef, DYNAMIC_GROUP_TAG } from './dynamic-gr
33
35
  export type { DynamicGroupRef } from './dynamic-group.js';
34
36
  export type { StepContext, Logger, WorkflowInfo, JobInfo, AgentInfo, MatrixJobOutputs, HostJobOutputs, RepoInfo, StepSecretsTyped, KnownSecretKeys, } from './context.js';
35
37
  export { isMatrixJobOutputs, isHostJobOutputs } from './context.js';
38
+ export type { FilterContext, FilterFn } from './filter.js';
39
+ export { createFilterContext } from './filter-context.js';
40
+ export type { CreateFilterContextInput } from './filter-context.js';
36
41
  export type { FanoutPosition } from './fanout-context.js';
37
42
  export { buildKiciApi } from './api-types.js';
38
43
  export type { KiciApi, KiciApiTransport, InfrastructureApi, InfrastructureListResult, InventoryApi, HostApi, HostInventoryEntry, InventorySelector, } from './api-types.js';
@@ -58,4 +63,9 @@ export type { WaitForHostAliveOptions, RestartHostOptions } from './host-restart
58
63
  export { agentVersionConverge } from './fleet/agent-version-converge.js';
59
64
  export type { AgentVersionConvergeOptions, AgentVersionDrift, } from './fleet/agent-version-converge.js';
60
65
  export { z } from 'zod';
66
+ export { SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleUpPayload, ScalerScaleDownPayload, } from '@kici-dev/engine';
67
+ export { buildAgentCloudInit } from './agent-cloud-init.js';
68
+ export type { CloudInitCredentials, ClaimCodeCredentials, AgentCloudInitCredentials, AgentDeliveryMode, UserDataEncoding, CloudInitWriteFile, AgentCloudInitOptions, } from './agent-cloud-init.js';
69
+ export { assertSecretName } from './git-types.js';
70
+ export type { ForgeName, GitHubPermissions, WriteOptions, GitGrant, Sourced, ContainerRegistryAuth, GitCredentialRef, GitCredentialMap, RepoHandle, GitTokenResult, GitHubApi, GitApi, } from './git-types.js';
61
71
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import "./rolldown-runtime-ClRpJifh.js";
2
+ import { buildAgentCloudInit } from "./agent-cloud-init.js";
2
3
  import { buildKiciApi } from "./api-types.js";
3
4
  import { normalizeApproval } from "./approval.js";
4
5
  import { ARTIFACT_NAME_MAX_LENGTH, ArtifactNameSchema } from "./artifacts-types.js";
@@ -6,15 +7,19 @@ import { CacheSpecSchema, normalizeCacheSpecs } from "./cache-types.js";
6
7
  import { isHostJobOutputs, isMatrixJobOutputs } from "./context.js";
7
8
  import { DYNAMIC_GROUP_TAG, dynamicGroup, isDynamicGroupRef } from "./dynamic-group.js";
8
9
  import { SecretNotFoundError } from "./errors.js";
9
- import { fixture } from "./fixture.js";
10
+ import { ChangedFilesUnavailableError } from "./rules/changed-files.js";
11
+ import { createFilterContext } from "./filter-context.js";
12
+ import { workflow } from "./workflow.js";
10
13
  import { createJobOutputProxy, createSnapshotOutputProxy, createStepOutputProxy, getJobOutputsMap, getStepOutputsMap, getStepRefMap, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "./outputs.js";
14
+ import { assertSecretName } from "./git-types.js";
15
+ import { job } from "./job.js";
11
16
  import { step } from "./step.js";
17
+ import { fixture } from "./fixture.js";
12
18
  import { WaitForTimeoutError, waitFor, waitForStep } from "./wait-for.js";
13
19
  import { restartHost, waitForHostAlive } from "./host-restart.js";
14
20
  import { checkStep, idempotent, idempotentStep } from "./idempotent.js";
15
- import { job } from "./job.js";
16
- import { workflow } from "./workflow.js";
17
21
  import { flattenStepInputs, isParallelGroup, parallel } from "./parallel.js";
22
+ import { invokeSource } from "./invoke.js";
18
23
  import { pr } from "./triggers/pr.js";
19
24
  import { push } from "./triggers/push.js";
20
25
  import { tag } from "./triggers/tag.js";
@@ -42,7 +47,7 @@ import { defineDispatchInputs } from "./triggers/dispatch-inputs.js";
42
47
  import "./triggers/index.js";
43
48
  import { afterStep, beforeStep, cleanup, onCancel, onFailure, onSuccess } from "./hooks/index.js";
44
49
  import { onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, rule, skip } from "./rules/rule.js";
45
- import { ChangedFilesUnavailableError, createRuleContext } from "./rules/context.js";
50
+ import { createRuleContext } from "./rules/context.js";
46
51
  import { evaluateRules } from "./rules/evaluator.js";
47
52
  import { isEventType } from "./events/event-payloads.js";
48
53
  import "./rules/index.js";
@@ -58,5 +63,6 @@ import "./matrix/index.js";
58
63
  import { defineEvent, isEventDefinition } from "./events/define-event.js";
59
64
  import "./events/index.js";
60
65
  import { agentVersionConverge } from "./fleet/agent-version-converge.js";
66
+ import { SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleDownPayload, ScalerScaleUpPayload } from "@kici-dev/engine";
61
67
  import { z } from "zod";
62
- export { ARTIFACT_NAME_MAX_LENGTH, ArtifactNameSchema, CacheSpecSchema, ChangedFilesUnavailableError, DYNAMIC_GROUP_TAG, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, SecretNotFoundError, WaitForTimeoutError, afterStep, agentVersionConverge, applyIncludeExclude, beforeStep, buildKiciApi, buildNeedsContext, checkStep, cleanup, comment, create, createJobOutputProxy, createRuleContext, createSnapshotOutputProxy, createStepOutputProxy, createStepSecrets, defineDispatchInputs, defineEvent, del as delete, dispatch, dynamicGroup, dynamicJob, evaluateRules, expandMatrix, fixture, flattenStepInputs, fork, genericWebhook, getDynamicJobGroup, getDynamicJobNeeds, getJobOutputsMap, getStepOutputsMap, getStepRefMap, idempotent, idempotentStep, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isEventDefinition, isEventType, isHostJobOutputs, isMatrixJobOutputs, isParallelGroup, isStaticArray, isStaticObject, job, jobComplete, kiciEvent, lifecycle, normalizeApproval, normalizeCacheSpecs, onCancel, onFailure, onSuccess, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, parallel, pr, provenanceSubjectIsPath, push, release, resolveJobOutputs, resolveStepOutputs, restartHost, review, reviewComment, rule, schedule, setJobOutputsMap, setStepOutputsMap, setStepRefMap, skip, star, status, step, tag, validateDag, waitFor, waitForHostAlive, waitForStep, watch, webhook, workflow, workflowComplete, workflowRun, workflowsFailedBatch, z };
68
+ export { ARTIFACT_NAME_MAX_LENGTH, ArtifactNameSchema, CacheSpecSchema, ChangedFilesUnavailableError, DYNAMIC_GROUP_TAG, DYNAMIC_JOB_GROUP_TAG, DYNAMIC_JOB_NEEDS_TAG, SCALER_EVENT_NAMES, ScaleDownReason, ScalerScaleDownPayload, ScalerScaleUpPayload, SecretNotFoundError, WaitForTimeoutError, afterStep, agentVersionConverge, applyIncludeExclude, assertSecretName, beforeStep, buildAgentCloudInit, buildKiciApi, buildNeedsContext, checkStep, cleanup, comment, create, createFilterContext, createJobOutputProxy, createRuleContext, createSnapshotOutputProxy, createStepOutputProxy, createStepSecrets, defineDispatchInputs, defineEvent, del as delete, dispatch, dynamicGroup, dynamicJob, evaluateRules, expandMatrix, fixture, flattenStepInputs, fork, genericWebhook, getDynamicJobGroup, getDynamicJobNeeds, getJobOutputsMap, getStepOutputsMap, getStepRefMap, idempotent, idempotentStep, invokeSource, isDynamicFunction, isDynamicGroupRef, isDynamicJobFn, isEventDefinition, isEventType, isHostJobOutputs, isMatrixJobOutputs, isParallelGroup, isStaticArray, isStaticObject, job, jobComplete, kiciEvent, lifecycle, normalizeApproval, normalizeCacheSpecs, onCancel, onFailure, onSuccess, onlyOnFanoutIndex, onlyOnFirstHost, onlyOnLastHost, parallel, pr, provenanceSubjectIsPath, push, release, resolveJobOutputs, resolveStepOutputs, restartHost, review, reviewComment, rule, schedule, setJobOutputsMap, setStepOutputsMap, setStepRefMap, skip, star, status, step, tag, validateDag, waitFor, waitForHostAlive, waitForStep, watch, webhook, workflow, workflowComplete, workflowRun, workflowsFailedBatch, z };
@@ -0,0 +1,29 @@
1
+ /** Config produced by {@link invokeSource}: emit a kici event at the source repo and gate on the runs it triggers. */
2
+ export interface InvokeConfig {
3
+ readonly _tag: 'InvokeSource';
4
+ /** The kici event name to emit. Subscribers opt in with `kiciEvent({ name })`. */
5
+ readonly event: string;
6
+ /** Target scope. `'source'` targets exactly `ctx.sourceRepo` (the only v1 scope). */
7
+ readonly scope: 'source';
8
+ /** Optional event payload delivered to subscribers. */
9
+ readonly payload?: Readonly<Record<string, unknown>>;
10
+ /**
11
+ * When true, a zero-subscriber emit succeeds immediately (the repo may opt out).
12
+ * When false/unset (the default), a zero-subscriber emit FAILS the gate — a repo
13
+ * that never wired up its tests must not silently pass the org gate.
14
+ */
15
+ readonly optional?: boolean;
16
+ }
17
+ /**
18
+ * Invoke the source repo's opt-in workflows and gate on them.
19
+ *
20
+ * Targets exactly `ctx.sourceRepo`, so a global workflow can hand control back to
21
+ * the repo whose event triggered it without any org-wide fan-out. Subscribers opt
22
+ * in with `kiciEvent({ name })`. Required by default: if no workflow subscribes,
23
+ * the gate fails unless `optional: true` is set.
24
+ */
25
+ export declare function invokeSource(event: string, opts?: {
26
+ payload?: Record<string, unknown>;
27
+ optional?: boolean;
28
+ }): InvokeConfig;
29
+ //# sourceMappingURL=invoke.d.ts.map
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)
@@ -0,0 +1,37 @@
1
+ import type { ChangedFilesStatus } from '@kici-dev/engine';
2
+ /**
3
+ * Thrown when a rule or a workflow `filter` reads `ctx.changedFiles` but the
4
+ * diff is not available (`changedFilesStatus !== 'fetched'`). `evaluateRules`
5
+ * re-throws this rather than folding it into a `passed=false` skip, so the job
6
+ * fails loudly instead of silently mis-evaluating a path-based gate; the global
7
+ * eval round reports the candidate indeterminate for the same reason.
8
+ */
9
+ export declare class ChangedFilesUnavailableError extends Error {
10
+ readonly changedFilesStatus: ChangedFilesStatus;
11
+ readonly eventType?: string;
12
+ constructor(status: ChangedFilesStatus, eventType?: string);
13
+ }
14
+ /** Read an event's `type` discriminant, when it carries a string one. */
15
+ export declare function eventTypeOf(event: unknown): string | undefined;
16
+ /**
17
+ * Install the throwing `changedFiles` accessor shared by `RuleContext` and
18
+ * `FilterContext`.
19
+ *
20
+ * Both contexts contract `changedFiles` to **throw** when the diff is
21
+ * unavailable rather than read as an empty list: a path-based gate that
22
+ * silently sees no changes suppresses work invisibly, and for a `filter` it
23
+ * suppresses every job the workflow declares. On the organization-wide path
24
+ * that leaves no run row at all; on the same-repo path it leaves a `success`
25
+ * run carrying only the workflow's `__init__*` evaluation jobs.
26
+ *
27
+ * The behaviour lives here, in one `Object.defineProperty`, precisely because a
28
+ * plain `{ changedFiles: [] }` property satisfies both context types with no
29
+ * error anywhere: a second construction site could violate the contract and
30
+ * still typecheck. Every builder of either context calls this.
31
+ */
32
+ export declare function defineChangedFilesGetter(target: object, input: {
33
+ files: string[];
34
+ status: ChangedFilesStatus;
35
+ eventType?: string;
36
+ }): void;
37
+ //# sourceMappingURL=changed-files.d.ts.map
@@ -0,0 +1,54 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ //#region src/rules/changed-files.ts
3
+ /**
4
+ * Thrown when a rule or a workflow `filter` reads `ctx.changedFiles` but the
5
+ * diff is not available (`changedFilesStatus !== 'fetched'`). `evaluateRules`
6
+ * re-throws this rather than folding it into a `passed=false` skip, so the job
7
+ * fails loudly instead of silently mis-evaluating a path-based gate; the global
8
+ * eval round reports the candidate indeterminate for the same reason.
9
+ */
10
+ var ChangedFilesUnavailableError = class extends Error {
11
+ changedFilesStatus;
12
+ eventType;
13
+ constructor(status, eventType) {
14
+ super(`ctx.changedFiles is not available (status: ${status}` + (eventType ? `, event: ${eventType}` : "") + "). Changed files are only defined for push / pull_request events with a computable diff. Guard with ctx.changedFilesStatus before accessing, e.g. `if (ctx.changedFilesStatus !== 'fetched') return true`.");
15
+ this.name = "ChangedFilesUnavailableError";
16
+ this.changedFilesStatus = status;
17
+ this.eventType = eventType;
18
+ }
19
+ };
20
+ /** Read an event's `type` discriminant, when it carries a string one. */
21
+ function eventTypeOf(event) {
22
+ const type = event?.type;
23
+ return typeof type === "string" ? type : void 0;
24
+ }
25
+ /**
26
+ * Install the throwing `changedFiles` accessor shared by `RuleContext` and
27
+ * `FilterContext`.
28
+ *
29
+ * Both contexts contract `changedFiles` to **throw** when the diff is
30
+ * unavailable rather than read as an empty list: a path-based gate that
31
+ * silently sees no changes suppresses work invisibly, and for a `filter` it
32
+ * suppresses every job the workflow declares. On the organization-wide path
33
+ * that leaves no run row at all; on the same-repo path it leaves a `success`
34
+ * run carrying only the workflow's `__init__*` evaluation jobs.
35
+ *
36
+ * The behaviour lives here, in one `Object.defineProperty`, precisely because a
37
+ * plain `{ changedFiles: [] }` property satisfies both context types with no
38
+ * error anywhere: a second construction site could violate the contract and
39
+ * still typecheck. Every builder of either context calls this.
40
+ */
41
+ function defineChangedFilesGetter(target, input) {
42
+ Object.defineProperty(target, "changedFiles", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ get() {
46
+ if (input.status !== "fetched") throw new ChangedFilesUnavailableError(input.status, input.eventType);
47
+ return input.files;
48
+ }
49
+ });
50
+ }
51
+ //#endregion
52
+ export { ChangedFilesUnavailableError, defineChangedFilesGetter, eventTypeOf };
53
+
54
+ //# sourceMappingURL=changed-files.js.map
@@ -1,18 +1,9 @@
1
1
  import type { ChangedFilesStatus } from '@kici-dev/engine';
2
2
  import type { EventPayload } from '../events/event-payloads.js';
3
3
  import type { FanoutPosition } from '../fanout-context.js';
4
+ import type { RepoInfo } from '../context.js';
4
5
  import type { RuleContext } from './types.js';
5
- /**
6
- * Thrown when a rule reads `ctx.changedFiles` but the diff is not available
7
- * (`changedFilesStatus !== 'fetched'`). `evaluateRules` re-throws this rather
8
- * than folding it into a `passed=false` skip, so the job fails loudly instead
9
- * of silently mis-evaluating a path-based gate.
10
- */
11
- export declare class ChangedFilesUnavailableError extends Error {
12
- readonly changedFilesStatus: ChangedFilesStatus;
13
- readonly eventType?: string;
14
- constructor(status: ChangedFilesStatus, eventType?: string);
15
- }
6
+ export { ChangedFilesUnavailableError } from './changed-files.js';
16
7
  /** Input for {@link createRuleContext}. */
17
8
  export interface CreateRuleContextInput {
18
9
  event: EventPayload | Record<string, unknown>;
@@ -22,6 +13,16 @@ export interface CreateRuleContextInput {
22
13
  env?: Record<string, string | undefined>;
23
14
  dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>;
24
15
  fanout?: FanoutPosition;
16
+ /**
17
+ * The repo whose event triggered this run. Supplied for a global workflow.
18
+ *
19
+ * `path` is an absolute path into the evaluating machine's work directory and
20
+ * is NOT stable across evaluations — read the tree through it, never compare
21
+ * it. `ref` / `sha` are optional on `RepoInfo`; do not assume either is set.
22
+ */
23
+ sourceRepo?: RepoInfo;
24
+ /** The repo that registered the workflow. Identical to `sourceRepo` outside a global workflow. */
25
+ workflowRepo?: RepoInfo;
25
26
  }
26
27
  /**
27
28
  * Build a RuleContext. `changedFiles` is exposed as a getter: it returns the