Skip to content

Commit 4cc1f7e

Browse files
authored
feat(macos): add internal auto-update support for Wire macOS wrapper (#9277)
1 parent 649fb4e commit 4cc1f7e

File tree

6 files changed

+313
-34
lines changed

6 files changed

+313
-34
lines changed

electron/src/main.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {config} from './settings/config';
6969
import {settings} from './settings/ConfigurationPersistence';
7070
import {SettingsType} from './settings/SettingsType';
7171
import {SingleSignOn} from './sso/SingleSignOn';
72+
import {initMacAutoUpdater} from './update/macosAutoUpdater';
7273
import {AboutWindow} from './window/AboutWindow';
7374
import {ProxyPromptWindow} from './window/ProxyPromptWindow';
7475
import {WindowManager} from './window/WindowManager';
@@ -105,6 +106,7 @@ const currentLocale = locale.getCurrent();
105106
const startHidden = Boolean(argv[config.ARGUMENT.STARTUP] || argv[config.ARGUMENT.HIDDEN]);
106107
const customDownloadPath = settings.restore<string | undefined>(SettingsType.DOWNLOAD_PATH);
107108
const appHomePath = (path: string) => `${app.getPath('home')}\\${path}`;
109+
const isInternalBuild = (): boolean => config.environment === 'internal';
108110

109111
if (customDownloadPath) {
110112
electronDl({
@@ -478,6 +480,25 @@ const handleAppEvents = (): void => {
478480
tray.initTray();
479481
}
480482
await showMainWindow(mainWindowState);
483+
484+
app.on('ready', async () => {
485+
const mainWindowState = initWindowStateKeeper();
486+
const appMenu = systemMenu.createMenu(isFullScreen);
487+
if (EnvironmentUtil.app.IS_DEVELOPMENT) {
488+
appMenu.append(developerMenu);
489+
}
490+
491+
Menu.setApplicationMenu(appMenu);
492+
tray = new TrayHandler();
493+
if (!EnvironmentUtil.platform.IS_MAC_OS) {
494+
tray.initTray();
495+
}
496+
await showMainWindow(mainWindowState);
497+
498+
if (EnvironmentUtil.platform.IS_MAC_OS && isInternalBuild()) {
499+
initMacAutoUpdater(main);
500+
}
501+
});
481502
});
482503
};
483504

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*
18+
*/
19+
20+
import {BrowserWindow, dialog} from 'electron';
21+
import {autoUpdater} from 'electron-updater';
22+
23+
import {getLogger} from '../logging/getLogger';
24+
import {config} from '../settings/config';
25+
26+
const logger = getLogger('MacAutoUpdater');
27+
const isInternalBuild = (): boolean => config.environment === 'internal';
28+
29+
export function initMacAutoUpdater(mainWindow: BrowserWindow): void {
30+
// Skip in dev
31+
if (process.env.NODE_ENV === 'development') {
32+
logger.log('Skipping auto-update in development');
33+
return;
34+
}
35+
36+
// Only run for internal builds (production = App Store -> handled by Apple)
37+
if (!isInternalBuild()) {
38+
logger.log('Skipping auto-update: not an internal build');
39+
return;
40+
}
41+
42+
logger.log('Initializing macOS auto-updater for internal build');
43+
44+
// INTERNAL FEED URL (served from S3)
45+
// We upload latest-mac.yml + WireInternal-<version>.zip here.
46+
const feedUrl =
47+
process.env.WIRE_INTERNAL_MAC_UPDATE_URL || 'https://wire-taco.s3.eu-west-1.amazonaws.com/mac/internal/updates/';
48+
49+
// TODO (infra): once internal mac auto-update is validated using the raw S3 URL,
50+
// expose it via https://wire-app.wire.com/mac/internal/updates/ (similar to WIN_URL_UPDATE).
51+
// const feedUrl =
52+
// process.env.WIRE_INTERNAL_MAC_UPDATE_URL ||
53+
// 'https://wire-app.wire.com/mac/internal/updates/';
54+
55+
logger.log(`Using update feed: ${feedUrl}`);
56+
57+
autoUpdater.setFeedURL({
58+
provider: 'generic',
59+
url: feedUrl,
60+
});
61+
62+
autoUpdater.on('checking-for-update', () => {
63+
logger.log('Checking for update…');
64+
});
65+
66+
autoUpdater.on('update-available', info => {
67+
logger.log(`Update available: ${info.version}`);
68+
});
69+
70+
autoUpdater.on('update-not-available', info => {
71+
logger.log(`No update available (current: ${info.version})`);
72+
});
73+
74+
autoUpdater.on('error', err => {
75+
logger.error('Auto-updater error', err);
76+
});
77+
78+
autoUpdater.on('download-progress', progress => {
79+
// For now we only log; no progress UI.
80+
logger.log(
81+
`Update download progress: ${progress.percent?.toFixed?.(1) ?? 'n/a'}% at ${progress.bytesPerSecond} B/s`,
82+
);
83+
});
84+
85+
autoUpdater.on('update-downloaded', async info => {
86+
logger.log(`Update downloaded: ${info.version}`);
87+
88+
// Show a simple native dialog when the update is ready.
89+
const result = await dialog.showMessageBox(mainWindow, {
90+
type: 'info',
91+
buttons: ['Install & Restart', 'Later'],
92+
defaultId: 0,
93+
cancelId: 1,
94+
title: 'Wire Update Ready',
95+
message: 'A new version of Wire is ready to install.',
96+
detail: `Version ${info.version} has been downloaded. Wire will restart to complete the update.`,
97+
});
98+
99+
if (result.response === 0) {
100+
logger.log('User chose to install update now');
101+
autoUpdater.quitAndInstall();
102+
} else {
103+
logger.log('User chose to install later');
104+
}
105+
});
106+
107+
// Kick off background check + download.
108+
autoUpdater.checkForUpdates();
109+
}

jenkins/deployment.groovy

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ node('built-in') {
3030

3131
stage('Checkout & Clean') {
3232
git branch: "${GIT_BRANCH}", url: 'https://github.com/wireapp/wire-desktop.git'
33-
sh returnStatus: true, script: 'rm -rf *.pkg *.zip ./wrap/dist/ ./node_modules/'
33+
sh returnStatus: true, script: 'rm -rf *.pkg *.zip ./wrap/dist/ ./wrap/build/ ./node_modules/'
3434
}
3535

3636
def projectName = env.WRAPPER_BUILD.tokenize('#')[0]
@@ -87,15 +87,15 @@ node('built-in') {
8787
def AWS_ACCESS_KEY_CREDENTIALS_ID = ''
8888
def AWS_SECRET_CREDENTIALS_ID = ''
8989

90-
if (params.Release.equals('Production')) {
90+
if (params.Release == 'Production') {
9191
env.S3_PATH = 'win/prod'
9292
AWS_ACCESS_KEY_CREDENTIALS_ID = 'AWS_ACCESS_KEY_ID'
9393
AWS_SECRET_CREDENTIALS_ID = 'AWS_SECRET_ACCESS_KEY'
94-
} else if (params.Release.equals('Internal')) {
94+
} else if (params.Release == 'Internal') {
9595
env.S3_PATH = 'win/internal'
9696
AWS_ACCESS_KEY_CREDENTIALS_ID = 'AWS_ACCESS_KEY_ID'
9797
AWS_SECRET_CREDENTIALS_ID = 'AWS_SECRET_ACCESS_KEY'
98-
} else if (params.Release.equals('Custom')) {
98+
} else if (params.Release == 'Custom') {
9999
env.S3_BUCKET = params.WIN_S3_BUCKET
100100
env.S3_PATH = params.WIN_S3_PATH
101101
AWS_ACCESS_KEY_CREDENTIALS_ID = params.AWS_CUSTOM_ACCESS_KEY_ID
@@ -133,17 +133,17 @@ node('built-in') {
133133
def AWS_SECRET_CREDENTIALS_ID = ''
134134

135135
// Decide the s3path based on release
136-
if (params.Release.equals('Production')) {
136+
if (params.Release == 'Production') {
137137
env.S3_PATH = 'mac/prod'
138138
env.S3_BUCKET = 'wire-taco'
139139
AWS_ACCESS_KEY_CREDENTIALS_ID = 'AWS_ACCESS_KEY_ID'
140140
AWS_SECRET_CREDENTIALS_ID = 'AWS_SECRET_ACCESS_KEY'
141-
} else if (params.Release.equals('Internal')) {
141+
} else if (params.Release == 'Internal') {
142142
env.S3_PATH = 'mac/internal'
143143
env.S3_BUCKET = 'wire-taco'
144144
AWS_ACCESS_KEY_CREDENTIALS_ID = 'AWS_ACCESS_KEY_ID'
145145
AWS_SECRET_CREDENTIALS_ID = 'AWS_SECRET_ACCESS_KEY'
146-
} else if (params.Release.equals('Custom')) {
146+
} else if (params.Release == 'Custom') {
147147
env.S3_BUCKET = params.MAC_S3_BUCKET
148148
env.S3_PATH = params.MAC_S3_PATH
149149
AWS_ACCESS_KEY_CREDENTIALS_ID = params.AWS_CUSTOM_ACCESS_KEY_ID
@@ -171,13 +171,69 @@ node('built-in') {
171171

172172
echo "Uploading ${pkgFilePath} to s3://${env.S3_BUCKET}/${s3DestinationPath}"
173173

174-
// Upload file
175-
s3Upload(
176-
acl: 'Private',
177-
bucket: env.S3_BUCKET,
178-
file: pkgFilePath,
179-
path: s3DestinationPath
180-
)
174+
if (params.DRY_RUN) {
175+
echo "DRY RUN enabled – skipping upload of macOS .pkg"
176+
} else {
177+
s3Upload(
178+
acl: 'Private',
179+
bucket: env.S3_BUCKET,
180+
file: pkgFilePath,
181+
path: s3DestinationPath
182+
)
183+
}
184+
185+
// ------------------------------------------------------------------
186+
// Internal macOS auto-update artifacts for electron-updater
187+
// (latest-mac.yml + WireInternal-<version>.zip)
188+
// ------------------------------------------------------------------
189+
if (params.Release == 'Internal') {
190+
echo 'Uploading internal macOS auto-update artifacts (latest-mac.yml + zip)'
191+
192+
def updatesPrefix = "${env.S3_PATH}/updates/"
193+
194+
// latest-mac.yml
195+
def latestFiles = findFiles(glob: 'wrap/dist/latest-mac.yml')
196+
if (latestFiles && latestFiles.size() > 0) {
197+
def latestPath = latestFiles[0].path
198+
def latestDest = updatesPrefix + 'latest-mac.yml'
199+
echo "Preparing latest-mac.yml upload to s3://${env.S3_BUCKET}/${latestDest}"
200+
201+
if (params.DRY_RUN) {
202+
echo "DRY RUN enabled – would upload ${latestPath} to s3://${env.S3_BUCKET}/${latestDest}"
203+
} else {
204+
s3Upload(
205+
acl: 'Private',
206+
bucket: env.S3_BUCKET,
207+
file: latestPath,
208+
path: latestDest
209+
)
210+
}
211+
} else {
212+
echo 'No latest-mac.yml found in wrap/dist – skipping upload'
213+
}
214+
215+
// WireInternal-<version>.zip
216+
def zipFiles = findFiles(glob: 'wrap/dist/WireInternal-*.zip')
217+
if (!zipFiles || zipFiles.size() == 0) {
218+
echo 'No WireInternal-*.zip found in wrap/dist – skipping zip upload'
219+
} else {
220+
zipFiles.each { z ->
221+
def zipDest = updatesPrefix + z.name
222+
echo "Preparing ${z.path} upload to s3://${env.S3_BUCKET}/${zipDest}"
223+
224+
if (params.DRY_RUN) {
225+
echo "DRY RUN enabled – would upload ${z.path} to s3://${env.S3_BUCKET}/${zipDest}"
226+
} else {
227+
s3Upload(
228+
acl: 'Private',
229+
bucket: env.S3_BUCKET,
230+
file: z.path,
231+
path: zipDest
232+
)
233+
}
234+
}
235+
}
236+
}
181237
}
182238
}
183239
} catch(e) {
@@ -192,7 +248,7 @@ node('built-in') {
192248
// 3) Linux S3 Upload
193249
// -----------------------------
194250
try {
195-
if (params.Release.equals('Production')) {
251+
if (params.Release == 'Production') {
196252
S3_NAME = 'linux'
197253

198254
withAWS(region:'eu-west-1', credentials: 'wire-taco') {
@@ -212,9 +268,9 @@ node('built-in') {
212268
path: S3_NAME + '/' + it.name
213269
}
214270
}
215-
} else if (params.Release.equals('Custom')) {
271+
} else if (params.Release == 'Custom') {
216272
error('Please set S3_NAME for custom Linux')
217-
} else if (params.Release.equals('Internal')) {
273+
} else if (params.Release == 'Internal') {
218274
S3_NAME = 'linux-internal'
219275

220276
withAWS(region:'eu-west-1', credentials: 'wire-taco') {
@@ -260,7 +316,7 @@ node('built-in') {
260316

261317
// Make sure we use Jenkins AWS plugin + local "aws" command
262318
withAWS(region: 'eu-west-1', credentials: 'wire-taco') {
263-
if (params.Release.equals('Custom')) {
319+
if (params.Release == 'Custom') {
264320
// Custom (on-prem)
265321
presignedFile = 'custom-presigned-urls.txt'
266322
sh "rm -f ${presignedFile}"
@@ -288,7 +344,7 @@ node('built-in') {
288344

289345
} else {
290346
// Internal or Production
291-
def fileSuffix = params.Release.equals('Production') ? 'prod' : 'internal'
347+
def fileSuffix = params.Release == 'Production' ? 'prod' : 'internal'
292348
presignedFile = "${fileSuffix}-presigned-urls.txt"
293349
sh "rm -f ${presignedFile}"
294350

@@ -328,17 +384,17 @@ node('built-in') {
328384
def AWS_ACCESS_KEY_CREDENTIALS_ID = ''
329385
def AWS_SECRET_CREDENTIALS_ID = ''
330386

331-
if (params.Release.equals('Production')) {
387+
if (params.Release == 'Production') {
332388
S3_PATH = 'win/prod'
333389
S3_NAME = 'wire-' + version
334390
AWS_ACCESS_KEY_CREDENTIALS_ID = 'AWS_ACCESS_KEY_ID'
335391
AWS_SECRET_CREDENTIALS_ID = 'AWS_SECRET_ACCESS_KEY'
336-
} else if (params.Release.equals('Internal')) {
392+
} else if (params.Release == 'Internal') {
337393
S3_PATH = 'win/internal'
338394
S3_NAME = 'wireinternal-' + version
339395
AWS_ACCESS_KEY_CREDENTIALS_ID = 'AWS_ACCESS_KEY_ID'
340396
AWS_SECRET_CREDENTIALS_ID = 'AWS_SECRET_ACCESS_KEY'
341-
} else if (params.Release.equals('Custom')) {
397+
} else if (params.Release == 'Custom') {
342398
S3_BUCKET = params.WIN_S3_BUCKET
343399
S3_PATH = params.WIN_S3_PATH
344400
S3_NAME = 'wire-ey-' + version
@@ -386,7 +442,7 @@ node('built-in') {
386442
// ------------------------------------------------------------------------
387443
// If this is macOS Production: upload .pkg to App Store Connect
388444
// ------------------------------------------------------------------------
389-
if (projectName.contains('macOS') && params.Release.equals('Production')) {
445+
if (projectName.contains('macOS') && params.Release == 'Production') {
390446
stage('Upload macOS pkg to App Store Connect') {
391447
try {
392448
// Find the .pkg that was copied from the build job
@@ -463,7 +519,7 @@ node('built-in') {
463519
// ------------------------------------------------------------------------
464520
// If Release == Production, do a draft GitHub release
465521
// ------------------------------------------------------------------------
466-
if (params.Release.equals('Production')) {
522+
if (params.Release == 'Production') {
467523
stage('Upload build as draft to GitHub') {
468524
try {
469525
withEnv(["PATH+NODE=${NODE}/bin"]) {

0 commit comments

Comments
 (0)