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
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: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-latest
- arch: x86_64
runner: macos-15-intel
permissions:
contents: read
env:
OPENLOGI_BUNDLE_ASSETS: ${{ vars.OPENLOGI_BUNDLE_ASSETS }}
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v1-rust
shared-key: macos-stable-release
save-if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
- name: Verify runner architecture
run: test "$(uname -m)" = "${{ matrix.arch }}"
- name: Load release secrets from 1Password
id: load_secrets
if: ${{ inputs.sign }}
uses: 1password/load-secrets-action@v4
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@v4
with:
path: |
~/Library/Caches/Homebrew
~/Library/Caches/Homebrew/downloads
key: brew-macos-${{ runner.arch }}-librsvg-create-dmg-v1
restore-keys: |
brew-macos-${{ runner.arch }}-
- name: Install packaging tools
run: |
brew install librsvg create-dmg
- 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 OpenLogi.app with cargo-bundle
env:
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"
cargo run -p xtask -- macos bundle
- name: Sign app bundle
if: ${{ inputs.sign }}
env:
APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }}
run: |
set -euo pipefail
app="target/release/bundle/osx/OpenLogi.app"
helper="$app/Contents/Library/LoginItems/OpenLogiAgent.app"
cli="$app/Contents/MacOS/openlogi"
# Hardened-runtime camera exception for the GUI (preview) and CLI
# (snapshot). The agent helper never opens the camera.
ents="crates/openlogi-gui/bundle/OpenLogi.entitlements"
# Inside-out signing: seal the nested agent helper with its own
# signature first, then the outer app (which seals the signed helper).
# --deep is deprecated and can't give the helper an independent
# signature, which a stable per-helper TCC identity — so the agent's
# Accessibility grant survives app updates — requires.
if [ -d "$helper" ]; then
codesign --force --options runtime --timestamp \
--sign "$APPLE_SIGNING_IDENTITY" "$helper"
fi
# The embedded CLI is a second Mach-O under Contents/MacOS; sign it
# with the hardened runtime before the outer app or notarization
# rejects its as-built ad-hoc signature.
if [ -f "$cli" ]; then
codesign --force --options runtime --timestamp \
--entitlements "$ents" \
--sign "$APPLE_SIGNING_IDENTITY" "$cli"
fi
codesign --force --options runtime --timestamp \
--entitlements "$ents" \
--sign "$APPLE_SIGNING_IDENTITY" "$app"
codesign --verify --strict --verbose=2 "$app"
[ -d "$helper" ] && codesign --verify --strict --verbose=2 "$helper"
[ -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: Create and sign DMG
if: ${{ inputs.sign }}
env:
APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }}
run: |
set -euo pipefail
cargo run -p xtask -- macos dmg --sign-identity "$APPLE_SIGNING_IDENTITY"
- name: Create unsigned DMG
if: ${{ !inputs.sign }}
run: |
set -euo pipefail
cargo run -p xtask -- macos dmg
- 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@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v1-rust
shared-key: windows-stable-release-${{ matrix.arch }}
save-if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
- name: Load Azure signing config from 1Password
id: azure_config
if: ${{ inputs.sign }}
uses: 1password/load-secrets-action@v4
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: Build OpenLogi (GUI + agent)
run: cargo build --release -p openlogi-gui -p openlogi-agent --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-gui.exe and openlogi-agent.exe 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-gui.exe
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-agent.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-gui.exe', 'openlogi-agent.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 | Out-Null
Copy-Item target\${{ matrix.target }}\release\openlogi-gui.exe stage\OpenLogi.exe
Copy-Item target\${{ matrix.target }}\release\openlogi-agent.exe stage\openlogi-agent.exe
# 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@v6
- 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@v4
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.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 two
# exes, 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
$agent = Get-Item signed-exe\openlogi-agent.exe -ErrorAction SilentlyContinue
if ($null -eq $gui -or $null -eq $agent) {
Write-Error "signed-exe\ must contain OpenLogi.exe and openlogi-agent.exe — nothing to package"
exit 1
}
New-Item -ItemType Directory -Force -Path out | Out-Null
wix build packaging\windows\OpenLogi.wxs `
-ext WixToolset.Util.wixext `
-arch ${{ matrix.msi_platform }} `
-d Version=$version `
-d ExeFile="$($gui.FullName)" `
-d AgentExeFile="$($agent.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-latest
nfpm_sha256: "d6417f99d5fa32bba7a4e007084615d3897651498c2e443118c26b9ec3b698a8"
- arch: arm64
runner: ubuntu-24.04-arm
nfpm_sha256: "dc63aa7228ec70490bae67ad3146883055a055639dcf0dffc82fa965bac75a31"
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v1-rust
shared-key: linux-stable-release
save-if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
- 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-gui, 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: 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 }}"