@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
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import process from 'node:process';
|
|
4
|
-
import { access, copyFile, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { dirname, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
|
|
9
|
+
import { parseStableReleaseTag } from './validate-release-version.mjs';
|
|
10
|
+
import { DESCRIPTOR_RELATIVE_PATH } from './generate-desktop-release-descriptor.mjs';
|
|
11
|
+
|
|
9
12
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
10
13
|
const PROJECT_ROOT = resolve(dirname(SCRIPT_PATH), '..');
|
|
11
14
|
const RUNTIME_ROOT = resolve(PROJECT_ROOT, '..', 'cats-runtime');
|
|
@@ -31,13 +34,30 @@ Options:
|
|
|
31
34
|
--sidecar-layout <split|bundle> Choose loose-file or bundled sidecars for both app/runtime.
|
|
32
35
|
--skip-mobile Skip the mobile bundle (\`expo export\`). Also honored via
|
|
33
36
|
CATS_SKIP_MOBILE=1 in the environment or .env.
|
|
37
|
+
--release Build an official release package: embed the release
|
|
38
|
+
descriptor, require platform signing credentials, and
|
|
39
|
+
allow signing identity discovery. Requires a GitHub
|
|
40
|
+
Actions tag run whose tag matches the package version.
|
|
41
|
+
Also honored via CATS_DESKTOP_RELEASE_MODE=1.
|
|
42
|
+
--preview Build an unsupported unsigned GitHub prerelease preview.
|
|
43
|
+
Requires the same tag/version provenance as --release,
|
|
44
|
+
omits the official descriptor, and disables signing
|
|
45
|
+
identity discovery. Also honored via
|
|
46
|
+
CATS_DESKTOP_PREVIEW_MODE=1.
|
|
47
|
+
--publish <never|always> Publish policy, default never. Publishing requires
|
|
48
|
+
--release or --preview plus a GitHub token. Also honored
|
|
49
|
+
via CATS_DESKTOP_PUBLISH.
|
|
34
50
|
--help Show this help text.
|
|
35
51
|
|
|
36
52
|
Without --arch/--format, the electron-builder target matrix from package.json is preserved.
|
|
53
|
+
|
|
54
|
+
Release mode and publishing are orthogonal. Stable releases are official and signed. Preview mode
|
|
55
|
+
may publish an unsigned prerelease but never embeds official update identity or discovers signing
|
|
56
|
+
identities. Local packaging is unofficial and never publishes.
|
|
37
57
|
`);
|
|
38
58
|
}
|
|
39
59
|
|
|
40
|
-
function
|
|
60
|
+
function parseBooleanFlag(value) {
|
|
41
61
|
if (typeof value !== 'string') {
|
|
42
62
|
return false;
|
|
43
63
|
}
|
|
@@ -45,6 +65,20 @@ function parseSkipMobileFlag(value) {
|
|
|
45
65
|
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
46
66
|
}
|
|
47
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Publishing is orthogonal to building an official package. Stable tag builds
|
|
70
|
+
* publish; an explicit official validation may still select `never`.
|
|
71
|
+
*/
|
|
72
|
+
export function resolvePublishPolicy(value) {
|
|
73
|
+
if (value === undefined || value === null || value === '') {
|
|
74
|
+
return 'never';
|
|
75
|
+
}
|
|
76
|
+
if (value === 'never' || value === 'always') {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Unsupported publish policy: ${value}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
function resolveSidecarLayout(value) {
|
|
49
83
|
if (value === undefined || value === null || value === '') {
|
|
50
84
|
return 'split';
|
|
@@ -60,12 +94,56 @@ export function parseArgs(argv, env = process.env) {
|
|
|
60
94
|
let arch = null;
|
|
61
95
|
let format = null;
|
|
62
96
|
let sidecarLayout = resolveSidecarLayout(env.CATS_DESKTOP_SIDECAR_LAYOUT);
|
|
63
|
-
let skipMobile =
|
|
97
|
+
let skipMobile = parseBooleanFlag(env.CATS_SKIP_MOBILE);
|
|
98
|
+
let releaseMode = parseBooleanFlag(env.CATS_DESKTOP_RELEASE_MODE);
|
|
99
|
+
let previewMode = parseBooleanFlag(env.CATS_DESKTOP_PREVIEW_MODE);
|
|
100
|
+
let publish = resolvePublishPolicy(env.CATS_DESKTOP_PUBLISH);
|
|
101
|
+
// GITHUB_REF_NAME is a runner-provided default. A workflow `env:` block
|
|
102
|
+
// cannot override anything in the GITHUB_ namespace, and a preview run is
|
|
103
|
+
// dispatched from a branch, so the tag has to arrive as an explicit input.
|
|
104
|
+
let tag = (env.CATS_DESKTOP_RELEASE_TAG ?? env.GITHUB_REF_NAME ?? '').trim();
|
|
64
105
|
|
|
65
106
|
for (let index = 0; index < argv.length; index += 1) {
|
|
66
107
|
const value = argv[index];
|
|
67
108
|
if (value === '--help' || value === '-h') {
|
|
68
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
help: true,
|
|
111
|
+
target,
|
|
112
|
+
arch,
|
|
113
|
+
format,
|
|
114
|
+
sidecarLayout,
|
|
115
|
+
skipMobile,
|
|
116
|
+
releaseMode,
|
|
117
|
+
previewMode,
|
|
118
|
+
publish,
|
|
119
|
+
tag,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (value === '--release') {
|
|
123
|
+
releaseMode = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (value === '--no-release') {
|
|
127
|
+
releaseMode = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (value === '--preview') {
|
|
131
|
+
previewMode = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (value === '--no-preview') {
|
|
135
|
+
previewMode = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (value === '--publish') {
|
|
139
|
+
publish = resolvePublishPolicy(argv[index + 1] ?? '');
|
|
140
|
+
index += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (value === '--tag') {
|
|
144
|
+
tag = (argv[index + 1] ?? '').trim();
|
|
145
|
+
index += 1;
|
|
146
|
+
continue;
|
|
69
147
|
}
|
|
70
148
|
if (value === '--target') {
|
|
71
149
|
target = argv[index + 1] ?? 'current';
|
|
@@ -98,7 +176,18 @@ export function parseArgs(argv, env = process.env) {
|
|
|
98
176
|
throw new Error(`Unknown option: ${value}`);
|
|
99
177
|
}
|
|
100
178
|
|
|
101
|
-
return {
|
|
179
|
+
return {
|
|
180
|
+
help: false,
|
|
181
|
+
target,
|
|
182
|
+
arch,
|
|
183
|
+
format,
|
|
184
|
+
sidecarLayout,
|
|
185
|
+
skipMobile,
|
|
186
|
+
releaseMode,
|
|
187
|
+
previewMode,
|
|
188
|
+
publish,
|
|
189
|
+
tag,
|
|
190
|
+
};
|
|
102
191
|
}
|
|
103
192
|
|
|
104
193
|
async function resolveNodeCliScript(command) {
|
|
@@ -142,13 +231,32 @@ async function resolveCommandInvocation(command, args) {
|
|
|
142
231
|
};
|
|
143
232
|
}
|
|
144
233
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
234
|
+
const SIGNING_CREDENTIAL_KEYS = [
|
|
235
|
+
'WIN_CSC_LINK',
|
|
236
|
+
'CSC_LINK',
|
|
237
|
+
'WIN_CSC_KEY_PASSWORD',
|
|
238
|
+
'CSC_KEY_PASSWORD',
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Local and test packaging must never reach for a signing identity, because an
|
|
243
|
+
* unconfigured machine would otherwise pick up an unrelated certificate from
|
|
244
|
+
* the OS keychain. Release mode leaves identity discovery to the workflow so a
|
|
245
|
+
* configured certificate can actually be used.
|
|
246
|
+
*
|
|
247
|
+
* Empty credential variables are dropped in both modes: electron-builder treats
|
|
248
|
+
* an empty CSC_LINK as a relative path and resolves it against the project
|
|
249
|
+
* root.
|
|
250
|
+
*/
|
|
251
|
+
export function buildInstallerEnvironment(baseEnv = process.env, options = {}) {
|
|
252
|
+
const releaseMode = options.releaseMode === true;
|
|
253
|
+
const env = { ...baseEnv };
|
|
254
|
+
|
|
255
|
+
if (!releaseMode) {
|
|
256
|
+
env.CSC_IDENTITY_AUTO_DISCOVERY = 'false';
|
|
257
|
+
}
|
|
150
258
|
|
|
151
|
-
for (const key of
|
|
259
|
+
for (const key of SIGNING_CREDENTIAL_KEYS) {
|
|
152
260
|
const value = env[key];
|
|
153
261
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
154
262
|
delete env[key];
|
|
@@ -158,7 +266,119 @@ export function buildInstallerEnvironment(baseEnv = process.env) {
|
|
|
158
266
|
return env;
|
|
159
267
|
}
|
|
160
268
|
|
|
161
|
-
|
|
269
|
+
export function hasWindowsSigningCredentials(env = process.env) {
|
|
270
|
+
const link = env.WIN_CSC_LINK ?? env.CSC_LINK;
|
|
271
|
+
return typeof link === 'string' && link.trim() !== '';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function hasMacosSigningCredentials(env = process.env) {
|
|
275
|
+
const link = env.CSC_LINK;
|
|
276
|
+
return typeof link === 'string' && link.trim() !== '';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Signing is a release gate, not a nice-to-have.
|
|
281
|
+
*
|
|
282
|
+
* SPEC-111 section 9 forbids advertising stable self-update on Windows or
|
|
283
|
+
* macOS before signing is available and validated, and the workflow publishes
|
|
284
|
+
* whatever the platform job produced. Without this check a missing secret would
|
|
285
|
+
* quietly ship unsigned stable binaries.
|
|
286
|
+
*/
|
|
287
|
+
export function resolveSigningProblems({ env = process.env, target } = {}) {
|
|
288
|
+
if (target === 'windows' && !hasWindowsSigningCredentials(env)) {
|
|
289
|
+
return [{
|
|
290
|
+
code: 'release_windows_signing_missing',
|
|
291
|
+
message: 'An official Windows release requires WIN_CSC_LINK or CSC_LINK.',
|
|
292
|
+
}];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (target === 'macos' && !hasMacosSigningCredentials(env)) {
|
|
296
|
+
return [{
|
|
297
|
+
code: 'release_macos_signing_missing',
|
|
298
|
+
message: 'An official macOS release requires CSC_LINK.',
|
|
299
|
+
}];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Release and preview publication are only meaningful inside the guarded
|
|
307
|
+
* workflow. Anything else could publish from an unreviewed tree, so the
|
|
308
|
+
* provenance inputs are checked before any platform build work starts.
|
|
309
|
+
*
|
|
310
|
+
* `.env` is loaded at module scope for developer convenience, which means
|
|
311
|
+
* A mode environment variable alone must never be enough. The checks below
|
|
312
|
+
* require the surrounding workflow to be real and the selected tag to match
|
|
313
|
+
* the package version.
|
|
314
|
+
*/
|
|
315
|
+
export function resolveReleaseModeProblems({
|
|
316
|
+
env = process.env,
|
|
317
|
+
tag = null,
|
|
318
|
+
target = null,
|
|
319
|
+
packageVersion = null,
|
|
320
|
+
publish = 'never',
|
|
321
|
+
requireSigning = true,
|
|
322
|
+
requireTagRef = true,
|
|
323
|
+
} = {}) {
|
|
324
|
+
const problems = [];
|
|
325
|
+
|
|
326
|
+
if (env.GITHUB_ACTIONS !== 'true') {
|
|
327
|
+
problems.push({
|
|
328
|
+
code: 'release_not_in_workflow',
|
|
329
|
+
message: 'Release and preview modes only run inside GitHub Actions (GITHUB_ACTIONS=true).',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Only a stable release is triggered by a tag push, so only it can assert a
|
|
334
|
+
// tag ref. A preview is dispatched from a branch and creates its tag as part
|
|
335
|
+
// of the run, so requiring one here would reject every preview.
|
|
336
|
+
//
|
|
337
|
+
// GITHUB_REF_TYPE is runner-provided and cannot be overridden from a
|
|
338
|
+
// workflow `env:` block, so this reads the real trigger rather than
|
|
339
|
+
// something the workflow claimed.
|
|
340
|
+
if (requireTagRef && env.GITHUB_REF_TYPE !== undefined && env.GITHUB_REF_TYPE !== 'tag') {
|
|
341
|
+
problems.push({
|
|
342
|
+
code: 'release_ref_not_tag',
|
|
343
|
+
message: `A stable release requires a tag ref, but GITHUB_REF_TYPE is '${env.GITHUB_REF_TYPE}'.`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const candidateTag = typeof tag === 'string' && tag.trim() !== ''
|
|
348
|
+
? tag
|
|
349
|
+
: env.GITHUB_REF_NAME ?? '';
|
|
350
|
+
const parsedTag = parseStableReleaseTag(candidateTag);
|
|
351
|
+
if (!parsedTag.ok) {
|
|
352
|
+
problems.push({
|
|
353
|
+
code: `release_${parsedTag.code}`,
|
|
354
|
+
message: `Release and preview modes require a vX.Y.Z tag. ${parsedTag.message}`,
|
|
355
|
+
});
|
|
356
|
+
} else if (typeof packageVersion === 'string' && packageVersion !== parsedTag.version) {
|
|
357
|
+
problems.push({
|
|
358
|
+
code: 'release_version_mismatch',
|
|
359
|
+
message: `Tag ${parsedTag.tag} does not match package version ${packageVersion}.`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (requireSigning) {
|
|
364
|
+
problems.push(...resolveSigningProblems({ env, target }));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Only publishing needs a token; non-publishing validation does not.
|
|
368
|
+
if (publish !== 'never') {
|
|
369
|
+
const token = env.GH_TOKEN ?? env.GITHUB_TOKEN;
|
|
370
|
+
if (typeof token !== 'string' || token.trim() === '') {
|
|
371
|
+
problems.push({
|
|
372
|
+
code: 'release_token_missing',
|
|
373
|
+
message: 'Publishing requires GH_TOKEN or GITHUB_TOKEN for the GitHub provider.',
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return problems;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function runCommand(command, args, cwd, envOverrides = {}, envOptions = {}) {
|
|
162
382
|
const invocation = await resolveCommandInvocation(command, args);
|
|
163
383
|
return new Promise((resolvePromise, reject) => {
|
|
164
384
|
const child = spawn(invocation.command, invocation.args, {
|
|
@@ -166,7 +386,7 @@ async function runCommand(command, args, cwd, envOverrides = {}) {
|
|
|
166
386
|
env: buildInstallerEnvironment({
|
|
167
387
|
...process.env,
|
|
168
388
|
...envOverrides,
|
|
169
|
-
}),
|
|
389
|
+
}, envOptions),
|
|
170
390
|
stdio: 'inherit',
|
|
171
391
|
shell: false,
|
|
172
392
|
});
|
|
@@ -193,6 +413,15 @@ async function prepareNativeBuildRoot() {
|
|
|
193
413
|
);
|
|
194
414
|
}
|
|
195
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Local packaging must never ship an official release descriptor. Removing it
|
|
418
|
+
* rather than merely not writing it also clears a stale descriptor left behind
|
|
419
|
+
* by an earlier release build in the same working tree.
|
|
420
|
+
*/
|
|
421
|
+
async function ensureReleaseDescriptorAbsent() {
|
|
422
|
+
await rm(resolve(PROJECT_ROOT, DESCRIPTOR_RELATIVE_PATH), { force: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
196
425
|
async function ensureMobileSkipPlaceholder() {
|
|
197
426
|
await mkdir(MOBILE_BUILD_ROOT, { recursive: true });
|
|
198
427
|
await writeFile(
|
|
@@ -204,18 +433,95 @@ async function ensureMobileSkipPlaceholder() {
|
|
|
204
433
|
);
|
|
205
434
|
}
|
|
206
435
|
|
|
207
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Asks SwiftPM where it put the binary instead of reconstructing the path.
|
|
438
|
+
*
|
|
439
|
+
* The layout differs per invocation: a plain build lands in `.build/release`,
|
|
440
|
+
* a single `--arch` cross-build in a triple-specific directory, and a
|
|
441
|
+
* multi-arch build in an Xcode-style `.build/apple/Products/Release`. Guessing
|
|
442
|
+
* wrong fails only after the compile succeeds, which reads like a build error
|
|
443
|
+
* when it is really a copy error.
|
|
444
|
+
*/
|
|
445
|
+
async function resolveSwiftBinPath(packageRoot, swiftArgs) {
|
|
446
|
+
const invocation = await resolveCommandInvocation('swift', [...swiftArgs, '--show-bin-path']);
|
|
447
|
+
return new Promise((resolvePromise, reject) => {
|
|
448
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
449
|
+
cwd: PROJECT_ROOT,
|
|
450
|
+
env: buildInstallerEnvironment({ ...process.env }, {}),
|
|
451
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
452
|
+
shell: false,
|
|
453
|
+
});
|
|
454
|
+
let out = '';
|
|
455
|
+
child.stdout.on('data', (chunk) => {
|
|
456
|
+
out += String(chunk);
|
|
457
|
+
});
|
|
458
|
+
child.once('exit', (code) => {
|
|
459
|
+
if (code !== 0) {
|
|
460
|
+
reject(new Error(`swift --show-bin-path exited with code ${code ?? 'null'}`));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const binPath = out.trim().split(/\r?\n/u).filter(Boolean).at(-1);
|
|
464
|
+
if (!binPath) {
|
|
465
|
+
reject(new Error('swift --show-bin-path produced no path.'));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
resolvePromise(binPath);
|
|
469
|
+
});
|
|
470
|
+
child.once('error', reject);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function buildMacosVoiceHelper(archOverride) {
|
|
208
475
|
if (process.platform !== 'darwin') {
|
|
209
476
|
throw new Error('The macOS voice helper must be built on macOS.');
|
|
210
477
|
}
|
|
211
478
|
|
|
212
479
|
const packageRoot = resolve(PROJECT_ROOT, 'desktop', 'native', 'macos-stt');
|
|
213
|
-
await runCommand('swift', ['build', '-c', 'release', '--package-path', packageRoot], PROJECT_ROOT);
|
|
214
|
-
|
|
215
480
|
const outputDir = resolve(NATIVE_BUILD_ROOT, 'macos-stt');
|
|
216
481
|
await mkdir(outputDir, { recursive: true });
|
|
482
|
+
|
|
483
|
+
if (archOverride === 'universal') {
|
|
484
|
+
// A universal DMG merges an x64 and an arm64 app bundle. electron-builder
|
|
485
|
+
// refuses when a binary is byte-identical in both, because it cannot tell
|
|
486
|
+
// whether that is a resource to share or a single-arch executable that
|
|
487
|
+
// should have been lipo'd. Building the helper universal removes the
|
|
488
|
+
// ambiguity instead of suppressing the check.
|
|
489
|
+
const universalArgs = [
|
|
490
|
+
'build',
|
|
491
|
+
'-c',
|
|
492
|
+
'release',
|
|
493
|
+
'--package-path',
|
|
494
|
+
packageRoot,
|
|
495
|
+
'--arch',
|
|
496
|
+
'x86_64',
|
|
497
|
+
'--arch',
|
|
498
|
+
'arm64',
|
|
499
|
+
];
|
|
500
|
+
await runCommand('swift', universalArgs, PROJECT_ROOT);
|
|
501
|
+
await copyFile(
|
|
502
|
+
resolve(await resolveSwiftBinPath(packageRoot, universalArgs), 'cats-stt-macos'),
|
|
503
|
+
resolve(outputDir, 'cats-stt-macos'),
|
|
504
|
+
);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Cross-compilation is the normal case now: the macOS runner is arm64 while
|
|
509
|
+
// the release targets x64, and an unqualified `swift build` follows the host.
|
|
510
|
+
// That would quietly bundle an arm64 helper inside an x64 app, which only
|
|
511
|
+
// fails when a user tries to dictate.
|
|
512
|
+
const swiftArch = archOverride === 'arm64' ? 'arm64' : 'x86_64';
|
|
513
|
+
const swiftArgs = [
|
|
514
|
+
'build',
|
|
515
|
+
'-c',
|
|
516
|
+
'release',
|
|
517
|
+
'--package-path',
|
|
518
|
+
packageRoot,
|
|
519
|
+
'--arch',
|
|
520
|
+
swiftArch,
|
|
521
|
+
];
|
|
522
|
+
await runCommand('swift', swiftArgs, PROJECT_ROOT);
|
|
217
523
|
await copyFile(
|
|
218
|
-
resolve(packageRoot,
|
|
524
|
+
resolve(await resolveSwiftBinPath(packageRoot, swiftArgs), 'cats-stt-macos'),
|
|
219
525
|
resolve(outputDir, 'cats-stt-macos'),
|
|
220
526
|
);
|
|
221
527
|
}
|
|
@@ -249,7 +555,7 @@ async function buildWindowsVoiceHelper(archOverride) {
|
|
|
249
555
|
async function buildNativeVoiceHelpers(target, archOverride) {
|
|
250
556
|
await prepareNativeBuildRoot();
|
|
251
557
|
if (target === 'macos') {
|
|
252
|
-
await buildMacosVoiceHelper();
|
|
558
|
+
await buildMacosVoiceHelper(archOverride);
|
|
253
559
|
return;
|
|
254
560
|
}
|
|
255
561
|
if (target === 'windows') {
|
|
@@ -294,46 +600,68 @@ const VALID_ARCHES = {
|
|
|
294
600
|
linux: ['x64', 'arm64'],
|
|
295
601
|
};
|
|
296
602
|
|
|
297
|
-
|
|
298
|
-
|
|
603
|
+
/**
|
|
604
|
+
* Accepts a single format or a comma-separated list. macOS needs both the DMG
|
|
605
|
+
* users install and the ZIP electron-updater reads, so the release matrix
|
|
606
|
+
* cannot be expressed with one format per platform.
|
|
607
|
+
*/
|
|
608
|
+
export function normalizeFormats(target, formatOverride) {
|
|
609
|
+
if (formatOverride === null || formatOverride === undefined || formatOverride === '') {
|
|
299
610
|
return null;
|
|
300
611
|
}
|
|
301
612
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
613
|
+
const requested = String(formatOverride).split(',').map((value) => value.trim()).filter(Boolean);
|
|
614
|
+
if (requested.length === 0) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
305
617
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
618
|
+
return requested.map((value) => {
|
|
619
|
+
const canonicalFormat = PLATFORM_FORMATS[target].find(
|
|
620
|
+
(candidate) => candidate.toLowerCase() === value.toLowerCase(),
|
|
309
621
|
);
|
|
310
|
-
}
|
|
311
622
|
|
|
312
|
-
|
|
623
|
+
if (!canonicalFormat) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`Unsupported format '${value}' for ${target}. Valid: ${PLATFORM_FORMATS[target].join(', ')}`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return canonicalFormat;
|
|
630
|
+
});
|
|
313
631
|
}
|
|
314
632
|
|
|
315
|
-
function electronBuilderArgs(target, archOverride, formatOverride) {
|
|
633
|
+
export function electronBuilderArgs(target, archOverride, formatOverride, options = {}) {
|
|
316
634
|
if (archOverride !== null && !VALID_ARCHES[target].includes(archOverride)) {
|
|
317
635
|
throw new Error(
|
|
318
636
|
`Unsupported arch '${archOverride}' for ${target}. Valid: ${VALID_ARCHES[target].join(', ')}`,
|
|
319
637
|
);
|
|
320
638
|
}
|
|
321
639
|
|
|
322
|
-
const
|
|
640
|
+
const releaseMode = options.releaseMode === true;
|
|
641
|
+
const publish = resolvePublishPolicy(options.publish ?? 'never');
|
|
642
|
+
const signWindowsExecutable = options.signWindowsExecutable === true;
|
|
643
|
+
const formats = normalizeFormats(target, formatOverride);
|
|
323
644
|
const platformFlag = target === 'windows' ? '--win' : target === 'macos' ? '--mac' : '--linux';
|
|
324
645
|
const args = ['electron-builder', platformFlag];
|
|
325
646
|
|
|
326
|
-
if (
|
|
327
|
-
args.push(
|
|
647
|
+
if (formats !== null) {
|
|
648
|
+
args.push(...formats);
|
|
328
649
|
} else if (archOverride !== null) {
|
|
329
650
|
args.push(...PLATFORM_FORMATS[target], `--${archOverride}`);
|
|
330
651
|
}
|
|
331
652
|
|
|
332
|
-
if (
|
|
653
|
+
if (formats !== null && archOverride !== null) {
|
|
333
654
|
args.push(`--${archOverride}`);
|
|
334
655
|
}
|
|
335
656
|
|
|
336
|
-
|
|
657
|
+
// package.json pins signAndEditExecutable to false so unsigned local builds
|
|
658
|
+
// avoid the winCodeSign download. A release build with real credentials has
|
|
659
|
+
// to opt back in, and only then.
|
|
660
|
+
if (releaseMode && target === 'windows' && signWindowsExecutable) {
|
|
661
|
+
args.push('-c.win.signAndEditExecutable=true');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
args.push('--publish', publish);
|
|
337
665
|
return args;
|
|
338
666
|
}
|
|
339
667
|
|
|
@@ -345,20 +673,86 @@ async function main() {
|
|
|
345
673
|
}
|
|
346
674
|
|
|
347
675
|
const resolvedTarget = resolveBuilderTarget(parsed.target);
|
|
676
|
+
const envOptions = { releaseMode: parsed.releaseMode };
|
|
677
|
+
|
|
678
|
+
if (parsed.releaseMode && parsed.previewMode) {
|
|
679
|
+
throw new Error('--release and --preview are mutually exclusive build modes.');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (parsed.publish !== 'never' && !parsed.releaseMode && !parsed.previewMode) {
|
|
683
|
+
throw new Error('Publishing requires --release or --preview.');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (parsed.releaseMode || parsed.previewMode) {
|
|
687
|
+
const { version: packageVersion } = JSON.parse(
|
|
688
|
+
await readFile(resolve(PROJECT_ROOT, 'package.json'), 'utf8'),
|
|
689
|
+
);
|
|
690
|
+
const problems = resolveReleaseModeProblems({
|
|
691
|
+
env: process.env,
|
|
692
|
+
tag: parsed.tag,
|
|
693
|
+
target: resolvedTarget,
|
|
694
|
+
packageVersion,
|
|
695
|
+
publish: parsed.publish,
|
|
696
|
+
requireSigning: parsed.releaseMode,
|
|
697
|
+
requireTagRef: parsed.releaseMode,
|
|
698
|
+
});
|
|
699
|
+
if (problems.length > 0) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Release/preview inputs are not valid:\n${problems
|
|
702
|
+
.map((problem) => ` ${problem.code}: ${problem.message}`)
|
|
703
|
+
.join('\n')}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const buildKind = parsed.releaseMode ? 'official' : 'unsigned preview';
|
|
707
|
+
process.stdout.write(
|
|
708
|
+
`[build-desktop-installer] ${buildKind} ${resolvedTarget} build for `
|
|
709
|
+
+ `${process.env.GITHUB_REF_NAME} (publish=${parsed.publish}).\n`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
348
713
|
const sidecarBuildEnv = {
|
|
349
714
|
CATS_DESKTOP_SIDECAR_LAYOUT: parsed.sidecarLayout,
|
|
350
715
|
};
|
|
351
|
-
await runCommand('npm', ['run', 'build'], RUNTIME_ROOT, sidecarBuildEnv);
|
|
716
|
+
await runCommand('npm', ['run', 'build'], RUNTIME_ROOT, sidecarBuildEnv, envOptions);
|
|
352
717
|
const platformBuildScript = parsed.skipMobile ? 'build:no-mobile' : 'build';
|
|
353
718
|
if (parsed.skipMobile) {
|
|
354
719
|
process.stdout.write(
|
|
355
720
|
'[build-desktop-installer] CATS_SKIP_MOBILE active — running build:no-mobile and seeding build/mobile/.skip-marker after build.\n',
|
|
356
721
|
);
|
|
357
722
|
}
|
|
358
|
-
await runCommand('npm', ['run', platformBuildScript], PROJECT_ROOT, sidecarBuildEnv);
|
|
723
|
+
await runCommand('npm', ['run', platformBuildScript], PROJECT_ROOT, sidecarBuildEnv, envOptions);
|
|
359
724
|
if (parsed.skipMobile) {
|
|
360
725
|
await ensureMobileSkipPlaceholder();
|
|
361
726
|
}
|
|
727
|
+
|
|
728
|
+
// npm run build wipes build/, so the descriptor has to be written after the
|
|
729
|
+
// platform build and before packaging collects build/desktop.
|
|
730
|
+
//
|
|
731
|
+
// A preview gets a descriptor too, marked `preview`, so the upgrade path can
|
|
732
|
+
// be exercised before signing exists. It resolves to its own distribution
|
|
733
|
+
// mode, so it never claims official update identity.
|
|
734
|
+
if (parsed.releaseMode || parsed.previewMode) {
|
|
735
|
+
await runCommand(
|
|
736
|
+
'node',
|
|
737
|
+
[
|
|
738
|
+
'scripts/generate-desktop-release-descriptor.mjs',
|
|
739
|
+
'--platform',
|
|
740
|
+
resolvedTarget,
|
|
741
|
+
'--kind',
|
|
742
|
+
parsed.releaseMode ? 'official' : 'preview',
|
|
743
|
+
// Forwarded rather than left to GITHUB_REF_NAME, which is the branch
|
|
744
|
+
// name on a preview dispatch.
|
|
745
|
+
'--tag',
|
|
746
|
+
parsed.tag,
|
|
747
|
+
],
|
|
748
|
+
PROJECT_ROOT,
|
|
749
|
+
{},
|
|
750
|
+
envOptions,
|
|
751
|
+
);
|
|
752
|
+
} else {
|
|
753
|
+
await ensureReleaseDescriptorAbsent();
|
|
754
|
+
}
|
|
755
|
+
|
|
362
756
|
await buildNativeVoiceHelpers(resolvedTarget, parsed.arch);
|
|
363
757
|
await runCommand(
|
|
364
758
|
'node',
|
|
@@ -371,11 +765,18 @@ async function main() {
|
|
|
371
765
|
],
|
|
372
766
|
PROJECT_ROOT,
|
|
373
767
|
sidecarBuildEnv,
|
|
768
|
+
envOptions,
|
|
374
769
|
);
|
|
375
770
|
await runCommand(
|
|
376
771
|
'npx',
|
|
377
|
-
electronBuilderArgs(resolvedTarget, parsed.arch, parsed.format
|
|
772
|
+
electronBuilderArgs(resolvedTarget, parsed.arch, parsed.format, {
|
|
773
|
+
releaseMode: parsed.releaseMode,
|
|
774
|
+
publish: parsed.publish,
|
|
775
|
+
signWindowsExecutable: hasWindowsSigningCredentials(process.env),
|
|
776
|
+
}),
|
|
378
777
|
PROJECT_ROOT,
|
|
778
|
+
{},
|
|
779
|
+
envOptions,
|
|
379
780
|
);
|
|
380
781
|
}
|
|
381
782
|
|
|
@@ -5,13 +5,24 @@ import { build } from 'esbuild';
|
|
|
5
5
|
|
|
6
6
|
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
7
|
const testsDir = join(rootDir, 'tests');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TypeScript tests are bundled here because `node --test` only collects the
|
|
11
|
+
* plain `.js` files under `tests/`. Both extensions must be picked up: a
|
|
12
|
+
* `.test.ts` that is not bundled is not merely unchecked, it never runs at all,
|
|
13
|
+
* and a test that never runs looks exactly like a passing one.
|
|
14
|
+
*/
|
|
15
|
+
const TEST_ENTRY_EXTENSIONS = ['.test.ts', '.test.tsx'];
|
|
16
|
+
|
|
8
17
|
const entryPoints = (await readdir(testsDir))
|
|
9
|
-
.filter((fileName) => fileName.endsWith(
|
|
18
|
+
.filter((fileName) => TEST_ENTRY_EXTENSIONS.some((ext) => fileName.endsWith(ext)))
|
|
10
19
|
.sort()
|
|
11
20
|
.map((fileName) => join(testsDir, fileName));
|
|
12
21
|
|
|
13
22
|
if (entryPoints.length === 0) {
|
|
14
|
-
throw new Error(
|
|
23
|
+
throw new Error(
|
|
24
|
+
`No test entry points found in tests/*{${TEST_ENTRY_EXTENSIONS.join(',')}}`,
|
|
25
|
+
);
|
|
15
26
|
}
|
|
16
27
|
|
|
17
28
|
await build({
|