@go-to-k/cdkd 0.219.8 → 0.220.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/cli.js +709 -36
- package/dist/cli.js.map +1 -1
- package/dist/{deploy-engine-Bl8wyosc.js → deploy-engine-CFaK71E9.js} +193 -8
- package/dist/deploy-engine-CFaK71E9.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/deploy-engine-Bl8wyosc.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { A as withErrorHandling, B as generateResourceNameWithFallback, C as StackHasActiveImportsError, F as getLiveRenderer, H as withStackName, I as PATTERN_B_NAME_PROPERTIES, L as PATTERN_B_RESOURCE_TYPES, M as getLogger, P as runStackBuffered, S as ResourceUpdateNotSupportedError, V as withSkipPrefix, _ as MissingCdkCliError, a as assertRegionMatch, b as ProvisioningError, f as LocalInvokeBuildError$1, i as resolveExplicitPhysicalId, k as normalizeAwsError, l as CdkdError, m as LocalStartServiceError, n as matchesCdkPath, o as disableInstanceApiTermination, p as LocalMigrateError, r as normalizeAwsTagsToCfn, s as isTerminationProtectionPropagationError, t as CDK_PATH_TAG, v as NestedStackChildDirectDestroyError, w as StackTerminationProtectionError, x as ResourceTimeoutError, y as PartialFailureError, z as generateResourceName } from "./import-helpers-wLipXr5g.js";
|
|
3
3
|
import { a as setAwsClients, i as resetAwsClients, r as getAwsClients, t as AwsClients } from "./aws-clients-pjPwZz1r.js";
|
|
4
|
-
import { A as
|
|
4
|
+
import { $ as resolveBucketRegion, A as stringifyValue, B as resolveApp, C as DiffCalculator, D as S3StateBackend, E as LockManager, F as runDockerForeground, G as warnDeprecatedNoPrefixCliFlag, H as resolveSkipPrefix, I as runDockerStreaming, J as MIGRATE_TMP_PREFIX, K as CFN_TEMPLATE_BODY_LIMIT, L as Synthesizer, M as buildDockerImage, N as formatDockerLoginError, O as shouldRetainResource, P as getDockerCmd, R as getDefaultStateBucketName, S as applyRoleArnIfSet, T as TemplateParser, U as resolveStateBucketWithDefault, V as resolveCaptureObservedState, W as resolveStateBucketWithDefaultAndSource, X as uploadCfnTemplate, Y as findLargeInlineResources, Z as AssemblyReader, _ as collectInlinePolicyNamesManagedBySiblings, a as withRetry, b as CloudControlProvider, c as extractDeploymentEventError, d as cyan, f as gray, g as IAMRoleProvider, h as yellow, i as withResourceDeadline, j as WorkGraph, k as AssetPublisher, l as formatResourceLine, m as red, n as DEFAULT_RESOURCE_WARN_AFTER_MS, o as isRetryableTransientError, p as green, q as CFN_TEMPLATE_URL_LIMIT, r as DeployEngine, s as IMPLICIT_DELETE_DEPENDENCIES, t as DEFAULT_RESOURCE_TIMEOUT_MS, u as bold, v as ProviderRegistry, w as DagBuilder, x as IntrinsicFunctionResolver, y as findActionableSilentDrops, z as getLegacyStateBucketName } from "./deploy-engine-CFaK71E9.js";
|
|
5
5
|
import { t as ASGProvider } from "./asg-provider-B_hrCxRx.js";
|
|
6
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
7
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
@@ -1507,6 +1507,387 @@ async function promptRecreateConfirm(input) {
|
|
|
1507
1507
|
}
|
|
1508
1508
|
}
|
|
1509
1509
|
|
|
1510
|
+
//#endregion
|
|
1511
|
+
//#region src/state/deployment-events-store.ts
|
|
1512
|
+
/**
|
|
1513
|
+
* Deployment-events store (issue #808) — persists the structured
|
|
1514
|
+
* deployment events emitted by the deploy engine / destroy runner as
|
|
1515
|
+
* JSONL to the state bucket, plus a small per-stack run index:
|
|
1516
|
+
*
|
|
1517
|
+
* - Events: `{prefix}/{stackName}/{region}/deployments/{runId}.jsonl`
|
|
1518
|
+
* - Index: `{prefix}/{stackName}/{region}/deployments/index.json`
|
|
1519
|
+
*
|
|
1520
|
+
* Design constraints (all load-bearing):
|
|
1521
|
+
*
|
|
1522
|
+
* - **Best-effort, never blocking**: `record()` is synchronous and only
|
|
1523
|
+
* buffers in memory; flushes run asynchronously (debounced timer +
|
|
1524
|
+
* size threshold) and are serialized on a write chain. A failed S3
|
|
1525
|
+
* write warns once and degrades to debug-level afterwards — it can
|
|
1526
|
+
* NEVER fail or block the deploy/destroy itself.
|
|
1527
|
+
* - **No locking**: each run writes to its own unique `{runId}.jsonl`
|
|
1528
|
+
* key (no concurrent writer by construction). `index.json` is
|
|
1529
|
+
* last-writer-wins — acceptable for a derived view; the `.jsonl`
|
|
1530
|
+
* files are the source of truth and `cdkd events` can read a run
|
|
1531
|
+
* directly by id even if the index lost the race.
|
|
1532
|
+
* - **No resource properties** in events — errors + metadata only
|
|
1533
|
+
* (properties may contain secrets and already live in state.json).
|
|
1534
|
+
* - **Separate keys from state.json** — no state schema bump; event
|
|
1535
|
+
* files survive `cdkd destroy` (state deletion does not touch
|
|
1536
|
+
* `deployments/`), preserving post-mortem context.
|
|
1537
|
+
*/
|
|
1538
|
+
/** Max runs retained in `deployments/index.json` (newest first). */
|
|
1539
|
+
const DEPLOYMENT_EVENTS_MAX_INDEX_RUNS = 20;
|
|
1540
|
+
/** Debounce window between buffered events and the async S3 flush. */
|
|
1541
|
+
const FLUSH_INTERVAL_MS = 2e3;
|
|
1542
|
+
/** Flush immediately once this many events are buffered. */
|
|
1543
|
+
const FLUSH_EVENT_THRESHOLD = 50;
|
|
1544
|
+
/** Build-time cdkd version, with a dev fallback for non-built contexts. */
|
|
1545
|
+
function getCdkdVersion() {
|
|
1546
|
+
return "0.220.0";
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Generate a time-sortable unique run id, e.g.
|
|
1550
|
+
* `20260613T012345678Z-1a2b3c4d`. The timestamp prefix keeps S3 listings
|
|
1551
|
+
* and `cdkd events` output chronologically meaningful; the random suffix
|
|
1552
|
+
* guarantees uniqueness across concurrent runs.
|
|
1553
|
+
*/
|
|
1554
|
+
function newDeploymentRunId(now = /* @__PURE__ */ new Date()) {
|
|
1555
|
+
return `${now.toISOString().replace(/[-:.]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
1556
|
+
}
|
|
1557
|
+
/** S3 key of a run's JSONL event stream. */
|
|
1558
|
+
function deploymentEventsKey(prefix, stackName, region, runId) {
|
|
1559
|
+
return `${prefix}/${stackName}/${region}/deployments/${runId}.jsonl`;
|
|
1560
|
+
}
|
|
1561
|
+
/** S3 key of a stack's run index. */
|
|
1562
|
+
function deploymentEventsIndexKey(prefix, stackName, region) {
|
|
1563
|
+
return `${prefix}/${stackName}/${region}/deployments/index.json`;
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Buffering JSONL writer for one deployment run. Implements the
|
|
1567
|
+
* {@link DeploymentEventRecorder} seam the deploy engine / destroy
|
|
1568
|
+
* runner emit through.
|
|
1569
|
+
*/
|
|
1570
|
+
var DeploymentEventsStore = class {
|
|
1571
|
+
logger = getLogger().child("DeploymentEvents");
|
|
1572
|
+
backend;
|
|
1573
|
+
stackName;
|
|
1574
|
+
region;
|
|
1575
|
+
command;
|
|
1576
|
+
runId;
|
|
1577
|
+
cdkdVersion;
|
|
1578
|
+
startedAt;
|
|
1579
|
+
/** All events recorded so far (the full JSONL body is re-PUT per flush —
|
|
1580
|
+
* S3 has no append, and metadata-only events are small). */
|
|
1581
|
+
events = [];
|
|
1582
|
+
/** Number of events already persisted by the last successful flush. */
|
|
1583
|
+
persistedCount = 0;
|
|
1584
|
+
flushTimer;
|
|
1585
|
+
/** Serializes S3 writes so flushes never interleave. */
|
|
1586
|
+
writeChain = Promise.resolve();
|
|
1587
|
+
warnedOnce = false;
|
|
1588
|
+
finalized = false;
|
|
1589
|
+
constructor(backend, options) {
|
|
1590
|
+
this.backend = backend;
|
|
1591
|
+
this.stackName = options.stackName;
|
|
1592
|
+
this.region = options.region;
|
|
1593
|
+
this.command = options.command;
|
|
1594
|
+
this.runId = options.runId ?? newDeploymentRunId();
|
|
1595
|
+
this.cdkdVersion = options.cdkdVersion ?? getCdkdVersion();
|
|
1596
|
+
this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Buffer one event (synchronous, never throws). The timestamp is
|
|
1600
|
+
* stamped here so emitters don't need to.
|
|
1601
|
+
*/
|
|
1602
|
+
record(event) {
|
|
1603
|
+
try {
|
|
1604
|
+
if (this.finalized) return;
|
|
1605
|
+
this.events.push({
|
|
1606
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1607
|
+
...event
|
|
1608
|
+
});
|
|
1609
|
+
if (this.events.length - this.persistedCount >= FLUSH_EVENT_THRESHOLD) this.scheduleFlush(0);
|
|
1610
|
+
else this.scheduleFlush(FLUSH_INTERVAL_MS);
|
|
1611
|
+
} catch {}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Final flush + index update. Called by the owner (deploy CLI per-stack
|
|
1615
|
+
* finally / destroy runner finally) after the run reaches a terminal
|
|
1616
|
+
* state. Best-effort: never throws.
|
|
1617
|
+
*/
|
|
1618
|
+
async finalize(result) {
|
|
1619
|
+
if (this.finalized) return;
|
|
1620
|
+
this.finalized = true;
|
|
1621
|
+
if (this.flushTimer) {
|
|
1622
|
+
clearTimeout(this.flushTimer);
|
|
1623
|
+
this.flushTimer = void 0;
|
|
1624
|
+
}
|
|
1625
|
+
if (this.events.length === 0) return;
|
|
1626
|
+
await this.enqueueWrite(async () => {
|
|
1627
|
+
await this.doFlush();
|
|
1628
|
+
await this.updateIndex(result);
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
/** Await any in-flight async flushes (used by tests). */
|
|
1632
|
+
async drain() {
|
|
1633
|
+
await this.writeChain;
|
|
1634
|
+
}
|
|
1635
|
+
scheduleFlush(delayMs) {
|
|
1636
|
+
if (this.flushTimer) {
|
|
1637
|
+
if (delayMs > 0) return;
|
|
1638
|
+
clearTimeout(this.flushTimer);
|
|
1639
|
+
}
|
|
1640
|
+
this.flushTimer = setTimeout(() => {
|
|
1641
|
+
this.flushTimer = void 0;
|
|
1642
|
+
this.enqueueWrite(() => this.doFlush());
|
|
1643
|
+
}, delayMs);
|
|
1644
|
+
this.flushTimer.unref?.();
|
|
1645
|
+
}
|
|
1646
|
+
enqueueWrite(op) {
|
|
1647
|
+
const next = this.writeChain.then(op).catch((err) => {
|
|
1648
|
+
this.warnOnce(`Failed to persist deployment events for run ${this.runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1649
|
+
});
|
|
1650
|
+
this.writeChain = next;
|
|
1651
|
+
return next;
|
|
1652
|
+
}
|
|
1653
|
+
async doFlush() {
|
|
1654
|
+
if (this.events.length === 0 || this.events.length === this.persistedCount) return;
|
|
1655
|
+
const snapshotCount = this.events.length;
|
|
1656
|
+
const body = this.events.slice(0, snapshotCount).map((e) => JSON.stringify(e)).join("\n");
|
|
1657
|
+
await this.backend.putRawObject(deploymentEventsKey(this.backend.prefix, this.stackName, this.region, this.runId), body + "\n", "application/x-ndjson");
|
|
1658
|
+
this.persistedCount = snapshotCount;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Prepend this run's summary to `deployments/index.json`, truncated to
|
|
1662
|
+
* the last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs. Read-modify-
|
|
1663
|
+
* write WITHOUT optimistic locking — last-writer-wins (documented
|
|
1664
|
+
* trade-off; the per-run `.jsonl` files are the source of truth).
|
|
1665
|
+
*/
|
|
1666
|
+
async updateIndex(result) {
|
|
1667
|
+
const key = deploymentEventsIndexKey(this.backend.prefix, this.stackName, this.region);
|
|
1668
|
+
let existingRuns = [];
|
|
1669
|
+
try {
|
|
1670
|
+
const raw = await this.backend.getRawObject(key);
|
|
1671
|
+
if (raw !== null) {
|
|
1672
|
+
const parsed = JSON.parse(raw);
|
|
1673
|
+
if (Array.isArray(parsed.runs)) existingRuns = parsed.runs;
|
|
1674
|
+
}
|
|
1675
|
+
} catch (err) {
|
|
1676
|
+
this.logger.debug(`Deployment-events index unreadable, rewriting: ${err instanceof Error ? err.message : String(err)}`);
|
|
1677
|
+
}
|
|
1678
|
+
const runs = [{
|
|
1679
|
+
runId: this.runId,
|
|
1680
|
+
command: this.command,
|
|
1681
|
+
cdkdVersion: this.cdkdVersion,
|
|
1682
|
+
startedAt: this.startedAt,
|
|
1683
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1684
|
+
result,
|
|
1685
|
+
eventCount: this.persistedCount
|
|
1686
|
+
}, ...existingRuns.filter((r) => r.runId !== this.runId)].slice(0, 20);
|
|
1687
|
+
const file = {
|
|
1688
|
+
indexVersion: 1,
|
|
1689
|
+
stackName: this.stackName,
|
|
1690
|
+
region: this.region,
|
|
1691
|
+
runs,
|
|
1692
|
+
lastModified: Date.now()
|
|
1693
|
+
};
|
|
1694
|
+
await this.backend.putRawObject(key, JSON.stringify(file, null, 2));
|
|
1695
|
+
}
|
|
1696
|
+
warnOnce(message) {
|
|
1697
|
+
if (this.warnedOnce) {
|
|
1698
|
+
this.logger.debug(message);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
this.warnedOnce = true;
|
|
1702
|
+
this.logger.warn(`${message} — continuing; deployment events are best-effort and never block the run.`);
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
/**
|
|
1706
|
+
* Read side for `cdkd events`: discovers regions / runs / event streams
|
|
1707
|
+
* under `{prefix}/{stackName}/{region}/deployments/`. Region discovery
|
|
1708
|
+
* deliberately does NOT rely on state.json — event files survive
|
|
1709
|
+
* `cdkd destroy`, so a destroyed stack's runs stay readable.
|
|
1710
|
+
*/
|
|
1711
|
+
var DeploymentEventsReader = class {
|
|
1712
|
+
backend;
|
|
1713
|
+
constructor(backend) {
|
|
1714
|
+
this.backend = backend;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Regions that have a `deployments/` index or event stream for the
|
|
1718
|
+
* stack. Derived from the raw key listing under `{prefix}/{stackName}/`.
|
|
1719
|
+
*/
|
|
1720
|
+
async listRegions(stackName) {
|
|
1721
|
+
const prefix = `${this.backend.prefix}/${stackName}/`;
|
|
1722
|
+
const keys = await this.backend.listRawKeys(prefix);
|
|
1723
|
+
const regions = /* @__PURE__ */ new Set();
|
|
1724
|
+
for (const key of keys) {
|
|
1725
|
+
const segments = key.slice(prefix.length).split("/");
|
|
1726
|
+
if (segments.length === 3 && segments[1] === "deployments" && segments[0]) regions.add(segments[0]);
|
|
1727
|
+
}
|
|
1728
|
+
return [...regions].sort();
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Run summaries for `(stackName, region)`, newest first. Returns the
|
|
1732
|
+
* index file's `runs` (already newest-first); when the index is missing
|
|
1733
|
+
* or unreadable, falls back to enumerating `{runId}.jsonl` keys (sorted
|
|
1734
|
+
* descending — runIds are time-prefixed).
|
|
1735
|
+
*
|
|
1736
|
+
* In the fallback path the run's terminal result / command / version are
|
|
1737
|
+
* NOT known from the key alone, so each run's JSONL is read and its last
|
|
1738
|
+
* `RUN_FINISHED` (+ first `RUN_STARTED`) event mined for the true result,
|
|
1739
|
+
* command, version, and timestamps. A run whose JSONL has no terminal
|
|
1740
|
+
* `RUN_FINISHED` (interrupted run, or one whose index write lost the
|
|
1741
|
+
* race) is reported as `'UNKNOWN'` — it is NEVER fabricated as `'FAILED'`,
|
|
1742
|
+
* which would mislabel a successful run that merely failed to update the
|
|
1743
|
+
* derived index.
|
|
1744
|
+
*/
|
|
1745
|
+
async listRuns(stackName, region) {
|
|
1746
|
+
const key = deploymentEventsIndexKey(this.backend.prefix, stackName, region);
|
|
1747
|
+
try {
|
|
1748
|
+
const raw = await this.backend.getRawObject(key);
|
|
1749
|
+
if (raw !== null) {
|
|
1750
|
+
const parsed = JSON.parse(raw);
|
|
1751
|
+
if (Array.isArray(parsed.runs)) return parsed.runs;
|
|
1752
|
+
}
|
|
1753
|
+
} catch {}
|
|
1754
|
+
const dirPrefix = `${this.backend.prefix}/${stackName}/${region}/deployments/`;
|
|
1755
|
+
const runIds = (await this.backend.listRawKeys(dirPrefix)).filter((k) => k.endsWith(".jsonl")).map((k) => k.slice(dirPrefix.length, -6)).sort().reverse();
|
|
1756
|
+
return Promise.all(runIds.map((runId) => this.summarizeRunFromJsonl(stackName, region, runId)));
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Reconstruct a {@link DeploymentRunSummary} for the index-fallback path
|
|
1760
|
+
* by reading the run's JSONL. Derives the true result from the last
|
|
1761
|
+
* `RUN_FINISHED` event; absent that, returns `'UNKNOWN'` rather than
|
|
1762
|
+
* fabricating a definitive failure.
|
|
1763
|
+
*/
|
|
1764
|
+
async summarizeRunFromJsonl(stackName, region, runId) {
|
|
1765
|
+
const fallback = {
|
|
1766
|
+
runId,
|
|
1767
|
+
command: "deploy",
|
|
1768
|
+
cdkdVersion: "unknown",
|
|
1769
|
+
startedAt: "",
|
|
1770
|
+
finishedAt: "",
|
|
1771
|
+
result: "UNKNOWN",
|
|
1772
|
+
eventCount: 0
|
|
1773
|
+
};
|
|
1774
|
+
let events = null;
|
|
1775
|
+
try {
|
|
1776
|
+
events = await this.readRunEvents(stackName, region, runId);
|
|
1777
|
+
} catch {
|
|
1778
|
+
return fallback;
|
|
1779
|
+
}
|
|
1780
|
+
if (events === null || events.length === 0) return fallback;
|
|
1781
|
+
const started = events.find((e) => e.eventType === "RUN_STARTED");
|
|
1782
|
+
let finished;
|
|
1783
|
+
for (const e of events) if (e.eventType === "RUN_FINISHED") finished = e;
|
|
1784
|
+
return {
|
|
1785
|
+
runId,
|
|
1786
|
+
command: started?.command ?? fallback.command,
|
|
1787
|
+
cdkdVersion: started?.cdkdVersion ?? fallback.cdkdVersion,
|
|
1788
|
+
startedAt: started?.timestamp ?? fallback.startedAt,
|
|
1789
|
+
finishedAt: finished?.timestamp ?? fallback.finishedAt,
|
|
1790
|
+
result: finished?.result ?? "UNKNOWN",
|
|
1791
|
+
eventCount: events.length
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Parse one run's JSONL event stream. Returns `null` when the run file
|
|
1796
|
+
* does not exist. Malformed lines are skipped (a torn final line from
|
|
1797
|
+
* an interrupted flush must not hide the rest of the stream).
|
|
1798
|
+
*/
|
|
1799
|
+
async readRunEvents(stackName, region, runId) {
|
|
1800
|
+
const key = deploymentEventsKey(this.backend.prefix, stackName, region, runId);
|
|
1801
|
+
const raw = await this.backend.getRawObject(key);
|
|
1802
|
+
if (raw === null) return null;
|
|
1803
|
+
const events = [];
|
|
1804
|
+
for (const line of raw.split("\n")) {
|
|
1805
|
+
const trimmed = line.trim();
|
|
1806
|
+
if (!trimmed) continue;
|
|
1807
|
+
try {
|
|
1808
|
+
events.push(JSON.parse(trimmed));
|
|
1809
|
+
} catch {}
|
|
1810
|
+
}
|
|
1811
|
+
return events;
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
|
|
1815
|
+
//#endregion
|
|
1816
|
+
//#region src/cli/commands/deployment-events-run.ts
|
|
1817
|
+
/**
|
|
1818
|
+
* Run-level deployment-event bracket helpers (issue #808).
|
|
1819
|
+
*
|
|
1820
|
+
* The per-resource + rollback events are emitted by `DeployEngine` /
|
|
1821
|
+
* `destroy-runner.ts`; the RUN-level events (`RUN_STARTED` /
|
|
1822
|
+
* `RUN_FINISHED`) and the recorder lifecycle (`finalize()` in a `finally`)
|
|
1823
|
+
* are owned by the `cdkd deploy` / `cdkd destroy` CLIs. That run-level
|
|
1824
|
+
* bracket was originally inline in `deploy.ts` / `destroy.ts` and could
|
|
1825
|
+
* only be exercised through the full synth → STS → work-graph pipeline.
|
|
1826
|
+
*
|
|
1827
|
+
* These thin helpers extract the bracket so it is directly unit-testable
|
|
1828
|
+
* (and shared between the two commands), with the exact contract:
|
|
1829
|
+
*
|
|
1830
|
+
* - **`--dry-run` creates NO recorder** (no events at all for a dry run).
|
|
1831
|
+
* - `RUN_STARTED` is emitted the moment the recorder is created.
|
|
1832
|
+
* - A successful run emits `RUN_FINISHED { result: 'SUCCEEDED', counts }`.
|
|
1833
|
+
* - A failed run emits `RUN_FINISHED { result: 'FAILED', error }` via
|
|
1834
|
+
* {@link extractDeploymentEventError} (error metadata only — no props).
|
|
1835
|
+
* - `finalize(result)` is always called (caller's `finally`).
|
|
1836
|
+
*
|
|
1837
|
+
* All of this is best-effort: the recorder's `record()` / `finalize()`
|
|
1838
|
+
* never throw, so these helpers never need their own try/catch.
|
|
1839
|
+
*/
|
|
1840
|
+
/**
|
|
1841
|
+
* Create a {@link DeploymentEventsStore} and immediately emit its
|
|
1842
|
+
* `RUN_STARTED` event. Returns `undefined` under `--dry-run` (no recorder,
|
|
1843
|
+
* no events). The caller wires the returned recorder into the engine /
|
|
1844
|
+
* runner and `finalize()`s it in a `finally`.
|
|
1845
|
+
*/
|
|
1846
|
+
function startRunRecorder(args) {
|
|
1847
|
+
if (args.dryRun) return void 0;
|
|
1848
|
+
const recorder = new DeploymentEventsStore(args.backend, {
|
|
1849
|
+
stackName: args.stackName,
|
|
1850
|
+
region: args.region,
|
|
1851
|
+
command: args.command,
|
|
1852
|
+
...args.runId !== void 0 && { runId: args.runId },
|
|
1853
|
+
...args.cdkdVersion !== void 0 && { cdkdVersion: args.cdkdVersion }
|
|
1854
|
+
});
|
|
1855
|
+
recorder.record({
|
|
1856
|
+
eventType: "RUN_STARTED",
|
|
1857
|
+
stackName: args.stackName,
|
|
1858
|
+
command: args.command,
|
|
1859
|
+
region: args.region,
|
|
1860
|
+
cdkdVersion: recorder.cdkdVersion
|
|
1861
|
+
});
|
|
1862
|
+
return recorder;
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Emit a success `RUN_FINISHED`. No-op when `recorder` is `undefined`
|
|
1866
|
+
* (dry-run / older state). `durationMs` is optional (destroy does not
|
|
1867
|
+
* carry one at the run level).
|
|
1868
|
+
*/
|
|
1869
|
+
function recordRunSucceeded(recorder, stackName, counts, durationMs) {
|
|
1870
|
+
recorder?.record({
|
|
1871
|
+
eventType: "RUN_FINISHED",
|
|
1872
|
+
stackName,
|
|
1873
|
+
result: "SUCCEEDED",
|
|
1874
|
+
...durationMs !== void 0 && { durationMs },
|
|
1875
|
+
counts
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Emit a failure `RUN_FINISHED` carrying the extracted error metadata.
|
|
1880
|
+
* No-op when `recorder` is `undefined`.
|
|
1881
|
+
*/
|
|
1882
|
+
function recordRunFailed(recorder, stackName, error) {
|
|
1883
|
+
recorder?.record({
|
|
1884
|
+
eventType: "RUN_FINISHED",
|
|
1885
|
+
stackName,
|
|
1886
|
+
result: "FAILED",
|
|
1887
|
+
error: extractDeploymentEventError(error)
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1510
1891
|
//#endregion
|
|
1511
1892
|
//#region src/state/export-index-store.ts
|
|
1512
1893
|
/**
|
|
@@ -33762,8 +34143,20 @@ function countProtectedResources(state) {
|
|
|
33762
34143
|
* - Switches `process.env.AWS_REGION` for the duration of the destroy when
|
|
33763
34144
|
* the stack's recorded region differs from `baseRegion`. Restored in the
|
|
33764
34145
|
* `finally` block.
|
|
34146
|
+
* - Persists state incrementally during the delete loop (issue #804):
|
|
34147
|
+
* each successfully deleted resource is removed from the state object
|
|
34148
|
+
* and the trimmed state is written back to S3, mirroring deploy's
|
|
34149
|
+
* `saveStateAfterResource`. An interrupted destroy therefore leaves a
|
|
34150
|
+
* state file listing only resources that still exist, so a re-run does
|
|
34151
|
+
* not replay deletes against already-deleted resources. Persist
|
|
34152
|
+
* failures are logged and never fail the destroy. Every persisted
|
|
34153
|
+
* destroy snapshot CLEARS `outputs` (and drops `imports` / `outputReads`)
|
|
34154
|
+
* so a partial-destroy state never advertises an export / import whose
|
|
34155
|
+
* backing resource is gone — the in-memory `state` the strong-ref check
|
|
34156
|
+
* above reads is untouched.
|
|
33765
34157
|
* - On full success, deletes the state file. On any failure, the state
|
|
33766
|
-
* file is preserved
|
|
34158
|
+
* file is preserved (trimmed to the remaining resources, outputs/imports
|
|
34159
|
+
* cleared) so the user can retry.
|
|
33767
34160
|
*/
|
|
33768
34161
|
async function runDestroyForStack(stackName, state, ctx) {
|
|
33769
34162
|
const logger = getLogger();
|
|
@@ -33843,6 +34236,27 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
33843
34236
|
throw new StackHasActiveImportsError(stackName, regionForState, consumers);
|
|
33844
34237
|
}
|
|
33845
34238
|
}
|
|
34239
|
+
const remainingResources = { ...state.resources };
|
|
34240
|
+
const buildDestroySnapshot = () => {
|
|
34241
|
+
const { imports: _imports, outputReads: _outputReads, ...rest } = state;
|
|
34242
|
+
return {
|
|
34243
|
+
...rest,
|
|
34244
|
+
resources: { ...remainingResources },
|
|
34245
|
+
outputs: {},
|
|
34246
|
+
lastModified: Date.now()
|
|
34247
|
+
};
|
|
34248
|
+
};
|
|
34249
|
+
let saveChain = Promise.resolve();
|
|
34250
|
+
const persistStateAfterDelete = (logicalId) => {
|
|
34251
|
+
saveChain = saveChain.then(async () => {
|
|
34252
|
+
try {
|
|
34253
|
+
await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
|
|
34254
|
+
logger.debug(`State persisted after deleting ${logicalId}`);
|
|
34255
|
+
} catch (error) {
|
|
34256
|
+
logger.warn(`Failed to persist state after deleting ${logicalId} (continuing): ${error instanceof Error ? error.message : String(error)}`);
|
|
34257
|
+
}
|
|
34258
|
+
});
|
|
34259
|
+
};
|
|
33846
34260
|
const renderer = getLiveRenderer();
|
|
33847
34261
|
renderer.start();
|
|
33848
34262
|
try {
|
|
@@ -33899,10 +34313,27 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
33899
34313
|
if (shouldRetainResource(resource.deletionPolicy)) {
|
|
33900
34314
|
logger.info(` ⊘ ${logicalId} (${resource.resourceType}) retained — DeletionPolicy: ${resource.deletionPolicy}`);
|
|
33901
34315
|
result.retainedCount++;
|
|
34316
|
+
ctx.eventRecorder?.record({
|
|
34317
|
+
eventType: "RESOURCE_RETAINED",
|
|
34318
|
+
stackName,
|
|
34319
|
+
operation: "DELETE",
|
|
34320
|
+
logicalId,
|
|
34321
|
+
resourceType: resource.resourceType,
|
|
34322
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy }
|
|
34323
|
+
});
|
|
33902
34324
|
return;
|
|
33903
34325
|
}
|
|
33904
34326
|
const baseLabel = `Deleting ${logicalId} (${resource.resourceType})`;
|
|
33905
34327
|
renderer.addTask(logicalId, baseLabel);
|
|
34328
|
+
const resourceStartedAt = Date.now();
|
|
34329
|
+
ctx.eventRecorder?.record({
|
|
34330
|
+
eventType: "RESOURCE_STARTED",
|
|
34331
|
+
stackName,
|
|
34332
|
+
operation: "DELETE",
|
|
34333
|
+
logicalId,
|
|
34334
|
+
resourceType: resource.resourceType,
|
|
34335
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy }
|
|
34336
|
+
});
|
|
33906
34337
|
try {
|
|
33907
34338
|
const provider = destroyProviderRegistry.getProviderFor({
|
|
33908
34339
|
resourceType: resource.resourceType,
|
|
@@ -33946,19 +34377,63 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
33946
34377
|
renderer.removeTask(logicalId);
|
|
33947
34378
|
logger.info(` ${formatResourceLine("deleted", logicalId, resource.resourceType)}`);
|
|
33948
34379
|
result.deletedCount++;
|
|
34380
|
+
ctx.eventRecorder?.record({
|
|
34381
|
+
eventType: "RESOURCE_SUCCEEDED",
|
|
34382
|
+
stackName,
|
|
34383
|
+
operation: "DELETE",
|
|
34384
|
+
logicalId,
|
|
34385
|
+
resourceType: resource.resourceType,
|
|
34386
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
|
|
34387
|
+
...resource.physicalId && { physicalId: resource.physicalId },
|
|
34388
|
+
durationMs: Date.now() - resourceStartedAt
|
|
34389
|
+
});
|
|
34390
|
+
delete remainingResources[logicalId];
|
|
34391
|
+
persistStateAfterDelete(logicalId);
|
|
33949
34392
|
} catch (error) {
|
|
33950
34393
|
renderer.removeTask(logicalId);
|
|
33951
34394
|
const msg = error instanceof Error ? error.message : String(error);
|
|
33952
34395
|
if (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException")) {
|
|
33953
34396
|
logger.debug(` ${logicalId} already deleted, removing from state`);
|
|
33954
34397
|
result.deletedCount++;
|
|
34398
|
+
ctx.eventRecorder?.record({
|
|
34399
|
+
eventType: "RESOURCE_SUCCEEDED",
|
|
34400
|
+
stackName,
|
|
34401
|
+
operation: "DELETE",
|
|
34402
|
+
logicalId,
|
|
34403
|
+
resourceType: resource.resourceType,
|
|
34404
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
|
|
34405
|
+
...resource.physicalId && { physicalId: resource.physicalId },
|
|
34406
|
+
durationMs: Date.now() - resourceStartedAt
|
|
34407
|
+
});
|
|
34408
|
+
delete remainingResources[logicalId];
|
|
34409
|
+
persistStateAfterDelete(logicalId);
|
|
33955
34410
|
} else if (error instanceof ResourceTimeoutError) {
|
|
33956
34411
|
const wrapped = new ProvisioningError(error.message, resource.resourceType, logicalId, resource.physicalId, error);
|
|
33957
34412
|
logger.error(` ✗ Failed to delete ${logicalId}:`, wrapped.message);
|
|
33958
34413
|
result.errorCount++;
|
|
34414
|
+
ctx.eventRecorder?.record({
|
|
34415
|
+
eventType: "RESOURCE_FAILED",
|
|
34416
|
+
stackName,
|
|
34417
|
+
operation: "DELETE",
|
|
34418
|
+
logicalId,
|
|
34419
|
+
resourceType: resource.resourceType,
|
|
34420
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
|
|
34421
|
+
durationMs: Date.now() - resourceStartedAt,
|
|
34422
|
+
error: extractDeploymentEventError(wrapped)
|
|
34423
|
+
});
|
|
33959
34424
|
} else {
|
|
33960
34425
|
logger.error(` ✗ Failed to delete ${logicalId}:`, String(error));
|
|
33961
34426
|
result.errorCount++;
|
|
34427
|
+
ctx.eventRecorder?.record({
|
|
34428
|
+
eventType: "RESOURCE_FAILED",
|
|
34429
|
+
stackName,
|
|
34430
|
+
operation: "DELETE",
|
|
34431
|
+
logicalId,
|
|
34432
|
+
resourceType: resource.resourceType,
|
|
34433
|
+
...resource.provisionedBy && { provisionedBy: resource.provisionedBy },
|
|
34434
|
+
durationMs: Date.now() - resourceStartedAt,
|
|
34435
|
+
error: extractDeploymentEventError(error)
|
|
34436
|
+
});
|
|
33962
34437
|
}
|
|
33963
34438
|
} finally {
|
|
33964
34439
|
renderer.removeTask(logicalId);
|
|
@@ -33966,16 +34441,25 @@ async function runDestroyForStack(stackName, state, ctx) {
|
|
|
33966
34441
|
});
|
|
33967
34442
|
await Promise.all(deletePromises);
|
|
33968
34443
|
}
|
|
34444
|
+
await saveChain;
|
|
33969
34445
|
if (result.errorCount === 0) {
|
|
33970
34446
|
await ctx.stateBackend.deleteState(stackName, regionForState);
|
|
33971
34447
|
logger.debug("State deleted");
|
|
33972
34448
|
if (ctx.exportIndexStore) await ctx.exportIndexStore.removeStack(stackName, regionForState);
|
|
33973
|
-
} else
|
|
34449
|
+
} else {
|
|
34450
|
+
try {
|
|
34451
|
+
await ctx.stateBackend.saveState(stackName, regionForState, buildDestroySnapshot());
|
|
34452
|
+
} catch (error) {
|
|
34453
|
+
logger.warn(`Failed to persist remaining state after partial destroy: ${error instanceof Error ? error.message : String(error)}. The state file may still list already-deleted resources; a re-run resolves them idempotently.`);
|
|
34454
|
+
}
|
|
34455
|
+
logger.warn(`${result.errorCount} resource(s) failed to delete. State preserved.`);
|
|
34456
|
+
}
|
|
33974
34457
|
const retainedSuffix = result.retainedCount > 0 ? `, ${result.retainedCount} retained` : "";
|
|
33975
34458
|
if (result.errorCount === 0) logger.info(`\n${green("✓")} ${bold(`Stack ${stackName} destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${result.errorCount} errors)`);
|
|
33976
34459
|
else logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up.`);
|
|
33977
34460
|
} finally {
|
|
33978
34461
|
renderer.stop();
|
|
34462
|
+
await saveChain;
|
|
33979
34463
|
logger.debug("Releasing lock...");
|
|
33980
34464
|
await ctx.lockManager.releaseLock(stackName, regionForState);
|
|
33981
34465
|
if (destroyAwsClients) {
|
|
@@ -35051,6 +35535,14 @@ async function deployCommand(stacks, options) {
|
|
|
35051
35535
|
stackProviderRegistry.setCustomResourceResponseBucket(stateBucket, baseRegion);
|
|
35052
35536
|
if (options.allowUnsupportedTypes?.length) stackProviderRegistry.allowUnsupportedTypes(options.allowUnsupportedTypes);
|
|
35053
35537
|
if (options.allowUnsupportedProperties?.length) stackProviderRegistry.allowUnsupportedProperties(options.allowUnsupportedProperties);
|
|
35538
|
+
const eventRecorder = startRunRecorder({
|
|
35539
|
+
backend: stackStateBackend,
|
|
35540
|
+
stackName: stackInfo.stackName,
|
|
35541
|
+
region: stackRegion,
|
|
35542
|
+
command: "deploy",
|
|
35543
|
+
dryRun: options.dryRun
|
|
35544
|
+
});
|
|
35545
|
+
let runResult = "SUCCEEDED";
|
|
35054
35546
|
try {
|
|
35055
35547
|
if (skipPrefix) {
|
|
35056
35548
|
const existing = await stackStateBackend.getState(stackInfo.stackName, stackRegion);
|
|
@@ -35106,6 +35598,7 @@ async function deployCommand(stacks, options) {
|
|
|
35106
35598
|
concurrency: options.concurrency,
|
|
35107
35599
|
dryRun: options.dryRun,
|
|
35108
35600
|
noRollback: !options.rollback,
|
|
35601
|
+
...eventRecorder && { eventRecorder },
|
|
35109
35602
|
...recreateViaCcApiTargets && recreateViaCcApiTargets.size > 0 && { recreateViaCcApiTargets },
|
|
35110
35603
|
...recreateViaSdkProviderTargets && recreateViaSdkProviderTargets.size > 0 && { recreateViaSdkProviderTargets },
|
|
35111
35604
|
captureObservedState: resolveCaptureObservedState(options.captureObservedState),
|
|
@@ -35143,7 +35636,17 @@ async function deployCommand(stacks, options) {
|
|
|
35143
35636
|
}
|
|
35144
35637
|
if (options.dryRun) logger.info(`\n${green("✓")} ${bold("Dry run completed")} - no actual changes made`);
|
|
35145
35638
|
else logger.info(`\n${green("✓")} ${bold("Deployment completed successfully")}`);
|
|
35639
|
+
recordRunSucceeded(eventRecorder, stackInfo.stackName, {
|
|
35640
|
+
created: deployResult.created,
|
|
35641
|
+
updated: deployResult.updated,
|
|
35642
|
+
deleted: deployResult.deleted
|
|
35643
|
+
}, deployResult.durationMs);
|
|
35644
|
+
} catch (deployError) {
|
|
35645
|
+
runResult = "FAILED";
|
|
35646
|
+
recordRunFailed(eventRecorder, stackInfo.stackName, deployError);
|
|
35647
|
+
throw deployError;
|
|
35146
35648
|
} finally {
|
|
35649
|
+
if (eventRecorder) await eventRecorder.finalize(runResult);
|
|
35147
35650
|
stackAwsClients.destroy();
|
|
35148
35651
|
stateS3Client.destroy();
|
|
35149
35652
|
switchRegion(baseRegion);
|
|
@@ -37645,42 +38148,70 @@ async function destroyCommand(stackArgs, options) {
|
|
|
37645
38148
|
totalErrors++;
|
|
37646
38149
|
continue;
|
|
37647
38150
|
}
|
|
37648
|
-
const
|
|
37649
|
-
stateBackend,
|
|
37650
|
-
|
|
37651
|
-
|
|
37652
|
-
|
|
37653
|
-
|
|
37654
|
-
|
|
37655
|
-
|
|
37656
|
-
|
|
37657
|
-
|
|
37658
|
-
|
|
38151
|
+
const eventRecorder = startRunRecorder({
|
|
38152
|
+
backend: stateBackend,
|
|
38153
|
+
stackName,
|
|
38154
|
+
region: stackTargetRegion,
|
|
38155
|
+
command: "destroy"
|
|
38156
|
+
});
|
|
38157
|
+
let destroyRunResult = "SUCCEEDED";
|
|
38158
|
+
try {
|
|
38159
|
+
const result = await withNestedStackContext({
|
|
38160
|
+
stateBackend,
|
|
38161
|
+
lockManager,
|
|
38162
|
+
providerRegistry,
|
|
38163
|
+
parentStackName: stackName,
|
|
38164
|
+
parentRegion: stackTargetRegion,
|
|
38165
|
+
accountId,
|
|
38166
|
+
awsClients,
|
|
38167
|
+
stateBucket,
|
|
38168
|
+
exportIndexStore,
|
|
38169
|
+
destroyOptions: {
|
|
38170
|
+
...options.profile && { profile: options.profile },
|
|
38171
|
+
...options.removeProtection === true && { removeProtection: true },
|
|
38172
|
+
...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
|
|
38173
|
+
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
38174
|
+
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
38175
|
+
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
38176
|
+
}
|
|
38177
|
+
}, () => runDestroyForStack(stackName, stateResult.state, {
|
|
38178
|
+
stateBackend,
|
|
38179
|
+
lockManager,
|
|
38180
|
+
providerRegistry,
|
|
38181
|
+
baseAwsClients: awsClients,
|
|
38182
|
+
baseRegion: region,
|
|
37659
38183
|
...options.profile && { profile: options.profile },
|
|
37660
|
-
|
|
38184
|
+
stateBucket,
|
|
38185
|
+
skipConfirmation: options.yes || options.force,
|
|
38186
|
+
removeProtection: options.removeProtection === true,
|
|
38187
|
+
exportIndexStore,
|
|
38188
|
+
...options.allowUnsupportedTypes?.length && { allowUnsupportedTypes: options.allowUnsupportedTypes },
|
|
37661
38189
|
...options.resourceWarnAfter?.globalMs !== void 0 && { resourceWarnAfterMs: options.resourceWarnAfter.globalMs },
|
|
37662
38190
|
...options.resourceTimeout?.globalMs !== void 0 && { resourceTimeoutMs: options.resourceTimeout.globalMs },
|
|
37663
38191
|
...options.resourceWarnAfter?.perTypeMs && { resourceWarnAfterByType: options.resourceWarnAfter.perTypeMs },
|
|
37664
|
-
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs }
|
|
37665
|
-
|
|
37666
|
-
|
|
37667
|
-
|
|
37668
|
-
|
|
37669
|
-
|
|
37670
|
-
|
|
37671
|
-
|
|
37672
|
-
|
|
37673
|
-
|
|
37674
|
-
|
|
37675
|
-
|
|
37676
|
-
|
|
37677
|
-
|
|
37678
|
-
|
|
37679
|
-
|
|
37680
|
-
|
|
37681
|
-
|
|
37682
|
-
|
|
37683
|
-
|
|
38192
|
+
...options.resourceTimeout?.perTypeMs && { resourceTimeoutByType: options.resourceTimeout.perTypeMs },
|
|
38193
|
+
eventRecorder
|
|
38194
|
+
}));
|
|
38195
|
+
totalErrors += result.errorCount;
|
|
38196
|
+
destroyRunResult = result.errorCount > 0 ? "FAILED" : "SUCCEEDED";
|
|
38197
|
+
eventRecorder.record({
|
|
38198
|
+
eventType: "RUN_FINISHED",
|
|
38199
|
+
stackName,
|
|
38200
|
+
result: destroyRunResult,
|
|
38201
|
+
counts: {
|
|
38202
|
+
created: 0,
|
|
38203
|
+
updated: 0,
|
|
38204
|
+
deleted: result.deletedCount,
|
|
38205
|
+
...result.errorCount > 0 && { failed: result.errorCount }
|
|
38206
|
+
}
|
|
38207
|
+
});
|
|
38208
|
+
} catch (destroyError) {
|
|
38209
|
+
destroyRunResult = "FAILED";
|
|
38210
|
+
recordRunFailed(eventRecorder, stackName, destroyError);
|
|
38211
|
+
throw destroyError;
|
|
38212
|
+
} finally {
|
|
38213
|
+
await eventRecorder.finalize(destroyRunResult);
|
|
38214
|
+
}
|
|
37684
38215
|
}
|
|
37685
38216
|
if (totalErrors > 0) throw new PartialFailureError(`Destroy completed with ${totalErrors} resource error(s). State preserved — inspect 'cdkd state show <stack>' and re-run 'cdkd destroy' to retry.`);
|
|
37686
38217
|
} finally {
|
|
@@ -37705,6 +38236,147 @@ function createDestroyCommand() {
|
|
|
37705
38236
|
return cmd;
|
|
37706
38237
|
}
|
|
37707
38238
|
|
|
38239
|
+
//#endregion
|
|
38240
|
+
//#region src/cli/commands/events.ts
|
|
38241
|
+
/**
|
|
38242
|
+
* `cdkd events <stack>` — read back the structured deployment events
|
|
38243
|
+
* (issue #808) cdkd persists per deploy / destroy run, the local
|
|
38244
|
+
* equivalent of CloudFormation's `DescribeStackEvents`.
|
|
38245
|
+
*
|
|
38246
|
+
* Two modes:
|
|
38247
|
+
* - No `--run`: list the recorded runs for the stack, newest first
|
|
38248
|
+
* (runId, command, cdkd version, result, started/finished, event count).
|
|
38249
|
+
* - `--run <id>`: print the full ordered event stream for that one run.
|
|
38250
|
+
*
|
|
38251
|
+
* `--format json` (alias `--json`) emits machine-readable JSON for tooling
|
|
38252
|
+
* / AI-agent hand-off. Events survive `cdkd destroy` (they live under a
|
|
38253
|
+
* separate `deployments/` key family from `state.json`), so a destroyed
|
|
38254
|
+
* stack's failure history stays readable.
|
|
38255
|
+
*/
|
|
38256
|
+
async function eventsCommand(stackName, options) {
|
|
38257
|
+
const logger = getLogger();
|
|
38258
|
+
if (options.verbose) logger.setLevel("debug");
|
|
38259
|
+
warnIfDeprecatedRegion(options);
|
|
38260
|
+
const asJson = options.json === true;
|
|
38261
|
+
const awsClients = new AwsClients({
|
|
38262
|
+
...options.region && { region: options.region },
|
|
38263
|
+
...options.profile && { profile: options.profile }
|
|
38264
|
+
});
|
|
38265
|
+
setAwsClients(awsClients);
|
|
38266
|
+
try {
|
|
38267
|
+
const region = options.region || process.env["AWS_REGION"] || "us-east-1";
|
|
38268
|
+
const bucket = await resolveStateBucketWithDefault(options.stateBucket, region);
|
|
38269
|
+
const prefix = options.statePrefix ?? "cdkd";
|
|
38270
|
+
const stateBackend = new S3StateBackend(awsClients.s3, {
|
|
38271
|
+
bucket,
|
|
38272
|
+
prefix
|
|
38273
|
+
}, {
|
|
38274
|
+
region,
|
|
38275
|
+
...options.profile && { profile: options.profile }
|
|
38276
|
+
});
|
|
38277
|
+
await stateBackend.verifyBucketExists();
|
|
38278
|
+
const reader = new DeploymentEventsReader(stateBackend);
|
|
38279
|
+
const targetRegion = await resolveEventsRegion(reader, stackName, options.stackRegion);
|
|
38280
|
+
if (options.run) {
|
|
38281
|
+
const events = await reader.readRunEvents(stackName, targetRegion, options.run);
|
|
38282
|
+
if (events === null) throw new CdkdError(`No deployment-event stream found for run '${options.run}' of stack '${stackName}' in region '${targetRegion}'.`, "EVENTS_RUN_NOT_FOUND");
|
|
38283
|
+
if (asJson) {
|
|
38284
|
+
process.stdout.write(JSON.stringify(events, null, 2) + "\n");
|
|
38285
|
+
return;
|
|
38286
|
+
}
|
|
38287
|
+
printRunEvents(stackName, targetRegion, options.run, events);
|
|
38288
|
+
return;
|
|
38289
|
+
}
|
|
38290
|
+
const runs = await reader.listRuns(stackName, targetRegion);
|
|
38291
|
+
if (asJson) {
|
|
38292
|
+
process.stdout.write(JSON.stringify({
|
|
38293
|
+
stackName,
|
|
38294
|
+
region: targetRegion,
|
|
38295
|
+
runs
|
|
38296
|
+
}, null, 2) + "\n");
|
|
38297
|
+
return;
|
|
38298
|
+
}
|
|
38299
|
+
printRunList(stackName, targetRegion, runs);
|
|
38300
|
+
} finally {
|
|
38301
|
+
awsClients.destroy();
|
|
38302
|
+
}
|
|
38303
|
+
}
|
|
38304
|
+
/**
|
|
38305
|
+
* Pick the region whose `deployments/` key family holds this stack's run
|
|
38306
|
+
* history. When `--stack-region` is supplied it is honored verbatim;
|
|
38307
|
+
* otherwise the single discovered region is used, and an ambiguous (>1)
|
|
38308
|
+
* or missing (0) history surfaces an actionable error.
|
|
38309
|
+
*/
|
|
38310
|
+
async function resolveEventsRegion(reader, stackName, explicitRegion) {
|
|
38311
|
+
if (explicitRegion) return explicitRegion;
|
|
38312
|
+
const regions = await reader.listRegions(stackName);
|
|
38313
|
+
if (regions.length === 0) throw new CdkdError(`No deployment-event history found for stack '${stackName}'. Events are recorded by 'cdkd deploy' / 'cdkd destroy' (issue #808); a stack deployed by an older cdkd version has none.`, "EVENTS_NOT_FOUND");
|
|
38314
|
+
if (regions.length > 1) throw new CdkdError(`Stack '${stackName}' has deployment-event history in multiple regions: ${regions.join(", ")}. Re-run with '--stack-region <region>' to disambiguate.`, "EVENTS_REGION_AMBIGUOUS");
|
|
38315
|
+
return regions[0];
|
|
38316
|
+
}
|
|
38317
|
+
/** Human-readable run listing (newest first). */
|
|
38318
|
+
function printRunList(stackName, region, runs) {
|
|
38319
|
+
const logger = getLogger();
|
|
38320
|
+
logger.info(`${bold("Deployment runs for")} ${cyan(stackName)} ${gray(`(${region})`)}`);
|
|
38321
|
+
if (runs.length === 0) {
|
|
38322
|
+
logger.info(gray(" (no runs recorded)"));
|
|
38323
|
+
return;
|
|
38324
|
+
}
|
|
38325
|
+
for (const run of runs) {
|
|
38326
|
+
const resultColored = run.result === "SUCCEEDED" ? green(run.result) : run.result === "UNKNOWN" ? gray(run.result) : red(run.result);
|
|
38327
|
+
logger.info(` ${cyan(run.runId)} ${run.command} ${resultColored} ${gray(run.startedAt || "?")} -> ${gray(run.finishedAt || "?")} ${gray(`cdkd ${run.cdkdVersion}`)} ${gray(`${run.eventCount} events`)}`);
|
|
38328
|
+
}
|
|
38329
|
+
logger.info(gray(`\nUse 'cdkd events ${stackName} --run <runId>' to read one run's events.`));
|
|
38330
|
+
}
|
|
38331
|
+
/** Human-readable single-run event stream (in recorded order). */
|
|
38332
|
+
function printRunEvents(stackName, region, runId, events) {
|
|
38333
|
+
const logger = getLogger();
|
|
38334
|
+
logger.info(`${bold("Events for run")} ${cyan(runId)} ${gray(`(${stackName}, ${region})`)}`);
|
|
38335
|
+
if (events.length === 0) {
|
|
38336
|
+
logger.info(gray(" (no events)"));
|
|
38337
|
+
return;
|
|
38338
|
+
}
|
|
38339
|
+
for (const e of events) {
|
|
38340
|
+
const parts = [gray(e.timestamp), colorizeEventType(e.eventType)];
|
|
38341
|
+
if (e.logicalId) parts.push(`${e.logicalId}${e.resourceType ? ` (${e.resourceType})` : ""}`);
|
|
38342
|
+
if (e.operation) parts.push(gray(e.operation));
|
|
38343
|
+
if (e.provisionedBy) parts.push(gray(`[${e.provisionedBy}]`));
|
|
38344
|
+
if (e.command) parts.push(gray(e.command));
|
|
38345
|
+
if (e.region) parts.push(gray(e.region));
|
|
38346
|
+
if (e.cdkdVersion) parts.push(gray(`cdkd ${e.cdkdVersion}`));
|
|
38347
|
+
if (e.result) parts.push(e.result === "SUCCEEDED" ? green(e.result) : red(e.result));
|
|
38348
|
+
if (typeof e.durationMs === "number") parts.push(gray(`${e.durationMs}ms`));
|
|
38349
|
+
if (e.counts) parts.push(gray(`+${e.counts.created}/~${e.counts.updated}/-${e.counts.deleted}` + (e.counts.failed ? ` !${e.counts.failed}` : "")));
|
|
38350
|
+
logger.info(` ${parts.join(" ")}`);
|
|
38351
|
+
if (e.error) {
|
|
38352
|
+
const code = e.error.awsErrorCode ? ` (${e.error.awsErrorCode})` : "";
|
|
38353
|
+
const reqId = e.error.requestId ? gray(` requestId=${e.error.requestId}`) : "";
|
|
38354
|
+
logger.info(` ${red(`${e.error.name}${code}: ${e.error.message}`)}${reqId}`);
|
|
38355
|
+
}
|
|
38356
|
+
}
|
|
38357
|
+
}
|
|
38358
|
+
/** Color the event-type token by its lifecycle phase. */
|
|
38359
|
+
function colorizeEventType(eventType) {
|
|
38360
|
+
if (eventType.endsWith("FAILED")) return red(eventType);
|
|
38361
|
+
if (eventType.endsWith("SUCCEEDED") || eventType === "RUN_FINISHED") return green(eventType);
|
|
38362
|
+
if (eventType.startsWith("ROLLBACK")) return yellow(eventType);
|
|
38363
|
+
return cyan(eventType);
|
|
38364
|
+
}
|
|
38365
|
+
/**
|
|
38366
|
+
* Create the `cdkd events` command.
|
|
38367
|
+
*/
|
|
38368
|
+
function createEventsCommand() {
|
|
38369
|
+
const cmd = new Command("events").description("Read back structured deployment events (cdkd's DescribeStackEvents equivalent, issue #808)").argument("<stack>", "Stack name (physical CloudFormation name)").option("--run <runId>", "Read a single run's full event stream instead of the run listing").option("--stack-region <region>", "Disambiguate a stack with event history in multiple regions").option("--json", "Output as JSON", false).option("--format <format>", "Output format ('json' is equivalent to --json)").action(withErrorHandling((stack, options) => {
|
|
38370
|
+
return eventsCommand(stack, {
|
|
38371
|
+
...options,
|
|
38372
|
+
json: options.json === true || options.format === "json"
|
|
38373
|
+
});
|
|
38374
|
+
}));
|
|
38375
|
+
[...commonOptions, ...stateOptions].forEach((opt) => cmd.addOption(opt));
|
|
38376
|
+
cmd.addOption(deprecatedRegionOption);
|
|
38377
|
+
return cmd;
|
|
38378
|
+
}
|
|
38379
|
+
|
|
37708
38380
|
//#endregion
|
|
37709
38381
|
//#region src/cli/cdk-path.ts
|
|
37710
38382
|
/**
|
|
@@ -52868,7 +53540,7 @@ function reorderArgs(argv) {
|
|
|
52868
53540
|
async function main() {
|
|
52869
53541
|
installPipeCloseHandler();
|
|
52870
53542
|
const program = new Command();
|
|
52871
|
-
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.
|
|
53543
|
+
program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.220.0");
|
|
52872
53544
|
program.addCommand(createBootstrapCommand());
|
|
52873
53545
|
program.addCommand(createSynthCommand());
|
|
52874
53546
|
program.addCommand(createListCommand());
|
|
@@ -52876,6 +53548,7 @@ async function main() {
|
|
|
52876
53548
|
program.addCommand(createDiffCommand());
|
|
52877
53549
|
program.addCommand(createDriftCommand());
|
|
52878
53550
|
program.addCommand(createDestroyCommand());
|
|
53551
|
+
program.addCommand(createEventsCommand());
|
|
52879
53552
|
program.addCommand(createOrphanCommand());
|
|
52880
53553
|
program.addCommand(createImportCommand());
|
|
52881
53554
|
program.addCommand(createPublishAssetsCommand());
|