Prepare release #86
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }}" |