Skip to content

chore: release v0.6.22 #34

chore: release v0.6.22

chore: release v0.6.22 #34

Workflow file for this run

name: Build
# Reusable build workflow. `release.yml` calls it with `sign: true` to produce
# the signed/notarized installers it publishes; PRs and manual dispatches build
# the SAME installers UNSIGNED and upload them as run artifacts for testing.
# Every signing step and the protected `release` environment are gated on
# `inputs.sign`, so an unsigned run needs no secrets at all — fork PRs (which
# get none) build fine.
on:
workflow_call:
inputs:
sign:
description: Sign and notarize the installers (release only).
type: boolean
required: false
default: false
outputs:
# The caller's publish gate ships the macOS DMG even when a Windows/Linux
# leg failed, so it needs each leg's result — a single reusable-workflow
# job result would be `failure` if any platform failed, collapsing that
# graceful degradation. Sourced from the always()-running `results` job.
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 }}
# Every PR builds unsigned installers for testing (all four platforms).
pull_request:
workflow_dispatch:
inputs:
sign:
description: Sign and notarize (needs the release-environment secrets).
type: boolean
default: false
concurrency:
group: build-${{ github.ref }}-${{ github.event_name }}
# A superseding PR push cancels the old build; never cancel a signed run.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
MACOSX_DEPLOYMENT_TARGET: "13.0"
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
jobs:
macos:
name: macOS DMG (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
# Bound a stuck brew/linker/notarize hang so it can't hold a PR build slot
# for the 6-hour runner default (the signed path also notarizes).
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
- 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: Install packaging tools
run: |
brew install librsvg
brew install 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"
# 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 \
--sign "$APPLE_SIGNING_IDENTITY" "$cli"
fi
codesign --force --options runtime --timestamp \
--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"
- 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"
# Unsigned PR/dispatch build: `--sign-identity` is optional, so the DMG is
# produced without it (Gatekeeper will warn on open — fine for testing).
- 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
# 'warn' (the default) would let an empty-dist job stay green while
# `results.macos` reports success and the publish gate ships nothing —
# same reasoning as the windows zip upload.
if-no-files-found: error
windows:
name: Windows EXE (${{ matrix.arch }})
# Both arches build on the x86_64 runner — arm64 as an MSVC cross-compile.
# Native Arm runners are not an option: artifact-signing-action refuses to
# run on them, while the signing itself is arch-agnostic over the PE file.
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
# A hung signing/login call must become a normal failure (which publish
# tolerates), not a 6-hour stall: publish waits for this job to reach a
# terminal state before its own gate is even evaluated. The GUI release
# build (thin LTO, codegen-units=1, the whole GPUI stack) dominates the
# runtime on the 4-core runner, hence a looser budget than the ~8-minute
# CLI-only runs needed.
timeout-minutes: 60
env:
# Static CRT: a bare exe must not depend on the VC++ Redistributable —
# the v0.6.6 exe died on clean machines with "VCRUNTIME140.dll was not
# found". The explicit --target below keeps the flag off host build
# scripts, which RUSTFLAGS would otherwise also reach.
RUSTFLAGS: -C target-feature=+crt-static
permissions:
# Mint an OIDC token so azure/login can exchange it for an Azure access token
# via a federated credential — no client secret is stored in GitHub.
id-token: write
contents: read
# The federated credential's subject is bound to this GitHub environment, so the
# OIDC `sub` claim stays stable (repo:AprilNEA/OpenLogi:environment:release)
# instead of changing with every tag ref. Only signed (release) runs enter it;
# an unsigned run needs no secrets, so it takes no environment (fork PRs, which
# can't access a protected environment at all, then build fine).
environment: ${{ inputs.sign && 'release' || '' }}
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
# No rust-cache here: this workflow runs only on tag refs, whose saved
# caches no future tag can restore (and no default-branch job produces a
# matching key), so the save was pure overhead churning the cache quota.
# Azure signing config lives in 1Password, matching the macOS job's secret flow.
# load-secrets-action is a node action, so it runs on Windows runners too. The
# client/tenant/subscription IDs are not secrets under OIDC, but are centralised
# here so all release config has a single source of truth.
- 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
# load-secrets-action silently skips values that aren't op:// references
# (e.g. when OP_AZURE_SECRET_ITEM is unset) and emits empty outputs for
# empty fields — without this gate the job only fails minutes later, in
# azure/login, with an error that never names the real cause.
- 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}"
# No separate CLI artifact will ever ship on Windows (cargo install
# covers that audience), so job and artifact names carry no GUI
# qualifier. The GUI is a thin IPC client; the headless agent (ported in
# #167) owns the device I/O, input hook, and IPC server, and the GUI
# spawns a sibling `openlogi-agent.exe` when the pipe is absent — so the
# agent MUST ship next to the GUI or the app connects to nothing (#347).
- name: Build OpenLogi (GUI + agent)
run: cargo build --release -p openlogi-gui -p openlogi-agent --target ${{ matrix.target }}
# Federated (passwordless) login. Requires an app registration whose federated
# credential subject is `repo:AprilNEA/OpenLogi:environment:release`, granted the
# "Artifact Signing Certificate Profile Signer" role on the signing account.
- 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 }}
# artifact-signing-action installs its ArtifactSigning module from PSGallery,
# which is intermittently missing on hosted windows runners (the action then
# dies with "Unable to find repository 'PSGallery'" — actions/runner-images#13758).
# That flake silently dropped the arm64 exe + msi from v0.6.14. Re-register the
# default PSGallery so a mis-provisioned runner can't lose an artifact again.
- name: Ensure PSGallery is registered
if: ${{ inputs.sign }}
shell: pwsh
run: |
if (-not (Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue)) {
Register-PSRepository -Default
}
# Authenticode-signs via Artifact Signing (ex-Trusted Signing). The action only
# runs on Windows runners. azure/login leaves an az CLI session that the action's
# default AzureCliCredential picks up, so no azure-* secret inputs are needed.
- 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` is a newline-separated list; one call signs both PE files.
files: |
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-gui.exe
${{ github.workspace }}\target\${{ matrix.target }}\release\openlogi-agent.exe
# Timestamping is mandatory: without it the signature expires after
# 3 days. The URL must stay http — SignTool rejects an https /tr up
# front ("SignTool Error: Invalid Timestamp URL", v0.6.5) before
# even contacting Azure. The RFC3161 response is itself signed, so
# http transport does not weaken it. The file/timestamp digests stay
# on the action's SHA256 defaults.
timestamp-rfc3161: http://timestamp.acs.microsoft.com
# Tag-only workflow: the action's four dependency caches save to
# per-tag refs no future run can restore, so skip them.
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
}
}
# The portable artifact is a zip of both exes: a bare exe has nowhere to
# put the agent, and GUI-only can never work (#347). Inside the zip the
# GUI carries the product name (`OpenLogi.exe`, matching the MSI layout)
# and the agent keeps the exact name the GUI's sibling lookup expects.
- 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
# 'warn' (the default) would let a no-zip job stay green, and publish
# would then die at the by-name download instead of shipping DMG-only.
if-no-files-found: error
windows-msi:
name: Windows MSI (${{ matrix.arch }})
runs-on: windows-2025
needs: windows
# `needs` only orders the jobs; this gate runs the MSI legs even when the
# windows matrix AGGREGATE failed. The by-name artifact download below
# then fails exactly the leg whose exe never shipped, and the other arch
# keeps going.
if: ${{ !cancelled() }}
# No Rust build here — dotnet tool install + wix build + signing.
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
msi_platform: x64
- arch: arm64
msi_platform: arm64
permissions:
# Same OIDC setup as the windows job — the MSI is signed with the same
# Artifact Signing profile as the exe it wraps.
id-token: write
contents: read
environment: ${{ inputs.sign && 'release' || '' }}
steps:
# Needed for packaging/windows/OpenLogi.wxs.
- uses: actions/checkout@v6
# The portable zip from this arch's windows leg, containing the
# already-signed OpenLogi.exe and openlogi-agent.exe. The MSI packages
# exactly the bytes the portable artifact ships.
- 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
# Pinned exactly: a WiX point release can silently change the produced
# MSI (validation, cab layout), and nothing in CI installs the MSI, so
# drift would only surface on end-user machines. Bump deliberately.
# The Util extension (util:CloseApplication in OpenLogi.wxs) is pinned
# to the same version — WiX extensions release in lockstep with wix.
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 }}
# Same PSGallery flake guard as the exe job — see that step's comment.
- 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
# http on purpose — see the exe signing step.
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 }})
runs-on: ${{ matrix.runner }}
# Bound a hang so it can't hold a PR build slot for the 6-hour runner
# default; a cargo build + nfpm package is well under this.
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
# The nfpm `arch` and the runner's native arch must agree — the packages
# are built natively, not cross-compiled, so a mismatched runner would
# silently ship a wrongly-labelled binary.
- 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/*
# Single owner of the GitHub Release. softprops/action-gh-release@v3 creates the
# release as a draft, uploads every asset to that (mutable) draft, then publishes
# it as its final step — so the release only becomes immutable once all DMGs and
# checksums are attached. The static updater assets are pushed to R2 *before* that
# softprops call, so the GitHub Release is the last thing published.
# Surfaces each platform's result as a workflow output so the caller's publish
# gate can ship a partial release (e.g. the macOS DMG even when a Windows leg
# failed). The reusable workflow's own job result would be `failure` if any leg
# failed, collapsing that graceful degradation. `always()` so the outputs are
# set regardless of which legs failed.
results:
name: Build results
if: ${{ always() }}
needs: [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 }}"