From 1c55aa780f7632a301e6f80b1a8a717e3c157698 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 8 Oct 2025 11:45:46 +0100 Subject: [PATCH] Improve workflows for better reusability (#2608) --- .github/workflows/build_linux.yaml | 67 ++++++++++++----- .github/workflows/build_macos.yaml | 41 +++++++++-- .github/workflows/build_test.yaml | 13 +++- .github/workflows/build_windows.yaml | 46 ++++++++++-- electron-builder.ts | 1 + src/electron-main.ts | 105 +++++++++++++++------------ 6 files changed, 197 insertions(+), 76 deletions(-) diff --git a/.github/workflows/build_linux.yaml b/.github/workflows/build_linux.yaml index 67bf617641..b72102955e 100644 --- a/.github/workflows/build_linux.yaml +++ b/.github/workflows/build_linux.yaml @@ -39,15 +39,39 @@ on: The artifact can also contain any additional files which will be applied as overrides to the checkout root before building, for example icons in the `build/` directory to override the app icons. default: "webapp" + test: + type: boolean + required: false + default: true + description: "Whether to run the test stage after building" + test-args: + type: string + required: false + description: "Additional arguments to pass to playwright" + runs-on: + type: string + required: false + description: "The runner image to use, normally set for you, may be needed for running in private repos." + artifact-prefix: + type: string + required: false + description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos." + default: "" + targets: + type: string + required: false + description: "List of targets to build" + default: "tar.gz deb" env: SQLCIPHER_BUNDLED: ${{ inputs.sqlcipher == 'static' && '1' || '' }} MAX_GLIBC: 2.31 # bullseye-era glibc, used by glibc-check.sh permissions: {} # No permissions required jobs: build: + name: Build Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }} # We build on native infrastructure as matrix-seshat fails to cross-compile properly # https://github.com/matrix-org/seshat/issues/135 - runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }} + runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }} env: HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-desktop-dockerbuild steps: @@ -109,7 +133,7 @@ jobs: - name: "Get modified files" id: changed_files - if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' + if: steps.cache.outputs.cache-hit != 'true' && github.event_name == 'pull_request' && github.repository == 'element-hq/element-desktop' uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46 with: files: | @@ -159,13 +183,11 @@ jobs: echo "USE_SYSTEM_FPM=true" >> $GITHUB_ENV - name: Build App - run: yarn build --publish never -l ${{ steps.config.outputs.build-args }} + run: yarn build --publish never ${{ steps.config.outputs.build-args }} -l ${{ inputs.targets }} env: VARIANT_PATH: variant.json # Only set for Nightly builds VERSION: ${{ inputs.version }} - FPM_DEBUG: true # TODO - DEBUG: electron-builder # TODO # Workaround for https://github.com/electron-userland/electron-builder/issues/5721 USE_HARD_LINKS: false @@ -199,39 +221,46 @@ jobs: - name: Upload Artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} + name: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} path: | dist !dist/*-unpacked/** retention-days: 1 - - name: Assert all required files are present + - name: Assert deb is present and valid + if: contains(inputs.targets, 'deb') run: | test -f ./dist/element-desktop*$ARCH.deb - test -f ./dist/element-desktop*.tar.gz - env: - ARCH: ${{ inputs.arch }} - - - name: Assert no hardlinks are found - run: | - TAR_GZ_LISTING=$(tar -tvf ./dist/element-desktop*.tar.gz) - echo "tar.gz listing: " - echo "$TAR_GZ_LISTING" - ! echo "$TAR_GZ_LISTING" | grep '^h' DEB_LISTING=$(dpkg-deb --fsys-tarfile ./dist/element-desktop*.deb | tar -tv) echo "deb listing: " echo "$DEB_LISTING" ! echo "$DEB_LISTING" | grep '^h' + env: + ARCH: ${{ inputs.arch }} + + - name: Assert tar.gz is present + if: contains(inputs.targets, 'tar.gz') + run: | + test -f ./dist/element-desktop*.tar.gz + + TAR_GZ_LISTING=$(tar -tvf ./dist/element-desktop*.tar.gz) + echo "tar.gz listing: " + echo "$TAR_GZ_LISTING" + ! echo "$TAR_GZ_LISTING" | grep '^h' test: + name: Test Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }} needs: build + if: inputs.test && contains(inputs.targets, 'deb') uses: ./.github/workflows/build_test.yaml with: - artifact: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} - runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }} + project: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} + artifact: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} + runs-on: ${{ inputs.runs-on || (inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04') }} executable: /opt/Element*/element-desktop* prepare_cmd: | sudo apt-get -qq update sudo apt install ./dist/*.deb blob_report: ${{ inputs.blob_report }} + args: ${{ inputs.test-args }} diff --git a/.github/workflows/build_macos.yaml b/.github/workflows/build_macos.yaml index 9a003b4603..0c14a1939c 100644 --- a/.github/workflows/build_macos.yaml +++ b/.github/workflows/build_macos.yaml @@ -49,9 +49,29 @@ on: The artifact can also contain any additional files which will be applied as overrides to the checkout root before building, for example icons in the `build/` directory to override the app icons. default: "webapp" + test: + type: boolean + required: false + default: true + description: "Whether to run the test stage after building" + test-args: + type: string + required: false + description: "Additional arguments to pass to playwright" + artifact-prefix: + type: string + required: false + description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos." + default: "" + targets: + type: string + required: false + description: "List of targets to build" + default: "dmg zip" permissions: {} # No permissions required jobs: build: + name: Build macOS Universal runs-on: macos-14 # M1 environment: ${{ inputs.sign && 'packages.element.io' || '' }} steps: @@ -105,7 +125,7 @@ jobs: - name: "[Signed] Build App" if: inputs.sign != '' run: | - yarn build:universal --publish never + yarn build:universal --publish never -m ${{ inputs.targets }} env: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_ID: ${{ secrets.APPLE_ID }} @@ -127,7 +147,7 @@ jobs: - name: "[Unsigned] Build App" if: inputs.sign == '' run: | - yarn build:universal --publish never + yarn build:universal --publish never -m ${{ inputs.targets }} env: CSC_IDENTITY_AUTO_DISCOVERY: false VARIANT_PATH: variant.json @@ -162,22 +182,30 @@ jobs: - name: Upload Artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: macos + name: ${{ inputs.artifact-prefix }}macos path: | dist !dist/mac-universal/** retention-days: 1 - - name: Assert all required files are present + - name: Assert zip is present + if: contains(inputs.targets, 'zip') run: | - test -f ./dist/Element*.dmg test -f ./dist/Element*-mac.zip + - name: Assert dmg is present + if: contains(inputs.targets, 'dmg') + run: | + test -f ./dist/Element*.dmg + test: + name: Test macOS Universal needs: build + if: inputs.test && contains(inputs.targets, 'dmg') uses: ./.github/workflows/build_test.yaml with: - artifact: macos + project: macos + artifact: ${{ inputs.artifact-prefix }}macos runs-on: macos-14 executable: /Users/runner/Applications/Element*.app/Contents/MacOS/Element* # We need to mount the DMG and copy the app to the Applications folder as a mounted DMG is @@ -187,3 +215,4 @@ jobs: rsync -a /Volumes/Element/Element*.app ~/Applications/ && hdiutil detach /Volumes/Element blob_report: ${{ inputs.blob_report }} + args: ${{ inputs.test-args }} diff --git a/.github/workflows/build_test.yaml b/.github/workflows/build_test.yaml index 7bfa3f8ef3..2ffbfc8561 100644 --- a/.github/workflows/build_test.yaml +++ b/.github/workflows/build_test.yaml @@ -10,6 +10,10 @@ on: type: string required: true description: "The name of the artifact to download" + project: + type: string + required: true + description: "The Playwright project to use for testing" executable: type: string required: true @@ -22,12 +26,19 @@ on: type: boolean default: false description: "Whether to upload a blob report instead of the HTML report" + args: + type: string + required: false + description: "Additional arguments to pass to playwright, for e.g. skipping specific tests" permissions: {} jobs: test: + name: Test ${{ inputs.project }} runs-on: ${{ inputs.runs-on }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + repository: ${{ github.repository == 'element-hq/element-web-pro' && 'element-hq/element-desktop' || github.repository }} - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: @@ -69,7 +80,7 @@ jobs: uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a timeout-minutes: 20 with: - run: yarn test --project=${{ inputs.artifact }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }} + run: yarn test --project=${{ inputs.project }} ${{ runner.os != 'Linux' && '--ignore-snapshots' || '' }} ${{ inputs.blob_report == false && '--reporter=html' || '' }} ${{ inputs.args }} env: ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }} diff --git a/.github/workflows/build_windows.yaml b/.github/workflows/build_windows.yaml index e21478012e..f2d2f33bea 100644 --- a/.github/workflows/build_windows.yaml +++ b/.github/workflows/build_windows.yaml @@ -53,9 +53,33 @@ on: The artifact can also contain any additional files which will be applied as overrides to the checkout root before building, for example icons in the `build/` directory to override the app icons. default: "webapp" + test: + type: boolean + required: false + default: true + description: "Whether to run the test stage after building" + test-runs-on: + type: string + required: false + description: "The runner image to use for testing, normally set for you, may be needed for running in private repos." + test-args: + type: string + required: false + description: "Additional arguments to pass to playwright" + artifact-prefix: + type: string + required: false + description: "An optional prefix to add to the artifact name, useful for distinguishing builds in private repos." + default: "" + targets: + type: string + required: false + description: "List of targets to build" + default: "squirrel msi" permissions: {} # No permissions required jobs: build: + name: Build Windows ${{ inputs.arch }} runs-on: windows-2025 environment: ${{ inputs.sign && 'packages.element.io' || '' }} env: @@ -208,7 +232,7 @@ jobs: MASTER_KEY_FILE: C:\Users\runneradmin\eSignerCKA\master.key - name: Build App - run: yarn build --publish never -w ${{ steps.config.outputs.build-args }} + run: yarn build --publish never ${{ steps.config.outputs.build-args }} -w ${{ inputs.targets }} env: VARIANT_PATH: variant.json # Only set for Nightly builds @@ -236,24 +260,36 @@ jobs: - name: Upload Artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: win-${{ inputs.arch }} + name: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }} path: | dist retention-days: 1 - - name: Assert all required files are present + - name: Assert executable is present run: | Test-Path './dist/win-*unpacked/Element*.exe' + + - name: Assert all Squirrel files are present + if: contains(inputs.targets, 'squirrel') + run: | Test-Path './dist/squirrel-windows*/Element Setup*.exe' Test-Path './dist/squirrel-windows*/element-desktop-*-full.nupkg' Test-Path './dist/squirrel-windows*/RELEASES' + + - name: Assert MSI is present + if: contains(inputs.targets, 'msi') + run: | Test-Path './dist/Element*.msi' test: + name: Test Windows ${{ inputs.arch }} needs: build + if: inputs.test uses: ./.github/workflows/build_test.yaml with: - artifact: win-${{ inputs.arch }} - runs-on: ${{ inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022' }} + project: win-${{ inputs.arch }} + artifact: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }} + runs-on: ${{ inputs.test-runs-on || (inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022') }} executable: ./dist/win*-unpacked/Element*.exe blob_report: ${{ inputs.blob_report }} + args: ${{ inputs.test-args }} diff --git a/electron-builder.ts b/electron-builder.ts index 147e811181..310875206b 100644 --- a/electron-builder.ts +++ b/electron-builder.ts @@ -151,6 +151,7 @@ const config: Omit, "electronFuses"> & { fpm: ["--deb-pre-depends", "libc6 (>= 2.31)"], }, mac: { + target: ["dmg", "zip"], category: "public.app-category.social-networking", darkModeSupport: true, hardenedRuntime: true, diff --git a/src/electron-main.ts b/src/electron-main.ts index 0f221dc2ef..c100722fee 100644 --- a/src/electron-main.ts +++ b/src/electron-main.ts @@ -169,59 +169,74 @@ function loadLocalConfigFile(): Json { } } +let loadConfigPromise: Promise | undefined; // Loads the config from asar, and applies a config.json from userData atop if one exists -// Writes config to `global.vectorConfig`. Does nothing if `global.vectorConfig` is already set. -async function loadConfig(): Promise { - if (global.vectorConfig) return; +// Writes config to `global.vectorConfig`. Idempotent, returns the same promise on subsequent calls. +function loadConfig(): Promise { + if (loadConfigPromise) return loadConfigPromise; - const asarPath = await getAsarPath(); + async function actuallyLoadConfig(): Promise { + const asarPath = await getAsarPath(); - try { - console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`); - global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename); - } catch { - // it would be nice to check the error code here and bail if the config - // is unparsable, but we get MODULE_NOT_FOUND in the case of a missing - // file or invalid json, so node is just very unhelpful. - // Continue with the defaults (ie. an empty config) - global.vectorConfig = {}; - } - - try { - // Load local config and use it to override values from the one baked with the build - const localConfig = loadLocalConfigFile(); - - // If the local config has a homeserver defined, don't use the homeserver from the build - // config. This is to avoid a problem where Riot thinks there are multiple homeservers - // defined, and panics as a result. - if (Object.keys(localConfig).find((k) => homeserverProps.includes(k))) { - // Rip out all the homeserver options from the vector config - global.vectorConfig = Object.keys(global.vectorConfig) - .filter((k) => !homeserverProps.includes(k)) - .reduce( - (obj, key) => { - obj[key] = global.vectorConfig[key]; - return obj; - }, - {} as Omit, keyof typeof homeserverProps>, - ); + try { + console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`); + global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename); + } catch { + // it would be nice to check the error code here and bail if the config + // is unparsable, but we get MODULE_NOT_FOUND in the case of a missing + // file or invalid json, so node is just very unhelpful. + // Continue with the defaults (ie. an empty config) + global.vectorConfig = {}; } - global.vectorConfig = Object.assign(global.vectorConfig, localConfig); - } catch (e) { - if (e instanceof SyntaxError) { - void dialog.showMessageBox({ - type: "error", - title: `Your ${global.vectorConfig.brand || "Element"} is misconfigured`, - message: - `Your custom ${global.vectorConfig.brand || "Element"} configuration contains invalid JSON. ` + - `Please correct the problem and reopen ${global.vectorConfig.brand || "Element"}.`, - detail: e.message || "", + try { + // Load local config and use it to override values from the one baked with the build + const localConfig = loadLocalConfigFile(); + + // If the local config has a homeserver defined, don't use the homeserver from the build + // config. This is to avoid a problem where Riot thinks there are multiple homeservers + // defined, and panics as a result. + if (Object.keys(localConfig).find((k) => homeserverProps.includes(k))) { + // Rip out all the homeserver options from the vector config + global.vectorConfig = Object.keys(global.vectorConfig) + .filter((k) => !homeserverProps.includes(k)) + .reduce( + (obj, key) => { + obj[key] = global.vectorConfig[key]; + return obj; + }, + {} as Omit, keyof typeof homeserverProps>, + ); + } + + global.vectorConfig = Object.assign(global.vectorConfig, localConfig); + } catch (e) { + if (e instanceof SyntaxError) { + void dialog.showMessageBox({ + type: "error", + title: `Your ${global.vectorConfig.brand || "Element"} is misconfigured`, + message: + `Your custom ${global.vectorConfig.brand || "Element"} configuration contains invalid JSON. ` + + `Please correct the problem and reopen ${global.vectorConfig.brand || "Element"}.`, + detail: e.message || "", + }); + } + + // Could not load local config, this is expected in most cases. + } + + // Tweak modules paths as they assume the root is at the same level as webapp, but for `vector://vector/webapp` it is not. + if (Array.isArray(global.vectorConfig.modules)) { + global.vectorConfig.modules = global.vectorConfig.modules.map((m) => { + if (m.startsWith("/")) { + return "/webapp" + m; + } + return m; }); } - - // Could not load local config, this is expected in most cases. } + loadConfigPromise = actuallyLoadConfig(); + return loadConfigPromise; } // Configure Electron Sentry and crashReporter using sentry.dsn in config.json if one is present.