name: Build
on:
workflow_call:
inputs:
sign:
description: Sign and notarize the installers (release only).
type: boolean
required: false
default: false
outputs:
macos_result:
description: Result of the macOS build job.
value: ${{ jobs.results.outputs.macos }}
windows_result:
description: Result of the Windows EXE build job.
value: ${{ jobs.results.outputs.windows }}
windows_msi_result:
description: Result of the Windows MSI build job.
value: ${{ jobs.results.outputs.windows_msi }}
linux_result:
description: Result of the Linux packages build job.
value: ${{ jobs.results.outputs.linux }}
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled, closed]
workflow_dispatch:
inputs:
sign:
description: Sign and notarize (needs the release-environment secrets).
type: boolean
default: false
concurrency:
group: build-${{ github.event.pull_request.number || format('{0}-{1}', github.ref, github.event_name) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' && (github.event.action == 'synchronize' || github.event.action == 'reopened' || github.event.action == 'closed' || (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) && github.event.label.name == format('needs{0} build', ':'))) }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
MACOSX_DEPLOYMENT_TARGET: "13.0"
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
RUSTC_WRAPPER: sccache
SCCACHE_GHA_ENABLED: "true"
SCCACHE_GHA_RW_MODE: ${{ (github.event_name != 'pull_request' && github.ref_type != 'tag') && 'READ_WRITE' || 'READ_ONLY' }}
jobs:
should-build:
name: Should build
runs-on: ubuntu-latest
outputs:
run: ${{ steps.gate.outputs.run }}
steps:
- id: gate
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
HAS_BUILD_LABEL: ${{ contains(github.event.pull_request.labels.*.name, format('needs{0} build', ':')) }}
EVENT_LABEL: ${{ github.event.label.name }}
BUILD_LABEL: ${{ format('needs{0} build', ':') }}
run: |
if [ "$EVENT_NAME" != "pull_request" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$EVENT_ACTION" = "closed" ] || [ "$EVENT_ACTION" = "unlabeled" ]; then
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$EVENT_ACTION" = "labeled" ] && [ "$EVENT_LABEL" != "$BUILD_LABEL" ]; then
echo "run=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$HAS_BUILD_LABEL" = "true" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
fi
macos:
name: macOS DMG (${{ matrix.arch }})
needs: should-build
if: ${{ needs.should-build.outputs.run == 'true' }}
runs-on: macos-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
target: aarch64-apple-darwin
- arch: x86_64
target: x86_64-apple-darwin
permissions:
contents: read
env:
OPENLOGI_BUNDLE_ASSETS: ${{ vars.OPENLOGI_BUNDLE_ASSETS }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba
with:
version: "v0.16.0"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v2-cargo
shared-key: macos-stable-release
cache-targets: false
save-if: ${{ github.event_name != 'pull_request' && github.ref_type != 'tag' }}
- name: Verify runner architecture
run: test "$(uname -m)" = arm64
- name: Select the Xcode both legs build with
run: |
# An unmatched glob stays literal, which `-d` then rejects — where
# `ls` would have failed the step under the shell's `-e -o pipefail`
# before this check could say what is actually wrong.
xcode=$(printf '%s\n' /Applications/Xcode_26*.app | sort -V | tail -1)
if [ ! -d "$xcode" ]; then
echo "No Xcode 26 on this runner; the app icon cannot be compiled." >&2
exit 1
fi
echo "OPENLOGI_DEVELOPER_DIR=$xcode/Contents/Developer" >> "$GITHUB_ENV"
echo "Building with $xcode"
- name: Load release secrets from 1Password
id: load_secrets
if: ${{ inputs.sign }}
uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
APPLE_SIGNING_IDENTITY: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_SIGNING_IDENTITY
APPLE_CERTIFICATE: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_CERTIFICATE
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_CERTIFICATE_PASSWORD
APPLE_ID: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_ID
APPLE_PASSWORD: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_PASSWORD
APPLE_TEAM_ID: ${{ secrets.OP_APPLE_SECRET_ITEM }}/APPLE_TEAM_ID
GITHUB_APP_ID: ${{ secrets.OP_GITHUB_APP_ITEM }}/GITHUB_APP_ID
GITHUB_APP_PRIVATE_KEY: ${{ secrets.OP_GITHUB_APP_ITEM }}/GITHUB_APP_PRIVATE_KEY
OPENLOGI_UPDATE_BASE_URL: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_BASE_URL
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY
- name: Cache Homebrew downloads
uses: actions/cache@v6
with:
path: |
~/Library/Caches/Homebrew
~/Library/Caches/Homebrew/downloads
key: brew-macos-${{ runner.arch }}-librsvg-create-dmg-cargo-bundle-v2
restore-keys: |
brew-macos-${{ runner.arch }}-
- name: Install packaging tools
run: |
# cargo-bundle has bottles for both hosted macOS architectures.
# Installing the bottle avoids compiling the tool from source in
# every release leg before OpenLogi itself can start building.
brew install librsvg create-dmg cargo-bundle
- name: Validate release secrets
if: ${{ inputs.sign }}
env:
APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }}
APPLE_CERTIFICATE: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ steps.load_secrets.outputs.APPLE_ID }}
APPLE_PASSWORD: ${{ steps.load_secrets.outputs.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ steps.load_secrets.outputs.APPLE_TEAM_ID }}
GITHUB_APP_ID: ${{ steps.load_secrets.outputs.GITHUB_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ steps.load_secrets.outputs.GITHUB_APP_PRIVATE_KEY }}
OPENLOGI_UPDATE_BASE_URL: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_BASE_URL }}
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }}
run: |
set -euo pipefail
: "${APPLE_SIGNING_IDENTITY:?Configure APPLE_SIGNING_IDENTITY in 1Password}"
: "${APPLE_CERTIFICATE:?Configure APPLE_CERTIFICATE in 1Password}"
: "${APPLE_CERTIFICATE_PASSWORD:?Configure APPLE_CERTIFICATE_PASSWORD in 1Password}"
: "${APPLE_ID:?Configure APPLE_ID in 1Password}"
: "${APPLE_PASSWORD:?Configure APPLE_PASSWORD in 1Password}"
: "${APPLE_TEAM_ID:?Configure APPLE_TEAM_ID in 1Password}"
: "${GITHUB_APP_ID:?Configure GITHUB_APP_ID in 1Password}"
: "${GITHUB_APP_PRIVATE_KEY:?Configure GITHUB_APP_PRIVATE_KEY in 1Password}"
: "${OPENLOGI_UPDATE_BASE_URL:?Configure OPENLOGI_UPDATE_BASE_URL in 1Password}"
: "${OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY:?Configure OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY in 1Password}"
- name: Import Apple signing certificate
if: ${{ inputs.sign }}
uses: apple-actions/import-codesign-certs@v7
with:
p12-file-base64: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE }}
p12-password: ${{ steps.load_secrets.outputs.APPLE_CERTIFICATE_PASSWORD }}
- name: Build and package OpenLogi
env:
APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }}
OPENLOGI_UPDATE_BASE_URL: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_BASE_URL }}
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.load_secrets.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }}
run: |
set -euo pipefail
export OPENLOGI_UPDATE_MANIFEST_URL="${OPENLOGI_UPDATE_BASE_URL%/}/channels/stable/latest.json"
if [ "${{ inputs.sign }}" = "true" ]; then
cargo run -p xtask -- macos package --target ${{ matrix.target }} --sign-identity "$APPLE_SIGNING_IDENTITY"
else
cargo run -p xtask -- macos package --target ${{ matrix.target }}
fi
- name: Verify bundled architectures
env:
EXPECTED_ARCH: ${{ matrix.arch }}
run: |
set -euo pipefail
app="target/release/bundle/osx/OpenLogi.app"
binaries=(
"$app/Contents/MacOS/openlogi-desktop"
"$app/Contents/MacOS/openlogi"
"$app/Contents/Library/LoginItems/OpenLogi Agent.app/Contents/MacOS/openlogi-agent"
"$app/Contents/Library/LoginItems/OpenLogi Overlay.app/Contents/MacOS/openlogi-overlay"
)
for binary in "${binaries[@]}"; do
archs=$(lipo -archs "$binary")
echo "$binary: $archs"
test "$archs" = "$EXPECTED_ARCH"
done
- name: Verify production bundle identities
run: |
set -euo pipefail
app="target/release/bundle/osx/OpenLogi.app"
plist() { /usr/libexec/PlistBuddy -c "Print :$2" "$1/Contents/Info.plist"; }
check() { # bundle expected-id expected-name
[ "$(plist "$1" CFBundleIdentifier)" = "$2" ]
[ "$(plist "$1" CFBundleName)" = "$3" ]
[ "$(plist "$1" CFBundleDisplayName)" = "$3" ]
[ -f "$1/Contents/Resources/AppIcon.icns" ]
}
check "$app" "org.openlogi.openlogi" "OpenLogi"
check "$app/Contents/Library/LoginItems/OpenLogi Agent.app" "org.openlogi.agent" "OpenLogi Agent"
check "$app/Contents/Library/LoginItems/OpenLogi Overlay.app" "org.openlogi.overlay" "OpenLogi Overlay"
- name: Verify signed app bundle
if: ${{ inputs.sign }}
run: |
set -euo pipefail
app="target/release/bundle/osx/OpenLogi.app"
helper="$app/Contents/Library/LoginItems/OpenLogi Agent.app"
overlay="$app/Contents/Library/LoginItems/OpenLogi Overlay.app"
cli="$app/Contents/MacOS/openlogi"
codesign --verify --strict --verbose=2 "$app"
[ -d "$helper" ] && codesign --verify --strict --verbose=2 "$helper"
[ -d "$overlay" ] && codesign --verify --strict --verbose=2 "$overlay"
[ -f "$cli" ] && codesign --verify --strict --verbose=2 "$cli"
# Fail the release if camera access would still be blocked at runtime.
codesign -d --entitlements - "$app" 2>/dev/null \
| grep -q 'com.apple.security.device.camera'
- name: Notarize DMG
if: ${{ inputs.sign }}
env:
APPLE_ID: ${{ steps.load_secrets.outputs.APPLE_ID }}
APPLE_PASSWORD: ${{ steps.load_secrets.outputs.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ steps.load_secrets.outputs.APPLE_TEAM_ID }}
run: |
set -euo pipefail
xcrun notarytool submit target/release/OpenLogi.dmg \
--apple-id "$APPLE_ID" \
--password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
- name: Staple DMG
if: ${{ inputs.sign }}
run: |
set -euo pipefail
xcrun stapler staple target/release/OpenLogi.dmg
xcrun stapler validate target/release/OpenLogi.dmg
- name: Collect DMG artifact
run: |
mkdir -p dist
ref_name="${GITHUB_REF_NAME:-dev}"
ref_name="${ref_name//\//-}"
cp target/release/OpenLogi.dmg "dist/OpenLogi-${ref_name}-macos-${{ matrix.arch }}.dmg"
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-macos-dmg-${{ matrix.arch }}
path: dist/*.dmg
if-no-files-found: error
windows:
name: Windows EXE (${{ matrix.arch }})
needs: should-build
if: ${{ needs.should-build.outputs.run == 'true' }}
runs-on: windows-2025
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
target: x86_64-pc-windows-msvc
- arch: arm64
target: aarch64-pc-windows-msvc
timeout-minutes: 60
env:
RUSTFLAGS: -C target-feature=+crt-static
permissions:
id-token: write
contents: read
environment: ${{ inputs.sign && 'release' || '' }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba
with:
version: "v0.16.0"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v2-cargo
shared-key: windows-stable-release
cache-targets: false
save-if: ${{ github.event_name != 'pull_request' && github.ref_type != 'tag' }}
- name: Load Azure signing config from 1Password
id: azure_config
if: ${{ inputs.sign }}
uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
AZURE_CLIENT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CLIENT_ID
AZURE_TENANT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_TENANT_ID
AZURE_SUBSCRIPTION_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SUBSCRIPTION_ID
AZURE_SIGNING_ENDPOINT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ENDPOINT
AZURE_SIGNING_ACCOUNT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ACCOUNT
AZURE_CERT_PROFILE: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CERT_PROFILE
- name: Validate Azure signing config
if: ${{ inputs.sign }}
shell: bash
env:
AZURE_CLIENT_ID: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }}
AZURE_SIGNING_ENDPOINT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }}
AZURE_CERT_PROFILE: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }}
run: |
set -euo pipefail
: "${AZURE_CLIENT_ID:?Configure AZURE_CLIENT_ID in the Azure 1Password item}"
: "${AZURE_TENANT_ID:?Configure AZURE_TENANT_ID in the Azure 1Password item}"
: "${AZURE_SUBSCRIPTION_ID:?Configure AZURE_SUBSCRIPTION_ID in the Azure 1Password item}"
: "${AZURE_SIGNING_ENDPOINT:?Configure AZURE_SIGNING_ENDPOINT in the Azure 1Password item}"
: "${AZURE_SIGNING_ACCOUNT:?Configure AZURE_SIGNING_ACCOUNT in the Azure 1Password item}"
: "${AZURE_CERT_PROFILE:?Configure AZURE_CERT_PROFILE in the Azure 1Password item}"
- name: Load static updater config from 1Password
id: update_config
if: ${{ inputs.sign }}
uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
OPENLOGI_UPDATE_BASE_URL: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_BASE_URL
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY
- name: Validate updater config
if: ${{ inputs.sign }}
shell: bash
env:
OPENLOGI_UPDATE_BASE_URL: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_BASE_URL }}
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }}
run: |
set -euo pipefail
: "${OPENLOGI_UPDATE_BASE_URL:?Configure OPENLOGI_UPDATE_BASE_URL in the R2 1Password item}"
: "${OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY:?Configure OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY in the R2 1Password item}"
- name: Build OpenLogi executables
shell: bash
env:
OPENLOGI_UPDATE_BASE_URL: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_BASE_URL }}
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.update_config.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }}
run: |
set -euo pipefail
if [ -n "$OPENLOGI_UPDATE_BASE_URL" ]; then
export OPENLOGI_UPDATE_MANIFEST_URL="${OPENLOGI_UPDATE_BASE_URL%/}/channels/stable/latest.json"
fi
cargo build --release -p openlogi-desktop -p openlogi-overlay -p openlogi-agent -p openlogi --target ${{ matrix.target }}
- name: Azure login (OIDC)
if: ${{ inputs.sign }}
uses: azure/login@v3
with:
client-id: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }}
tenant-id: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }}
subscription-id: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }}
- name: Ensure PSGallery is registered
if: ${{ inputs.sign }}
shell: pwsh
run: |
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
Register-PSRepository -Default
}
- name: Sign OpenLogi Windows executables with Artifact Signing
if: ${{ inputs.sign }}
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }}
signing-account-name: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }}
files: |
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-desktop.exe
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-overlay.exe
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-agent.exe
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi.exe
timestamp-rfc3161: http://timestamp.acs.microsoft.com
cache-dependencies: false
- name: Verify the binaries are signed
if: ${{ inputs.sign }}
shell: pwsh
run: |
# The signing step above already fails on error; this guards against it
# silently producing an unsigned or corrupted binary. NotSigned and
# HashMismatch (bytes no longer match the signature) are trust-store
# independent and always ship-blockers; NotTrusted/UnknownError depend
# on the runner's trust store, so they stay log-only.
foreach ($exe in 'openlogi-desktop.exe', 'openlogi-overlay.exe', 'openlogi-agent.exe', 'openlogi.exe') {
$sig = Get-AuthenticodeSignature "target\${{ matrix.target }}\release\$exe"
$sig | Format-List
if ($null -eq $sig.SignerCertificate -or
"$($sig.Status)" -in 'NotSigned', 'HashMismatch') {
Write-Error "$exe Authenticode signature is invalid: $($sig.Status)"
exit 1
}
}
- name: Collect signed portable zip artifact
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist, stage, stage\bin | Out-Null
Copy-Item target\${{ matrix.target }}\release\openlogi-desktop.exe stage\OpenLogi.exe
Copy-Item target\${{ matrix.target }}\release\openlogi-overlay.exe stage\openlogi-overlay.exe
Copy-Item target\${{ matrix.target }}\release\openlogi-agent.exe stage\openlogi-agent.exe
Copy-Item target\${{ matrix.target }}\release\openlogi.exe stage\bin\openlogi.exe
# Guard the collision above: four inputs must produce four staged
# files. A case-insensitive overwrite would silently leave three.
$staged = (Get-ChildItem stage -Recurse -File).Count
if ($staged -ne 4) {
Write-Error "expected 4 staged files, found $staged — a name collision dropped one"
exit 1
}
# The MSI extracts this ZIP but only installs its explicitly listed
# executables, so the portable marker never enters an MSI install.
Copy-Item packaging\windows\openlogi-installation.json stage\openlogi-installation.json
# Zipping after signing is safe: Authenticode lives in the PE files.
$ref = $env:GITHUB_REF_NAME -replace '/', '-'
Compress-Archive -Path stage\* -DestinationPath "dist\OpenLogi-$ref-windows-${{ matrix.arch }}.zip"
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-windows-${{ matrix.arch }}
path: dist/*.zip
if-no-files-found: error
windows-msi:
name: Windows MSI (${{ matrix.arch }})
runs-on: windows-2025
needs: [should-build, windows]
if: ${{ !cancelled() && needs.should-build.outputs.run == 'true' }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
msi_platform: x64
- arch: arm64
msi_platform: arm64
permissions:
id-token: write
contents: read
environment: ${{ inputs.sign && 'release' || '' }}
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: OpenLogi-windows-${{ matrix.arch }}
path: signed-zip
- name: Extract the signed exes
shell: pwsh
run: |
$zip = Get-ChildItem signed-zip\*.zip | Select-Object -First 1
if ($null -eq $zip) {
Write-Error "No portable zip found in signed-zip\ — nothing to package"
exit 1
}
Expand-Archive -Path $zip.FullName -DestinationPath signed-exe
- name: Load Azure signing config from 1Password
id: azure_config
if: ${{ inputs.sign }}
uses: 1password/load-secrets-action@70062d7a876d3eb6334754fa26efd2fbd90c32f2
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
AZURE_CLIENT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CLIENT_ID
AZURE_TENANT_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_TENANT_ID
AZURE_SUBSCRIPTION_ID: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SUBSCRIPTION_ID
AZURE_SIGNING_ENDPOINT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ENDPOINT
AZURE_SIGNING_ACCOUNT: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_SIGNING_ACCOUNT
AZURE_CERT_PROFILE: ${{ secrets.OP_AZURE_SECRET_ITEM }}/AZURE_CERT_PROFILE
- name: Validate Azure signing config
if: ${{ inputs.sign }}
shell: bash
env:
AZURE_CLIENT_ID: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }}
AZURE_SIGNING_ENDPOINT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }}
AZURE_CERT_PROFILE: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }}
run: |
set -euo pipefail
: "${AZURE_CLIENT_ID:?Configure AZURE_CLIENT_ID in the Azure 1Password item}"
: "${AZURE_TENANT_ID:?Configure AZURE_TENANT_ID in the Azure 1Password item}"
: "${AZURE_SUBSCRIPTION_ID:?Configure AZURE_SUBSCRIPTION_ID in the Azure 1Password item}"
: "${AZURE_SIGNING_ENDPOINT:?Configure AZURE_SIGNING_ENDPOINT in the Azure 1Password item}"
: "${AZURE_SIGNING_ACCOUNT:?Configure AZURE_SIGNING_ACCOUNT in the Azure 1Password item}"
: "${AZURE_CERT_PROFILE:?Configure AZURE_CERT_PROFILE in the Azure 1Password item}"
- name: Install the WiX toolset
run: |
dotnet tool install --global wix --version 6.0.2
wix extension add --global WixToolset.UI.wixext/6.0.2
wix extension add --global WixToolset.Util.wixext/6.0.2
- name: Build the MSI
shell: pwsh
run: |
# ProductVersion must be a numeric x.y.z; non-tag dispatches build a
# throwaway 0.0.0 package.
if ($env:GITHUB_REF_NAME -match '^v(\d+\.\d+\.\d+)$') {
$version = $Matches[1]
} else {
$version = '0.0.0'
}
# Selected by exact name, never by glob order: the zip carries multiple
# executables, and Get-ChildItem's alphabetical sort would put the
# agent first. Upstream invariants (upload's if-no-files-found: error, the
# by-name download, the extract step's guard) should make the misses
# unreachable; the guards turn a confusing "$null.FullName -> empty
# -d" wix error into a named cause if those invariants ever shift.
$gui = Get-Item signed-exe\OpenLogi.exe -ErrorAction SilentlyContinue
$overlay = Get-Item signed-exe\openlogi-overlay.exe -ErrorAction SilentlyContinue
$agent = Get-Item signed-exe\openlogi-agent.exe -ErrorAction SilentlyContinue
$cli = Get-Item signed-exe\bin\openlogi.exe -ErrorAction SilentlyContinue
if ($null -eq $gui -or $null -eq $overlay -or $null -eq $agent -or $null -eq $cli) {
Write-Error "signed-exe\ must contain OpenLogi.exe, openlogi-overlay.exe, openlogi-agent.exe, and bin\openlogi.exe — nothing to package"
exit 1
}
New-Item -ItemType Directory -Force -Path out | Out-Null
wix build packaging\windows\OpenLogi.wxs `
-ext WixToolset.UI.wixext `
-ext WixToolset.Util.wixext `
-arch ${{ matrix.msi_platform }} `
-d Version=$version `
-d ExeFile="$($gui.FullName)" `
-d AgentExeFile="$($agent.FullName)" `
-d OverlayExeFile="$($overlay.FullName)" `
-d CliExeFile="$($cli.FullName)" `
-o out\OpenLogi.msi
- name: Azure login (OIDC)
if: ${{ inputs.sign }}
uses: azure/login@v3
with:
client-id: ${{ steps.azure_config.outputs.AZURE_CLIENT_ID }}
tenant-id: ${{ steps.azure_config.outputs.AZURE_TENANT_ID }}
subscription-id: ${{ steps.azure_config.outputs.AZURE_SUBSCRIPTION_ID }}
- name: Ensure PSGallery is registered
if: ${{ inputs.sign }}
shell: pwsh
run: |
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
Register-PSRepository -Default
}
- name: Sign OpenLogi.msi with Artifact Signing
if: ${{ inputs.sign }}
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ steps.azure_config.outputs.AZURE_SIGNING_ENDPOINT }}
signing-account-name: ${{ steps.azure_config.outputs.AZURE_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ steps.azure_config.outputs.AZURE_CERT_PROFILE }}
files: ${{ github.workspace }}\out\OpenLogi.msi
timestamp-rfc3161: http://timestamp.acs.microsoft.com
cache-dependencies: false
- name: Verify the MSI is signed
if: ${{ inputs.sign }}
shell: pwsh
run: |
$sig = Get-AuthenticodeSignature out\OpenLogi.msi
$sig | Format-List
# Same gate as the exe: NotSigned/HashMismatch are trust-store
# independent ship-blockers; NotTrusted/UnknownError stay log-only.
if ($null -eq $sig.SignerCertificate -or
"$($sig.Status)" -in 'NotSigned', 'HashMismatch') {
Write-Error "OpenLogi.msi Authenticode signature is invalid: $($sig.Status)"
exit 1
}
- name: Collect signed MSI artifact
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
# Renaming after signing is safe: the signature lives in the MSI.
$ref = $env:GITHUB_REF_NAME -replace '/', '-'
Copy-Item out\OpenLogi.msi "dist\OpenLogi-$ref-windows-${{ matrix.arch }}.msi"
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-windows-msi-${{ matrix.arch }}
path: dist/*.msi
if-no-files-found: error
linux-packages:
name: Linux packages (${{ matrix.arch }})
needs: should-build
if: ${{ needs.should-build.outputs.run == 'true' }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-22.04
nfpm_sha256: "d6417f99d5fa32bba7a4e007084615d3897651498c2e443118c26b9ec3b698a8"
- arch: arm64
runner: ubuntu-22.04-arm
nfpm_sha256: "dc63aa7228ec70490bae67ad3146883055a055639dcf0dffc82fa965bac75a31"
permissions:
contents: read
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba
with:
version: "v0.16.0"
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v2-cargo
shared-key: linux-jammy-stable-release
cache-targets: false
save-if: ${{ github.event_name != 'pull_request' && github.ref_type != 'tag' }}
- name: Verify runner architecture
env:
EXPECTED_ARCH: ${{ matrix.arch }}
run: |
case "$(uname -m)" in
x86_64) got=amd64 ;;
aarch64) got=arm64 ;;
*) echo "unexpected machine $(uname -m)" >&2; exit 1 ;;
esac
test "$got" = "$EXPECTED_ARCH"
- name: Install system dependencies and nfpm
env:
NFPM_VERSION: "2.46.3"
NFPM_ARCH: ${{ matrix.arch }}
NFPM_SHA256: ${{ matrix.nfpm_sha256 }}
run: |
sudo apt-get update
# The Linux packaging task builds openlogi-desktop, which links GPUI's full
# wayland / x11 stack — keep this in sync with ci.yml's Linux deps
# (the proven set). The previous short list was missing
# libxkbcommon-x11-dev / libwayland-dev / libx11-xcb-dev, so every
# tagged Linux build failed to link (`-lxkbcommon-x11`) and took the
# whole GitHub Release down with it.
sudo apt-get install -y \
libudev-dev \
gcc g++ clang libfontconfig-dev libwayland-dev \
libxkbcommon-x11-dev libx11-xcb-dev \
libssl-dev libzstd-dev pkg-config
curl -fsSLo /tmp/nfpm.deb \
"https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${NFPM_ARCH}.deb"
echo "${NFPM_SHA256} /tmp/nfpm.deb" | sha256sum -c
sudo dpkg -i /tmp/nfpm.deb
- name: Build and package
run: cargo run -p xtask -- linux package
- name: Verify GLIBC baseline
env:
MAX_GLIBC_VERSION: "2.35"
run: |
for binary in openlogi openlogi-desktop openlogi-overlay openlogi-agent; do
required=$(
readelf --version-info "target/release/$binary" \
| sed -n 's/.*Name: GLIBC_\([0-9][0-9.]*\).*/\1/p' \
| sort -Vu \
| tail -n 1
)
if [ -z "$required" ]; then
echo "could not determine the GLIBC requirement for $binary" >&2
exit 1
fi
echo "$binary requires GLIBC $required"
newest=$(printf '%s\n%s\n' "$MAX_GLIBC_VERSION" "$required" | sort -V | tail -n 1)
if [ "$newest" != "$MAX_GLIBC_VERSION" ]; then
echo "$binary exceeds the GLIBC $MAX_GLIBC_VERSION release baseline" >&2
exit 1
fi
done
- name: Collect packages
env:
PKG_ARCH: ${{ matrix.arch }}
run: |
mkdir -p dist
ref_name="${GITHUB_REF_NAME:-dev}"
ref_name="${ref_name//\//-}"
for pkg in target/release/*.deb target/release/*.rpm; do
[ -f "$pkg" ] || continue
ext="${pkg##*.}"
cp "$pkg" "dist/openlogi-${ref_name}-linux-${PKG_ARCH}.${ext}"
done
# .pkg.tar.zst has a compound extension; handle it separately.
for pkg in target/release/*.pkg.tar.zst; do
[ -f "$pkg" ] || continue
cp "$pkg" "dist/openlogi-${ref_name}-linux-${PKG_ARCH}.pkg.tar.zst"
done
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-linux-packages-${{ matrix.arch }}
path: dist/*
results:
name: Build results
if: ${{ always() }}
needs: [should-build, macos, windows, windows-msi, linux-packages]
runs-on: ubuntu-latest
outputs:
macos: ${{ needs.macos.result }}
windows: ${{ needs.windows.result }}
windows_msi: ${{ needs['windows-msi'].result }}
linux: ${{ needs['linux-packages'].result }}
steps:
- name: Summarize
run: |
echo "macos=${{ needs.macos.result }}"
echo "windows=${{ needs.windows.result }}"
echo "windows-msi=${{ needs['windows-msi'].result }}"
echo "linux-packages=${{ needs['linux-packages'].result }}"