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:
build:
name: Build installers
uses: ./.github/workflows/build.yml
secrets: inherit
permissions:
contents: read
id-token: write with:
sign: true
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
publish:
name: Publish release
runs-on: ubuntu-latest
needs: [build, release-notes]
if: ${{ github.ref_type == 'tag' && !cancelled() && needs.build.outputs.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 a .zip, *.zip drops to
# nothing instead of erroring on a literal pattern, so the DMGs are
# hashed alone. The Windows portable artifact is a zip of the GUI +
# agent exes (#347); no bare .exe ships anymore.
shopt -s nullglob
sha256sum -- *.dmg *.zip *.msi *.deb *.rpm *.pkg.tar.zst > SHA256SUMS
- name: Detect best-effort artifacts
id: artifacts
run: |
for ext in zip 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
if compgen -G "dist/*.pkg.tar.zst" > /dev/null; then
echo "pacman=true" >> "$GITHUB_OUTPUT"
else
echo "pacman=false" >> "$GITHUB_OUTPUT"
fi
- 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 zip/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/*.zip dist/*.msi dist/*.deb dist/*.rpm dist/*.pkg.tar.zst; 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 -- release latest-json \
--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 zip/msi or their minisigs — keep them all out of
# the immutable releases/ prefix until the Windows auto-updater
# lands (#347 PR 4). SHA256SUMS still lists them — it describes the
# GitHub Release's asset set. *.exe stays excluded defensively even
# though no bare exe ships anymore.
aws s3 cp dist/ "$release_prefix/" \
--recursive \
--exclude latest.json \
--exclude "*.exe" \
--exclude "*.exe.minisig" \
--exclude "*.zip" \
--exclude "*.zip.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.zip == 'true' && 'dist/*.zip' || '' }}
${{ steps.artifacts.outputs.msi == 'true' && 'dist/*.msi' || '' }}
${{ steps.artifacts.outputs.deb == 'true' && 'dist/*.deb' || '' }}
${{ steps.artifacts.outputs.rpm == 'true' && 'dist/*.rpm' || '' }}
${{ steps.artifacts.outputs.pacman == 'true' && 'dist/*.pkg.tar.zst' || '' }}
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 }}"}