From 94ddefdab5d138c56a35d91ad380d8be140092a4 Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Mon, 13 Jul 2026 13:17:34 +0200 Subject: [PATCH 1/2] feat: add Electron app version constraint flags for live update bundles Co-Authored-By: Claude Fable 5 --- src/commands/apps/bundles/create.ts | 12 ++++++++++++ src/commands/apps/bundles/update.ts | 12 ++++++++++++ src/commands/apps/liveupdates/register.ts | 18 ++++++++++++++++++ src/commands/apps/liveupdates/upload.ts | 18 ++++++++++++++++++ src/types/app-bundle.ts | 6 ++++++ 5 files changed, 66 insertions(+) diff --git a/src/commands/apps/bundles/create.ts b/src/commands/apps/bundles/create.ts index 0722f86..c5b883c 100644 --- a/src/commands/apps/bundles/create.ts +++ b/src/commands/apps/bundles/create.ts @@ -45,6 +45,18 @@ export default defineCommand({ .describe( 'A custom property to assign to the bundle. Must be in the format `key=value`. Can be specified multiple times.', ), + electronMax: z + .string() + .optional() + .describe('The maximum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronMin: z + .string() + .optional() + .describe('The minimum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronEq: z + .string() + .optional() + .describe('The exact Electron app version (`app.getVersion()`) that the bundle does not support.'), expiresInDays: z.coerce .number({ message: 'Expiration days must be an integer.', diff --git a/src/commands/apps/bundles/update.ts b/src/commands/apps/bundles/update.ts index 8ac4d40..bf6e0f2 100644 --- a/src/commands/apps/bundles/update.ts +++ b/src/commands/apps/bundles/update.ts @@ -28,6 +28,18 @@ export default defineCommand({ }) .optional() .describe('The percentage of devices to deploy the bundle to. Must be a number between 0 and 1 (e.g. 0.5).'), + electronMax: z + .string() + .optional() + .describe('The maximum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronMin: z + .string() + .optional() + .describe('The minimum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronEq: z + .string() + .optional() + .describe('The exact Electron app version (`app.getVersion()`) that the bundle should not support.'), iosMax: z .string() .optional() diff --git a/src/commands/apps/liveupdates/register.ts b/src/commands/apps/liveupdates/register.ts index f677b27..98a3118 100644 --- a/src/commands/apps/liveupdates/register.ts +++ b/src/commands/apps/liveupdates/register.ts @@ -60,6 +60,18 @@ export default defineCommand({ .describe( 'A custom property to assign to the bundle. Must be in the format `key=value`. Can be specified multiple times.', ), + electronMax: z + .string() + .optional() + .describe('The maximum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronMin: z + .string() + .optional() + .describe('The minimum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronEq: z + .string() + .optional() + .describe('The exact Electron app version (`app.getVersion()`) that the bundle does not support.'), expiresInDays: z.coerce .number({ message: 'Expiration days must be an integer.', @@ -122,6 +134,9 @@ export default defineCommand({ commitRef, commitSha, customProperty, + electronEq, + electronMax, + electronMin, expiresInDays, gitRef, iosEq, @@ -268,6 +283,7 @@ export default defineCommand({ channelName: channel, checksum, eqAndroidAppVersionCode: androidEq, + eqElectronAppVersionCode: electronEq, eqIosAppVersionCode: iosEq, gitCommitMessage: commitMessage, gitCommitRef: commitRef, @@ -276,8 +292,10 @@ export default defineCommand({ customProperties: parseCustomProperties(customProperty), url, maxAndroidAppVersionCode: androidMax, + maxElectronAppVersionCode: electronMax, maxIosAppVersionCode: iosMax, minAndroidAppVersionCode: androidMin, + minElectronAppVersionCode: electronMin, minIosAppVersionCode: iosMin, rolloutPercentage: (rolloutPercentage ?? 100) / 100, signature, diff --git a/src/commands/apps/liveupdates/upload.ts b/src/commands/apps/liveupdates/upload.ts index 2bd5085..0d0c421 100644 --- a/src/commands/apps/liveupdates/upload.ts +++ b/src/commands/apps/liveupdates/upload.ts @@ -83,6 +83,18 @@ export default defineCommand({ .describe( 'A custom property to assign to the bundle. Must be in the format `key=value`. Can be specified multiple times.', ), + electronMax: z + .string() + .optional() + .describe('The maximum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronMin: z + .string() + .optional() + .describe('The minimum Electron app version (`app.getVersion()`) that the bundle supports.'), + electronEq: z + .string() + .optional() + .describe('The exact Electron app version (`app.getVersion()`) that the bundle does not support.'), expiresInDays: z.coerce .number({ message: 'Expiration days must be an integer.', @@ -148,6 +160,9 @@ export default defineCommand({ commitRef, commitSha, customProperty, + electronEq, + electronMax, + electronMin, expiresInDays, gitRef, iosEq, @@ -311,6 +326,7 @@ export default defineCommand({ artifactType, channelName: channel, eqAndroidAppVersionCode: androidEq, + eqElectronAppVersionCode: electronEq, eqIosAppVersionCode: iosEq, gitCommitMessage: commitMessage, gitCommitRef: commitRef, @@ -318,8 +334,10 @@ export default defineCommand({ gitRef, customProperties: parseCustomProperties(customProperty), maxAndroidAppVersionCode: androidMax, + maxElectronAppVersionCode: electronMax, maxIosAppVersionCode: iosMax, minAndroidAppVersionCode: androidMin, + minElectronAppVersionCode: electronMin, minIosAppVersionCode: iosMin, // Convert percentage from 0-100 to 0-1 for API rolloutPercentage: (rolloutPercentage ?? 100) / 100, diff --git a/src/types/app-bundle.ts b/src/types/app-bundle.ts index a28c3b7..86f4263 100644 --- a/src/types/app-bundle.ts +++ b/src/types/app-bundle.ts @@ -10,6 +10,7 @@ export interface CreateAppBundleDto { channelName?: string; checksum?: string; eqAndroidAppVersionCode?: string; + eqElectronAppVersionCode?: string; eqIosAppVersionCode?: string; gitCommitMessage?: string; gitCommitRef?: string; @@ -19,8 +20,10 @@ export interface CreateAppBundleDto { expiresAt?: string; url?: string; maxAndroidAppVersionCode?: string; + maxElectronAppVersionCode?: string; maxIosAppVersionCode?: string; minAndroidAppVersionCode?: string; + minElectronAppVersionCode?: string; minIosAppVersionCode?: string; rolloutPercentage?: number; signature?: string; @@ -37,10 +40,13 @@ export interface UpdateAppBundleDto { appId: string; artifactStatus?: 'pending' | 'ready'; maxAndroidAppVersionCode?: string; + maxElectronAppVersionCode?: string; maxIosAppVersionCode?: string; minAndroidAppVersionCode?: string; + minElectronAppVersionCode?: string; minIosAppVersionCode?: string; eqAndroidAppVersionCode?: string; + eqElectronAppVersionCode?: string; eqIosAppVersionCode?: string; rolloutPercentage?: number; } From b9b2560d8af95746c0493ec2d87fd6e1c157e8c4 Mon Sep 17 00:00:00 2001 From: Robin Genz Date: Tue, 14 Jul 2026 11:12:41 +0200 Subject: [PATCH 2/2] test: add Electron version constraint pass-through tests Co-Authored-By: Claude Fable 5 --- .../apps/liveupdates/register.test.ts | 45 ++++++++++++ src/commands/apps/liveupdates/upload.test.ts | 71 +++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/src/commands/apps/liveupdates/register.test.ts b/src/commands/apps/liveupdates/register.test.ts index eecebf6..e65db6a 100644 --- a/src/commands/apps/liveupdates/register.test.ts +++ b/src/commands/apps/liveupdates/register.test.ts @@ -172,6 +172,51 @@ describe('apps-liveupdates-register', () => { expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully registered.'); }); + it('should pass Electron version constraints to API when provided', async () => { + const appId = 'app-123'; + const bundleUrl = 'https://example.com/bundle.zip'; + const bundleId = 'bundle-456'; + const testToken = 'test-token'; + const electronEq = '1.0.0'; + const electronMin = '2.0.0'; + const electronMax = '3.0.0'; + + const options = { + appId, + url: bundleUrl, + rolloutPercentage: 1, + electronEq, + electronMin, + electronMax, + yes: true, + }; + + const appScope = nock(DEFAULT_API_BASE_URL) + .get(`/v1/apps/${appId}`) + .matchHeader('Authorization', `Bearer ${testToken}`) + .reply(200, { id: appId, name: 'Test App' }); + + const bundleScope = nock(DEFAULT_API_BASE_URL) + .post(`/v1/apps/${appId}/bundles`, { + appId, + url: bundleUrl, + artifactType: 'zip', + eqElectronAppVersionCode: electronEq, + minElectronAppVersionCode: electronMin, + maxElectronAppVersionCode: electronMax, + rolloutPercentage: 0.01, + }) + .matchHeader('Authorization', `Bearer ${testToken}`) + .reply(201, { id: bundleId, appBuildId: 'build-789' }); + + await registerCommand.action(options, undefined); + + expect(appScope.isDone()).toBe(true); + expect(bundleScope.isDone()).toBe(true); + expect(mockConsola.info).toHaveBeenCalledWith(`Bundle Artifact ID: ${bundleId}`); + expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully registered.'); + }); + it('should register bundle with checksum when path is provided', async () => { const appId = 'app-123'; const bundleUrl = 'https://example.com/bundle.zip'; diff --git a/src/commands/apps/liveupdates/upload.test.ts b/src/commands/apps/liveupdates/upload.test.ts index 86a0889..e1f3b4f 100644 --- a/src/commands/apps/liveupdates/upload.test.ts +++ b/src/commands/apps/liveupdates/upload.test.ts @@ -228,6 +228,77 @@ describe('apps-liveupdates-upload', () => { expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully uploaded.'); }); + it('should pass Electron version constraints to API when provided', async () => { + const appId = 'app-123'; + const bundlePath = './dist'; + const bundleId = 'bundle-456'; + const testToken = 'test-token'; + const testBuffer = Buffer.from('test'); + const electronEq = '1.0.0'; + const electronMin = '2.0.0'; + const electronMax = '3.0.0'; + + const options = { + appId, + path: bundlePath, + artifactType: 'zip' as const, + rollout: 1, + electronEq, + electronMin, + electronMax, + }; + + mockIsReadable.mockResolvedValue(true); + mockIsDirectory.mockResolvedValue(true); + mockGetFilesInDirectoryAndSubdirectories.mockResolvedValue([ + { href: 'index.html', mimeType: 'text/html', name: 'index.html', path: 'index.html' }, + ]); + + // Mock utility functions + const mockZip = await import('@/utils/zip.js'); + const mockHash = await import('@/utils/hash.js'); + + vi.mocked(mockZip.default.isZipped).mockReturnValue(false); + vi.mocked(mockZip.default.zipFolder).mockResolvedValue(testBuffer); + vi.mocked(mockHash.createHash).mockResolvedValue('test-hash'); + + const appScope = nock(DEFAULT_API_BASE_URL) + .get(`/v1/apps/${appId}`) + .matchHeader('Authorization', `Bearer ${testToken}`) + .reply(200, { id: appId, name: 'Test App' }); + + const bundleScope = nock(DEFAULT_API_BASE_URL) + .post(`/v1/apps/${appId}/bundles`, { + appId, + artifactType: 'zip', + eqElectronAppVersionCode: electronEq, + minElectronAppVersionCode: electronMin, + maxElectronAppVersionCode: electronMax, + rolloutPercentage: 1, + }) + .matchHeader('Authorization', `Bearer ${testToken}`) + .reply(201, { id: bundleId, appBuildId: 'build-789' }); + + const uploadScope = nock(DEFAULT_API_BASE_URL) + .post(`/v1/apps/${appId}/bundles/${bundleId}/files`) + .matchHeader('Authorization', `Bearer ${testToken}`) + .reply(201, { id: 'file-123' }); + + const updateScope = nock(DEFAULT_API_BASE_URL) + .patch(`/v1/apps/${appId}/bundles/${bundleId}`) + .matchHeader('Authorization', `Bearer ${testToken}`) + .reply(200, { id: bundleId }); + + await uploadCommand.action(options, undefined); + + expect(appScope.isDone()).toBe(true); + expect(bundleScope.isDone()).toBe(true); + expect(uploadScope.isDone()).toBe(true); + expect(updateScope.isDone()).toBe(true); + expect(mockConsola.info).toHaveBeenCalledWith(`Build Artifact ID: ${bundleId}`); + expect(mockConsola.success).toHaveBeenCalledWith('Live Update successfully uploaded.'); + }); + it('should output JSON when json flag is set', async () => { const appId = 'app-123'; const bundlePath = './dist';