@kici-dev/orchestrator 0.12.0 → 0.13.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.
Files changed (46) hide show
  1. package/dist/agent/dispatcher.d.ts +22 -0
  2. package/dist/agent/registry.d.ts +31 -0
  3. package/dist/app.d.ts +6 -0
  4. package/dist/cache/cache-upload-settle.d.ts +75 -0
  5. package/dist/cache/pending-inits.d.ts +9 -0
  6. package/dist/cancel/cancel-run.d.ts +22 -4
  7. package/dist/cancel/peer-job-cancel.d.ts +50 -0
  8. package/dist/cancel/sweep-stuck-cancelling.d.ts +3 -1
  9. package/dist/cli/commands/local-hook.d.ts +3 -2
  10. package/dist/cli/commands/local-trigger.d.ts +7 -0
  11. package/dist/cli.js +983 -683
  12. package/dist/cluster/coordinator-credential.d.ts +45 -0
  13. package/dist/cluster/coordinator.d.ts +18 -2
  14. package/dist/cluster/index.d.ts +1 -0
  15. package/dist/cluster/instance-heartbeat.d.ts +6 -1
  16. package/dist/cluster/join-token.d.ts +2 -0
  17. package/dist/cluster/peer-auth-coordinator.d.ts +13 -0
  18. package/dist/cluster/peer-client.d.ts +4 -0
  19. package/dist/cluster/peer-credentials.d.ts +31 -0
  20. package/dist/config.d.ts +3 -0
  21. package/dist/db/migrations/152_org_settings_cache_upload_settle_timeout.d.ts +15 -0
  22. package/dist/db/migrations/153_execution_runs_cancel_force.d.ts +16 -0
  23. package/dist/db/types.d.ts +15 -0
  24. package/dist/index.js +192 -53
  25. package/dist/pipeline/changed-files.d.ts +51 -0
  26. package/dist/pipeline/cross-source-tally.d.ts +21 -0
  27. package/dist/pipeline/degraded-reason.d.ts +8 -0
  28. package/dist/pipeline/dispatch-matched-workflow.d.ts +10 -0
  29. package/dist/pipeline/global-eval-round.d.ts +3 -2
  30. package/dist/pipeline/processor.d.ts +9 -1
  31. package/dist/pipeline/rerun-inputs.d.ts +16 -7
  32. package/dist/providers/github/changed-files.d.ts +14 -4
  33. package/dist/providers/universal-git/changed-files.d.ts +6 -0
  34. package/dist/providers/universal-git/config.d.ts +4 -0
  35. package/dist/scaler/container-backend.d.ts +2 -0
  36. package/dist/scaler/container-runtime.d.ts +9 -0
  37. package/dist/scaler/manager.d.ts +26 -2
  38. package/dist/server.js +2710 -1486
  39. package/dist/stale-detector/workflow-deadline-detector.d.ts +7 -1
  40. package/dist/standalone.js +11527 -10302
  41. package/dist/storage/s3.d.ts +36 -0
  42. package/dist/ws/agent-handler.d.ts +24 -3
  43. package/dist/ws/agent-heartbeat.d.ts +2 -2
  44. package/installer-image-digests.json +3 -3
  45. package/package.json +5 -5
  46. package/sbom.spdx.json +53 -53
