From 97d31cd1a5a13b647c5718557c559142d1723fde Mon Sep 17 00:00:00 2001 From: trevor-signal <131492920+trevor-signal@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:42:40 -0500 Subject: [PATCH] Add adhoc release channel for public ad-hoc testing --- package.json | 2 + reproducible-builds/docker-entrypoint.sh | 3 + scripts/prepare_adhoc_build.js | 100 +++++++++++++++++++++++ scripts/prepare_tagged_version.js | 2 +- ts/services/backups/import.ts | 4 +- ts/services/backups/index.ts | 4 +- ts/updater/common.ts | 20 ++++- ts/util/isLinkAndSyncEnabled.ts | 9 +- ts/util/version.ts | 3 + 9 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 scripts/prepare_adhoc_build.js diff --git a/package.json b/package.json index 6c7a38267..c43edca6e 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "prepare-alpha-version": "node scripts/prepare_tagged_version.js alpha", "prepare-axolotl-build": "node scripts/prepare_axolotl_build.js", "prepare-axolotl-version": "node scripts/prepare_tagged_version.js axolotl", + "prepare-adhoc-build": "node scripts/prepare_adhoc_build.js", + "prepare-adhoc-version": "node scripts/prepare_tagged_version.js adhoc", "prepare-staging-build": "node scripts/prepare_staging_build.js", "prepare-windows-cert": "node scripts/prepare_windows_cert.js", "test": "run-s test-node test-electron test-lint-intl test-eslint", diff --git a/reproducible-builds/docker-entrypoint.sh b/reproducible-builds/docker-entrypoint.sh index d8210a70f..5e156926e 100644 --- a/reproducible-builds/docker-entrypoint.sh +++ b/reproducible-builds/docker-entrypoint.sh @@ -47,6 +47,9 @@ elif [ "${BUILD_TYPE}" = "alpha" ]; then elif [ "${BUILD_TYPE}" = "axolotl" ]; then npm run prepare-axolotl-version npm run prepare-axolotl-build +elif [ "${BUILD_TYPE}" = "adhoc" ]; then + npm run prepare-adhoc-version + npm run prepare-adhoc-build elif [ "${BUILD_TYPE}" = "staging" ]; then npm run prepare-alpha-version npm run prepare-staging-build diff --git a/scripts/prepare_adhoc_build.js b/scripts/prepare_adhoc_build.js new file mode 100644 index 000000000..6f23ef602 --- /dev/null +++ b/scripts/prepare_adhoc_build.js @@ -0,0 +1,100 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +const fs = require('fs'); +const _ = require('lodash'); +const { execSync } = require('child_process'); + +const packageJson = require('../package.json'); +const { isAdHoc } = 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 adhoc 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 (!isAdHoc(version)) { + console.error(`Version '${version}' is not an adhoc version!`); + process.exit(1); +} + +const shortSha = execSync('git rev-parse --short HEAD') + .toString('utf8') + .replace(/[\n\r]/g, ''); + +const dateTimeParts = new Intl.DateTimeFormat('en', { + day: '2-digit', + hour: '2-digit', + hourCycle: 'h23', + month: '2-digit', + timeZone: 'GMT', + year: 'numeric', +}).formatToParts(new Date()); +const dateTimeMap = new Map(); +dateTimeParts.forEach(({ type, value }) => { + dateTimeMap.set(type, value); +}); +const formattedDate = `${dateTimeMap.get('year')}${dateTimeMap.get( + 'month' +)}${dateTimeMap.get('day')}`; + +console.log( + `prepare_adhoc_build(adhoc-${formattedDate}-${shortSha}): updating package.json` +); + +// ------- + +const NAME_PATH = 'name'; +const PRODUCTION_NAME = 'signal-desktop'; +const ADHOC_NAME = `signal-desktop-adhoc-${formattedDate}-${shortSha}`; + +const PRODUCT_NAME_PATH = 'productName'; +const PRODUCTION_PRODUCT_NAME = 'Signal'; +const ADHOC_PRODUCT_NAME = `Signal AdHoc ${formattedDate} ${shortSha}`; + +const APP_ID_PATH = 'build.appId'; +const PRODUCTION_APP_ID = 'org.whispersystems.signal-desktop'; +const ADHOC_APP_ID = `org.whispersystems.signal-desktop-adhoc-${formattedDate}-${shortSha}`; + +const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass'; +const PRODUCTION_STARTUP_WM_CLASS = 'Signal'; +const ADHOC_STARTUP_WM_CLASS = `Signal AdHoc ${formattedDate} ${shortSha}`; + +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 ADHOC_DESKTOP_NAME = `signaladhoc.${formattedDate}.${shortSha}.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, ADHOC_NAME); +_.set(packageJson, PRODUCT_NAME_PATH, ADHOC_PRODUCT_NAME); +_.set(packageJson, APP_ID_PATH, ADHOC_APP_ID); +_.set(packageJson, STARTUP_WM_CLASS_PATH, ADHOC_STARTUP_WM_CLASS); +_.set(packageJson, DESKTOP_NAME_PATH, ADHOC_DESKTOP_NAME); + +// ------- + +fs.writeFileSync('./package.json', JSON.stringify(packageJson, null, ' ')); diff --git a/scripts/prepare_tagged_version.js b/scripts/prepare_tagged_version.js index 99f7903ab..603d275d2 100644 --- a/scripts/prepare_tagged_version.js +++ b/scripts/prepare_tagged_version.js @@ -7,7 +7,7 @@ const { execSync } = require('child_process'); const _ = require('lodash'); const release = process.argv[2]; -if (release !== 'alpha' && release !== 'axolotl') { +if (release !== 'alpha' && release !== 'axolotl' && release !== 'adhoc') { console.error(`Invalid release line: ${release}`); process.exit(1); } diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index 59dcc375b..af5fb4fc4 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -119,7 +119,7 @@ import { } from '../../util/backupMediaDownload'; import { getEnvironment, isTestEnvironment } from '../../environment'; import { hasAttachmentDownloads } from '../../util/hasAttachmentDownloads'; -import { isNightly } from '../../util/version'; +import { isAdhoc, isNightly } from '../../util/version'; import { ToastType } from '../../types/Toast'; import { isConversationAccepted } from '../../util/isConversationAccepted'; import { saveBackupsSubscriberData } from '../../util/backupSubscriptionData'; @@ -382,7 +382,7 @@ export class BackupImportStream extends Writable { log.error( `${this.logId}: errored while processing ${this.frameErrorCount} frames.` ); - if (isNightly(window.getVersion())) { + if (isNightly(window.getVersion()) || isAdhoc(window.getVersion())) { window.reduxActions.toast.showToast({ toastType: ToastType.FailedToImportBackup, }); diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 97416865c..e4f700d27 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -57,7 +57,7 @@ import { UnsupportedBackupVersion, } from './errors'; import { ToastType } from '../../types/Toast'; -import { isNightly } from '../../util/version'; +import { isAdhoc, isNightly } from '../../util/version'; export { BackupType }; @@ -431,7 +431,7 @@ export class BackupsService { } catch (error) { log.info(`importBackup: failed, error: ${Errors.toLogFormat(error)}`); - if (isNightly(window.getVersion())) { + if (isNightly(window.getVersion()) || isAdhoc(window.getVersion())) { window.reduxActions.toast.showToast({ toastType: ToastType.FailedToImportBackup, }); diff --git a/ts/updater/common.ts b/ts/updater/common.ts index b14e11530..aa3f3e9a8 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -27,7 +27,13 @@ import * as Errors from '../types/errors'; import { strictAssert } from '../util/assert'; import { drop } from '../util/drop'; import * as durations from '../util/durations'; -import { isAlpha, isAxolotl, isBeta, isStaging } from '../util/version'; +import { + isAdhoc, + isAlpha, + isAxolotl, + isBeta, + isStaging, +} from '../util/version'; import * as packageJson from '../../package.json'; import type { SettingsChannel } from '../main/settingsChannel'; @@ -489,6 +495,13 @@ export abstract class Updater { private async checkForUpdates( checkType: CheckType ): Promise { + if (isAdhoc(packageJson.version)) { + this.logger.info( + 'checkForUpdates: not checking for updates, this is an adhoc build' + ); + return; + } + const yaml = await getUpdateYaml(); const parsedYaml = parseYaml(yaml); @@ -905,7 +918,10 @@ export function getUpdatesFileName(): string { function getChannel(): string { const { version } = packageJson; - + if (isAdhoc(version)) { + // we don't want ad hoc versions to update + return version; + } if (isStaging(version)) { return 'staging'; } diff --git a/ts/util/isLinkAndSyncEnabled.ts b/ts/util/isLinkAndSyncEnabled.ts index 09271dccb..7c48c1090 100644 --- a/ts/util/isLinkAndSyncEnabled.ts +++ b/ts/util/isLinkAndSyncEnabled.ts @@ -3,7 +3,7 @@ import { isTestOrMockEnvironment } from '../environment'; import { isStagingServer } from './isStagingServer'; -import { isNightly } from './version'; +import { isAdhoc, isNightly } from './version'; import { everDone as wasRegistrationEverDone } from './registration'; export function isLinkAndSyncEnabled(version: string): boolean { @@ -12,5 +12,10 @@ export function isLinkAndSyncEnabled(version: string): boolean { return false; } - return isStagingServer() || isTestOrMockEnvironment() || isNightly(version); + return ( + isStagingServer() || + isTestOrMockEnvironment() || + isNightly(version) || + isAdhoc(version) + ); } diff --git a/ts/util/version.ts b/ts/util/version.ts index 22b7f4715..3d7100468 100644 --- a/ts/util/version.ts +++ b/ts/util/version.ts @@ -25,6 +25,9 @@ export const isAlpha = (version: string): boolean => export const isAxolotl = (version: string): boolean => semver.parse(version)?.prerelease[0] === 'axolotl'; +export const isAdhoc = (version: string): boolean => + semver.parse(version)?.prerelease[0] === 'adhoc'; + export const isStaging = (version: string): boolean => semver.parse(version)?.prerelease[0] === 'staging';