Improve workflows for better reusability (#2608)

This commit is contained in:
Michael Telatynski
2025-10-08 11:45:46 +01:00
committed by GitHub
parent 5dd1984896
commit 1c55aa780f
6 changed files with 197 additions and 76 deletions
+48 -19
View File
@@ -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, 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. for example icons in the `build/` directory to override the app icons.
default: "webapp" 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: env:
SQLCIPHER_BUNDLED: ${{ inputs.sqlcipher == 'static' && '1' || '' }} SQLCIPHER_BUNDLED: ${{ inputs.sqlcipher == 'static' && '1' || '' }}
MAX_GLIBC: 2.31 # bullseye-era glibc, used by glibc-check.sh MAX_GLIBC: 2.31 # bullseye-era glibc, used by glibc-check.sh
permissions: {} # No permissions required permissions: {} # No permissions required
jobs: jobs:
build: build:
name: Build Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }}
# We build on native infrastructure as matrix-seshat fails to cross-compile properly # We build on native infrastructure as matrix-seshat fails to cross-compile properly
# https://github.com/matrix-org/seshat/issues/135 # 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: env:
HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-desktop-dockerbuild HAK_DOCKER_IMAGE: ghcr.io/element-hq/element-desktop-dockerbuild
steps: steps:
@@ -109,7 +133,7 @@ jobs:
- name: "Get modified files" - name: "Get modified files"
id: changed_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 uses: tj-actions/changed-files@823fcebdb31bb35fdf2229d9f769b400309430d0 # v46
with: with:
files: | files: |
@@ -159,13 +183,11 @@ jobs:
echo "USE_SYSTEM_FPM=true" >> $GITHUB_ENV echo "USE_SYSTEM_FPM=true" >> $GITHUB_ENV
- name: Build App - 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: env:
VARIANT_PATH: variant.json VARIANT_PATH: variant.json
# Only set for Nightly builds # Only set for Nightly builds
VERSION: ${{ inputs.version }} VERSION: ${{ inputs.version }}
FPM_DEBUG: true # TODO
DEBUG: electron-builder # TODO
# Workaround for https://github.com/electron-userland/electron-builder/issues/5721 # Workaround for https://github.com/electron-userland/electron-builder/issues/5721
USE_HARD_LINKS: false USE_HARD_LINKS: false
@@ -199,39 +221,46 @@ jobs:
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} name: ${{ inputs.artifact-prefix }}linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
path: | path: |
dist dist
!dist/*-unpacked/** !dist/*-unpacked/**
retention-days: 1 retention-days: 1
- name: Assert all required files are present - name: Assert deb is present and valid
if: contains(inputs.targets, 'deb')
run: | run: |
test -f ./dist/element-desktop*$ARCH.deb 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) DEB_LISTING=$(dpkg-deb --fsys-tarfile ./dist/element-desktop*.deb | tar -tv)
echo "deb listing: " echo "deb listing: "
echo "$DEB_LISTING" echo "$DEB_LISTING"
! echo "$DEB_LISTING" | grep '^h' ! 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: test:
name: Test Linux ${{ inputs.arch }} SQLCipher ${{ inputs.sqlcipher }}
needs: build needs: build
if: inputs.test && contains(inputs.targets, 'deb')
uses: ./.github/workflows/build_test.yaml uses: ./.github/workflows/build_test.yaml
with: with:
artifact: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }} project: linux-${{ inputs.arch }}-sqlcipher-${{ inputs.sqlcipher }}
runs-on: ${{ inputs.arch == 'arm64' && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }} 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* executable: /opt/Element*/element-desktop*
prepare_cmd: | prepare_cmd: |
sudo apt-get -qq update sudo apt-get -qq update
sudo apt install ./dist/*.deb sudo apt install ./dist/*.deb
blob_report: ${{ inputs.blob_report }} blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}
+35 -6
View File
@@ -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, 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. for example icons in the `build/` directory to override the app icons.
default: "webapp" 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 permissions: {} # No permissions required
jobs: jobs:
build: build:
name: Build macOS Universal
runs-on: macos-14 # M1 runs-on: macos-14 # M1
environment: ${{ inputs.sign && 'packages.element.io' || '' }} environment: ${{ inputs.sign && 'packages.element.io' || '' }}
steps: steps:
@@ -105,7 +125,7 @@ jobs:
- name: "[Signed] Build App" - name: "[Signed] Build App"
if: inputs.sign != '' if: inputs.sign != ''
run: | run: |
yarn build:universal --publish never yarn build:universal --publish never -m ${{ inputs.targets }}
env: env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
@@ -127,7 +147,7 @@ jobs:
- name: "[Unsigned] Build App" - name: "[Unsigned] Build App"
if: inputs.sign == '' if: inputs.sign == ''
run: | run: |
yarn build:universal --publish never yarn build:universal --publish never -m ${{ inputs.targets }}
env: env:
CSC_IDENTITY_AUTO_DISCOVERY: false CSC_IDENTITY_AUTO_DISCOVERY: false
VARIANT_PATH: variant.json VARIANT_PATH: variant.json
@@ -162,22 +182,30 @@ jobs:
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: macos name: ${{ inputs.artifact-prefix }}macos
path: | path: |
dist dist
!dist/mac-universal/** !dist/mac-universal/**
retention-days: 1 retention-days: 1
- name: Assert all required files are present - name: Assert zip is present
if: contains(inputs.targets, 'zip')
run: | run: |
test -f ./dist/Element*.dmg
test -f ./dist/Element*-mac.zip test -f ./dist/Element*-mac.zip
- name: Assert dmg is present
if: contains(inputs.targets, 'dmg')
run: |
test -f ./dist/Element*.dmg
test: test:
name: Test macOS Universal
needs: build needs: build
if: inputs.test && contains(inputs.targets, 'dmg')
uses: ./.github/workflows/build_test.yaml uses: ./.github/workflows/build_test.yaml
with: with:
artifact: macos project: macos
artifact: ${{ inputs.artifact-prefix }}macos
runs-on: macos-14 runs-on: macos-14
executable: /Users/runner/Applications/Element*.app/Contents/MacOS/Element* 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 # 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/ && rsync -a /Volumes/Element/Element*.app ~/Applications/ &&
hdiutil detach /Volumes/Element hdiutil detach /Volumes/Element
blob_report: ${{ inputs.blob_report }} blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}
+12 -1
View File
@@ -10,6 +10,10 @@ on:
type: string type: string
required: true required: true
description: "The name of the artifact to download" description: "The name of the artifact to download"
project:
type: string
required: true
description: "The Playwright project to use for testing"
executable: executable:
type: string type: string
required: true required: true
@@ -22,12 +26,19 @@ on:
type: boolean type: boolean
default: false default: false
description: "Whether to upload a blob report instead of the HTML report" 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: {} permissions: {}
jobs: jobs:
test: test:
name: Test ${{ inputs.project }}
runs-on: ${{ inputs.runs-on }} runs-on: ${{ inputs.runs-on }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - 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 - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with: with:
@@ -69,7 +80,7 @@ jobs:
uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a uses: coactions/setup-xvfb@6b00cf1889f4e1d5a48635647013c0508128ee1a
timeout-minutes: 20 timeout-minutes: 20
with: 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: env:
ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }} ELEMENT_DESKTOP_EXECUTABLE: ${{ steps.executable.outputs.path }}
+41 -5
View File
@@ -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, 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. for example icons in the `build/` directory to override the app icons.
default: "webapp" 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 permissions: {} # No permissions required
jobs: jobs:
build: build:
name: Build Windows ${{ inputs.arch }}
runs-on: windows-2025 runs-on: windows-2025
environment: ${{ inputs.sign && 'packages.element.io' || '' }} environment: ${{ inputs.sign && 'packages.element.io' || '' }}
env: env:
@@ -208,7 +232,7 @@ jobs:
MASTER_KEY_FILE: C:\Users\runneradmin\eSignerCKA\master.key MASTER_KEY_FILE: C:\Users\runneradmin\eSignerCKA\master.key
- name: Build App - 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: env:
VARIANT_PATH: variant.json VARIANT_PATH: variant.json
# Only set for Nightly builds # Only set for Nightly builds
@@ -236,24 +260,36 @@ jobs:
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with: with:
name: win-${{ inputs.arch }} name: ${{ inputs.artifact-prefix }}win-${{ inputs.arch }}
path: | path: |
dist dist
retention-days: 1 retention-days: 1
- name: Assert all required files are present - name: Assert executable is present
run: | run: |
Test-Path './dist/win-*unpacked/Element*.exe' 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 Setup*.exe'
Test-Path './dist/squirrel-windows*/element-desktop-*-full.nupkg' Test-Path './dist/squirrel-windows*/element-desktop-*-full.nupkg'
Test-Path './dist/squirrel-windows*/RELEASES' Test-Path './dist/squirrel-windows*/RELEASES'
- name: Assert MSI is present
if: contains(inputs.targets, 'msi')
run: |
Test-Path './dist/Element*.msi' Test-Path './dist/Element*.msi'
test: test:
name: Test Windows ${{ inputs.arch }}
needs: build needs: build
if: inputs.test
uses: ./.github/workflows/build_test.yaml uses: ./.github/workflows/build_test.yaml
with: with:
artifact: win-${{ inputs.arch }} project: win-${{ inputs.arch }}
runs-on: ${{ inputs.arch == 'arm64' && 'windows-11-arm' || 'windows-2022' }} 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 executable: ./dist/win*-unpacked/Element*.exe
blob_report: ${{ inputs.blob_report }} blob_report: ${{ inputs.blob_report }}
args: ${{ inputs.test-args }}
+1
View File
@@ -151,6 +151,7 @@ const config: Omit<Writable<Configuration>, "electronFuses"> & {
fpm: ["--deb-pre-depends", "libc6 (>= 2.31)"], fpm: ["--deb-pre-depends", "libc6 (>= 2.31)"],
}, },
mac: { mac: {
target: ["dmg", "zip"],
category: "public.app-category.social-networking", category: "public.app-category.social-networking",
darkModeSupport: true, darkModeSupport: true,
hardenedRuntime: true, hardenedRuntime: true,
+60 -45
View File
@@ -169,59 +169,74 @@ function loadLocalConfigFile(): Json {
} }
} }
let loadConfigPromise: Promise<void> | undefined;
// Loads the config from asar, and applies a config.json from userData atop if one exists // 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. // Writes config to `global.vectorConfig`. Idempotent, returns the same promise on subsequent calls.
async function loadConfig(): Promise<void> { function loadConfig(): Promise<void> {
if (global.vectorConfig) return; if (loadConfigPromise) return loadConfigPromise;
const asarPath = await getAsarPath(); async function actuallyLoadConfig(): Promise<void> {
const asarPath = await getAsarPath();
try { try {
console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`); console.log(`Loading app config: ${path.join(asarPath, LocalConfigFilename)}`);
global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename); global.vectorConfig = loadJsonFile(asarPath, LocalConfigFilename);
} catch { } catch {
// it would be nice to check the error code here and bail if the config // 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 // is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
// file or invalid json, so node is just very unhelpful. // file or invalid json, so node is just very unhelpful.
// Continue with the defaults (ie. an empty config) // Continue with the defaults (ie. an empty config)
global.vectorConfig = {}; 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(<any>k))) {
// Rip out all the homeserver options from the vector config
global.vectorConfig = Object.keys(global.vectorConfig)
.filter((k) => !homeserverProps.includes(<any>k))
.reduce(
(obj, key) => {
obj[key] = global.vectorConfig[key];
return obj;
},
{} as Omit<Partial<(typeof global)["vectorConfig"]>, keyof typeof homeserverProps>,
);
} }
global.vectorConfig = Object.assign(global.vectorConfig, localConfig); try {
} catch (e) { // Load local config and use it to override values from the one baked with the build
if (e instanceof SyntaxError) { const localConfig = loadLocalConfigFile();
void dialog.showMessageBox({
type: "error", // If the local config has a homeserver defined, don't use the homeserver from the build
title: `Your ${global.vectorConfig.brand || "Element"} is misconfigured`, // config. This is to avoid a problem where Riot thinks there are multiple homeservers
message: // defined, and panics as a result.
`Your custom ${global.vectorConfig.brand || "Element"} configuration contains invalid JSON. ` + if (Object.keys(localConfig).find((k) => homeserverProps.includes(<any>k))) {
`Please correct the problem and reopen ${global.vectorConfig.brand || "Element"}.`, // Rip out all the homeserver options from the vector config
detail: e.message || "", global.vectorConfig = Object.keys(global.vectorConfig)
.filter((k) => !homeserverProps.includes(<any>k))
.reduce(
(obj, key) => {
obj[key] = global.vectorConfig[key];
return obj;
},
{} as Omit<Partial<(typeof global)["vectorConfig"]>, 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. // Configure Electron Sentry and crashReporter using sentry.dsn in config.json if one is present.