Skip to content

Commit 9e4981d

Browse files
authored
Merge pull request #2 from ccofallen/feat/cross-platform-desktop-release
Fix native Linux and macOS release packaging
2 parents db292ee + e46f38b commit 9e4981d

6 files changed

Lines changed: 101 additions & 25 deletions

.github/workflows/build-desktop.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ jobs:
4646
- id: mac-arm64
4747
target: mac-arm64
4848
os: macos-latest
49-
command: npm run desktop:pack:mac -- --arm64
49+
command: npm run desktop:pack -- --mac --arm64
5050
artifact: release/*-mac-arm64.dmg
5151
- id: mac-x64
5252
target: mac-x64
5353
os: macos-latest
54-
command: npm run desktop:pack:mac -- --x64
54+
command: npm run desktop:pack -- --mac --x64
5555
artifact: release/*-mac-x64.dmg
5656
- id: windows-x64
5757
target: windows-x64

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
"url": "https://github.com/ccofallen/codex-pet-pause.git"
1010
},
1111
"homepage": "https://ccofallen.github.io/codex-pet-pause/",
12+
"author": {
13+
"name": "ccofallen",
14+
"email": "ccofallen@users.noreply.github.com"
15+
},
1216
"type": "module",
1317
"engines": {
1418
"node": ">=22.12.0"
@@ -45,7 +49,7 @@
4549
"desktop:launch:dev": "concurrently -k \"npm run desktop:dev:server\" \"wait-on tcp:5173 && ./node_modules/.bin/electron . --dev\"",
4650
"desktop:package": "npm run desktop:pack",
4751
"desktop:pack": "npm run desktop:build && electron-builder --publish=never",
48-
"desktop:pack:mac": "npm run desktop:pack -- --mac",
52+
"desktop:pack:mac": "npm run desktop:pack -- --mac --arm64 --x64",
4953
"desktop:pack:win": "npm run desktop:pack -- --win",
5054
"desktop:pack:linux": "npm run desktop:pack -- --linux",
5155
"test:desktop-release-config": "node --test scripts/verify-desktop-release-config.test.mjs",
@@ -112,11 +116,7 @@
112116
"icon": "build/icons/icon.icns",
113117
"target": [
114118
{
115-
"target": "dmg",
116-
"arch": [
117-
"arm64",
118-
"x64"
119-
]
119+
"target": "dmg"
120120
}
121121
],
122122
"artifactName": "Codex-Pet-Pause-${version}-mac-${arch}.${ext}"
@@ -135,6 +135,7 @@
135135
},
136136
"linux": {
137137
"icon": "build/icons/png",
138+
"maintainer": "ccofallen <ccofallen@users.noreply.github.com>",
138139
"category": "Utility",
139140
"target": [
140141
{
@@ -150,7 +151,7 @@
150151
]
151152
}
152153
],
153-
"artifactName": "Codex-Pet-Pause-${version}-linux-${arch}.${ext}"
154+
"artifactName": "Codex-Pet-Pause-${version}-linux-x64.${ext}"
154155
}
155156
}
156157
}

scripts/verify-desktop-release-config.mjs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ const expected = {
88
macIcon: 'build/icons/icon.icns',
99
winIcon: 'build/icons/icon.ico',
1010
linuxIcon: 'build/icons/png',
11+
authorName: 'ccofallen',
12+
authorEmail: 'ccofallen@users.noreply.github.com',
13+
linuxMaintainer: 'ccofallen <ccofallen@users.noreply.github.com>',
14+
localMacPackageCommand: 'npm run desktop:pack -- --mac --arm64 --x64',
1115
macArtifactName: 'Codex-Pet-Pause-${version}-mac-${arch}.${ext}',
1216
winArtifactName: 'Codex-Pet-Pause-${version}-windows-${arch}.${ext}',
13-
linuxArtifactName: 'Codex-Pet-Pause-${version}-linux-${arch}.${ext}',
17+
linuxArtifactName: 'Codex-Pet-Pause-${version}-linux-x64.${ext}',
1418
};
1519

1620
function targetEntriesFor(platform, target) {
@@ -27,6 +31,17 @@ function hasExactlyArchitectures(architectures, expectedArchitectures) {
2731
export function verifyDesktopReleaseConfig(packageJson, fileExists = existsSync) {
2832
const failures = [];
2933
const build = packageJson.build ?? {};
34+
if (
35+
packageJson.author?.name !== expected.authorName
36+
|| packageJson.author?.email !== expected.authorEmail
37+
) {
38+
failures.push(
39+
`author must be ${expected.authorName} <${expected.authorEmail}>`,
40+
);
41+
}
42+
if (build.linux?.maintainer !== expected.linuxMaintainer) {
43+
failures.push(`Linux maintainer must be ${expected.linuxMaintainer}`);
44+
}
3045
if (build.appId !== expected.appId) failures.push(`appId must be ${expected.appId}`);
3146
if (build.productName !== expected.productName) failures.push(`productName must be ${expected.productName}`);
3247

@@ -36,8 +51,11 @@ export function verifyDesktopReleaseConfig(packageJson, fileExists = existsSync)
3651
failures.push('macOS targets must be exactly dmg');
3752
failures.push('macOS targets must be exactly one dmg entry');
3853
}
39-
if (!hasExactlyArchitectures(macDmgEntries[0]?.arch, ['arm64', 'x64'])) {
40-
failures.push('macOS DMG must target exactly arm64 and x64');
54+
if (macDmgEntries[0]?.arch !== undefined) {
55+
failures.push('macOS architecture must be selected by the command, not embedded in build.mac.target');
56+
}
57+
if (packageJson.scripts?.['desktop:pack:mac'] !== expected.localMacPackageCommand) {
58+
failures.push(`local mac package command must be ${expected.localMacPackageCommand}`);
4159
}
4260
const winTargetEntries = build.win?.target ?? [];
4361
const winNsisEntries = targetEntriesFor(build.win, 'nsis');

scripts/verify-desktop-release-config.test.mjs

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,20 @@ import test from 'node:test';
33
import { verifyDesktopReleaseConfig } from './verify-desktop-release-config.mjs';
44

55
const validPackage = {
6+
author: {
7+
name: 'ccofallen',
8+
email: 'ccofallen@users.noreply.github.com',
9+
},
10+
scripts: {
11+
'desktop:pack': 'npm run desktop:build && electron-builder --publish=never',
12+
'desktop:pack:mac': 'npm run desktop:pack -- --mac --arm64 --x64',
13+
},
614
build: {
715
appId: 'io.elevenlabs.codexpetpause',
816
productName: 'Codex Pet Pause',
917
mac: {
1018
icon: 'build/icons/icon.icns',
11-
target: [{ target: 'dmg', arch: ['arm64', 'x64'] }],
19+
target: [{ target: 'dmg' }],
1220
artifactName: 'Codex-Pet-Pause-${version}-mac-${arch}.${ext}',
1321
},
1422
win: {
@@ -18,11 +26,12 @@ const validPackage = {
1826
},
1927
linux: {
2028
icon: 'build/icons/png',
29+
maintainer: 'ccofallen <ccofallen@users.noreply.github.com>',
2130
target: [
2231
{ target: 'AppImage', arch: ['x64'] },
2332
{ target: 'deb', arch: ['x64'] },
2433
],
25-
artifactName: 'Codex-Pet-Pause-${version}-linux-${arch}.${ext}',
34+
artifactName: 'Codex-Pet-Pause-${version}-linux-x64.${ext}',
2635
},
2736
},
2837
};
@@ -40,7 +49,45 @@ test('accepts the approved desktop release contract', () => {
4049
);
4150
assert.equal(
4251
validPackage.build.linux.artifactName,
43-
'Codex-Pet-Pause-${version}-linux-${arch}.${ext}',
52+
'Codex-Pet-Pause-${version}-linux-x64.${ext}',
53+
);
54+
});
55+
56+
test('requires the approved privacy-preserving Linux maintainer identity', () => {
57+
const missingAuthorEmail = structuredClone(validPackage);
58+
delete missingAuthorEmail.author.email;
59+
const missingMaintainer = structuredClone(validPackage);
60+
delete missingMaintainer.build.linux.maintainer;
61+
const invalidMaintainer = structuredClone(validPackage);
62+
invalidMaintainer.build.linux.maintainer = 'ccofallen';
63+
64+
assert.ok(
65+
verifyDesktopReleaseConfig(missingAuthorEmail, () => true)
66+
.some((failure) => failure.includes('author')),
67+
);
68+
assert.ok(
69+
verifyDesktopReleaseConfig(missingMaintainer, () => true)
70+
.some((failure) => failure.includes('Linux maintainer')),
71+
);
72+
assert.ok(
73+
verifyDesktopReleaseConfig(invalidMaintainer, () => true)
74+
.some((failure) => failure.includes('Linux maintainer')),
75+
);
76+
});
77+
78+
test('keeps mac config architecture-neutral and the local mac command explicitly dual-arch', () => {
79+
const embeddedArchitectures = structuredClone(validPackage);
80+
embeddedArchitectures.build.mac.target[0].arch = ['arm64', 'x64'];
81+
const ambiguousLocalCommand = structuredClone(validPackage);
82+
ambiguousLocalCommand.scripts['desktop:pack:mac'] = 'npm run desktop:pack -- --mac';
83+
84+
assert.ok(
85+
verifyDesktopReleaseConfig(embeddedArchitectures, () => true)
86+
.some((failure) => failure.includes('macOS architecture must be selected by the command')),
87+
);
88+
assert.ok(
89+
verifyDesktopReleaseConfig(ambiguousLocalCommand, () => true)
90+
.some((failure) => failure.includes('local mac package command')),
4491
);
4592
});
4693

@@ -69,7 +116,7 @@ test('rejects artifact names that differ from the release contract', () => {
69116
const invalid = structuredClone(validPackage);
70117
invalid.build.mac.artifactName = 'Codex-Pet-Pause-${version}-mac.${ext}';
71118
invalid.build.win.artifactName = 'Codex-Pet-Pause-${version}-win-${arch}.${ext}';
72-
invalid.build.linux.artifactName = 'Codex-Pet-Pause-${version}-linux-${version}.${ext}';
119+
invalid.build.linux.artifactName = 'Codex-Pet-Pause-${version}-linux-${arch}.${ext}';
73120
const failures = verifyDesktopReleaseConfig(invalid, () => true);
74121
assert.ok(failures.some((failure) => failure.includes('macOS artifact name')));
75122
assert.ok(failures.some((failure) => failure.includes('Windows artifact name')));
@@ -89,20 +136,20 @@ test('rejects unintended extra target formats on every platform', () => {
89136

90137
test('rejects unsupported or duplicate target architectures', () => {
91138
const invalid = structuredClone(validPackage);
92-
invalid.build.mac.target[0].arch.push('arm64');
139+
invalid.build.mac.target[0].arch = ['arm64'];
93140
invalid.build.win.target[0].arch.push('arm64');
94141
invalid.build.linux.target[0].arch.push('arm64');
95142
invalid.build.linux.target[1].arch.push('x64');
96143
const failures = verifyDesktopReleaseConfig(invalid, () => true);
97-
assert.ok(failures.some((failure) => failure.includes('macOS DMG must target exactly arm64 and x64')));
144+
assert.ok(failures.some((failure) => failure.includes('macOS architecture must be selected by the command')));
98145
assert.ok(failures.some((failure) => failure.includes('Windows NSIS must target exactly x64')));
99146
assert.ok(failures.some((failure) => failure.includes('Linux AppImage must target exactly x64')));
100147
assert.ok(failures.some((failure) => failure.includes('Linux DEB must target exactly x64')));
101148
});
102149

103150
test('rejects duplicate expected target definitions', () => {
104151
const invalid = structuredClone(validPackage);
105-
invalid.build.mac.target.push({ target: 'dmg', arch: ['arm64', 'x64'] });
152+
invalid.build.mac.target.push({ target: 'dmg' });
106153
invalid.build.win.target.push({ target: 'nsis', arch: ['x64'] });
107154
invalid.build.linux.target.push({ target: 'AppImage', arch: ['x64'] });
108155
const failures = verifyDesktopReleaseConfig(invalid, () => true);
@@ -111,9 +158,8 @@ test('rejects duplicate expected target definitions', () => {
111158
assert.ok(failures.some((failure) => failure.includes('Linux targets must be exactly one AppImage entry and one deb entry')));
112159
});
113160

114-
test('accepts approved target and architecture entries in any order', () => {
161+
test('accepts approved target entries in any order', () => {
115162
const reordered = structuredClone(validPackage);
116-
reordered.build.mac.target[0].arch = ['x64', 'arm64'];
117163
reordered.build.linux.target.reverse();
118164
const failures = verifyDesktopReleaseConfig(reordered, () => true);
119165
assert.deepEqual(failures, []);

scripts/verify-desktop-workflow.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { fileURLToPath } from 'node:url';
33
import { parse } from 'yaml';
44

55
const expectedPackages = [
6-
['mac-arm64', 'mac-arm64', 'macos-latest', 'npm run desktop:pack:mac -- --arm64', 'release/*-mac-arm64.dmg'],
7-
['mac-x64', 'mac-x64', 'macos-latest', 'npm run desktop:pack:mac -- --x64', 'release/*-mac-x64.dmg'],
6+
['mac-arm64', 'mac-arm64', 'macos-latest', 'npm run desktop:pack -- --mac --arm64', 'release/*-mac-arm64.dmg'],
7+
['mac-x64', 'mac-x64', 'macos-latest', 'npm run desktop:pack -- --mac --x64', 'release/*-mac-x64.dmg'],
88
['windows-x64', 'windows-x64', 'windows-latest', 'npm run desktop:pack:win -- --x64', 'release/*-windows-x64.exe'],
99
['linux-x64', 'linux-x64', 'ubuntu-latest', 'npm run desktop:pack:linux -- --x64', 'release/*-linux-x64.AppImage\nrelease/*-linux-x64.deb\n'],
1010
];

scripts/verify-desktop-workflow.test.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ const validWorkflow = {
5454
id: 'mac-arm64',
5555
target: 'mac-arm64',
5656
os: 'macos-latest',
57-
command: 'npm run desktop:pack:mac -- --arm64',
57+
command: 'npm run desktop:pack -- --mac --arm64',
5858
artifact: 'release/*-mac-arm64.dmg',
5959
},
6060
{
6161
id: 'mac-x64',
6262
target: 'mac-x64',
6363
os: 'macos-latest',
64-
command: 'npm run desktop:pack:mac -- --x64',
64+
command: 'npm run desktop:pack -- --mac --x64',
6565
artifact: 'release/*-mac-x64.dmg',
6666
},
6767
{
@@ -251,6 +251,17 @@ test('rejects a workflow with a missing native package target or command', () =>
251251
assert.ok(failures.some((failure) => failure.includes('linux-x64')));
252252
});
253253

254+
test('rejects mac matrix commands that inherit the local dual-arch wrapper', () => {
255+
const invalid = structuredClone(validWorkflow);
256+
invalid.jobs.package.strategy.matrix.include[0].command =
257+
'npm run desktop:pack:mac -- --arm64';
258+
invalid.jobs.package.strategy.matrix.include[1].command =
259+
'npm run desktop:pack:mac -- --x64';
260+
const failures = verifyDesktopWorkflow(invalid);
261+
assert.ok(failures.some((failure) => failure.includes('mac-arm64 command')));
262+
assert.ok(failures.some((failure) => failure.includes('mac-x64 command')));
263+
});
264+
254265
test('rejects packaging without npm ci or a protected matrix artifact upload', () => {
255266
const invalid = structuredClone(validWorkflow);
256267
invalid.jobs.package.steps = invalid.jobs.package.steps.filter((step) => step.run !== 'npm ci');

0 commit comments

Comments
 (0)