@kici-dev/orchestrator 0.1.21 → 0.1.22
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/agent/dispatcher.d.ts +44 -0
- package/dist/agent/host-roster-reaper.d.ts +2 -1
- package/dist/agent/host-roster.d.ts +18 -0
- package/dist/approvals/step-approval-bridge.d.ts +5 -0
- package/dist/cli/commands/source-manifest.d.ts +8 -0
- package/dist/cli/service/compose.d.ts +13 -0
- package/dist/cli/service/deploy-env.d.ts +31 -0
- package/dist/cli.js +515 -209
- package/dist/config.d.ts +6 -0
- package/dist/dashboard/needs-edges.d.ts +4 -3
- package/dist/db/migrations/047_needs_run_on.d.ts +4 -0
- package/dist/db/migrations/048_host_reboot_pending.d.ts +19 -0
- package/dist/db/migrations/049_held_runs_payload.d.ts +14 -0
- package/dist/db/migrations/050_sources_slug.d.ts +17 -0
- package/dist/db/types.d.ts +27 -4
- package/dist/deployment/deployment-identity.d.ts +9 -0
- package/dist/entry-helpers.d.ts +7 -0
- package/dist/environments/held-runs.d.ts +7 -1
- package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +77 -0
- package/dist/index.js +21 -2
- package/dist/orchestrator-core.d.ts +13 -1
- package/dist/pipeline/decorating-secret-resolver.d.ts +32 -0
- package/dist/pipeline/dispatch-matched-workflow.d.ts +75 -3
- package/dist/pipeline/install-secrets-resolver.d.ts +2 -2
- package/dist/pipeline/needs-scheduler.d.ts +22 -13
- package/dist/pipeline/processor.d.ts +2 -2
- package/dist/pipeline/test-pipeline.d.ts +31 -59
- package/dist/providers/github/manifest.d.ts +25 -0
- package/dist/routes/admin-sources.d.ts +7 -0
- package/dist/secrets/secret-resolver.d.ts +16 -1
- package/dist/server.js +2728 -1725
- package/dist/sources/source-store.d.ts +4 -0
- package/dist/sources/source-validator.d.ts +2 -0
- package/dist/stale-detector/reboot-deadline-sweep.d.ts +29 -0
- package/dist/standalone.js +1307 -677
- package/dist/ws/agent-handler.d.ts +11 -6
- package/dist/ws/dashboard-fleet-handler.d.ts +33 -0
- package/dist/ws/dashboard-fleet-write-handler.d.ts +60 -0
- package/dist/ws/fleet-runs-on-all.d.ts +16 -0
- package/dist/ws/platform-client.d.ts +13 -1
- package/dist/ws/test-relay-handlers.d.ts +4 -2
- package/installer-image-digests.json +3 -3
- package/package.json +4 -4
- package/sbom.spdx.json +50 -50
package/dist/config.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ declare const configSchema: z.ZodObject<{
|
|
|
68
68
|
staleDetectorScanIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
69
69
|
staleDetectorThresholdMultiplier: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
70
70
|
jobHeartbeatIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
71
|
+
githubAppNameRefreshIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
71
72
|
secretKey: z.ZodOptional<z.ZodString>;
|
|
72
73
|
secretKeyFile: z.ZodOptional<z.ZodString>;
|
|
73
74
|
secretKeyOld: z.ZodOptional<z.ZodString>;
|
|
@@ -84,6 +85,8 @@ declare const configSchema: z.ZodObject<{
|
|
|
84
85
|
agentTokenTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
85
86
|
rosterGraceMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
86
87
|
rosterTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
88
|
+
hostRebootDeadlineMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
89
|
+
orchestratorHostAgentId: z.ZodOptional<z.ZodString>;
|
|
87
90
|
maxFanoutHosts: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
88
91
|
eventRouterMaxChainDepth: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
89
92
|
eventRouterRateLimitPerWorkflowPerMinute: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
@@ -197,11 +200,13 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
|
|
|
197
200
|
staleDetectorScanIntervalMs: number;
|
|
198
201
|
staleDetectorThresholdMultiplier: number;
|
|
199
202
|
jobHeartbeatIntervalMs: number;
|
|
203
|
+
githubAppNameRefreshIntervalMs: number;
|
|
200
204
|
pgCustomerSecrets: boolean;
|
|
201
205
|
agentAuth: "token" | "none";
|
|
202
206
|
agentTokenTtlMs: number;
|
|
203
207
|
rosterGraceMs: number;
|
|
204
208
|
rosterTtlMs: number;
|
|
209
|
+
hostRebootDeadlineMs: number;
|
|
205
210
|
maxFanoutHosts: number;
|
|
206
211
|
eventRouterMaxChainDepth: number;
|
|
207
212
|
eventRouterRateLimitPerWorkflowPerMinute: number;
|
|
@@ -265,6 +270,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
|
|
|
265
270
|
secretKeyOld?: string | undefined;
|
|
266
271
|
secretKeyFileOld?: string | undefined;
|
|
267
272
|
bootstrapAdminToken?: string | undefined;
|
|
273
|
+
orchestratorHostAgentId?: string | undefined;
|
|
268
274
|
testEventFailFirstN?: string | undefined;
|
|
269
275
|
otelExporterOtlpEndpoint?: string | undefined;
|
|
270
276
|
clusterName?: string | undefined;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ExecutionJobStatus } from '@kici-dev/engine';
|
|
2
2
|
/** A single resolved upstream dependency edge for a job. */
|
|
3
3
|
export interface JobNeedEdge {
|
|
4
4
|
upstreamName: string;
|
|
5
|
-
|
|
5
|
+
/** Upstream terminal statuses that satisfy this edge (the run-on set). */
|
|
6
|
+
runOn: ExecutionJobStatus[];
|
|
6
7
|
}
|
|
7
8
|
/** Group raw execution_job_needs rows by downstream job_name. */
|
|
8
9
|
export declare function groupNeedsByJobName(rows: ReadonlyArray<{
|
|
9
10
|
job_name: string;
|
|
10
11
|
upstream_name: string;
|
|
11
|
-
|
|
12
|
+
run_on: string;
|
|
12
13
|
}>): Map<string, JobNeedEdge[]>;
|
|
13
14
|
//# sourceMappingURL=needs-edges.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Kysely } from 'kysely';
|
|
2
|
+
/**
|
|
3
|
+
* Add `host_roster.reboot_pending_until timestamptz NULL` — the persisted
|
|
4
|
+
* reboot-pending flag for workflow-level host restart.
|
|
5
|
+
*
|
|
6
|
+
* Set when an agent's `restartHost()` step calls `host.requestReboot` (so it
|
|
7
|
+
* survives an orchestrator restart during the host's reboot window). While the
|
|
8
|
+
* value is in the future it (1) makes the agent's imminent disconnect an
|
|
9
|
+
* expected reboot rather than a recovery-fail, (2) gates the pinned-drain off
|
|
10
|
+
* so the post-restart job is not dispatched into the about-to-reboot box, and
|
|
11
|
+
* (3) clears on the next reconnect (down-then-up), releasing the held job. NULL
|
|
12
|
+
* = no reboot pending.
|
|
13
|
+
*
|
|
14
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
|
|
15
|
+
* preserved.
|
|
16
|
+
*/
|
|
17
|
+
export declare function up(db: Kysely<unknown>): Promise<void>;
|
|
18
|
+
export declare function down(db: Kysely<unknown>): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=048_host_reboot_pending.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Kysely } from 'kysely';
|
|
2
|
+
/**
|
|
3
|
+
* Add `held_runs.payload jsonb NULL` — the drift payload captured when a
|
|
4
|
+
* `when: 'drift'` step-approval gate fires. Holds `{ summaryMarkdown, drift }`:
|
|
5
|
+
* the author's `summarize(drift)` rendering plus the structured drift blob, so
|
|
6
|
+
* the dashboard approval queue and the CLI render the computed diff the
|
|
7
|
+
* operator approves. NULL for every non-drift hold.
|
|
8
|
+
*
|
|
9
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
|
|
10
|
+
* preserved.
|
|
11
|
+
*/
|
|
12
|
+
export declare function up(db: Kysely<unknown>): Promise<void>;
|
|
13
|
+
export declare function down(db: Kysely<unknown>): Promise<void>;
|
|
14
|
+
//# sourceMappingURL=049_held_runs_payload.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Kysely } from 'kysely';
|
|
2
|
+
/**
|
|
3
|
+
* Add `sources.slug TEXT NULL` — the GitHub App slug (the URL-safe identifier
|
|
4
|
+
* GitHub assigns, e.g. `my-kici-app`).
|
|
5
|
+
*
|
|
6
|
+
* For GitHub-App sources GitHub is the source of truth for both the display
|
|
7
|
+
* `name` and the `slug`: both are captured at creation and kept fresh by the
|
|
8
|
+
* daily refresher + `kici-admin source refresh`. NULL when the identity fetch
|
|
9
|
+
* hasn't populated it yet (manual `--app-id` flow whose initial fetch failed,
|
|
10
|
+
* or a row created before the rollout).
|
|
11
|
+
*
|
|
12
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
|
|
13
|
+
* preserved.
|
|
14
|
+
*/
|
|
15
|
+
export declare function up(db: Kysely<unknown>): Promise<void>;
|
|
16
|
+
export declare function down(db: Kysely<unknown>): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=050_sources_slug.d.ts.map
|
package/dist/db/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely';
|
|
2
|
-
import type { ApprovalRequirement, ApproverClause, InitFailure } from '@kici-dev/engine';
|
|
2
|
+
import type { ApprovalRequirement, ApproverClause, InitFailure, StepApprovalPayload } from '@kici-dev/engine';
|
|
3
3
|
/**
|
|
4
4
|
* PostgreSQL-only database types.
|
|
5
5
|
* Column names use snake_case matching the actual database column names.
|
|
@@ -486,8 +486,13 @@ export interface ExecutionJobNeedsTable {
|
|
|
486
486
|
job_name: string;
|
|
487
487
|
/** Upstream job name (the job that must complete first) */
|
|
488
488
|
upstream_name: string;
|
|
489
|
-
/**
|
|
490
|
-
|
|
489
|
+
/**
|
|
490
|
+
* Per-edge run-on status-set: a JSON-encoded array of upstream terminal
|
|
491
|
+
* statuses (ExecutionJobStatus[]) that satisfy the edge. Default
|
|
492
|
+
* `'["success"]'`. The downstream dispatches when the upstream's terminal
|
|
493
|
+
* status is a member of this set.
|
|
494
|
+
*/
|
|
495
|
+
run_on: Generated<string>;
|
|
491
496
|
}
|
|
492
497
|
export type ExecutionJobNeeds = Selectable<ExecutionJobNeedsTable>;
|
|
493
498
|
export type NewExecutionJobNeeds = Insertable<ExecutionJobNeedsTable>;
|
|
@@ -695,7 +700,7 @@ export interface HeldRunsTable {
|
|
|
695
700
|
step_index: number | null;
|
|
696
701
|
/**
|
|
697
702
|
* What created the hold: 'environment' (mandatory env policy) | 'explicit'
|
|
698
|
-
* (SDK `
|
|
703
|
+
* (SDK `approval`). Engine `TriggerSource`.
|
|
699
704
|
*/
|
|
700
705
|
trigger_source: Generated<string>;
|
|
701
706
|
/**
|
|
@@ -703,6 +708,11 @@ export interface HeldRunsTable {
|
|
|
703
708
|
* must satisfy. Null for legacy rows that predate the approval model.
|
|
704
709
|
*/
|
|
705
710
|
approval_requirement: ColumnType<ApprovalRequirement | null, ApprovalRequirement | string | null | undefined, ApprovalRequirement | string | null>;
|
|
711
|
+
/**
|
|
712
|
+
* Drift payload `{ summaryMarkdown, drift }` captured when a `when: 'drift'`
|
|
713
|
+
* step-approval gate fires. Null for every non-drift hold.
|
|
714
|
+
*/
|
|
715
|
+
payload: ColumnType<StepApprovalPayload | null, StepApprovalPayload | string | null | undefined, StepApprovalPayload | string | null>;
|
|
706
716
|
}
|
|
707
717
|
export type HeldRun = Selectable<HeldRunsTable>;
|
|
708
718
|
export type NewHeldRun = Insertable<HeldRunsTable>;
|
|
@@ -1215,6 +1225,12 @@ export interface SourcesTable {
|
|
|
1215
1225
|
routing_key: string;
|
|
1216
1226
|
/** JSONB config (non-sensitive, e.g. { appId: '12345' }) */
|
|
1217
1227
|
config: string;
|
|
1228
|
+
/**
|
|
1229
|
+
* GitHub App slug (the URL-safe identifier GitHub assigns, e.g.
|
|
1230
|
+
* `my-kici-app`). NULL until the GitHub identity fetch populates it. GitHub
|
|
1231
|
+
* is the source of truth for both `name` and `slug` on GitHub-App sources.
|
|
1232
|
+
*/
|
|
1233
|
+
slug: string | null;
|
|
1218
1234
|
/** Customer/org identifier for secret and environment scoping */
|
|
1219
1235
|
customer_id: Generated<string>;
|
|
1220
1236
|
/** When this source was created */
|
|
@@ -1616,6 +1632,13 @@ export interface HostRosterTable {
|
|
|
1616
1632
|
*/
|
|
1617
1633
|
host_properties: ColumnType<Record<string, string | number | boolean>, Record<string, string | number | boolean> | string | undefined, Record<string, string | number | boolean> | string>;
|
|
1618
1634
|
last_seen: ColumnType<Date, Date | string | undefined, Date | string>;
|
|
1635
|
+
/**
|
|
1636
|
+
* Reboot-pending flag for workflow-level host restart. When set to a future
|
|
1637
|
+
* timestamp, the host's imminent disconnect is an expected reboot (not a
|
|
1638
|
+
* recovery-fail) and its pinned post-restart job is held until the host
|
|
1639
|
+
* reconnects (down-then-up). NULL = no reboot pending.
|
|
1640
|
+
*/
|
|
1641
|
+
reboot_pending_until: ColumnType<Date | null, Date | string | null, Date | string | null>;
|
|
1619
1642
|
created_at: Generated<Date>;
|
|
1620
1643
|
updated_at: ColumnType<Date, Date | string | undefined, Date | string>;
|
|
1621
1644
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type DeploymentIdentity } from '@kici-dev/engine';
|
|
2
|
+
/**
|
|
3
|
+
* Read the orchestrator's deployment shape from the env the installer injects
|
|
4
|
+
* (`KICI_DEPLOY_MODE` / `KICI_DEPLOY_CONTAINER` / `KICI_DEPLOY_CONTAINER_RUNTIME`).
|
|
5
|
+
* Hand-run / dev orchestrators carry no `KICI_DEPLOY_*` env and report `unknown`.
|
|
6
|
+
* Container fields are kept only for the `compose` mode.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readDeploymentIdentity(env?: NodeJS.ProcessEnv): DeploymentIdentity;
|
|
9
|
+
//# sourceMappingURL=deployment-identity.d.ts.map
|
package/dist/entry-helpers.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export interface ProviderSource {
|
|
|
19
19
|
routingKey: string;
|
|
20
20
|
name: string;
|
|
21
21
|
subtype: SourceSubtype;
|
|
22
|
+
/**
|
|
23
|
+
* GitHub App slug (URL-safe identifier GitHub assigns). Only set for
|
|
24
|
+
* GitHub-App sources, where it propagates orchestrator → Platform → dashboard
|
|
25
|
+
* alongside the display `name`. Undefined for generic / universal-git / local
|
|
26
|
+
* sources, and for a GitHub source whose identity fetch hasn't run yet.
|
|
27
|
+
*/
|
|
28
|
+
slug?: string;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* Map a generic_webhook_sources `provider_type` (plus optional `git_config`
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages the lifecycle: pending -> approved/rejected/expired.
|
|
5
5
|
*/
|
|
6
6
|
import { type Kysely } from 'kysely';
|
|
7
|
-
import { type ApprovalRequirement, type ApproverClause, ApprovalDecision, HoldScope, TriggerSource } from '@kici-dev/engine';
|
|
7
|
+
import { type ApprovalRequirement, type ApproverClause, type StepApprovalPayload, ApprovalDecision, HoldScope, TriggerSource } from '@kici-dev/engine';
|
|
8
8
|
import type { Database, HeldRun, HeldRunApproval } from '../db/types.js';
|
|
9
9
|
/** Status values for held runs (held_runs table). */
|
|
10
10
|
export declare enum HeldRunStatus {
|
|
@@ -51,6 +51,12 @@ export interface CreateHoldData {
|
|
|
51
51
|
* the automated release sweeps can find their rows.
|
|
52
52
|
*/
|
|
53
53
|
holdType?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Drift payload `{ summaryMarkdown, drift }` captured for a `when: 'drift'`
|
|
56
|
+
* step gate; persisted to `held_runs.payload` and surfaced in the dashboard
|
|
57
|
+
* approval queue + the CLI. Omit for non-drift holds.
|
|
58
|
+
*/
|
|
59
|
+
payload?: StepApprovalPayload;
|
|
54
60
|
}
|
|
55
61
|
/** A single decision to record against a hold. */
|
|
56
62
|
export interface RecordDecisionData {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily refresh of every GitHub source's display name + slug from GitHub.
|
|
3
|
+
*
|
|
4
|
+
* GitHub is the source of truth for a GitHub-App source's display name and
|
|
5
|
+
* slug. They are captured at creation, but an operator can rename the App in
|
|
6
|
+
* the GitHub UI afterwards — this periodic task re-fetches `GET /app` for each
|
|
7
|
+
* GitHub source and, when the name or slug drifted, writes the new values to
|
|
8
|
+
* the `sources` row. The `sources_change` DB trigger then fans the change out
|
|
9
|
+
* (SourceManager reload → `platformClient.updateSources()` → re-register), so
|
|
10
|
+
* the Platform `webhook_sources` row and the dashboard Sources tab pick up the
|
|
11
|
+
* new name/slug without any extra plumbing here.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle mirrors `StaleRunDetector`: `start()` runs an immediate refresh
|
|
14
|
+
* (so a rename made while the orchestrator was down propagates on next boot)
|
|
15
|
+
* then a `setInterval`; `stop()` clears it. Per-source errors are logged and
|
|
16
|
+
* never abort the loop.
|
|
17
|
+
*/
|
|
18
|
+
/** The slice of {@link SourceStore} this module needs. */
|
|
19
|
+
export interface RefreshableSourceStore {
|
|
20
|
+
listSources(): Promise<Array<{
|
|
21
|
+
routing_key: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
name: string;
|
|
24
|
+
slug: string | null;
|
|
25
|
+
}>>;
|
|
26
|
+
getSourceWithSecrets(routingKey: string): Promise<{
|
|
27
|
+
provider: string;
|
|
28
|
+
config: string;
|
|
29
|
+
privateKey: string;
|
|
30
|
+
} | null>;
|
|
31
|
+
updateSource(routingKey: string, updates: {
|
|
32
|
+
name?: string;
|
|
33
|
+
slug?: string | null;
|
|
34
|
+
}): Promise<unknown>;
|
|
35
|
+
}
|
|
36
|
+
/** Fetch a GitHub App's authoritative identity. Matches `fetchGithubAppIdentity`. */
|
|
37
|
+
export type FetchGithubAppIdentity = (creds: {
|
|
38
|
+
appId: string;
|
|
39
|
+
privateKey: string;
|
|
40
|
+
}) => Promise<{
|
|
41
|
+
name: string;
|
|
42
|
+
slug: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Outcome of a single source refresh. */
|
|
45
|
+
export interface RefreshResult {
|
|
46
|
+
routingKey: string;
|
|
47
|
+
changed: boolean;
|
|
48
|
+
oldName: string;
|
|
49
|
+
newName: string;
|
|
50
|
+
oldSlug: string | null;
|
|
51
|
+
newSlug: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Re-fetch one GitHub source's identity from GitHub and persist it when the
|
|
55
|
+
* name or slug drifted. Shared by the daily task and `kici-admin source
|
|
56
|
+
* refresh`. Throws for a missing or non-GitHub routing key.
|
|
57
|
+
*/
|
|
58
|
+
export declare function refreshGithubSourceIdentity(sourceStore: RefreshableSourceStore, routingKey: string, fetchIdentity: FetchGithubAppIdentity): Promise<RefreshResult>;
|
|
59
|
+
export interface GithubAppNameRefresherDeps {
|
|
60
|
+
sourceStore: RefreshableSourceStore;
|
|
61
|
+
fetchIdentity: FetchGithubAppIdentity;
|
|
62
|
+
/** Refresh cadence in ms. Default cluster value: 24h (`config.githubAppNameRefreshIntervalMs`). */
|
|
63
|
+
scanIntervalMs: number;
|
|
64
|
+
}
|
|
65
|
+
export declare class GithubAppNameRefresher {
|
|
66
|
+
private readonly sourceStore;
|
|
67
|
+
private readonly fetchIdentity;
|
|
68
|
+
private readonly scanIntervalMs;
|
|
69
|
+
private interval;
|
|
70
|
+
constructor(deps: GithubAppNameRefresherDeps);
|
|
71
|
+
/** Immediate refresh then periodic scans. */
|
|
72
|
+
start(): Promise<void>;
|
|
73
|
+
stop(): void;
|
|
74
|
+
/** Refresh every GitHub source once. Per-source failures are isolated. */
|
|
75
|
+
refresh(): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=github-app-name-refresher.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1892,6 +1892,23 @@ function createInstallationOctokit(config, installationId) {
|
|
|
1892
1892
|
* registration and returns it on the conversion response, so both GitHub and
|
|
1893
1893
|
* the Platform end up sharing the same secret with zero operator effort.
|
|
1894
1894
|
*/
|
|
1895
|
+
/**
|
|
1896
|
+
* Validate a self-hosted webhook URL supplied via `source add github
|
|
1897
|
+
* --webhook-url`. Must be a well-formed absolute `https://` URL. Returns the URL
|
|
1898
|
+
* verbatim on success; throws a clear error otherwise. The validated URL is
|
|
1899
|
+
* baked into `manifest.hook_attributes.url` as-is — KiCI adds no ingress and
|
|
1900
|
+
* does not receive events at it; the operator owns delivery.
|
|
1901
|
+
*/
|
|
1902
|
+
function validateWebhookUrl(value) {
|
|
1903
|
+
let url;
|
|
1904
|
+
try {
|
|
1905
|
+
url = new URL(value);
|
|
1906
|
+
} catch {
|
|
1907
|
+
throw new Error(`--webhook-url must be a valid absolute URL: ${value}`);
|
|
1908
|
+
}
|
|
1909
|
+
if (url.protocol !== "https:") throw new Error(`--webhook-url must be an https:// URL (got ${url.protocol}//…)`);
|
|
1910
|
+
return value;
|
|
1911
|
+
}
|
|
1895
1912
|
function buildGithubAppManifest(input) {
|
|
1896
1913
|
return {
|
|
1897
1914
|
name: input.name,
|
|
@@ -1930,6 +1947,7 @@ async function convertManifestCode(code, deps = {}) {
|
|
|
1930
1947
|
return {
|
|
1931
1948
|
appId: String(d.id),
|
|
1932
1949
|
slug: d.slug,
|
|
1950
|
+
name: d.name,
|
|
1933
1951
|
privateKey: d.pem,
|
|
1934
1952
|
webhookSecret: d.webhook_secret,
|
|
1935
1953
|
clientId: d.client_id,
|
|
@@ -2178,7 +2196,8 @@ async function storeAndRegister(opts, client, deps, creds) {
|
|
|
2178
2196
|
try {
|
|
2179
2197
|
return await client.post("/api/v1/admin/sources", {
|
|
2180
2198
|
provider: "github",
|
|
2181
|
-
name:
|
|
2199
|
+
name: creds.name,
|
|
2200
|
+
slug: creds.slug,
|
|
2182
2201
|
appId: creds.appId,
|
|
2183
2202
|
privateKey: creds.privateKey,
|
|
2184
2203
|
webhookSecret: creds.webhookSecret
|
|
@@ -2195,7 +2214,7 @@ async function storeAndRegister(opts, client, deps, creds) {
|
|
|
2195
2214
|
}
|
|
2196
2215
|
}
|
|
2197
2216
|
async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps) {
|
|
2198
|
-
const webhookUrl = await resolveWebhookUrl(client);
|
|
2217
|
+
const webhookUrl = opts.webhookUrl ? validateWebhookUrl(opts.webhookUrl) : await resolveWebhookUrl(client);
|
|
2199
2218
|
const state = randomBytes(16).toString("hex");
|
|
2200
2219
|
const createUrl = manifestCreateUrl(opts.githubOrg);
|
|
2201
2220
|
const loopback = await deps.startLoopback({
|
|
@@ -23,7 +23,7 @@ import { DedupCache } from './webhook/dedup.js';
|
|
|
23
23
|
import { ObserverRegistry } from './ws/observer-registry.js';
|
|
24
24
|
import { AgentMetricsAggregator } from './metrics/agent-metrics-aggregator.js';
|
|
25
25
|
import { SourceLocationStore } from './app.js';
|
|
26
|
-
import { type LabelMatcher, type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
|
|
26
|
+
import { ExecutionJobStatus, type LabelMatcher, type PeerHeartbeat, type PeerLogsCollectRequest, type PeerToPeerMessage } from '@kici-dev/engine';
|
|
27
27
|
import { ScalerManager } from './scaler/index.js';
|
|
28
28
|
import type { ScalerConfig } from './scaler/index.js';
|
|
29
29
|
import type { CacheStorage } from './storage/index.js';
|
|
@@ -253,6 +253,17 @@ export declare function buildUpstreamOutputsByBase(baseNames: string[], rows: Ar
|
|
|
253
253
|
variant_label?: string | null;
|
|
254
254
|
status?: string | null;
|
|
255
255
|
}>): Record<string, Record<string, unknown>> | undefined;
|
|
256
|
+
/**
|
|
257
|
+
* Build the downstream `upstreamJobStatuses` map keyed by each upstream job
|
|
258
|
+
* row's name. A single non-fanned upstream is keyed by its base name; a
|
|
259
|
+
* fanned-out upstream contributes one entry per expanded child name (`base
|
|
260
|
+
* (child)`). The agent uses this to stamp `ctx.needs.<job>.status` (single) and
|
|
261
|
+
* the per-child status of group / matrix / host-fanout entries.
|
|
262
|
+
*/
|
|
263
|
+
export declare function buildUpstreamStatusesByBase(rows: Array<{
|
|
264
|
+
job_name: string;
|
|
265
|
+
status?: string | null;
|
|
266
|
+
}>): Record<string, ExecutionJobStatus> | undefined;
|
|
256
267
|
/**
|
|
257
268
|
* Fold a `runsOnAll` upstream's host children into the `byHost` envelope
|
|
258
269
|
* `{ byHost: { '<host>': outputs }, summary: { succeededHosts, failedHosts, outputs } }`.
|
|
@@ -302,6 +313,7 @@ export declare function buildMatrixOutputsEnvelope(baseName: string, children: A
|
|
|
302
313
|
export declare function mergeUpstreamOutputs(db: Kysely<Database>, runId: string, jobName: string, needs: unknown, dispatchSecrets: Record<string, string> | undefined, secretKey: string): Promise<{
|
|
303
314
|
mergedSecrets: Record<string, string> | undefined;
|
|
304
315
|
upstreamJobOutputs: Record<string, Record<string, unknown>> | undefined;
|
|
316
|
+
upstreamJobStatuses: Record<string, ExecutionJobStatus> | undefined;
|
|
305
317
|
}>;
|
|
306
318
|
export declare function bootstrapOrchestrator(config: AppConfig, hooks: OrchestratorHooks, options?: {
|
|
307
319
|
otelSdk?: {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-secret overlay for `kici run` test dispatch.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the orchestrator's environment `SecretResolver` and overlays the
|
|
5
|
+
* developer's CLI-uploaded local secrets on top of the env-resolved secrets,
|
|
6
|
+
* with CLI winning on collision. Passed to the shared dispatch core via
|
|
7
|
+
* `ProcessingDeps.secretResolver`, so the core's secret-resolution path is
|
|
8
|
+
* unchanged and oblivious to "test secrets".
|
|
9
|
+
*
|
|
10
|
+
* Precedence: env-resolved secrets → CLI context for the requested environment
|
|
11
|
+
* → CLI flat. The CLI flat overlay is applied last so a CLI flat key always
|
|
12
|
+
* wins.
|
|
13
|
+
*/
|
|
14
|
+
import type { SecretResolverApi, ResolvedSecretMeta } from '../secrets/secret-resolver.js';
|
|
15
|
+
/** Decrypted CLI-uploaded local secrets: flat keys + per-context namespaces. */
|
|
16
|
+
export interface CliSecrets {
|
|
17
|
+
flat: Record<string, string>;
|
|
18
|
+
contexts: Record<string, Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
export declare class DecoratingSecretResolver implements SecretResolverApi {
|
|
21
|
+
private readonly base;
|
|
22
|
+
private readonly cli;
|
|
23
|
+
constructor(base: SecretResolverApi, cli: CliSecrets);
|
|
24
|
+
resolveForJob(orgId: string, environmentName: string): Promise<Record<string, string>>;
|
|
25
|
+
resolveNamed(orgId: string, scope: string, key: string, opts?: {
|
|
26
|
+
store?: string;
|
|
27
|
+
runId?: string;
|
|
28
|
+
jobId?: string;
|
|
29
|
+
}): Promise<string | null>;
|
|
30
|
+
resolveForJobWithMeta(orgId: string, environmentName: string): Promise<Record<string, ResolvedSecretMeta>>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=decorating-secret-resolver.d.ts.map
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* exported function is a narrative orchestrator that threads the typed
|
|
14
14
|
* results through the pipeline.
|
|
15
15
|
*/
|
|
16
|
-
import { CacheRefScope } from '@kici-dev/engine';
|
|
17
|
-
import type { LabelMatcher, LockWorkflow, LockJob, SimulatedEvent, WorkflowDecision, MaterializedJob } from '@kici-dev/engine';
|
|
16
|
+
import { ExecutionJobStatus, InitFailureCategory, CacheRefScope } from '@kici-dev/engine';
|
|
17
|
+
import type { LabelMatcher, LockWorkflow, LockJob, HostTargetSelector, SimulatedEvent, WorkflowDecision, MaterializedJob, ResolvedHostAgent } from '@kici-dev/engine';
|
|
18
18
|
import { type HostRosterStore } from '../agent/host-roster.js';
|
|
19
19
|
import type { WebhookInfo } from '../webhook/handler.js';
|
|
20
20
|
import type { ProviderBundle } from '../provider-registry.js';
|
|
@@ -41,7 +41,15 @@ export declare function deriveCacheRefScope(trust: TrustResolution | undefined):
|
|
|
41
41
|
export interface WorkflowDispatchContext {
|
|
42
42
|
info: WebhookInfo;
|
|
43
43
|
deps: ProcessingDeps;
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Provider bundle for the matched source. Undefined for local-repo test runs
|
|
46
|
+
* (`kici run` against an inline lock file with no remote provider): in that
|
|
47
|
+
* mode there is no clone-url builder / check-status poster / clone-token
|
|
48
|
+
* provider, and `repoUrl` falls back to `''` (the agent treats a missing url
|
|
49
|
+
* as a local/`fullRepo` clone). The webhook adapter always passes a defined
|
|
50
|
+
* bundle, so its dispatch behavior is unchanged.
|
|
51
|
+
*/
|
|
52
|
+
bundle?: ProviderBundle;
|
|
45
53
|
payload: unknown;
|
|
46
54
|
repoIdentifier: string;
|
|
47
55
|
credentials: Record<string, unknown>;
|
|
@@ -88,10 +96,36 @@ export interface WorkflowDispatchContext {
|
|
|
88
96
|
* correct clone + logging.
|
|
89
97
|
*/
|
|
90
98
|
extraJobConfig?: Record<string, unknown>;
|
|
99
|
+
/**
|
|
100
|
+
* Test-run provenance. Present only for `kici run` / test-trigger dispatches.
|
|
101
|
+
* When set, `recordRunStart` stamps `is_test_run = true` and
|
|
102
|
+
* `fixture_id = testRun.fixtureId` on the `execution_runs` row. Undefined for
|
|
103
|
+
* webhook runs (the stamp block is skipped).
|
|
104
|
+
*/
|
|
105
|
+
testRun?: {
|
|
106
|
+
fixtureId: string;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Run-wide flat secrets layered onto EVERY dispatched job's `jobConfig.secrets`
|
|
110
|
+
* (env-declaring or not). Used by the test path to deliver `kici run --secret`
|
|
111
|
+
* / `--env` CLI flat secrets, which must reach a job regardless of whether it
|
|
112
|
+
* declares an `environment:`. Merged UNDER the per-job env-resolved secrets so
|
|
113
|
+
* the CLI value wins on a key collision (matching the prior B1-env -> A-CLI
|
|
114
|
+
* precedence). Undefined for webhook runs.
|
|
115
|
+
*/
|
|
116
|
+
runWideFlatSecrets?: Record<string, string>;
|
|
117
|
+
/**
|
|
118
|
+
* Runtime host narrowing from `kici run --target` (Ansible `--limit`). Applied
|
|
119
|
+
* as a post-filter over each runsOnAll job's matched roster: effective hosts =
|
|
120
|
+
* runsOnAll ∩ target. Narrow-only. Undefined for webhook runs (no narrowing).
|
|
121
|
+
*/
|
|
122
|
+
target?: HostTargetSelector;
|
|
91
123
|
}
|
|
92
124
|
export interface DispatchMatchedWorkflowResult {
|
|
93
125
|
/** Number of jobs successfully dispatched (non-rejected). */
|
|
94
126
|
dispatchedJobCount: number;
|
|
127
|
+
/** Execution job ids of every dispatched/tracked job (root, gated, synthetic). */
|
|
128
|
+
dispatchedJobIds: string[];
|
|
95
129
|
/** True when the workflow install gate paused the dispatch (held run). */
|
|
96
130
|
held?: boolean;
|
|
97
131
|
}
|
|
@@ -110,6 +144,39 @@ export interface DispatchMatchedWorkflowOptions {
|
|
|
110
144
|
*/
|
|
111
145
|
reuseRunId?: string;
|
|
112
146
|
}
|
|
147
|
+
interface RejectedJob {
|
|
148
|
+
jobId: string;
|
|
149
|
+
jobName: string;
|
|
150
|
+
reason: string;
|
|
151
|
+
/** Explicit init-failure category override; inferred from reason when absent. */
|
|
152
|
+
category?: InitFailureCategory;
|
|
153
|
+
/**
|
|
154
|
+
* Terminal status to record for this job. Defaults to `failed`. A zeroed
|
|
155
|
+
* `runsOnAll` that intentionally narrowed to no hosts is recorded as `skipped`
|
|
156
|
+
* (no init-failure) so its downstreams' `when` sets govern propagation.
|
|
157
|
+
*/
|
|
158
|
+
terminalStatus?: ExecutionJobStatus;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Phase B orchestrator: probe caches, dispatch the build job (if needed),
|
|
162
|
+
* and surface enough state for downstream phases to skip / continue
|
|
163
|
+
* appropriately.
|
|
164
|
+
*/
|
|
165
|
+
/**
|
|
166
|
+
* Materialize each static job's matrix into dispatchable children. A job whose
|
|
167
|
+
* matrix is invalid (zero combinations / over the cap) is dropped from the
|
|
168
|
+
* dispatch list and recorded as a `matrix_expansion` matrix failure so the run's
|
|
169
|
+
* other jobs still proceed. Dynamic-matrix jobs pass through with a
|
|
170
|
+
* `pendingDynamicMatrix` marker for the eval flow.
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
* Resolve a `runsOnAll` lock job against the declared host roster and partition
|
|
174
|
+
* the matched hosts into the target set per the `onUnreachable` policy (R2):
|
|
175
|
+
* `ready` hosts always run; unreachable durable (`static`) hosts hold / fail /
|
|
176
|
+
* skip; stale ephemeral hosts are always skipped. Throws {@link FanoutError}
|
|
177
|
+
* when the run can't proceed (fail policy with an absent host, or zero targets).
|
|
178
|
+
*/
|
|
179
|
+
export declare function resolveHostFanoutTargets(lockJob: LockJob, deps: ProcessingDeps, target?: HostTargetSelector): Promise<ResolvedHostAgent[]>;
|
|
113
180
|
/** Exact labels + regex patterns partitioned from a lock job's selectors. */
|
|
114
181
|
interface JobRoutingSelectors {
|
|
115
182
|
runsOnLabels: string[];
|
|
@@ -169,6 +236,11 @@ export interface WavePlan {
|
|
|
169
236
|
* job (single child) or one without `maxParallel` contributes nothing.
|
|
170
237
|
*/
|
|
171
238
|
export declare function computeWavePlan(materializedJobs: readonly MaterializedJob[]): WavePlan;
|
|
239
|
+
export declare function materializeStaticJobsSafe(staticJobs: readonly LockJob[], deps: ProcessingDeps, target?: HostTargetSelector): Promise<{
|
|
240
|
+
materializedJobs: MaterializedJob[];
|
|
241
|
+
expansionMap: Map<string, readonly string[]>;
|
|
242
|
+
matrixFailures: RejectedJob[];
|
|
243
|
+
}>;
|
|
172
244
|
export interface GeneratedJobConfig {
|
|
173
245
|
/**
|
|
174
246
|
* The generated lock job with its `name` and `needs` rewritten to expanded
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import type { ApproverClause, LockRegistry } from '@kici-dev/engine';
|
|
33
33
|
import type { TrustResolution } from '../security/trust-resolver.js';
|
|
34
|
-
import type {
|
|
34
|
+
import type { SecretResolverApi } from '../secrets/secret-resolver.js';
|
|
35
35
|
import type { EnvironmentStore } from '../environments/environment-store.js';
|
|
36
36
|
import { type JobDispatchContext } from '../environments/protection/pipeline.js';
|
|
37
37
|
/** Registry spec carried on the dispatch message (token already resolved). */
|
|
@@ -48,7 +48,7 @@ export interface ResolveInstallSecretsArgs {
|
|
|
48
48
|
resolvedOrgId: string;
|
|
49
49
|
trustResolution: TrustResolution | undefined;
|
|
50
50
|
environmentStore: EnvironmentStore | undefined;
|
|
51
|
-
secretResolver:
|
|
51
|
+
secretResolver: SecretResolverApi | undefined;
|
|
52
52
|
protectionContext: JobDispatchContext;
|
|
53
53
|
/**
|
|
54
54
|
* Resume path: skip the protection-rule gate (already satisfied) and resolve
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DB-backed needs-aware dispatch scheduler
|
|
2
|
+
* DB-backed needs-aware dispatch scheduler.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* needs edges (static-to-static, static-to-dyn-group, dyn-to-static, dyn-to-dyn).
|
|
4
|
+
* Gates ALL needs edges (static-to-static, static-to-dyn-group, dyn-to-static,
|
|
5
|
+
* dyn-to-dyn) with event-driven scheduling instead of concurrent dispatch.
|
|
7
6
|
*
|
|
8
7
|
* The scheduler is pure DB — no in-memory state. Every scheduling decision is
|
|
9
8
|
* a fresh DB query against execution_jobs + execution_job_needs. This means
|
|
10
9
|
* zero recovery code on orchestrator restart.
|
|
11
10
|
*
|
|
11
|
+
* Each edge carries a `run_on` status-set (the upstream terminal statuses that
|
|
12
|
+
* satisfy the edge). A downstream edge is dispatch-satisfied when the upstream's
|
|
13
|
+
* terminal status is a member of the edge's run_on set; otherwise the downstream
|
|
14
|
+
* is skipped. A downstream dispatches only when every edge is satisfied.
|
|
15
|
+
*
|
|
12
16
|
* Entry points:
|
|
13
17
|
* - insertEdgesForRun: called at run start for static-to-static edges
|
|
14
18
|
* - resolveGroupEdges: called on dynamic-eval completion
|
|
15
19
|
* - evaluateDownstreams: called from onJobStatus(terminal)
|
|
16
20
|
* - recomputeNeedsSatisfied: batch recompute after group resolution
|
|
17
|
-
* - checkSchedulerInvariant:
|
|
21
|
+
* - checkSchedulerInvariant: defensive stuck-job check
|
|
18
22
|
* - getFailurePropagationTargets: cascade for transitive skip
|
|
19
23
|
*/
|
|
20
24
|
import type { Kysely } from 'kysely';
|
|
21
25
|
import type { Database } from '../db/types.js';
|
|
26
|
+
import { ExecutionJobStatus } from '@kici-dev/engine';
|
|
22
27
|
import type { MaterializedJob } from '@kici-dev/engine';
|
|
23
28
|
/** Result of evaluating downstream jobs after an upstream completes. */
|
|
24
29
|
export interface SchedulerResult {
|
|
@@ -48,21 +53,22 @@ export declare function insertEdgesForRun(db: Kysely<Database>, runId: string, j
|
|
|
48
53
|
* edge row. Empty groups (0 members) trigger immediate needs_satisfied=true
|
|
49
54
|
* for dependents.
|
|
50
55
|
*
|
|
51
|
-
* CRITICAL: dependentStaticJobs carries per-job
|
|
56
|
+
* CRITICAL: dependentStaticJobs carries the per-job run_on status-set from the
|
|
52
57
|
* NeedsGroupEntry in the lock file. Without this, all group edges would
|
|
53
|
-
* silently default to
|
|
58
|
+
* silently default to success-only.
|
|
54
59
|
*/
|
|
55
60
|
export declare function resolveGroupEdges(db: Kysely<Database>, runId: string, groupName: string, memberJobNames: string[], dependentStaticJobs: Array<{
|
|
56
61
|
jobName: string;
|
|
57
|
-
|
|
62
|
+
runOn: ExecutionJobStatus[];
|
|
58
63
|
}>): Promise<void>;
|
|
59
64
|
/**
|
|
60
65
|
* Evaluate downstream jobs after an upstream reaches terminal state.
|
|
61
66
|
*
|
|
62
67
|
* This is the core scheduler hook. For each downstream of the completed job:
|
|
63
|
-
* 1. If
|
|
64
|
-
*
|
|
65
|
-
*
|
|
68
|
+
* 1. If the completed status is not in this edge's run_on set, mark the
|
|
69
|
+
* downstream as 'skip' immediately.
|
|
70
|
+
* 2. Otherwise, check if ALL upstreams are terminal and satisfied.
|
|
71
|
+
* 3. If all satisfied, mark needs_satisfied=true and return for dispatch.
|
|
66
72
|
*/
|
|
67
73
|
export declare function evaluateDownstreams(db: Kysely<Database>, runId: string, completedJobName: string, completedStatus: string): Promise<SchedulerResult[]>;
|
|
68
74
|
/**
|
|
@@ -80,8 +86,11 @@ export declare function recomputeNeedsSatisfied(db: Kysely<Database>, runId: str
|
|
|
80
86
|
*/
|
|
81
87
|
export declare function checkSchedulerInvariant(db: Kysely<Database>, runId: string): Promise<string[]>;
|
|
82
88
|
/**
|
|
83
|
-
* cascade: find all transitive downstreams that should be
|
|
84
|
-
*
|
|
89
|
+
* Failure-propagation cascade: find all transitive downstreams that should be
|
|
90
|
+
* skipped because a terminal upstream's status is not in their edge's run_on
|
|
91
|
+
* set. At each hop, the propagating job's actual terminal status decides which
|
|
92
|
+
* downstream edges propagate (status not in run_on → the downstream skips and
|
|
93
|
+
* propagates further).
|
|
85
94
|
*/
|
|
86
95
|
export declare function getFailurePropagationTargets(db: Kysely<Database>, runId: string, failedJobName: string): Promise<string[]>;
|
|
87
96
|
//# sourceMappingURL=needs-scheduler.d.ts.map
|