Skip to content

Commit 48af620

Browse files
AlCalzoneCopilot
andauthored
fix: preserve JSON comments in firmware submission workflow (#292)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 0351df3 commit 48af620

File tree

4 files changed

+167
-46
lines changed

4 files changed

+167
-46
lines changed

.github/scripts/firmware-submission/process-submission.mts

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import * as githubActions from "@actions/github";
22
import { downloadFirmware, generateHash } from "@zwave-js/firmware-integrity";
3+
import {
4+
parse as parseCommentJson,
5+
stringify as stringifyCommentJson,
6+
} from "comment-json";
37
import JSON5 from "json5";
48
import { spawnSync } from "node:child_process";
59
import * as fs from "node:fs";
@@ -284,7 +288,11 @@ function buildIssueFieldHeadingSequences(): string[][] {
284288
"single-target-chip-1-with-target",
285289
];
286290
for (let deviceCount = 1; deviceCount <= MAX_DEVICES; deviceCount++) {
287-
for (let upgradeCount = 1; upgradeCount <= MAX_UPGRADES; upgradeCount++) {
291+
for (
292+
let upgradeCount = 1;
293+
upgradeCount <= MAX_UPGRADES;
294+
upgradeCount++
295+
) {
288296
for (const fileLayout of fileLayouts) {
289297
sequences.add(
290298
JSON.stringify(
@@ -446,6 +454,33 @@ function loadFirmwareConfigs(): FirmwareConfigFile[] {
446454
});
447455
}
448456

457+
export function appendUpgradesToFirmwareConfigText(
458+
configText: string,
459+
newUpgrades: readonly Record<string, any>[],
460+
): string {
461+
const config = parseCommentJson<Record<string, any>>(configText);
462+
if (!Array.isArray(config.upgrades)) {
463+
throw new Error("Firmware config does not contain an upgrades array.");
464+
}
465+
466+
for (const upgrade of newUpgrades) {
467+
config.upgrades.push(upgrade);
468+
}
469+
470+
return `${stringifyCommentJson(config, null, "\t")}\n`;
471+
}
472+
473+
export async function formatWithPrettier(
474+
text: string,
475+
parser: string,
476+
prettierConfig: Record<string, any> = {},
477+
): Promise<string> {
478+
return prettier.format(text, {
479+
...prettierConfig,
480+
parser,
481+
});
482+
}
483+
449484
function chooseExistingDirectory(
450485
candidates: string[],
451486
preferredDirectory: string,
@@ -616,7 +651,8 @@ export function parseIssueBody(body: string): Record<string, string | null> {
616651
? candidateSequences
617652
.filter(
618653
(candidate) =>
619-
candidate.headings[candidate.nextHeadingIndex] === heading,
654+
candidate.headings[candidate.nextHeadingIndex] ===
655+
heading,
620656
)
621657
.map((candidate) => ({
622658
headings: candidate.headings,
@@ -888,7 +924,11 @@ function normalizeUpgradeVariant(upgrade: Record<string, any>): {
888924
function getUpgradeVariantKey(
889925
variant: NonNullable<ReturnType<typeof normalizeUpgradeVariant>>,
890926
): string {
891-
return JSON.stringify([variant.version, variant.region, variant.ifCondition]);
927+
return JSON.stringify([
928+
variant.version,
929+
variant.region,
930+
variant.ifCondition,
931+
]);
892932
}
893933

894934
function describeUpgradeVariant(
@@ -1030,21 +1070,15 @@ export function parseUpgradeFilesFromSections({
10301070
? getSingleTargetTargetNumberFieldLabel(upgradeIndex)
10311071
: descriptor.targetLabels[0],
10321072
});
1033-
if (
1034-
urlLabel === singleTargetUrlLabel &&
1035-
targetRaw != null
1036-
) {
1073+
if (urlLabel === singleTargetUrlLabel && targetRaw != null) {
10371074
errors.push(
10381075
`'${targetLabel}' is not supported in the single-target submission form. That form always uses target number 0. Use the 'Firmware Submission' form instead.`,
10391076
);
10401077
}
10411078
const target =
10421079
targetRaw != null && urlLabel !== singleTargetUrlLabel
1043-
? (validateTargetNumber(
1044-
targetRaw,
1045-
targetLabel,
1046-
errors,
1047-
) ?? descriptor.defaultTarget)
1080+
? (validateTargetNumber(targetRaw, targetLabel, errors) ??
1081+
descriptor.defaultTarget)
10481082
: descriptor.defaultTarget;
10491083

10501084
files.push({ target, url });
@@ -1761,10 +1795,11 @@ export default async function main({
17611795
for (const upgrade of upgradeFormData) {
17621796
const raw = upgrade.changelog.trim().replace(/\r\n/g, "\n");
17631797
try {
1764-
const formatted = await prettier.format(raw, {
1765-
...prettierConfig,
1766-
parser: "markdown",
1767-
});
1798+
const formatted = await formatWithPrettier(
1799+
raw,
1800+
"markdown",
1801+
prettierConfig,
1802+
);
17681803
formattedChangelogs.push(formatted.trim());
17691804
} catch {
17701805
formattedChangelogs.push(raw);
@@ -1801,42 +1836,44 @@ export default async function main({
18011836
]);
18021837
}
18031838

1804-
const config = existingConfig
1805-
? {
1806-
...existingConfig,
1807-
upgrades: [
1808-
...(existingConfig.upgrades ?? []),
1809-
...newUpgrades,
1810-
],
1811-
}
1812-
: {
1813-
devices: devices.map((device) => {
1814-
const entry: Record<string, any> = {
1815-
brand: device.brand,
1816-
model: device.model,
1817-
manufacturerId: device.manufacturerId,
1818-
productType: device.productType,
1819-
productId: device.productId,
1820-
};
1821-
if (device.firmwareVersion) {
1822-
entry.firmwareVersion = device.firmwareVersion;
1823-
}
1824-
return entry;
1825-
}),
1826-
upgrades: newUpgrades,
1827-
};
1839+
const configText = matchedExistingFile
1840+
? appendUpgradesToFirmwareConfigText(
1841+
fs.readFileSync(matchedExistingFile.absolutePath, "utf-8"),
1842+
newUpgrades,
1843+
)
1844+
: `${stringifyCommentJson(
1845+
{
1846+
devices: devices.map((device) => {
1847+
const entry: Record<string, any> = {
1848+
brand: device.brand,
1849+
model: device.model,
1850+
manufacturerId: device.manufacturerId,
1851+
productType: device.productType,
1852+
productId: device.productId,
1853+
};
1854+
if (device.firmwareVersion) {
1855+
entry.firmwareVersion = device.firmwareVersion;
1856+
}
1857+
return entry;
1858+
}),
1859+
upgrades: newUpgrades,
1860+
},
1861+
null,
1862+
"\t",
1863+
)}\n`;
1864+
const formattedConfigText = await formatWithPrettier(
1865+
configText,
1866+
"jsonc",
1867+
prettierConfig,
1868+
);
18281869

18291870
console.log(
18301871
"Re-checking approved issue state before writing changes...",
18311872
);
18321873
await ensureIssueStillApproved(true);
18331874
console.log(`Writing config to ${relativeFilePath}...`);
18341875
fs.mkdirSync(path.dirname(absoluteFilePath), { recursive: true });
1835-
fs.writeFileSync(
1836-
absoluteFilePath,
1837-
`${JSON.stringify(config, null, "\t")}\n`,
1838-
"utf-8",
1839-
);
1876+
fs.writeFileSync(absoluteFilePath, formattedConfigText, "utf-8");
18401877

18411878
git("add", relativeFilePath);
18421879
const lastVersion =

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@typescript-eslint/parser": "^8.43.0",
3838
"@zwave-js/firmware-integrity": "^1.0.2",
3939
"ava": "^4.3.3",
40+
"comment-json": "^4.6.2",
4041
"esbuild": "^0.15.7",
4142
"esbuild-register": "^3.3.3",
4243
"eslint": "^9.35.0",

test/firmware-submission-regressions.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import test from "ava";
2+
import JSON5 from "json5";
23
import { readFile } from "node:fs/promises";
34

45
const processSubmissionModulePath =
@@ -16,10 +17,12 @@ const resetOnEditModule = await import(resetOnEditModulePath);
1617
const cleanupLabelsModule = await import(cleanupLabelsModulePath);
1718

1819
const {
20+
appendUpgradesToFirmwareConfigText,
1921
createUpgradeEntry,
2022
extractIssueTemplateFieldHeadings,
2123
findDuplicateTargets,
2224
findDuplicateUpgradeVariants,
25+
formatWithPrettier,
2326
getApprovalInvalidReason,
2427
parseIssueBody,
2528
parseUpgradeFilesFromSections,
@@ -692,6 +695,68 @@ test("createUpgradeEntry preserves submitted file order and targets", (t) => {
692695
);
693696
});
694697

698+
test("appendUpgradesToFirmwareConfigText preserves existing JSONC comments after formatting", async (t) => {
699+
const existingConfig = `{
700+
"devices": [
701+
{
702+
"brand": "Zooz",
703+
"model": "ZEN51", // existing model comment
704+
"manufacturerId": "0x027a",
705+
"productType": "0x7000",
706+
"productId": "0xa008"
707+
}
708+
],
709+
"upgrades": [
710+
// existing upgrade comment
711+
{
712+
"version": "1.60",
713+
"changelog": "Existing release",
714+
"url": "https://example.com/existing.gbl",
715+
"integrity": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
716+
}
717+
]
718+
}
719+
`;
720+
721+
const updatedConfig = await formatWithPrettier(
722+
appendUpgradesToFirmwareConfigText(existingConfig, [
723+
createUpgradeEntry({
724+
version: "1.61",
725+
changelog: "New release",
726+
channel: "stable",
727+
region: null,
728+
ifCondition: null,
729+
files: [
730+
{
731+
target: 0,
732+
url: "https://example.com/new.gbl",
733+
integrity:
734+
"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
735+
},
736+
],
737+
}),
738+
]),
739+
"jsonc",
740+
{
741+
endOfLine: "lf",
742+
tabWidth: 4,
743+
useTabs: true,
744+
},
745+
);
746+
747+
t.true(updatedConfig.includes("// existing model comment"));
748+
t.true(updatedConfig.includes("// existing upgrade comment"));
749+
t.true(updatedConfig.includes('\t"devices": ['));
750+
751+
const parsed: {
752+
upgrades: Array<{ version: string }>;
753+
} = JSON5.parse(updatedConfig);
754+
t.deepEqual(
755+
parsed.upgrades.map((upgrade) => upgrade.version),
756+
["1.60", "1.61"],
757+
);
758+
});
759+
695760
test("createUpgradeEntry uses top-level url and integrity for a single target 0 file", (t) => {
696761
const entry = createUpgradeEntry({
697762
version: "1.61",

yarn.lock

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ __metadata:
10681068
"@typescript-eslint/parser": ^8.43.0
10691069
"@zwave-js/firmware-integrity": ^1.0.2
10701070
ava: ^4.3.3
1071+
comment-json: ^4.6.2
10711072
esbuild: ^0.15.7
10721073
esbuild-register: ^3.3.3
10731074
eslint: ^9.35.0
@@ -1289,6 +1290,13 @@ __metadata:
12891290
languageName: node
12901291
linkType: hard
12911292

1293+
"array-timsort@npm:^1.0.3":
1294+
version: 1.0.3
1295+
resolution: "array-timsort@npm:1.0.3"
1296+
checksum: fd4b5b0911214bdc8b5699ed10d309685551b518b3819c611c967cff59b87aee01cf591a10e36a3f14dbff696984bd6682b845f6fdbf1217195e910f241a4f78
1297+
languageName: node
1298+
linkType: hard
1299+
12921300
"array-union@npm:^2.1.0":
12931301
version: 2.1.0
12941302
resolution: "array-union@npm:2.1.0"
@@ -1738,6 +1746,16 @@ __metadata:
17381746
languageName: node
17391747
linkType: hard
17401748

1749+
"comment-json@npm:^4.6.2":
1750+
version: 4.6.2
1751+
resolution: "comment-json@npm:4.6.2"
1752+
dependencies:
1753+
array-timsort: ^1.0.3
1754+
esprima: ^4.0.1
1755+
checksum: d235957843363c562ce6df85dd17cabe908e515ff8d7641cc90fdda624c1d3b73a85aec1bfe6a6601c3cc6ce8132aaa5dc35f99b0404d26c0a1f119fe97534ff
1756+
languageName: node
1757+
linkType: hard
1758+
17411759
"common-path-prefix@npm:^3.0.0":
17421760
version: 3.0.0
17431761
resolution: "common-path-prefix@npm:3.0.0"
@@ -2509,7 +2527,7 @@ __metadata:
25092527
languageName: node
25102528
linkType: hard
25112529

2512-
"esprima@npm:^4.0.0":
2530+
"esprima@npm:^4.0.0, esprima@npm:^4.0.1":
25132531
version: 4.0.1
25142532
resolution: "esprima@npm:4.0.1"
25152533
bin:

0 commit comments

Comments
 (0)