name: Release
on:
push:
tags:
- "v*"
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
MACOSX_DEPLOYMENT_TARGET: "13.0"
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
jobs:
dmg:
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
release-notes:
name: Generate release notes
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
publish:
name: Publish release
runs-on: ubuntu-latest
needs: [dmg, release-notes]
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:
name: OpenLogi-release-notes
path: .release
- name: Generate checksums
run: |
cd dist
sha256sum *.dmg > SHA256SUMS
- 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 updater artifacts
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"
for artifact in dist/*.dmg; 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"
aws s3 cp dist/ "$release_prefix/" \
--recursive \
--exclude latest.json \
--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
fail_on_unmatched_files: true
homebrew-tap:
name: Dispatch homebrew-tap update
runs-on: ubuntu-latest
needs: publish
permissions:
contents: read
steps:
- 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 }}"}