name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
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 }}
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
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
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
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 -- bundle-macos
- name: Sign app bundle
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"
# 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
codesign --force --options runtime --timestamp \
--sign "$APPLE_SIGNING_IDENTITY" "$app"
codesign --verify --strict --verbose=2 "$app"
[ -d "$helper" ] && codesign --verify --strict --verbose=2 "$helper"
- name: Create and sign DMG
env:
APPLE_SIGNING_IDENTITY: ${{ steps.load_secrets.outputs.APPLE_SIGNING_IDENTITY }}
run: |
set -euo pipefail
cargo run -p xtask -- dmg-macos --sign-identity "$APPLE_SIGNING_IDENTITY"
- name: Notarize DMG
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
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
windows:
name: Windows EXE (${{ matrix.arch }})
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: release
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Load Azure signing config from 1Password
id: azure_config
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
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
run: cargo build --release -p openlogi-gui --target ${{ matrix.target }}
- name: Azure login (OIDC)
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: Sign openlogi-gui.exe with Artifact Signing
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
timestamp-rfc3161: http://timestamp.acs.microsoft.com
cache-dependencies: false
- name: Verify the binary is signed
shell: pwsh
run: |
$sig = Get-AuthenticodeSignature target\${{ matrix.target }}\release\openlogi-gui.exe
$sig | Format-List
# 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.
if ($null -eq $sig.SignerCertificate -or
"$($sig.Status)" -in 'NotSigned', 'HashMismatch') {
Write-Error "openlogi-gui.exe Authenticode signature is invalid: $($sig.Status)"
exit 1
}
- name: Collect signed exe artifact
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
# Renaming after signing is safe: Authenticode lives in the PE file.
$ref = $env:GITHUB_REF_NAME -replace '/', '-'
Copy-Item target\${{ matrix.target }}\release\openlogi-gui.exe "dist\OpenLogi-$ref-windows-${{ matrix.arch }}.exe"
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-windows-${{ matrix.arch }}
path: dist/*.exe
if-no-files-found: error
windows-msi:
name: Windows MSI (${{ matrix.arch }})
runs-on: windows-2025
needs: windows
if: ${{ !cancelled() }}
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: release
steps:
- uses: actions/checkout@v6
- uses: actions/download-artifact@v8
with:
name: OpenLogi-windows-${{ matrix.arch }}
path: signed-exe
- name: Load Azure signing config from 1Password
id: azure_config
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
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
- 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'
}
$exe = Get-ChildItem signed-exe\*.exe | Select-Object -First 1
# Upstream invariants (upload's if-no-files-found: error, the
# by-name download) should make this unreachable; the guard turns a
# confusing "$null.FullName -> empty -d ExeFile" wix error into a
# named cause if those invariants ever shift.
if ($null -eq $exe) {
Write-Error "No signed exe found in signed-exe\ — nothing to package"
exit 1
}
New-Item -ItemType Directory -Force -Path out | Out-Null
wix build packaging\windows\OpenLogi.wxs `
-arch ${{ matrix.msi_platform }} `
-d Version=$version `
-d ExeFile="$($exe.FullName)" `
-o out\OpenLogi.msi
- name: Azure login (OIDC)
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: Sign OpenLogi.msi with Artifact Signing
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
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
release-notes:
name: Generate release notes
if: ${{ github.ref_type == 'tag' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 10.11.0
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
cache-dependency-path: scripts/release-notes/pnpm-lock.yaml
- name: Load Codex release-notes credentials from 1Password
id: release_notes_config
continue-on-error: true
uses: 1password/load-secrets-action@v4
with:
export-env: false
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
CODEX_ACCESS_TOKEN: ${{ secrets.OP_CODEX_SECRET_ITEM }}/credential
CHATGPT_USERNAME: ${{ secrets.OP_CODEX_SECRET_ITEM }}/username
CODEX_ENDPOINT: ${{ secrets.OP_CODEX_SECRET_ITEM }}/ENDPOINT
- name: Mint release-notes GitHub App token
id: release_notes_app_token
continue-on-error: true
uses: ./.github/actions/github-app-token-from-1password
with:
op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
op-github-app-item: ${{ secrets.OP_GITHUB_APP_ITEM }}
- name: Install release-notes dependencies
run: pnpm --dir scripts/release-notes install --frozen-lockfile
- name: Generate GitHub Release notes
env:
CODEX_ACCESS_TOKEN: ${{ steps.release_notes_config.outputs.CODEX_ACCESS_TOKEN }}
CHATGPT_USERNAME: ${{ steps.release_notes_config.outputs.CHATGPT_USERNAME }}
CODEX_ENDPOINT: ${{ steps.release_notes_config.outputs.CODEX_ENDPOINT }}
GH_TOKEN: ${{ steps.release_notes_app_token.outputs.token }}
run: |
set -euo pipefail
node scripts/release-notes/generate.ts \
--tag "$GITHUB_REF_NAME" \
--output .release/RELEASE_NOTES.md
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-release-notes
path: .release/RELEASE_NOTES.md
linux-packages:
name: Linux packages (${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
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
- 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
# package-linux 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 -- package-linux
- 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
- uses: actions/upload-artifact@v7
with:
name: OpenLogi-linux-packages-${{ matrix.arch }}
path: dist/*
publish:
name: Publish release
runs-on: ubuntu-latest
needs: [macos, windows, windows-msi, linux-packages, release-notes]
if: ${{ github.ref_type == 'tag' && !cancelled() && needs.macos.result == 'success' && needs.release-notes.result == 'success' }}
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/download-artifact@v8
with:
pattern: OpenLogi-macos-dmg-*
path: dist
merge-multiple: true
- uses: actions/download-artifact@v8
with:
pattern: OpenLogi-windows-*
path: dist
merge-multiple: true
- uses: actions/download-artifact@v8
with:
name: OpenLogi-release-notes
path: .release
- uses: actions/download-artifact@v8
with:
pattern: OpenLogi-linux-packages-*
path: dist
merge-multiple: true
- name: Generate checksums
run: |
cd dist
# The DMGs are the release gate — fail loudly if none arrived (with
# nullglob alone, sha256sum with zero args would silently hash stdin).
compgen -G '*.dmg' > /dev/null
# nullglob: when the Windows job didn't ship an .exe, *.exe drops to
# nothing instead of erroring on a literal pattern, so the DMGs are
# hashed alone.
shopt -s nullglob
sha256sum *.dmg *.exe *.msi *.deb *.rpm > SHA256SUMS
- name: Detect best-effort artifacts
id: artifacts
run: |
for ext in exe msi deb rpm; do
if compgen -G "dist/*.$ext" > /dev/null; then
echo "$ext=true" >> "$GITHUB_OUTPUT"
else
echo "$ext=false" >> "$GITHUB_OUTPUT"
fi
done
- name: Load static updater publishing config from 1Password
id: r2_config
uses: 1password/load-secrets-action@v4
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
OPENLOGI_UPDATE_MINISIGN_SECRET_KEY: ${{ secrets.OP_R2_SECRET_ITEM }}/OPENLOGI_UPDATE_MINISIGN_SECRET_KEY
R2_ACCOUNT_ID: ${{ secrets.OP_R2_SECRET_ITEM }}/CLOUDFLARE_R2_ACCOUNT_ID
R2_BUCKET: ${{ secrets.OP_R2_SECRET_ITEM }}/CLOUDFLARE_R2_BUCKET
R2_ACCESS_KEY_ID: ${{ secrets.OP_R2_SECRET_ITEM }}/CLOUDFLARE_R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY: ${{ secrets.OP_R2_SECRET_ITEM }}/CLOUDFLARE_R2_SECRET_ACCESS_KEY
- name: Validate R2 publishing configuration
env:
OPENLOGI_UPDATE_BASE_URL: ${{ steps.r2_config.outputs.OPENLOGI_UPDATE_BASE_URL }}
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.r2_config.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }}
OPENLOGI_UPDATE_MINISIGN_SECRET_KEY: ${{ steps.r2_config.outputs.OPENLOGI_UPDATE_MINISIGN_SECRET_KEY }}
R2_ACCOUNT_ID: ${{ steps.r2_config.outputs.R2_ACCOUNT_ID }}
R2_BUCKET: ${{ steps.r2_config.outputs.R2_BUCKET }}
R2_ACCESS_KEY_ID: ${{ steps.r2_config.outputs.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ steps.r2_config.outputs.R2_SECRET_ACCESS_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}"
: "${OPENLOGI_UPDATE_MINISIGN_SECRET_KEY:?Configure OPENLOGI_UPDATE_MINISIGN_SECRET_KEY in the R2 1Password item}"
: "${R2_ACCOUNT_ID:?Configure CLOUDFLARE_R2_ACCOUNT_ID in the R2 1Password item}"
: "${R2_BUCKET:?Configure CLOUDFLARE_R2_BUCKET in the R2 1Password item}"
: "${R2_ACCESS_KEY_ID:?Configure CLOUDFLARE_R2_ACCESS_KEY_ID in the R2 1Password item}"
: "${R2_SECRET_ACCESS_KEY:?Configure CLOUDFLARE_R2_SECRET_ACCESS_KEY in the R2 1Password item}"
- name: Install minisign
run: |
sudo apt-get update
sudo apt-get install -y minisign
- name: Sign release artifacts with minisign
env:
OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY: ${{ steps.r2_config.outputs.OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY }}
OPENLOGI_UPDATE_MINISIGN_SECRET_KEY: ${{ steps.r2_config.outputs.OPENLOGI_UPDATE_MINISIGN_SECRET_KEY }}
run: |
set -euo pipefail
key_file="${RUNNER_TEMP}/openlogi-minisign.key"
umask 077
trap 'rm -f "$key_file"' EXIT
# The secret key file is stored base64-encoded in 1Password so its two
# lines survive paste/round-trip (a raw multi-line value can have its
# newline mangled to a space — see the GitHub App key note above).
# `tr -d` drops any wrapping whitespace before decoding.
printf '%s' "$OPENLOGI_UPDATE_MINISIGN_SECRET_KEY" | tr -d '[:space:]' | base64 -d > "$key_file"
# The Windows binaries and Linux packages get the same minisign
# treatment as the DMGs: manual verification today, and the future
# auto-updaters need the detached signatures to exist for every
# shipped version.
# nullglob: the exe/msi/deb/rpm sets are best-effort per arch leg, so
# the globs may match nothing; the DMGs are guaranteed by the publish
# gate.
shopt -s nullglob
for artifact in dist/*.dmg dist/*.exe dist/*.msi dist/*.deb dist/*.rpm; do
minisign -S -m "$artifact" -s "$key_file" -x "$artifact.minisig" -W
minisign -V -m "$artifact" -P "$OPENLOGI_UPDATE_MINISIGN_PUBLIC_KEY" -x "$artifact.minisig"
done
- name: Generate static updater manifest
env:
OPENLOGI_UPDATE_BASE_URL: ${{ steps.r2_config.outputs.OPENLOGI_UPDATE_BASE_URL }}
run: |
set -euo pipefail
cargo run -p xtask -- generate-updater-manifest \
--dist dist \
--tag "$GITHUB_REF_NAME" \
--base-url "$OPENLOGI_UPDATE_BASE_URL" \
--output dist/latest.json
- name: Upload static updater assets to R2
env:
AWS_ACCESS_KEY_ID: ${{ steps.r2_config.outputs.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ steps.r2_config.outputs.R2_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
AWS_REGION: auto
R2_ACCOUNT_ID: ${{ steps.r2_config.outputs.R2_ACCOUNT_ID }}
R2_BUCKET: ${{ steps.r2_config.outputs.R2_BUCKET }}
run: |
set -euo pipefail
endpoint="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
release_prefix="s3://${R2_BUCKET}/releases/${GITHUB_REF_NAME}"
channel_manifest="s3://${R2_BUCKET}/channels/stable/latest.json"
# The Windows binaries' distribution channel is the GitHub Release;
# latest.json is still dmg-only, so nothing in the updater bucket
# references the exe/msi or their minisigs — keep them all out of
# the immutable releases/ prefix until the Windows auto-updater
# lands. SHA256SUMS still lists them — it describes the GitHub
# Release's asset set.
aws s3 cp dist/ "$release_prefix/" \
--recursive \
--exclude latest.json \
--exclude "*.exe" \
--exclude "*.exe.minisig" \
--exclude "*.msi" \
--exclude "*.msi.minisig" \
--cache-control "public, max-age=31536000, immutable" \
--endpoint-url "$endpoint"
aws s3 cp dist/latest.json "$channel_manifest" \
--content-type "application/json" \
--cache-control "no-cache" \
--endpoint-url "$endpoint"
- name: Publish GitHub Release with assets
uses: softprops/action-gh-release@v3
with:
body_path: .release/RELEASE_NOTES.md
files: |
dist/*.dmg
dist/*.minisig
dist/SHA256SUMS
${{ steps.artifacts.outputs.exe == 'true' && 'dist/*.exe' || '' }}
${{ steps.artifacts.outputs.msi == 'true' && 'dist/*.msi' || '' }}
${{ steps.artifacts.outputs.deb == 'true' && 'dist/*.deb' || '' }}
${{ steps.artifacts.outputs.rpm == 'true' && 'dist/*.rpm' || '' }}
fail_on_unmatched_files: true
homebrew-tap:
name: Dispatch homebrew-tap update
runs-on: ubuntu-latest
needs: publish
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Mint homebrew-tap token (GitHub App)
id: tap_token
uses: ./.github/actions/github-app-token-from-1password
with:
op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
op-github-app-item: ${{ secrets.OP_GITHUB_APP_ITEM }}
owner: AprilNEA
repositories: homebrew-tap
- name: Dispatch homebrew-tap update
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ steps.tap_token.outputs.token }}
repository: AprilNEA/homebrew-tap
event-type: update-openlogi
client-payload: |
{"version": "${{ github.ref_name }}"}