@kici-dev/shared 0.5.0 → 0.6.1
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/cold-store/chunk-id.js +3 -2
- package/dist/cold-store/config.js +3 -3
- package/dist/container-runtime.d.ts +148 -0
- package/dist/container-runtime.js +298 -0
- package/dist/container-runtime.test.d.ts +2 -0
- package/dist/db-admin.d.ts +0 -1079
- package/dist/db-admin.js +1 -1819
- package/dist/db.js +2 -1
- package/dist/diagnostics/bundle-archive.d.ts +7 -9
- package/dist/diagnostics/bundle-archive.js +12 -85
- package/dist/diagnostics/bundle-chunks.js +1 -1
- package/dist/env/define-env.d.ts +7 -6
- package/dist/env/define-env.js +8 -7
- package/dist/graceful-shutdown.d.ts +6 -2
- package/dist/graceful-shutdown.js +1 -1
- package/dist/idempotency-files.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/telemetry/init.js +2 -1
- package/package.json +13 -7
- package/sbom.spdx.json +391 -391
package/dist/db-admin.js
CHANGED
|
@@ -2,7 +2,6 @@ import "./rolldown-runtime-ClRpJifh.js";
|
|
|
2
2
|
import { createPool } from "./db.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import pg from "pg";
|
|
5
|
-
import { HoldType, unknownContributorHoldReason } from "@kici-dev/engine";
|
|
6
5
|
//#region src/db-admin.ts
|
|
7
6
|
/**
|
|
8
7
|
* Admin/DB-operations helpers shared between `kici-admin` (orchestrator DB)
|
|
@@ -63,23 +62,6 @@ async function withAdminPool(adminUrl, fn) {
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
/**
|
|
66
|
-
* DROP `dbName` (if it exists). Terminates existing backend connections
|
|
67
|
-
* so the DROP doesn't block. Idempotent — drops are IF EXISTS.
|
|
68
|
-
*
|
|
69
|
-
* Used by e2e cleanup after a full-lifecycle service-deploy test tears
|
|
70
|
-
* down its isolated database. Shares the same admin-URL + identifier-
|
|
71
|
-
* validation + backend-termination scaffolding as dropAndCreateDatabase
|
|
72
|
-
* so the two helpers cannot drift.
|
|
73
|
-
*/
|
|
74
|
-
async function dropDatabaseDirect(databaseUrl) {
|
|
75
|
-
const { adminUrl, dbName } = parseDatabaseUrl(databaseUrl);
|
|
76
|
-
assertValidIdentifier(dbName, "database name");
|
|
77
|
-
await withAdminPool(adminUrl, async (pool) => {
|
|
78
|
-
await pool.query("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()", [dbName]);
|
|
79
|
-
await pool.query(`DROP DATABASE IF EXISTS "${dbName}"`);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
65
|
* Drop `dbName` (if it exists), then recreate it owned by `owner`. Terminates
|
|
84
66
|
* existing backend connections so the DROP doesn't block.
|
|
85
67
|
*/
|
|
@@ -243,27 +225,6 @@ async function clearDispatchQueueDirect(databaseUrl) {
|
|
|
243
225
|
}
|
|
244
226
|
}
|
|
245
227
|
/**
|
|
246
|
-
* TRUNCATE the three scaler-state tables on the orchestrator DB (direct SQL).
|
|
247
|
-
*
|
|
248
|
-
* These tables (`scaler_reservations`, `scaler_spawning_agents`,
|
|
249
|
-
* `scaler_agent_jobs`) hold in-flight scaler bookkeeping that a boot / Raft
|
|
250
|
-
* leader switch rehydrates via `ScalerManager.recoverState`. A row for an agent
|
|
251
|
-
* that spawned but never registered (or whose reservation leaked) is counted as
|
|
252
|
-
* used capacity forever, so a warm-reused orchestrator DB can accumulate stale
|
|
253
|
-
* reservations that push `used` past the resource cap and stop every future
|
|
254
|
-
* scale-up. E2E setups that reuse a warm orch DB call this before the
|
|
255
|
-
* orchestrator starts so it rehydrates a clean slate — the DB analog of
|
|
256
|
-
* allocating a fresh machine-ledger directory per run.
|
|
257
|
-
*/
|
|
258
|
-
async function clearScalerStateDirect(databaseUrl) {
|
|
259
|
-
const pool = createPool(databaseUrl);
|
|
260
|
-
try {
|
|
261
|
-
await pool.query("TRUNCATE scaler_reservations, scaler_spawning_agents, scaler_agent_jobs");
|
|
262
|
-
} finally {
|
|
263
|
-
await pool.end();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
228
|
* DELETE orphan execution_runs + execution_jobs for routing keys other than
|
|
268
229
|
* `routingKey` (and rows with NULL routing_key). Returns row counts.
|
|
269
230
|
*/
|
|
@@ -891,24 +852,6 @@ async function showExecutionRunDirect(databaseUrl, opts) {
|
|
|
891
852
|
}
|
|
892
853
|
}
|
|
893
854
|
/**
|
|
894
|
-
* READ-ONLY: list execution_jobs for a given execution_runs.id. Ordered by
|
|
895
|
-
* created_at ASC so downstream diffs show timeline order.
|
|
896
|
-
*/
|
|
897
|
-
async function listExecutionJobsDirect(databaseUrl, opts) {
|
|
898
|
-
const pool = createPool(databaseUrl);
|
|
899
|
-
try {
|
|
900
|
-
return { jobs: (await pool.query(`SELECT j.id, j.run_id, j.job_id, j.job_name, j.status, j.agent_id,
|
|
901
|
-
j.started_at, j.completed_at, j.duration_ms, j.created_at, j.error_message,
|
|
902
|
-
j.contexts
|
|
903
|
-
FROM execution_jobs j
|
|
904
|
-
INNER JOIN execution_runs r ON r.run_id = j.run_id
|
|
905
|
-
WHERE r.run_id::text = $1 OR r.id::text = $1
|
|
906
|
-
ORDER BY j.created_at ASC`, [opts.runId])).rows };
|
|
907
|
-
} finally {
|
|
908
|
-
await pool.end();
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
855
|
* READ-ONLY: list workflow_registrations with optional filters. Also returns
|
|
913
856
|
* the latest registry_versions.version so callers can assert registry bumps
|
|
914
857
|
* without a second round trip.
|
|
@@ -1118,48 +1061,6 @@ async function emitKiciEventDirect(databaseUrl, opts) {
|
|
|
1118
1061
|
}
|
|
1119
1062
|
}
|
|
1120
1063
|
/**
|
|
1121
|
-
* Upsert a row into `generic_webhook_sources`. Uses
|
|
1122
|
-
* `ON CONFLICT (routing_key) DO UPDATE` so warm-start mode (where the source
|
|
1123
|
-
* may already exist from a prior run) is idempotent.
|
|
1124
|
-
*
|
|
1125
|
-
* IMPORTANT: callers must invoke this BEFORE the orchestrator starts, because
|
|
1126
|
-
* GenericSourceManager caches sources at boot and does not reload them later.
|
|
1127
|
-
* This helper supersedes seedGenericWebhookSource() in
|
|
1128
|
-
* e2e/helpers/local-webhook.ts.
|
|
1129
|
-
*/
|
|
1130
|
-
async function seedGenericWebhookSourceDirect(databaseUrl, opts) {
|
|
1131
|
-
const pool = createPool(databaseUrl);
|
|
1132
|
-
try {
|
|
1133
|
-
await pool.query(`INSERT INTO generic_webhook_sources (
|
|
1134
|
-
id, customer_id, name, routing_key,
|
|
1135
|
-
verification_method, verification_config,
|
|
1136
|
-
event_type_header, dedup_window_seconds,
|
|
1137
|
-
max_payload_bytes, rate_limit_rpm, enabled,
|
|
1138
|
-
provider_type, git_config
|
|
1139
|
-
) VALUES ($1, $2, $3, $4, $5, '{}', 'x-event-type', 300, 10485760, 600, true, $6, $7::jsonb)
|
|
1140
|
-
ON CONFLICT (routing_key) DO UPDATE SET
|
|
1141
|
-
customer_id = EXCLUDED.customer_id,
|
|
1142
|
-
name = EXCLUDED.name,
|
|
1143
|
-
verification_method = EXCLUDED.verification_method,
|
|
1144
|
-
verification_config = EXCLUDED.verification_config,
|
|
1145
|
-
provider_type = EXCLUDED.provider_type,
|
|
1146
|
-
git_config = EXCLUDED.git_config,
|
|
1147
|
-
enabled = true,
|
|
1148
|
-
deleted_at = NULL,
|
|
1149
|
-
updated_at = NOW()`, [
|
|
1150
|
-
opts.sourceId,
|
|
1151
|
-
opts.orgId,
|
|
1152
|
-
opts.name,
|
|
1153
|
-
opts.routingKey,
|
|
1154
|
-
opts.verificationMethod ?? "none",
|
|
1155
|
-
opts.providerType ?? "generic",
|
|
1156
|
-
opts.gitConfig ? JSON.stringify(opts.gitConfig) : null
|
|
1157
|
-
]);
|
|
1158
|
-
} finally {
|
|
1159
|
-
await pool.end();
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
/**
|
|
1163
1064
|
* Return `{ current: true }` if the applied migration count matches the
|
|
1164
1065
|
* provider's migration count AND the content hash in `_migration_content_hash`
|
|
1165
1066
|
* matches the provider's current hash. Otherwise return `{ current: false,
|
|
@@ -1211,449 +1112,6 @@ async function purgeSecretBackendsDirect(databaseUrl) {
|
|
|
1211
1112
|
}
|
|
1212
1113
|
}
|
|
1213
1114
|
/**
|
|
1214
|
-
* Check if an API key exists in the Platform DB (api_keys table — orchestrator-
|
|
1215
|
-
* managed, NOT user_api_keys). Returns true when a row matches the hashed key.
|
|
1216
|
-
*/
|
|
1217
|
-
async function apiKeyExistsDirect(databaseUrl, apiKey) {
|
|
1218
|
-
const keyHash = createHash("sha256").update(apiKey).digest("hex");
|
|
1219
|
-
const pool = createPool(databaseUrl);
|
|
1220
|
-
try {
|
|
1221
|
-
return (await pool.query("SELECT 1 FROM api_keys WHERE key_hash = $1", [keyHash])).rows.length > 0;
|
|
1222
|
-
} finally {
|
|
1223
|
-
await pool.end();
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
/**
|
|
1227
|
-
* Insert an api_keys row (Platform-side orchestrator credential). Used by
|
|
1228
|
-
* e2e setup to seed an orchestrator authentication token. Returns the id.
|
|
1229
|
-
*/
|
|
1230
|
-
async function seedApiKeyInlineDirect(databaseUrl, opts) {
|
|
1231
|
-
const keyHash = createHash("sha256").update(opts.fullKey).digest("hex");
|
|
1232
|
-
const keyPrefix = opts.fullKey.slice(0, 16);
|
|
1233
|
-
const pool = createPool(databaseUrl);
|
|
1234
|
-
try {
|
|
1235
|
-
return { keyId: (await pool.query(`INSERT INTO api_keys (key_hash, key_prefix, name, org_id)
|
|
1236
|
-
VALUES ($1, $2, $3, $4)
|
|
1237
|
-
RETURNING id`, [
|
|
1238
|
-
keyHash,
|
|
1239
|
-
keyPrefix,
|
|
1240
|
-
opts.keyName,
|
|
1241
|
-
opts.orgId
|
|
1242
|
-
])).rows[0].id };
|
|
1243
|
-
} finally {
|
|
1244
|
-
await pool.end();
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
/**
|
|
1248
|
-
* Lookup a `platform_connections` row by connection_id. Returns true when
|
|
1249
|
-
* present. Used by the orphan-sweeper e2e test.
|
|
1250
|
-
*/
|
|
1251
|
-
async function platformConnectionExistsDirect(databaseUrl, connectionId) {
|
|
1252
|
-
const pool = createPool(databaseUrl);
|
|
1253
|
-
try {
|
|
1254
|
-
const res = await pool.query(`SELECT 1 FROM platform_connections WHERE connection_id = $1 LIMIT 1`, [connectionId]);
|
|
1255
|
-
return res.rowCount !== null && res.rowCount > 0;
|
|
1256
|
-
} finally {
|
|
1257
|
-
await pool.end();
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
/**
|
|
1261
|
-
* Count `webhook_sources` rows for a given orchestrator_connection_id.
|
|
1262
|
-
* Used by the orphan-sweeper e2e test to assert the FK CASCADE introduced
|
|
1263
|
-
* by Platform migration 017 actually fires when the parent
|
|
1264
|
-
* `platform_connections` row is deleted.
|
|
1265
|
-
*/
|
|
1266
|
-
/**
|
|
1267
|
-
* Look up a Platform-side `webhook_sources` row by routing key. Returns the
|
|
1268
|
-
* row (org_id + provider + connection) or null. Used by E2E to assert that a
|
|
1269
|
-
* source added at runtime on the orchestrator propagated to the Platform's
|
|
1270
|
-
* `webhook_sources` table (the dashboard-visible source list) without a
|
|
1271
|
-
* restart.
|
|
1272
|
-
*/
|
|
1273
|
-
async function getWebhookSourceByRoutingKeyDirect(databaseUrl, routingKey) {
|
|
1274
|
-
const pool = createPool(databaseUrl);
|
|
1275
|
-
try {
|
|
1276
|
-
const row = (await pool.query(`SELECT routing_key, org_id, provider
|
|
1277
|
-
FROM webhook_sources
|
|
1278
|
-
WHERE routing_key = $1
|
|
1279
|
-
LIMIT 1`, [routingKey])).rows[0];
|
|
1280
|
-
return row ? {
|
|
1281
|
-
routing_key: String(row.routing_key),
|
|
1282
|
-
org_id: String(row.org_id),
|
|
1283
|
-
provider: String(row.provider)
|
|
1284
|
-
} : null;
|
|
1285
|
-
} finally {
|
|
1286
|
-
await pool.end();
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
async function countWebhookSourcesByConnectionIdDirect(databaseUrl, connectionId) {
|
|
1290
|
-
const pool = createPool(databaseUrl);
|
|
1291
|
-
try {
|
|
1292
|
-
const res = await pool.query(`SELECT COUNT(*)::int AS cnt
|
|
1293
|
-
FROM webhook_sources
|
|
1294
|
-
WHERE orchestrator_connection_id = $1`, [connectionId]);
|
|
1295
|
-
return Number(res.rows[0]?.cnt ?? 0);
|
|
1296
|
-
} finally {
|
|
1297
|
-
await pool.end();
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Find a `user_api_keys.id` preferring rows scoped to `preferredOrgId`, else
|
|
1302
|
-
* any row in the table. Used by orphan-sweeper test to get a realistic
|
|
1303
|
-
* `key_id` for platform_connections seeding.
|
|
1304
|
-
*/
|
|
1305
|
-
async function findAnyUserApiKeyIdDirect(databaseUrl, preferredOrgId) {
|
|
1306
|
-
const pool = createPool(databaseUrl);
|
|
1307
|
-
try {
|
|
1308
|
-
const scoped = await pool.query(`SELECT id FROM user_api_keys WHERE org_id = $1 LIMIT 1`, [preferredOrgId]);
|
|
1309
|
-
if (scoped.rows.length > 0) return String(scoped.rows[0].id);
|
|
1310
|
-
const any = await pool.query(`SELECT id FROM user_api_keys LIMIT 1`);
|
|
1311
|
-
return any.rows.length > 0 ? String(any.rows[0].id) : null;
|
|
1312
|
-
} finally {
|
|
1313
|
-
await pool.end();
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
/**
|
|
1317
|
-
* Seed or refresh a synthetic GitHub webhook source on the Platform DB.
|
|
1318
|
-
* Used by HMAC E2E tests that post to `/webhook/:orgId/github`.
|
|
1319
|
-
* Idempotent — refreshes the secret/org_id via ON CONFLICT DO UPDATE,
|
|
1320
|
-
* and clears stale rows with a different routing_key under the same
|
|
1321
|
-
* (org_id, provider, connection_id) triple.
|
|
1322
|
-
*/
|
|
1323
|
-
async function seedSyntheticGithubSourceDirect(databaseUrl, opts) {
|
|
1324
|
-
const pool = createPool(databaseUrl);
|
|
1325
|
-
try {
|
|
1326
|
-
await pool.query(`INSERT INTO platform_connections (
|
|
1327
|
-
org_id, instance_id, connection_id, key_id, routing_keys
|
|
1328
|
-
)
|
|
1329
|
-
VALUES ($1, 'e2e-synthetic-instance', 'e2e-synthetic',
|
|
1330
|
-
'00000000-0000-4000-8000-000000000000', $2::text)
|
|
1331
|
-
ON CONFLICT (connection_id) DO UPDATE
|
|
1332
|
-
SET org_id = $1,
|
|
1333
|
-
routing_keys = $2::text,
|
|
1334
|
-
last_heartbeat_at = NOW()`, [opts.orgId, JSON.stringify([opts.routingKey])]);
|
|
1335
|
-
await pool.query(`DELETE FROM webhook_sources
|
|
1336
|
-
WHERE org_id = $1 AND provider = 'github'
|
|
1337
|
-
AND orchestrator_connection_id = 'e2e-synthetic'
|
|
1338
|
-
AND routing_key != $2`, [opts.orgId, opts.routingKey]);
|
|
1339
|
-
await pool.query(`INSERT INTO webhook_sources (routing_key, provider, orchestrator_connection_id, org_id, name, subtype, slug)
|
|
1340
|
-
VALUES ($1, 'github', 'e2e-synthetic', $2, $3, $4, $5)
|
|
1341
|
-
ON CONFLICT (routing_key, orchestrator_connection_id)
|
|
1342
|
-
DO UPDATE SET org_id = $2, name = $3, subtype = $4, slug = $5`, [
|
|
1343
|
-
opts.routingKey,
|
|
1344
|
-
opts.orgId,
|
|
1345
|
-
opts.name ?? null,
|
|
1346
|
-
opts.subtype ?? null,
|
|
1347
|
-
opts.slug ?? null
|
|
1348
|
-
]);
|
|
1349
|
-
} finally {
|
|
1350
|
-
await pool.end();
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Seed a webhook secret into the orchestrator's `scoped_secrets` table,
|
|
1355
|
-
* encrypted with the caller-supplied key. Ensures a `sources` row exists
|
|
1356
|
-
* for the routing_key. Used by e2e setup on the orchestrator DB side.
|
|
1357
|
-
*
|
|
1358
|
-
* encryptFn takes plaintext + AAD and returns ciphertext bytes. The
|
|
1359
|
-
* caller owns the crypto primitive so this helper stays decoupled from
|
|
1360
|
-
* the orchestrator's PgSecretStore crypto module.
|
|
1361
|
-
*
|
|
1362
|
-
* `customerId` is the org the source belongs to, and passing it is what makes
|
|
1363
|
-
* this seed faithful to a deployed environment. `sources.customer_id` carries a
|
|
1364
|
-
* column DEFAULT of `'__default__'`, so a row inserted without it lands on the
|
|
1365
|
-
* no-tenant anchor — and `resolveOrgId` then reports `'__default__'` for every
|
|
1366
|
-
* event on that routing key, which denies org-scoped features (global-workflow
|
|
1367
|
-
* registration, the multi-provider lock fallback) for reasons that surface
|
|
1368
|
-
* nowhere near the source row. Supplied on both paths so a row an earlier seed
|
|
1369
|
-
* left on the default anchor is repaired rather than inherited; that is exactly
|
|
1370
|
-
* what the staging deploy's own `updateSourcesCustomerId()` step does after the
|
|
1371
|
-
* fact. Omit it only when the caller genuinely has no org (a single-tenant
|
|
1372
|
-
* fixture), never because it is inconvenient to thread.
|
|
1373
|
-
*/
|
|
1374
|
-
async function seedWebhookSecretDirect(databaseUrl, opts) {
|
|
1375
|
-
const { randomBytes } = await import("node:crypto");
|
|
1376
|
-
const pool = createPool(databaseUrl);
|
|
1377
|
-
try {
|
|
1378
|
-
let sourceId;
|
|
1379
|
-
const sourceResult = await pool.query("SELECT id FROM sources WHERE routing_key = $1", [opts.routingKey]);
|
|
1380
|
-
if (sourceResult.rows.length > 0) {
|
|
1381
|
-
sourceId = sourceResult.rows[0].id;
|
|
1382
|
-
if (opts.customerId) await pool.query("UPDATE sources SET customer_id = $1, updated_at = now() WHERE routing_key = $2", [opts.customerId, opts.routingKey]);
|
|
1383
|
-
} else {
|
|
1384
|
-
sourceId = randomBytes(16).toString("hex");
|
|
1385
|
-
const [provider, appId] = opts.routingKey.split(":");
|
|
1386
|
-
await pool.query(`INSERT INTO sources (id, provider, name, routing_key, config${opts.customerId ? ", customer_id" : ""})
|
|
1387
|
-
VALUES ($1, $2, $3, $4, $5${opts.customerId ? ", $6" : ""})
|
|
1388
|
-
${opts.customerId ? "ON CONFLICT (routing_key) DO UPDATE SET customer_id = EXCLUDED.customer_id, updated_at = now()" : "ON CONFLICT (routing_key) DO NOTHING"}`, [
|
|
1389
|
-
sourceId,
|
|
1390
|
-
provider || "github",
|
|
1391
|
-
`e2e-${appId}`,
|
|
1392
|
-
opts.routingKey,
|
|
1393
|
-
JSON.stringify({ appId: appId || "" }),
|
|
1394
|
-
...opts.customerId ? [opts.customerId] : []
|
|
1395
|
-
]);
|
|
1396
|
-
sourceId = (await pool.query("SELECT id FROM sources WHERE routing_key = $1", [opts.routingKey])).rows[0].id;
|
|
1397
|
-
}
|
|
1398
|
-
const scope = `__source__/${sourceId}`;
|
|
1399
|
-
const orgId = "__system__";
|
|
1400
|
-
const aad = `${orgId}:${scope}:webhookSecret`;
|
|
1401
|
-
const encrypted = opts.encryptFn(opts.webhookSecret, aad);
|
|
1402
|
-
await pool.query(`INSERT INTO scoped_secrets (org_id, scope, key, encrypted_value, backend_type, key_version)
|
|
1403
|
-
VALUES ($1, $2, 'webhookSecret', $3, 'pg', 1)
|
|
1404
|
-
ON CONFLICT (org_id, scope, key) DO UPDATE SET encrypted_value = $3, updated_at = now()`, [
|
|
1405
|
-
orgId,
|
|
1406
|
-
scope,
|
|
1407
|
-
encrypted
|
|
1408
|
-
]);
|
|
1409
|
-
return { sourceId };
|
|
1410
|
-
} finally {
|
|
1411
|
-
await pool.end();
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
/**
|
|
1415
|
-
* Seed a source private key into the orchestrator's `scoped_secrets` table.
|
|
1416
|
-
* encryptFn signature matches seedWebhookSecretDirect. Returns null if the
|
|
1417
|
-
* sources row is missing (matches legacy warning-and-skip behaviour).
|
|
1418
|
-
*/
|
|
1419
|
-
async function seedSourcePrivateKeyDirect(databaseUrl, opts) {
|
|
1420
|
-
const pool = createPool(databaseUrl);
|
|
1421
|
-
try {
|
|
1422
|
-
const sourceResult = await pool.query("SELECT id FROM sources WHERE routing_key = $1", [opts.routingKey]);
|
|
1423
|
-
if (sourceResult.rows.length === 0) return null;
|
|
1424
|
-
const sourceId = sourceResult.rows[0].id;
|
|
1425
|
-
const scope = `__source__/${sourceId}`;
|
|
1426
|
-
const orgId = "__system__";
|
|
1427
|
-
const aad = `${orgId}:${scope}:privateKey`;
|
|
1428
|
-
const encrypted = opts.encryptFn(opts.privateKey, aad);
|
|
1429
|
-
await pool.query(`INSERT INTO scoped_secrets (org_id, scope, key, encrypted_value, backend_type, key_version)
|
|
1430
|
-
VALUES ($1, $2, 'privateKey', $3, 'pg', 1)
|
|
1431
|
-
ON CONFLICT (org_id, scope, key) DO UPDATE SET encrypted_value = $3, updated_at = now()`, [
|
|
1432
|
-
orgId,
|
|
1433
|
-
scope,
|
|
1434
|
-
encrypted
|
|
1435
|
-
]);
|
|
1436
|
-
return { sourceId };
|
|
1437
|
-
} finally {
|
|
1438
|
-
await pool.end();
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
|
-
* Bump `registry_versions.version` for the default registry and return the
|
|
1443
|
-
* new value. Used by cron-scheduler e2e to retrigger index after a manual
|
|
1444
|
-
* workflow change.
|
|
1445
|
-
*/
|
|
1446
|
-
async function bumpRegistryVersionDirect(databaseUrl) {
|
|
1447
|
-
const pool = createPool(databaseUrl);
|
|
1448
|
-
try {
|
|
1449
|
-
return (await pool.query(`UPDATE registry_versions
|
|
1450
|
-
SET version = version + 1, updated_at = NOW()
|
|
1451
|
-
WHERE id = 'default'
|
|
1452
|
-
RETURNING version`)).rows[0].version;
|
|
1453
|
-
} finally {
|
|
1454
|
-
await pool.end();
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
/**
|
|
1458
|
-
* Poll `kici_events` for an event matching `eventName` created after `since`.
|
|
1459
|
-
* Returns the newest match, or throws on timeout. Used by e2e event-routing tests.
|
|
1460
|
-
*/
|
|
1461
|
-
async function pollKiciEventsDirect(databaseUrl, opts) {
|
|
1462
|
-
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
1463
|
-
const pollInterval = opts.pollIntervalMs ?? 2e3;
|
|
1464
|
-
const deadline = Date.now() + timeoutMs;
|
|
1465
|
-
const pool = createPool(databaseUrl);
|
|
1466
|
-
try {
|
|
1467
|
-
while (Date.now() < deadline) {
|
|
1468
|
-
const filter = opts.payloadFilter;
|
|
1469
|
-
const result = filter ? await pool.query(`SELECT * FROM kici_events
|
|
1470
|
-
WHERE event_name = $1 AND created_at > $2 AND payload->>$3 = $4
|
|
1471
|
-
ORDER BY created_at DESC
|
|
1472
|
-
LIMIT 1`, [
|
|
1473
|
-
opts.eventName,
|
|
1474
|
-
opts.since.toISOString(),
|
|
1475
|
-
filter.key,
|
|
1476
|
-
filter.value
|
|
1477
|
-
]) : await pool.query(`SELECT * FROM kici_events
|
|
1478
|
-
WHERE event_name = $1 AND created_at > $2
|
|
1479
|
-
ORDER BY created_at DESC
|
|
1480
|
-
LIMIT 1`, [opts.eventName, opts.since.toISOString()]);
|
|
1481
|
-
if (result.rows.length > 0) return result.rows[0];
|
|
1482
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
1483
|
-
}
|
|
1484
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for kici_event '${opts.eventName}' since ${opts.since.toISOString()}`);
|
|
1485
|
-
} finally {
|
|
1486
|
-
await pool.end();
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Ping a PostgreSQL database; retries until `SELECT 1` succeeds or the
|
|
1491
|
-
* timeout expires. Used by e2e startup to wait for Postgres readiness.
|
|
1492
|
-
*/
|
|
1493
|
-
async function waitForPostgresDirect(databaseUrl, opts) {
|
|
1494
|
-
const timeout = opts?.timeoutMs ?? 3e4;
|
|
1495
|
-
const interval = opts?.intervalMs ?? 2e3;
|
|
1496
|
-
const deadline = Date.now() + timeout;
|
|
1497
|
-
while (Date.now() < deadline) {
|
|
1498
|
-
const pool = createPool(databaseUrl);
|
|
1499
|
-
try {
|
|
1500
|
-
await pool.query("SELECT 1");
|
|
1501
|
-
await pool.end();
|
|
1502
|
-
return;
|
|
1503
|
-
} catch {
|
|
1504
|
-
await pool.end();
|
|
1505
|
-
}
|
|
1506
|
-
await new Promise((r) => setTimeout(r, interval));
|
|
1507
|
-
}
|
|
1508
|
-
throw new Error(`PostgreSQL at ${databaseUrl} did not become available within ${timeout}ms`);
|
|
1509
|
-
}
|
|
1510
|
-
/**
|
|
1511
|
-
* Wait for a specific execution_runs row to reach a terminal status.
|
|
1512
|
-
* Used by test-pipeline e2e to gate on run completion. Terminal statuses
|
|
1513
|
-
* are success / failed / cancelled / timed_out_stale.
|
|
1514
|
-
*/
|
|
1515
|
-
async function waitForRunCompletionDirect(databaseUrl, runId, opts) {
|
|
1516
|
-
const timeoutMs = opts?.timeoutMs ?? 12e4;
|
|
1517
|
-
const intervalMs = opts?.intervalMs ?? 2e3;
|
|
1518
|
-
const terminalStatuses = [
|
|
1519
|
-
"success",
|
|
1520
|
-
"failed",
|
|
1521
|
-
"cancelled",
|
|
1522
|
-
"timed_out_stale"
|
|
1523
|
-
];
|
|
1524
|
-
const deadline = Date.now() + timeoutMs;
|
|
1525
|
-
const pool = createPool(databaseUrl);
|
|
1526
|
-
try {
|
|
1527
|
-
while (Date.now() < deadline) {
|
|
1528
|
-
const result = await pool.query(`SELECT status FROM execution_runs WHERE run_id = $1 LIMIT 1`, [runId]);
|
|
1529
|
-
if (result.rows.length > 0) {
|
|
1530
|
-
const status = result.rows[0].status;
|
|
1531
|
-
if (terminalStatuses.includes(status)) return { status };
|
|
1532
|
-
}
|
|
1533
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1534
|
-
}
|
|
1535
|
-
throw new Error(`Run ${runId} did not complete within ${timeoutMs}ms`);
|
|
1536
|
-
} finally {
|
|
1537
|
-
await pool.end();
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* DELETE from execution_jobs + execution_runs where started_at > since.
|
|
1542
|
-
* Used by e2e cleanup for tests that want explicit post-test row cleanup.
|
|
1543
|
-
*/
|
|
1544
|
-
async function cleanupExecutionRowsDirect(databaseUrl, since) {
|
|
1545
|
-
const pool = createPool(databaseUrl);
|
|
1546
|
-
try {
|
|
1547
|
-
const jobsResult = await pool.query(`DELETE FROM execution_jobs WHERE run_id IN (
|
|
1548
|
-
SELECT run_id FROM execution_runs WHERE started_at > $1
|
|
1549
|
-
)`, [since]);
|
|
1550
|
-
return {
|
|
1551
|
-
runs: (await pool.query(`DELETE FROM execution_runs WHERE started_at > $1`, [since])).rowCount ?? 0,
|
|
1552
|
-
jobs: jobsResult.rowCount ?? 0
|
|
1553
|
-
};
|
|
1554
|
-
} finally {
|
|
1555
|
-
await pool.end();
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Check whether the schema is "current" by comparing applied migration count
|
|
1560
|
-
* and content hash against a caller-supplied set of migration files.
|
|
1561
|
-
* Returns false if the migration table is missing, counts mismatch, or the
|
|
1562
|
-
* stored hash differs from the caller-supplied hash. Used by e2e warm-start.
|
|
1563
|
-
*/
|
|
1564
|
-
async function isSchemaCurrentFromFilesDirect(databaseUrl, opts) {
|
|
1565
|
-
const tableName = opts.tableName ?? "kysely_migration";
|
|
1566
|
-
const pool = createPool(databaseUrl);
|
|
1567
|
-
try {
|
|
1568
|
-
try {
|
|
1569
|
-
if (((await pool.query(`SELECT COUNT(*)::int AS count FROM "${tableName}"`)).rows[0]?.count ?? 0) !== opts.expectedCount) return false;
|
|
1570
|
-
const hashResult = await pool.query(`SELECT hash FROM ${MIGRATION_HASH_TABLE} WHERE table_name = $1`, [tableName]);
|
|
1571
|
-
if (hashResult.rows.length === 0 || hashResult.rows[0].hash !== opts.expectedContentHash) return false;
|
|
1572
|
-
return true;
|
|
1573
|
-
} catch {
|
|
1574
|
-
return false;
|
|
1575
|
-
}
|
|
1576
|
-
} finally {
|
|
1577
|
-
await pool.end();
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
/**
|
|
1581
|
-
* Store a migration content hash in the `_migration_content_hash` marker
|
|
1582
|
-
* table. Creates the table if missing. Used by e2e freshDatabase() after
|
|
1583
|
-
* migrations run so warm-start detection can compare on next run.
|
|
1584
|
-
*/
|
|
1585
|
-
async function storeMigrationContentHashInTableDirect(databaseUrl, opts) {
|
|
1586
|
-
const tableName = opts.tableName ?? "kysely_migration";
|
|
1587
|
-
const pool = createPool(databaseUrl);
|
|
1588
|
-
try {
|
|
1589
|
-
await pool.query(`
|
|
1590
|
-
CREATE TABLE IF NOT EXISTS ${MIGRATION_HASH_TABLE} (
|
|
1591
|
-
table_name text PRIMARY KEY,
|
|
1592
|
-
hash text NOT NULL
|
|
1593
|
-
)
|
|
1594
|
-
`);
|
|
1595
|
-
await pool.query(`INSERT INTO ${MIGRATION_HASH_TABLE} (table_name, hash) VALUES ($1, $2)
|
|
1596
|
-
ON CONFLICT (table_name) DO UPDATE SET hash = $2`, [tableName, opts.contentHash]);
|
|
1597
|
-
} finally {
|
|
1598
|
-
await pool.end();
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
/**
|
|
1602
|
-
* Insert a join_tokens row (orchestrator DB) for cluster peer auth.
|
|
1603
|
-
* Used by cluster e2e helpers to provision a shared secret the second
|
|
1604
|
-
* orchestrator will use when joining the cluster.
|
|
1605
|
-
*/
|
|
1606
|
-
async function createJoinTokenDirect(databaseUrl, opts) {
|
|
1607
|
-
const pool = createPool(databaseUrl);
|
|
1608
|
-
try {
|
|
1609
|
-
await pool.query(`INSERT INTO join_tokens (id, token_hash, routing_info, role, created_by, expires_at)
|
|
1610
|
-
VALUES ($1, $2, $3, $4, $5, $6)`, [
|
|
1611
|
-
opts.id,
|
|
1612
|
-
opts.tokenHash,
|
|
1613
|
-
JSON.stringify(opts.routingInfo),
|
|
1614
|
-
opts.role,
|
|
1615
|
-
opts.createdBy,
|
|
1616
|
-
opts.expiresAt
|
|
1617
|
-
]);
|
|
1618
|
-
} finally {
|
|
1619
|
-
await pool.end();
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
/**
|
|
1623
|
-
* Delete join_tokens rows by `created_by` (orchestrator DB). Used by cluster
|
|
1624
|
-
* E2E tests to clean up test-provisioned tokens between runs.
|
|
1625
|
-
*/
|
|
1626
|
-
async function deleteJoinTokensByCreatedByDirect(databaseUrl, opts) {
|
|
1627
|
-
const pool = createPool(databaseUrl);
|
|
1628
|
-
try {
|
|
1629
|
-
await pool.query(`DELETE FROM join_tokens WHERE created_by = $1`, [opts.createdBy]);
|
|
1630
|
-
} finally {
|
|
1631
|
-
await pool.end();
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
/**
|
|
1635
|
-
* Update the routing_key on `sources` rows for a given provider. Used by
|
|
1636
|
-
* cluster e2e to swap the staging routing key for an isolated test key
|
|
1637
|
-
* (and to restore it on teardown).
|
|
1638
|
-
*
|
|
1639
|
-
* `whereRoutingKey`: optional filter on the current routing_key. When
|
|
1640
|
-
* present only rows matching it are updated; when absent the provider
|
|
1641
|
-
* filter alone is used (with an implicit `!= newRoutingKey` guard so the
|
|
1642
|
-
* update is idempotent).
|
|
1643
|
-
*/
|
|
1644
|
-
async function updateSourceRoutingKeyDirect(databaseUrl, opts) {
|
|
1645
|
-
const pool = createPool(databaseUrl);
|
|
1646
|
-
try {
|
|
1647
|
-
return { updated: (opts.whereRoutingKey ? await pool.query(`UPDATE sources SET routing_key = $1 WHERE provider = $2 AND routing_key = $3`, [
|
|
1648
|
-
opts.newRoutingKey,
|
|
1649
|
-
opts.provider,
|
|
1650
|
-
opts.whereRoutingKey
|
|
1651
|
-
]) : await pool.query(`UPDATE sources SET routing_key = $1 WHERE provider = $2 AND routing_key != $1`, [opts.newRoutingKey, opts.provider])).rowCount ?? 0 };
|
|
1652
|
-
} finally {
|
|
1653
|
-
await pool.end();
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
/**
|
|
1657
1115
|
* Delete peer_credentials rows whose instance_id does NOT match a pattern.
|
|
1658
1116
|
* Used by cluster e2e to wipe stale staging peer credentials while leaving
|
|
1659
1117
|
* e2e-* peers intact.
|
|
@@ -1666,1283 +1124,7 @@ async function prunePeerCredentialsDirect(databaseUrl, opts) {
|
|
|
1666
1124
|
await pool.end();
|
|
1667
1125
|
}
|
|
1668
1126
|
}
|
|
1669
|
-
/**
|
|
1670
|
-
* Poll Platform until at least `minRegistrations` distinct orchestrator
|
|
1671
|
-
* connections are BOTH registered for the routing key (row in webhook_sources)
|
|
1672
|
-
* AND live (row in platform_connections with status='connected'). The live-
|
|
1673
|
-
* connection join is critical — Platform's webhook_sources rows persist after
|
|
1674
|
-
* a connection disconnects (the orphan sweeper eventually reaps them), so a
|
|
1675
|
-
* naive COUNT(DISTINCT) on webhook_sources alone would inflate the number
|
|
1676
|
-
* and mask a missing coordinator registration.
|
|
1677
|
-
*/
|
|
1678
|
-
async function waitForPlatformRegistrationsDirect(platformDbUrl, routingKey, opts) {
|
|
1679
|
-
const minRegistrations = opts?.minRegistrations ?? 1;
|
|
1680
|
-
const timeoutMs = opts?.timeoutMs ?? 3e4;
|
|
1681
|
-
const intervalMs = opts?.intervalMs ?? 2e3;
|
|
1682
|
-
const deadline = Date.now() + timeoutMs;
|
|
1683
|
-
const pool = createPool(platformDbUrl);
|
|
1684
|
-
try {
|
|
1685
|
-
while (Date.now() < deadline) {
|
|
1686
|
-
if (((await pool.query(`SELECT COUNT(DISTINCT ws.orchestrator_connection_id)::int AS cnt
|
|
1687
|
-
FROM webhook_sources ws
|
|
1688
|
-
JOIN platform_connections pc
|
|
1689
|
-
ON pc.connection_id = ws.orchestrator_connection_id
|
|
1690
|
-
AND pc.status = 'connected'
|
|
1691
|
-
WHERE ws.routing_key = $1
|
|
1692
|
-
AND ws.orchestrator_connection_id != 'e2e-synthetic'`, [routingKey])).rows[0]?.cnt ?? 0) >= minRegistrations) return;
|
|
1693
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1694
|
-
}
|
|
1695
|
-
throw new Error(`Timed out waiting for ${minRegistrations} orchestrator registration(s) for routing key ${routingKey} (waited ${timeoutMs}ms)`);
|
|
1696
|
-
} finally {
|
|
1697
|
-
await pool.end();
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
async function seedUniversalGitSourceDirect(databaseUrl, opts) {
|
|
1701
|
-
const eventTypeHeader = opts.eventTypeHeader ?? "x-gitea-event";
|
|
1702
|
-
const pool = createPool(databaseUrl);
|
|
1703
|
-
try {
|
|
1704
|
-
await pool.query(`INSERT INTO generic_webhook_sources (
|
|
1705
|
-
id, customer_id, name, routing_key,
|
|
1706
|
-
verification_method, verification_config,
|
|
1707
|
-
event_type_header, dedup_window_seconds,
|
|
1708
|
-
max_payload_bytes, rate_limit_rpm, enabled,
|
|
1709
|
-
provider_type, git_config
|
|
1710
|
-
) VALUES ($1, $2, $3, $4, 'none', '{}', $6, 300, 10485760, 600, true,
|
|
1711
|
-
'generic', $5::jsonb)
|
|
1712
|
-
ON CONFLICT (routing_key) DO UPDATE SET
|
|
1713
|
-
customer_id = EXCLUDED.customer_id,
|
|
1714
|
-
name = EXCLUDED.name,
|
|
1715
|
-
event_type_header = EXCLUDED.event_type_header,
|
|
1716
|
-
git_config = EXCLUDED.git_config,
|
|
1717
|
-
enabled = true,
|
|
1718
|
-
deleted_at = NULL,
|
|
1719
|
-
updated_at = NOW()`, [
|
|
1720
|
-
opts.sourceId,
|
|
1721
|
-
opts.orgId,
|
|
1722
|
-
opts.sourceName,
|
|
1723
|
-
opts.routingKey,
|
|
1724
|
-
JSON.stringify(opts.gitConfig),
|
|
1725
|
-
eventTypeHeader
|
|
1726
|
-
]);
|
|
1727
|
-
} finally {
|
|
1728
|
-
await pool.end();
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
/** repo_identifier used for the different-repo isolation hold. */
|
|
1732
|
-
const CI_SECURITY_OTHER_REPO = "other/repo";
|
|
1733
|
-
async function seedCiSecurityFixturesDirect(databaseUrl, opts) {
|
|
1734
|
-
const contextName = opts.contextName ?? "ci-security-env";
|
|
1735
|
-
const sourceName = opts.sourceName ?? "ci-security-dashboard-resolver";
|
|
1736
|
-
const sourceRoutingKey = opts.sourceRoutingKey ?? `generic:${opts.orgId}:ci-security-dashboard`;
|
|
1737
|
-
const pool = createPool(databaseUrl);
|
|
1738
|
-
try {
|
|
1739
|
-
await pool.query(`INSERT INTO sources (id, provider, name, routing_key, config, customer_id)
|
|
1740
|
-
VALUES (gen_random_uuid(), \'generic\', $1, $2, \'{}\', $3)
|
|
1741
|
-
ON CONFLICT (routing_key) DO UPDATE SET customer_id = EXCLUDED.customer_id`, [
|
|
1742
|
-
sourceName,
|
|
1743
|
-
sourceRoutingKey,
|
|
1744
|
-
opts.orgId
|
|
1745
|
-
]);
|
|
1746
|
-
const envId = (await pool.query(`INSERT INTO contexts (org_id, name, type, enabled)
|
|
1747
|
-
VALUES ($1, $2, \'fixed\', true)
|
|
1748
|
-
ON CONFLICT (org_id, name) DO UPDATE SET enabled = true
|
|
1749
|
-
RETURNING id`, [opts.orgId, contextName])).rows[0].id;
|
|
1750
|
-
async function seedPrHold(args) {
|
|
1751
|
-
await pool.query(`INSERT INTO execution_runs (
|
|
1752
|
-
run_id, workflow_name, provider, repo_identifier,
|
|
1753
|
-
ref, sha, delivery_id, status, trust_tier, lock_file_source,
|
|
1754
|
-
contributor_username, routing_key, pr_number
|
|
1755
|
-
) VALUES ($1, \'e2e-security-wf\', \'internal\', $4, \'refs/heads/feature\',
|
|
1756
|
-
$5, $2, \'pending\', \'unknown\', \'base\', \'unknown-dev\', $3, $6)`, [
|
|
1757
|
-
args.runId,
|
|
1758
|
-
args.deliveryId,
|
|
1759
|
-
opts.runsRoutingKey,
|
|
1760
|
-
args.repoIdentifier,
|
|
1761
|
-
args.sha,
|
|
1762
|
-
args.prNumber
|
|
1763
|
-
]);
|
|
1764
|
-
await pool.query(`INSERT INTO execution_jobs (job_id, run_id, job_name, status)
|
|
1765
|
-
VALUES ($1, $2, \'security-test-job\', \'pending\')`, [args.jobId, args.runId]);
|
|
1766
|
-
return (await pool.query(`INSERT INTO held_runs (org_id, run_id, job_id, context_id, hold_type, queue_type, reason, expires_at)
|
|
1767
|
-
VALUES ($1, $2, $3, $4, $5, \'security\', $6, NOW() + INTERVAL \'72 hours\')
|
|
1768
|
-
RETURNING id`, [
|
|
1769
|
-
opts.orgId,
|
|
1770
|
-
args.runId,
|
|
1771
|
-
args.jobId,
|
|
1772
|
-
args.contextId === void 0 ? envId : args.contextId,
|
|
1773
|
-
args.holdType ?? HoldType.enum.security,
|
|
1774
|
-
args.reason ?? unknownContributorHoldReason(contextName)
|
|
1775
|
-
])).rows[0].id;
|
|
1776
|
-
}
|
|
1777
|
-
const heldRunId = await seedPrHold({
|
|
1778
|
-
runId: opts.unknownRunId,
|
|
1779
|
-
deliveryId: opts.unknownDeliveryId,
|
|
1780
|
-
jobId: opts.unknownJobId,
|
|
1781
|
-
repoIdentifier: ".",
|
|
1782
|
-
prNumber: 1,
|
|
1783
|
-
sha: "abc123"
|
|
1784
|
-
});
|
|
1785
|
-
const secondHeldRunId = await seedPrHold({
|
|
1786
|
-
runId: opts.secondPrRunId,
|
|
1787
|
-
deliveryId: opts.secondPrDeliveryId,
|
|
1788
|
-
jobId: opts.secondPrJobId,
|
|
1789
|
-
repoIdentifier: ".",
|
|
1790
|
-
prNumber: 2,
|
|
1791
|
-
sha: "bbb222"
|
|
1792
|
-
});
|
|
1793
|
-
const otherRepoHeldRunId = await seedPrHold({
|
|
1794
|
-
runId: opts.otherRepoRunId,
|
|
1795
|
-
deliveryId: opts.otherRepoDeliveryId,
|
|
1796
|
-
jobId: opts.otherRepoJobId,
|
|
1797
|
-
repoIdentifier: CI_SECURITY_OTHER_REPO,
|
|
1798
|
-
prNumber: 1,
|
|
1799
|
-
sha: "ccc333"
|
|
1800
|
-
});
|
|
1801
|
-
const wfModHeldRunId = await seedPrHold({
|
|
1802
|
-
runId: opts.wfModRunId,
|
|
1803
|
-
deliveryId: opts.wfModDeliveryId,
|
|
1804
|
-
jobId: opts.wfModJobId,
|
|
1805
|
-
repoIdentifier: ".",
|
|
1806
|
-
prNumber: 3,
|
|
1807
|
-
sha: "ddd444",
|
|
1808
|
-
holdType: HoldType.enum.security,
|
|
1809
|
-
reason: "workflow_modification",
|
|
1810
|
-
contextId: null
|
|
1811
|
-
});
|
|
1812
|
-
const forkPrHeldRunId = await seedPrHold({
|
|
1813
|
-
runId: opts.forkPrRunId,
|
|
1814
|
-
deliveryId: opts.forkPrDeliveryId,
|
|
1815
|
-
jobId: opts.forkPrJobId,
|
|
1816
|
-
repoIdentifier: ".",
|
|
1817
|
-
prNumber: 4,
|
|
1818
|
-
sha: "eee555",
|
|
1819
|
-
holdType: HoldType.enum.security,
|
|
1820
|
-
reason: "fork_pr",
|
|
1821
|
-
contextId: null
|
|
1822
|
-
});
|
|
1823
|
-
await pool.query(`INSERT INTO execution_runs (
|
|
1824
|
-
run_id, workflow_name, provider, repo_identifier,
|
|
1825
|
-
ref, sha, delivery_id, status, trust_tier, lock_file_source,
|
|
1826
|
-
contributor_username, routing_key, pr_number
|
|
1827
|
-
) VALUES ($1, \'e2e-security-wf\', \'internal\', \'.\', \'refs/heads/feature\',
|
|
1828
|
-
\'def456\', $2, \'running\', \'trusted\', \'head\', \'trusted-dev\', $3, 1)`, [
|
|
1829
|
-
opts.trustedRunId,
|
|
1830
|
-
opts.trustedDeliveryId,
|
|
1831
|
-
opts.runsRoutingKey
|
|
1832
|
-
]);
|
|
1833
|
-
await pool.query(`INSERT INTO execution_jobs (job_id, run_id, job_name, status)
|
|
1834
|
-
VALUES ($1, $2, \'security-test-job\', \'running\')`, [opts.trustedJobId, opts.trustedRunId]);
|
|
1835
|
-
return {
|
|
1836
|
-
contextName,
|
|
1837
|
-
envId,
|
|
1838
|
-
heldRunId,
|
|
1839
|
-
secondHeldRunId,
|
|
1840
|
-
otherRepoHeldRunId,
|
|
1841
|
-
wfModHeldRunId,
|
|
1842
|
-
forkPrHeldRunId
|
|
1843
|
-
};
|
|
1844
|
-
} finally {
|
|
1845
|
-
await pool.end();
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
/**
|
|
1849
|
-
* Poll `execution_runs` for the newest run started since `since` whose status
|
|
1850
|
-
* is in `statuses`, returning that status. Resolves `{ status: null }` if the
|
|
1851
|
-
* deadline passes before any run reaches a target status.
|
|
1852
|
-
*
|
|
1853
|
-
* Callers wanting "did the run finish?" pass the terminal status set and read
|
|
1854
|
-
* the landed status — a terminal failure is reported immediately rather than
|
|
1855
|
-
* indistinguishable from a timeout. Used by the cluster reroute tests to gate
|
|
1856
|
-
* on a workflow reaching a terminal state after a webhook trigger.
|
|
1857
|
-
*
|
|
1858
|
-
* Pass `deliveryId` to scope the poll to the caller's own delivery. A `since`
|
|
1859
|
-
* window alone does NOT identify a run: any workflow reaching a terminal state
|
|
1860
|
-
* inside the same window matches, so a neighbouring run — including one whose
|
|
1861
|
-
* failure is the point of some other test — is reported as if it were the
|
|
1862
|
-
* caller's. The stored `delivery_id` is the source-prefixed form
|
|
1863
|
-
* (`generic:<org>:<source>:<caller-id>`), so the caller's id is matched as a
|
|
1864
|
-
* suffix.
|
|
1865
|
-
*/
|
|
1866
|
-
async function waitForExecutionRunReachesStatusSinceDirect(databaseUrl, opts) {
|
|
1867
|
-
const timeoutMs = opts.timeoutMs ?? 24e4;
|
|
1868
|
-
const intervalMs = opts.intervalMs ?? 5e3;
|
|
1869
|
-
const deadline = Date.now() + timeoutMs;
|
|
1870
|
-
const pool = createPool(databaseUrl);
|
|
1871
|
-
try {
|
|
1872
|
-
while (Date.now() < deadline) {
|
|
1873
|
-
const result = await pool.query(`SELECT status FROM execution_runs
|
|
1874
|
-
WHERE started_at > $1 AND status = ANY($2)${opts.deliveryId ? " AND delivery_id LIKE $3" : ""}
|
|
1875
|
-
ORDER BY started_at DESC
|
|
1876
|
-
LIMIT 1`, opts.deliveryId ? [
|
|
1877
|
-
opts.since,
|
|
1878
|
-
[...opts.statuses],
|
|
1879
|
-
`%${opts.deliveryId}`
|
|
1880
|
-
] : [opts.since, [...opts.statuses]]);
|
|
1881
|
-
if (result.rows.length > 0) return { status: result.rows[0].status };
|
|
1882
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1883
|
-
}
|
|
1884
|
-
return { status: null };
|
|
1885
|
-
} finally {
|
|
1886
|
-
await pool.end();
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
async function latestExecutionRunByStatusDirect(databaseUrl, opts) {
|
|
1890
|
-
const pool = createPool(databaseUrl);
|
|
1891
|
-
try {
|
|
1892
|
-
const runResult = await pool.query(`SELECT run_id, workflow_name, status FROM execution_runs
|
|
1893
|
-
WHERE status = $1${opts.deliveryId ? " AND delivery_id LIKE $2" : ""}
|
|
1894
|
-
ORDER BY started_at DESC LIMIT 1`, opts.deliveryId ? [opts.status, `%${opts.deliveryId}`] : [opts.status]);
|
|
1895
|
-
if (runResult.rows.length === 0) return null;
|
|
1896
|
-
const run = runResult.rows[0];
|
|
1897
|
-
return {
|
|
1898
|
-
run,
|
|
1899
|
-
jobs: (await pool.query(`SELECT job_id, job_name, status FROM execution_jobs WHERE run_id = $1`, [run.run_id])).rows
|
|
1900
|
-
};
|
|
1901
|
-
} finally {
|
|
1902
|
-
await pool.end();
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
async function waitForLatestExecutionJobStatusDirect(databaseUrl, opts) {
|
|
1906
|
-
const terminal = opts.terminalStatuses ?? [
|
|
1907
|
-
"success",
|
|
1908
|
-
"completed",
|
|
1909
|
-
"failed"
|
|
1910
|
-
];
|
|
1911
|
-
const timeoutMs = opts.timeoutMs ?? 18e4;
|
|
1912
|
-
const intervalMs = opts.intervalMs ?? 3e3;
|
|
1913
|
-
const deadline = Date.now() + timeoutMs;
|
|
1914
|
-
const pool = createPool(databaseUrl);
|
|
1915
|
-
try {
|
|
1916
|
-
let status = null;
|
|
1917
|
-
let errorMessage = null;
|
|
1918
|
-
let runId = null;
|
|
1919
|
-
while (Date.now() < deadline) {
|
|
1920
|
-
const result = await pool.query(`SELECT j.status, j.error_message, r.run_id
|
|
1921
|
-
FROM execution_jobs j
|
|
1922
|
-
JOIN execution_runs r ON r.run_id = j.run_id
|
|
1923
|
-
WHERE r.workflow_name = $1
|
|
1924
|
-
AND r.started_at >= $2
|
|
1925
|
-
ORDER BY r.started_at DESC
|
|
1926
|
-
LIMIT 1`, [opts.workflowName, opts.since]);
|
|
1927
|
-
if (result.rows.length > 0) {
|
|
1928
|
-
status = result.rows[0].status;
|
|
1929
|
-
errorMessage = result.rows[0].error_message;
|
|
1930
|
-
runId = result.rows[0].run_id;
|
|
1931
|
-
if (status && terminal.includes(status)) break;
|
|
1932
|
-
}
|
|
1933
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1934
|
-
}
|
|
1935
|
-
return {
|
|
1936
|
-
status,
|
|
1937
|
-
errorMessage,
|
|
1938
|
-
runId
|
|
1939
|
-
};
|
|
1940
|
-
} finally {
|
|
1941
|
-
await pool.end();
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
async function describeTableColumnsDirect(databaseUrl, opts) {
|
|
1945
|
-
const pool = createPool(databaseUrl);
|
|
1946
|
-
try {
|
|
1947
|
-
return (await pool.query(`SELECT column_name, data_type FROM information_schema.columns
|
|
1948
|
-
WHERE table_name = $1
|
|
1949
|
-
ORDER BY ordinal_position`, [opts.tableName])).rows.map((r) => ({
|
|
1950
|
-
name: r.column_name,
|
|
1951
|
-
dataType: r.data_type
|
|
1952
|
-
}));
|
|
1953
|
-
} finally {
|
|
1954
|
-
await pool.end();
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
/**
|
|
1958
|
-
* READ-ONLY: return true if a table exists in the public schema.
|
|
1959
|
-
*/
|
|
1960
|
-
async function tableExistsDirect(databaseUrl, opts) {
|
|
1961
|
-
const pool = createPool(databaseUrl);
|
|
1962
|
-
try {
|
|
1963
|
-
return (await pool.query(`SELECT 1 FROM information_schema.tables
|
|
1964
|
-
WHERE table_schema = $1 AND table_name = $2`, [opts.schema ?? "public", opts.tableName])).rows.length > 0;
|
|
1965
|
-
} finally {
|
|
1966
|
-
await pool.end();
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
async function insertKiciEventRawDirect(databaseUrl, opts) {
|
|
1970
|
-
const expiresIn = opts.expiresIn ?? "1 hour";
|
|
1971
|
-
const pool = createPool(databaseUrl);
|
|
1972
|
-
try {
|
|
1973
|
-
return { id: (await pool.query(`INSERT INTO kici_events (event_name, payload, source_routing_key, chain_depth, expires_at)
|
|
1974
|
-
VALUES ($1, $2, $3, $4, NOW() + ($5)::interval)
|
|
1975
|
-
RETURNING id`, [
|
|
1976
|
-
opts.eventName,
|
|
1977
|
-
JSON.stringify(opts.payload),
|
|
1978
|
-
opts.sourceRoutingKey ?? null,
|
|
1979
|
-
opts.chainDepth ?? 0,
|
|
1980
|
-
expiresIn
|
|
1981
|
-
])).rows[0].id };
|
|
1982
|
-
} finally {
|
|
1983
|
-
await pool.end();
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
async function showKiciEventDirect(databaseUrl, opts) {
|
|
1987
|
-
const pool = createPool(databaseUrl);
|
|
1988
|
-
try {
|
|
1989
|
-
return (await pool.query(`SELECT id, event_name, payload, chain_depth, source_routing_key,
|
|
1990
|
-
source_repo, processed, expires_at, created_at
|
|
1991
|
-
FROM kici_events WHERE id = $1`, [opts.id])).rows[0] ?? null;
|
|
1992
|
-
} finally {
|
|
1993
|
-
await pool.end();
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
/**
|
|
1997
|
-
* READ-ONLY: list kici_events with filter hooks used by e2e tests:
|
|
1998
|
-
* by `event_name`, minimum `chain_depth`, expiry window. Returns the
|
|
1999
|
-
* rows with chain_depth ordered ascending. Callers apply assertions.
|
|
2000
|
-
*/
|
|
2001
|
-
async function listKiciEventsDirect(databaseUrl, opts = {}) {
|
|
2002
|
-
const pool = createPool(databaseUrl);
|
|
2003
|
-
try {
|
|
2004
|
-
const clauses = [];
|
|
2005
|
-
const params = [];
|
|
2006
|
-
let idx = 1;
|
|
2007
|
-
if (opts.eventName !== void 0) {
|
|
2008
|
-
clauses.push(`event_name = $${idx}`);
|
|
2009
|
-
params.push(opts.eventName);
|
|
2010
|
-
idx += 1;
|
|
2011
|
-
}
|
|
2012
|
-
if (opts.minChainDepth !== void 0) {
|
|
2013
|
-
clauses.push(`chain_depth > $${idx}`);
|
|
2014
|
-
params.push(opts.minChainDepth);
|
|
2015
|
-
idx += 1;
|
|
2016
|
-
}
|
|
2017
|
-
if (opts.onlyExpired) clauses.push(`expires_at < NOW()`);
|
|
2018
|
-
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
2019
|
-
const limit = Math.max(1, Math.min(1e4, opts.limit ?? 1e3));
|
|
2020
|
-
return (await pool.query(`SELECT id, event_name, payload, chain_depth, source_routing_key,
|
|
2021
|
-
source_repo, processed, expires_at, created_at
|
|
2022
|
-
FROM kici_events
|
|
2023
|
-
${where}
|
|
2024
|
-
ORDER BY chain_depth ASC, created_at ASC
|
|
2025
|
-
LIMIT ${limit}`, params)).rows;
|
|
2026
|
-
} finally {
|
|
2027
|
-
await pool.end();
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
/**
|
|
2031
|
-
* DELETE kici_events by id or event_name. Returns the number of rows
|
|
2032
|
-
* deleted. Used by event-routing teardown.
|
|
2033
|
-
*/
|
|
2034
|
-
async function deleteKiciEventsDirect(databaseUrl, opts) {
|
|
2035
|
-
if (!opts.id && !opts.eventName) throw new Error("deleteKiciEventsDirect: one of {id, eventName} is required");
|
|
2036
|
-
const pool = createPool(databaseUrl);
|
|
2037
|
-
try {
|
|
2038
|
-
return { deleted: (opts.id ? await pool.query(`DELETE FROM kici_events WHERE id = $1`, [opts.id]) : await pool.query(`DELETE FROM kici_events WHERE event_name = $1`, [opts.eventName])).rowCount ?? 0 };
|
|
2039
|
-
} finally {
|
|
2040
|
-
await pool.end();
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
/**
|
|
2044
|
-
* INSERT a kici_events row with an explicit created_at. Lets a test place
|
|
2045
|
-
* many events on the exact same timestamp so the catch-up keyset cursor's
|
|
2046
|
-
* same-created_at tie handling is genuinely exercised (the default-NOW insert
|
|
2047
|
-
* path cannot force ties deterministically). Returns the inserted id.
|
|
2048
|
-
*/
|
|
2049
|
-
async function insertKiciEventAtDirect(databaseUrl, opts) {
|
|
2050
|
-
const expiresIn = opts.expiresIn ?? "7 days";
|
|
2051
|
-
const pool = createPool(databaseUrl);
|
|
2052
|
-
try {
|
|
2053
|
-
return { id: (await pool.query(`INSERT INTO kici_events
|
|
2054
|
-
(event_name, payload, source_routing_key, chain_depth, processed, created_at, expires_at)
|
|
2055
|
-
VALUES ($1, $2::jsonb, $3, $4, false, $5::timestamptz, NOW() + ($6)::interval)
|
|
2056
|
-
RETURNING id`, [
|
|
2057
|
-
opts.eventName,
|
|
2058
|
-
JSON.stringify(opts.payload ?? {}),
|
|
2059
|
-
opts.sourceRoutingKey ?? null,
|
|
2060
|
-
opts.chainDepth ?? 0,
|
|
2061
|
-
opts.createdAt.toISOString(),
|
|
2062
|
-
expiresIn
|
|
2063
|
-
])).rows[0].id };
|
|
2064
|
-
} finally {
|
|
2065
|
-
await pool.end();
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
/**
|
|
2069
|
-
* Page through unprocessed, non-DLQ kici_events for a given event_name using
|
|
2070
|
-
* the same composite `(created_at, id)` keyset cursor + `ORDER BY created_at
|
|
2071
|
-
* ASC, id ASC` that EventStore.getUnprocessedSince runs. Keep this SQL in sync
|
|
2072
|
-
* with `packages/orchestrator/src/events/event-store.ts`.
|
|
2073
|
-
*
|
|
2074
|
-
* Returns every id seen across all pages in traversal order (with duplicates
|
|
2075
|
-
* preserved so the caller can assert none occur) plus the page count. This is
|
|
2076
|
-
* the real-Postgres guard that the keyset cursor pages the entire backlog —
|
|
2077
|
-
* including same-created_at ties across a page boundary — with no skip and no
|
|
2078
|
-
* duplicate.
|
|
2079
|
-
*/
|
|
2080
|
-
async function paginateUnprocessedEventsKeysetDirect(databaseUrl, opts) {
|
|
2081
|
-
const batchSize = Math.max(1, Math.min(1e4, opts.batchSize ?? 100));
|
|
2082
|
-
const pool = createPool(databaseUrl);
|
|
2083
|
-
try {
|
|
2084
|
-
const ids = [];
|
|
2085
|
-
let cursor = null;
|
|
2086
|
-
let pages = 0;
|
|
2087
|
-
for (;;) {
|
|
2088
|
-
const params = [opts.eventName];
|
|
2089
|
-
let cursorClause = "";
|
|
2090
|
-
if (cursor) {
|
|
2091
|
-
cursorClause = `AND (created_at, id) > ($2::timestamptz, $3::uuid)`;
|
|
2092
|
-
params.push(cursor.createdAt, cursor.id);
|
|
2093
|
-
}
|
|
2094
|
-
const { rows } = await pool.query(`SELECT id, created_at FROM kici_events
|
|
2095
|
-
WHERE event_name = $1 AND processed = false AND dlq_at IS NULL ${cursorClause}
|
|
2096
|
-
ORDER BY created_at ASC, id ASC
|
|
2097
|
-
LIMIT ${batchSize}`, params);
|
|
2098
|
-
if (rows.length === 0) break;
|
|
2099
|
-
pages += 1;
|
|
2100
|
-
for (const r of rows) ids.push(r.id);
|
|
2101
|
-
const last = rows[rows.length - 1];
|
|
2102
|
-
cursor = {
|
|
2103
|
-
createdAt: new Date(last.created_at).toISOString(),
|
|
2104
|
-
id: last.id
|
|
2105
|
-
};
|
|
2106
|
-
if (rows.length < batchSize) break;
|
|
2107
|
-
}
|
|
2108
|
-
return {
|
|
2109
|
-
ids,
|
|
2110
|
-
pages
|
|
2111
|
-
};
|
|
2112
|
-
} finally {
|
|
2113
|
-
await pool.end();
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
/**
|
|
2117
|
-
* Simulate a NOTIFY on `kici_event_channel` and verify a LISTEN client
|
|
2118
|
-
* receives it. Used by event-routing e2e to prove the infrastructure
|
|
2119
|
-
* the EventRouter uses for real-time delivery is functional. Owns the
|
|
2120
|
-
* pool for the full test to keep the listen/notify correlated.
|
|
2121
|
-
*/
|
|
2122
|
-
async function verifyKiciEventNotifyDirect(databaseUrl, opts) {
|
|
2123
|
-
const waitMs = opts.waitMs ?? 1e3;
|
|
2124
|
-
const pool = createPool(databaseUrl);
|
|
2125
|
-
const client = await pool.connect();
|
|
2126
|
-
try {
|
|
2127
|
-
const notifications = [];
|
|
2128
|
-
client.on("notification", (msg) => {
|
|
2129
|
-
if (msg.channel === "kici_event_channel") notifications.push(msg.payload ?? "");
|
|
2130
|
-
});
|
|
2131
|
-
await client.query("LISTEN kici_event_channel");
|
|
2132
|
-
await pool.query(`SELECT pg_notify('kici_event_channel', $1)`, [opts.payload]);
|
|
2133
|
-
await new Promise((r) => setTimeout(r, waitMs));
|
|
2134
|
-
await client.query("UNLISTEN kici_event_channel");
|
|
2135
|
-
return { received: notifications };
|
|
2136
|
-
} finally {
|
|
2137
|
-
client.release();
|
|
2138
|
-
await pool.end();
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
/**
|
|
2142
|
-
* INSERT a cross_repo_trust row. Returns the new id and allowed_events
|
|
2143
|
-
* array. Used by generic-webhook + event-routing e2e until a proper
|
|
2144
|
-
* `kici-admin trust` CLI ships.
|
|
2145
|
-
*/
|
|
2146
|
-
async function seedCrossRepoTrustDirect(databaseUrl, opts) {
|
|
2147
|
-
const pool = createPool(databaseUrl);
|
|
2148
|
-
try {
|
|
2149
|
-
const result = await pool.query(`INSERT INTO cross_repo_trust
|
|
2150
|
-
(source_repo, source_routing_key, target_repo, target_routing_key, allowed_events)
|
|
2151
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
2152
|
-
RETURNING id, allowed_events`, [
|
|
2153
|
-
opts.sourceRepo,
|
|
2154
|
-
opts.sourceRoutingKey,
|
|
2155
|
-
opts.targetRepo,
|
|
2156
|
-
opts.targetRoutingKey,
|
|
2157
|
-
opts.allowedEvents ? JSON.stringify(opts.allowedEvents) : null
|
|
2158
|
-
]);
|
|
2159
|
-
return {
|
|
2160
|
-
id: result.rows[0].id,
|
|
2161
|
-
allowedEvents: result.rows[0].allowed_events
|
|
2162
|
-
};
|
|
2163
|
-
} finally {
|
|
2164
|
-
await pool.end();
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
/**
|
|
2168
|
-
* READ-ONLY: list cross_repo_trust rows by source_routing_key. Used by
|
|
2169
|
-
* generic-webhook e2e to find a trust row it just inserted.
|
|
2170
|
-
*/
|
|
2171
|
-
async function listCrossRepoTrustBySourceRoutingKeyDirect(databaseUrl, opts) {
|
|
2172
|
-
const pool = createPool(databaseUrl);
|
|
2173
|
-
try {
|
|
2174
|
-
return (await pool.query(`SELECT id, source_repo, source_routing_key, target_repo, target_routing_key, allowed_events
|
|
2175
|
-
FROM cross_repo_trust WHERE source_routing_key = $1`, [opts.sourceRoutingKey])).rows;
|
|
2176
|
-
} finally {
|
|
2177
|
-
await pool.end();
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
/**
|
|
2181
|
-
* DELETE cross_repo_trust rows. Deletes by id, or asserts uniqueness
|
|
2182
|
-
* check error on duplicate INSERT (handled by caller). Returns row count.
|
|
2183
|
-
*/
|
|
2184
|
-
async function deleteCrossRepoTrustDirect(databaseUrl, opts) {
|
|
2185
|
-
const pool = createPool(databaseUrl);
|
|
2186
|
-
try {
|
|
2187
|
-
return { deleted: (await pool.query(`DELETE FROM cross_repo_trust WHERE id = $1`, [opts.id])).rowCount ?? 0 };
|
|
2188
|
-
} finally {
|
|
2189
|
-
await pool.end();
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
/**
|
|
2193
|
-
* Direct INSERT with duplicate-key detection. Used by event-routing
|
|
2194
|
-
* e2e to verify the unique constraint enforces. Throws on duplicate;
|
|
2195
|
-
* caller asserts the error message matches /unique/i.
|
|
2196
|
-
*/
|
|
2197
|
-
async function insertCrossRepoTrustStrictDirect(databaseUrl, opts) {
|
|
2198
|
-
const pool = createPool(databaseUrl);
|
|
2199
|
-
try {
|
|
2200
|
-
await pool.query(`INSERT INTO cross_repo_trust (source_repo, source_routing_key, target_repo, target_routing_key)
|
|
2201
|
-
VALUES ($1, $2, $3, $4)`, [
|
|
2202
|
-
opts.sourceRepo,
|
|
2203
|
-
opts.sourceRoutingKey,
|
|
2204
|
-
opts.targetRepo,
|
|
2205
|
-
opts.targetRoutingKey
|
|
2206
|
-
]);
|
|
2207
|
-
} finally {
|
|
2208
|
-
await pool.end();
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
/**
|
|
2212
|
-
* Cleanup helper for workflow_registrations. Accepts id, routingKey,
|
|
2213
|
-
* or repoIdentifier filter (exactly one). Returns delete count.
|
|
2214
|
-
*/
|
|
2215
|
-
async function deleteWorkflowRegistrationsDirect(databaseUrl, opts) {
|
|
2216
|
-
if ([
|
|
2217
|
-
opts.id,
|
|
2218
|
-
opts.routingKey,
|
|
2219
|
-
opts.repoIdentifier
|
|
2220
|
-
].filter((v) => v !== void 0).length !== 1) throw new Error("deleteWorkflowRegistrationsDirect: exactly one of {id, routingKey, repoIdentifier} required");
|
|
2221
|
-
const pool = createPool(databaseUrl);
|
|
2222
|
-
try {
|
|
2223
|
-
let result;
|
|
2224
|
-
if (opts.id) result = await pool.query(`DELETE FROM workflow_registrations WHERE id = $1`, [opts.id]);
|
|
2225
|
-
else if (opts.routingKey) result = await pool.query(`DELETE FROM workflow_registrations WHERE routing_key = $1`, [opts.routingKey]);
|
|
2226
|
-
else result = await pool.query(`DELETE FROM workflow_registrations WHERE repo_identifier = $1`, [opts.repoIdentifier]);
|
|
2227
|
-
const deleted = result.rowCount ?? 0;
|
|
2228
|
-
if (deleted > 0) await pool.query(`UPDATE registry_versions SET version = version + 1, updated_at = NOW() WHERE id = 'default'`);
|
|
2229
|
-
return { deleted };
|
|
2230
|
-
} finally {
|
|
2231
|
-
await pool.end();
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
async function getWorkflowRegistrationByIdDirect(databaseUrl, opts) {
|
|
2235
|
-
const pool = createPool(databaseUrl);
|
|
2236
|
-
try {
|
|
2237
|
-
return (await pool.query(`SELECT id, repo_identifier, workflow_name, routing_key, customer_id,
|
|
2238
|
-
trigger_types, disabled, is_global, commit_sha, source_file,
|
|
2239
|
-
created_at, updated_at, lock_entry
|
|
2240
|
-
FROM workflow_registrations
|
|
2241
|
-
WHERE id = $1`, [opts.id])).rows[0] ?? null;
|
|
2242
|
-
} finally {
|
|
2243
|
-
await pool.end();
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
async function listRegistrationsByRoutingKeyDirect(databaseUrl, opts) {
|
|
2247
|
-
const pool = createPool(databaseUrl);
|
|
2248
|
-
try {
|
|
2249
|
-
const globalFilter = opts.onlyGlobal ? `AND is_global = TRUE` : "";
|
|
2250
|
-
const result = await pool.query(`SELECT workflow_name, is_global, updated_at
|
|
2251
|
-
FROM workflow_registrations
|
|
2252
|
-
WHERE routing_key = $1 ${globalFilter}`, [opts.routingKey]);
|
|
2253
|
-
let latest = null;
|
|
2254
|
-
for (const r of result.rows) {
|
|
2255
|
-
const d = r.updated_at ? new Date(r.updated_at) : null;
|
|
2256
|
-
if (d && (!latest || d.getTime() > latest.getTime())) latest = d;
|
|
2257
|
-
}
|
|
2258
|
-
return {
|
|
2259
|
-
count: result.rows.length,
|
|
2260
|
-
latestUpdatedAt: latest,
|
|
2261
|
-
rows: result.rows.map((r) => ({
|
|
2262
|
-
workflow_name: r.workflow_name,
|
|
2263
|
-
is_global: r.is_global
|
|
2264
|
-
}))
|
|
2265
|
-
};
|
|
2266
|
-
} finally {
|
|
2267
|
-
await pool.end();
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
/**
|
|
2271
|
-
* Poll `workflow_registrations` for at least `minCount` rows for a
|
|
2272
|
-
* routing key. Returns the rows or throws on timeout (keeps parity with
|
|
2273
|
-
* the forgejo helper pattern it replaces).
|
|
2274
|
-
*/
|
|
2275
|
-
async function waitForRegistrationsByRoutingKeyDirect(databaseUrl, opts) {
|
|
2276
|
-
const minCount = opts.minCount ?? 1;
|
|
2277
|
-
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2278
|
-
const intervalMs = opts.intervalMs ?? 1e3;
|
|
2279
|
-
const deadline = Date.now() + timeoutMs;
|
|
2280
|
-
let rows = [];
|
|
2281
|
-
while (Date.now() < deadline) {
|
|
2282
|
-
rows = (await listRegistrationsByRoutingKeyDirect(databaseUrl, {
|
|
2283
|
-
routingKey: opts.routingKey,
|
|
2284
|
-
onlyGlobal: opts.onlyGlobal
|
|
2285
|
-
})).rows;
|
|
2286
|
-
if (rows.length >= minCount) return rows;
|
|
2287
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2288
|
-
}
|
|
2289
|
-
return rows;
|
|
2290
|
-
}
|
|
2291
|
-
/**
|
|
2292
|
-
* Wait for `workflow_registrations.updated_at` for a routing key to
|
|
2293
|
-
* exceed `baselineUpdatedAt`. Used by forgejo rotated-PAT e2e.
|
|
2294
|
-
*/
|
|
2295
|
-
async function waitForRegistrationsUpdatedAtAdvanceDirect(databaseUrl, opts) {
|
|
2296
|
-
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2297
|
-
const intervalMs = opts.intervalMs ?? 1e3;
|
|
2298
|
-
const deadline = Date.now() + timeoutMs;
|
|
2299
|
-
let latest = null;
|
|
2300
|
-
while (Date.now() < deadline) {
|
|
2301
|
-
latest = (await listRegistrationsByRoutingKeyDirect(databaseUrl, { routingKey: opts.routingKey })).latestUpdatedAt;
|
|
2302
|
-
if (latest && (!opts.baselineUpdatedAt || latest.getTime() > opts.baselineUpdatedAt.getTime())) return {
|
|
2303
|
-
advanced: true,
|
|
2304
|
-
latestUpdatedAt: latest
|
|
2305
|
-
};
|
|
2306
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2307
|
-
}
|
|
2308
|
-
return {
|
|
2309
|
-
advanced: false,
|
|
2310
|
-
latestUpdatedAt: latest
|
|
2311
|
-
};
|
|
2312
|
-
}
|
|
2313
|
-
/**
|
|
2314
|
-
* UPDATE a workflow_registrations row's commit_sha. Used by cron-scheduler
|
|
2315
|
-
* e2e where the manual-schedule handler rejects null commit SHAs.
|
|
2316
|
-
*/
|
|
2317
|
-
async function updateWorkflowRegistrationCommitShaDirect(databaseUrl, opts) {
|
|
2318
|
-
const pool = createPool(databaseUrl);
|
|
2319
|
-
try {
|
|
2320
|
-
await pool.query(`UPDATE workflow_registrations SET commit_sha = $1 WHERE id = $2`, [opts.commitSha, opts.id]);
|
|
2321
|
-
} finally {
|
|
2322
|
-
await pool.end();
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
/**
|
|
2326
|
-
* Lock-entry seeding for the admin-API e2e, which uses a custom
|
|
2327
|
-
* shape (cron schedule). Returns the inserted id.
|
|
2328
|
-
*/
|
|
2329
|
-
async function insertWorkflowRegistrationRawDirect(databaseUrl, opts) {
|
|
2330
|
-
const pool = createPool(databaseUrl);
|
|
2331
|
-
try {
|
|
2332
|
-
const idClause = opts.id ? "$7" : "gen_random_uuid()";
|
|
2333
|
-
const extras = [];
|
|
2334
|
-
if (opts.id) extras.push(opts.id);
|
|
2335
|
-
const columns = [];
|
|
2336
|
-
const placeholders = [];
|
|
2337
|
-
if (opts.isGlobal !== void 0) {
|
|
2338
|
-
columns.push("is_global");
|
|
2339
|
-
placeholders.push(`$${7 + extras.length}`);
|
|
2340
|
-
extras.push(opts.isGlobal);
|
|
2341
|
-
}
|
|
2342
|
-
if (opts.disabled !== void 0) {
|
|
2343
|
-
columns.push("disabled");
|
|
2344
|
-
placeholders.push(`$${7 + extras.length}`);
|
|
2345
|
-
extras.push(opts.disabled);
|
|
2346
|
-
}
|
|
2347
|
-
if (opts.commitSha !== void 0) {
|
|
2348
|
-
columns.push("commit_sha");
|
|
2349
|
-
placeholders.push(`$${7 + extras.length}`);
|
|
2350
|
-
extras.push(opts.commitSha);
|
|
2351
|
-
}
|
|
2352
|
-
const extraColumns = columns.length > 0 ? `, ${columns.join(", ")}` : "";
|
|
2353
|
-
const extraPlaceholders = placeholders.length > 0 ? `, ${placeholders.join(", ")}` : "";
|
|
2354
|
-
const sql = `INSERT INTO workflow_registrations
|
|
2355
|
-
(routing_key, repo_identifier, workflow_name, lock_entry, trigger_types, customer_id${opts.id ? ", id" : ""}${extraColumns})
|
|
2356
|
-
VALUES ($1, $2, $3, $4, $5, $6${opts.id ? `, ${idClause}` : ""}${extraPlaceholders})
|
|
2357
|
-
ON CONFLICT (routing_key, repo_identifier, workflow_name) DO UPDATE SET
|
|
2358
|
-
lock_entry = EXCLUDED.lock_entry,
|
|
2359
|
-
trigger_types = EXCLUDED.trigger_types,
|
|
2360
|
-
customer_id = EXCLUDED.customer_id
|
|
2361
|
-
RETURNING id`;
|
|
2362
|
-
return { id: (await pool.query(sql, [
|
|
2363
|
-
opts.routingKey,
|
|
2364
|
-
opts.repoIdentifier,
|
|
2365
|
-
opts.workflowName,
|
|
2366
|
-
JSON.stringify(opts.lockEntry),
|
|
2367
|
-
opts.triggerTypes,
|
|
2368
|
-
opts.customerId,
|
|
2369
|
-
...extras
|
|
2370
|
-
])).rows[0].id };
|
|
2371
|
-
} finally {
|
|
2372
|
-
await pool.end();
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
/**
|
|
2376
|
-
* INSERT with a STRICT shape (no ON CONFLICT). Used by registration-schema
|
|
2377
|
-
* e2e to prove the unique constraint rejects duplicates. Throws the raw
|
|
2378
|
-
* DB error (caller matches /unique/i).
|
|
2379
|
-
*/
|
|
2380
|
-
async function insertWorkflowRegistrationStrictDirect(databaseUrl, opts) {
|
|
2381
|
-
const pool = createPool(databaseUrl);
|
|
2382
|
-
try {
|
|
2383
|
-
return { id: (await pool.query(`INSERT INTO workflow_registrations
|
|
2384
|
-
(routing_key, repo_identifier, workflow_name, lock_entry, trigger_types, customer_id)
|
|
2385
|
-
VALUES ($1, $2, $3, $4::jsonb, $5, $6)
|
|
2386
|
-
RETURNING id`, [
|
|
2387
|
-
opts.routingKey,
|
|
2388
|
-
opts.repoIdentifier,
|
|
2389
|
-
opts.workflowName,
|
|
2390
|
-
opts.lockEntryJson ?? "{}",
|
|
2391
|
-
opts.triggerTypes,
|
|
2392
|
-
opts.customerId
|
|
2393
|
-
])).rows[0]?.id ?? null };
|
|
2394
|
-
} finally {
|
|
2395
|
-
await pool.end();
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
/**
|
|
2399
|
-
* READ-ONLY: latest registry_versions.version, or null if the default
|
|
2400
|
-
* row is missing. Used by registration-admin-api e2e to bracket a
|
|
2401
|
-
* refresh call.
|
|
2402
|
-
*/
|
|
2403
|
-
async function getRegistryVersionDirect(databaseUrl, opts = {}) {
|
|
2404
|
-
const pool = createPool(databaseUrl);
|
|
2405
|
-
try {
|
|
2406
|
-
return (await pool.query(`SELECT version FROM registry_versions WHERE id = $1`, [opts.id ?? "default"])).rows[0]?.version ?? null;
|
|
2407
|
-
} finally {
|
|
2408
|
-
await pool.end();
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
/**
|
|
2412
|
-
* UPDATE `registry_versions.version = version + 1` WHERE id (default).
|
|
2413
|
-
* Simpler than `bumpRegistryVersionDirect` — used by global-workflow
|
|
2414
|
-
* e2e which wants the side-effect (force index refresh) without the
|
|
2415
|
-
* return value.
|
|
2416
|
-
*/
|
|
2417
|
-
async function bumpRegistryVersionSimpleDirect(databaseUrl, opts = {}) {
|
|
2418
|
-
const pool = createPool(databaseUrl);
|
|
2419
|
-
try {
|
|
2420
|
-
await pool.query(`UPDATE registry_versions SET version = version + 1 WHERE id = $1`, [opts.id ?? "default"]);
|
|
2421
|
-
} finally {
|
|
2422
|
-
await pool.end();
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
/**
|
|
2426
|
-
* Seed cron_last_fired for a registration with an `-INTERVAL` offset so
|
|
2427
|
-
* the scheduler fires on the next evaluation. Upsert via ON CONFLICT.
|
|
2428
|
-
*/
|
|
2429
|
-
async function upsertCronLastFiredDirect(databaseUrl, opts) {
|
|
2430
|
-
const pool = createPool(databaseUrl);
|
|
2431
|
-
try {
|
|
2432
|
-
await pool.query(`INSERT INTO cron_last_fired (registration_id, schedule_key, last_fired_at)
|
|
2433
|
-
VALUES ($1, $3, NOW() - ($2)::interval)
|
|
2434
|
-
ON CONFLICT (registration_id, schedule_key) DO UPDATE SET
|
|
2435
|
-
last_fired_at = NOW() - ($2)::interval,
|
|
2436
|
-
updated_at = NOW()`, [
|
|
2437
|
-
opts.registrationId,
|
|
2438
|
-
opts.agoInterval,
|
|
2439
|
-
opts.scheduleKey
|
|
2440
|
-
]);
|
|
2441
|
-
} finally {
|
|
2442
|
-
await pool.end();
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
/**
|
|
2446
|
-
* READ-ONLY: count cron_last_fired rows for a registration. Used by
|
|
2447
|
-
* registration-schema e2e to assert the FK cascade deletes the row.
|
|
2448
|
-
*/
|
|
2449
|
-
async function countCronLastFiredDirect(databaseUrl, opts) {
|
|
2450
|
-
const pool = createPool(databaseUrl);
|
|
2451
|
-
try {
|
|
2452
|
-
return (await pool.query(`SELECT COUNT(*)::int AS cnt FROM cron_last_fired WHERE registration_id = $1`, [opts.registrationId])).rows[0]?.cnt ?? 0;
|
|
2453
|
-
} finally {
|
|
2454
|
-
await pool.end();
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
/**
|
|
2458
|
-
* INSERT a cron_last_fired row with an explicit timestamp. Used by
|
|
2459
|
-
* registration-schema e2e to set up the FK cascade test.
|
|
2460
|
-
*/
|
|
2461
|
-
async function insertCronLastFiredNowDirect(databaseUrl, opts) {
|
|
2462
|
-
const pool = createPool(databaseUrl);
|
|
2463
|
-
try {
|
|
2464
|
-
await pool.query(`INSERT INTO cron_last_fired (registration_id, schedule_key, last_fired_at) VALUES ($1, $2, NOW())`, [opts.registrationId, opts.scheduleKey]);
|
|
2465
|
-
} finally {
|
|
2466
|
-
await pool.end();
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
|
-
/**
|
|
2470
|
-
* DELETE cron_last_fired rows for a registration. Teardown helper.
|
|
2471
|
-
*/
|
|
2472
|
-
async function deleteCronLastFiredDirect(databaseUrl, opts) {
|
|
2473
|
-
const pool = createPool(databaseUrl);
|
|
2474
|
-
try {
|
|
2475
|
-
await pool.query(`DELETE FROM cron_last_fired WHERE registration_id = $1`, [opts.registrationId]);
|
|
2476
|
-
} finally {
|
|
2477
|
-
await pool.end();
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
/**
|
|
2481
|
-
* DELETE execution_runs by workflow_name. Teardown helper for
|
|
2482
|
-
* manual-schedule e2e.
|
|
2483
|
-
*/
|
|
2484
|
-
async function deleteExecutionRunsByWorkflowNameDirect(databaseUrl, opts) {
|
|
2485
|
-
const pool = createPool(databaseUrl);
|
|
2486
|
-
try {
|
|
2487
|
-
return { deleted: (await pool.query(`DELETE FROM execution_runs WHERE workflow_name = $1`, [opts.workflowName])).rowCount ?? 0 };
|
|
2488
|
-
} finally {
|
|
2489
|
-
await pool.end();
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2492
|
-
/**
|
|
2493
|
-
* READ-ONLY: SELECT generic_webhook_sources by routing_key.
|
|
2494
|
-
* Used by forgejo e2e to assert the source exists with correct git_config.
|
|
2495
|
-
*/
|
|
2496
|
-
async function getGenericWebhookSourceByRoutingKeyDirect(databaseUrl, opts) {
|
|
2497
|
-
const pool = createPool(databaseUrl);
|
|
2498
|
-
try {
|
|
2499
|
-
return (await pool.query(`SELECT id, git_config, customer_id FROM generic_webhook_sources WHERE routing_key = $1`, [opts.routingKey])).rows[0] ?? null;
|
|
2500
|
-
} finally {
|
|
2501
|
-
await pool.end();
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
/**
|
|
2505
|
-
* READ-ONLY: list active (enabled=true) generic_webhook_sources.
|
|
2506
|
-
* Used by cluster-leader-failover e2e to prove the seeded source exists
|
|
2507
|
-
* before a leader crash.
|
|
2508
|
-
*/
|
|
2509
|
-
async function listActiveGenericWebhookSourcesDirect(databaseUrl) {
|
|
2510
|
-
const pool = createPool(databaseUrl);
|
|
2511
|
-
try {
|
|
2512
|
-
return (await pool.query(`SELECT id, customer_id, routing_key FROM generic_webhook_sources WHERE enabled = true`)).rows;
|
|
2513
|
-
} finally {
|
|
2514
|
-
await pool.end();
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
/**
|
|
2518
|
-
* UPDATE generic_webhook_sources.verification_config for a source by
|
|
2519
|
-
* name + customer_id. Used by the generic-webhook-auth e2e which seeds
|
|
2520
|
-
* sources and then writes custom auth configs.
|
|
2521
|
-
*/
|
|
2522
|
-
async function updateGenericWebhookVerificationConfigDirect(databaseUrl, opts) {
|
|
2523
|
-
const pool = createPool(databaseUrl);
|
|
2524
|
-
try {
|
|
2525
|
-
await pool.query(`UPDATE generic_webhook_sources
|
|
2526
|
-
SET verification_config = $1
|
|
2527
|
-
WHERE name = $2 AND customer_id = $3`, [
|
|
2528
|
-
JSON.stringify(opts.verificationConfig),
|
|
2529
|
-
opts.name,
|
|
2530
|
-
opts.customerId
|
|
2531
|
-
]);
|
|
2532
|
-
} finally {
|
|
2533
|
-
await pool.end();
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
/**
|
|
2537
|
-
* DELETE generic_webhook_sources by name list. Teardown for the auth
|
|
2538
|
-
* e2e, which seeded 3 sources by name.
|
|
2539
|
-
*/
|
|
2540
|
-
async function deleteGenericWebhookSourcesByNameDirect(databaseUrl, opts) {
|
|
2541
|
-
const pool = createPool(databaseUrl);
|
|
2542
|
-
try {
|
|
2543
|
-
return { deleted: (await pool.query(`DELETE FROM generic_webhook_sources WHERE name = ANY($1::text[])`, [opts.names])).rowCount ?? 0 };
|
|
2544
|
-
} finally {
|
|
2545
|
-
await pool.end();
|
|
2546
|
-
}
|
|
2547
|
-
}
|
|
2548
|
-
/**
|
|
2549
|
-
* UPDATE generic_webhook_sources.deleted_at = NULL for a row by id.
|
|
2550
|
-
* Used by the generic-webhook e2e to restore a soft-deleted source
|
|
2551
|
-
* after the soft-delete test ran — there is no CLI-level undelete.
|
|
2552
|
-
*/
|
|
2553
|
-
async function restoreSoftDeletedGenericWebhookSourceDirect(databaseUrl, opts) {
|
|
2554
|
-
const pool = createPool(databaseUrl);
|
|
2555
|
-
try {
|
|
2556
|
-
await pool.query(`UPDATE generic_webhook_sources SET deleted_at = NULL WHERE id = $1`, [opts.id]);
|
|
2557
|
-
} finally {
|
|
2558
|
-
await pool.end();
|
|
2559
|
-
}
|
|
2560
|
-
}
|
|
2561
|
-
async function upsertOrgSettingsGlobalWorkflowsDirect(databaseUrl, opts) {
|
|
2562
|
-
const pool = createPool(databaseUrl);
|
|
2563
|
-
try {
|
|
2564
|
-
await pool.query(`INSERT INTO org_settings (
|
|
2565
|
-
customer_id,
|
|
2566
|
-
global_workflow_allowed_repos,
|
|
2567
|
-
global_workflow_denied_repos,
|
|
2568
|
-
global_workflow_elevated_repos
|
|
2569
|
-
) VALUES ($1, $2::jsonb, $3::jsonb, $4::jsonb)
|
|
2570
|
-
ON CONFLICT (customer_id) DO UPDATE SET
|
|
2571
|
-
global_workflow_allowed_repos = EXCLUDED.global_workflow_allowed_repos,
|
|
2572
|
-
global_workflow_denied_repos = EXCLUDED.global_workflow_denied_repos,
|
|
2573
|
-
global_workflow_elevated_repos = EXCLUDED.global_workflow_elevated_repos,
|
|
2574
|
-
updated_at = NOW()`, [
|
|
2575
|
-
opts.customerId,
|
|
2576
|
-
opts.allowedRepos == null ? null : JSON.stringify(opts.allowedRepos),
|
|
2577
|
-
opts.deniedRepos == null ? null : JSON.stringify(opts.deniedRepos),
|
|
2578
|
-
opts.elevatedRepos == null ? null : JSON.stringify(opts.elevatedRepos)
|
|
2579
|
-
]);
|
|
2580
|
-
} finally {
|
|
2581
|
-
await pool.end();
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
/**
|
|
2585
|
-
* Set (or clear) the fleet-wide global-workflows master switch directly.
|
|
2586
|
-
*
|
|
2587
|
-
* Direct-DB by design, matching every other `*Direct` helper here: these exist
|
|
2588
|
-
* for E2E seeding, which must set the switch BEFORE an orchestrator is up and
|
|
2589
|
-
* therefore cannot go through the admin HTTP API. Operators use
|
|
2590
|
-
* `kici-admin cluster-settings set --global-workflows-enabled <bool>`.
|
|
2591
|
-
*
|
|
2592
|
-
* Passing `null` clears the override, so the orchestrator's configured default
|
|
2593
|
-
* applies. Upserts the singleton `id='default'` row, so it works against a
|
|
2594
|
-
* fresh database with no cluster_settings row at all.
|
|
2595
|
-
*
|
|
2596
|
-
* `cluster_settings.version` is left untouched: seeding happens before any
|
|
2597
|
-
* worker reads the row, so there is nothing to invalidate yet — the operator
|
|
2598
|
-
* PATCH path (admin-cluster-settings.ts) owns the version bump for live changes.
|
|
2599
|
-
*/
|
|
2600
|
-
async function setClusterGlobalWorkflowsEnabledDirect(databaseUrl, enabled) {
|
|
2601
|
-
const pool = createPool(databaseUrl);
|
|
2602
|
-
try {
|
|
2603
|
-
await pool.query(`INSERT INTO cluster_settings (id, global_workflows_enabled)
|
|
2604
|
-
VALUES ('default', $1)
|
|
2605
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
2606
|
-
global_workflows_enabled = EXCLUDED.global_workflows_enabled`, [enabled]);
|
|
2607
|
-
} finally {
|
|
2608
|
-
await pool.end();
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
/**
|
|
2612
|
-
* UPDATE org_settings.global_workflow_denied_repos for a customer/org id.
|
|
2613
|
-
* Assumes the row exists (upsertOrgSettingsGlobalWorkflowsDirect was
|
|
2614
|
-
* called earlier in the test).
|
|
2615
|
-
*/
|
|
2616
|
-
async function updateOrgSettingsDeniedReposDirect(databaseUrl, opts) {
|
|
2617
|
-
const pool = createPool(databaseUrl);
|
|
2618
|
-
try {
|
|
2619
|
-
await pool.query(`UPDATE org_settings
|
|
2620
|
-
SET global_workflow_denied_repos = $2::jsonb,
|
|
2621
|
-
updated_at = NOW()
|
|
2622
|
-
WHERE customer_id = $1`, [opts.customerId, opts.deniedRepos == null ? null : JSON.stringify(opts.deniedRepos)]);
|
|
2623
|
-
} finally {
|
|
2624
|
-
await pool.end();
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2627
|
-
/**
|
|
2628
|
-
* DELETE org_settings by customer/org id. Teardown helper.
|
|
2629
|
-
*/
|
|
2630
|
-
async function deleteOrgSettingsByCustomerIdDirect(databaseUrl, opts) {
|
|
2631
|
-
const pool = createPool(databaseUrl);
|
|
2632
|
-
try {
|
|
2633
|
-
return { deleted: (await pool.query(`DELETE FROM org_settings WHERE customer_id = $1`, [opts.customerId])).rowCount ?? 0 };
|
|
2634
|
-
} finally {
|
|
2635
|
-
await pool.end();
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
async function getExecutionRunSecurityDirect(databaseUrl, opts) {
|
|
2639
|
-
const pool = createPool(databaseUrl);
|
|
2640
|
-
try {
|
|
2641
|
-
const result = await pool.query(`SELECT run_id, trust_tier, lock_file_source, contributor_username, status
|
|
2642
|
-
FROM execution_runs WHERE run_id = $1`, [opts.runId]);
|
|
2643
|
-
if (result.rows.length === 0) throw new Error(`execution_runs: row not found (run_id=${opts.runId})`);
|
|
2644
|
-
return result.rows[0];
|
|
2645
|
-
} finally {
|
|
2646
|
-
await pool.end();
|
|
2647
|
-
}
|
|
2648
|
-
}
|
|
2649
|
-
async function getHeldRunByIdDirect(databaseUrl, opts) {
|
|
2650
|
-
const pool = createPool(databaseUrl);
|
|
2651
|
-
try {
|
|
2652
|
-
return (await pool.query(`SELECT id, run_id, hold_type, queue_type, status, reason,
|
|
2653
|
-
expires_at, approved_by, resolved_at
|
|
2654
|
-
FROM held_runs WHERE id = $1`, [opts.id])).rows[0] ?? null;
|
|
2655
|
-
} finally {
|
|
2656
|
-
await pool.end();
|
|
2657
|
-
}
|
|
2658
|
-
}
|
|
2659
|
-
/**
|
|
2660
|
-
* READ-ONLY: list the recorded approver decisions for a held run, newest
|
|
2661
|
-
* first. Approver attribution lives in `held_run_approvals` (one row per
|
|
2662
|
-
* decision), not on the `held_runs` row — used by ci-security e2e to prove
|
|
2663
|
-
* an approval was stamped with the approving user's sub.
|
|
2664
|
-
*/
|
|
2665
|
-
async function listHeldRunApprovalsDirect(databaseUrl, opts) {
|
|
2666
|
-
const pool = createPool(databaseUrl);
|
|
2667
|
-
try {
|
|
2668
|
-
return (await pool.query(`SELECT id, held_run_id, approver_user_id, decision, created_at
|
|
2669
|
-
FROM held_run_approvals WHERE held_run_id = $1
|
|
2670
|
-
ORDER BY created_at DESC`, [opts.heldRunId])).rows;
|
|
2671
|
-
} finally {
|
|
2672
|
-
await pool.end();
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
/**
|
|
2676
|
-
* READ-ONLY: count held_runs rows matching run_id + queue_type.
|
|
2677
|
-
* Used by ci-security e2e to prove no security hold exists for a
|
|
2678
|
-
* trusted contributor PR.
|
|
2679
|
-
*/
|
|
2680
|
-
async function countHeldRunsByRunIdDirect(databaseUrl, opts) {
|
|
2681
|
-
const pool = createPool(databaseUrl);
|
|
2682
|
-
try {
|
|
2683
|
-
return (opts.queueType ? await pool.query(`SELECT COUNT(*)::int AS cnt FROM held_runs WHERE run_id = $1 AND queue_type = $2`, [opts.runId, opts.queueType]) : await pool.query(`SELECT COUNT(*)::int AS cnt FROM held_runs WHERE run_id = $1`, [opts.runId])).rows[0]?.cnt ?? 0;
|
|
2684
|
-
} finally {
|
|
2685
|
-
await pool.end();
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
/**
|
|
2689
|
-
* Wait for Platform-side `execution_runs.status = <status>` where
|
|
2690
|
-
* `created_at > since`. Returns the final status + failure_reason or
|
|
2691
|
-
* null if the timeout elapsed. Used by webhook-pipeline (failed) and
|
|
2692
|
-
* orchestrator-never-reconnects (timed_out_stale).
|
|
2693
|
-
*/
|
|
2694
|
-
async function waitForPlatformExecutionRunStatusDirect(databaseUrl, opts) {
|
|
2695
|
-
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2696
|
-
const intervalMs = opts.intervalMs ?? 2e3;
|
|
2697
|
-
const deadline = Date.now() + timeoutMs;
|
|
2698
|
-
const pool = createPool(databaseUrl);
|
|
2699
|
-
try {
|
|
2700
|
-
while (Date.now() < deadline) {
|
|
2701
|
-
const result = await pool.query(`SELECT status, failure_reason FROM execution_runs
|
|
2702
|
-
WHERE created_at > $1 AND status = $2
|
|
2703
|
-
ORDER BY created_at DESC LIMIT 1`, [opts.since, opts.status]);
|
|
2704
|
-
if (result.rows.length > 0) return result.rows[0];
|
|
2705
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2706
|
-
}
|
|
2707
|
-
return null;
|
|
2708
|
-
} finally {
|
|
2709
|
-
await pool.end();
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
/**
|
|
2713
|
-
* Wait for Platform `event_log` to show at least `minDistinctRouted`
|
|
2714
|
-
* distinct `routed_to` values since a given timestamp. Used by
|
|
2715
|
-
* cluster-round-robin to prove at least 2 orchestrators received
|
|
2716
|
-
* deliveries. Returns the observed count (0 if timed out).
|
|
2717
|
-
*/
|
|
2718
|
-
async function waitForPlatformEventLogDistinctRoutedDirect(databaseUrl, opts) {
|
|
2719
|
-
const timeoutMs = opts.timeoutMs ?? 6e4;
|
|
2720
|
-
const intervalMs = opts.intervalMs ?? 3e3;
|
|
2721
|
-
const deadline = Date.now() + timeoutMs;
|
|
2722
|
-
const pool = createPool(databaseUrl);
|
|
2723
|
-
try {
|
|
2724
|
-
let distinct = 0;
|
|
2725
|
-
while (Date.now() < deadline) {
|
|
2726
|
-
distinct = (await pool.query(`SELECT COUNT(DISTINCT routed_to)::int AS cnt FROM event_log WHERE received_at > $1`, [opts.since])).rows[0]?.cnt ?? 0;
|
|
2727
|
-
if (distinct >= opts.minDistinctRouted) return { distinctRouted: distinct };
|
|
2728
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2729
|
-
}
|
|
2730
|
-
return { distinctRouted: distinct };
|
|
2731
|
-
} finally {
|
|
2732
|
-
await pool.end();
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
async function waitForEventLogRowByDeliveryIdDirect(databaseUrl, opts) {
|
|
2736
|
-
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
2737
|
-
const intervalMs = opts.intervalMs ?? 200;
|
|
2738
|
-
const deadline = Date.now() + timeoutMs;
|
|
2739
|
-
const pool = createPool(databaseUrl);
|
|
2740
|
-
try {
|
|
2741
|
-
while (Date.now() < deadline) {
|
|
2742
|
-
const res = await pool.query(`SELECT org_id, delivery_id, status, source, event, provider, payload_hash,
|
|
2743
|
-
payload_omitted, payload_key, payload_size_bytes, matched_count, run_id
|
|
2744
|
-
FROM event_log WHERE delivery_id = $1`, [opts.deliveryId]);
|
|
2745
|
-
if (res.rowCount && res.rowCount > 0) return res.rows[0];
|
|
2746
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2747
|
-
}
|
|
2748
|
-
return null;
|
|
2749
|
-
} finally {
|
|
2750
|
-
await pool.end();
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
/**
|
|
2754
|
-
* Resolve a Platform `webhook_sources.routing_key` for an org.
|
|
2755
|
-
*
|
|
2756
|
-
* - With `routingKeyLikePrefix` only: returns the newest match. Note that
|
|
2757
|
-
* this picks whichever generic source was registered most recently — if
|
|
2758
|
-
* tests have created additional generic sources (e.g. universal-git-forgejo
|
|
2759
|
-
* creating `stg-universal-git-forgejo`), the newest may NOT be the
|
|
2760
|
-
* `stg-generic` default seeded by `pnpm deploy:stg`.
|
|
2761
|
-
* - With `orchDbUrl` + `nameInOrchDb`: looks up the source by name in the
|
|
2762
|
-
* orchestrator's `generic_webhook_sources` table, then verifies the
|
|
2763
|
-
* resulting routing key is registered in Platform's `webhook_sources`.
|
|
2764
|
-
* This is the disambiguating path Bucket-B tests should use when other
|
|
2765
|
-
* generic sources may exist alongside the default.
|
|
2766
|
-
*/
|
|
2767
|
-
async function resolvePlatformWebhookSourceRoutingKeyDirect(platformDbUrl, opts) {
|
|
2768
|
-
if (opts.orchDbUrl && opts.nameInOrchDb) {
|
|
2769
|
-
const orchPool = createPool(opts.orchDbUrl);
|
|
2770
|
-
let candidateRoutingKey = null;
|
|
2771
|
-
try {
|
|
2772
|
-
const id = (await orchPool.query(`SELECT id FROM generic_webhook_sources
|
|
2773
|
-
WHERE customer_id = $1 AND name = $2 AND deleted_at IS NULL
|
|
2774
|
-
LIMIT 1`, [opts.orgId, opts.nameInOrchDb])).rows[0]?.id;
|
|
2775
|
-
if (!id) return null;
|
|
2776
|
-
candidateRoutingKey = `generic:${opts.orgId}:${id}`;
|
|
2777
|
-
} finally {
|
|
2778
|
-
await orchPool.end();
|
|
2779
|
-
}
|
|
2780
|
-
const platformPool = createPool(platformDbUrl);
|
|
2781
|
-
try {
|
|
2782
|
-
return (await platformPool.query(`SELECT routing_key FROM webhook_sources
|
|
2783
|
-
WHERE org_id = $1 AND routing_key = $2 LIMIT 1`, [opts.orgId, candidateRoutingKey])).rows[0]?.routing_key ?? null;
|
|
2784
|
-
} finally {
|
|
2785
|
-
await platformPool.end();
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
const pool = createPool(platformDbUrl);
|
|
2789
|
-
try {
|
|
2790
|
-
return (await pool.query(`SELECT routing_key FROM webhook_sources
|
|
2791
|
-
WHERE org_id = $1 AND routing_key LIKE $2
|
|
2792
|
-
ORDER BY registered_at DESC LIMIT 1`, [opts.orgId, `${opts.routingKeyLikePrefix}%`])).rows[0]?.routing_key ?? null;
|
|
2793
|
-
} finally {
|
|
2794
|
-
await pool.end();
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
/**
|
|
2798
|
-
* Idempotent seeding of an E2E regular user + org membership + owner
|
|
2799
|
-
* role on the Platform DB. Used by stg-ha-smoke before seeding a user
|
|
2800
|
-
* API key. Requires the org to already have an owner role.
|
|
2801
|
-
*/
|
|
2802
|
-
async function ensureOrgOwnerMemberDirect(platformDbUrl, opts) {
|
|
2803
|
-
const pool = createPool(platformDbUrl);
|
|
2804
|
-
try {
|
|
2805
|
-
const ownerRole = await pool.query(`SELECT id FROM roles WHERE org_id = $1 AND is_owner = true LIMIT 1`, [opts.orgId]);
|
|
2806
|
-
if (ownerRole.rows.length === 0) throw new Error(`ensureOrgOwnerMemberDirect: no owner role found for org ${opts.orgId}`);
|
|
2807
|
-
const ownerRoleId = ownerRole.rows[0].id;
|
|
2808
|
-
await pool.query(`INSERT INTO users (idp_sub, email, display_name)
|
|
2809
|
-
VALUES ($1, $2, $3)
|
|
2810
|
-
ON CONFLICT (idp_sub) DO NOTHING`, [
|
|
2811
|
-
opts.idpSub,
|
|
2812
|
-
opts.email,
|
|
2813
|
-
opts.displayName
|
|
2814
|
-
]);
|
|
2815
|
-
await pool.query(`INSERT INTO org_members (org_id, user_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [opts.orgId, opts.idpSub]);
|
|
2816
|
-
await pool.query(`INSERT INTO role_assignments (org_id, user_id, role_id, assigned_by)
|
|
2817
|
-
VALUES ($1, $2, $3, $2)
|
|
2818
|
-
ON CONFLICT (org_id, user_id, role_id) DO NOTHING`, [
|
|
2819
|
-
opts.orgId,
|
|
2820
|
-
opts.idpSub,
|
|
2821
|
-
ownerRoleId
|
|
2822
|
-
]);
|
|
2823
|
-
return { ownerRoleId };
|
|
2824
|
-
} finally {
|
|
2825
|
-
await pool.end();
|
|
2826
|
-
}
|
|
2827
|
-
}
|
|
2828
|
-
/**
|
|
2829
|
-
* Cleanup peer_credentials by instance_id LIKE pattern. Used by the
|
|
2830
|
-
* cluster-peer-credentials e2e before/after each test to wipe its
|
|
2831
|
-
* own seeded rows without touching real cluster credentials.
|
|
2832
|
-
*/
|
|
2833
|
-
async function deletePeerCredentialsByInstanceIdLikeDirect(databaseUrl, opts) {
|
|
2834
|
-
const pool = createPool(databaseUrl);
|
|
2835
|
-
try {
|
|
2836
|
-
return { deleted: (await pool.query(`DELETE FROM peer_credentials WHERE instance_id LIKE $1`, [opts.pattern])).rowCount ?? 0 };
|
|
2837
|
-
} finally {
|
|
2838
|
-
await pool.end();
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Insert a peer_credentials row with an explicit expires_at. Used by
|
|
2843
|
-
* the "expired credential is not returned" e2e — the store's save()
|
|
2844
|
-
* always computes a future expiry, so tests that need a pre-expired
|
|
2845
|
-
* row must bypass it.
|
|
2846
|
-
*/
|
|
2847
|
-
async function insertPeerCredentialExpiredDirect(databaseUrl, opts) {
|
|
2848
|
-
const pool = createPool(databaseUrl);
|
|
2849
|
-
try {
|
|
2850
|
-
await pool.query(`INSERT INTO peer_credentials (instance_id, credential_hash, role, routing_keys, expires_at)
|
|
2851
|
-
VALUES ($1, $2, $3, $4::text[], $5)`, [
|
|
2852
|
-
opts.instanceId,
|
|
2853
|
-
opts.credentialHash,
|
|
2854
|
-
opts.role,
|
|
2855
|
-
Array.from(opts.routingKeys),
|
|
2856
|
-
opts.expiresAt
|
|
2857
|
-
]);
|
|
2858
|
-
} finally {
|
|
2859
|
-
await pool.end();
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
/**
|
|
2863
|
-
* READ-ONLY: `revoked_at` for a peer_credentials row by hash. Used
|
|
2864
|
-
* by the "save revokes old credential" e2e to assert revocation.
|
|
2865
|
-
* Returns null when the row is missing entirely (distinct from
|
|
2866
|
-
* "present but not revoked").
|
|
2867
|
-
*/
|
|
2868
|
-
async function getPeerCredentialRevokedAtDirect(databaseUrl, opts) {
|
|
2869
|
-
const pool = createPool(databaseUrl);
|
|
2870
|
-
try {
|
|
2871
|
-
const result = await pool.query(`SELECT revoked_at FROM peer_credentials WHERE credential_hash = $1`, [opts.credentialHash]);
|
|
2872
|
-
if (result.rows.length === 0) return {
|
|
2873
|
-
present: false,
|
|
2874
|
-
revokedAt: null
|
|
2875
|
-
};
|
|
2876
|
-
return {
|
|
2877
|
-
present: true,
|
|
2878
|
-
revokedAt: result.rows[0].revoked_at
|
|
2879
|
-
};
|
|
2880
|
-
} finally {
|
|
2881
|
-
await pool.end();
|
|
2882
|
-
}
|
|
2883
|
-
}
|
|
2884
|
-
/**
|
|
2885
|
-
* READ-ONLY: list active peer_credentials ids EXCLUDING a
|
|
2886
|
-
* `instance_id LIKE` pattern. Used by revokeAll e2e to snapshot
|
|
2887
|
-
* real cluster credentials it will restore later.
|
|
2888
|
-
*/
|
|
2889
|
-
async function listActivePeerCredentialsExcludingDirect(databaseUrl, opts) {
|
|
2890
|
-
const pool = createPool(databaseUrl);
|
|
2891
|
-
try {
|
|
2892
|
-
return { ids: (await pool.query(`SELECT id FROM peer_credentials
|
|
2893
|
-
WHERE revoked_at IS NULL AND instance_id NOT LIKE $1`, [opts.excludeInstanceIdPattern])).rows.map((r) => r.id) };
|
|
2894
|
-
} finally {
|
|
2895
|
-
await pool.end();
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
/**
|
|
2899
|
-
* Clear revoked_at on a set of peer_credentials ids. Used by revokeAll
|
|
2900
|
-
* e2e to restore real cluster credentials after the destructive test.
|
|
2901
|
-
*/
|
|
2902
|
-
async function clearPeerCredentialsRevokedAtByIdsDirect(databaseUrl, opts) {
|
|
2903
|
-
if (opts.ids.length === 0) return { updated: 0 };
|
|
2904
|
-
const pool = createPool(databaseUrl);
|
|
2905
|
-
try {
|
|
2906
|
-
return { updated: (await pool.query(`UPDATE peer_credentials SET revoked_at = NULL WHERE id = ANY($1::uuid[])`, [opts.ids])).rowCount ?? 0 };
|
|
2907
|
-
} finally {
|
|
2908
|
-
await pool.end();
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
/**
|
|
2912
|
-
* READ-ONLY: count currently-active (non-revoked, non-expired)
|
|
2913
|
-
* peer_credentials rows for a given instance_id. Used by the
|
|
2914
|
-
* concurrent-save e2e to assert 1 <= count <= 2.
|
|
2915
|
-
*/
|
|
2916
|
-
async function countActivePeerCredentialsByInstanceDirect(databaseUrl, opts) {
|
|
2917
|
-
const pool = createPool(databaseUrl);
|
|
2918
|
-
try {
|
|
2919
|
-
return (await pool.query(`SELECT count(*)::int AS cnt FROM peer_credentials
|
|
2920
|
-
WHERE instance_id = $1 AND revoked_at IS NULL AND expires_at > now()`, [opts.instanceId])).rows[0]?.cnt ?? 0;
|
|
2921
|
-
} finally {
|
|
2922
|
-
await pool.end();
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
/**
|
|
2926
|
-
* Terminate every idle backend of the connecting user except our own
|
|
2927
|
-
* connection. Mirrors what a Postgres leader demotion does to idle pooled
|
|
2928
|
-
* connections — used by resilience tests to verify the pg pool error
|
|
2929
|
-
* handlers absorb the termination without a process restart.
|
|
2930
|
-
*
|
|
2931
|
-
* Returns the number of backends terminated.
|
|
2932
|
-
*/
|
|
2933
|
-
async function terminateIdleDbBackendsDirect(databaseUrl) {
|
|
2934
|
-
const pool = createPool(databaseUrl);
|
|
2935
|
-
try {
|
|
2936
|
-
return (await pool.query(`SELECT pg_terminate_backend(pid)
|
|
2937
|
-
FROM pg_stat_activity
|
|
2938
|
-
WHERE pid <> pg_backend_pid()
|
|
2939
|
-
AND usename = current_user
|
|
2940
|
-
AND state = 'idle'`)).rowCount ?? 0;
|
|
2941
|
-
} finally {
|
|
2942
|
-
await pool.end();
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
1127
|
//#endregion
|
|
2946
|
-
export {
|
|
1128
|
+
export { MIGRATION_HASH_TABLE, PROVIDER_HASH_KEY, REGISTERABLE_TRIGGER_TYPES, clearDispatchQueueDirect, computeMigrationsHash, createContextTemplateDirect, createDbRole, createReadOnlyDbUser, deleteContextDirect, dropAndCreateDatabase, emitKiciEventDirect, ensureDatabase, isSchemaCurrent, listCheckRunTrackingDirect, listContextsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, prunePeerCredentialsDirect, purgeContextsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, seedContextBindingDirect, seedContextDirect, setContextPolicyDirect, setContextSecretDirect, showContextDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash };
|
|
2947
1129
|
|
|
2948
1130
|
//# sourceMappingURL=db-admin.js.map
|