@cats-inc/cats-platform 0.1.1 → 0.1.5
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/.env.example +2 -2
- package/README.md +6 -0
- package/build/desktop/bootstrapPage.d.ts.map +1 -1
- package/build/desktop/bootstrapPage.js +81 -4
- package/build/desktop/bootstrapPage.js.map +1 -1
- package/build/desktop/config.d.ts +11 -1
- package/build/desktop/config.d.ts.map +1 -1
- package/build/desktop/config.js +5 -2
- package/build/desktop/config.js.map +1 -1
- package/build/desktop/contracts.d.ts +66 -11
- package/build/desktop/contracts.d.ts.map +1 -1
- package/build/desktop/contracts.js +50 -1
- package/build/desktop/contracts.js.map +1 -1
- package/build/desktop/hostState.d.ts +4 -3
- package/build/desktop/hostState.d.ts.map +1 -1
- package/build/desktop/hostState.js +22 -30
- package/build/desktop/hostState.js.map +1 -1
- package/build/desktop/hostVersion.d.ts +1 -0
- package/build/desktop/hostVersion.d.ts.map +1 -1
- package/build/desktop/hostVersion.js +29 -5
- package/build/desktop/hostVersion.js.map +1 -1
- package/build/desktop/main.js +234 -37
- package/build/desktop/main.js.map +1 -1
- package/build/desktop/packaging.d.ts.map +1 -1
- package/build/desktop/packaging.js +10 -4
- package/build/desktop/packaging.js.map +1 -1
- package/build/desktop/preload.cjs +29 -0
- package/build/desktop/preload.cjs.map +1 -1
- package/build/desktop/processSupervisor.d.ts +2 -0
- package/build/desktop/processSupervisor.d.ts.map +1 -1
- package/build/desktop/processSupervisor.js +28 -13
- package/build/desktop/processSupervisor.js.map +1 -1
- package/build/desktop/readiness.d.ts +2 -2
- package/build/desktop/readiness.d.ts.map +1 -1
- package/build/desktop/readiness.js +2 -2
- package/build/desktop/readiness.js.map +1 -1
- package/build/desktop/releaseDescriptor.d.ts +85 -0
- package/build/desktop/releaseDescriptor.d.ts.map +1 -0
- package/build/desktop/releaseDescriptor.js +246 -0
- package/build/desktop/releaseDescriptor.js.map +1 -0
- package/build/desktop/tray.d.ts +1 -0
- package/build/desktop/tray.d.ts.map +1 -1
- package/build/desktop/tray.js +26 -0
- package/build/desktop/tray.js.map +1 -1
- package/build/desktop/trayMenu.d.ts +22 -1
- package/build/desktop/trayMenu.d.ts.map +1 -1
- package/build/desktop/trayMenu.js +61 -0
- package/build/desktop/trayMenu.js.map +1 -1
- package/build/desktop/updateInstallHandoff.d.ts +30 -0
- package/build/desktop/updateInstallHandoff.d.ts.map +1 -0
- package/build/desktop/updateInstallHandoff.js +39 -0
- package/build/desktop/updateInstallHandoff.js.map +1 -0
- package/build/desktop/updateIpc.d.ts +42 -0
- package/build/desktop/updateIpc.d.ts.map +1 -0
- package/build/desktop/updateIpc.js +71 -0
- package/build/desktop/updateIpc.js.map +1 -0
- package/build/desktop/updateManager.d.ts +76 -0
- package/build/desktop/updateManager.d.ts.map +1 -0
- package/build/desktop/updateManager.js +342 -0
- package/build/desktop/updateManager.js.map +1 -0
- package/build/desktop/updateNotifications.d.ts +64 -0
- package/build/desktop/updateNotifications.d.ts.map +1 -0
- package/build/desktop/updateNotifications.js +143 -0
- package/build/desktop/updateNotifications.js.map +1 -0
- package/build/desktop/updaterAdapter.d.ts +72 -0
- package/build/desktop/updaterAdapter.d.ts.map +1 -0
- package/build/desktop/updaterAdapter.js +198 -0
- package/build/desktop/updaterAdapter.js.map +1 -0
- package/build/renderer/assets/{ActiveSessionPermissionChip-BYr8cDpb.js → ActiveSessionPermissionChip-BoDkKkmi.js} +1 -1
- package/build/renderer/assets/{App-BbuTWT6Y.js → App-Bs1eRarv.js} +1 -1
- package/build/renderer/assets/{App-B13AzNko.js → App-C4S1Wa9f.js} +1 -1
- package/build/renderer/assets/{App-n-RtGh42.js → App-CNJj5gi9.js} +1 -1
- package/build/renderer/assets/{WorkspaceAppRoutes-BktWrQK-.js → WorkspaceAppRoutes-CFAuhWGF.js} +1 -1
- package/build/renderer/assets/app-uJJx-k1S.js +43 -0
- package/build/renderer/assets/{conversationSidebarRecentEntries-BNPz3X1V.js → conversationSidebarRecentEntries-CPGopayy.js} +1 -1
- package/build/renderer/assets/{useDraftSessionChips-3z_Je4ju.js → useDraftSessionChips-BsOdYfWw.js} +1 -1
- package/build/renderer/index.html +1 -1
- package/build/server/app/renderer/settings/settingsDesktopErrorLabels.d.ts +5 -0
- package/build/server/app/renderer/settings/settingsDesktopErrorLabels.d.ts.map +1 -0
- package/build/server/app/renderer/settings/settingsDesktopErrorLabels.js +16 -0
- package/build/server/app/renderer/settings/settingsDesktopErrorLabels.js.map +1 -0
- package/build/server/app/renderer/settings/settingsDesktopPreferences.d.ts +11 -0
- package/build/server/app/renderer/settings/settingsDesktopPreferences.d.ts.map +1 -0
- package/build/server/app/renderer/settings/settingsDesktopPreferences.js +30 -0
- package/build/server/app/renderer/settings/settingsDesktopPreferences.js.map +1 -0
- package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.d.ts +31 -0
- package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.d.ts.map +1 -0
- package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.js +101 -0
- package/build/server/app/renderer/settings/settingsDesktopUpdateLabels.js.map +1 -0
- package/build/server/app/server/index.js +1 -1
- package/build/server/app/server/index.js.map +1 -1
- package/build/server/app/server/platformSetupRoutes.d.ts.map +1 -1
- package/build/server/app/server/platformSetupRoutes.js +42 -5
- package/build/server/app/server/platformSetupRoutes.js.map +1 -1
- package/build/server/app/server/requestRouter.d.ts.map +1 -1
- package/build/server/app/server/requestRouter.js +2 -0
- package/build/server/app/server/requestRouter.js.map +1 -1
- package/build/server/design/components/runtimeChips.d.ts +1 -1
- package/build/server/design/components/runtimeChips.d.ts.map +1 -1
- package/build/server/design/components/runtimeChips.js +4 -1
- package/build/server/design/components/runtimeChips.js.map +1 -1
- package/build/server/index.js +12 -1
- package/build/server/index.js.map +1 -1
- package/build/server/platform/auth/sessionSecretProvisioning.d.ts +23 -0
- package/build/server/platform/auth/sessionSecretProvisioning.d.ts.map +1 -0
- package/build/server/platform/auth/sessionSecretProvisioning.js +291 -0
- package/build/server/platform/auth/sessionSecretProvisioning.js.map +1 -0
- package/build/server/products/code/livePreview/realProcessAdapter.d.ts.map +1 -1
- package/build/server/products/code/livePreview/realProcessAdapter.js +7 -1
- package/build/server/products/code/livePreview/realProcessAdapter.js.map +1 -1
- package/build/server/products/code/state/appExport.d.ts.map +1 -1
- package/build/server/products/code/state/appExport.js +2 -0
- package/build/server/products/code/state/appExport.js.map +1 -1
- package/build/server/shared/desktopRecoveryBridge.d.ts +46 -0
- package/build/server/shared/desktopRecoveryBridge.d.ts.map +1 -1
- package/build/server/shared/desktopRecoveryBridge.js +20 -0
- package/build/server/shared/desktopRecoveryBridge.js.map +1 -1
- package/build/server/shared/i18n/catalogs/en.d.ts.map +1 -1
- package/build/server/shared/i18n/catalogs/en.js +37 -0
- package/build/server/shared/i18n/catalogs/en.js.map +1 -1
- package/build/server/shared/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/build/server/shared/i18n/catalogs/zh-TW.js +35 -0
- package/build/server/shared/i18n/catalogs/zh-TW.js.map +1 -1
- package/build/server/shared/i18n/index.d.ts +35 -0
- package/build/server/shared/i18n/index.d.ts.map +1 -1
- package/build/server/shared/i18n/messageKeys.d.ts +35 -0
- package/build/server/shared/i18n/messageKeys.d.ts.map +1 -1
- package/build/server/shared/i18n/messageKeys.js +35 -0
- package/build/server/shared/i18n/messageKeys.js.map +1 -1
- package/build/server/shared/loadProjectEnvFile.d.ts.map +1 -1
- package/build/server/shared/loadProjectEnvFile.js +18 -19
- package/build/server/shared/loadProjectEnvFile.js.map +1 -1
- package/build/server/shared/platformChildProcessEnv.d.ts +2 -0
- package/build/server/shared/platformChildProcessEnv.d.ts.map +1 -0
- package/build/server/shared/platformChildProcessEnv.js +22 -0
- package/build/server/shared/platformChildProcessEnv.js.map +1 -0
- package/package.json +19 -27
- package/scripts/build-desktop-installer.mjs +438 -37
- package/scripts/build-test-ui.mjs +13 -2
- package/scripts/generate-desktop-release-descriptor.mjs +258 -0
- package/scripts/validate-release-assets.mjs +422 -0
- package/scripts/validate-release-version.mjs +217 -0
- package/scripts/windows/Test-WindowsInstallerSmoke.ps1 +55 -1
- package/build/desktop/update.d.ts +0 -24
- package/build/desktop/update.d.ts.map +0 -1
- package/build/desktop/update.js +0 -173
- package/build/desktop/update.js.map +0 -1
- package/build/renderer/assets/app-DnVQMpW_.js +0 -43
- package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.d.ts +0 -5
- package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.d.ts.map +0 -1
- package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.js +0 -11
- package/build/server/app/renderer/settings/settingsDesktopStartupErrorLabels.js.map +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Script: generate-desktop-release-descriptor.mjs
|
|
4
|
+
// Description: Emit the non-secret provenance descriptor that marks a packaged
|
|
5
|
+
// build as an official tag-gated desktop release. The desktop
|
|
6
|
+
// host refuses to advertise self-update without it.
|
|
7
|
+
//
|
|
8
|
+
// Usage: node scripts/generate-desktop-release-descriptor.mjs [options]
|
|
9
|
+
//
|
|
10
|
+
// Options:
|
|
11
|
+
// --tag <vX.Y.Z> Release tag. Defaults to GITHUB_REF_NAME.
|
|
12
|
+
// --commit <sha> Source commit. Defaults to GITHUB_SHA.
|
|
13
|
+
// --repository <o/r> GitHub owner/repo. Defaults to GITHUB_REPOSITORY.
|
|
14
|
+
// --platform <windows|macos|linux>
|
|
15
|
+
// Target platform. Defaults to the current platform.
|
|
16
|
+
// --output <path> Descriptor path. Defaults to the packaged location.
|
|
17
|
+
// --help Show this help text.
|
|
18
|
+
//
|
|
19
|
+
// The descriptor contains no secrets. It is provenance only: which tag, which
|
|
20
|
+
// commit, which platform, which channel, and which update provider produced
|
|
21
|
+
// the package. Local packaging never generates it.
|
|
22
|
+
|
|
23
|
+
import process from 'node:process';
|
|
24
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
25
|
+
import { dirname, resolve } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { parseStableReleaseTag } from './validate-release-version.mjs';
|
|
29
|
+
|
|
30
|
+
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
31
|
+
const PROJECT_ROOT = resolve(dirname(SCRIPT_PATH), '..');
|
|
32
|
+
|
|
33
|
+
// The descriptor ships inside build/desktop, which package.json already lists
|
|
34
|
+
// in build.files, so it lands in the asar without extra packaging rules.
|
|
35
|
+
export const DESCRIPTOR_RELATIVE_PATH = 'build/desktop/release-descriptor.json';
|
|
36
|
+
|
|
37
|
+
export const DESCRIPTOR_SCHEMA_VERSION = 1;
|
|
38
|
+
|
|
39
|
+
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
40
|
+
const GITHUB_REPOSITORY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
41
|
+
|
|
42
|
+
const PLATFORM_ALIASES = {
|
|
43
|
+
windows: 'windows',
|
|
44
|
+
win32: 'windows',
|
|
45
|
+
win: 'windows',
|
|
46
|
+
macos: 'macos',
|
|
47
|
+
darwin: 'macos',
|
|
48
|
+
mac: 'macos',
|
|
49
|
+
linux: 'linux',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function printHelp() {
|
|
53
|
+
process.stdout.write(`Usage: node scripts/generate-desktop-release-descriptor.mjs [options]
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
--tag <vX.Y.Z> Release tag. Defaults to GITHUB_REF_NAME.
|
|
57
|
+
--commit <sha> Source commit. Defaults to GITHUB_SHA.
|
|
58
|
+
--repository <o/r> GitHub owner/repo. Defaults to GITHUB_REPOSITORY.
|
|
59
|
+
--platform <windows|macos|linux>
|
|
60
|
+
Target platform. Defaults to the current platform.
|
|
61
|
+
--output <path> Descriptor path. Defaults to ${DESCRIPTOR_RELATIVE_PATH}.
|
|
62
|
+
--help Show this help text.
|
|
63
|
+
|
|
64
|
+
Writes the non-secret official release descriptor consumed by the desktop
|
|
65
|
+
update capability check. Local packaging must not run this script.
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Explicit rather than derived from the flag name, because --runtime-commit
|
|
70
|
+
// does not slice into a valid camelCase key.
|
|
71
|
+
const VALUE_FLAGS = {
|
|
72
|
+
'--tag': 'tag',
|
|
73
|
+
'--commit': 'commit',
|
|
74
|
+
'--repository': 'repository',
|
|
75
|
+
'--platform': 'platform',
|
|
76
|
+
'--runtime-commit': 'runtimeCommit',
|
|
77
|
+
'--kind': 'kind',
|
|
78
|
+
'--output': 'output',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const RELEASE_KINDS = ['official', 'preview'];
|
|
82
|
+
|
|
83
|
+
export function resolveDescriptorPlatform(value) {
|
|
84
|
+
const candidate = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
85
|
+
return PLATFORM_ALIASES[candidate] ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseArgs(argv, env = process.env) {
|
|
89
|
+
const options = {
|
|
90
|
+
help: false,
|
|
91
|
+
tag: (env.GITHUB_REF_NAME ?? '').trim(),
|
|
92
|
+
commit: (env.GITHUB_SHA ?? '').trim(),
|
|
93
|
+
repository: (env.GITHUB_REPOSITORY ?? '').trim(),
|
|
94
|
+
platform: env.CATS_DESKTOP_RELEASE_PLATFORM ?? process.platform,
|
|
95
|
+
runtimeCommit: (env.CATS_DESKTOP_RUNTIME_COMMIT ?? '').trim(),
|
|
96
|
+
// Official unless the caller says otherwise, so a missing flag can never
|
|
97
|
+
// silently downgrade a stable release into an unsigned preview.
|
|
98
|
+
kind: (env.CATS_DESKTOP_RELEASE_KIND ?? 'official').trim(),
|
|
99
|
+
output: DESCRIPTOR_RELATIVE_PATH,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
103
|
+
const value = argv[index];
|
|
104
|
+
if (value === '--help' || value === '-h') {
|
|
105
|
+
return { ...options, help: true };
|
|
106
|
+
}
|
|
107
|
+
const key = VALUE_FLAGS[value];
|
|
108
|
+
if (key !== undefined) {
|
|
109
|
+
options[key] = (argv[index + 1] ?? '').trim();
|
|
110
|
+
index += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Unknown option: ${value}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return options;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Builds the descriptor and reports every invalid input at once. Returning
|
|
121
|
+
* problems instead of throwing keeps the release workflow output readable when
|
|
122
|
+
* more than one environment variable is missing.
|
|
123
|
+
*/
|
|
124
|
+
export function buildReleaseDescriptor({
|
|
125
|
+
tag,
|
|
126
|
+
commit,
|
|
127
|
+
repository,
|
|
128
|
+
platform,
|
|
129
|
+
runtimeCommit,
|
|
130
|
+
kind = 'official',
|
|
131
|
+
generatedAt = null,
|
|
132
|
+
} = {}) {
|
|
133
|
+
const problems = [];
|
|
134
|
+
|
|
135
|
+
const normalizedKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
|
|
136
|
+
if (!RELEASE_KINDS.includes(normalizedKind)) {
|
|
137
|
+
problems.push({
|
|
138
|
+
code: 'descriptor_kind_invalid',
|
|
139
|
+
message: `Release kind '${kind ?? ''}' is not official or preview.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const parsedTag = parseStableReleaseTag(tag);
|
|
144
|
+
if (!parsedTag.ok) {
|
|
145
|
+
problems.push({ code: `descriptor_${parsedTag.code}`, message: parsedTag.message });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const normalizedCommit = typeof commit === 'string' ? commit.trim().toLowerCase() : '';
|
|
149
|
+
if (!COMMIT_SHA.test(normalizedCommit)) {
|
|
150
|
+
problems.push({
|
|
151
|
+
code: 'descriptor_commit_invalid',
|
|
152
|
+
message: `Source commit '${commit ?? ''}' is not a full 40-character sha.`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const normalizedRepository = typeof repository === 'string' ? repository.trim() : '';
|
|
157
|
+
if (!GITHUB_REPOSITORY.test(normalizedRepository)) {
|
|
158
|
+
problems.push({
|
|
159
|
+
code: 'descriptor_repository_invalid',
|
|
160
|
+
message: `Repository '${repository ?? ''}' is not in owner/name form.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const normalizedPlatform = resolveDescriptorPlatform(platform);
|
|
165
|
+
if (normalizedPlatform === null) {
|
|
166
|
+
problems.push({
|
|
167
|
+
code: 'descriptor_platform_invalid',
|
|
168
|
+
message: `Platform '${platform ?? ''}' is not windows, macos, or linux.`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The packaged runtime is a separate repository, so the platform commit
|
|
173
|
+
// alone does not identify what shipped. Recording it makes a re-run of the
|
|
174
|
+
// same Cats tag verifiable rather than merely hopeful.
|
|
175
|
+
const normalizedRuntimeCommit = typeof runtimeCommit === 'string'
|
|
176
|
+
? runtimeCommit.trim().toLowerCase()
|
|
177
|
+
: '';
|
|
178
|
+
if (!COMMIT_SHA.test(normalizedRuntimeCommit)) {
|
|
179
|
+
problems.push({
|
|
180
|
+
code: 'descriptor_runtime_commit_invalid',
|
|
181
|
+
message: `Runtime commit '${runtimeCommit ?? ''}' is not a full 40-character sha.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (problems.length > 0) {
|
|
186
|
+
return { ok: false, descriptor: null, problems };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
problems: [],
|
|
192
|
+
descriptor: {
|
|
193
|
+
schemaVersion: DESCRIPTOR_SCHEMA_VERSION,
|
|
194
|
+
kind: normalizedKind,
|
|
195
|
+
tag: parsedTag.tag,
|
|
196
|
+
version: parsedTag.version,
|
|
197
|
+
commit: normalizedCommit,
|
|
198
|
+
platform: normalizedPlatform,
|
|
199
|
+
// SPEC-111 restricts Phase 1 publishing to the stable channel, so the
|
|
200
|
+
// descriptor states it rather than accepting it as an input.
|
|
201
|
+
channel: 'stable',
|
|
202
|
+
provider: 'github_release',
|
|
203
|
+
repository: normalizedRepository,
|
|
204
|
+
runtimeCommit: normalizedRuntimeCommit,
|
|
205
|
+
generatedAt,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function serializeReleaseDescriptor(descriptor) {
|
|
211
|
+
return `${JSON.stringify(descriptor, null, 2)}\n`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function writeReleaseDescriptor(descriptor, outputPath, projectRoot = PROJECT_ROOT) {
|
|
215
|
+
const absolutePath = resolve(projectRoot, outputPath);
|
|
216
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
217
|
+
await writeFile(absolutePath, serializeReleaseDescriptor(descriptor), 'utf8');
|
|
218
|
+
return absolutePath;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function main() {
|
|
222
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
223
|
+
if (parsed.help) {
|
|
224
|
+
printHelp();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = buildReleaseDescriptor({
|
|
229
|
+
tag: parsed.tag,
|
|
230
|
+
commit: parsed.commit,
|
|
231
|
+
repository: parsed.repository,
|
|
232
|
+
platform: parsed.platform,
|
|
233
|
+
runtimeCommit: parsed.runtimeCommit,
|
|
234
|
+
kind: parsed.kind,
|
|
235
|
+
generatedAt: new Date().toISOString(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
for (const problem of result.problems) {
|
|
240
|
+
process.stderr.write(`[release-descriptor] ${problem.code}: ${problem.message}\n`);
|
|
241
|
+
}
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const writtenPath = await writeReleaseDescriptor(result.descriptor, parsed.output);
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
`[release-descriptor] wrote ${result.descriptor.tag} `
|
|
249
|
+
+ `(${result.descriptor.platform}) to ${writtenPath}\n`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (resolve(process.argv[1] ?? '') === SCRIPT_PATH) {
|
|
254
|
+
void main().catch((error) => {
|
|
255
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Script: validate-release-assets.mjs
|
|
4
|
+
// Description: Verify that every file referenced by the generated update
|
|
5
|
+
// metadata was actually produced, and that each platform's
|
|
6
|
+
// primary user-facing artifact is present, before a draft
|
|
7
|
+
// GitHub Release is published.
|
|
8
|
+
//
|
|
9
|
+
// Usage: node scripts/validate-release-assets.mjs --root <dir>
|
|
10
|
+
//
|
|
11
|
+
// Options:
|
|
12
|
+
// --root <dir> Directory to scan recursively. Defaults to release/.
|
|
13
|
+
// --json Emit the machine-readable result instead of prose.
|
|
14
|
+
// --help Show this help text.
|
|
15
|
+
//
|
|
16
|
+
// Exit codes:
|
|
17
|
+
// 0 every required artifact and every referenced file exists
|
|
18
|
+
// 1 metadata is missing, unreadable, or references a file that was not built
|
|
19
|
+
|
|
20
|
+
import process from 'node:process';
|
|
21
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
22
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
import yaml from 'js-yaml';
|
|
26
|
+
|
|
27
|
+
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
28
|
+
const PROJECT_ROOT = resolve(dirname(SCRIPT_PATH), '..');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* electron-updater publishes one metadata file per platform. Each names the
|
|
32
|
+
* primary artifact the updater will download, so a missing metadata file means
|
|
33
|
+
* that platform cannot self-update even if its installer uploaded fine.
|
|
34
|
+
*/
|
|
35
|
+
export const DESKTOP_RELEASE_MATRIX = [
|
|
36
|
+
{ platform: 'windows', formats: ['nsis'], arches: ['x64'] },
|
|
37
|
+
// macOS ships x64 only; Apple Silicon runs it under Rosetta 2.
|
|
38
|
+
{ platform: 'macos', formats: ['dmg', 'zip'], arches: ['x64'] },
|
|
39
|
+
// Linux ships an arm64 .deb. electron-updater installs it with dpkg, which
|
|
40
|
+
// needs elevation -- unlike the per-user Windows path.
|
|
41
|
+
{ platform: 'linux', formats: ['deb'], arches: ['arm64'] },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const METADATA_BASENAMES = {
|
|
45
|
+
windows: 'latest',
|
|
46
|
+
macos: 'latest-mac',
|
|
47
|
+
linux: 'latest-linux',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derives the channel file each platform will emit.
|
|
52
|
+
*
|
|
53
|
+
* electron-builder suffixes the name with the architecture for anything other
|
|
54
|
+
* than x64, so an arm64 Linux target writes `latest-linux-arm64.yml`. Hardcoding
|
|
55
|
+
* the three unsuffixed names meant a retarget reported the real file as an
|
|
56
|
+
* unknown artifact *and* the expected file as missing -- two errors for one
|
|
57
|
+
* naming rule.
|
|
58
|
+
*/
|
|
59
|
+
export function expectedMetadataFiles(matrix = DESKTOP_RELEASE_MATRIX) {
|
|
60
|
+
return matrix.map((entry) => {
|
|
61
|
+
const base = METADATA_BASENAMES[entry.platform];
|
|
62
|
+
const [arch] = entry.arches;
|
|
63
|
+
return arch === 'x64' || arch === 'universal' ? `${base}.yml` : `${base}-${arch}.yml`;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const UPDATE_METADATA_FILES = expectedMetadataFiles();
|
|
68
|
+
|
|
69
|
+
export const PRIMARY_ARTIFACT_PATTERNS = [
|
|
70
|
+
{ platform: 'windows', pattern: /\.exe$/iu },
|
|
71
|
+
{ platform: 'macos', pattern: /\.dmg$/iu },
|
|
72
|
+
{ platform: 'linux', pattern: /\.deb$/iu },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function printHelp() {
|
|
76
|
+
process.stdout.write(`Usage: node scripts/validate-release-assets.mjs [options]
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--root <dir> Directory to scan recursively. Defaults to release/.
|
|
80
|
+
--json Emit the machine-readable result instead of prose.
|
|
81
|
+
--help Show this help text.
|
|
82
|
+
|
|
83
|
+
Fails when generated update metadata references a file that is not present, or
|
|
84
|
+
when a platform's primary user-facing artifact is missing.
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseArgs(argv) {
|
|
89
|
+
const options = { help: false, root: 'release', json: false };
|
|
90
|
+
|
|
91
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
92
|
+
const value = argv[index];
|
|
93
|
+
if (value === '--help' || value === '-h') {
|
|
94
|
+
return { ...options, help: true };
|
|
95
|
+
}
|
|
96
|
+
if (value === '--root') {
|
|
97
|
+
options.root = (argv[index + 1] ?? '').trim() || 'release';
|
|
98
|
+
index += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (value === '--json') {
|
|
102
|
+
options.json = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Unknown option: ${value}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return options;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Collects the file names referenced by one metadata document. Both the
|
|
113
|
+
* top-level `path` and each `files[].url` are checked, because electron-updater
|
|
114
|
+
* reads whichever the client version supports.
|
|
115
|
+
*/
|
|
116
|
+
export function collectReferencedFiles(document) {
|
|
117
|
+
if (typeof document !== 'object' || document === null) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const referenced = new Set();
|
|
122
|
+
const record = document;
|
|
123
|
+
|
|
124
|
+
if (typeof record.path === 'string' && record.path.trim() !== '') {
|
|
125
|
+
referenced.add(basename(decodeURIComponent(record.path.trim())));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(record.files)) {
|
|
129
|
+
for (const entry of record.files) {
|
|
130
|
+
if (typeof entry?.url === 'string' && entry.url.trim() !== '') {
|
|
131
|
+
referenced.add(basename(decodeURIComponent(entry.url.trim())));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [...referenced];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The declared release set.
|
|
141
|
+
*
|
|
142
|
+
* This is the single source of truth for what an official release may contain.
|
|
143
|
+
* It is not a statement that other formats or architectures are undesirable —
|
|
144
|
+
* it is a statement of what this release publishes, so `--publish always`
|
|
145
|
+
* cannot quietly upload something the release page never promised.
|
|
146
|
+
*
|
|
147
|
+
* Widening the release set is one edit here plus the matching workflow matrix
|
|
148
|
+
* entry. A drift test keeps the two aligned, so adding, say, a Linux arm64
|
|
149
|
+
* AppImage is a deliberate two-line change rather than something the validator
|
|
150
|
+
* refuses on principle.
|
|
151
|
+
*/
|
|
152
|
+
const FORMAT_EXTENSIONS = {
|
|
153
|
+
nsis: ['.exe'],
|
|
154
|
+
dmg: ['.dmg'],
|
|
155
|
+
// The updater archive is named from this repository's artifactName template,
|
|
156
|
+
// which produces Cats-<version>-x64.zip rather than the -mac.zip default some
|
|
157
|
+
// electron-builder setups emit. The name carries no platform marker, so match
|
|
158
|
+
// it by extension and rely on the per-platform format list above.
|
|
159
|
+
zip: ['.zip'],
|
|
160
|
+
AppImage: ['.AppImage'],
|
|
161
|
+
deb: ['.deb'],
|
|
162
|
+
'tar.gz': ['.tar.gz'],
|
|
163
|
+
pkg: ['.pkg'],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// electron-builder spells the same architecture differently per target.
|
|
167
|
+
const ARCH_TOKENS = {
|
|
168
|
+
x64: ['x64', 'x86_64', 'amd64'],
|
|
169
|
+
arm64: ['arm64', 'aarch64'],
|
|
170
|
+
ia32: ['ia32', 'i386'],
|
|
171
|
+
armv7l: ['armv7l'],
|
|
172
|
+
universal: ['universal'],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const ALL_ARCH_TOKENS = Object.values(ARCH_TOKENS).flat();
|
|
176
|
+
|
|
177
|
+
function releasedExtensions(matrix = DESKTOP_RELEASE_MATRIX) {
|
|
178
|
+
return matrix.flatMap((entry) => entry.formats.flatMap(
|
|
179
|
+
(format) => FORMAT_EXTENSIONS[format] ?? [],
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolves which platform an artifact belongs to from its extension.
|
|
185
|
+
*
|
|
186
|
+
* Architecture has to be judged per platform, not across the whole matrix. With
|
|
187
|
+
* Windows on x64 and Linux on arm64, a flattened token list accepts an x64 .deb
|
|
188
|
+
* and an arm64 .exe -- each is released *somewhere*, just not where it was built.
|
|
189
|
+
*/
|
|
190
|
+
function platformForArtifact(name, matrix = DESKTOP_RELEASE_MATRIX) {
|
|
191
|
+
const lowered = stripBlockmap(name).toLowerCase();
|
|
192
|
+
for (const entry of matrix) {
|
|
193
|
+
for (const format of entry.formats) {
|
|
194
|
+
for (const extension of FORMAT_EXTENSIONS[format] ?? []) {
|
|
195
|
+
if (lowered.endsWith(extension.toLowerCase())) {
|
|
196
|
+
return entry;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function archTokensFor(entry) {
|
|
205
|
+
return entry.arches.flatMap((arch) => ARCH_TOKENS[arch] ?? [arch]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isUpdateMetadata(name) {
|
|
209
|
+
return UPDATE_METADATA_FILES.includes(name);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function stripBlockmap(name) {
|
|
213
|
+
return name.endsWith('.blockmap') ? name.slice(0, -'.blockmap'.length) : name;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function resolveMissingPrimaryArtifacts(fileNames) {
|
|
217
|
+
return PRIMARY_ARTIFACT_PATTERNS
|
|
218
|
+
.filter(({ pattern }) => !fileNames.some((name) => pattern.test(name)))
|
|
219
|
+
.map(({ platform }) => platform);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Flags artifacts whose format is not part of the declared release set.
|
|
224
|
+
* Update metadata and the blockmaps that accompany a released artifact are
|
|
225
|
+
* always allowed.
|
|
226
|
+
*/
|
|
227
|
+
export function resolveDisallowedArtifacts(fileNames, matrix = DESKTOP_RELEASE_MATRIX) {
|
|
228
|
+
const extensions = releasedExtensions(matrix);
|
|
229
|
+
|
|
230
|
+
return fileNames.filter((name) => {
|
|
231
|
+
if (isUpdateMetadata(name)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const base = stripBlockmap(name);
|
|
235
|
+
return !extensions.some((extension) => base.toLowerCase().endsWith(extension.toLowerCase()));
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Flags artifacts built for an architecture the declared release set does not
|
|
241
|
+
* include. An artifact whose name carries no recognizable architecture token is
|
|
242
|
+
* left alone rather than guessed at.
|
|
243
|
+
*/
|
|
244
|
+
export function resolveUnreleasedArchitectureArtifacts(
|
|
245
|
+
fileNames,
|
|
246
|
+
matrix = DESKTOP_RELEASE_MATRIX,
|
|
247
|
+
) {
|
|
248
|
+
return fileNames.filter((name) => {
|
|
249
|
+
if (isUpdateMetadata(name)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const entry = platformForArtifact(name, matrix);
|
|
253
|
+
if (entry === null) {
|
|
254
|
+
// Its format is not released at all; resolveDisallowedArtifacts owns that.
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const released = archTokensFor(entry).map((token) => token.toLowerCase());
|
|
258
|
+
const lowered = name.toLowerCase();
|
|
259
|
+
const present = ALL_ARCH_TOKENS.filter(
|
|
260
|
+
(token) => new RegExp(`(^|[-_.])${token}([-_.]|$)`, 'iu').test(lowered),
|
|
261
|
+
);
|
|
262
|
+
if (present.length === 0) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
return !present.some((token) => released.includes(token.toLowerCase()));
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function resolveMissingMetadataFiles(fileNames) {
|
|
270
|
+
const present = new Set(fileNames);
|
|
271
|
+
return UPDATE_METADATA_FILES.filter((name) => !present.has(name));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function listFilesRecursively(root) {
|
|
275
|
+
const found = [];
|
|
276
|
+
|
|
277
|
+
async function walk(directory) {
|
|
278
|
+
let entries;
|
|
279
|
+
try {
|
|
280
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
281
|
+
} catch {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
const entryPath = join(directory, entry.name);
|
|
286
|
+
if (entry.isDirectory()) {
|
|
287
|
+
await walk(entryPath);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
found.push(entryPath);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const rootStat = await stat(root).catch(() => null);
|
|
295
|
+
if (rootStat === null) {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
await walk(root);
|
|
299
|
+
return found;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function validateReleaseAssets({ files, metadataDocuments }) {
|
|
303
|
+
const problems = [];
|
|
304
|
+
const fileNames = files.map((path) => basename(path));
|
|
305
|
+
|
|
306
|
+
for (const platform of resolveMissingPrimaryArtifacts(fileNames)) {
|
|
307
|
+
problems.push({
|
|
308
|
+
code: 'primary_artifact_missing',
|
|
309
|
+
message: `No primary user-facing artifact was produced for ${platform}.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const name of resolveMissingMetadataFiles(fileNames)) {
|
|
314
|
+
problems.push({
|
|
315
|
+
code: 'update_metadata_missing',
|
|
316
|
+
message: `Update metadata ${name} was not produced.`,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const name of resolveDisallowedArtifacts(fileNames)) {
|
|
321
|
+
problems.push({
|
|
322
|
+
code: 'artifact_outside_release_contract',
|
|
323
|
+
message: `${name} is not one of the declared release artifacts. `
|
|
324
|
+
+ 'Add its format to DESKTOP_RELEASE_MATRIX if it should ship.',
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
for (const name of resolveUnreleasedArchitectureArtifacts(fileNames)) {
|
|
329
|
+
problems.push({
|
|
330
|
+
code: 'artifact_architecture_not_released',
|
|
331
|
+
message: `${name} targets an architecture the declared release set does not include. `
|
|
332
|
+
+ 'Add it to DESKTOP_RELEASE_MATRIX if it should ship.',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const present = new Set(fileNames);
|
|
337
|
+
for (const { name, document, error } of metadataDocuments) {
|
|
338
|
+
if (error) {
|
|
339
|
+
problems.push({
|
|
340
|
+
code: 'update_metadata_unreadable',
|
|
341
|
+
message: `Update metadata ${name} could not be parsed: ${error}`,
|
|
342
|
+
});
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const referenced = collectReferencedFiles(document);
|
|
347
|
+
if (referenced.length === 0) {
|
|
348
|
+
problems.push({
|
|
349
|
+
code: 'update_metadata_empty',
|
|
350
|
+
message: `Update metadata ${name} references no artifact.`,
|
|
351
|
+
});
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const target of referenced) {
|
|
356
|
+
if (!present.has(target)) {
|
|
357
|
+
problems.push({
|
|
358
|
+
code: 'referenced_artifact_missing',
|
|
359
|
+
message: `Update metadata ${name} references ${target}, which was not produced.`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { ok: problems.length === 0, problems, fileNames: [...present].sort() };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function main() {
|
|
369
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
370
|
+
if (parsed.help) {
|
|
371
|
+
printHelp();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const root = resolve(PROJECT_ROOT, parsed.root);
|
|
376
|
+
const files = await listFilesRecursively(root);
|
|
377
|
+
|
|
378
|
+
const metadataDocuments = [];
|
|
379
|
+
for (const path of files) {
|
|
380
|
+
if (!UPDATE_METADATA_FILES.includes(basename(path))) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
metadataDocuments.push({
|
|
385
|
+
name: basename(path),
|
|
386
|
+
document: yaml.load(await readFile(path, 'utf8')),
|
|
387
|
+
error: null,
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
metadataDocuments.push({
|
|
391
|
+
name: basename(path),
|
|
392
|
+
document: null,
|
|
393
|
+
error: error instanceof Error ? error.message : String(error),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const result = validateReleaseAssets({ files, metadataDocuments });
|
|
399
|
+
|
|
400
|
+
if (parsed.json) {
|
|
401
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
402
|
+
} else if (result.ok) {
|
|
403
|
+
process.stdout.write(
|
|
404
|
+
`[validate-release-assets] ${result.fileNames.length} files verified in ${root}.\n`,
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
for (const problem of result.problems) {
|
|
408
|
+
process.stderr.write(`[validate-release-assets] ${problem.code}: ${problem.message}\n`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!result.ok) {
|
|
413
|
+
process.exitCode = 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (resolve(process.argv[1] ?? '') === SCRIPT_PATH) {
|
|
418
|
+
void main().catch((error) => {
|
|
419
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
});
|
|
422
|
+
}
|