Skip to content

Commit e69e257

Browse files
committed
function to do cf things directly
1 parent 2eb9181 commit e69e257

2 files changed

Lines changed: 112 additions & 2 deletions

File tree

packages/deploymentUtils/src/changesets/checkDestructiveChanges.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {CloudFormationClient, DescribeChangeSetCommand} from "@aws-sdk/client-cloudformation"
2+
13
export type ChangeRequiringAttention = {
24
logicalId: string;
35
physicalId: string;
@@ -62,3 +64,32 @@ export function checkDestructiveChanges(changeSet: ChangeSet | undefined | null)
6264
})
6365
.filter((change): change is ChangeRequiringAttention => Boolean(change))
6466
}
67+
68+
export async function checkDestructiveChangeSet(
69+
changeSetName: string,
70+
stackName: string,
71+
region: string): Promise<void> {
72+
if (!changeSetName || !stackName || !region) {
73+
throw new Error("Change set name, stack name, and region are required")
74+
}
75+
76+
const client = new CloudFormationClient({region})
77+
const command = new DescribeChangeSetCommand({
78+
ChangeSetName: changeSetName,
79+
StackName: stackName
80+
})
81+
82+
const response = await client.send(command)
83+
const destructiveChanges = checkDestructiveChanges(response)
84+
85+
if (destructiveChanges.length === 0) {
86+
console.log(`Change set ${changeSetName} for stack ${stackName} has no destructive changes.`)
87+
return
88+
}
89+
90+
console.error("Resources that require attention:")
91+
destructiveChanges.forEach(({logicalId, physicalId, resourceType, reason}) => {
92+
console.error(`- LogicalId: ${logicalId}, PhysicalId: ${physicalId}, Type: ${resourceType}, Reason: ${reason}`)
93+
})
94+
throw new Error(`Change set ${changeSetName} contains destructive changes`)
95+
}

packages/deploymentUtils/tests/changesets/checkDestructiveChanges.test.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,42 @@
11
import {readFileSync} from "node:fs"
22
import {dirname, join} from "node:path"
33
import {fileURLToPath} from "node:url"
4-
import {describe, expect, test} from "vitest"
5-
import {checkDestructiveChanges} from "../../src/changesets/checkDestructiveChanges"
4+
import {
5+
beforeEach,
6+
describe,
7+
expect,
8+
test,
9+
vi
10+
} from "vitest"
11+
import {checkDestructiveChanges, checkDestructiveChangeSet} from "../../src/changesets/checkDestructiveChanges"
12+
13+
const mockCloudFormationSend = vi.fn()
14+
15+
vi.mock("@aws-sdk/client-cloudformation", () => {
16+
class CloudFormationClient {
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
config: any
19+
constructor(config: {region: string}) {
20+
this.config = config
21+
}
22+
23+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
24+
send(command: any) {
25+
return mockCloudFormationSend(command)
26+
}
27+
}
28+
29+
class DescribeChangeSetCommand {
30+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
31+
input: any
32+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
33+
constructor(input: any) {
34+
this.input = input
35+
}
36+
}
37+
38+
return {CloudFormationClient, DescribeChangeSetCommand}
39+
})
640

741
const __filename = fileURLToPath(import.meta.url)
842
const __dirname = dirname(__filename)
@@ -13,6 +47,10 @@ const loadChangeSet = (filePath: string) => JSON.parse(readFileSync(filePath, "u
1347
const destructiveChangeSet = loadChangeSet(join(fixturesDir, "destructive_changeset.json"))
1448
const safeChangeSet = loadChangeSet(join(fixturesDir, "safe_changeset.json"))
1549

50+
beforeEach(() => {
51+
mockCloudFormationSend.mockReset()
52+
})
53+
1654
describe("checkDestructiveChanges", () => {
1755
test("returns resources that require replacement", () => {
1856
const replacements = checkDestructiveChanges(destructiveChangeSet)
@@ -58,3 +96,44 @@ describe("checkDestructiveChanges", () => {
5896
])
5997
})
6098
})
99+
100+
describe("checkDestructiveChangeSet", () => {
101+
test("logs success when no destructive changes are present", async () => {
102+
mockCloudFormationSend.mockResolvedValueOnce(safeChangeSet)
103+
104+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined)
105+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
106+
107+
try {
108+
await expect(checkDestructiveChangeSet("cs", "stack", "eu-west-2")).resolves.toBeUndefined()
109+
110+
expect(mockCloudFormationSend).toHaveBeenCalledTimes(1)
111+
const command = mockCloudFormationSend.mock.calls[0][0] as {input: {ChangeSetName: string; StackName: string}}
112+
expect(command.input).toEqual({ChangeSetName: "cs", StackName: "stack"})
113+
expect(logSpy).toHaveBeenCalledWith("Change set cs for stack stack has no destructive changes.")
114+
expect(errorSpy).not.toHaveBeenCalled()
115+
} finally {
116+
logSpy.mockRestore()
117+
errorSpy.mockRestore()
118+
}
119+
})
120+
121+
test("logs details and throws when destructive changes exist", async () => {
122+
mockCloudFormationSend.mockResolvedValueOnce(destructiveChangeSet)
123+
124+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined)
125+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined)
126+
127+
try {
128+
await expect(checkDestructiveChangeSet("cs", "stack", "eu-west-2"))
129+
.rejects.toThrow("Change set cs contains destructive changes")
130+
131+
expect(mockCloudFormationSend).toHaveBeenCalledTimes(1)
132+
expect(logSpy).not.toHaveBeenCalled()
133+
expect(errorSpy).toHaveBeenCalledWith("Resources that require attention:")
134+
} finally {
135+
logSpy.mockRestore()
136+
errorSpy.mockRestore()
137+
}
138+
})
139+
})

0 commit comments

Comments
 (0)