Skip to content

Commit b104eed

Browse files
committed
add checkDestructiveChanges.ts
1 parent 81f327f commit b104eed

5 files changed

Lines changed: 7364 additions & 0 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {readFileSync} from "node:fs"
2+
3+
export type ChangeRequiringAttention = {
4+
logicalId: string;
5+
physicalId: string;
6+
resourceType: string;
7+
reason: string;
8+
}
9+
10+
type RawChange = {
11+
ResourceChange?: {
12+
LogicalResourceId?: string;
13+
PhysicalResourceId?: string;
14+
ResourceType?: string;
15+
Replacement?: unknown;
16+
Action?: string;
17+
} | null;
18+
}
19+
20+
const requiresReplacement = (replacement: unknown): boolean => {
21+
if (replacement === undefined || replacement === null) {
22+
return false
23+
}
24+
25+
const normalized = String(replacement)
26+
return normalized === "True" || normalized === "Conditional"
27+
}
28+
29+
export function checkDestructiveChanges(filePath: string): Array<ChangeRequiringAttention> {
30+
if (!filePath) {
31+
throw new Error("A change set file path must be provided")
32+
}
33+
34+
const raw = readFileSync(filePath, "utf-8")
35+
const data = JSON.parse(raw)
36+
const changes = Array.isArray(data?.Changes) ? data.Changes : []
37+
38+
return changes
39+
.map((change: RawChange) => {
40+
const resourceChange = change?.ResourceChange
41+
if (!resourceChange) {
42+
return undefined
43+
}
44+
45+
const replacementNeeded = requiresReplacement(resourceChange.Replacement)
46+
const action = resourceChange.Action
47+
const isRemoval = action === "Remove"
48+
49+
if (!replacementNeeded && !isRemoval) {
50+
return undefined
51+
}
52+
53+
return {
54+
logicalId: resourceChange.LogicalResourceId ?? "<unknown logical id>",
55+
physicalId: resourceChange.PhysicalResourceId ?? "<unknown physical id>",
56+
resourceType: resourceChange.ResourceType ?? "<unknown type>",
57+
reason: replacementNeeded
58+
? `Replacement: ${String(resourceChange.Replacement)}`
59+
: `Action: ${action ?? "<unknown action>"}`
60+
}
61+
})
62+
.filter((change): change is ChangeRequiringAttention => Boolean(change))
63+
}

packages/deploymentUtils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from "./specifications/deployApi"
22
export * from "./specifications/writeSchemas"
33
export * from "./specifications/deleteProxygenDeployments"
44
export * from "./config/index"
5+
export * from "./changesets/checkDestructiveChanges"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {mkdtempSync, writeFileSync} from "node:fs"
2+
import {tmpdir} from "node:os"
3+
import {dirname, join} from "node:path"
4+
import {fileURLToPath} from "node:url"
5+
import {describe, expect, test} from "vitest"
6+
import {checkDestructiveChanges} from "../../src/changesets/checkDestructiveChanges"
7+
8+
const __filename = fileURLToPath(import.meta.url)
9+
const __dirname = dirname(__filename)
10+
const fixturesDir = join(__dirname, "examples")
11+
12+
const destructiveChangeSet = join(fixturesDir, "destructive_changeset.json")
13+
const safeChangeSet = join(fixturesDir, "safe_changeset.json")
14+
15+
describe("checkDestructiveChanges", () => {
16+
test("returns resources that require replacement", () => {
17+
const replacements = checkDestructiveChanges(destructiveChangeSet)
18+
19+
expect(replacements.length).toBeGreaterThan(0)
20+
expect(replacements).toContainEqual({
21+
logicalId: "AlarmsAccountLambdaConcurrencyAlarm8AF49AD8",
22+
physicalId: "monitoring-Account_Lambda_Concurrency",
23+
resourceType: "AWS::CloudWatch::Alarm",
24+
reason: "Replacement: True"
25+
})
26+
})
27+
28+
test("returns an empty array when no replacements or removals exist", () => {
29+
const replacements = checkDestructiveChanges(safeChangeSet)
30+
31+
expect(replacements).toEqual([])
32+
})
33+
34+
test("includes resources marked for removal", () => {
35+
const tempDir = mkdtempSync(join(tmpdir(), "changeset-"))
36+
const removalFixture = join(tempDir, "removal.json")
37+
const changeSet = {
38+
Changes: [
39+
{
40+
ResourceChange: {
41+
LogicalResourceId: "ResourceToRemove",
42+
PhysicalResourceId: "physical-id",
43+
ResourceType: "AWS::S3::Bucket",
44+
Action: "Remove",
45+
Replacement: "False"
46+
}
47+
}
48+
]
49+
}
50+
writeFileSync(removalFixture, JSON.stringify(changeSet), "utf-8")
51+
52+
const replacements = checkDestructiveChanges(removalFixture)
53+
54+
expect(replacements).toEqual([
55+
{
56+
logicalId: "ResourceToRemove",
57+
physicalId: "physical-id",
58+
resourceType: "AWS::S3::Bucket",
59+
reason: "Action: Remove"
60+
}
61+
])
62+
})
63+
})

0 commit comments

Comments
 (0)