diff --git a/app/menu.ts b/app/menu.ts index 63815a07e1a..918534bf109 100644 --- a/app/menu.ts +++ b/app/menu.ts @@ -13,7 +13,7 @@ type OptionsType = { development: boolean; devTools: boolean; includeSetup: boolean; - isBeta: (version: string) => boolean; + isProduction: boolean; platform: string; // actions @@ -42,7 +42,7 @@ export const createTemplate = ( } const { - isBeta, + isProduction, devTools, includeSetup, openContactUs, @@ -212,7 +212,7 @@ export const createTemplate = ( label: messages.goToSupportPage.message, click: openSupportPage, }, - ...(!isBeta + ...(isProduction ? [ { label: messages.joinTheBeta.message, diff --git a/main.js b/main.js index ded3862e88e..fb1260ecfd1 100644 --- a/main.js +++ b/main.js @@ -104,7 +104,7 @@ const { } = require('./app/protocol_filter'); const { installPermissionsHandler } = require('./app/permissions'); const OS = require('./ts/OS'); -const { isBeta } = require('./ts/util/version'); +const { isProduction } = require('./ts/util/version'); const { isSgnlHref, isCaptchaHref, @@ -149,7 +149,7 @@ const defaultWebPrefs = { devTools: process.argv.some(arg => arg === '--enable-dev-tools') || config.environment !== Environment.Production || - isBeta(app.getVersion()), + !isProduction(app.getVersion()), }; async function getSpellCheckSetting() { @@ -1400,7 +1400,7 @@ function setupMenu(options) { const menuOptions = { ...options, development, - isBeta: isBeta(app.getVersion()), + isProduction: isProduction(app.getVersion()), devTools: defaultWebPrefs.devTools, showDebugLog: showDebugLogWindow, showKeyboardShortcuts, diff --git a/package.json b/package.json index ebe7733acd4..b096e73af65 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,10 @@ "clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js", "build-protobuf": "yarn build-module-protobuf", "clean-protobuf": "yarn clean-module-protobuf", - "prepare-beta-build": "node prepare_beta_build.js", - "prepare-import-build": "node prepare_import_build.js", + "prepare-beta-build": "node scripts/prepare_beta_build.js", + "prepare-alpha-build": "node scripts/prepare_alpha_build.js", + "prepare-alpha-version": "node scripts/prepare_alpha_version.js", + "prepare-windows-cert": "node scripts/prepare_windows_cert.js", "publish-to-apt": "NAME=$npm_package_name VERSION=$npm_package_version ./aptly.sh", "test": "yarn test-node && yarn test-electron", "test-electron": "yarn grunt test", @@ -329,9 +331,12 @@ "node_modules/@signalapp/signal-client/build/*.node" ], "artifactName": "${name}-win-${version}.${ext}", - "certificateSubjectName": "Signal (Quiet Riddle Ventures, LLC)", - "certificateSha1": "77B2AA4421E5F377454B8B91E573746592D1543D", - "publisherName": "Signal (Quiet Riddle Ventures, LLC)", + "certificateSubjectName": "Signal Messenger, LLC", + "certificateSha1": "8C9A0B5C852EC703D83EF7BFBCEB54B796073759", + "signingHashAlgorithms": [ + "sha256" + ], + "publisherName": "Signal Messenger, LLC", "icon": "build/icons/win/icon.ico", "publish": [ { diff --git a/scripts/prepare_alpha_build.js b/scripts/prepare_alpha_build.js new file mode 100644 index 00000000000..68cc211bd45 --- /dev/null +++ b/scripts/prepare_alpha_build.js @@ -0,0 +1,78 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/* eslint-disable no-console */ + +const fs = require('fs'); +const _ = require('lodash'); + +const packageJson = require('../package.json'); +const { isAlpha } = require('../ts/util/version'); + +const { version } = packageJson; + +// You might be wondering why this file is necessary. It comes down to our desire to allow +// side-by-side installation of production and alpha builds. Electron-Builder uses +// top-level data from package.json for many things, like the executable name, the +// debian package name, the install directory under /opt on linux, etc. We tried +// adding the ${channel} macro to these values, but Electron-Builder didn't like that. + +if (!isAlpha(version)) { + process.exit(); +} + +console.log('prepare_alpha_build: updating package.json'); + +// ------- + +const NAME_PATH = 'name'; +const PRODUCTION_NAME = 'signal-desktop'; +const ALPHA_NAME = 'signal-desktop-alpha'; + +const PRODUCT_NAME_PATH = 'productName'; +const PRODUCTION_PRODUCT_NAME = 'Signal'; +const ALPHA_PRODUCT_NAME = 'Signal Alpha'; + +const APP_ID_PATH = 'build.appId'; +const PRODUCTION_APP_ID = 'org.whispersystems.signal-desktop'; +const ALPHA_APP_ID = 'org.whispersystems.signal-desktop-alpha'; + +const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass'; +const PRODUCTION_STARTUP_WM_CLASS = 'Signal'; +const ALPHA_STARTUP_WM_CLASS = 'Signal Alpha'; + +const DESKTOP_NAME_PATH = 'desktopName'; + +// Note: we're avoiding dashes in our .desktop name due to xdg-settings behavior +// https://github.com/signalapp/Signal-Desktop/issues/3602 +const PRODUCTION_DESKTOP_NAME = 'signal.desktop'; +const ALPHA_DESKTOP_NAME = 'signalalpha.desktop'; + +// ------- + +function checkValue(object, objectPath, expected) { + const actual = _.get(object, objectPath); + if (actual !== expected) { + throw new Error(`${objectPath} was ${actual}; expected ${expected}`); + } +} + +// ------ + +checkValue(packageJson, NAME_PATH, PRODUCTION_NAME); +checkValue(packageJson, PRODUCT_NAME_PATH, PRODUCTION_PRODUCT_NAME); +checkValue(packageJson, APP_ID_PATH, PRODUCTION_APP_ID); +checkValue(packageJson, STARTUP_WM_CLASS_PATH, PRODUCTION_STARTUP_WM_CLASS); +checkValue(packageJson, DESKTOP_NAME_PATH, PRODUCTION_DESKTOP_NAME); + +// ------- + +_.set(packageJson, NAME_PATH, ALPHA_NAME); +_.set(packageJson, PRODUCT_NAME_PATH, ALPHA_PRODUCT_NAME); +_.set(packageJson, APP_ID_PATH, ALPHA_APP_ID); +_.set(packageJson, STARTUP_WM_CLASS_PATH, ALPHA_STARTUP_WM_CLASS); +_.set(packageJson, DESKTOP_NAME_PATH, ALPHA_DESKTOP_NAME); + +// ------- + +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' ')); diff --git a/scripts/prepare_alpha_version.js b/scripts/prepare_alpha_version.js new file mode 100644 index 00000000000..d7198cf9e1c --- /dev/null +++ b/scripts/prepare_alpha_version.js @@ -0,0 +1,33 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/* eslint-disable no-console */ + +const fs = require('fs'); +const { execSync } = require('child_process'); + +const _ = require('lodash'); + +const { generateAlphaVersion } = require('../ts/util/version'); + +const packageJson = require('../package.json'); + +const { version: currentVersion } = packageJson; + +const shortSha = execSync('git rev-parse --short HEAD') + .toString('utf8') + .replace(/[\n\r]/g, ''); + +const alphaVersion = generateAlphaVersion({ currentVersion, shortSha }); + +console.log( + `prepare_alpha_version: updating package.json.\n Previous: ${currentVersion}\n New: ${alphaVersion}` +); + +// ------- + +_.set(packageJson, 'version', alphaVersion); + +// ------- + +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' ')); diff --git a/prepare_beta_build.js b/scripts/prepare_beta_build.js similarity index 96% rename from prepare_beta_build.js rename to scripts/prepare_beta_build.js index 2540d92df07..478d5050ad5 100644 --- a/prepare_beta_build.js +++ b/scripts/prepare_beta_build.js @@ -6,8 +6,8 @@ const fs = require('fs'); const _ = require('lodash'); -const packageJson = require('./package.json'); -const { isBeta } = require('./ts/util/version'); +const packageJson = require('../package.json'); +const { isBeta } = require('../ts/util/version'); const { version } = packageJson; diff --git a/scripts/prepare_windows_cert.js b/scripts/prepare_windows_cert.js new file mode 100644 index 00000000000..ca0cc17bc52 --- /dev/null +++ b/scripts/prepare_windows_cert.js @@ -0,0 +1,50 @@ +// Copyright 2018-2020 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +/* eslint-disable no-console */ + +const fs = require('fs'); +const _ = require('lodash'); + +const packageJson = require('../package.json'); + +// We have different windows certificates used in each of our build machines, and this +// script makes it easier to ready the app to build on a given machine. + +// ------- + +const KEY = 'build.win.certificateSha1'; +const DEFAULT_VALUE = '8C9A0B5C852EC703D83EF7BFBCEB54B796073759'; + +const BUILDER_A = '507769334DA990A8DDE858314B0CDFC228E7CFA1'; +const BUILDER_B = 'C689B0988CA1A7DF99E4CE4433AC7EA8B82F8D41'; + +let targetValue = DEFAULT_VALUE; + +if (process.env.WINDOWS_BUILDER === 'A') { + targetValue = BUILDER_A; +} +if (process.env.WINDOWS_BUILDER === 'B') { + targetValue = BUILDER_B; +} + +// ------- + +function checkValue(object, objectPath, expected) { + const actual = _.get(object, objectPath); + if (actual !== expected) { + throw new Error(`${objectPath} was ${actual}; expected ${expected}`); + } +} + +// ------ + +checkValue(packageJson, KEY, DEFAULT_VALUE); + +// ------- + +_.set(packageJson, KEY, targetValue); + +// ------- + +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' ')); diff --git a/test/app/menu_test.js b/test/app/menu_test.js index c4fe6b8c4cb..fbdfb03165a 100644 --- a/test/app/menu_test.js +++ b/test/app/menu_test.js @@ -49,7 +49,7 @@ describe('SignalMenu', () => { }, }; const options = { - isBeta: false, + isProduction: true, devTools: true, openContactUs: null, openForums: null, diff --git a/ts/test-both/util/version_test.ts b/ts/test-both/util/version_test.ts index f21cb138060..129c1306b53 100644 --- a/ts/test-both/util/version_test.ts +++ b/ts/test-both/util/version_test.ts @@ -2,10 +2,31 @@ // SPDX-License-Identifier: AGPL-3.0-only import { assert } from 'chai'; +import { useFakeTimers } from 'sinon'; +import * as semver from 'semver'; -import { isBeta } from '../../util/version'; +import { + generateAlphaVersion, + isAlpha, + isBeta, + isProduction, +} from '../../util/version'; describe('version utilities', () => { + describe('isProduction', () => { + it('returns false for anything non-basic version number', () => { + assert.isFalse(isProduction('1.2.3-1')); + assert.isFalse(isProduction('1.2.3-alpha.1')); + assert.isFalse(isProduction('1.2.3-beta.1')); + assert.isFalse(isProduction('1.2.3-rc')); + }); + + it('returns true for production version strings', () => { + assert.isTrue(isProduction('1.2.3')); + assert.isTrue(isProduction('5.10.0')); + }); + }); + describe('isBeta', () => { it('returns false for non-beta version strings', () => { assert.isFalse(isBeta('1.2.3')); @@ -19,4 +40,88 @@ describe('version utilities', () => { assert.isTrue(isBeta('1.2.3-beta.1')); }); }); + + describe('isAlpha', () => { + it('returns false for non-alpha version strings', () => { + assert.isFalse(isAlpha('1.2.3')); + assert.isFalse(isAlpha('1.2.3-beta')); + assert.isFalse(isAlpha('1.2.3-beta.1')); + assert.isFalse(isAlpha('1.2.3-rc.1')); + }); + + it('returns true for Alpha version strings', () => { + assert.isTrue(isAlpha('1.2.3-alpha')); + assert.isTrue(isAlpha('1.2.3-alpha.1')); + }); + }); + + describe('generateAlphaVersion', () => { + beforeEach(function beforeEach() { + // This isn't a hook. + // eslint-disable-next-line react-hooks/rules-of-hooks + this.clock = useFakeTimers(); + }); + + afterEach(function afterEach() { + this.clock.restore(); + }); + + it('uses the current date and provided shortSha', function test() { + this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + const expected = '5.12.0-alpha.20210723.01-07f0efc45'; + const actual = generateAlphaVersion({ currentVersion, shortSha }); + + assert.strictEqual(expected, actual); + }); + + it('same production version is semver.gt', function test() { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actual = generateAlphaVersion({ currentVersion, shortSha }); + + assert.isTrue(semver.gt('5.12.0', actual)); + }); + + it('same beta version is semver.gt', function test() { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actual = generateAlphaVersion({ currentVersion, shortSha }); + + assert.isTrue(semver.gt(currentVersion, actual)); + }); + + it('build earlier same day is semver.lt', function test() { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + this.clock.setSystemTime(new Date('2021-07-23T00:22:55.692Z').getTime()); + const actualEarlier = generateAlphaVersion({ currentVersion, shortSha }); + + this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actualLater = generateAlphaVersion({ currentVersion, shortSha }); + + assert.isTrue(semver.lt(actualEarlier, actualLater)); + }); + + it('build previous day is semver.lt', function test() { + const currentVersion = '5.12.0-beta.1'; + const shortSha = '07f0efc45'; + + this.clock.setSystemTime(new Date('2021-07-22T01:22:55.692Z').getTime()); + const actualEarlier = generateAlphaVersion({ currentVersion, shortSha }); + + this.clock.setSystemTime(new Date('2021-07-23T01:22:55.692Z').getTime()); + const actualLater = generateAlphaVersion({ currentVersion, shortSha }); + + assert.isTrue(semver.lt(actualEarlier, actualLater)); + }); + }); }); diff --git a/ts/types/Settings.ts b/ts/types/Settings.ts index 02e15c38e70..8cbb710421f 100644 --- a/ts/types/Settings.ts +++ b/ts/types/Settings.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as OS from '../OS'; -import { isBeta } from '../util/version'; +import { isProduction } from '../util/version'; const MIN_WINDOWS_VERSION = '8.0.0'; @@ -57,4 +57,4 @@ export const getTitleBarVisibility = (): TitleBarVisibility => */ export const isSystemTraySupported = (appVersion: string): boolean => // We eventually want to support Linux in production. - OS.isWindows() || (OS.isLinux() && isBeta(appVersion)); + OS.isWindows() || (OS.isLinux() && !isProduction(appVersion)); diff --git a/ts/updater/common.ts b/ts/updater/common.ts index 2ee00299d3a..f5e626e12f8 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -25,6 +25,7 @@ import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import { getTempPath } from '../../app/attachments'; import { Dialogs } from '../types/Dialogs'; import { getUserAgent } from '../util/getUserAgent'; +import { isAlpha, isBeta } from '../util/version'; import * as packageJson from '../../package.json'; import { getSignatureFileName } from './signature'; @@ -260,7 +261,7 @@ export function getProxyUrl(): string | undefined { } export function getUpdatesFileName(): string { - const prefix = isBetaChannel() ? 'beta' : 'latest'; + const prefix = getChannel(); if (platform === 'darwin') { return `${prefix}-mac.yml`; @@ -269,9 +270,16 @@ export function getUpdatesFileName(): string { return `${prefix}.yml`; } -const hasBeta = /beta/i; -function isBetaChannel(): boolean { - return hasBeta.test(packageJson.version); +function getChannel(): string { + const { version } = packageJson; + + if (isAlpha(version)) { + return 'alpha'; + } + if (isBeta(version)) { + return 'beta'; + } + return 'latest'; } function isVersionNewer(newVersion: string): boolean { diff --git a/ts/util/handleRetry.ts b/ts/util/handleRetry.ts index 75162119343..e80e2d32b07 100644 --- a/ts/util/handleRetry.ts +++ b/ts/util/handleRetry.ts @@ -7,7 +7,7 @@ import { } from '@signalapp/signal-client'; import { isNumber } from 'lodash'; -import { isBeta } from './version'; +import { isProduction } from './version'; import { strictAssert } from './assert'; import { getSendOptions } from './getSendOptions'; import { handleMessageSend } from './handleMessageSend'; @@ -124,7 +124,7 @@ export async function onRetryRequest(event: RetryRequestEvent): Promise { } function maybeShowDecryptionToast(logId: string) { - if (!isBeta(window.getVersion())) { + if (isProduction(window.getVersion())) { return; } diff --git a/ts/util/isGroupCallingEnabled.ts b/ts/util/isGroupCallingEnabled.ts index 2ee31b96ae5..831d9a6e3da 100644 --- a/ts/util/isGroupCallingEnabled.ts +++ b/ts/util/isGroupCallingEnabled.ts @@ -1,13 +1,13 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { isBeta } from './version'; +import { isProduction } from './version'; import * as RemoteConfig from '../RemoteConfig'; // We can remove this function once group calling has been turned on for everyone. export function isGroupCallingEnabled(): boolean { return ( RemoteConfig.isEnabled('desktop.groupCalling') || - isBeta(window.getVersion()) + !isProduction(window.getVersion()) ); } diff --git a/ts/util/isScreenSharingEnabled.ts b/ts/util/isScreenSharingEnabled.ts index 7d5e5919f13..a32e2c03129 100644 --- a/ts/util/isScreenSharingEnabled.ts +++ b/ts/util/isScreenSharingEnabled.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as RemoteConfig from '../RemoteConfig'; -import { isBeta } from './version'; +import { isProduction } from './version'; // We can remove this function once screen sharing has been turned on for everyone export function isScreenSharingEnabled(): boolean { @@ -12,6 +12,6 @@ export function isScreenSharingEnabled(): boolean { return Boolean( RemoteConfig.isEnabled('desktop.internalUser') || RemoteConfig.isEnabled('desktop.screensharing2') || - (version && isBeta(version)) + (version && !isProduction(version)) ); } diff --git a/ts/util/version.ts b/ts/util/version.ts index 0f2b0a7d02b..59410dce496 100644 --- a/ts/util/version.ts +++ b/ts/util/version.ts @@ -2,6 +2,37 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as semver from 'semver'; +import moment from 'moment'; + +export const isProduction = (version: string): boolean => { + const parsed = semver.parse(version); + + if (!parsed) { + return false; + } + + return !parsed.prerelease.length && !parsed.build.length; +}; export const isBeta = (version: string): boolean => semver.parse(version)?.prerelease[0] === 'beta'; + +export const isAlpha = (version: string): boolean => + semver.parse(version)?.prerelease[0] === 'alpha'; + +export const generateAlphaVersion = (options: { + currentVersion: string; + shortSha: string; +}): string => { + const { currentVersion, shortSha } = options; + + const parsed = semver.parse(currentVersion); + if (!parsed) { + throw new Error(`generateAlphaVersion: Invalid version ${currentVersion}`); + } + + const formattedDate = moment().utc().format('YYYYMMDD.HH'); + const formattedVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`; + + return `${formattedVersion}-alpha.${formattedDate}-${shortSha}`; +};