@kici-dev/shared 0.1.13 → 0.1.15
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/README.md +13 -1
- package/dist/cold-store/chunk-encoder.js +1 -1
- package/dist/cold-store/chunk-id.js +1 -1
- package/dist/cold-store/cold-store.js +1 -1
- package/dist/db-admin.d.ts +39 -0
- package/dist/db-admin.js +60 -5
- package/dist/db.d.ts +20 -1
- package/dist/db.js +34 -2
- package/dist/db.test.d.ts +2 -0
- package/dist/env/define-env.js +27 -6
- package/dist/env/logger-env.d.ts +1 -1
- package/dist/graceful-shutdown.d.ts +13 -1
- package/dist/graceful-shutdown.js +24 -10
- package/dist/idempotency.d.ts +1 -67
- package/dist/idempotency.js +2 -48
- package/dist/index.d.ts +3 -9
- package/dist/index.js +3 -9
- package/dist/package-manager-types.d.ts +1 -21
- package/dist/package-manager-types.js +2 -34
- package/dist/package-manager.d.ts +1 -51
- package/dist/package-manager.js +2 -130
- package/dist/s3-client.js +4 -1
- package/dist/s3-client.test.d.ts +2 -0
- package/dist/ts-loader-hook.d.ts +1 -25
- package/dist/ts-loader-hook.js +2 -47
- package/package.json +12 -7
- package/sbom.spdx.json +60 -5
- package/dist/crypto.d.ts +0 -33
- package/dist/crypto.js +0 -67
- package/dist/error.d.ts +0 -16
- package/dist/error.js +0 -58
- package/dist/error.test.d.ts +0 -2
- package/dist/format-bytes.d.ts +0 -5
- package/dist/format-bytes.js +0 -15
- package/dist/format-bytes.test.d.ts +0 -2
- package/dist/format-duration.d.ts +0 -11
- package/dist/format-duration.js +0 -32
- package/dist/format-duration.test.d.ts +0 -2
- package/dist/idempotency.test.d.ts +0 -2
- package/dist/logger.d.ts +0 -60
- package/dist/logger.js +0 -181
- package/dist/logger.test.d.ts +0 -2
- package/dist/package-manager.test.d.ts +0 -2
- package/dist/request-context.d.ts +0 -42
- package/dist/request-context.js +0 -37
- package/dist/zx.d.ts +0 -8
- package/dist/zx.js +0 -78
package/README.md
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
# @kici-dev/shared
|
|
2
|
+
|
|
3
|
+
Shared utilities for the KiCI CI/CD stack — logging, zx setup, crypto, telemetry, health and metrics routes. No business logic. Re-exports `@kici-dev/core`.
|
|
4
|
+
|
|
5
|
+
This is an internal support library — other `@kici-dev` packages depend on it; it is not meant to be installed directly.
|
|
6
|
+
|
|
7
|
+
Part of [KiCI](https://kici.dev) — CI/CD workflows as TypeScript code: author them with full language power, dry-run them locally, and run them on your own infrastructure.
|
|
8
|
+
|
|
9
|
+
## Links
|
|
10
|
+
|
|
11
|
+
- Documentation: <https://docs.kici.dev/architecture/overview/>
|
|
12
|
+
- Source: <https://github.com/kici-dev/kici-public/tree/main/packages/shared>
|
|
13
|
+
- License: Apache-2.0
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import "../chunk-gOLHoazu.js";
|
|
2
|
-
import { sha256 } from "../crypto.js";
|
|
3
2
|
import { createS3Client } from "../s3-client.js";
|
|
4
3
|
import { chunkObjectKey, encodeKeySegment, tablePrefix, tenantDayPrefix } from "./key.js";
|
|
5
4
|
import { coldDaysToBucket, isLongerColdRetention } from "./bucket.js";
|
|
@@ -7,6 +6,7 @@ import { computeChunkId } from "./chunk-id.js";
|
|
|
7
6
|
import { decodeChunk, encodeChunk } from "./chunk-encoder.js";
|
|
8
7
|
import { parseManifest, serializeManifest } from "./manifest.js";
|
|
9
8
|
import { coldStoreArchiveBytesTotal, coldStoreArchiveCyclesTotal, coldStoreArchiveDurationSeconds, coldStoreArchiveRowsTotal, coldStorePurgeBytesTotal, coldStorePurgeChunksTotal, coldStorePurgeDurationSeconds, coldStoreRehydrateBytesTotal, coldStoreRehydrateDurationSeconds, coldStoreRehydrateRequestsTotal, coldStoreReplayDurationSeconds, coldStoreReplayRowsTotal, coldStoreVerifyFailuresTotal } from "./metrics.js";
|
|
9
|
+
import { sha256 } from "@kici-dev/core";
|
|
10
10
|
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
11
11
|
//#region src/cold-store/cold-store.ts
|
|
12
12
|
/**
|
package/dist/db-admin.d.ts
CHANGED
|
@@ -60,6 +60,16 @@ export interface EnsureDatabaseOpts {
|
|
|
60
60
|
* grant is a no-op).
|
|
61
61
|
*/
|
|
62
62
|
revokeConnectFromPublic?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* After creating (or finding) the database — and after the optional
|
|
65
|
+
* `REVOKE CONNECT … FROM PUBLIC` — `GRANT CONNECT ON DATABASE "<name>"
|
|
66
|
+
* TO "<role>"` for each role here. Pairs with `revokeConnectFromPublic`
|
|
67
|
+
* to re-grant CONNECT to the specific non-PUBLIC roles that legitimately
|
|
68
|
+
* need it once the default PUBLIC grant is revoked. Idempotent (GRANT on
|
|
69
|
+
* an already-present grant is a no-op). Each name is validated as a SQL
|
|
70
|
+
* identifier before interpolation.
|
|
71
|
+
*/
|
|
72
|
+
grantConnectToRoles?: string[];
|
|
63
73
|
}
|
|
64
74
|
/**
|
|
65
75
|
* CREATE DATABASE IF NOT EXISTS (idempotent). With no `opts`, the URL's
|
|
@@ -177,6 +187,7 @@ export interface SeedEnvironmentOpts {
|
|
|
177
187
|
waitTimerSeconds?: number | null;
|
|
178
188
|
holdExpirySeconds?: number | null;
|
|
179
189
|
minimumTrust?: string | null;
|
|
190
|
+
globPattern?: string | null;
|
|
180
191
|
}
|
|
181
192
|
export interface SeedEnvironmentResult {
|
|
182
193
|
envId: string;
|
|
@@ -188,6 +199,24 @@ export interface SeedEnvironmentResult {
|
|
|
188
199
|
* are JSON-serialised server-side; pass them as plain arrays or objects.
|
|
189
200
|
*/
|
|
190
201
|
export declare function seedEnvironmentDirect(databaseUrl: string, opts: SeedEnvironmentOpts): Promise<SeedEnvironmentResult>;
|
|
202
|
+
export interface DeleteEnvironmentOpts {
|
|
203
|
+
orgId: string;
|
|
204
|
+
name: string;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Delete an environment keyed by (org_id, name). Returns whether a row was
|
|
208
|
+
* removed. The `environment_bindings`, `environment_variables`, and
|
|
209
|
+
* `environment_source_overrides` children all carry
|
|
210
|
+
* `FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE`,
|
|
211
|
+
* so a single DELETE on `environments` cascades to those children. The
|
|
212
|
+
* `held_runs` FK uses `ON DELETE SET NULL`, so terminal held-run history
|
|
213
|
+
* survives the delete with a null environment reference. Pending held runs
|
|
214
|
+
* still reference the environment, so this helper pre-checks their count and
|
|
215
|
+
* throws before issuing the DELETE — approve or reject them first.
|
|
216
|
+
*/
|
|
217
|
+
export declare function deleteEnvironmentDirect(databaseUrl: string, opts: DeleteEnvironmentOpts): Promise<{
|
|
218
|
+
deleted: boolean;
|
|
219
|
+
}>;
|
|
191
220
|
export interface SeedEnvironmentBindingOpts {
|
|
192
221
|
orgId: string;
|
|
193
222
|
envName: string;
|
|
@@ -209,6 +238,7 @@ export interface SetEnvironmentPolicyOpts {
|
|
|
209
238
|
holdExpirySeconds?: number | null;
|
|
210
239
|
minimumTrust?: string | null;
|
|
211
240
|
enabled?: boolean;
|
|
241
|
+
allowLocalExecution?: boolean;
|
|
212
242
|
}
|
|
213
243
|
/**
|
|
214
244
|
* UPDATE only the policy fields that were explicitly provided. Columns that
|
|
@@ -1452,5 +1482,14 @@ export declare function clearPeerCredentialsRevokedAtByIdsDirect(databaseUrl: st
|
|
|
1452
1482
|
export declare function countActivePeerCredentialsByInstanceDirect(databaseUrl: string, opts: {
|
|
1453
1483
|
instanceId: string;
|
|
1454
1484
|
}): Promise<number>;
|
|
1485
|
+
/**
|
|
1486
|
+
* Terminate every idle backend of the connecting user except our own
|
|
1487
|
+
* connection. Mirrors what a Postgres leader demotion does to idle pooled
|
|
1488
|
+
* connections — used by resilience tests to verify the pg pool error
|
|
1489
|
+
* handlers absorb the termination without a process restart.
|
|
1490
|
+
*
|
|
1491
|
+
* Returns the number of backends terminated.
|
|
1492
|
+
*/
|
|
1493
|
+
export declare function terminateIdleDbBackendsDirect(databaseUrl: string): Promise<number>;
|
|
1455
1494
|
export {};
|
|
1456
1495
|
//# sourceMappingURL=db-admin.d.ts.map
|
package/dist/db-admin.js
CHANGED
|
@@ -111,6 +111,10 @@ async function ensureDatabase(databaseUrl, opts = {}) {
|
|
|
111
111
|
outcome = "created";
|
|
112
112
|
}
|
|
113
113
|
if (opts.revokeConnectFromPublic) await pool.query(`REVOKE CONNECT ON DATABASE "${dbName}" FROM PUBLIC`);
|
|
114
|
+
for (const role of opts.grantConnectToRoles ?? []) {
|
|
115
|
+
assertValidIdentifier(role, "grant-connect role");
|
|
116
|
+
await pool.query(`GRANT CONNECT ON DATABASE "${dbName}" TO "${role}"`);
|
|
117
|
+
}
|
|
114
118
|
return outcome;
|
|
115
119
|
});
|
|
116
120
|
}
|
|
@@ -347,7 +351,8 @@ const ENV_POLICY_COLUMNS = new Set([
|
|
|
347
351
|
"wait_timer_seconds",
|
|
348
352
|
"hold_expiry_seconds",
|
|
349
353
|
"minimum_trust",
|
|
350
|
-
"enabled"
|
|
354
|
+
"enabled",
|
|
355
|
+
"allow_local_execution"
|
|
351
356
|
]);
|
|
352
357
|
/**
|
|
353
358
|
* Upsert an environment row keyed by (org_id, name). Returns the env id and
|
|
@@ -366,9 +371,9 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
366
371
|
const reviewersJson = opts.requiredReviewers === void 0 ? null : JSON.stringify(opts.requiredReviewers);
|
|
367
372
|
const row = (await pool.query(`INSERT INTO environments
|
|
368
373
|
(org_id, name, type, enabled, branch_restrictions, required_reviewers,
|
|
369
|
-
wait_timer_seconds, hold_expiry_seconds, minimum_trust)
|
|
374
|
+
wait_timer_seconds, hold_expiry_seconds, minimum_trust, glob_pattern)
|
|
370
375
|
VALUES ($1, $2, COALESCE($3, 'fixed'), COALESCE($4, true), $5::jsonb, $6::jsonb,
|
|
371
|
-
$7, COALESCE($8, 86400), $9)
|
|
376
|
+
$7, COALESCE($8, 86400), $9, $10)
|
|
372
377
|
ON CONFLICT (org_id, name) DO UPDATE SET
|
|
373
378
|
type = COALESCE(EXCLUDED.type, environments.type),
|
|
374
379
|
enabled = EXCLUDED.enabled,
|
|
@@ -377,6 +382,7 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
377
382
|
wait_timer_seconds = EXCLUDED.wait_timer_seconds,
|
|
378
383
|
hold_expiry_seconds = EXCLUDED.hold_expiry_seconds,
|
|
379
384
|
minimum_trust = EXCLUDED.minimum_trust,
|
|
385
|
+
glob_pattern = COALESCE(EXCLUDED.glob_pattern, environments.glob_pattern),
|
|
380
386
|
updated_at = now()
|
|
381
387
|
RETURNING id, (xmax = 0) AS inserted`, [
|
|
382
388
|
opts.orgId,
|
|
@@ -387,7 +393,8 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
387
393
|
reviewersJson,
|
|
388
394
|
opts.waitTimerSeconds ?? null,
|
|
389
395
|
opts.holdExpirySeconds ?? null,
|
|
390
|
-
opts.minimumTrust ?? null
|
|
396
|
+
opts.minimumTrust ?? null,
|
|
397
|
+
opts.globPattern ?? null
|
|
391
398
|
])).rows[0];
|
|
392
399
|
if (!row) throw new Error(`environment: upsert returned no row for ${opts.name}`);
|
|
393
400
|
return {
|
|
@@ -399,6 +406,33 @@ async function seedEnvironmentDirect(databaseUrl, opts) {
|
|
|
399
406
|
}
|
|
400
407
|
}
|
|
401
408
|
/**
|
|
409
|
+
* Delete an environment keyed by (org_id, name). Returns whether a row was
|
|
410
|
+
* removed. The `environment_bindings`, `environment_variables`, and
|
|
411
|
+
* `environment_source_overrides` children all carry
|
|
412
|
+
* `FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE`,
|
|
413
|
+
* so a single DELETE on `environments` cascades to those children. The
|
|
414
|
+
* `held_runs` FK uses `ON DELETE SET NULL`, so terminal held-run history
|
|
415
|
+
* survives the delete with a null environment reference. Pending held runs
|
|
416
|
+
* still reference the environment, so this helper pre-checks their count and
|
|
417
|
+
* throws before issuing the DELETE — approve or reject them first.
|
|
418
|
+
*/
|
|
419
|
+
async function deleteEnvironmentDirect(databaseUrl, opts) {
|
|
420
|
+
const pool = new pg.Pool({
|
|
421
|
+
connectionString: databaseUrl,
|
|
422
|
+
max: 1
|
|
423
|
+
});
|
|
424
|
+
try {
|
|
425
|
+
const pending = await pool.query(`SELECT count(*)::text AS count FROM held_runs hr
|
|
426
|
+
JOIN environments e ON e.id = hr.environment_id
|
|
427
|
+
WHERE e.org_id = $1 AND e.name = $2 AND hr.status = 'pending'`, [opts.orgId, opts.name]);
|
|
428
|
+
const pendingCount = Number(pending.rows[0]?.count ?? 0);
|
|
429
|
+
if (pendingCount > 0) throw new Error(`environment has ${pendingCount} pending held run(s) — approve or reject them first`);
|
|
430
|
+
return { deleted: (await pool.query(`DELETE FROM environments WHERE org_id = $1 AND name = $2 RETURNING id`, [opts.orgId, opts.name])).rows.length > 0 };
|
|
431
|
+
} finally {
|
|
432
|
+
await pool.end();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
402
436
|
* Upsert an `environment_bindings` row connecting `envName` to `scopePattern`.
|
|
403
437
|
* Throws if the environment does not exist.
|
|
404
438
|
*/
|
|
@@ -445,6 +479,7 @@ async function setEnvironmentPolicyDirect(databaseUrl, opts) {
|
|
|
445
479
|
if (opts.holdExpirySeconds !== void 0) addSet("hold_expiry_seconds", opts.holdExpirySeconds);
|
|
446
480
|
if (opts.minimumTrust !== void 0) addSet("minimum_trust", opts.minimumTrust);
|
|
447
481
|
if (opts.enabled !== void 0) addSet("enabled", opts.enabled);
|
|
482
|
+
if (opts.allowLocalExecution !== void 0) addSet("allow_local_execution", opts.allowLocalExecution);
|
|
448
483
|
if (setClauses.length === 0) throw new Error("environment: setEnvironmentPolicy requires at least one policy field");
|
|
449
484
|
const pool = new pg.Pool({
|
|
450
485
|
connectionString: databaseUrl,
|
|
@@ -2551,7 +2586,27 @@ async function countActivePeerCredentialsByInstanceDirect(databaseUrl, opts) {
|
|
|
2551
2586
|
await pool.end();
|
|
2552
2587
|
}
|
|
2553
2588
|
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Terminate every idle backend of the connecting user except our own
|
|
2591
|
+
* connection. Mirrors what a Postgres leader demotion does to idle pooled
|
|
2592
|
+
* connections — used by resilience tests to verify the pg pool error
|
|
2593
|
+
* handlers absorb the termination without a process restart.
|
|
2594
|
+
*
|
|
2595
|
+
* Returns the number of backends terminated.
|
|
2596
|
+
*/
|
|
2597
|
+
async function terminateIdleDbBackendsDirect(databaseUrl) {
|
|
2598
|
+
const pool = createPool(databaseUrl);
|
|
2599
|
+
try {
|
|
2600
|
+
return (await pool.query(`SELECT pg_terminate_backend(pid)
|
|
2601
|
+
FROM pg_stat_activity
|
|
2602
|
+
WHERE pid <> pg_backend_pid()
|
|
2603
|
+
AND usename = current_user
|
|
2604
|
+
AND state = 'idle'`)).rowCount ?? 0;
|
|
2605
|
+
} finally {
|
|
2606
|
+
await pool.end();
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2554
2609
|
//#endregion
|
|
2555
|
-
export { MIGRATION_HASH_TABLE, PROVIDER_HASH_KEY, REGISTERABLE_TRIGGER_TYPES, apiKeyExistsDirect, bumpRegistryVersionDirect, bumpRegistryVersionSimpleDirect, cleanupExecutionRowsDirect, clearDispatchQueueDirect, clearPeerCredentialsRevokedAtByIdsDirect, computeMigrationsHash, countActivePeerCredentialsByInstanceDirect, countCronLastFiredDirect, countHeldRunsByRunIdDirect, countWebhookSourcesByConnectionIdDirect, createDbRole, createEnvironmentTemplateDirect, createJoinTokenDirect, createReadOnlyDbUser, deleteCronLastFiredDirect, deleteCrossRepoTrustDirect, deleteExecutionRunsByWorkflowNameDirect, deleteGenericWebhookSourcesByNameDirect, deleteKiciEventsDirect, deleteOrgSettingsByCustomerIdDirect, deletePeerCredentialsByInstanceIdLikeDirect, deleteWorkflowRegistrationsDirect, describeTableColumnsDirect, dropAndCreateDatabase, dropDatabaseDirect, emitKiciEventDirect, ensureDatabase, ensureOrgOwnerMemberDirect, findAnyUserApiKeyIdDirect, getExecutionRunSecurityDirect, getGenericWebhookSourceByRoutingKeyDirect, getHeldRunByIdDirect, getPeerCredentialRevokedAtDirect, getRegistryVersionDirect, getWebhookSourceByRoutingKeyDirect, getWorkflowRegistrationByIdDirect, insertCronLastFiredNowDirect, insertCrossRepoTrustStrictDirect, insertKiciEventRawDirect, insertPeerCredentialExpiredDirect, insertWorkflowRegistrationRawDirect, insertWorkflowRegistrationStrictDirect, isSchemaCurrent, isSchemaCurrentFromFilesDirect, latestExecutionRunByStatusDirect, listActiveGenericWebhookSourcesDirect, listActivePeerCredentialsExcludingDirect, listCrossRepoTrustBySourceRoutingKeyDirect, listEnvironmentsDirect, listExecutionJobsDirect, listExecutionRunsDirect, listKiciEventsDirect, listQueueDirect, listRegistrationsByRoutingKeyDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, platformConnectionExistsDirect, pollKiciEventsDirect, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, resolvePlatformWebhookSourceRoutingKeyDirect, restoreSoftDeletedGenericWebhookSourceDirect, seedApiKeyInlineDirect, seedCiSecurityFixturesDirect, seedCrossRepoTrustDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, seedGenericWebhookSourceDirect, seedSourcePrivateKeyDirect, seedSyntheticGithubSourceDirect, seedUniversalGitSourceDirect, seedWebhookSecretDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, showEnvironmentDirect, showExecutionRunDirect, showKiciEventDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, storeMigrationContentHashInTableDirect, tableExistsDirect, updateGenericWebhookVerificationConfigDirect, updateOrgSettingsDeniedReposDirect, updateSourceRoutingKeyDirect, updateWorkflowRegistrationCommitShaDirect, upsertCronLastFiredDirect, upsertOrgSettingsGlobalWorkflowsDirect, verifyKiciEventNotifyDirect, waitForEventLogRowByDeliveryIdDirect, waitForExecutionRunStatusSinceDirect, waitForLatestExecutionJobStatusDirect, waitForPlatformEventLogDistinctRoutedDirect, waitForPlatformExecutionRunStatusDirect, waitForPlatformRegistrationsDirect, waitForPostgresDirect, waitForRegistrationsByRoutingKeyDirect, waitForRegistrationsUpdatedAtAdvanceDirect, waitForRunCompletionDirect };
|
|
2610
|
+
export { MIGRATION_HASH_TABLE, PROVIDER_HASH_KEY, REGISTERABLE_TRIGGER_TYPES, apiKeyExistsDirect, bumpRegistryVersionDirect, bumpRegistryVersionSimpleDirect, cleanupExecutionRowsDirect, clearDispatchQueueDirect, clearPeerCredentialsRevokedAtByIdsDirect, computeMigrationsHash, countActivePeerCredentialsByInstanceDirect, countCronLastFiredDirect, countHeldRunsByRunIdDirect, countWebhookSourcesByConnectionIdDirect, createDbRole, createEnvironmentTemplateDirect, createJoinTokenDirect, createReadOnlyDbUser, deleteCronLastFiredDirect, deleteCrossRepoTrustDirect, deleteEnvironmentDirect, deleteExecutionRunsByWorkflowNameDirect, deleteGenericWebhookSourcesByNameDirect, deleteKiciEventsDirect, deleteOrgSettingsByCustomerIdDirect, deletePeerCredentialsByInstanceIdLikeDirect, deleteWorkflowRegistrationsDirect, describeTableColumnsDirect, dropAndCreateDatabase, dropDatabaseDirect, emitKiciEventDirect, ensureDatabase, ensureOrgOwnerMemberDirect, findAnyUserApiKeyIdDirect, getExecutionRunSecurityDirect, getGenericWebhookSourceByRoutingKeyDirect, getHeldRunByIdDirect, getPeerCredentialRevokedAtDirect, getRegistryVersionDirect, getWebhookSourceByRoutingKeyDirect, getWorkflowRegistrationByIdDirect, insertCronLastFiredNowDirect, insertCrossRepoTrustStrictDirect, insertKiciEventRawDirect, insertPeerCredentialExpiredDirect, insertWorkflowRegistrationRawDirect, insertWorkflowRegistrationStrictDirect, isSchemaCurrent, isSchemaCurrentFromFilesDirect, latestExecutionRunByStatusDirect, listActiveGenericWebhookSourcesDirect, listActivePeerCredentialsExcludingDirect, listCrossRepoTrustBySourceRoutingKeyDirect, listEnvironmentsDirect, listExecutionJobsDirect, listExecutionRunsDirect, listKiciEventsDirect, listQueueDirect, listRegistrationsByRoutingKeyDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, platformConnectionExistsDirect, pollKiciEventsDirect, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, readStoredMigrationContentHash, registerWorkflowManualDirect, resetRaftStateDirect, resolvePlatformWebhookSourceRoutingKeyDirect, restoreSoftDeletedGenericWebhookSourceDirect, seedApiKeyInlineDirect, seedCiSecurityFixturesDirect, seedCrossRepoTrustDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, seedGenericWebhookSourceDirect, seedSourcePrivateKeyDirect, seedSyntheticGithubSourceDirect, seedUniversalGitSourceDirect, seedWebhookSecretDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, showEnvironmentDirect, showExecutionRunDirect, showKiciEventDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, storeMigrationContentHashInTableDirect, tableExistsDirect, terminateIdleDbBackendsDirect, updateGenericWebhookVerificationConfigDirect, updateOrgSettingsDeniedReposDirect, updateSourceRoutingKeyDirect, updateWorkflowRegistrationCommitShaDirect, upsertCronLastFiredDirect, upsertOrgSettingsGlobalWorkflowsDirect, verifyKiciEventNotifyDirect, waitForEventLogRowByDeliveryIdDirect, waitForExecutionRunStatusSinceDirect, waitForLatestExecutionJobStatusDirect, waitForPlatformEventLogDistinctRoutedDirect, waitForPlatformExecutionRunStatusDirect, waitForPlatformRegistrationsDirect, waitForPostgresDirect, waitForRegistrationsByRoutingKeyDirect, waitForRegistrationsUpdatedAtAdvanceDirect, waitForRunCompletionDirect };
|
|
2556
2611
|
|
|
2557
2612
|
//# sourceMappingURL=db-admin.js.map
|
package/dist/db.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import pg from 'pg';
|
|
2
2
|
import { Kysely } from 'kysely';
|
|
3
|
+
/** Where a pg connection error surfaced. */
|
|
4
|
+
export type PgPoolErrorSource = 'idle-pool' | 'client';
|
|
5
|
+
export interface CreatePoolOptions {
|
|
6
|
+
/** Extra pg.Pool config merged over the connection string (e.g. max, connectionTimeoutMillis). */
|
|
7
|
+
config?: Omit<pg.PoolConfig, 'connectionString'>;
|
|
8
|
+
/**
|
|
9
|
+
* Optional hook invoked after the built-in log line on every absorbed
|
|
10
|
+
* connection error (e.g. to increment a metrics counter). Additive — it
|
|
11
|
+
* never replaces the log.
|
|
12
|
+
*/
|
|
13
|
+
onError?: (err: Error, source: PgPoolErrorSource) => void;
|
|
14
|
+
}
|
|
3
15
|
/**
|
|
4
16
|
* Create PostgreSQL connection pool.
|
|
17
|
+
*
|
|
18
|
+
* Always attaches error handlers for both idle pooled clients (the pool's
|
|
19
|
+
* own 'error' event) and checked-out clients (per-client 'error' via the
|
|
20
|
+
* 'connect' hook). Without them, a terminated backend — e.g. a Postgres
|
|
21
|
+
* leader switchover — escalates to an uncaughtException and a full process
|
|
22
|
+
* restart. The broken connection is logged and discarded; pg replaces it on
|
|
23
|
+
* the next acquire. In-flight query failures still reject to their callers.
|
|
5
24
|
*/
|
|
6
|
-
export declare function createPool(databaseUrl: string): pg.Pool;
|
|
25
|
+
export declare function createPool(databaseUrl: string, options?: CreatePoolOptions): pg.Pool;
|
|
7
26
|
/**
|
|
8
27
|
* Create Kysely database instance (PostgreSQL only).
|
|
9
28
|
*
|
package/dist/db.js
CHANGED
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
import "./chunk-gOLHoazu.js";
|
|
2
2
|
import pg from "pg";
|
|
3
3
|
import { Kysely, PostgresDialect } from "kysely";
|
|
4
|
+
import { createLogger } from "@kici-dev/core";
|
|
4
5
|
//#region src/db.ts
|
|
6
|
+
let poolLogger;
|
|
7
|
+
function getPoolLogger() {
|
|
8
|
+
poolLogger ??= createLogger({ prefix: "pg-pool" });
|
|
9
|
+
return poolLogger;
|
|
10
|
+
}
|
|
5
11
|
/**
|
|
6
12
|
* Create PostgreSQL connection pool.
|
|
13
|
+
*
|
|
14
|
+
* Always attaches error handlers for both idle pooled clients (the pool's
|
|
15
|
+
* own 'error' event) and checked-out clients (per-client 'error' via the
|
|
16
|
+
* 'connect' hook). Without them, a terminated backend — e.g. a Postgres
|
|
17
|
+
* leader switchover — escalates to an uncaughtException and a full process
|
|
18
|
+
* restart. The broken connection is logged and discarded; pg replaces it on
|
|
19
|
+
* the next acquire. In-flight query failures still reject to their callers.
|
|
7
20
|
*/
|
|
8
|
-
function createPool(databaseUrl) {
|
|
9
|
-
|
|
21
|
+
function createPool(databaseUrl, options) {
|
|
22
|
+
const pool = new pg.Pool({
|
|
23
|
+
connectionString: databaseUrl,
|
|
24
|
+
...options?.config
|
|
25
|
+
});
|
|
26
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
27
|
+
const handle = (err, source) => {
|
|
28
|
+
if (seen.has(err)) return;
|
|
29
|
+
seen.add(err);
|
|
30
|
+
getPoolLogger().warn("Discarded broken pg connection", {
|
|
31
|
+
source,
|
|
32
|
+
error: err.message,
|
|
33
|
+
stack: err.stack
|
|
34
|
+
});
|
|
35
|
+
options?.onError?.(err, source);
|
|
36
|
+
};
|
|
37
|
+
pool.on("error", (err) => handle(err, "idle-pool"));
|
|
38
|
+
pool.on("connect", (client) => {
|
|
39
|
+
client.on("error", (err) => handle(err, "client"));
|
|
40
|
+
});
|
|
41
|
+
return pool;
|
|
10
42
|
}
|
|
11
43
|
/**
|
|
12
44
|
* Create Kysely database instance (PostgreSQL only).
|
package/dist/env/define-env.js
CHANGED
|
@@ -60,9 +60,11 @@ function extractDefault(field) {
|
|
|
60
60
|
const def = t.def;
|
|
61
61
|
if (!def) return void 0;
|
|
62
62
|
if (def.type === "default" && def.defaultValue !== void 0) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const probe1 = def.defaultValue;
|
|
64
|
+
const probe2 = def.defaultValue;
|
|
65
|
+
if (typeof probe1 === "function" || probe1 !== probe2) return "\"<computed>\"";
|
|
66
|
+
if (probe1 === "" || probe1 === void 0) return void 0;
|
|
67
|
+
return JSON.stringify(probe1);
|
|
66
68
|
}
|
|
67
69
|
if (def.in) {
|
|
68
70
|
t = def.in;
|
|
@@ -80,7 +82,7 @@ function isOptional(field) {
|
|
|
80
82
|
for (let i = 0; i < 6; i++) {
|
|
81
83
|
const def = t.def;
|
|
82
84
|
if (!def) return false;
|
|
83
|
-
if (def.type === "optional" || def.type === "default") return true;
|
|
85
|
+
if (def.type === "optional" || def.type === "default" || def.type === "prefault") return true;
|
|
84
86
|
if (def.in) {
|
|
85
87
|
t = def.in;
|
|
86
88
|
continue;
|
|
@@ -93,6 +95,26 @@ function isOptional(field) {
|
|
|
93
95
|
}
|
|
94
96
|
return false;
|
|
95
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Walk through `.optional()` / `.default(...)` / `.prefault(...)` wrappers to
|
|
100
|
+
* find a nested ZodObject's `.shape`. Returns undefined if `field` does not
|
|
101
|
+
* (eventually) wrap a ZodObject. Mirrors the unwrap loop in
|
|
102
|
+
* `extractDefault` / `isOptional` so that a nested object can be wrapped in
|
|
103
|
+
* any of the common compositional modifiers and still be walked for docs.
|
|
104
|
+
*/
|
|
105
|
+
function findInnerShape(field) {
|
|
106
|
+
let t = field;
|
|
107
|
+
for (let i = 0; i < 8; i++) {
|
|
108
|
+
const node = t;
|
|
109
|
+
if (node.shape) return node.shape;
|
|
110
|
+
if (node.def?.shape) return node.def.shape;
|
|
111
|
+
if (node.def?.innerType) {
|
|
112
|
+
t = node.def.innerType;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
96
118
|
function describeFieldRecursive(shape, envMap, fieldPath, descriptions, out) {
|
|
97
119
|
for (const [name, field] of Object.entries(shape)) {
|
|
98
120
|
const path = fieldPath ? `${fieldPath}.${name}` : name;
|
|
@@ -113,8 +135,7 @@ function describeFieldRecursive(shape, envMap, fieldPath, descriptions, out) {
|
|
|
113
135
|
description: explicitDesc ?? zodDesc ?? def2?.description
|
|
114
136
|
});
|
|
115
137
|
} else {
|
|
116
|
-
const
|
|
117
|
-
const innerShape = nested.shape ?? nested.def?.shape;
|
|
138
|
+
const innerShape = findInnerShape(field);
|
|
118
139
|
if (innerShape) describeFieldRecursive(innerShape, mapping, path, descriptions, out);
|
|
119
140
|
}
|
|
120
141
|
}
|
package/dist/env/logger-env.d.ts
CHANGED
|
@@ -20,9 +20,9 @@ export declare const LoggerEnvSchema: z.ZodObject<{
|
|
|
20
20
|
KICI_LOG_MAX_SIZE: z.ZodDefault<z.ZodString>;
|
|
21
21
|
KICI_LOG_RETENTION_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
22
22
|
KICI_LOG_FORMAT: z.ZodDefault<z.ZodEnum<{
|
|
23
|
-
plain: "plain";
|
|
24
23
|
json: "json";
|
|
25
24
|
auto: "auto";
|
|
25
|
+
plain: "plain";
|
|
26
26
|
}>>;
|
|
27
27
|
KICI_CLUSTER_INSTANCE_ID: z.ZodOptional<z.ZodString>;
|
|
28
28
|
KICI_AGENT_ID: z.ZodOptional<z.ZodString>;
|
|
@@ -50,9 +50,21 @@ export interface ShutdownHandle {
|
|
|
50
50
|
/**
|
|
51
51
|
* Wire up SIGTERM / SIGINT (and optionally uncaughtException /
|
|
52
52
|
* unhandledRejection) handlers that execute the provided teardown
|
|
53
|
-
* steps sequentially, then
|
|
53
|
+
* steps sequentially, then exit.
|
|
54
|
+
*
|
|
55
|
+
* Exit codes: signal-triggered and programmatic shutdowns exit 0;
|
|
56
|
+
* shutdowns triggered by uncaughtException / unhandledRejection exit 1 so
|
|
57
|
+
* `Restart=on-failure`-style supervisor policies and exit-code alerting
|
|
58
|
+
* see the fatal cause. Escalation is sticky — a fatal trigger arriving
|
|
59
|
+
* while a clean shutdown is already in progress still raises the final
|
|
60
|
+
* exit code to 1.
|
|
54
61
|
*
|
|
55
62
|
* A force-exit timer ensures the process terminates even if a step hangs.
|
|
63
|
+
* The force-exit honors the same sticky exit code as a clean completion: a
|
|
64
|
+
* slow SIGTERM/SIGINT stop that merely overran the grace period still exits 0,
|
|
65
|
+
* so systemd records an intentional stop rather than marking the unit `failed`
|
|
66
|
+
* (which would break `systemctl restart` recovery). Only a fatal trigger
|
|
67
|
+
* (uncaughtException / unhandledRejection) raises the force-exit code to 1.
|
|
56
68
|
*/
|
|
57
69
|
export declare function setupGracefulShutdown(options: GracefulShutdownOptions): ShutdownHandle;
|
|
58
70
|
//# sourceMappingURL=graceful-shutdown.d.ts.map
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import "./chunk-gOLHoazu.js";
|
|
2
|
-
import { toErrorMessage } from "
|
|
2
|
+
import { toErrorMessage } from "@kici-dev/core";
|
|
3
3
|
//#region src/graceful-shutdown.ts
|
|
4
4
|
/**
|
|
5
5
|
* Wire up SIGTERM / SIGINT (and optionally uncaughtException /
|
|
6
6
|
* unhandledRejection) handlers that execute the provided teardown
|
|
7
|
-
* steps sequentially, then
|
|
7
|
+
* steps sequentially, then exit.
|
|
8
|
+
*
|
|
9
|
+
* Exit codes: signal-triggered and programmatic shutdowns exit 0;
|
|
10
|
+
* shutdowns triggered by uncaughtException / unhandledRejection exit 1 so
|
|
11
|
+
* `Restart=on-failure`-style supervisor policies and exit-code alerting
|
|
12
|
+
* see the fatal cause. Escalation is sticky — a fatal trigger arriving
|
|
13
|
+
* while a clean shutdown is already in progress still raises the final
|
|
14
|
+
* exit code to 1.
|
|
8
15
|
*
|
|
9
16
|
* A force-exit timer ensures the process terminates even if a step hangs.
|
|
17
|
+
* The force-exit honors the same sticky exit code as a clean completion: a
|
|
18
|
+
* slow SIGTERM/SIGINT stop that merely overran the grace period still exits 0,
|
|
19
|
+
* so systemd records an intentional stop rather than marking the unit `failed`
|
|
20
|
+
* (which would break `systemctl restart` recovery). Only a fatal trigger
|
|
21
|
+
* (uncaughtException / unhandledRejection) raises the force-exit code to 1.
|
|
10
22
|
*/
|
|
11
23
|
function setupGracefulShutdown(options) {
|
|
12
24
|
const { logger, steps, timeoutMs = 3e4, onForceExit, skipErrorHandlers = false } = options;
|
|
13
25
|
let isShuttingDown = false;
|
|
14
|
-
|
|
26
|
+
let exitCode = 0;
|
|
27
|
+
async function gracefulShutdown(signal, code = 0) {
|
|
28
|
+
exitCode = Math.max(exitCode, code);
|
|
15
29
|
if (isShuttingDown) {
|
|
16
30
|
logger.warn("Shutdown already in progress, ignoring signal", { signal });
|
|
17
31
|
return;
|
|
@@ -19,8 +33,8 @@ function setupGracefulShutdown(options) {
|
|
|
19
33
|
isShuttingDown = true;
|
|
20
34
|
logger.info(`Received ${signal}, starting graceful shutdown...`);
|
|
21
35
|
const forceExitTimeout = setTimeout(() => {
|
|
22
|
-
logger.error(`Graceful shutdown timed out after ${timeoutMs / 1e3}s, forcing exit
|
|
23
|
-
if (onForceExit?.() !== true) process.exit(
|
|
36
|
+
logger.error(`Graceful shutdown timed out after ${timeoutMs / 1e3}s, forcing exit`, { exitCode });
|
|
37
|
+
if (onForceExit?.() !== true) process.exit(exitCode);
|
|
24
38
|
}, timeoutMs);
|
|
25
39
|
try {
|
|
26
40
|
for (const step of steps) try {
|
|
@@ -34,8 +48,8 @@ function setupGracefulShutdown(options) {
|
|
|
34
48
|
});
|
|
35
49
|
}
|
|
36
50
|
clearTimeout(forceExitTimeout);
|
|
37
|
-
logger.info("Graceful shutdown complete");
|
|
38
|
-
process.exit(
|
|
51
|
+
logger.info("Graceful shutdown complete", { exitCode });
|
|
52
|
+
process.exit(exitCode);
|
|
39
53
|
} catch (error) {
|
|
40
54
|
logger.error("Error during graceful shutdown", {
|
|
41
55
|
error: toErrorMessage(error),
|
|
@@ -53,17 +67,17 @@ function setupGracefulShutdown(options) {
|
|
|
53
67
|
error: error.message,
|
|
54
68
|
stack: error.stack
|
|
55
69
|
});
|
|
56
|
-
gracefulShutdown("uncaughtException");
|
|
70
|
+
gracefulShutdown("uncaughtException", 1);
|
|
57
71
|
});
|
|
58
72
|
process.on("unhandledRejection", (reason) => {
|
|
59
73
|
logger.error("Unhandled rejection", {
|
|
60
74
|
reason: toErrorMessage(reason),
|
|
61
75
|
stack: reason instanceof Error ? reason.stack : void 0
|
|
62
76
|
});
|
|
63
|
-
gracefulShutdown("unhandledRejection");
|
|
77
|
+
gracefulShutdown("unhandledRejection", 1);
|
|
64
78
|
});
|
|
65
79
|
}
|
|
66
|
-
return { shutdown: gracefulShutdown };
|
|
80
|
+
return { shutdown: (signal) => gracefulShutdown(signal) };
|
|
67
81
|
}
|
|
68
82
|
//#endregion
|
|
69
83
|
export { setupGracefulShutdown };
|
package/dist/idempotency.d.ts
CHANGED
|
@@ -1,68 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Idempotent-step primitive: a check / prompt / apply runner that is
|
|
3
|
-
* UI-agnostic and embeddable across the product.
|
|
4
|
-
*
|
|
5
|
-
* The pattern: every destructive operation on shared state (prod infra,
|
|
6
|
-
* npm, git remotes, DNS, TF state, workflow step side effects) is wrapped
|
|
7
|
-
* as an IdempotentStep whose check() returns a typed drift value or null.
|
|
8
|
-
* Null means the system is already in the desired state — the runner
|
|
9
|
-
* silently skips, optionally invoking whenInSync() to surface the
|
|
10
|
-
* already-satisfied resource (e.g. an existing resource id). A non-null
|
|
11
|
-
* drift means apply() would change state — the runner asks the caller's
|
|
12
|
-
* confirm() before invoking apply(), unless yes or dryRun overrides are
|
|
13
|
-
* set. apply() returns the typed result of the change for the caller.
|
|
14
|
-
*
|
|
15
|
-
* The runner has no UI dependency. CLI consumers pass an inquirer-backed
|
|
16
|
-
* confirm; future SDK / agent consumers pass their own policy function.
|
|
17
|
-
* See `.claude/rules/idempotency.md` for the full rule and adopters.
|
|
18
|
-
*/
|
|
19
|
-
export interface IdempotentStep<TDrift, TInSync = void, TApplied = void> {
|
|
20
|
-
/** Human-readable name; appears in logs and the confirm prompt. */
|
|
21
|
-
name: string;
|
|
22
|
-
/** Read-only inspection. Returns drift value if apply() would change
|
|
23
|
-
* state, or null if the system is already in the desired state. */
|
|
24
|
-
check: () => Promise<TDrift | null>;
|
|
25
|
-
/** Multi-line description of what apply() would do, given drift. */
|
|
26
|
-
summarize: (drift: TDrift) => string;
|
|
27
|
-
/** Destructive action that brings the system into the desired state.
|
|
28
|
-
* Its return value is surfaced in StepResult.result on the 'applied'
|
|
29
|
-
* outcome. */
|
|
30
|
-
apply: (drift: TDrift) => Promise<TApplied>;
|
|
31
|
-
/** Optional: runs when check() returns null. Use this to fetch the
|
|
32
|
-
* already-satisfied resource (e.g. read the existing id when a
|
|
33
|
-
* create-if-missing was already done). Return value is surfaced in
|
|
34
|
-
* StepResult.result on the 'skipped' outcome. */
|
|
35
|
-
whenInSync?: () => Promise<TInSync>;
|
|
36
|
-
}
|
|
37
|
-
export type ConfirmFn = (message: string) => Promise<boolean>;
|
|
38
|
-
export interface RunOptions {
|
|
39
|
-
/** Pluggable confirm. Required unless `yes` or `dryRun` is set. */
|
|
40
|
-
confirm?: ConfirmFn;
|
|
41
|
-
/** Breakglass: skip prompts, apply on drift. The CALLER prints any
|
|
42
|
-
* loud "auto-confirm" banner before invoking the runner. */
|
|
43
|
-
yes?: boolean;
|
|
44
|
-
/** Report only; bypass confirm; never call apply(). */
|
|
45
|
-
dryRun?: boolean;
|
|
46
|
-
/** Sink for `name`-prefixed status lines. Defaults to console.log. */
|
|
47
|
-
log?: (line: string) => void;
|
|
48
|
-
}
|
|
49
|
-
export type StepOutcome = 'skipped' | 'applied' | 'declined' | 'dry-run';
|
|
50
|
-
export type StepResult<TDrift, TInSync = void, TApplied = void> = {
|
|
51
|
-
outcome: 'skipped';
|
|
52
|
-
drift: null;
|
|
53
|
-
result: TInSync;
|
|
54
|
-
} | {
|
|
55
|
-
outcome: 'applied';
|
|
56
|
-
drift: TDrift;
|
|
57
|
-
result: TApplied;
|
|
58
|
-
} | {
|
|
59
|
-
outcome: 'declined';
|
|
60
|
-
drift: TDrift;
|
|
61
|
-
result: undefined;
|
|
62
|
-
} | {
|
|
63
|
-
outcome: 'dry-run';
|
|
64
|
-
drift: TDrift;
|
|
65
|
-
result: undefined;
|
|
66
|
-
};
|
|
67
|
-
export declare function runIdempotentStep<TDrift, TInSync = void, TApplied = void>(step: IdempotentStep<TDrift, TInSync, TApplied>, opts?: RunOptions): Promise<StepResult<TDrift, TInSync, TApplied>>;
|
|
1
|
+
export * from '@kici-dev/core/idempotency';
|
|
68
2
|
//# sourceMappingURL=idempotency.d.ts.map
|
package/dist/idempotency.js
CHANGED
|
@@ -1,49 +1,3 @@
|
|
|
1
1
|
import "./chunk-gOLHoazu.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const log = opts.log ?? ((line) => console.log(line));
|
|
5
|
-
const drift = await step.check();
|
|
6
|
-
if (drift === null) {
|
|
7
|
-
log(`✓ ${step.name} — in sync, skipping`);
|
|
8
|
-
return {
|
|
9
|
-
outcome: "skipped",
|
|
10
|
-
drift: null,
|
|
11
|
-
result: step.whenInSync ? await step.whenInSync() : void 0
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
log(`! ${step.name} — drift detected:`);
|
|
15
|
-
for (const line of step.summarize(drift).split("\n")) log(` ${line}`);
|
|
16
|
-
if (opts.dryRun) {
|
|
17
|
-
log(` (dry-run; would apply)`);
|
|
18
|
-
return {
|
|
19
|
-
outcome: "dry-run",
|
|
20
|
-
drift,
|
|
21
|
-
result: void 0
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
let approved;
|
|
25
|
-
if (opts.yes) approved = true;
|
|
26
|
-
else {
|
|
27
|
-
if (!opts.confirm) throw new Error(`runIdempotentStep(${step.name}): drift detected but no confirm callback provided and yes/dryRun not set. Pass opts.confirm, opts.yes, or opts.dryRun.`);
|
|
28
|
-
approved = await opts.confirm(`Apply ${step.name}?`);
|
|
29
|
-
}
|
|
30
|
-
if (!approved) {
|
|
31
|
-
log(` declined; skipping`);
|
|
32
|
-
return {
|
|
33
|
-
outcome: "declined",
|
|
34
|
-
drift,
|
|
35
|
-
result: void 0
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
const appliedResult = await step.apply(drift);
|
|
39
|
-
log(`✓ ${step.name} — applied`);
|
|
40
|
-
return {
|
|
41
|
-
outcome: "applied",
|
|
42
|
-
drift,
|
|
43
|
-
result: appliedResult
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
|
-
export { runIdempotentStep };
|
|
48
|
-
|
|
49
|
-
//# sourceMappingURL=idempotency.js.map
|
|
2
|
+
export * from "@kici-dev/core/idempotency";
|
|
3
|
+
export {};
|