Skip to content

Prepare release

Prepare release #86

Workflow file for this run

name: "Release"
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Release tag to build. Defaults to package.json version."
required: false
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release-metadata:
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' }}
outputs:
tag_name: ${{ steps.release_meta.outputs.tag_name }}
release_name: ${{ steps.release_meta.outputs.release_name }}
is_prerelease: ${{ steps.release_meta.outputs.is_prerelease }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Generate release metadata
id: release_meta
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_TAG: ${{ github.event.inputs.tag || '' }}
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
if [[ -n "$INPUT_TAG" ]]; then
tag="$INPUT_TAG"
else
version="$(bun -e 'console.log(JSON.parse(await Bun.file("package.json").text()).version)')"
tag="v${version}"
fi
else
tag="${GITHUB_REF_NAME}"
fi
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid release tag: $tag"
exit 1
fi
echo "tag_name=$tag" >> "${GITHUB_OUTPUT}"
bun scripts/release/generate-notes.mjs \
--tag "$tag" \
--github-output "${GITHUB_OUTPUT}"
bun scripts/release/generate-notes.mjs \
--tag "$tag" \
> release-body.md
- name: Upload release metadata
uses: actions/upload-artifact@v4
with:
name: release-metadata
path: release-body.md
if-no-files-found: error
build-tauri:
needs: release-metadata
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- platform: "macos-latest"
artifact: "macos-aarch64"
os: "macos"
args: "--target aarch64-apple-darwin"
cargo_features: ""
target: "aarch64-apple-darwin"
bundle_path: "target/aarch64-apple-darwin/release/bundle"
timeout_minutes: 60
rustflags: ""
- platform: "macos-latest"
artifact: "macos-x86_64"
os: "macos"
args: "--target x86_64-apple-darwin"
cargo_features: ""
target: "x86_64-apple-darwin"
bundle_path: "target/x86_64-apple-darwin/release/bundle"
timeout_minutes: 60
rustflags: ""
- platform: "ubuntu-22.04"
artifact: "linux-x86_64"
os: "linux"
args: ""
cargo_features: "--no-default-features --features linux"
target: ""
bundle_path: "target/release/bundle"
timeout_minutes: 120
rustflags: ""
- platform: "ubuntu-22.04-arm"
artifact: "linux-aarch64"
os: "linux"
args: ""
cargo_features: "--no-default-features --features linux"
target: ""
bundle_path: "target/release/bundle"
timeout_minutes: 120
rustflags: ""
- platform: "windows-latest"
artifact: "windows-x86_64"
os: "windows"
args: "--bundles nsis"
cargo_features: ""
target: ""
bundle_path: "target/release/bundle"
timeout_minutes: 90
rustflags: ""
- platform: "windows-latest"
artifact: "windows-aarch64"
os: "windows"
args: "--target aarch64-pc-windows-msvc --bundles nsis"
cargo_features: ""
target: "aarch64-pc-windows-msvc"
bundle_path: "target/aarch64-pc-windows-msvc/release/bundle"
timeout_minutes: 90
rustflags: ""
runs-on: ${{ matrix.platform }}
timeout-minutes: ${{ matrix.timeout_minutes }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Free disk space (Linux only)
if: matrix.os == 'linux'
run: |
echo "Disk usage before cleanup:"
df -h
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL || true
sudo docker system prune -af || true
sudo apt-get clean
echo "Disk usage after cleanup:"
df -h
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Ensure Rust target is installed
if: matrix.target != ''
run: rustup target add ${{ matrix.target }}
- name: Setup Zig
uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: Cache CEF binaries (Linux only)
if: matrix.os == 'linux'
uses: actions/cache@v4
with:
path: .cache/tauri-cef
key: ${{ runner.os }}-${{ runner.arch }}-cef-v146.4.1
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies (Linux only)
if: matrix.os == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils file
- name: Install Tauri CEF CLI (Linux only)
if: matrix.os == 'linux'
env:
RUSTFLAGS: "--cfg rustix_use_libc"
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/cef --locked
- name: Install frontend dependencies
run: bun install --ignore-scripts && bun scripts/postinstall.ts
- name: Import Apple Certificate (macOS only)
if: matrix.os == 'macos'
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
run: |
set -euo pipefail
if [[ -z "$APPLE_CERTIFICATE" || -z "$APPLE_CERT_PASSWORD" ]]; then
echo "APPLE_CODE_SIGNING=false" >> "$GITHUB_ENV"
echo "Apple signing certificate secrets are not configured; building unsigned macOS bundles."
exit 0
fi
echo "APPLE_CODE_SIGNING=true" >> "$GITHUB_ENV"
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$APPLE_CERT_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security find-identity -v -p codesigning
- name: Prepare Tauri macOS signing (macOS only)
if: matrix.os == 'macos'
env:
APPLE_SIGNING_IDENTITY: "${{ secrets.APPLE_SIGNING_IDENTITY || 'Developer ID Application: Mehmet Ozgul (N43BXFX4H4)' }}"
run: node scripts/release/macos/prepare-tauri-signing.mjs
- name: Sign bundled macOS binaries (macOS only)
if: matrix.os == 'macos'
env:
APPLE_SIGNING_IDENTITY: "${{ secrets.APPLE_SIGNING_IDENTITY || 'Developer ID Application: Mehmet Ozgul (N43BXFX4H4)' }}"
shell: bash
run: |
set -euo pipefail
if [[ "${APPLE_CODE_SIGNING:-}" != "true" ]]; then
echo "Apple code signing is not configured; skipping bundled binary signing."
exit 0
fi
if [[ ! -d src/extensions/bundled ]]; then
exit 0
fi
while IFS= read -r -d '' file_path; do
file_type="$(file -b "$file_path" || true)"
if [[ "$file_type" == *"Mach-O"* ]]; then
echo "Signing bundled binary: $file_path"
codesign --force --options runtime --sign "$APPLE_SIGNING_IDENTITY" "$file_path"
fi
done < <(find src/extensions/bundled -type f -perm -111 -print0)
- name: Import Windows Certificate (Windows only)
if: matrix.os == 'windows'
shell: pwsh
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
WINDOWS_TIMESTAMP_URL: ${{ secrets.WINDOWS_TIMESTAMP_URL }}
run: |
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE) -or
[string]::IsNullOrWhiteSpace($env:WINDOWS_CERTIFICATE_PASSWORD)) {
"WINDOWS_AUTHENTICODE_SIGNING=false" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
Write-Host "Windows Authenticode signing secrets are not configured; building unsigned Windows installers."
exit 0
}
"WINDOWS_AUTHENTICODE_SIGNING=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
New-Item -ItemType Directory -Force -Path certificate | Out-Null
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
if ($LASTEXITCODE -ne 0) {
throw "Failed to decode Windows certificate from base64."
}
$securePassword = ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force
$importedCertificates = Import-PfxCertificate `
-FilePath certificate/certificate.pfx `
-CertStoreLocation Cert:\CurrentUser\My `
-Password $securePassword
$signingCertificate = $importedCertificates |
Where-Object { $_.HasPrivateKey } |
Select-Object -First 1
if (-not $signingCertificate) {
throw "Failed to import a Windows signing certificate with a private key."
}
"WINDOWS_CERTIFICATE_THUMBPRINT=$($signingCertificate.Thumbprint)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"WINDOWS_DIGEST_ALGORITHM=sha256" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
if ([string]::IsNullOrWhiteSpace($env:WINDOWS_TIMESTAMP_URL)) {
"WINDOWS_TIMESTAMP_URL=http://time.certum.pl/" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
} else {
"WINDOWS_TIMESTAMP_URL=$($env:WINDOWS_TIMESTAMP_URL)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
}
Remove-Item -Recurse -Force certificate
- name: Prepare Tauri Windows signing (Windows only)
if: matrix.os == 'windows'
run: node scripts/release/windows/prepare-tauri-signing.mjs
- name: Build Linux Tauri bundles
if: matrix.os == 'linux'
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
CEF_PATH: ${{ github.workspace }}/.cache/tauri-cef
NO_STRIP: "true"
RUSTFLAGS: ${{ matrix.rustflags }}
run: |
cargo tauri build --no-bundle ${{ needs.release-metadata.outputs.is_prerelease == 'true' && '--config src-tauri/tauri.preview.conf.json' || '' }} -- ${{ matrix.cargo_features }}
- name: Build desktop Tauri bundles
if: matrix.os != 'linux'
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
APPLE_ID: ${{ matrix.os == 'macos' && secrets.APPLE_ID || '' }}
APPLE_PASSWORD: ${{ matrix.os == 'macos' && secrets.APPLE_PASSWORD || '' }}
APPLE_TEAM_ID: ${{ matrix.os == 'macos' && secrets.APPLE_TEAM_ID || '' }}
RUSTFLAGS: ${{ matrix.rustflags }}
run: bun tauri build ${{ matrix.args }} ${{ needs.release-metadata.outputs.is_prerelease == 'true' && '--config src-tauri/tauri.preview.conf.json' || '' }}
- name: Package Linux tarball (Linux only)
if: matrix.os == 'linux'
env:
ATHAS_RELEASE_CHANNEL: ${{ needs.release-metadata.outputs.is_prerelease == 'true' && 'preview' || 'stable' }}
CEF_PATH: ${{ github.workspace }}/.cache/tauri-cef
run: bash scripts/release/package-linux-tarball.sh "${{ runner.arch }}" release-dist
- name: Sign Linux tarball (Linux only)
if: matrix.os == 'linux'
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
set -euo pipefail
for archive in release-dist/*.tar.gz; do
bun tauri signer sign "$archive"
test -s "${archive}.sig"
done
- name: Collect Windows user installer (Windows only)
if: matrix.os == 'windows'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path release-dist | Out-Null
$setup = Get-ChildItem -Recurse -Path "${{ matrix.bundle_path }}" -File -Filter "*-setup.exe" |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $setup) {
throw "Could not find Windows setup installer."
}
Copy-Item $setup.FullName release-dist/
if (-not (Test-Path "$($setup.FullName).sig")) {
throw "Could not find updater signature for $($setup.Name)."
}
- name: Upload platform artifacts
uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.artifact }}
path: |
${{ matrix.bundle_path }}/**/*.dmg
${{ matrix.bundle_path }}/**/*.app.tar.gz
${{ matrix.bundle_path }}/**/*.sig
release-dist/*
if-no-files-found: error
assemble-release:
needs: [release-metadata, build-tauri]
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ success() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') }}
steps:
- uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Download release metadata
uses: actions/download-artifact@v4
with:
name: release-metadata
path: release-metadata
- name: Download platform artifacts
uses: actions/download-artifact@v4
with:
pattern: release-*
path: release-artifacts
merge-multiple: true
- name: Assemble release assets
env:
TAG_NAME: ${{ needs.release-metadata.outputs.tag_name }}
run: |
bun scripts/release/release-assets.mjs assemble \
--tag "${TAG_NAME}" \
--repo "${{ github.repository }}" \
--dir release-artifacts \
--notes-file release-metadata/release-body.md \
--out prepared-release
- name: Create or update draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release-metadata.outputs.tag_name }}
RELEASE_NAME: ${{ needs.release-metadata.outputs.release_name }}
IS_PRERELEASE: ${{ needs.release-metadata.outputs.is_prerelease }}
run: |
set -euo pipefail
release_count="$(gh api "/repos/${{ github.repository }}/releases?per_page=100" \
--jq "map(select(.tag_name == \"${TAG_NAME}\")) | length")"
if [[ "$release_count" -gt 1 ]]; then
echo "Found ${release_count} releases with tag ${TAG_NAME}; refusing to upload into ambiguous release state."
exit 1
fi
if gh release view "${TAG_NAME}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
is_draft="$(gh release view "${TAG_NAME}" --repo "${{ github.repository }}" --json isDraft --jq ".isDraft")"
if [[ "$is_draft" != "true" ]]; then
echo "Release ${TAG_NAME} is already published; refusing to mutate it."
exit 1
fi
gh release edit "${TAG_NAME}" \
--repo "${{ github.repository }}" \
--title "${RELEASE_NAME}" \
--notes-file release-metadata/release-body.md
else
create_args=(
release create "${TAG_NAME}"
--repo "${{ github.repository }}"
--draft
--target "${GITHUB_SHA}"
--title "${RELEASE_NAME}"
--notes-file release-metadata/release-body.md
)
if [[ "$IS_PRERELEASE" == "true" ]]; then
create_args+=(--prerelease)
fi
gh "${create_args[@]}"
fi
- name: Upload release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release-metadata.outputs.tag_name }}
run: |
gh release upload "${TAG_NAME}" \
--repo "${{ github.repository }}" \
prepared-release/* \
--clobber
- name: Validate draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.release-metadata.outputs.tag_name }}
run: |
bun scripts/release/release-assets.mjs verify \
--tag "${TAG_NAME}" \
--repo "${{ github.repository }}"