@kici-dev/orchestrator 0.1.20 → 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 +54 -1
- package/dist/agent/registry.d.ts +22 -0
- package/dist/app.d.ts +5 -0
- package/dist/approvals/step-approval-bridge.d.ts +5 -0
- package/dist/cli/commands/source-manifest.d.ts +42 -0
- package/dist/cli/loopback-callback.d.ts +24 -0
- package/dist/cli/open-browser.d.ts +12 -0
- package/dist/cli/service/compose.d.ts +13 -0
- package/dist/cli/service/deploy-env.d.ts +31 -0
- package/dist/cli.js +1204 -256
- package/dist/cluster/coordinator.d.ts +5 -3
- package/dist/cluster/index.d.ts +2 -0
- package/dist/cluster/join-token.d.ts +27 -5
- package/dist/cluster/peer-auth-coordinator.d.ts +36 -0
- package/dist/cluster/peer-client.d.ts +34 -14
- package/dist/cluster/peer-credentials.d.ts +14 -2
- package/dist/cluster/peer-handler.d.ts +2 -2
- package/dist/cluster/rerouted-job-guard.d.ts +39 -0
- package/dist/config.d.ts +6 -0
- package/dist/dashboard/needs-edges.d.ts +4 -3
- package/dist/db/migrations/043_rerouted_to_peer.d.ts +4 -0
- package/dist/db/migrations/044_check_mode.d.ts +4 -0
- package/dist/db/migrations/045_host_properties.d.ts +16 -0
- package/dist/db/migrations/046_join_token_consumed_by_instance.d.ts +17 -0
- 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 +60 -5
- 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.d.ts +5 -1
- package/dist/index.js +1187 -33
- package/dist/lockfile-validate.d.ts +24 -0
- 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 +127 -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 +37 -58
- package/dist/providers/github/manifest-form.d.ts +15 -0
- package/dist/providers/github/manifest.d.ts +103 -0
- package/dist/reporting/execution-tracker.d.ts +10 -1
- package/dist/routes/admin-sources.d.ts +18 -0
- package/dist/routes/admin.d.ts +8 -0
- package/dist/secrets/pg-secret-store.d.ts +9 -0
- package/dist/secrets/secret-resolver.d.ts +16 -1
- package/dist/server.js +4373 -2322
- 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/stale-detector/stale-run-detector.d.ts +8 -0
- package/dist/standalone.js +3582 -1932
- package/dist/worker/in-memory-job-queue.d.ts +17 -0
- package/dist/worker/peer-outbox.d.ts +36 -0
- package/dist/worker/worker-outbox-relay.d.ts +13 -0
- 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/inventory-api.d.ts +17 -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 +77 -128
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import * as path$1 from "node:path";
|
|
5
5
|
import path, { join, resolve } from "node:path";
|
|
6
6
|
import * as fs$1 from "node:fs";
|
|
7
|
-
import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
|
|
7
|
+
import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, mkdtempSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { BaseColdStore, ChunkLru, addLogsToArchive, chunkObjectKey, clearDispatchQueueDirect, computeChunkId, computeMigrationsHash, createDb, createDbRole, createEnvironmentTemplateDirect, createLogger, createPool, createReadOnlyDbUser, decodeChunk, deleteEnvironmentDirect, dropAndCreateDatabase, emitKiciEventDirect, encodeKeySegment, ensureDatabase, formatBytes, formatUptime, isSchemaCurrent, listEnvironmentsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, registerWorkflowManualDirect, resetRaftStateDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, sha256, showEnvironmentDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, tablePrefix, toErrorMessage } from "@kici-dev/shared";
|
|
10
10
|
import crypto, { createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -12,22 +12,24 @@ import { createInterface } from "node:readline";
|
|
|
12
12
|
import { access, chmod, constants, copyFile, link, mkdir, open, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
13
13
|
import { parse, stringify } from "yaml";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
-
import os from "node:os";
|
|
15
|
+
import os, { platform, tmpdir } from "node:os";
|
|
16
16
|
import { getDatabaseCollationDrift, refreshDatabaseCollationVersion, reindexDatabaseConcurrently } from "@kici-dev/shared/db-collation";
|
|
17
17
|
import { sql } from "kysely";
|
|
18
18
|
import { Migrator } from "kysely/migration";
|
|
19
19
|
import "jsonpath-plus";
|
|
20
|
-
import { execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
20
|
+
import { exec, execFile, execFileSync, execSync, spawn } from "node:child_process";
|
|
21
21
|
import { promisify } from "node:util";
|
|
22
|
+
import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, parseHostPropertyAssignments, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
|
|
23
|
+
import { Octokit } from "@octokit/rest";
|
|
24
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
25
|
+
import http, { createServer } from "node:http";
|
|
22
26
|
import { ZipArchive } from "archiver";
|
|
23
27
|
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
24
28
|
import JSZip from "jszip";
|
|
25
29
|
import { pipeline } from "node:stream/promises";
|
|
26
30
|
import "pg";
|
|
27
|
-
import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
|
|
28
31
|
import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
|
|
29
32
|
import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
|
|
30
|
-
import http from "node:http";
|
|
31
33
|
import { PassThrough } from "node:stream";
|
|
32
34
|
import Docker from "dockerode";
|
|
33
35
|
var __defProp = Object.defineProperty;
|
|
@@ -1554,8 +1556,8 @@ var init_client = __esmMin((() => {
|
|
|
1554
1556
|
//#region src/db/migrations/001_initial.ts
|
|
1555
1557
|
init_client();
|
|
1556
1558
|
var _001_initial_exports = /* @__PURE__ */ __exportAll({
|
|
1557
|
-
down: () => down$
|
|
1558
|
-
up: () => up$
|
|
1559
|
+
down: () => down$49,
|
|
1560
|
+
up: () => up$49
|
|
1559
1561
|
});
|
|
1560
1562
|
/**
|
|
1561
1563
|
* Squashed initial migration -- creates the complete Orchestrator database schema.
|
|
@@ -2247,7 +2249,7 @@ const DDL_STATEMENTS = [
|
|
|
2247
2249
|
`ALTER TABLE ONLY public.held_runs
|
|
2248
2250
|
ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
|
|
2249
2251
|
];
|
|
2250
|
-
async function up$
|
|
2252
|
+
async function up$49(db) {
|
|
2251
2253
|
for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
|
|
2252
2254
|
await sql`
|
|
2253
2255
|
INSERT INTO cluster_meta (key, value)
|
|
@@ -2269,7 +2271,7 @@ async function up$41(db) {
|
|
|
2269
2271
|
* Rollback drops everything created above. Uses CASCADE on table drops to cut
|
|
2270
2272
|
* through the FK graph without relying on exact topological order.
|
|
2271
2273
|
*/
|
|
2272
|
-
async function down$
|
|
2274
|
+
async function down$49(db) {
|
|
2273
2275
|
for (const [trig, tbl] of [["source_secrets_change_trigger", "scoped_secrets"], ["sources_change_trigger", "sources"]]) await sql.raw(`DROP TRIGGER IF EXISTS ${trig} ON public.${tbl}`).execute(db);
|
|
2274
2276
|
for (const table of [
|
|
2275
2277
|
"workflow_registrations",
|
|
@@ -2316,8 +2318,8 @@ async function down$41(db) {
|
|
|
2316
2318
|
//#endregion
|
|
2317
2319
|
//#region src/db/migrations/002_config_versions_key_version.ts
|
|
2318
2320
|
var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
|
|
2319
|
-
down: () => down$
|
|
2320
|
-
up: () => up$
|
|
2321
|
+
down: () => down$48,
|
|
2322
|
+
up: () => up$48
|
|
2321
2323
|
});
|
|
2322
2324
|
/**
|
|
2323
2325
|
* Add key_version column to config_versions so that sensitive-field encryption
|
|
@@ -2331,13 +2333,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
|
|
|
2331
2333
|
* No index is needed: rotation does a full-table scan; reads are by
|
|
2332
2334
|
* `version` primary key and never filter on `key_version`.
|
|
2333
2335
|
*/
|
|
2334
|
-
async function up$
|
|
2336
|
+
async function up$48(db) {
|
|
2335
2337
|
await sql`
|
|
2336
2338
|
ALTER TABLE public.config_versions
|
|
2337
2339
|
ADD COLUMN key_version integer NOT NULL DEFAULT 1
|
|
2338
2340
|
`.execute(db);
|
|
2339
2341
|
}
|
|
2340
|
-
async function down$
|
|
2342
|
+
async function down$48(db) {
|
|
2341
2343
|
await sql`
|
|
2342
2344
|
ALTER TABLE public.config_versions
|
|
2343
2345
|
DROP COLUMN key_version
|
|
@@ -2346,8 +2348,8 @@ async function down$40(db) {
|
|
|
2346
2348
|
//#endregion
|
|
2347
2349
|
//#region src/db/migrations/003_access_log.ts
|
|
2348
2350
|
var _003_access_log_exports = /* @__PURE__ */ __exportAll({
|
|
2349
|
-
down: () => down$
|
|
2350
|
-
up: () => up$
|
|
2351
|
+
down: () => down$47,
|
|
2352
|
+
up: () => up$47
|
|
2351
2353
|
});
|
|
2352
2354
|
/**
|
|
2353
2355
|
* Access log: one row per read or orchestrator-admin mutation attributable
|
|
@@ -2371,7 +2373,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
|
|
|
2371
2373
|
* Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
|
|
2372
2374
|
* cleanup.ts picks up the prune pass.
|
|
2373
2375
|
*/
|
|
2374
|
-
async function up$
|
|
2376
|
+
async function up$47(db) {
|
|
2375
2377
|
await sql`
|
|
2376
2378
|
CREATE TABLE public.access_log (
|
|
2377
2379
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
@@ -2409,28 +2411,28 @@ async function up$39(db) {
|
|
|
2409
2411
|
ON public.access_log (actor_type, actor_id, created_at DESC)
|
|
2410
2412
|
`.execute(db);
|
|
2411
2413
|
}
|
|
2412
|
-
async function down$
|
|
2414
|
+
async function down$47(db) {
|
|
2413
2415
|
await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
|
|
2414
2416
|
}
|
|
2415
2417
|
//#endregion
|
|
2416
2418
|
//#region src/db/migrations/004_rename_bundle_to_source.ts
|
|
2417
2419
|
var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
|
|
2418
|
-
down: () => down$
|
|
2419
|
-
up: () => up$
|
|
2420
|
+
down: () => down$46,
|
|
2421
|
+
up: () => up$46
|
|
2420
2422
|
});
|
|
2421
|
-
async function up$
|
|
2423
|
+
async function up$46(db) {
|
|
2422
2424
|
await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
|
|
2423
2425
|
await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
|
|
2424
2426
|
}
|
|
2425
|
-
async function down$
|
|
2427
|
+
async function down$46(db) {
|
|
2426
2428
|
await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
|
|
2427
2429
|
await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
|
|
2428
2430
|
}
|
|
2429
2431
|
//#endregion
|
|
2430
2432
|
//#region src/db/migrations/005_cold_store_chunk_counter.ts
|
|
2431
2433
|
var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
|
|
2432
|
-
down: () => down$
|
|
2433
|
-
up: () => up$
|
|
2434
|
+
down: () => down$45,
|
|
2435
|
+
up: () => up$45
|
|
2434
2436
|
});
|
|
2435
2437
|
/**
|
|
2436
2438
|
* Cold-store chunk counter table.
|
|
@@ -2447,7 +2449,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
|
|
|
2447
2449
|
*
|
|
2448
2450
|
* sections 5 and 8.
|
|
2449
2451
|
*/
|
|
2450
|
-
async function up$
|
|
2452
|
+
async function up$45(db) {
|
|
2451
2453
|
await sql`
|
|
2452
2454
|
CREATE TABLE public.cold_store_chunk_counts (
|
|
2453
2455
|
db TEXT NOT NULL,
|
|
@@ -2465,14 +2467,14 @@ async function up$37(db) {
|
|
|
2465
2467
|
ON public.cold_store_chunk_counts (db, table_name)
|
|
2466
2468
|
`.execute(db);
|
|
2467
2469
|
}
|
|
2468
|
-
async function down$
|
|
2470
|
+
async function down$45(db) {
|
|
2469
2471
|
await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
|
|
2470
2472
|
}
|
|
2471
2473
|
//#endregion
|
|
2472
2474
|
//#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
|
|
2473
2475
|
var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2474
|
-
down: () => down$
|
|
2475
|
-
up: () => up$
|
|
2476
|
+
down: () => down$44,
|
|
2477
|
+
up: () => up$44
|
|
2476
2478
|
});
|
|
2477
2479
|
/**
|
|
2478
2480
|
* `execution_runs` / `execution_jobs` / `execution_steps` cold-store
|
|
@@ -2508,7 +2510,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2508
2510
|
* - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
|
|
2509
2511
|
* - `idx_execution_steps_routing_key_created (routing_key, created_at)`
|
|
2510
2512
|
*/
|
|
2511
|
-
async function up$
|
|
2513
|
+
async function up$44(db) {
|
|
2512
2514
|
await sql`
|
|
2513
2515
|
ALTER TABLE public.execution_runs
|
|
2514
2516
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2553,7 +2555,7 @@ async function up$36(db) {
|
|
|
2553
2555
|
ON public.execution_steps (routing_key, created_at)
|
|
2554
2556
|
`.execute(db);
|
|
2555
2557
|
}
|
|
2556
|
-
async function down$
|
|
2558
|
+
async function down$44(db) {
|
|
2557
2559
|
await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
|
|
2558
2560
|
await sql`
|
|
2559
2561
|
ALTER TABLE public.execution_steps
|
|
@@ -2578,8 +2580,8 @@ async function down$36(db) {
|
|
|
2578
2580
|
//#endregion
|
|
2579
2581
|
//#region src/db/migrations/007_audit_logs_archived_at.ts
|
|
2580
2582
|
var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2581
|
-
down: () => down$
|
|
2582
|
-
up: () => up$
|
|
2583
|
+
down: () => down$43,
|
|
2584
|
+
up: () => up$43
|
|
2583
2585
|
});
|
|
2584
2586
|
/**
|
|
2585
2587
|
* `secret_audit_log` and `access_log` cold-store schema additions, plus
|
|
@@ -2619,7 +2621,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2619
2621
|
* `down()` would not have meaningful retention bounds. Acceptable for
|
|
2620
2622
|
* staging.
|
|
2621
2623
|
*/
|
|
2622
|
-
async function up$
|
|
2624
|
+
async function up$43(db) {
|
|
2623
2625
|
await sql`
|
|
2624
2626
|
ALTER TABLE public.secret_audit_log
|
|
2625
2627
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2640,7 +2642,7 @@ async function up$35(db) {
|
|
|
2640
2642
|
DROP COLUMN IF EXISTS expires_at
|
|
2641
2643
|
`.execute(db);
|
|
2642
2644
|
}
|
|
2643
|
-
async function down$
|
|
2645
|
+
async function down$43(db) {
|
|
2644
2646
|
await sql`
|
|
2645
2647
|
ALTER TABLE public.access_log
|
|
2646
2648
|
ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
|
|
@@ -2668,8 +2670,8 @@ async function down$35(db) {
|
|
|
2668
2670
|
//#endregion
|
|
2669
2671
|
//#region src/db/migrations/008_event_log_archived_at.ts
|
|
2670
2672
|
var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
2671
|
-
down: () => down$
|
|
2672
|
-
up: () => up$
|
|
2673
|
+
down: () => down$42,
|
|
2674
|
+
up: () => up$42
|
|
2673
2675
|
});
|
|
2674
2676
|
/**
|
|
2675
2677
|
* `event_log` cold-store schema additions plus removal of the
|
|
@@ -2707,7 +2709,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
|
|
|
2707
2709
|
* — best effort; rows inserted between `up()` and a hypothetical
|
|
2708
2710
|
* `down()` would not have meaningful retention bounds.
|
|
2709
2711
|
*/
|
|
2710
|
-
async function up$
|
|
2712
|
+
async function up$42(db) {
|
|
2711
2713
|
await sql`
|
|
2712
2714
|
ALTER TABLE public.event_log
|
|
2713
2715
|
ADD COLUMN archived_at TIMESTAMPTZ NULL,
|
|
@@ -2723,7 +2725,7 @@ async function up$34(db) {
|
|
|
2723
2725
|
DROP COLUMN IF EXISTS expires_at
|
|
2724
2726
|
`.execute(db);
|
|
2725
2727
|
}
|
|
2726
|
-
async function down$
|
|
2728
|
+
async function down$42(db) {
|
|
2727
2729
|
await sql`
|
|
2728
2730
|
ALTER TABLE public.event_log
|
|
2729
2731
|
ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
|
|
@@ -2742,8 +2744,8 @@ async function down$34(db) {
|
|
|
2742
2744
|
//#endregion
|
|
2743
2745
|
//#region src/db/migrations/009_access_log_trigram.ts
|
|
2744
2746
|
var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
|
|
2745
|
-
down: () => down$
|
|
2746
|
-
up: () => up$
|
|
2747
|
+
down: () => down$41,
|
|
2748
|
+
up: () => up$41
|
|
2747
2749
|
});
|
|
2748
2750
|
/**
|
|
2749
2751
|
* Trigram (pg_trgm) index on access_log.error_message for the federated
|
|
@@ -2756,7 +2758,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
|
|
|
2756
2758
|
* EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
|
|
2757
2759
|
* migrations inside a transaction; the lock is brief on a sampled table.
|
|
2758
2760
|
*/
|
|
2759
|
-
async function up$
|
|
2761
|
+
async function up$41(db) {
|
|
2760
2762
|
await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
|
|
2761
2763
|
await sql`
|
|
2762
2764
|
CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
|
|
@@ -2765,14 +2767,14 @@ async function up$33(db) {
|
|
|
2765
2767
|
WHERE error_message IS NOT NULL
|
|
2766
2768
|
`.execute(db);
|
|
2767
2769
|
}
|
|
2768
|
-
async function down$
|
|
2770
|
+
async function down$41(db) {
|
|
2769
2771
|
await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
|
|
2770
2772
|
}
|
|
2771
2773
|
//#endregion
|
|
2772
2774
|
//#region src/db/migrations/010_cold_store_chunks.ts
|
|
2773
2775
|
var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
|
|
2774
|
-
down: () => down$
|
|
2775
|
-
up: () => up$
|
|
2776
|
+
down: () => down$40,
|
|
2777
|
+
up: () => up$40
|
|
2776
2778
|
});
|
|
2777
2779
|
/**
|
|
2778
2780
|
* Cold-store chunk index — Phase 2 (cold-store purge).
|
|
@@ -2804,7 +2806,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
|
|
|
2804
2806
|
* forever. Adapters that don't opt into per-bucket archival via
|
|
2805
2807
|
* `coldTtlDays` don't insert here either.
|
|
2806
2808
|
*/
|
|
2807
|
-
async function up$
|
|
2809
|
+
async function up$40(db) {
|
|
2808
2810
|
await sql`
|
|
2809
2811
|
CREATE TABLE public.cold_store_chunks (
|
|
2810
2812
|
db TEXT NOT NULL,
|
|
@@ -2831,14 +2833,14 @@ async function up$32(db) {
|
|
|
2831
2833
|
ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
|
|
2832
2834
|
`.execute(db);
|
|
2833
2835
|
}
|
|
2834
|
-
async function down$
|
|
2836
|
+
async function down$40(db) {
|
|
2835
2837
|
await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
|
|
2836
2838
|
}
|
|
2837
2839
|
//#endregion
|
|
2838
2840
|
//#region src/db/migrations/011_drop_source_secrets_notify.ts
|
|
2839
2841
|
var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
|
|
2840
|
-
down: () => down$
|
|
2841
|
-
up: () => up$
|
|
2842
|
+
down: () => down$39,
|
|
2843
|
+
up: () => up$39
|
|
2842
2844
|
});
|
|
2843
2845
|
/**
|
|
2844
2846
|
* Drop the `source_secrets_change_trigger` and the
|
|
@@ -2857,11 +2859,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
|
|
|
2857
2859
|
* in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
|
|
2858
2860
|
* is restored, so this migration is safe to roll back.
|
|
2859
2861
|
*/
|
|
2860
|
-
async function up$
|
|
2862
|
+
async function up$39(db) {
|
|
2861
2863
|
await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
|
|
2862
2864
|
await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
|
|
2863
2865
|
}
|
|
2864
|
-
async function down$
|
|
2866
|
+
async function down$39(db) {
|
|
2865
2867
|
await sql`
|
|
2866
2868
|
CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
|
|
2867
2869
|
LANGUAGE plpgsql
|
|
@@ -2900,8 +2902,8 @@ async function down$31(db) {
|
|
|
2900
2902
|
//#endregion
|
|
2901
2903
|
//#region src/db/migrations/012_peer_credentials_active_uniq.ts
|
|
2902
2904
|
var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
|
|
2903
|
-
down: () => down$
|
|
2904
|
-
up: () => up$
|
|
2905
|
+
down: () => down$38,
|
|
2906
|
+
up: () => up$38
|
|
2905
2907
|
});
|
|
2906
2908
|
/**
|
|
2907
2909
|
* Add a partial unique index on `peer_credentials (instance_id) WHERE
|
|
@@ -2927,7 +2929,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
|
|
|
2927
2929
|
* `down()` only drops the index; it does NOT undo the dedupe (there's no
|
|
2928
2930
|
* safe way to recreate revoked rows, and the dedupe is monotonic).
|
|
2929
2931
|
*/
|
|
2930
|
-
async function up$
|
|
2932
|
+
async function up$38(db) {
|
|
2931
2933
|
await sql`
|
|
2932
2934
|
UPDATE public.peer_credentials
|
|
2933
2935
|
SET revoked_at = NOW()
|
|
@@ -2945,14 +2947,14 @@ async function up$30(db) {
|
|
|
2945
2947
|
WHERE revoked_at IS NULL
|
|
2946
2948
|
`.execute(db);
|
|
2947
2949
|
}
|
|
2948
|
-
async function down$
|
|
2950
|
+
async function down$38(db) {
|
|
2949
2951
|
await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
|
|
2950
2952
|
}
|
|
2951
2953
|
//#endregion
|
|
2952
2954
|
//#region src/db/migrations/013_execution_log_bytes.ts
|
|
2953
2955
|
var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
|
|
2954
|
-
down: () => down$
|
|
2955
|
-
up: () => up$
|
|
2956
|
+
down: () => down$37,
|
|
2957
|
+
up: () => up$37
|
|
2956
2958
|
});
|
|
2957
2959
|
/**
|
|
2958
2960
|
* Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
|
|
@@ -2971,7 +2973,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
|
|
|
2971
2973
|
*
|
|
2972
2974
|
* Idempotent (`ADD COLUMN IF NOT EXISTS`).
|
|
2973
2975
|
*/
|
|
2974
|
-
async function up$
|
|
2976
|
+
async function up$37(db) {
|
|
2975
2977
|
await sql`
|
|
2976
2978
|
ALTER TABLE public.execution_runs
|
|
2977
2979
|
ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
|
|
@@ -2981,15 +2983,15 @@ async function up$29(db) {
|
|
|
2981
2983
|
ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
|
|
2982
2984
|
`.execute(db);
|
|
2983
2985
|
}
|
|
2984
|
-
async function down$
|
|
2986
|
+
async function down$37(db) {
|
|
2985
2987
|
await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
|
|
2986
2988
|
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
|
|
2987
2989
|
}
|
|
2988
2990
|
//#endregion
|
|
2989
2991
|
//#region src/db/migrations/014_kici_events_lease_retry.ts
|
|
2990
2992
|
var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
|
|
2991
|
-
down: () => down$
|
|
2992
|
-
up: () => up$
|
|
2993
|
+
down: () => down$36,
|
|
2994
|
+
up: () => up$36
|
|
2993
2995
|
});
|
|
2994
2996
|
/**
|
|
2995
2997
|
* Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
|
|
@@ -3023,7 +3025,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
|
|
|
3023
3025
|
*
|
|
3024
3026
|
* Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
|
|
3025
3027
|
*/
|
|
3026
|
-
async function up$
|
|
3028
|
+
async function up$36(db) {
|
|
3027
3029
|
await sql`
|
|
3028
3030
|
ALTER TABLE public.kici_events
|
|
3029
3031
|
ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
|
|
@@ -3054,7 +3056,7 @@ async function up$28(db) {
|
|
|
3054
3056
|
WHERE dlq_at IS NOT NULL
|
|
3055
3057
|
`.execute(db);
|
|
3056
3058
|
}
|
|
3057
|
-
async function down$
|
|
3059
|
+
async function down$36(db) {
|
|
3058
3060
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
|
|
3059
3061
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
|
|
3060
3062
|
await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
|
|
@@ -3072,8 +3074,8 @@ async function down$28(db) {
|
|
|
3072
3074
|
//#endregion
|
|
3073
3075
|
//#region src/db/migrations/015_org_settings_customer_scoped.ts
|
|
3074
3076
|
var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
|
|
3075
|
-
down: () => down$
|
|
3076
|
-
up: () => up$
|
|
3077
|
+
down: () => down$35,
|
|
3078
|
+
up: () => up$35
|
|
3077
3079
|
});
|
|
3078
3080
|
/**
|
|
3079
3081
|
* Org-scope `org_settings` and qualify each glob entry by source.
|
|
@@ -3101,7 +3103,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
|
|
|
3101
3103
|
* Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
|
|
3102
3104
|
* and the list columns are already jsonb, so it is a no-op.
|
|
3103
3105
|
*/
|
|
3104
|
-
async function up$
|
|
3106
|
+
async function up$35(db) {
|
|
3105
3107
|
if ((await sql`
|
|
3106
3108
|
SELECT EXISTS (
|
|
3107
3109
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3226,7 +3228,7 @@ async function up$27(db) {
|
|
|
3226
3228
|
await sql`DROP TABLE _org_settings_merged`.execute(db);
|
|
3227
3229
|
await sql`DROP TABLE _org_settings_stage`.execute(db);
|
|
3228
3230
|
}
|
|
3229
|
-
async function down$
|
|
3231
|
+
async function down$35(db) {
|
|
3230
3232
|
if (!(await sql`
|
|
3231
3233
|
SELECT EXISTS (
|
|
3232
3234
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3251,8 +3253,8 @@ async function down$27(db) {
|
|
|
3251
3253
|
//#endregion
|
|
3252
3254
|
//#region src/db/migrations/016_org_settings_allow_http_npm.ts
|
|
3253
3255
|
var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
|
|
3254
|
-
down: () => down$
|
|
3255
|
-
up: () => up$
|
|
3256
|
+
down: () => down$34,
|
|
3257
|
+
up: () => up$34
|
|
3256
3258
|
});
|
|
3257
3259
|
/**
|
|
3258
3260
|
* Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
|
|
@@ -3265,7 +3267,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
|
|
|
3265
3267
|
*
|
|
3266
3268
|
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
3267
3269
|
*/
|
|
3268
|
-
async function up$
|
|
3270
|
+
async function up$34(db) {
|
|
3269
3271
|
if ((await sql`
|
|
3270
3272
|
SELECT EXISTS (
|
|
3271
3273
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3279,7 +3281,7 @@ async function up$26(db) {
|
|
|
3279
3281
|
ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
|
|
3280
3282
|
`.execute(db);
|
|
3281
3283
|
}
|
|
3282
|
-
async function down$
|
|
3284
|
+
async function down$34(db) {
|
|
3283
3285
|
await sql`
|
|
3284
3286
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
|
|
3285
3287
|
`.execute(db);
|
|
@@ -3287,8 +3289,8 @@ async function down$26(db) {
|
|
|
3287
3289
|
//#endregion
|
|
3288
3290
|
//#region src/db/migrations/017_org_id_widen.ts
|
|
3289
3291
|
var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
|
|
3290
|
-
down: () => down$
|
|
3291
|
-
up: () => up$
|
|
3292
|
+
down: () => down$33,
|
|
3293
|
+
up: () => up$33
|
|
3292
3294
|
});
|
|
3293
3295
|
/**
|
|
3294
3296
|
* Widen every orchestrator-side `org_id varchar(12)` column to
|
|
@@ -3328,17 +3330,17 @@ const ORG_ID_TABLES$1 = [
|
|
|
3328
3330
|
"held_runs",
|
|
3329
3331
|
"scoped_secrets"
|
|
3330
3332
|
];
|
|
3331
|
-
async function up$
|
|
3333
|
+
async function up$33(db) {
|
|
3332
3334
|
for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
|
|
3333
3335
|
}
|
|
3334
|
-
async function down$
|
|
3336
|
+
async function down$33(db) {
|
|
3335
3337
|
for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
|
|
3336
3338
|
}
|
|
3337
3339
|
//#endregion
|
|
3338
3340
|
//#region src/db/migrations/018_org_id_prefix_backfill.ts
|
|
3339
3341
|
var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
|
|
3340
|
-
down: () => down$
|
|
3341
|
-
up: () => up$
|
|
3342
|
+
down: () => down$32,
|
|
3343
|
+
up: () => up$32
|
|
3342
3344
|
});
|
|
3343
3345
|
/**
|
|
3344
3346
|
* Prefix every orchestrator-side tenant string with `org_` to align
|
|
@@ -3383,19 +3385,19 @@ const CUSTOMER_ID_TABLES = [
|
|
|
3383
3385
|
"workflow_registrations",
|
|
3384
3386
|
"org_settings"
|
|
3385
3387
|
];
|
|
3386
|
-
async function up$
|
|
3388
|
+
async function up$32(db) {
|
|
3387
3389
|
for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = 'org_' || org_id WHERE org_id <> 'kici-admin' AND org_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3388
3390
|
for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = 'org_' || customer_id WHERE customer_id <> 'kici-admin' AND customer_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3389
3391
|
}
|
|
3390
|
-
async function down$
|
|
3392
|
+
async function down$32(db) {
|
|
3391
3393
|
for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = substring(customer_id from 5) WHERE customer_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3392
3394
|
for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = substring(org_id from 5) WHERE org_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
|
|
3393
3395
|
}
|
|
3394
3396
|
//#endregion
|
|
3395
3397
|
//#region src/db/migrations/019_generic_sources_change_notify.ts
|
|
3396
3398
|
var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
|
|
3397
|
-
down: () => down$
|
|
3398
|
-
up: () => up$
|
|
3399
|
+
down: () => down$31,
|
|
3400
|
+
up: () => up$31
|
|
3399
3401
|
});
|
|
3400
3402
|
/**
|
|
3401
3403
|
* Add a Postgres trigger on `generic_webhook_sources` that emits
|
|
@@ -3418,7 +3420,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
|
|
|
3418
3420
|
* (`notify_sources_change()` + `sources_change_trigger`, defined in
|
|
3419
3421
|
* `001_initial.ts`).
|
|
3420
3422
|
*/
|
|
3421
|
-
async function up$
|
|
3423
|
+
async function up$31(db) {
|
|
3422
3424
|
await sql`
|
|
3423
3425
|
CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
|
|
3424
3426
|
LANGUAGE plpgsql
|
|
@@ -3439,15 +3441,15 @@ async function up$23(db) {
|
|
|
3439
3441
|
FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
|
|
3440
3442
|
`.execute(db);
|
|
3441
3443
|
}
|
|
3442
|
-
async function down$
|
|
3444
|
+
async function down$31(db) {
|
|
3443
3445
|
await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
|
|
3444
3446
|
await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
|
|
3445
3447
|
}
|
|
3446
3448
|
//#endregion
|
|
3447
3449
|
//#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
|
|
3448
3450
|
var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
|
|
3449
|
-
down: () => down$
|
|
3450
|
-
up: () => up$
|
|
3451
|
+
down: () => down$30,
|
|
3452
|
+
up: () => up$30
|
|
3451
3453
|
});
|
|
3452
3454
|
/**
|
|
3453
3455
|
* Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
|
|
@@ -3462,7 +3464,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
|
|
|
3462
3464
|
*
|
|
3463
3465
|
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
3464
3466
|
*/
|
|
3465
|
-
async function up$
|
|
3467
|
+
async function up$30(db) {
|
|
3466
3468
|
if ((await sql`
|
|
3467
3469
|
SELECT EXISTS (
|
|
3468
3470
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3476,7 +3478,7 @@ async function up$22(db) {
|
|
|
3476
3478
|
ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3477
3479
|
`.execute(db);
|
|
3478
3480
|
}
|
|
3479
|
-
async function down$
|
|
3481
|
+
async function down$30(db) {
|
|
3480
3482
|
await sql`
|
|
3481
3483
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
|
|
3482
3484
|
`.execute(db);
|
|
@@ -3484,8 +3486,8 @@ async function down$22(db) {
|
|
|
3484
3486
|
//#endregion
|
|
3485
3487
|
//#region src/db/migrations/021_check_run_tracking.ts
|
|
3486
3488
|
var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
|
|
3487
|
-
down: () => down$
|
|
3488
|
-
up: () => up$
|
|
3489
|
+
down: () => down$29,
|
|
3490
|
+
up: () => up$29
|
|
3489
3491
|
});
|
|
3490
3492
|
/**
|
|
3491
3493
|
* Add `check_run_tracking` table for HA-safe check-run state persistence.
|
|
@@ -3509,7 +3511,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
|
|
|
3509
3511
|
*
|
|
3510
3512
|
* Idempotent: a re-run on a DB that already has the table is a no-op.
|
|
3511
3513
|
*/
|
|
3512
|
-
async function up$
|
|
3514
|
+
async function up$29(db) {
|
|
3513
3515
|
if ((await sql`
|
|
3514
3516
|
SELECT EXISTS (
|
|
3515
3517
|
SELECT 1 FROM information_schema.tables
|
|
@@ -3540,14 +3542,14 @@ async function up$21(db) {
|
|
|
3540
3542
|
WHERE run_id IS NOT NULL
|
|
3541
3543
|
`.execute(db);
|
|
3542
3544
|
}
|
|
3543
|
-
async function down$
|
|
3545
|
+
async function down$29(db) {
|
|
3544
3546
|
await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
|
|
3545
3547
|
}
|
|
3546
3548
|
//#endregion
|
|
3547
3549
|
//#region src/db/migrations/022_scaler_manager_state.ts
|
|
3548
3550
|
var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
|
|
3549
|
-
down: () => down$
|
|
3550
|
-
up: () => up$
|
|
3551
|
+
down: () => down$28,
|
|
3552
|
+
up: () => up$28
|
|
3551
3553
|
});
|
|
3552
3554
|
/**
|
|
3553
3555
|
* Add three tables persisting `ScalerManager` per-coord state:
|
|
@@ -3574,7 +3576,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
|
|
|
3574
3576
|
* Idempotent: a re-run on a DB that already has any of these tables
|
|
3575
3577
|
* leaves the existing one alone.
|
|
3576
3578
|
*/
|
|
3577
|
-
async function up$
|
|
3579
|
+
async function up$28(db) {
|
|
3578
3580
|
const tableExists = async (name) => {
|
|
3579
3581
|
return (await sql`
|
|
3580
3582
|
SELECT EXISTS (
|
|
@@ -3624,7 +3626,7 @@ async function up$20(db) {
|
|
|
3624
3626
|
`.execute(db);
|
|
3625
3627
|
}
|
|
3626
3628
|
}
|
|
3627
|
-
async function down$
|
|
3629
|
+
async function down$28(db) {
|
|
3628
3630
|
await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
|
|
3629
3631
|
await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
|
|
3630
3632
|
await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
|
|
@@ -3632,8 +3634,8 @@ async function down$20(db) {
|
|
|
3632
3634
|
//#endregion
|
|
3633
3635
|
//#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
|
|
3634
3636
|
var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
|
|
3635
|
-
down: () => down$
|
|
3636
|
-
up: () => up$
|
|
3637
|
+
down: () => down$27,
|
|
3638
|
+
up: () => up$27
|
|
3637
3639
|
});
|
|
3638
3640
|
/**
|
|
3639
3641
|
* Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
|
|
@@ -3657,7 +3659,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
|
|
|
3657
3659
|
* Idempotent: re-running on a DB that already has either column is a
|
|
3658
3660
|
* no-op.
|
|
3659
3661
|
*/
|
|
3660
|
-
async function up$
|
|
3662
|
+
async function up$27(db) {
|
|
3661
3663
|
const colExists = async (name) => {
|
|
3662
3664
|
return (await sql`
|
|
3663
3665
|
SELECT EXISTS (
|
|
@@ -3682,7 +3684,7 @@ async function up$19(db) {
|
|
|
3682
3684
|
WHERE recovery_deadline IS NOT NULL
|
|
3683
3685
|
`.execute(db);
|
|
3684
3686
|
}
|
|
3685
|
-
async function down$
|
|
3687
|
+
async function down$27(db) {
|
|
3686
3688
|
await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
|
|
3687
3689
|
await sql`
|
|
3688
3690
|
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
|
|
@@ -3694,8 +3696,8 @@ async function down$19(db) {
|
|
|
3694
3696
|
//#endregion
|
|
3695
3697
|
//#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
|
|
3696
3698
|
var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
|
|
3697
|
-
down: () => down$
|
|
3698
|
-
up: () => up$
|
|
3699
|
+
down: () => down$26,
|
|
3700
|
+
up: () => up$26
|
|
3699
3701
|
});
|
|
3700
3702
|
/**
|
|
3701
3703
|
* Add `dispatch_queue.last_provisioning_error TEXT` recording the most
|
|
@@ -3711,7 +3713,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
|
|
|
3711
3713
|
*
|
|
3712
3714
|
* Idempotent: re-running on a DB that already has the column is a no-op.
|
|
3713
3715
|
*/
|
|
3714
|
-
async function up$
|
|
3716
|
+
async function up$26(db) {
|
|
3715
3717
|
const colExists = async (name) => {
|
|
3716
3718
|
return (await sql`
|
|
3717
3719
|
SELECT EXISTS (
|
|
@@ -3727,7 +3729,7 @@ async function up$18(db) {
|
|
|
3727
3729
|
ADD COLUMN last_provisioning_error TEXT
|
|
3728
3730
|
`.execute(db);
|
|
3729
3731
|
}
|
|
3730
|
-
async function down$
|
|
3732
|
+
async function down$26(db) {
|
|
3731
3733
|
await sql`
|
|
3732
3734
|
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
|
|
3733
3735
|
`.execute(db);
|
|
@@ -3735,8 +3737,8 @@ async function down$18(db) {
|
|
|
3735
3737
|
//#endregion
|
|
3736
3738
|
//#region src/db/migrations/025_init_failure.ts
|
|
3737
3739
|
var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
|
|
3738
|
-
down: () => down$
|
|
3739
|
-
up: () => up$
|
|
3740
|
+
down: () => down$25,
|
|
3741
|
+
up: () => up$25
|
|
3740
3742
|
});
|
|
3741
3743
|
/**
|
|
3742
3744
|
* Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
|
|
@@ -3750,7 +3752,7 @@ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
|
|
|
3750
3752
|
* Idempotent: re-running on a DB that already has either column is a no-op
|
|
3751
3753
|
* for that column.
|
|
3752
3754
|
*/
|
|
3753
|
-
async function up$
|
|
3755
|
+
async function up$25(db) {
|
|
3754
3756
|
const colExists = async (table, name) => {
|
|
3755
3757
|
return (await sql`
|
|
3756
3758
|
SELECT EXISTS (
|
|
@@ -3770,7 +3772,7 @@ async function up$17(db) {
|
|
|
3770
3772
|
ADD COLUMN init_failure JSONB DEFAULT NULL
|
|
3771
3773
|
`.execute(db);
|
|
3772
3774
|
}
|
|
3773
|
-
async function down$
|
|
3775
|
+
async function down$25(db) {
|
|
3774
3776
|
await sql`
|
|
3775
3777
|
ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
|
|
3776
3778
|
`.execute(db);
|
|
@@ -3781,8 +3783,8 @@ async function down$17(db) {
|
|
|
3781
3783
|
//#endregion
|
|
3782
3784
|
//#region src/db/migrations/026_event_log_lockfile_corrupt.ts
|
|
3783
3785
|
var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
|
|
3784
|
-
down: () => down$
|
|
3785
|
-
up: () => up$
|
|
3786
|
+
down: () => down$24,
|
|
3787
|
+
up: () => up$24
|
|
3786
3788
|
});
|
|
3787
3789
|
/**
|
|
3788
3790
|
* Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
|
|
@@ -3791,7 +3793,7 @@ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
|
|
|
3791
3793
|
*
|
|
3792
3794
|
* Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
|
|
3793
3795
|
*/
|
|
3794
|
-
async function up$
|
|
3796
|
+
async function up$24(db) {
|
|
3795
3797
|
await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
|
|
3796
3798
|
await sql`
|
|
3797
3799
|
ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
|
|
@@ -3801,7 +3803,7 @@ async function up$16(db) {
|
|
|
3801
3803
|
])))
|
|
3802
3804
|
`.execute(db);
|
|
3803
3805
|
}
|
|
3804
|
-
async function down$
|
|
3806
|
+
async function down$24(db) {
|
|
3805
3807
|
await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
|
|
3806
3808
|
await sql`
|
|
3807
3809
|
ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
|
|
@@ -3814,8 +3816,8 @@ async function down$16(db) {
|
|
|
3814
3816
|
//#endregion
|
|
3815
3817
|
//#region src/db/migrations/027_workflow_timeout.ts
|
|
3816
3818
|
var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
|
|
3817
|
-
down: () => down$
|
|
3818
|
-
up: () => up$
|
|
3819
|
+
down: () => down$23,
|
|
3820
|
+
up: () => up$23
|
|
3819
3821
|
});
|
|
3820
3822
|
/**
|
|
3821
3823
|
* Add `workflow_timeout_ms integer` to `execution_runs`.
|
|
@@ -3833,13 +3835,13 @@ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
|
|
|
3833
3835
|
*
|
|
3834
3836
|
* Idempotent: re-running on a DB that already has the column is a no-op.
|
|
3835
3837
|
*/
|
|
3836
|
-
async function up$
|
|
3838
|
+
async function up$23(db) {
|
|
3837
3839
|
await sql`
|
|
3838
3840
|
ALTER TABLE public.execution_runs
|
|
3839
3841
|
ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
|
|
3840
3842
|
`.execute(db);
|
|
3841
3843
|
}
|
|
3842
|
-
async function down$
|
|
3844
|
+
async function down$23(db) {
|
|
3843
3845
|
await sql`
|
|
3844
3846
|
ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
|
|
3845
3847
|
`.execute(db);
|
|
@@ -3847,8 +3849,8 @@ async function down$15(db) {
|
|
|
3847
3849
|
//#endregion
|
|
3848
3850
|
//#region src/db/migrations/028_org_settings_user_cache.ts
|
|
3849
3851
|
var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
|
|
3850
|
-
down: () => down$
|
|
3851
|
-
up: () => up$
|
|
3852
|
+
down: () => down$22,
|
|
3853
|
+
up: () => up$22
|
|
3852
3854
|
});
|
|
3853
3855
|
/**
|
|
3854
3856
|
* Add `org_settings.user_cache_quota_bytes bigint` and
|
|
@@ -3877,7 +3879,7 @@ async function columnExists(db, column) {
|
|
|
3877
3879
|
) AS exists
|
|
3878
3880
|
`.execute(db)).rows[0]?.exists ?? false;
|
|
3879
3881
|
}
|
|
3880
|
-
async function up$
|
|
3882
|
+
async function up$22(db) {
|
|
3881
3883
|
if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
|
|
3882
3884
|
ALTER TABLE public.org_settings
|
|
3883
3885
|
ADD COLUMN user_cache_quota_bytes bigint
|
|
@@ -3887,7 +3889,7 @@ async function up$14(db) {
|
|
|
3887
3889
|
ADD COLUMN user_cache_ttl_ms bigint
|
|
3888
3890
|
`.execute(db);
|
|
3889
3891
|
}
|
|
3890
|
-
async function down$
|
|
3892
|
+
async function down$22(db) {
|
|
3891
3893
|
await sql`
|
|
3892
3894
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
|
|
3893
3895
|
`.execute(db);
|
|
@@ -3898,8 +3900,8 @@ async function down$14(db) {
|
|
|
3898
3900
|
//#endregion
|
|
3899
3901
|
//#region src/db/migrations/029_dispatch_queue_attempts.ts
|
|
3900
3902
|
var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
|
|
3901
|
-
down: () => down$
|
|
3902
|
-
up: () => up$
|
|
3903
|
+
down: () => down$21,
|
|
3904
|
+
up: () => up$21
|
|
3903
3905
|
});
|
|
3904
3906
|
/**
|
|
3905
3907
|
* Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
|
|
@@ -3913,7 +3915,7 @@ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
|
|
|
3913
3915
|
*
|
|
3914
3916
|
* Idempotent: re-running on a DB that already has the column is a no-op.
|
|
3915
3917
|
*/
|
|
3916
|
-
async function up$
|
|
3918
|
+
async function up$21(db) {
|
|
3917
3919
|
if (!((await sql`
|
|
3918
3920
|
SELECT EXISTS (
|
|
3919
3921
|
SELECT 1 FROM information_schema.columns
|
|
@@ -3926,7 +3928,7 @@ async function up$13(db) {
|
|
|
3926
3928
|
ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
|
|
3927
3929
|
`.execute(db);
|
|
3928
3930
|
}
|
|
3929
|
-
async function down$
|
|
3931
|
+
async function down$21(db) {
|
|
3930
3932
|
await sql`
|
|
3931
3933
|
ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
|
|
3932
3934
|
`.execute(db);
|
|
@@ -3934,8 +3936,8 @@ async function down$13(db) {
|
|
|
3934
3936
|
//#endregion
|
|
3935
3937
|
//#region src/db/migrations/030_held_runs_env_set_null.ts
|
|
3936
3938
|
var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
|
|
3937
|
-
down: () => down$
|
|
3938
|
-
up: () => up$
|
|
3939
|
+
down: () => down$20,
|
|
3940
|
+
up: () => up$20
|
|
3939
3941
|
});
|
|
3940
3942
|
/**
|
|
3941
3943
|
* held_runs.environment_id becomes nullable with ON DELETE SET NULL so
|
|
@@ -3946,14 +3948,14 @@ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
|
|
|
3946
3948
|
* Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
|
|
3947
3949
|
* re-run, and the constraint is re-created with the SET NULL action.
|
|
3948
3950
|
*/
|
|
3949
|
-
async function up$
|
|
3951
|
+
async function up$20(db) {
|
|
3950
3952
|
await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
|
|
3951
3953
|
await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
|
|
3952
3954
|
await sql`ALTER TABLE public.held_runs
|
|
3953
3955
|
ADD CONSTRAINT held_runs_environment_id_fkey
|
|
3954
3956
|
FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
|
|
3955
3957
|
}
|
|
3956
|
-
async function down$
|
|
3958
|
+
async function down$20(db) {
|
|
3957
3959
|
await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
|
|
3958
3960
|
await sql`ALTER TABLE public.held_runs
|
|
3959
3961
|
ADD CONSTRAINT held_runs_environment_id_fkey
|
|
@@ -3963,8 +3965,8 @@ async function down$12(db) {
|
|
|
3963
3965
|
//#endregion
|
|
3964
3966
|
//#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
|
|
3965
3967
|
var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
|
|
3966
|
-
down: () => down$
|
|
3967
|
-
up: () => up$
|
|
3968
|
+
down: () => down$19,
|
|
3969
|
+
up: () => up$19
|
|
3968
3970
|
});
|
|
3969
3971
|
/**
|
|
3970
3972
|
* Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
|
|
@@ -3981,7 +3983,7 @@ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
|
|
|
3981
3983
|
*
|
|
3982
3984
|
* Idempotent: re-running on a DB that already has either column is a no-op.
|
|
3983
3985
|
*/
|
|
3984
|
-
async function up$
|
|
3986
|
+
async function up$19(db) {
|
|
3985
3987
|
const colExists = async (name) => {
|
|
3986
3988
|
return (await sql`
|
|
3987
3989
|
SELECT EXISTS (
|
|
@@ -4006,7 +4008,7 @@ async function up$11(db) {
|
|
|
4006
4008
|
WHERE ack_deadline IS NOT NULL
|
|
4007
4009
|
`.execute(db);
|
|
4008
4010
|
}
|
|
4009
|
-
async function down$
|
|
4011
|
+
async function down$19(db) {
|
|
4010
4012
|
await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
|
|
4011
4013
|
await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
|
|
4012
4014
|
await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
|
|
@@ -4014,8 +4016,8 @@ async function down$11(db) {
|
|
|
4014
4016
|
//#endregion
|
|
4015
4017
|
//#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
|
|
4016
4018
|
var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
|
|
4017
|
-
down: () => down$
|
|
4018
|
-
up: () => up$
|
|
4019
|
+
down: () => down$18,
|
|
4020
|
+
up: () => up$18
|
|
4019
4021
|
});
|
|
4020
4022
|
/**
|
|
4021
4023
|
* Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
|
|
@@ -4027,7 +4029,7 @@ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll
|
|
|
4027
4029
|
*
|
|
4028
4030
|
* Idempotent: a re-run on a DB that already has the column is a no-op.
|
|
4029
4031
|
*/
|
|
4030
|
-
async function up$
|
|
4032
|
+
async function up$18(db) {
|
|
4031
4033
|
if ((await sql`
|
|
4032
4034
|
SELECT EXISTS (
|
|
4033
4035
|
SELECT 1 FROM information_schema.columns
|
|
@@ -4041,7 +4043,7 @@ async function up$10(db) {
|
|
|
4041
4043
|
ADD COLUMN dispatch_ack_timeout_ms BIGINT
|
|
4042
4044
|
`.execute(db);
|
|
4043
4045
|
}
|
|
4044
|
-
async function down$
|
|
4046
|
+
async function down$18(db) {
|
|
4045
4047
|
await sql`
|
|
4046
4048
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
|
|
4047
4049
|
`.execute(db);
|
|
@@ -4049,8 +4051,8 @@ async function down$10(db) {
|
|
|
4049
4051
|
//#endregion
|
|
4050
4052
|
//#region src/db/migrations/033_org_settings_approval.ts
|
|
4051
4053
|
var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
|
|
4052
|
-
down: () => down$
|
|
4053
|
-
up: () => up$
|
|
4054
|
+
down: () => down$17,
|
|
4055
|
+
up: () => up$17
|
|
4054
4056
|
});
|
|
4055
4057
|
/**
|
|
4056
4058
|
* Add the two approval-policy columns to `org_settings`:
|
|
@@ -4067,7 +4069,7 @@ var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
|
|
|
4067
4069
|
* and the orchestrator admin route. Idempotent: a re-run on a DB that already
|
|
4068
4070
|
* has the columns is a no-op (each column is guarded independently).
|
|
4069
4071
|
*/
|
|
4070
|
-
async function up$
|
|
4072
|
+
async function up$17(db) {
|
|
4071
4073
|
const colExists = async (column) => {
|
|
4072
4074
|
return (await sql`
|
|
4073
4075
|
SELECT EXISTS (
|
|
@@ -4087,7 +4089,7 @@ async function up$9(db) {
|
|
|
4087
4089
|
ADD COLUMN allow_self_approval BOOLEAN NOT NULL DEFAULT true
|
|
4088
4090
|
`.execute(db);
|
|
4089
4091
|
}
|
|
4090
|
-
async function down$
|
|
4092
|
+
async function down$17(db) {
|
|
4091
4093
|
await sql`
|
|
4092
4094
|
ALTER TABLE public.org_settings DROP COLUMN IF EXISTS approval_expiry_seconds
|
|
4093
4095
|
`.execute(db);
|
|
@@ -4098,8 +4100,8 @@ async function down$9(db) {
|
|
|
4098
4100
|
//#endregion
|
|
4099
4101
|
//#region src/db/migrations/034_held_runs_generalize.ts
|
|
4100
4102
|
var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
|
|
4101
|
-
down: () => down$
|
|
4102
|
-
up: () => up$
|
|
4103
|
+
down: () => down$16,
|
|
4104
|
+
up: () => up$16
|
|
4103
4105
|
});
|
|
4104
4106
|
/**
|
|
4105
4107
|
* Generalize `held_runs` from an environment-only hold into the unified
|
|
@@ -4121,7 +4123,7 @@ var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
|
|
|
4121
4123
|
* New `held_run_approvals` table: one row per approver decision, FK to
|
|
4122
4124
|
* `held_runs.id` (uuid) with ON DELETE CASCADE.
|
|
4123
4125
|
*/
|
|
4124
|
-
async function up$
|
|
4126
|
+
async function up$16(db) {
|
|
4125
4127
|
const colExists = async (column) => {
|
|
4126
4128
|
return (await sql`
|
|
4127
4129
|
SELECT EXISTS (
|
|
@@ -4154,7 +4156,7 @@ async function up$8(db) {
|
|
|
4154
4156
|
ON public.held_run_approvals USING btree (held_run_id)
|
|
4155
4157
|
`.execute(db);
|
|
4156
4158
|
}
|
|
4157
|
-
async function down$
|
|
4159
|
+
async function down$16(db) {
|
|
4158
4160
|
await sql`DROP TABLE IF EXISTS public.held_run_approvals`.execute(db);
|
|
4159
4161
|
await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS approval_requirement`.execute(db);
|
|
4160
4162
|
await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS trigger_source`.execute(db);
|
|
@@ -4164,8 +4166,8 @@ async function down$8(db) {
|
|
|
4164
4166
|
//#endregion
|
|
4165
4167
|
//#region src/db/migrations/035_pending_workflow_contexts.ts
|
|
4166
4168
|
var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
|
|
4167
|
-
down: () => down$
|
|
4168
|
-
up: () => up$
|
|
4169
|
+
down: () => down$15,
|
|
4170
|
+
up: () => up$15
|
|
4169
4171
|
});
|
|
4170
4172
|
/**
|
|
4171
4173
|
* Pending workflow dispatch context — backs resume of a workflow whose install
|
|
@@ -4174,7 +4176,7 @@ var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
|
|
|
4174
4176
|
* wait-timer expiry, concurrency slot free). The row is deleted once the resume
|
|
4175
4177
|
* dispatch has been kicked off.
|
|
4176
4178
|
*/
|
|
4177
|
-
async function up$
|
|
4179
|
+
async function up$15(db) {
|
|
4178
4180
|
await sql`
|
|
4179
4181
|
CREATE TABLE IF NOT EXISTS public.pending_workflow_contexts (
|
|
4180
4182
|
run_id text PRIMARY KEY,
|
|
@@ -4184,14 +4186,14 @@ async function up$7(db) {
|
|
|
4184
4186
|
)
|
|
4185
4187
|
`.execute(db);
|
|
4186
4188
|
}
|
|
4187
|
-
async function down$
|
|
4189
|
+
async function down$15(db) {
|
|
4188
4190
|
await sql`DROP TABLE IF EXISTS public.pending_workflow_contexts`.execute(db);
|
|
4189
4191
|
}
|
|
4190
4192
|
//#endregion
|
|
4191
4193
|
//#region src/db/migrations/036_attestations.ts
|
|
4192
4194
|
var _036_attestations_exports = /* @__PURE__ */ __exportAll({
|
|
4193
|
-
down: () => down$
|
|
4194
|
-
up: () => up$
|
|
4195
|
+
down: () => down$14,
|
|
4196
|
+
up: () => up$14
|
|
4195
4197
|
});
|
|
4196
4198
|
/**
|
|
4197
4199
|
* Add the `attestations` table for build-provenance bundles.
|
|
@@ -4204,7 +4206,7 @@ var _036_attestations_exports = /* @__PURE__ */ __exportAll({
|
|
|
4204
4206
|
*
|
|
4205
4207
|
* Idempotent: a re-run on a DB that already has the table is a no-op.
|
|
4206
4208
|
*/
|
|
4207
|
-
async function up$
|
|
4209
|
+
async function up$14(db) {
|
|
4208
4210
|
if ((await sql`
|
|
4209
4211
|
SELECT EXISTS (
|
|
4210
4212
|
SELECT 1 FROM information_schema.tables
|
|
@@ -4230,14 +4232,14 @@ async function up$6(db) {
|
|
|
4230
4232
|
ON public.attestations (run_id, job_id)
|
|
4231
4233
|
`.execute(db);
|
|
4232
4234
|
}
|
|
4233
|
-
async function down$
|
|
4235
|
+
async function down$14(db) {
|
|
4234
4236
|
await sql`DROP TABLE IF EXISTS public.attestations`.execute(db);
|
|
4235
4237
|
}
|
|
4236
4238
|
//#endregion
|
|
4237
4239
|
//#region src/db/migrations/037_generic_sources_provider_type_local.ts
|
|
4238
4240
|
var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportAll({
|
|
4239
|
-
down: () => down$
|
|
4240
|
-
up: () => up$
|
|
4241
|
+
down: () => down$13,
|
|
4242
|
+
up: () => up$13
|
|
4241
4243
|
});
|
|
4242
4244
|
/**
|
|
4243
4245
|
* Replace the `generic_webhook_sources.provider_type` CHECK constraint so it
|
|
@@ -4254,7 +4256,7 @@ var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportA
|
|
|
4254
4256
|
* Idempotent: the constraint is dropped IF EXISTS and recreated; the data
|
|
4255
4257
|
* backfill is a plain UPDATE that is a no-op once no `'internal'` rows remain.
|
|
4256
4258
|
*/
|
|
4257
|
-
async function up$
|
|
4259
|
+
async function up$13(db) {
|
|
4258
4260
|
await sql`
|
|
4259
4261
|
ALTER TABLE public.generic_webhook_sources
|
|
4260
4262
|
DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
|
|
@@ -4270,7 +4272,7 @@ async function up$5(db) {
|
|
|
4270
4272
|
CHECK (provider_type = ANY (ARRAY['generic'::text, 'local'::text]))
|
|
4271
4273
|
`.execute(db);
|
|
4272
4274
|
}
|
|
4273
|
-
async function down$
|
|
4275
|
+
async function down$13(db) {
|
|
4274
4276
|
await sql`
|
|
4275
4277
|
ALTER TABLE public.generic_webhook_sources
|
|
4276
4278
|
DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
|
|
@@ -4289,8 +4291,8 @@ async function down$5(db) {
|
|
|
4289
4291
|
//#endregion
|
|
4290
4292
|
//#region src/db/migrations/038_remote_sources.ts
|
|
4291
4293
|
var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
|
|
4292
|
-
down: () => down$
|
|
4293
|
-
up: () => up$
|
|
4294
|
+
down: () => down$12,
|
|
4295
|
+
up: () => up$12
|
|
4294
4296
|
});
|
|
4295
4297
|
/**
|
|
4296
4298
|
* `remote_sources` anchors a Platform-relayed `kici run remote` to its real
|
|
@@ -4302,7 +4304,7 @@ var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
|
|
|
4302
4304
|
*
|
|
4303
4305
|
* Idempotent: a re-run on a DB that already has the table is a no-op.
|
|
4304
4306
|
*/
|
|
4305
|
-
async function up$
|
|
4307
|
+
async function up$12(db) {
|
|
4306
4308
|
if ((await sql`
|
|
4307
4309
|
SELECT EXISTS (
|
|
4308
4310
|
SELECT 1 FROM information_schema.tables
|
|
@@ -4321,14 +4323,14 @@ async function up$4(db) {
|
|
|
4321
4323
|
)
|
|
4322
4324
|
`.execute(db);
|
|
4323
4325
|
}
|
|
4324
|
-
async function down$
|
|
4326
|
+
async function down$12(db) {
|
|
4325
4327
|
await sql`DROP TABLE IF EXISTS public.remote_sources`.execute(db);
|
|
4326
4328
|
}
|
|
4327
4329
|
//#endregion
|
|
4328
4330
|
//#region src/db/migrations/039_host_roster.ts
|
|
4329
4331
|
var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
|
|
4330
|
-
down: () => down$
|
|
4331
|
-
up: () => up$
|
|
4332
|
+
down: () => down$11,
|
|
4333
|
+
up: () => up$11
|
|
4332
4334
|
});
|
|
4333
4335
|
/**
|
|
4334
4336
|
* `host_roster` is KiCI's declared inventory: one durable row per agent the
|
|
@@ -4345,7 +4347,7 @@ var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
|
|
|
4345
4347
|
*
|
|
4346
4348
|
* Idempotent: a re-run on a DB that already has the table is a no-op.
|
|
4347
4349
|
*/
|
|
4348
|
-
async function up$
|
|
4350
|
+
async function up$11(db) {
|
|
4349
4351
|
if ((await sql`
|
|
4350
4352
|
SELECT EXISTS (
|
|
4351
4353
|
SELECT 1 FROM information_schema.tables
|
|
@@ -4375,14 +4377,14 @@ async function up$3(db) {
|
|
|
4375
4377
|
await sql`CREATE INDEX idx_host_roster_reap
|
|
4376
4378
|
ON public.host_roster (lifecycle_class, last_seen)`.execute(db);
|
|
4377
4379
|
}
|
|
4378
|
-
async function down$
|
|
4380
|
+
async function down$11(db) {
|
|
4379
4381
|
await sql`DROP TABLE IF EXISTS public.host_roster`.execute(db);
|
|
4380
4382
|
}
|
|
4381
4383
|
//#endregion
|
|
4382
4384
|
//#region src/db/migrations/040_runsonall_pin.ts
|
|
4383
4385
|
var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
|
|
4384
|
-
down: () => down$
|
|
4385
|
-
up: () => up$
|
|
4386
|
+
down: () => down$10,
|
|
4387
|
+
up: () => up$10
|
|
4386
4388
|
});
|
|
4387
4389
|
/**
|
|
4388
4390
|
* Add the `runsOnAll` host fan-out columns:
|
|
@@ -4399,7 +4401,7 @@ var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
|
|
|
4399
4401
|
*
|
|
4400
4402
|
* Idempotent: re-running on a DB that already has any column is a no-op.
|
|
4401
4403
|
*/
|
|
4402
|
-
async function colExists$
|
|
4404
|
+
async function colExists$5(db, table, name) {
|
|
4403
4405
|
return (await sql`
|
|
4404
4406
|
SELECT EXISTS (
|
|
4405
4407
|
SELECT 1 FROM information_schema.columns
|
|
@@ -4409,18 +4411,18 @@ async function colExists$2(db, table, name) {
|
|
|
4409
4411
|
) AS exists
|
|
4410
4412
|
`.execute(db)).rows[0]?.exists ?? false;
|
|
4411
4413
|
}
|
|
4412
|
-
async function up$
|
|
4413
|
-
if (!await colExists$
|
|
4414
|
-
if (!await colExists$
|
|
4415
|
-
if (!await colExists$
|
|
4416
|
-
if (!await colExists$
|
|
4414
|
+
async function up$10(db) {
|
|
4415
|
+
if (!await colExists$5(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
|
|
4416
|
+
if (!await colExists$5(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
|
|
4417
|
+
if (!await colExists$5(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
|
|
4418
|
+
if (!await colExists$5(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
|
|
4417
4419
|
await sql`
|
|
4418
4420
|
CREATE INDEX IF NOT EXISTS idx_dispatch_queue_pinned_agent
|
|
4419
4421
|
ON public.dispatch_queue (pinned_agent_id)
|
|
4420
4422
|
WHERE pinned_agent_id IS NOT NULL
|
|
4421
4423
|
`.execute(db);
|
|
4422
4424
|
}
|
|
4423
|
-
async function down$
|
|
4425
|
+
async function down$10(db) {
|
|
4424
4426
|
await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_pinned_agent`.execute(db);
|
|
4425
4427
|
await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS pinned_agent_id`.execute(db);
|
|
4426
4428
|
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_label`.execute(db);
|
|
@@ -4428,91 +4430,365 @@ async function down$2(db) {
|
|
|
4428
4430
|
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS base_job_name`.execute(db);
|
|
4429
4431
|
}
|
|
4430
4432
|
//#endregion
|
|
4431
|
-
//#region src/db/migrations/041_wave_gated.ts
|
|
4432
|
-
var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
|
|
4433
|
+
//#region src/db/migrations/041_wave_gated.ts
|
|
4434
|
+
var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
|
|
4435
|
+
down: () => down$9,
|
|
4436
|
+
up: () => up$9
|
|
4437
|
+
});
|
|
4438
|
+
/**
|
|
4439
|
+
* Add the rolling fan-out wave-gate columns:
|
|
4440
|
+
*
|
|
4441
|
+
* - `execution_jobs.wave_gated boolean NOT NULL DEFAULT false` — when a fan-out
|
|
4442
|
+
* job declares `maxParallel`, children beyond the sliding window are persisted
|
|
4443
|
+
* `wave_gated=true` (held, not enqueued). The dispatch loop skips them; the
|
|
4444
|
+
* wave-scheduler clears the flag one-per-terminal as siblings complete (or, on
|
|
4445
|
+
* `failFast`, skips the held remainder).
|
|
4446
|
+
* - `execution_jobs.wave_max_parallel int` / `wave_fail_fast boolean` — the
|
|
4447
|
+
* base's wave policy, stamped on every fan-out child so the wave-scheduler can
|
|
4448
|
+
* read it at terminal time without re-fetching the lock file (the tracker has
|
|
4449
|
+
* no lock access). NULL for any job not part of a bounded wave.
|
|
4450
|
+
*
|
|
4451
|
+
* A composite index on (run_id, base_job_name, wave_gated) supports the
|
|
4452
|
+
* wave-scheduler's "next held sibling of this base" lookups.
|
|
4453
|
+
*
|
|
4454
|
+
* Idempotent: re-running on a DB that already has the columns is a no-op.
|
|
4455
|
+
*/
|
|
4456
|
+
async function colExists$4(db, table, name) {
|
|
4457
|
+
return (await sql`
|
|
4458
|
+
SELECT EXISTS (
|
|
4459
|
+
SELECT 1 FROM information_schema.columns
|
|
4460
|
+
WHERE table_schema = 'public'
|
|
4461
|
+
AND table_name = ${table}
|
|
4462
|
+
AND column_name = ${name}
|
|
4463
|
+
) AS exists
|
|
4464
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
4465
|
+
}
|
|
4466
|
+
async function up$9(db) {
|
|
4467
|
+
if (!await colExists$4(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
|
|
4468
|
+
if (!await colExists$4(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
|
|
4469
|
+
if (!await colExists$4(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
|
|
4470
|
+
await sql`
|
|
4471
|
+
CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
|
|
4472
|
+
ON public.execution_jobs (run_id, base_job_name, wave_gated)
|
|
4473
|
+
`.execute(db);
|
|
4474
|
+
}
|
|
4475
|
+
async function down$9(db) {
|
|
4476
|
+
await sql`DROP INDEX IF EXISTS public.idx_execution_jobs_wave`.execute(db);
|
|
4477
|
+
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
|
|
4478
|
+
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
|
|
4479
|
+
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_gated`.execute(db);
|
|
4480
|
+
}
|
|
4481
|
+
//#endregion
|
|
4482
|
+
//#region src/db/migrations/042_dispatch_queue_patterns.ts
|
|
4483
|
+
var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
|
|
4484
|
+
down: () => down$8,
|
|
4485
|
+
up: () => up$8
|
|
4486
|
+
});
|
|
4487
|
+
/**
|
|
4488
|
+
* Add pattern columns to dispatch_queue. Exact labels stay in runs_on_labels /
|
|
4489
|
+
* exclude_labels (the SQL @> prefilter); regex matchers go here and are applied
|
|
4490
|
+
* as a JS post-filter at drain time, since Postgres `~` regex semantics differ
|
|
4491
|
+
* from JavaScript `RegExp` and the engine's `matcherSatisfiedBy` is the single
|
|
4492
|
+
* matching authority.
|
|
4493
|
+
*
|
|
4494
|
+
* - `runs_on_patterns jsonb NOT NULL DEFAULT '[]'` — regex matchers the agent's
|
|
4495
|
+
* labels must satisfy.
|
|
4496
|
+
* - `exclude_patterns jsonb NOT NULL DEFAULT '[]'` — regex matchers that
|
|
4497
|
+
* disqualify an agent.
|
|
4498
|
+
*
|
|
4499
|
+
* Idempotent: re-running on a DB that already has the columns is a no-op.
|
|
4500
|
+
*/
|
|
4501
|
+
async function colExists$3(db, table, name) {
|
|
4502
|
+
return (await sql`
|
|
4503
|
+
SELECT EXISTS (
|
|
4504
|
+
SELECT 1 FROM information_schema.columns
|
|
4505
|
+
WHERE table_schema = 'public'
|
|
4506
|
+
AND table_name = ${table}
|
|
4507
|
+
AND column_name = ${name}
|
|
4508
|
+
) AS exists
|
|
4509
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
4510
|
+
}
|
|
4511
|
+
async function up$8(db) {
|
|
4512
|
+
if (!await colExists$3(db, "dispatch_queue", "runs_on_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN runs_on_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
|
|
4513
|
+
if (!await colExists$3(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
|
|
4514
|
+
}
|
|
4515
|
+
async function down$8(db) {
|
|
4516
|
+
await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
|
|
4517
|
+
await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
|
|
4518
|
+
}
|
|
4519
|
+
//#endregion
|
|
4520
|
+
//#region src/db/migrations/043_rerouted_to_peer.ts
|
|
4521
|
+
var _043_rerouted_to_peer_exports = /* @__PURE__ */ __exportAll({
|
|
4522
|
+
down: () => down$7,
|
|
4523
|
+
up: () => up$7
|
|
4524
|
+
});
|
|
4525
|
+
/**
|
|
4526
|
+
* Add a nullable `rerouted_to_peer text` marker to execution_jobs. Non-null
|
|
4527
|
+
* means the job was rerouted to a remote worker peer (the value is that
|
|
4528
|
+
* peer's instance id); NULL means the job runs locally. The run-recovery
|
|
4529
|
+
* sweepers read it so a rerouted job whose worker is still connected is not
|
|
4530
|
+
* prematurely failed.
|
|
4531
|
+
*
|
|
4532
|
+
* Idempotent: re-running on a DB that already has the column is a no-op.
|
|
4533
|
+
*/
|
|
4534
|
+
async function colExists$2(db, table, name) {
|
|
4535
|
+
return (await sql`
|
|
4536
|
+
SELECT EXISTS (
|
|
4537
|
+
SELECT 1 FROM information_schema.columns
|
|
4538
|
+
WHERE table_schema = 'public'
|
|
4539
|
+
AND table_name = ${table}
|
|
4540
|
+
AND column_name = ${name}
|
|
4541
|
+
) AS exists
|
|
4542
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
4543
|
+
}
|
|
4544
|
+
async function up$7(db) {
|
|
4545
|
+
if (!await colExists$2(db, "execution_jobs", "rerouted_to_peer")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN rerouted_to_peer text`.execute(db);
|
|
4546
|
+
}
|
|
4547
|
+
async function down$7(db) {
|
|
4548
|
+
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS rerouted_to_peer`.execute(db);
|
|
4549
|
+
}
|
|
4550
|
+
//#endregion
|
|
4551
|
+
//#region src/db/migrations/044_check_mode.ts
|
|
4552
|
+
var _044_check_mode_exports = /* @__PURE__ */ __exportAll({
|
|
4553
|
+
down: () => down$6,
|
|
4554
|
+
up: () => up$6
|
|
4555
|
+
});
|
|
4556
|
+
/**
|
|
4557
|
+
* Add idempotent check-mode columns:
|
|
4558
|
+
*
|
|
4559
|
+
* - `execution_runs.check_mode text` — the run's mode (`apply` | `check` |
|
|
4560
|
+
* `check-fail-on-drift`). NULL means a legacy/apply run.
|
|
4561
|
+
* - `execution_steps.check_outcome text` — the per-step `CheckStepOutcome`
|
|
4562
|
+
* (`skipped` | `applied` | `declined` | `dry-run` | `no_check`). NULL when
|
|
4563
|
+
* the step ran without a check mode.
|
|
4564
|
+
* - `execution_steps.drift_summary text` — the human-readable `summarize(drift)`
|
|
4565
|
+
* line. NULL when no drift.
|
|
4566
|
+
* - `execution_steps.drift jsonb` — the structured drift value. NULL when no drift.
|
|
4567
|
+
*
|
|
4568
|
+
* All nullable. Idempotent: re-running on a DB that already has the columns is
|
|
4569
|
+
* a no-op.
|
|
4570
|
+
*/
|
|
4571
|
+
async function colExists$1(db, table, name) {
|
|
4572
|
+
return (await sql`
|
|
4573
|
+
SELECT EXISTS (
|
|
4574
|
+
SELECT 1 FROM information_schema.columns
|
|
4575
|
+
WHERE table_schema = 'public'
|
|
4576
|
+
AND table_name = ${table}
|
|
4577
|
+
AND column_name = ${name}
|
|
4578
|
+
) AS exists
|
|
4579
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
4580
|
+
}
|
|
4581
|
+
async function up$6(db) {
|
|
4582
|
+
if (!await colExists$1(db, "execution_runs", "check_mode")) await sql`ALTER TABLE public.execution_runs ADD COLUMN check_mode text`.execute(db);
|
|
4583
|
+
if (!await colExists$1(db, "execution_steps", "check_outcome")) await sql`ALTER TABLE public.execution_steps ADD COLUMN check_outcome text`.execute(db);
|
|
4584
|
+
if (!await colExists$1(db, "execution_steps", "drift_summary")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift_summary text`.execute(db);
|
|
4585
|
+
if (!await colExists$1(db, "execution_steps", "drift")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift jsonb`.execute(db);
|
|
4586
|
+
}
|
|
4587
|
+
async function down$6(db) {
|
|
4588
|
+
await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift`.execute(db);
|
|
4589
|
+
await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift_summary`.execute(db);
|
|
4590
|
+
await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS check_outcome`.execute(db);
|
|
4591
|
+
await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS check_mode`.execute(db);
|
|
4592
|
+
}
|
|
4593
|
+
//#endregion
|
|
4594
|
+
//#region src/db/migrations/045_host_properties.ts
|
|
4595
|
+
var _045_host_properties_exports = /* @__PURE__ */ __exportAll({
|
|
4596
|
+
down: () => down$5,
|
|
4597
|
+
up: () => up$5
|
|
4598
|
+
});
|
|
4599
|
+
/**
|
|
4600
|
+
* Add the typed host-vars dimension to the host roster:
|
|
4601
|
+
*
|
|
4602
|
+
* - `host_roster.host_properties jsonb NOT NULL DEFAULT '{}'` — the typed
|
|
4603
|
+
* property bag (`string | number | boolean` values) reported by the agent at
|
|
4604
|
+
* registration and/or declared by the operator (`kici-admin host declare
|
|
4605
|
+
* --prop`). Labels stay the flat-string grouping dimension; properties are
|
|
4606
|
+
* the separate queryable host-vars dimension.
|
|
4607
|
+
*
|
|
4608
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
|
|
4609
|
+
* the column is a no-op. Staging data is preserved (additive column).
|
|
4610
|
+
*/
|
|
4611
|
+
async function up$5(db) {
|
|
4612
|
+
await sql`ALTER TABLE public.host_roster
|
|
4613
|
+
ADD COLUMN IF NOT EXISTS host_properties jsonb NOT NULL DEFAULT '{}'::jsonb`.execute(db);
|
|
4614
|
+
}
|
|
4615
|
+
async function down$5(db) {
|
|
4616
|
+
await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS host_properties`.execute(db);
|
|
4617
|
+
}
|
|
4618
|
+
//#endregion
|
|
4619
|
+
//#region src/db/migrations/046_join_token_consumed_by_instance.ts
|
|
4620
|
+
var _046_join_token_consumed_by_instance_exports = /* @__PURE__ */ __exportAll({
|
|
4621
|
+
down: () => down$4,
|
|
4622
|
+
up: () => up$4
|
|
4623
|
+
});
|
|
4624
|
+
/**
|
|
4625
|
+
* Record the joining peer's instanceId at first consumption of a join token:
|
|
4626
|
+
*
|
|
4627
|
+
* - `join_tokens.consumed_by_instance text` — the `instanceId` of the peer that
|
|
4628
|
+
* consumed the token. This is the *joining peer*, distinct from `consumed_by`
|
|
4629
|
+
* (the coordinator that processed the claim). It lets a token be reused by the
|
|
4630
|
+
* same peer instance until `expires_at`, so a peer that lost its credential
|
|
4631
|
+
* (transient outage / deleted credential file) self-heals by re-presenting the
|
|
4632
|
+
* join token already in its env — no operator action, no cluster redeploy.
|
|
4633
|
+
*
|
|
4634
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
|
|
4635
|
+
* the column is a no-op. Staging data is preserved (additive nullable column).
|
|
4636
|
+
*/
|
|
4637
|
+
async function up$4(db) {
|
|
4638
|
+
await sql`ALTER TABLE public.join_tokens
|
|
4639
|
+
ADD COLUMN IF NOT EXISTS consumed_by_instance text`.execute(db);
|
|
4640
|
+
}
|
|
4641
|
+
async function down$4(db) {
|
|
4642
|
+
await sql`ALTER TABLE public.join_tokens DROP COLUMN IF EXISTS consumed_by_instance`.execute(db);
|
|
4643
|
+
}
|
|
4644
|
+
//#endregion
|
|
4645
|
+
//#region src/db/migrations/047_needs_run_on.ts
|
|
4646
|
+
var _047_needs_run_on_exports = /* @__PURE__ */ __exportAll({
|
|
4647
|
+
down: () => down$3,
|
|
4648
|
+
up: () => up$3
|
|
4649
|
+
});
|
|
4650
|
+
/**
|
|
4651
|
+
* Migrate `execution_job_needs` from the binary `if_failed` policy to a
|
|
4652
|
+
* programmable `run_on` status-set:
|
|
4653
|
+
*
|
|
4654
|
+
* - Add `run_on text` (JSON-encoded array of upstream terminal statuses) with a
|
|
4655
|
+
* default of `'["success"]'`.
|
|
4656
|
+
* - Backfill from the existing `if_failed`: `skip` → `["success"]`,
|
|
4657
|
+
* `run` → every terminal job status.
|
|
4658
|
+
* - Drop `if_failed`.
|
|
4659
|
+
*
|
|
4660
|
+
* Idempotent: column-exists guarded so re-running on a DB that already has
|
|
4661
|
+
* `run_on` (and no `if_failed`) is a no-op. Existing rows are preserved
|
|
4662
|
+
* (staging data is not dropped).
|
|
4663
|
+
*/
|
|
4664
|
+
const ALL_TERMINAL_JSON = JSON.stringify([
|
|
4665
|
+
"success",
|
|
4666
|
+
"failed",
|
|
4667
|
+
"cancelled",
|
|
4668
|
+
"skipped",
|
|
4669
|
+
"timed_out_stale",
|
|
4670
|
+
"drift_dropped"
|
|
4671
|
+
]);
|
|
4672
|
+
const SUCCESS_ONLY_JSON = JSON.stringify(["success"]);
|
|
4673
|
+
async function colExists(db, table, name) {
|
|
4674
|
+
return (await sql`
|
|
4675
|
+
SELECT EXISTS (
|
|
4676
|
+
SELECT 1 FROM information_schema.columns
|
|
4677
|
+
WHERE table_schema = 'public'
|
|
4678
|
+
AND table_name = ${table}
|
|
4679
|
+
AND column_name = ${name}
|
|
4680
|
+
) AS exists
|
|
4681
|
+
`.execute(db)).rows[0]?.exists ?? false;
|
|
4682
|
+
}
|
|
4683
|
+
const SUCCESS_ONLY_DEFAULT = `'${SUCCESS_ONLY_JSON}'`;
|
|
4684
|
+
async function up$3(db) {
|
|
4685
|
+
if (!await colExists(db, "execution_job_needs", "run_on")) await sql`
|
|
4686
|
+
ALTER TABLE public.execution_job_needs
|
|
4687
|
+
ADD COLUMN run_on text NOT NULL DEFAULT ${sql.raw(SUCCESS_ONLY_DEFAULT)}
|
|
4688
|
+
`.execute(db);
|
|
4689
|
+
if (await colExists(db, "execution_job_needs", "if_failed")) {
|
|
4690
|
+
await sql`
|
|
4691
|
+
UPDATE public.execution_job_needs
|
|
4692
|
+
SET run_on = ${ALL_TERMINAL_JSON}
|
|
4693
|
+
WHERE if_failed = 'run'
|
|
4694
|
+
`.execute(db);
|
|
4695
|
+
await sql`
|
|
4696
|
+
UPDATE public.execution_job_needs
|
|
4697
|
+
SET run_on = ${SUCCESS_ONLY_JSON}
|
|
4698
|
+
WHERE if_failed = 'skip'
|
|
4699
|
+
`.execute(db);
|
|
4700
|
+
await sql`ALTER TABLE public.execution_job_needs DROP COLUMN if_failed`.execute(db);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
async function down$3(db) {
|
|
4704
|
+
if (!await colExists(db, "execution_job_needs", "if_failed")) await sql`
|
|
4705
|
+
ALTER TABLE public.execution_job_needs
|
|
4706
|
+
ADD COLUMN if_failed text NOT NULL DEFAULT 'skip'
|
|
4707
|
+
`.execute(db);
|
|
4708
|
+
if (await colExists(db, "execution_job_needs", "run_on")) {
|
|
4709
|
+
await sql`
|
|
4710
|
+
UPDATE public.execution_job_needs
|
|
4711
|
+
SET if_failed = CASE WHEN run_on = ${SUCCESS_ONLY_JSON} THEN 'skip' ELSE 'run' END
|
|
4712
|
+
`.execute(db);
|
|
4713
|
+
await sql`ALTER TABLE public.execution_job_needs DROP COLUMN run_on`.execute(db);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
//#endregion
|
|
4717
|
+
//#region src/db/migrations/048_host_reboot_pending.ts
|
|
4718
|
+
var _048_host_reboot_pending_exports = /* @__PURE__ */ __exportAll({
|
|
4719
|
+
down: () => down$2,
|
|
4720
|
+
up: () => up$2
|
|
4721
|
+
});
|
|
4722
|
+
/**
|
|
4723
|
+
* Add `host_roster.reboot_pending_until timestamptz NULL` — the persisted
|
|
4724
|
+
* reboot-pending flag for workflow-level host restart.
|
|
4725
|
+
*
|
|
4726
|
+
* Set when an agent's `restartHost()` step calls `host.requestReboot` (so it
|
|
4727
|
+
* survives an orchestrator restart during the host's reboot window). While the
|
|
4728
|
+
* value is in the future it (1) makes the agent's imminent disconnect an
|
|
4729
|
+
* expected reboot rather than a recovery-fail, (2) gates the pinned-drain off
|
|
4730
|
+
* so the post-restart job is not dispatched into the about-to-reboot box, and
|
|
4731
|
+
* (3) clears on the next reconnect (down-then-up), releasing the held job. NULL
|
|
4732
|
+
* = no reboot pending.
|
|
4733
|
+
*
|
|
4734
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
|
|
4735
|
+
* preserved.
|
|
4736
|
+
*/
|
|
4737
|
+
async function up$2(db) {
|
|
4738
|
+
await sql`ALTER TABLE public.host_roster
|
|
4739
|
+
ADD COLUMN IF NOT EXISTS reboot_pending_until timestamptz`.execute(db);
|
|
4740
|
+
}
|
|
4741
|
+
async function down$2(db) {
|
|
4742
|
+
await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS reboot_pending_until`.execute(db);
|
|
4743
|
+
}
|
|
4744
|
+
//#endregion
|
|
4745
|
+
//#region src/db/migrations/049_held_runs_payload.ts
|
|
4746
|
+
var _049_held_runs_payload_exports = /* @__PURE__ */ __exportAll({
|
|
4433
4747
|
down: () => down$1,
|
|
4434
4748
|
up: () => up$1
|
|
4435
4749
|
});
|
|
4436
4750
|
/**
|
|
4437
|
-
* Add the
|
|
4438
|
-
*
|
|
4439
|
-
*
|
|
4440
|
-
*
|
|
4441
|
-
*
|
|
4442
|
-
* wave-scheduler clears the flag one-per-terminal as siblings complete (or, on
|
|
4443
|
-
* `failFast`, skips the held remainder).
|
|
4444
|
-
* - `execution_jobs.wave_max_parallel int` / `wave_fail_fast boolean` — the
|
|
4445
|
-
* base's wave policy, stamped on every fan-out child so the wave-scheduler can
|
|
4446
|
-
* read it at terminal time without re-fetching the lock file (the tracker has
|
|
4447
|
-
* no lock access). NULL for any job not part of a bounded wave.
|
|
4448
|
-
*
|
|
4449
|
-
* A composite index on (run_id, base_job_name, wave_gated) supports the
|
|
4450
|
-
* wave-scheduler's "next held sibling of this base" lookups.
|
|
4751
|
+
* Add `held_runs.payload jsonb NULL` — the drift payload captured when a
|
|
4752
|
+
* `when: 'drift'` step-approval gate fires. Holds `{ summaryMarkdown, drift }`:
|
|
4753
|
+
* the author's `summarize(drift)` rendering plus the structured drift blob, so
|
|
4754
|
+
* the dashboard approval queue and the CLI render the computed diff the
|
|
4755
|
+
* operator approves. NULL for every non-drift hold.
|
|
4451
4756
|
*
|
|
4452
|
-
* Idempotent
|
|
4757
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
|
|
4758
|
+
* preserved.
|
|
4453
4759
|
*/
|
|
4454
|
-
async function colExists$1(db, table, name) {
|
|
4455
|
-
return (await sql`
|
|
4456
|
-
SELECT EXISTS (
|
|
4457
|
-
SELECT 1 FROM information_schema.columns
|
|
4458
|
-
WHERE table_schema = 'public'
|
|
4459
|
-
AND table_name = ${table}
|
|
4460
|
-
AND column_name = ${name}
|
|
4461
|
-
) AS exists
|
|
4462
|
-
`.execute(db)).rows[0]?.exists ?? false;
|
|
4463
|
-
}
|
|
4464
4760
|
async function up$1(db) {
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
if (!await colExists$1(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
|
|
4468
|
-
await sql`
|
|
4469
|
-
CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
|
|
4470
|
-
ON public.execution_jobs (run_id, base_job_name, wave_gated)
|
|
4471
|
-
`.execute(db);
|
|
4761
|
+
await sql`ALTER TABLE public.held_runs
|
|
4762
|
+
ADD COLUMN IF NOT EXISTS payload jsonb`.execute(db);
|
|
4472
4763
|
}
|
|
4473
4764
|
async function down$1(db) {
|
|
4474
|
-
await sql`DROP
|
|
4475
|
-
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
|
|
4476
|
-
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
|
|
4477
|
-
await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_gated`.execute(db);
|
|
4765
|
+
await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS payload`.execute(db);
|
|
4478
4766
|
}
|
|
4479
4767
|
//#endregion
|
|
4480
|
-
//#region src/db/migrations/
|
|
4481
|
-
var
|
|
4768
|
+
//#region src/db/migrations/050_sources_slug.ts
|
|
4769
|
+
var _050_sources_slug_exports = /* @__PURE__ */ __exportAll({
|
|
4482
4770
|
down: () => down,
|
|
4483
4771
|
up: () => up
|
|
4484
4772
|
});
|
|
4485
4773
|
/**
|
|
4486
|
-
* Add
|
|
4487
|
-
*
|
|
4488
|
-
* as a JS post-filter at drain time, since Postgres `~` regex semantics differ
|
|
4489
|
-
* from JavaScript `RegExp` and the engine's `matcherSatisfiedBy` is the single
|
|
4490
|
-
* matching authority.
|
|
4774
|
+
* Add `sources.slug TEXT NULL` — the GitHub App slug (the URL-safe identifier
|
|
4775
|
+
* GitHub assigns, e.g. `my-kici-app`).
|
|
4491
4776
|
*
|
|
4492
|
-
* -
|
|
4493
|
-
*
|
|
4494
|
-
*
|
|
4495
|
-
*
|
|
4777
|
+
* For GitHub-App sources GitHub is the source of truth for both the display
|
|
4778
|
+
* `name` and the `slug`: both are captured at creation and kept fresh by the
|
|
4779
|
+
* daily refresher + `kici-admin source refresh`. NULL when the identity fetch
|
|
4780
|
+
* hasn't populated it yet (manual `--app-id` flow whose initial fetch failed,
|
|
4781
|
+
* or a row created before the rollout).
|
|
4496
4782
|
*
|
|
4497
|
-
* Idempotent
|
|
4783
|
+
* Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
|
|
4784
|
+
* preserved.
|
|
4498
4785
|
*/
|
|
4499
|
-
async function colExists(db, table, name) {
|
|
4500
|
-
return (await sql`
|
|
4501
|
-
SELECT EXISTS (
|
|
4502
|
-
SELECT 1 FROM information_schema.columns
|
|
4503
|
-
WHERE table_schema = 'public'
|
|
4504
|
-
AND table_name = ${table}
|
|
4505
|
-
AND column_name = ${name}
|
|
4506
|
-
) AS exists
|
|
4507
|
-
`.execute(db)).rows[0]?.exists ?? false;
|
|
4508
|
-
}
|
|
4509
4786
|
async function up(db) {
|
|
4510
|
-
|
|
4511
|
-
|
|
4787
|
+
await sql`ALTER TABLE public.sources
|
|
4788
|
+
ADD COLUMN IF NOT EXISTS slug text`.execute(db);
|
|
4512
4789
|
}
|
|
4513
4790
|
async function down(db) {
|
|
4514
|
-
await sql`ALTER TABLE public.
|
|
4515
|
-
await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
|
|
4791
|
+
await sql`ALTER TABLE public.sources DROP COLUMN IF EXISTS slug`.execute(db);
|
|
4516
4792
|
}
|
|
4517
4793
|
//#endregion
|
|
4518
4794
|
//#region src/db/migration-provider.ts
|
|
@@ -4560,7 +4836,15 @@ function createMigrationProvider() {
|
|
|
4560
4836
|
"039_host_roster": _039_host_roster_exports,
|
|
4561
4837
|
"040_runsonall_pin": _040_runsonall_pin_exports,
|
|
4562
4838
|
"041_wave_gated": _041_wave_gated_exports,
|
|
4563
|
-
"042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports
|
|
4839
|
+
"042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports,
|
|
4840
|
+
"043_rerouted_to_peer": _043_rerouted_to_peer_exports,
|
|
4841
|
+
"044_check_mode": _044_check_mode_exports,
|
|
4842
|
+
"045_host_properties": _045_host_properties_exports,
|
|
4843
|
+
"046_join_token_consumed_by_instance": _046_join_token_consumed_by_instance_exports,
|
|
4844
|
+
"047_needs_run_on": _047_needs_run_on_exports,
|
|
4845
|
+
"048_host_reboot_pending": _048_host_reboot_pending_exports,
|
|
4846
|
+
"049_held_runs_payload": _049_held_runs_payload_exports,
|
|
4847
|
+
"050_sources_slug": _050_sources_slug_exports
|
|
4564
4848
|
};
|
|
4565
4849
|
} };
|
|
4566
4850
|
}
|
|
@@ -5187,6 +5471,419 @@ function installPostReceiveHook(repoPath, script) {
|
|
|
5187
5471
|
return hookPath;
|
|
5188
5472
|
}
|
|
5189
5473
|
//#endregion
|
|
5474
|
+
//#region src/providers/github/auth.ts
|
|
5475
|
+
/**
|
|
5476
|
+
* GitHub clone token provider and authentication utilities.
|
|
5477
|
+
*
|
|
5478
|
+
* Implements the CloneTokenProvider interface from @kici-dev/engine for GitHub.
|
|
5479
|
+
* Uses GitHub App installation tokens for repository access.
|
|
5480
|
+
*/
|
|
5481
|
+
/**
|
|
5482
|
+
* Create an Octokit instance authenticated as a GitHub App installation.
|
|
5483
|
+
*
|
|
5484
|
+
* Uses @octokit/auth-app to handle:
|
|
5485
|
+
* - JWT generation for app-level authentication
|
|
5486
|
+
* - Installation token creation and automatic refresh
|
|
5487
|
+
*
|
|
5488
|
+
* Each call creates a new Octokit instance scoped to the specific installation.
|
|
5489
|
+
* The auth-app strategy handles token caching internally per Octokit instance.
|
|
5490
|
+
*
|
|
5491
|
+
* @param config - GitHub App credentials (appId + privateKey)
|
|
5492
|
+
* @param installationId - GitHub App installation ID for the target account/org
|
|
5493
|
+
* @returns Octokit instance with auto-refreshing installation tokens
|
|
5494
|
+
*/
|
|
5495
|
+
function createInstallationOctokit(config, installationId) {
|
|
5496
|
+
return new Octokit({
|
|
5497
|
+
authStrategy: createAppAuth,
|
|
5498
|
+
auth: {
|
|
5499
|
+
appId: config.appId,
|
|
5500
|
+
privateKey: config.privateKey,
|
|
5501
|
+
installationId
|
|
5502
|
+
}
|
|
5503
|
+
});
|
|
5504
|
+
}
|
|
5505
|
+
//#endregion
|
|
5506
|
+
//#region src/providers/github/manifest.ts
|
|
5507
|
+
/**
|
|
5508
|
+
* GitHub App Manifest flow helpers. The manifest encodes KiCI's exact
|
|
5509
|
+
* permissions + events + webhook config so the operator never picks them by
|
|
5510
|
+
* hand — GitHub creates a correctly-configured App from this object in one
|
|
5511
|
+
* click. See docs.github.com "Registering a GitHub App from a manifest".
|
|
5512
|
+
*
|
|
5513
|
+
* The webhook secret is NOT part of the manifest: GitHub generates it during
|
|
5514
|
+
* registration and returns it on the conversion response, so both GitHub and
|
|
5515
|
+
* the Platform end up sharing the same secret with zero operator effort.
|
|
5516
|
+
*/
|
|
5517
|
+
/**
|
|
5518
|
+
* Validate a self-hosted webhook URL supplied via `source add github
|
|
5519
|
+
* --webhook-url`. Must be a well-formed absolute `https://` URL. Returns the URL
|
|
5520
|
+
* verbatim on success; throws a clear error otherwise. The validated URL is
|
|
5521
|
+
* baked into `manifest.hook_attributes.url` as-is — KiCI adds no ingress and
|
|
5522
|
+
* does not receive events at it; the operator owns delivery.
|
|
5523
|
+
*/
|
|
5524
|
+
function validateWebhookUrl(value) {
|
|
5525
|
+
let url;
|
|
5526
|
+
try {
|
|
5527
|
+
url = new URL(value);
|
|
5528
|
+
} catch {
|
|
5529
|
+
throw new Error(`--webhook-url must be a valid absolute URL: ${value}`);
|
|
5530
|
+
}
|
|
5531
|
+
if (url.protocol !== "https:") throw new Error(`--webhook-url must be an https:// URL (got ${url.protocol}//…)`);
|
|
5532
|
+
return value;
|
|
5533
|
+
}
|
|
5534
|
+
function buildGithubAppManifest(input) {
|
|
5535
|
+
return {
|
|
5536
|
+
name: input.name,
|
|
5537
|
+
url: "https://kici.dev",
|
|
5538
|
+
hook_attributes: {
|
|
5539
|
+
url: input.webhookUrl,
|
|
5540
|
+
active: true
|
|
5541
|
+
},
|
|
5542
|
+
redirect_url: input.redirectUrl,
|
|
5543
|
+
...input.setupUrl ? { setup_url: input.setupUrl } : {},
|
|
5544
|
+
public: false,
|
|
5545
|
+
default_permissions: {
|
|
5546
|
+
contents: "read",
|
|
5547
|
+
metadata: "read",
|
|
5548
|
+
pull_requests: "read",
|
|
5549
|
+
checks: "write",
|
|
5550
|
+
members: "read"
|
|
5551
|
+
},
|
|
5552
|
+
default_events: [
|
|
5553
|
+
"push",
|
|
5554
|
+
"pull_request",
|
|
5555
|
+
"check_run",
|
|
5556
|
+
"check_suite"
|
|
5557
|
+
]
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5560
|
+
/**
|
|
5561
|
+
* Exchange the short-lived manifest `code` for the App's id, private key, and
|
|
5562
|
+
* webhook secret. Runs server-to-server directly against GitHub — the private
|
|
5563
|
+
* key never transits the Platform.
|
|
5564
|
+
*/
|
|
5565
|
+
async function convertManifestCode(code, deps = {}) {
|
|
5566
|
+
const { data } = await (deps.octokit ?? new Octokit()).request("POST /app-manifests/{code}/conversions", { code });
|
|
5567
|
+
const d = data;
|
|
5568
|
+
if (!d.webhook_secret) throw new Error("GitHub returned no webhook secret for the new App — cannot verify inbound events. Re-run the setup, or configure a webhook secret manually with source update.");
|
|
5569
|
+
return {
|
|
5570
|
+
appId: String(d.id),
|
|
5571
|
+
slug: d.slug,
|
|
5572
|
+
name: d.name,
|
|
5573
|
+
privateKey: d.pem,
|
|
5574
|
+
webhookSecret: d.webhook_secret,
|
|
5575
|
+
clientId: d.client_id,
|
|
5576
|
+
clientSecret: d.client_secret,
|
|
5577
|
+
htmlUrl: d.html_url
|
|
5578
|
+
};
|
|
5579
|
+
}
|
|
5580
|
+
function appOctokitFor(creds) {
|
|
5581
|
+
return new Octokit({
|
|
5582
|
+
authStrategy: createAppAuth,
|
|
5583
|
+
auth: {
|
|
5584
|
+
appId: creds.appId,
|
|
5585
|
+
privateKey: creds.privateKey
|
|
5586
|
+
}
|
|
5587
|
+
});
|
|
5588
|
+
}
|
|
5589
|
+
/**
|
|
5590
|
+
* Poll GitHub (as the App, via a JWT) until at least one installation exists,
|
|
5591
|
+
* returning the first installation's id + account login. Throws on timeout.
|
|
5592
|
+
*/
|
|
5593
|
+
async function waitForInstallation(creds, opts) {
|
|
5594
|
+
const now = opts.now ?? Date.now;
|
|
5595
|
+
const octokit = opts.appOctokit ?? appOctokitFor(creds);
|
|
5596
|
+
const deadline = now() + opts.timeoutMs;
|
|
5597
|
+
for (;;) {
|
|
5598
|
+
const { data } = await octokit.request("GET /app/installations", { per_page: 1 });
|
|
5599
|
+
const installs = data;
|
|
5600
|
+
if (Array.isArray(installs) && installs.length > 0) return {
|
|
5601
|
+
installationId: installs[0].id,
|
|
5602
|
+
accountLogin: installs[0].account?.login ?? ""
|
|
5603
|
+
};
|
|
5604
|
+
if (now() >= deadline) throw new Error("Timed out waiting for the GitHub App to be installed");
|
|
5605
|
+
await new Promise((r) => setTimeout(r, opts.pollMs));
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5609
|
+
* Mint an installation token from the captured private key and confirm the App
|
|
5610
|
+
* can reach repos — proves the key works end-to-end (the same path the agent's
|
|
5611
|
+
* clone uses at runtime).
|
|
5612
|
+
*/
|
|
5613
|
+
async function verifyRepoAccess(creds, installationId, deps = {}) {
|
|
5614
|
+
const { data } = await (deps.octokit ?? createInstallationOctokit(creds, installationId)).request("GET /installation/repositories", { per_page: 1 });
|
|
5615
|
+
return { repoCount: data.total_count };
|
|
5616
|
+
}
|
|
5617
|
+
//#endregion
|
|
5618
|
+
//#region src/providers/github/manifest-form.ts
|
|
5619
|
+
/**
|
|
5620
|
+
* GitHub's create-from-manifest flow is an HTML form POST (the `manifest`
|
|
5621
|
+
* JSON cannot ride in a query string). These helpers render the auto-submitting
|
|
5622
|
+
* form and the headless code-display page. Pure string builders — no IO — so
|
|
5623
|
+
* both the CLI loopback server and the static marketing-site page can reuse the
|
|
5624
|
+
* same shapes.
|
|
5625
|
+
*/
|
|
5626
|
+
function escapeHtml(s) {
|
|
5627
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5628
|
+
}
|
|
5629
|
+
function manifestCreateUrl(githubOrg) {
|
|
5630
|
+
return githubOrg ? `https://github.com/organizations/${githubOrg}/settings/apps/new` : "https://github.com/settings/apps/new";
|
|
5631
|
+
}
|
|
5632
|
+
function renderManifestFormHtml(opts) {
|
|
5633
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Create your KiCI GitHub App</title></head>
|
|
5634
|
+
<body>
|
|
5635
|
+
<p>Redirecting you to GitHub to create your KiCI GitHub App…</p>
|
|
5636
|
+
<form id="f" method="post" action="${escapeHtml(`${opts.createUrl}?state=${encodeURIComponent(opts.state)}`)}">
|
|
5637
|
+
<input type="hidden" name="manifest" value="${escapeHtml(opts.manifestJson)}">
|
|
5638
|
+
<noscript><button type="submit">Continue to GitHub</button></noscript>
|
|
5639
|
+
</form>
|
|
5640
|
+
<script>document.getElementById('f').submit();<\/script>
|
|
5641
|
+
</body></html>`;
|
|
5642
|
+
}
|
|
5643
|
+
function renderCodeDisplayHtml(code) {
|
|
5644
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>KiCI — copy your setup code</title></head>
|
|
5645
|
+
<body>
|
|
5646
|
+
<p>App created. Copy this code back into the <code>kici-admin</code> prompt:</p>
|
|
5647
|
+
<pre id="c" style="font-size:1.2rem;padding:12px;border:1px solid #ccc">${escapeHtml(code)}</pre>
|
|
5648
|
+
<button onclick="navigator.clipboard.writeText(document.getElementById('c').textContent)">Copy</button>
|
|
5649
|
+
</body></html>`;
|
|
5650
|
+
}
|
|
5651
|
+
//#endregion
|
|
5652
|
+
//#region src/cli/loopback-callback.ts
|
|
5653
|
+
function startManifestLoopback(opts) {
|
|
5654
|
+
return new Promise((resolveServer) => {
|
|
5655
|
+
let outcome;
|
|
5656
|
+
const waiters = [];
|
|
5657
|
+
const resolveCode = (value) => {
|
|
5658
|
+
if (outcome) return;
|
|
5659
|
+
outcome = {
|
|
5660
|
+
ok: true,
|
|
5661
|
+
value
|
|
5662
|
+
};
|
|
5663
|
+
for (const w of waiters) w.resolve(value);
|
|
5664
|
+
};
|
|
5665
|
+
const rejectCode = (error) => {
|
|
5666
|
+
if (outcome) return;
|
|
5667
|
+
outcome = {
|
|
5668
|
+
ok: false,
|
|
5669
|
+
error
|
|
5670
|
+
};
|
|
5671
|
+
for (const w of waiters) w.reject(error);
|
|
5672
|
+
};
|
|
5673
|
+
const server = createServer((req, res) => {
|
|
5674
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
5675
|
+
if (url.pathname === "/cb") {
|
|
5676
|
+
const code = url.searchParams.get("code") ?? "";
|
|
5677
|
+
const state = url.searchParams.get("state") ?? "";
|
|
5678
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
5679
|
+
res.end(renderCodeDisplayHtml(code));
|
|
5680
|
+
if (state !== opts.state) rejectCode(/* @__PURE__ */ new Error("OAuth state mismatch — aborting"));
|
|
5681
|
+
else resolveCode({
|
|
5682
|
+
code,
|
|
5683
|
+
state
|
|
5684
|
+
});
|
|
5685
|
+
return;
|
|
5686
|
+
}
|
|
5687
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
5688
|
+
res.end(renderManifestFormHtml({
|
|
5689
|
+
createUrl: opts.createUrl,
|
|
5690
|
+
state: opts.state,
|
|
5691
|
+
manifestJson: opts.manifestJson
|
|
5692
|
+
}));
|
|
5693
|
+
});
|
|
5694
|
+
server.on("clientError", () => {});
|
|
5695
|
+
server.listen(0, "127.0.0.1", () => {
|
|
5696
|
+
const base = `http://127.0.0.1:${server.address().port}`;
|
|
5697
|
+
resolveServer({
|
|
5698
|
+
redirectUrl: `${base}/cb`,
|
|
5699
|
+
formUrl: `${base}/`,
|
|
5700
|
+
waitForCode: (timeoutMs) => new Promise((resolve, reject) => {
|
|
5701
|
+
if (outcome) {
|
|
5702
|
+
outcome.ok ? resolve(outcome.value) : reject(outcome.error);
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for GitHub callback")), timeoutMs);
|
|
5706
|
+
waiters.push({
|
|
5707
|
+
resolve: (v) => {
|
|
5708
|
+
clearTimeout(timer);
|
|
5709
|
+
resolve(v);
|
|
5710
|
+
},
|
|
5711
|
+
reject: (e) => {
|
|
5712
|
+
clearTimeout(timer);
|
|
5713
|
+
reject(e);
|
|
5714
|
+
}
|
|
5715
|
+
});
|
|
5716
|
+
}),
|
|
5717
|
+
close: () => server.close()
|
|
5718
|
+
});
|
|
5719
|
+
});
|
|
5720
|
+
});
|
|
5721
|
+
}
|
|
5722
|
+
//#endregion
|
|
5723
|
+
//#region src/cli/open-browser.ts
|
|
5724
|
+
/**
|
|
5725
|
+
* Open a URL in the operator's default browser, best-effort. Used by the GitHub
|
|
5726
|
+
* App manifest setup flow to launch the create / install pages. Failure is
|
|
5727
|
+
* non-fatal — the caller always prints the URL too, so a headless host (or a
|
|
5728
|
+
* blocked launcher) just falls back to copy-paste.
|
|
5729
|
+
*
|
|
5730
|
+
* `KICI_BROWSER_CMD` overrides the launcher: `none` suppresses it entirely
|
|
5731
|
+
* (E2E / headless capture), any other value is run with `{url}` substituted —
|
|
5732
|
+
* mirroring the `kici login` convention.
|
|
5733
|
+
*/
|
|
5734
|
+
function openBrowserBestEffort(url) {
|
|
5735
|
+
const override = process.env.KICI_BROWSER_CMD;
|
|
5736
|
+
if (override === "none") return;
|
|
5737
|
+
if (override) {
|
|
5738
|
+
exec(override.replace("{url}", url), () => {});
|
|
5739
|
+
return;
|
|
5740
|
+
}
|
|
5741
|
+
exec(platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`, () => {});
|
|
5742
|
+
}
|
|
5743
|
+
//#endregion
|
|
5744
|
+
//#region src/cli/commands/source-manifest.ts
|
|
5745
|
+
/**
|
|
5746
|
+
* `kici-admin source add github --manifest` — the one-click GitHub App setup
|
|
5747
|
+
* flow. Drives GitHub's App Manifest flow end-to-end: builds a pre-configured
|
|
5748
|
+
* manifest (KiCI's exact permissions/events/webhook URL), hands it to GitHub via
|
|
5749
|
+
* an auto-submitting form, catches the returned short-lived setup code over a
|
|
5750
|
+
* localhost loopback (or copy-paste in --no-browser mode), exchanges it for the
|
|
5751
|
+
* App's id + private key + webhook secret, then reuses the existing source
|
|
5752
|
+
* storage + Platform-registration path.
|
|
5753
|
+
*
|
|
5754
|
+
* The private-key-bearing conversion happens entirely here on the orchestrator
|
|
5755
|
+
* host — it never transits the Platform (sovereignty invariant).
|
|
5756
|
+
*/
|
|
5757
|
+
/** Static marketing-site page the CLI points at in headless paste-code mode. */
|
|
5758
|
+
const STATIC_CALLBACK_URL = "https://kici.dev/gh-manifest-callback";
|
|
5759
|
+
/** How long to wait for the operator to click "Create" on GitHub. */
|
|
5760
|
+
const CREATE_TIMEOUT_MS = 5 * 6e4;
|
|
5761
|
+
/** How long to wait for the operator to install the App on at least one repo. */
|
|
5762
|
+
const INSTALL_TIMEOUT_MS = 5 * 6e4;
|
|
5763
|
+
const INSTALL_POLL_MS = 3e3;
|
|
5764
|
+
function defaultReadLine(prompt) {
|
|
5765
|
+
const rl = createInterface({
|
|
5766
|
+
input: process.stdin,
|
|
5767
|
+
output: process.stdout
|
|
5768
|
+
});
|
|
5769
|
+
return new Promise((resolve) => {
|
|
5770
|
+
rl.question(prompt, (answer) => {
|
|
5771
|
+
rl.close();
|
|
5772
|
+
resolve(answer.trim());
|
|
5773
|
+
});
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
function defaultWriteRecoveryFile(appId, pem) {
|
|
5777
|
+
const file = join(mkdtempSync(join(tmpdir(), "kici-gh-app-")), `app-${appId}.private-key.pem`);
|
|
5778
|
+
writeFileSync(file, pem, { mode: 384 });
|
|
5779
|
+
return file;
|
|
5780
|
+
}
|
|
5781
|
+
const realManifestSetupDeps = {
|
|
5782
|
+
startLoopback: startManifestLoopback,
|
|
5783
|
+
openBrowser: openBrowserBestEffort,
|
|
5784
|
+
readLine: defaultReadLine,
|
|
5785
|
+
convert: (code) => convertManifestCode(code),
|
|
5786
|
+
waitForInstallation,
|
|
5787
|
+
verifyRepoAccess,
|
|
5788
|
+
writeRecoveryFile: defaultWriteRecoveryFile
|
|
5789
|
+
};
|
|
5790
|
+
/** Resolve the org-scoped webhook URL via the pre-flight route; abort if null. */
|
|
5791
|
+
async function resolveWebhookUrl(client) {
|
|
5792
|
+
const pre = await client.get("/api/v1/admin/sources/github-webhook-url");
|
|
5793
|
+
if (!pre.webhookUrl) throw new Error(`Cannot resolve the GitHub webhook URL${pre.webhookNote ? ` (${pre.webhookNote})` : ""}. The manifest flow needs it to configure the App. Ensure the orchestrator is connected to the Platform and has identified its org, then retry.`);
|
|
5794
|
+
return pre.webhookUrl;
|
|
5795
|
+
}
|
|
5796
|
+
/**
|
|
5797
|
+
* Catch the short-lived setup code: over the loopback (browser mode) or by
|
|
5798
|
+
* printing the static-site URL and reading the pasted code (headless mode).
|
|
5799
|
+
*/
|
|
5800
|
+
async function captureCode(opts, deps, manifestJson, state, redirectUrl, createUrl, loopback) {
|
|
5801
|
+
if (opts.noBrowser) {
|
|
5802
|
+
const url = `${STATIC_CALLBACK_URL}#m=${Buffer.from(manifestJson, "utf-8").toString("base64url")}&state=${encodeURIComponent(state)}&createUrl=${encodeURIComponent(createUrl)}`;
|
|
5803
|
+
console.log("Open this URL in a browser to create your App:");
|
|
5804
|
+
console.log(` ${url}`);
|
|
5805
|
+
console.log("After clicking \"Create\", copy the setup code shown on the page.");
|
|
5806
|
+
const code = await deps.readLine("Paste the setup code here: ");
|
|
5807
|
+
if (!code) throw new Error("No setup code was provided");
|
|
5808
|
+
return code;
|
|
5809
|
+
}
|
|
5810
|
+
console.log("→ Opening GitHub to create your App…");
|
|
5811
|
+
await deps.openBrowser(loopback.formUrl);
|
|
5812
|
+
console.log(` (if your browser did not open, visit ${loopback.formUrl} )`);
|
|
5813
|
+
const { code } = await loopback.waitForCode(CREATE_TIMEOUT_MS);
|
|
5814
|
+
return code;
|
|
5815
|
+
}
|
|
5816
|
+
/** Store credentials + register; on failure, save the PEM so the App is not orphaned. */
|
|
5817
|
+
async function storeAndRegister(opts, client, deps, creds) {
|
|
5818
|
+
try {
|
|
5819
|
+
return await client.post("/api/v1/admin/sources", {
|
|
5820
|
+
provider: "github",
|
|
5821
|
+
name: creds.name,
|
|
5822
|
+
slug: creds.slug,
|
|
5823
|
+
appId: creds.appId,
|
|
5824
|
+
privateKey: creds.privateKey,
|
|
5825
|
+
webhookSecret: creds.webhookSecret
|
|
5826
|
+
});
|
|
5827
|
+
} catch (err) {
|
|
5828
|
+
const file = deps.writeRecoveryFile(creds.appId, creds.privateKey);
|
|
5829
|
+
console.error("");
|
|
5830
|
+
console.error("⚠ The GitHub App was created but storing it on the orchestrator failed.");
|
|
5831
|
+
console.error(` App id: ${creds.appId}`);
|
|
5832
|
+
console.error(` Private key saved to: ${file}`);
|
|
5833
|
+
console.error(" Recover with:");
|
|
5834
|
+
console.error(` kici-admin source add github --name "${opts.name}" --app-id ${creds.appId} --private-key @${file} --webhook-secret ${creds.webhookSecret}`);
|
|
5835
|
+
throw err;
|
|
5836
|
+
}
|
|
5837
|
+
}
|
|
5838
|
+
async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps) {
|
|
5839
|
+
const webhookUrl = opts.webhookUrl ? validateWebhookUrl(opts.webhookUrl) : await resolveWebhookUrl(client);
|
|
5840
|
+
const state = randomBytes(16).toString("hex");
|
|
5841
|
+
const createUrl = manifestCreateUrl(opts.githubOrg);
|
|
5842
|
+
const loopback = await deps.startLoopback({
|
|
5843
|
+
state,
|
|
5844
|
+
manifestJson: "{}",
|
|
5845
|
+
createUrl
|
|
5846
|
+
});
|
|
5847
|
+
try {
|
|
5848
|
+
const manifest = buildGithubAppManifest({
|
|
5849
|
+
name: opts.name,
|
|
5850
|
+
webhookUrl,
|
|
5851
|
+
redirectUrl: opts.noBrowser ? STATIC_CALLBACK_URL : loopback.redirectUrl
|
|
5852
|
+
});
|
|
5853
|
+
const manifestJson = JSON.stringify(manifest);
|
|
5854
|
+
loopback.close();
|
|
5855
|
+
const lb = await deps.startLoopback({
|
|
5856
|
+
state,
|
|
5857
|
+
manifestJson,
|
|
5858
|
+
createUrl
|
|
5859
|
+
});
|
|
5860
|
+
try {
|
|
5861
|
+
const code = await captureCode(opts, deps, manifestJson, state, lb.redirectUrl, createUrl, lb);
|
|
5862
|
+
const creds = await deps.convert(code);
|
|
5863
|
+
console.log(`→ ✓ App created (id ${creds.appId}), credentials captured`);
|
|
5864
|
+
const stored = await storeAndRegister(opts, client, deps, creds);
|
|
5865
|
+
console.log(`→ ✓ Stored on orchestrator (encrypted), registered as ${stored.routingKey}`);
|
|
5866
|
+
const installUrl = `https://github.com/apps/${creds.slug}/installations/new`;
|
|
5867
|
+
console.log(`→ Install the App on your repos: ${installUrl}`);
|
|
5868
|
+
if (!opts.noBrowser) await deps.openBrowser(installUrl);
|
|
5869
|
+
const { installationId, accountLogin } = await deps.waitForInstallation(creds, {
|
|
5870
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
5871
|
+
pollMs: INSTALL_POLL_MS
|
|
5872
|
+
});
|
|
5873
|
+
console.log(`→ ✓ Installation detected (account ${accountLogin})`);
|
|
5874
|
+
const { repoCount } = await deps.verifyRepoAccess(creds, installationId);
|
|
5875
|
+
console.log(`→ ✓ Credentials verified (${repoCount} repositories reachable)`);
|
|
5876
|
+
console.log("");
|
|
5877
|
+
console.log(`GitHub App "${opts.name}" is live.`);
|
|
5878
|
+
console.log(` Webhook: ${webhookUrl}`);
|
|
5879
|
+
} finally {
|
|
5880
|
+
lb.close();
|
|
5881
|
+
}
|
|
5882
|
+
} finally {
|
|
5883
|
+
loopback.close();
|
|
5884
|
+
}
|
|
5885
|
+
}
|
|
5886
|
+
//#endregion
|
|
5190
5887
|
//#region src/cli/commands/source.ts
|
|
5191
5888
|
/**
|
|
5192
5889
|
* Source management commands for kici-admin.
|
|
@@ -5268,6 +5965,16 @@ async function readFromStdin() {
|
|
|
5268
5965
|
for await (const line of rl) chunks.push(line);
|
|
5269
5966
|
return chunks.join("\n");
|
|
5270
5967
|
}
|
|
5968
|
+
/** Print a `source refresh` result, showing old → new for name + slug. */
|
|
5969
|
+
function printRefreshResult(r) {
|
|
5970
|
+
if (!r.changed) {
|
|
5971
|
+
console.log(`${r.routingKey}: up to date (name "${r.newName}", slug "${r.newSlug}")`);
|
|
5972
|
+
return;
|
|
5973
|
+
}
|
|
5974
|
+
console.log(`${r.routingKey}: updated`);
|
|
5975
|
+
if (r.oldName !== r.newName) console.log(` name: ${r.oldName} → ${r.newName}`);
|
|
5976
|
+
if (r.oldSlug !== r.newSlug) console.log(` slug: ${r.oldSlug ?? "(none)"} → ${r.newSlug}`);
|
|
5977
|
+
}
|
|
5271
5978
|
/**
|
|
5272
5979
|
* Format a generic source for display.
|
|
5273
5980
|
*/
|
|
@@ -5375,8 +6082,26 @@ async function buildUniversalGitConfigFromOpts(opts) {
|
|
|
5375
6082
|
function registerSourceCommands(program, getClient) {
|
|
5376
6083
|
const src = program.command("source").description("Manage webhook sources");
|
|
5377
6084
|
const add = src.command("add").description("Add a new webhook source");
|
|
5378
|
-
add.command("github").description("Add a new GitHub App source").requiredOption("--name <name>", "Human-readable source name").
|
|
6085
|
+
add.command("github").description("Add a new GitHub App source").requiredOption("--name <name>", "Human-readable source name").option("--app-id <id>", "GitHub App ID (omit when using --manifest)").option("--private-key <pathOrValue>", "Private key (prefix with @ for file path)").option("--webhook-secret <secret>", "Webhook secret").option("--from-env <varName>", "Read private key from environment variable").option("--stdin", "Read private key from stdin").option("--manifest", "One-click setup: create and configure a new GitHub App via the App Manifest flow").option("--no-browser", "Headless manifest setup: print a URL and paste the setup code back").option("--github-org <slug>", "Create the App under a GitHub org instead of your personal account").option("--webhook-url <url>", "Advanced/self-hosted: bake this https:// URL into the App webhook verbatim and skip platform-mode URL resolution. KiCI adds no ingress at this URL — your own infra owns delivery.").option("--json", "Emit raw JSON (the API response) instead of formatted text").action(async (opts) => {
|
|
6086
|
+
if (opts.manifest) {
|
|
6087
|
+
try {
|
|
6088
|
+
await runGithubManifestSetup({
|
|
6089
|
+
name: opts.name,
|
|
6090
|
+
noBrowser: !opts.browser,
|
|
6091
|
+
githubOrg: opts.githubOrg,
|
|
6092
|
+
webhookUrl: opts.webhookUrl
|
|
6093
|
+
}, getClient());
|
|
6094
|
+
} catch (err) {
|
|
6095
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
6096
|
+
process.exit(1);
|
|
6097
|
+
}
|
|
6098
|
+
return;
|
|
6099
|
+
}
|
|
5379
6100
|
try {
|
|
6101
|
+
if (!opts.appId) {
|
|
6102
|
+
console.error("Error: --app-id is required (or use --manifest for one-click setup)");
|
|
6103
|
+
process.exit(1);
|
|
6104
|
+
}
|
|
5380
6105
|
const privateKey = await resolveSecret({
|
|
5381
6106
|
value: opts.privateKey,
|
|
5382
6107
|
stdin: opts.stdin,
|
|
@@ -5605,6 +6330,34 @@ function registerSourceCommands(program, getClient) {
|
|
|
5605
6330
|
process.exit(1);
|
|
5606
6331
|
}
|
|
5607
6332
|
});
|
|
6333
|
+
src.command("refresh [routingKey]").description("Re-sync a GitHub source's name and slug from GitHub (use --all for every source)").option("--all", "Refresh every GitHub source").option("--json", "Emit raw JSON instead of formatted text").action(async (routingKey, opts) => {
|
|
6334
|
+
try {
|
|
6335
|
+
if (opts.all) {
|
|
6336
|
+
const res = await getClient().post("/api/v1/admin/sources/refresh-all", {});
|
|
6337
|
+
if (opts.json) {
|
|
6338
|
+
console.log(JSON.stringify(res, null, 2));
|
|
6339
|
+
return;
|
|
6340
|
+
}
|
|
6341
|
+
for (const r of res.results) printRefreshResult(r);
|
|
6342
|
+
for (const e of res.errors) console.error(` ${e.routingKey}: ${e.error}`);
|
|
6343
|
+
if (res.results.length === 0 && res.errors.length === 0) console.log("No GitHub sources to refresh.");
|
|
6344
|
+
return;
|
|
6345
|
+
}
|
|
6346
|
+
if (!routingKey) {
|
|
6347
|
+
console.error("Error: provide a <routingKey> or use --all");
|
|
6348
|
+
process.exit(1);
|
|
6349
|
+
}
|
|
6350
|
+
const result = await getClient().post(`/api/v1/admin/sources/${encodeURIComponent(routingKey)}/refresh`, {});
|
|
6351
|
+
if (opts.json) {
|
|
6352
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6353
|
+
return;
|
|
6354
|
+
}
|
|
6355
|
+
printRefreshResult(result);
|
|
6356
|
+
} catch (err) {
|
|
6357
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
6358
|
+
process.exit(1);
|
|
6359
|
+
}
|
|
6360
|
+
});
|
|
5608
6361
|
src.command("update-generic <id>").description("Update a generic webhook source").option("--name <name>", "New name").option("--verification <method>", "Verification method: hmac_sha256, bearer_token, ip_allowlist, none").option("--secret <value>", "New verification secret (prefix with @ for file)").option("--from-env <varName>", "Read verification secret from environment variable").option("--stdin", "Read verification secret from stdin").option("--event-type-header <header>", "Header name for event type extraction").option("--event-type-path <jsonpath>", "JSONPath for event type extraction from body").option("--idempotency-key-header <header>", "Header name for idempotency key").option("--idempotency-key-path <jsonpath>", "JSONPath for idempotency key from body").option("--dedup-window <seconds>", "Dedup window in seconds", parseInt).option("--max-payload <bytes>", "Maximum payload size in bytes", parseInt).option("--allowed-events <events>", "Comma-separated list of allowed event types").option("--strip-headers <headers>", "Comma-separated list of headers to strip").option("--rate-limit <rpm>", "Rate limit in requests per minute", parseInt).option("--preset <name>", "Universal-git preset").option("--git-url-template <url>", "Clone URL template").option("--credential-ref <key>", "Secret key name").option("--credential-store <backend>", "Secret backend name").option("--credential-type <type>", "Credential type: pat, basic, ssh").option("--credential-user <user>", "Username for PAT/basic auth").option("--ssh-host-key-policy <policy>", "SSH host-key policy: accept-new, pinned").option("--ssh-known-hosts-pem <pathOrValue>", "Pinned SSH known_hosts (prefix with @ for file)").option("--clear-git-config", "Revert this source back to a payload-only generic webhook").option("--provider-type <type>", "Provider implementation: generic (default) or local (a git repo on the agent filesystem cloned via file://)").option("--json", "Emit raw JSON (the full source row) instead of formatted text").action(async (id, opts) => {
|
|
5609
6362
|
try {
|
|
5610
6363
|
const secret = await resolveSecret({
|
|
@@ -8597,11 +9350,17 @@ var init_image_digests = __esmMin((() => {
|
|
|
8597
9350
|
* a container-based service management backend that works on any
|
|
8598
9351
|
* platform with Docker or Podman installed.
|
|
8599
9352
|
*/
|
|
8600
|
-
var compose_exports = /* @__PURE__ */ __exportAll({
|
|
9353
|
+
var compose_exports = /* @__PURE__ */ __exportAll({
|
|
9354
|
+
ComposeServiceManager: () => ComposeServiceManager,
|
|
9355
|
+
detectRuntime: () => detectRuntime$1
|
|
9356
|
+
});
|
|
8601
9357
|
/**
|
|
8602
9358
|
* Detect which container runtime is available.
|
|
8603
9359
|
* Tries podman compose first, then docker compose.
|
|
8604
9360
|
*
|
|
9361
|
+
* Exported so the orchestrator installer can resolve the runtime once when
|
|
9362
|
+
* injecting the `KICI_DEPLOY_CONTAINER_RUNTIME` env var for a compose deploy.
|
|
9363
|
+
*
|
|
8605
9364
|
* @throws When neither runtime is found.
|
|
8606
9365
|
*/
|
|
8607
9366
|
function detectRuntime$1() {
|
|
@@ -8851,7 +9610,36 @@ function resolveServiceExecutable(opts) {
|
|
|
8851
9610
|
};
|
|
8852
9611
|
}
|
|
8853
9612
|
//#endregion
|
|
9613
|
+
//#region src/cli/service/deploy-env.ts
|
|
9614
|
+
/** Prefix every line uses; also the strip marker for idempotent re-install. */
|
|
9615
|
+
const DEPLOY_ENV_PREFIX = "KICI_DEPLOY_";
|
|
9616
|
+
/**
|
|
9617
|
+
* Build the `KICI_DEPLOY_*` env lines for a given deployment shape. The mode
|
|
9618
|
+
* line is always emitted; container name + runtime are emitted only for the
|
|
9619
|
+
* `compose` platform (the only shape with a container to `exec` into).
|
|
9620
|
+
*/
|
|
9621
|
+
function buildDeployEnvLines(input) {
|
|
9622
|
+
const lines = [`KICI_DEPLOY_MODE=${input.platform}`];
|
|
9623
|
+
if (input.platform === "compose") {
|
|
9624
|
+
lines.push(`KICI_DEPLOY_CONTAINER=${input.serviceName}`);
|
|
9625
|
+
if (input.containerRuntime) lines.push(`KICI_DEPLOY_CONTAINER_RUNTIME=${input.containerRuntime}`);
|
|
9626
|
+
}
|
|
9627
|
+
return lines;
|
|
9628
|
+
}
|
|
9629
|
+
/**
|
|
9630
|
+
* Append the freshly-computed deploy lines to existing env-file content,
|
|
9631
|
+
* idempotently: any pre-existing `KICI_DEPLOY_*` line is stripped first so a
|
|
9632
|
+
* re-install / upgrade heals the shape rather than duplicating it.
|
|
9633
|
+
*/
|
|
9634
|
+
function upsertDeployEnvLines(existingContent, lines) {
|
|
9635
|
+
const kept = existingContent.split("\n").filter((line) => !line.trimStart().startsWith(DEPLOY_ENV_PREFIX));
|
|
9636
|
+
while (kept.length > 0 && kept[kept.length - 1].trim() === "") kept.pop();
|
|
9637
|
+
const body = kept.join("\n");
|
|
9638
|
+
return `${body.length > 0 ? `${body}\n` : ""}${lines.join("\n")}\n`;
|
|
9639
|
+
}
|
|
9640
|
+
//#endregion
|
|
8854
9641
|
//#region src/cli/commands/shared/versioned-upgrade.ts
|
|
9642
|
+
init_compose();
|
|
8855
9643
|
/**
|
|
8856
9644
|
* Shared versioned directory upgrade logic for kici-admin upgrade commands.
|
|
8857
9645
|
*
|
|
@@ -9663,6 +10451,18 @@ function registerOrchestratorInstall(orchestrator) {
|
|
|
9663
10451
|
fs.appendFileSync(envFilePath, `\nKICI_DATABASE_URL=${devDbUrl}\n`);
|
|
9664
10452
|
console.log(`Appended KICI_DATABASE_URL to ${envFilePath}`);
|
|
9665
10453
|
}
|
|
10454
|
+
let composeRuntime;
|
|
10455
|
+
if (platform === "compose") try {
|
|
10456
|
+
composeRuntime = detectRuntime$1();
|
|
10457
|
+
} catch {
|
|
10458
|
+
composeRuntime = void 0;
|
|
10459
|
+
}
|
|
10460
|
+
const deployLines = buildDeployEnvLines({
|
|
10461
|
+
platform,
|
|
10462
|
+
serviceName,
|
|
10463
|
+
containerRuntime: composeRuntime
|
|
10464
|
+
});
|
|
10465
|
+
fs.writeFileSync(envFilePath, upsertDeployEnvLines(fs.readFileSync(envFilePath, "utf-8"), deployLines), "utf-8");
|
|
9666
10466
|
const entryScript = opts.binary ? void 0 : fileURLToPath(import.meta.resolve(`@kici-dev/orchestrator/${selectServerEntry(fs.readFileSync(envFilePath, "utf-8"))}`));
|
|
9667
10467
|
const { executablePath, args } = resolveServiceExecutable({
|
|
9668
10468
|
binary: opts.binary ? path.resolve(opts.binary) : void 0,
|
|
@@ -10677,19 +11477,33 @@ var JoinTokenManager = class {
|
|
|
10677
11477
|
* `peer_credentials_active_uniq` partial unique index).
|
|
10678
11478
|
*
|
|
10679
11479
|
* On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
|
|
10680
|
-
* / already-used so callers can branch on the
|
|
10681
|
-
* recovery path in peer-handler.ts keys on the
|
|
10682
|
-
* specifically. The follow-up only fires on the
|
|
11480
|
+
* / already-used / reusable-by-same-instance so callers can branch on the
|
|
11481
|
+
* specific reason — the recovery path in peer-handler.ts keys on the
|
|
11482
|
+
* "already used" string specifically. The follow-up only fires on the
|
|
11483
|
+
* unhappy path.
|
|
11484
|
+
*
|
|
11485
|
+
* Self-healing reuse: a join token is re-consumable by the same joining
|
|
11486
|
+
* peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
|
|
11487
|
+
* credential (transient outage / deleted credential file) re-presents the
|
|
11488
|
+
* still-valid join token already in its env; the coordinator accepts the
|
|
11489
|
+
* reuse and issues a fresh credential — no operator action, no cluster
|
|
11490
|
+
* redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
|
|
11491
|
+
* instanceId, so it never widens a leaked token's usefulness beyond the
|
|
11492
|
+
* instance that first consumed it.
|
|
10683
11493
|
*/
|
|
10684
|
-
async validateAndConsumeToken(token, consumedBy) {
|
|
11494
|
+
async validateAndConsumeToken(token, consumedBy, peerInstanceId) {
|
|
10685
11495
|
const parsed = parseToken(token);
|
|
10686
11496
|
const keys = deriveKeys(Buffer.from(parsed.secretHex, "hex"));
|
|
10687
11497
|
const updateResult = await this.deps.db.updateTable("join_tokens").set({
|
|
10688
11498
|
consumed_at: /* @__PURE__ */ new Date(),
|
|
10689
|
-
consumed_by: consumedBy
|
|
11499
|
+
consumed_by: consumedBy,
|
|
11500
|
+
consumed_by_instance: peerInstanceId
|
|
10690
11501
|
}).where("token_hash", "=", keys.validationHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
|
|
10691
11502
|
if (Number(updateResult?.numUpdatedRows ?? 0n) > 0) {
|
|
10692
|
-
joinTokenLogger.info("Consumed join token", {
|
|
11503
|
+
joinTokenLogger.info("Consumed join token", {
|
|
11504
|
+
consumedBy,
|
|
11505
|
+
peerInstanceId
|
|
11506
|
+
});
|
|
10693
11507
|
return {
|
|
10694
11508
|
routing: parsed.routing,
|
|
10695
11509
|
keys
|
|
@@ -10697,8 +11511,21 @@ var JoinTokenManager = class {
|
|
|
10697
11511
|
}
|
|
10698
11512
|
const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
|
|
10699
11513
|
if (!row) throw new Error("Invalid join token");
|
|
10700
|
-
if (row.
|
|
10701
|
-
|
|
11514
|
+
if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
|
|
11515
|
+
if (row.consumed_at) {
|
|
11516
|
+
if (row.consumed_by_instance === peerInstanceId) {
|
|
11517
|
+
joinTokenLogger.info("Re-validated join token for returning instance", {
|
|
11518
|
+
consumedBy,
|
|
11519
|
+
peerInstanceId
|
|
11520
|
+
});
|
|
11521
|
+
return {
|
|
11522
|
+
routing: parsed.routing,
|
|
11523
|
+
keys
|
|
11524
|
+
};
|
|
11525
|
+
}
|
|
11526
|
+
throw new Error(TOKEN_ALREADY_USED_MESSAGE);
|
|
11527
|
+
}
|
|
11528
|
+
throw new Error("Invalid join token");
|
|
10702
11529
|
}
|
|
10703
11530
|
};
|
|
10704
11531
|
/**
|
|
@@ -10765,14 +11592,24 @@ var PeerCredentialStore = class {
|
|
|
10765
11592
|
this.db = db;
|
|
10766
11593
|
}
|
|
10767
11594
|
/**
|
|
10768
|
-
* Save a new peer credential
|
|
11595
|
+
* Save a new peer credential, revoking any prior active credential for the
|
|
11596
|
+
* same instanceId (the `peer_credentials_active_uniq` partial unique index
|
|
11597
|
+
* permits exactly one active credential per instanceId).
|
|
11598
|
+
*
|
|
11599
|
+
* Returns `{ revokedCount }` — how many previously-active credentials this
|
|
11600
|
+
* save revoked. Because a coordinator's peer-clients share one
|
|
11601
|
+
* identity-scoped credential, a `revokedCount > 0` here means every sibling
|
|
11602
|
+
* peer-client of `instanceId` that was authenticating with the old
|
|
11603
|
+
* credential is now invalidated and will be rejected on its next proof. The
|
|
11604
|
+
* caller logs this so a revoke that cascades sibling rejections is visible in
|
|
11605
|
+
* the orchestrator logs.
|
|
10769
11606
|
*/
|
|
10770
11607
|
async save(opts) {
|
|
10771
11608
|
const expiryDays = opts.expiryDays ?? DEFAULT_EXPIRY_DAYS;
|
|
10772
11609
|
const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1e3);
|
|
10773
11610
|
const runOnce = async () => {
|
|
10774
|
-
|
|
10775
|
-
await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).
|
|
11611
|
+
return this.db.transaction().execute(async (trx) => {
|
|
11612
|
+
const revokeResult = await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).executeTakeFirst();
|
|
10776
11613
|
await trx.insertInto("peer_credentials").values({
|
|
10777
11614
|
instance_id: opts.instanceId,
|
|
10778
11615
|
credential_hash: opts.credentialHash,
|
|
@@ -10781,15 +11618,13 @@ var PeerCredentialStore = class {
|
|
|
10781
11618
|
source_token_hash: opts.sourceTokenHash ?? null,
|
|
10782
11619
|
expires_at: expiresAt
|
|
10783
11620
|
}).execute();
|
|
11621
|
+
return Number(revokeResult?.numUpdatedRows ?? 0n);
|
|
10784
11622
|
});
|
|
10785
11623
|
};
|
|
10786
11624
|
try {
|
|
10787
|
-
await runOnce();
|
|
11625
|
+
return { revokedCount: await runOnce() };
|
|
10788
11626
|
} catch (err) {
|
|
10789
|
-
if (err instanceof Error && "code" in err && err.code === "23505") {
|
|
10790
|
-
await runOnce();
|
|
10791
|
-
return;
|
|
10792
|
-
}
|
|
11627
|
+
if (err instanceof Error && "code" in err && err.code === "23505") return { revokedCount: await runOnce() };
|
|
10793
11628
|
throw err;
|
|
10794
11629
|
}
|
|
10795
11630
|
}
|
|
@@ -11005,6 +11840,23 @@ function deriveHostStatus(row, nowMs, graceMs) {
|
|
|
11005
11840
|
return "unreachable";
|
|
11006
11841
|
}
|
|
11007
11842
|
/**
|
|
11843
|
+
* Normalize a stored `host_properties` value into a typed host-vars bag. The pg
|
|
11844
|
+
* driver returns parsed JSON for a `jsonb` column, but accept a JSON string too
|
|
11845
|
+
* (defensive for non-pg paths / tests). Non-object / null reads back as `{}`.
|
|
11846
|
+
*/
|
|
11847
|
+
function parseHostProperties(value) {
|
|
11848
|
+
const parsed = typeof value === "string" ? safeJsonParse(value) : value;
|
|
11849
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
11850
|
+
return {};
|
|
11851
|
+
}
|
|
11852
|
+
function safeJsonParse(value) {
|
|
11853
|
+
try {
|
|
11854
|
+
return JSON.parse(value);
|
|
11855
|
+
} catch {
|
|
11856
|
+
return null;
|
|
11857
|
+
}
|
|
11858
|
+
}
|
|
11859
|
+
/**
|
|
11008
11860
|
* Durable, cluster-shared roster of every agent the cluster has ever enrolled.
|
|
11009
11861
|
*
|
|
11010
11862
|
* The in-memory `AgentRegistry` reconciles into this table on register
|
|
@@ -11022,6 +11874,7 @@ var HostRosterStore = class {
|
|
|
11022
11874
|
/** Idempotent upsert on agent_id; stamps connected_instance_id + last_seen. */
|
|
11023
11875
|
async upsert(input) {
|
|
11024
11876
|
const labelsJson = JSON.stringify(input.labels);
|
|
11877
|
+
const reportedJson = JSON.stringify(input.properties ?? {});
|
|
11025
11878
|
await this.db.insertInto("host_roster").values({
|
|
11026
11879
|
agent_id: input.agentId,
|
|
11027
11880
|
token_id: input.tokenId,
|
|
@@ -11031,6 +11884,7 @@ var HostRosterStore = class {
|
|
|
11031
11884
|
platform: input.platform,
|
|
11032
11885
|
arch: input.arch,
|
|
11033
11886
|
connected_instance_id: input.instanceId,
|
|
11887
|
+
host_properties: reportedJson,
|
|
11034
11888
|
last_seen: sql`now()`,
|
|
11035
11889
|
updated_at: sql`now()`
|
|
11036
11890
|
}).onConflict((oc) => oc.column("agent_id").doUpdateSet({
|
|
@@ -11041,6 +11895,7 @@ var HostRosterStore = class {
|
|
|
11041
11895
|
platform: input.platform,
|
|
11042
11896
|
arch: input.arch,
|
|
11043
11897
|
connected_instance_id: input.instanceId,
|
|
11898
|
+
host_properties: sql`COALESCE(host_roster.host_properties, '{}'::jsonb) || ${reportedJson}::jsonb`,
|
|
11044
11899
|
last_seen: sql`now()`,
|
|
11045
11900
|
updated_at: sql`now()`
|
|
11046
11901
|
})).execute();
|
|
@@ -11065,6 +11920,7 @@ var HostRosterStore = class {
|
|
|
11065
11920
|
labels: JSON.stringify(input.labels),
|
|
11066
11921
|
hostname: input.hostname ?? null,
|
|
11067
11922
|
connected_instance_id: null,
|
|
11923
|
+
host_properties: JSON.stringify(input.properties ?? {}),
|
|
11068
11924
|
last_seen: sql`now()`,
|
|
11069
11925
|
updated_at: sql`now()`
|
|
11070
11926
|
}).onConflict((oc) => oc.column("agent_id").doNothing()).execute();
|
|
@@ -11099,13 +11955,51 @@ var HostRosterStore = class {
|
|
|
11099
11955
|
connectedInstanceId: row.connected_instance_id,
|
|
11100
11956
|
status: deriveHostStatus(row, now, graceMs),
|
|
11101
11957
|
platform: row.platform,
|
|
11102
|
-
arch: row.arch
|
|
11958
|
+
arch: row.arch,
|
|
11959
|
+
properties: parseHostProperties(row.host_properties)
|
|
11103
11960
|
});
|
|
11104
11961
|
}
|
|
11105
11962
|
out.sort((a, b) => a.agentId.localeCompare(b.agentId));
|
|
11106
11963
|
return out;
|
|
11107
11964
|
}
|
|
11108
11965
|
/**
|
|
11966
|
+
* Map a roster row to the canonical {@link HostInventoryEntry} — the queryable
|
|
11967
|
+
* shape returned by the `inventory.query`/`inventory.get` RPC and typed on the
|
|
11968
|
+
* SDK's `ctx.kici.inventory`. Status is derived the same way `findMatching`
|
|
11969
|
+
* derives it (live + fresh ⇒ ready; declared-but-absent static ⇒ unreachable;
|
|
11970
|
+
* ephemeral past ttl ⇒ stale).
|
|
11971
|
+
*/
|
|
11972
|
+
toInventoryEntry(row, graceMs) {
|
|
11973
|
+
return {
|
|
11974
|
+
agentId: row.agent_id,
|
|
11975
|
+
labels: JSON.parse(row.labels),
|
|
11976
|
+
properties: parseHostProperties(row.host_properties),
|
|
11977
|
+
hostname: row.hostname,
|
|
11978
|
+
platform: row.platform,
|
|
11979
|
+
arch: row.arch,
|
|
11980
|
+
lifecycleClass: row.lifecycle_class,
|
|
11981
|
+
status: deriveHostStatus(row, Date.now(), graceMs),
|
|
11982
|
+
lastSeen: new Date(row.last_seen).toISOString()
|
|
11983
|
+
};
|
|
11984
|
+
}
|
|
11985
|
+
/**
|
|
11986
|
+
* Query the roster as canonical {@link HostInventoryEntry} records. With a
|
|
11987
|
+
* selector, reuses `findMatching`'s label filtering (server-side, glob/regex);
|
|
11988
|
+
* property filtering is done client-side in the workflow. Omit the selector ⇒
|
|
11989
|
+
* every host.
|
|
11990
|
+
*/
|
|
11991
|
+
async queryInventory(selector, graceMs) {
|
|
11992
|
+
if (!selector || !selector.include && !selector.exclude) return (await this.listAll()).map((r) => this.toInventoryEntry(r, graceMs));
|
|
11993
|
+
const matched = await this.findMatching(selector.include ?? [], selector.exclude ?? [], graceMs);
|
|
11994
|
+
const byId = new Map(matched.map((m) => [m.agentId, m]));
|
|
11995
|
+
return (await this.listAll()).filter((r) => byId.has(r.agent_id)).map((r) => this.toInventoryEntry(r, graceMs));
|
|
11996
|
+
}
|
|
11997
|
+
/** Single-host inventory lookup; null when the agent is not in the roster. */
|
|
11998
|
+
async getInventory(agentId, graceMs) {
|
|
11999
|
+
const row = await this.get(agentId);
|
|
12000
|
+
return row ? this.toInventoryEntry(row, graceMs) : null;
|
|
12001
|
+
}
|
|
12002
|
+
/**
|
|
11109
12003
|
* Count `static` (declared) hosts whose derived status is `unreachable` —
|
|
11110
12004
|
* the "declared-but-absent" alarm population. Reuses the single-source
|
|
11111
12005
|
* {@link deriveHostStatus} so the count never diverges from what
|
|
@@ -11118,14 +12012,54 @@ var HostRosterStore = class {
|
|
|
11118
12012
|
const now = Date.now();
|
|
11119
12013
|
return rows.filter((r) => deriveHostStatus(r, now, graceMs) === "unreachable").length;
|
|
11120
12014
|
}
|
|
12015
|
+
/** Remove a host from the roster by agent id. Returns rows deleted. */
|
|
12016
|
+
async removeStatic(agentId) {
|
|
12017
|
+
const res = await this.db.deleteFrom("host_roster").where("agent_id", "=", agentId).executeTakeFirst();
|
|
12018
|
+
return Number(res.numDeletedRows ?? 0n);
|
|
12019
|
+
}
|
|
11121
12020
|
/** Delete ephemeral rows whose last_seen is older than ttl. Returns count. */
|
|
11122
12021
|
async reapEphemeralPastTtl(ttlMs) {
|
|
11123
12022
|
const res = await this.db.deleteFrom("host_roster").where("lifecycle_class", "=", "ephemeral").where("last_seen", "<", sql`now() - (${ttlMs}::text || ' milliseconds')::interval`).executeTakeFirst();
|
|
11124
12023
|
return Number(res.numDeletedRows ?? 0n);
|
|
11125
12024
|
}
|
|
12025
|
+
/**
|
|
12026
|
+
* Mark a host as about-to-reboot until `until`. Set by the `host.requestReboot`
|
|
12027
|
+
* API handler when an agent's `restartHost()` step runs. While the value is in
|
|
12028
|
+
* the future, the host's disconnect is the expected reboot and its pinned
|
|
12029
|
+
* post-restart job is held.
|
|
12030
|
+
*/
|
|
12031
|
+
async setRebootPending(agentId, until) {
|
|
12032
|
+
await this.db.updateTable("host_roster").set({
|
|
12033
|
+
reboot_pending_until: until,
|
|
12034
|
+
updated_at: sql`now()`
|
|
12035
|
+
}).where("agent_id", "=", agentId).execute();
|
|
12036
|
+
}
|
|
12037
|
+
/** Clear the reboot-pending flag (on reconnect, deadline expiry, or cancel). */
|
|
12038
|
+
async clearRebootPending(agentId) {
|
|
12039
|
+
await this.db.updateTable("host_roster").set({
|
|
12040
|
+
reboot_pending_until: null,
|
|
12041
|
+
updated_at: sql`now()`
|
|
12042
|
+
}).where("agent_id", "=", agentId).execute();
|
|
12043
|
+
}
|
|
12044
|
+
/** True when the host has a reboot-pending deadline still in the future at `nowMs`. */
|
|
12045
|
+
async isRebootPending(agentId, nowMs) {
|
|
12046
|
+
const row = await this.db.selectFrom("host_roster").select("reboot_pending_until").where("agent_id", "=", agentId).executeTakeFirst();
|
|
12047
|
+
return (row?.reboot_pending_until ? new Date(row.reboot_pending_until).getTime() : 0) > nowMs;
|
|
12048
|
+
}
|
|
12049
|
+
/**
|
|
12050
|
+
* Agent ids whose reboot-pending deadline has passed at `nowMs`. The deadline
|
|
12051
|
+
* sweep clears these; the held post-restart job then hits the queue timeout.
|
|
12052
|
+
*/
|
|
12053
|
+
async listExpiredRebootPending(nowMs) {
|
|
12054
|
+
return (await this.db.selectFrom("host_roster").select("agent_id").where("reboot_pending_until", "is not", null).where("reboot_pending_until", "<=", new Date(nowMs)).execute()).map((r) => r.agent_id);
|
|
12055
|
+
}
|
|
11126
12056
|
};
|
|
11127
12057
|
//#endregion
|
|
11128
12058
|
//#region src/cli/commands/host.ts
|
|
12059
|
+
/** Collect a repeatable `--prop key=value` flag into an array. */
|
|
12060
|
+
function collectProp(value, previous) {
|
|
12061
|
+
return [...previous, value];
|
|
12062
|
+
}
|
|
11129
12063
|
/**
|
|
11130
12064
|
* The CLI reads the roster DB directly (no full orchestrator config load), so
|
|
11131
12065
|
* it sources the grace window from the same env var the orchestrator config
|
|
@@ -11179,13 +12113,15 @@ function registerHostCommands(program) {
|
|
|
11179
12113
|
process.exit(1);
|
|
11180
12114
|
}
|
|
11181
12115
|
});
|
|
11182
|
-
host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").action(async (opts) => {
|
|
12116
|
+
host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").option("--prop <key=value>", "Typed host property (repeatable; true/false ⇒ boolean, numeric ⇒ number)", collectProp, []).action(async (opts) => {
|
|
11183
12117
|
try {
|
|
11184
12118
|
const labels = opts.labels ? String(opts.labels).split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
12119
|
+
const properties = parseHostPropertyAssignments(opts.prop);
|
|
11185
12120
|
await withDb((db) => new HostRosterStore(db).declareStatic({
|
|
11186
12121
|
agentId: opts.agentId,
|
|
11187
12122
|
labels,
|
|
11188
|
-
hostname: opts.hostname
|
|
12123
|
+
hostname: opts.hostname,
|
|
12124
|
+
properties
|
|
11189
12125
|
}));
|
|
11190
12126
|
console.log(`Declared static host: ${opts.agentId}`);
|
|
11191
12127
|
} catch (err) {
|
|
@@ -11193,6 +12129,18 @@ function registerHostCommands(program) {
|
|
|
11193
12129
|
process.exit(1);
|
|
11194
12130
|
}
|
|
11195
12131
|
});
|
|
12132
|
+
host.command("remove").description("Remove a host from the roster").requiredOption("--agent-id <id>", "Agent id to remove").action(async (opts) => {
|
|
12133
|
+
try {
|
|
12134
|
+
if (await withDb((db) => new HostRosterStore(db).removeStatic(opts.agentId)) === 0) {
|
|
12135
|
+
console.error(`No host found: ${opts.agentId}`);
|
|
12136
|
+
process.exit(1);
|
|
12137
|
+
}
|
|
12138
|
+
console.log(`Removed host: ${opts.agentId}`);
|
|
12139
|
+
} catch (err) {
|
|
12140
|
+
console.error(`Error: ${toErrorMessage(err)}`);
|
|
12141
|
+
process.exit(1);
|
|
12142
|
+
}
|
|
12143
|
+
});
|
|
11196
12144
|
}
|
|
11197
12145
|
//#endregion
|
|
11198
12146
|
//#region src/cli/commands/org-settings.ts
|