@@ -0,0 +1,45 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { type Logger } from '@kici-dev/shared';
3
+ import type { Database } from '../db/types.js';
4
+ import { PeerCredentialStore } from './peer-credentials.js';
5
+ /** What one reconciliation did. */
6
+ export declare enum CoordinatorCredentialOutcome {
7
+ Valid = "valid",
8
+ Issued = "issued",
9
+ Replaced = "replaced",
10
+ Revoked = "revoked",
11
+ Failed = "failed"
12
+ }
13
+ /** Why a new credential was issued, recorded on the issue log line. */
14
+ export declare enum CredentialIssueReason {
15
+ NoCredential = "no-credential",
16
+ Expired = "expired",
17
+ FileMissing = "credential-file-missing",
18
+ FileMismatch = "credential-file-mismatch"
19
+ }
20
+ export declare const COORDINATOR_CREDENTIAL_REVOKED_MESSAGE = "Peer credential for this coordinator was revoked; not issuing a new one";
21
+ export type CoordinatorCredentialStore = Pick<PeerCredentialStore, 'save' | 'findUnrevokedByInstanceId' | 'findLatestRevokedByInstanceId' | 'retireSelfIssued'>;
22
+ export interface EnsureCoordinatorCredentialOptions {
23
+ store: CoordinatorCredentialStore;
24
+ /** Absolute path of the credential file, with `~` already expanded. */
25
+ credentialFile: string;
26
+ instanceId: string;
27
+ /** Whether an instance has a live `cluster_instances` heartbeat. */
28
+ isInstanceLive: (instanceId: string) => Promise<boolean>;
29
+ logger?: Logger;
30
+ now?: () => Date;
31
+ }
32
+ /**
33
+ * Make sure this coordinator holds a credential its peers accept, issuing one
34
+ * when it has none. Never throws: a failure is logged and reported as `Failed`.
35
+ */
36
+ export declare function ensureCoordinatorCredential(opts: EnsureCoordinatorCredentialOptions): Promise<CoordinatorCredentialOutcome>;
37
+ /** The `selfIssue` callback the coordinator's `PeerAuthCoordinator` runs. */
38
+ export declare function coordinatorSelfIssuer(opts: {
39
+ db: Kysely<Database>;
40
+ credentialFile: string;
41
+ instanceId: string;
42
+ agentMaxReconnectDelayMs: number;
43
+ clusterInstanceHeartbeatMs: number;
44
+ }): () => Promise<CoordinatorCredentialOutcome>;
45
+ //# sourceMappingURL=coordinator-credential.d.ts.map
@@ -334,9 +334,25 @@ export declare class RunCoordinator {
334
334
  * The run-cancel path needs this for a job this coordinator never routed:
335
335
  * the Platform picks any pool member to receive `run.cancel.request`, and only
336
336
  * the coordinator holding the agent's socket can put a cancel frame on it.
337
- * {@link RunCoordinator.cancelRun} covers rerouted jobs and is unchanged.
337
+ * `force` rides on the message, so a force cancel stays forced on the peer.
338
+ * {@link RunCoordinator.cancelRun} covers rerouted jobs.
338
339
  */
339
- cancelJobOnPeer(peerId: string, runId: string, jobId: string, reason: string): boolean;
340
+ cancelJobOnPeer(peerId: string, runId: string, jobId: string, reason: string, force?: boolean): boolean;
341
+ /**
342
+ * Send a fire-and-forget message to a peer. The outbound client carries it
343
+ * when connected; otherwise the peer's own connection to this process does.
344
+ * The outbound client is down while it reconnects, and for good on a
345
+ * coordinator whose credential an operator revoked.
346
+ * `PeerClient.send` returns false only when it sent nothing, so the fallback
347
+ * never delivers a message twice.
348
+ */
349
+ private sendToPeer;
350
+ /**
351
+ * The link a reroute uses, chosen before sending. A reroute is never retried
352
+ * over the second link: `false` from a connected client is a NAK or an ack
353
+ * timeout, and resending would deliver the job twice.
354
+ */
355
+ private rerouteTransport;
340
356
  /**
341
357
  * Local fallback for a re-dispatch: dispatch the job on this coordinator under
342
358
  * the existing jobId. Returns true when the local dispatcher accepts it.
@@ -9,6 +9,7 @@ export type { PeerInfo, PeerAgentInfo, PeerRegistryOptions } from './peer-regist
9
9
  export { PeerClient } from './peer-client.js';
10
10
  export type { PeerClientOptions } from './peer-client.js';
11
11
  export { PeerAuthCoordinator } from './peer-auth-coordinator.js';
12
+ export { coordinatorSelfIssuer } from './coordinator-credential.js';
12
13
  export type { AuthDecision, RejectionAction } from './peer-auth-coordinator.js';
13
14
  export { createPeerHandler } from './peer-handler.js';
14
15
  export type { PeerHandlerDeps } from './peer-handler.js';
@@ -1,4 +1,4 @@
1
- import type { Kysely } from 'kysely';
1
+ import { type Kysely } from 'kysely';
2
2
  import type { Database } from '../db/types.js';
3
3
  /** Default interval between `cluster_instances` heartbeat writes. */
4
4
  export declare const DEFAULT_INSTANCE_HEARTBEAT_MS = 10000;
@@ -23,6 +23,11 @@ export declare const DEFAULT_RECOVERY_GRACE_MS = 120000;
23
23
  * recovery machinery would have given it to reconnect.
24
24
  */
25
25
  export declare function instanceLivenessGraceMs(gracePeriodMs: number, heartbeatMs?: number): number;
26
+ /**
27
+ * Whether `instanceId` has a `cluster_instances` heartbeat inside `graceMs`:
28
+ * the same liveness the dispatch plane's ownership predicate reads.
29
+ */
30
+ export declare function isInstanceLive(db: Kysely<Database>, instanceId: string, graceMs: number): Promise<boolean>;
26
31
  export interface InstanceHeartbeatOptions {
27
32
  db: Kysely<Database>;
28
33
  instanceId: string;
@@ -18,6 +18,8 @@ import { Kysely } from 'kysely';
18
18
  * and a stray log line would break the contract).
19
19
  */
20
20
  export declare function silenceJoinTokenLogger(): void;
21
+ export declare const TOKEN_ALREADY_USED_MESSAGE = "Join token has already been used. Create a new token with: kici-admin peer create-token";
22
+ export declare const TOKEN_EXPIRED_MESSAGE = "Join token has expired. Create a new token with: kici-admin peer create-token";
21
23
  interface TokenRouting {
22
24
  orgId: string;
23
25
  routingKey: string;
@@ -1,4 +1,5 @@
1
1
  import { type CredentialFileData } from './peer-credentials.js';
2
+ import { CoordinatorCredentialOutcome } from './coordinator-credential.js';
2
3
  export type AuthDecision = {
3
4
  mode: 'credential';
4
5
  credential: CredentialFileData;
@@ -15,6 +16,11 @@ export declare class PeerAuthCoordinator {
15
16
  private readonly instanceId;
16
17
  private readonly joinToken?;
17
18
  private readonly joinWaitTimeoutMs;
19
+ private readonly selfIssue?;
20
+ /** The issuance in progress; concurrent sibling clients share it. */
21
+ private selfIssueInFlight;
22
+ /** An operator revoked this coordinator's credential: never issue again in this process. */
23
+ private selfIssueRevoked;
18
24
  /** Promise-chain mutex tail; every file op awaits the prior one. */
19
25
  private lock;
20
26
  /** Set while one peer-client is mid token-join; siblings await it. */
@@ -24,11 +30,18 @@ export declare class PeerAuthCoordinator {
24
30
  instanceId: string;
25
31
  joinToken?: string;
26
32
  joinWaitTimeoutMs?: number;
33
+ /**
34
+ * Issues this coordinator its own credential when a decision finds no
35
+ * credential and no join token. Only a coordinator wires it.
36
+ */
37
+ selfIssue?: () => Promise<CoordinatorCredentialOutcome>;
27
38
  });
28
39
  /** Run `fn` exclusively against the credential file. */
29
40
  private withLock;
30
41
  private readValidCredential;
31
42
  decideAuth(): Promise<AuthDecision>;
43
+ /** Run the issuer under the mutex, once for every concurrent caller. */
44
+ private runSelfIssue;
32
45
  private makeComplete;
33
46
  private awaitInFlightJoin;
34
47
  reportRejection(provedCredential: string | null, reason: string): Promise<RejectionAction>;
@@ -13,6 +13,8 @@
13
13
  import { type PeerHeartbeat, type PeerToPeerMessage, type JobReroute, type JobProgress, type JobProgressAck, type PeerJobCancel, type PeerLogChunk, type PeerCacheUploadRequest, type PeerCacheUploadResponse, type PeerConfigReload, type PeerConfigReloadResponse, type PeerClusterSettingsRequest, type PeerClusterSettingsResponse, type PeerLogsCollectRequest, type PeerLeaving, type PeerAgentTokenRevoke, type RaftVoteRequest, type RaftVoteResponse, type RaftAppendEntries } from '@kici-dev/engine';
14
14
  import type { PeerRegistry } from './peer-registry.js';
15
15
  import type { PeerAuthCoordinator } from './peer-auth-coordinator.js';
16
+ /** Logged when this instance has neither a credential file nor a join token. */
17
+ export declare const NO_AUTH_METHOD_MESSAGE = "No peer auth method: this instance has no credential file and no join token";
16
18
  type PeerConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'authenticating' | 'connected';
17
19
  export interface PeerClientOptions {
18
20
  /** WebSocket URL of the remote peer orchestrator. */
@@ -108,6 +110,8 @@ export declare class PeerClient {
108
110
  private heartbeatTimer;
109
111
  private reconnectTimer;
110
112
  private reconnectAttempts;
113
+ /** Whether this client already warned that it has no auth method; re-armed by an accepted auth. */
114
+ private noAuthWarned;
111
115
  private intentionalDisconnect;
112
116
  private _targetInstanceId;
113
117
  private sessionKey;
@@ -1,4 +1,15 @@
1
1
  import { Kysely } from 'kysely';
2
+ /** How a credential row was issued, recorded in `peer_credentials.metadata.issuance`. */
3
+ export declare enum PeerCredentialIssuance {
4
+ /** A coordinator issued the credential to itself (`coordinator-credential.ts`). */
5
+ Self = "self"
6
+ }
7
+ /**
8
+ * `metadata` key set when a coordinator retires the self-issued credential its
9
+ * previous run left behind. An operator revoke never sets it, which is how a
10
+ * retirement is told apart from a revoke.
11
+ */
12
+ export declare const RETIRED_BY_INSTANCE_KEY = "retiredByInstance";
2
13
  /**
3
14
  * A peer credential record stored in the coordinator's database.
4
15
  */
@@ -14,6 +25,7 @@ export interface PeerCredential {
14
25
  lastValidatedBy: string | null;
15
26
  expiresAt: Date;
16
27
  revokedAt: Date | null;
28
+ metadata: Record<string, unknown>;
17
29
  }
18
30
  /**
19
31
  * Data persisted in this orchestrator's local credential file
@@ -55,6 +67,7 @@ export declare class PeerCredentialStore {
55
67
  routingKeys: string[];
56
68
  sourceTokenHash?: string;
57
69
  expiryDays?: number;
70
+ metadata?: Record<string, unknown>;
58
71
  }): Promise<{
59
72
  revokedCount: number;
60
73
  }>;
@@ -66,6 +79,24 @@ export declare class PeerCredentialStore {
66
79
  * Find a non-revoked, non-expired credential by instance ID.
67
80
  */
68
81
  findByInstanceId(instanceId: string): Promise<PeerCredential | null>;
82
+ /**
83
+ * The credential row for `instanceId` that nobody revoked, expired or not.
84
+ * `peer_credentials_active_uniq` allows at most one.
85
+ */
86
+ findUnrevokedByInstanceId(instanceId: string): Promise<PeerCredential | null>;
87
+ /** The most recently revoked credential row for `instanceId`, or null. */
88
+ findLatestRevokedByInstanceId(instanceId: string): Promise<PeerCredential | null>;
89
+ /**
90
+ * Revoke `instanceId`'s unrevoked, self-issued row whose hash is
91
+ * `credentialHash`, recording who retired it. Every condition sits in the
92
+ * WHERE clause, so a row that changed meanwhile is left alone.
93
+ * Returns whether a row was retired.
94
+ */
95
+ retireSelfIssued(opts: {
96
+ instanceId: string;
97
+ credentialHash: string;
98
+ retiredByInstance: string;
99
+ }): Promise<boolean>;
69
100
  /**
70
101
  * Update the last_seen_at timestamp for a credential.
71
102
  * Optionally records which coordinator validated it.
package/dist/config.d.ts CHANGED
@@ -147,6 +147,7 @@ declare const configSchema: z.ZodObject<{
147
147
  reconnectReplayWindowHours: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
148
148
  cacheBuildTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
149
149
  cacheMaxTarballBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
150
+ cacheUploadSettleTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
150
151
  userCacheQuotaBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
151
152
  userCacheTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
152
153
  artifactQuotaBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -357,6 +358,7 @@ export declare const packagingConfigSchema: z.ZodObject<{
357
358
  reconnectReplayWindowHours: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
358
359
  cacheBuildTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
359
360
  cacheMaxTarballBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
361
+ cacheUploadSettleTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
360
362
  userCacheQuotaBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
361
363
  userCacheTtlMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
362
364
  artifactQuotaBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
@@ -591,6 +593,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
591
593
  reconnectReplayWindowHours: number;
592
594
  cacheBuildTimeoutMs: number;
593
595
  cacheMaxTarballBytes: number;
596
+ cacheUploadSettleTimeoutMs: number;
594
597
  userCacheQuotaBytes: number;
595
598
  userCacheTtlMs: number;
596
599
  artifactQuotaBytes: number;
@@ -0,0 +1,15 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `org_settings.cache_upload_settle_timeout_ms BIGINT` (nullable).
4
+ *
5
+ * Per-org bound on how long a build job's `job.status success` waits for the
6
+ * cache publish (object metadata plus pointer) its agent reported just before
7
+ * it. NULL falls back to the cluster default
8
+ * (`KICI_CACHE_UPLOAD_SETTLE_TIMEOUT_MS`, 10s); 0 turns the wait off. Operators
9
+ * tune it via `kici-admin org-settings cache-upload-settle`.
10
+ *
11
+ * Idempotent: a re-run on a DB that already has the column is a no-op.
12
+ */
13
+ export declare function up(db: Kysely<unknown>): Promise<void>;
14
+ export declare function down(db: Kysely<unknown>): Promise<void>;
15
+ //# sourceMappingURL=152_org_settings_cache_upload_settle_timeout.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `execution_runs.cancel_force BOOLEAN` (nullable, no default).
4
+ *
5
+ * TRUE once a forced cancel reached the run. The leader's stuck-cancelling
6
+ * sweep reads it to re-send a cancel that did not complete with `force` set,
7
+ * so a forced cancel whose first forward to a sibling coordinator was lost is
8
+ * not re-sent as a graceful one. NULL (every existing row, and every run no
9
+ * forced cancel reached) re-sends gracefully. No backfill: nothing recorded
10
+ * whether an earlier cancel was forced.
11
+ *
12
+ * Idempotent: a re-run on a DB that already has the column is a no-op.
13
+ */
14
+ export declare function up(db: Kysely<unknown>): Promise<void>;
15
+ export declare function down(db: Kysely<unknown>): Promise<void>;
16
+ //# sourceMappingURL=153_execution_runs_cancel_force.d.ts.map
@@ -514,6 +514,13 @@ export interface ExecutionRunTable {
514
514
  * cancelled and for one that entered `cancelling` before the column existed.
515
515
  */
516
516
  cancelling_at: ColumnType<Date | null, Date | null | undefined, Date | null>;
517
+ /**
518
+ * TRUE once a forced cancel reached the run (migration 153). The
519
+ * stuck-cancelling re-drive reads it so a cancel that did not complete is
520
+ * re-sent with `force`. Never reset to FALSE: a later graceful cancel does
521
+ * not undo a forced one. NULL re-drives gracefully.
522
+ */
523
+ cancel_force: ColumnType<boolean | null, boolean | null | undefined, boolean | null>;
517
524
  /**
518
525
  * The run's status generation (migration 150). Starts at 0 and rises by one
519
526
  * each time the run leaves a terminal status to continue. Forwarded on every
@@ -1994,6 +2001,14 @@ export interface OrgSettingsTable {
1994
2001
  * returns a string on select; accept a number on insert/update.
1995
2002
  */
1996
2003
  queue_timeout_ms: ColumnType<string | null, number | null | undefined, number | null>;
2004
+ /**
2005
+ * Per-org bound (ms) on how long a build job's success waits for the cache
2006
+ * publish its agent reported just before it. 0 turns the wait off. Null =
2007
+ * cluster default (config.cacheUploadSettleTimeoutMs /
2008
+ * KICI_CACHE_UPLOAD_SETTLE_TIMEOUT_MS). Postgres BIGINT — pg returns a string
2009
+ * on select; accept a number on insert/update.
2010
+ */
2011
+ cache_upload_settle_timeout_ms: ColumnType<string | null, number | null | undefined, number | null>;
1997
2012
  /**
1998
2013
  * Per-org expiry (seconds) for a held approval element before it is rejected
1999
2014
  * and its run/job/step fails. NOT NULL, default 86400 (one day). An SDK