Skip to content

desktop-release-r2

desktop-release-r2 #18

name: desktop-release-r2
run-name: desktop-release-r2
on:
workflow_dispatch:
inputs:
release_tag:
description: Optional existing release tag to publish from during a manual run.
required: false
type: string
release_channel:
description: Channel to publish when running the workflow manually.
required: false
default: stable
type: choice
options:
- stable
- beta
release:
types:
- published
- prereleased
permissions:
contents: write
jobs:
macos-r2-release:
runs-on: macos-latest
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
MAABARIUM_UPDATE_PUBKEY: ${{ vars.MAABARIUM_UPDATE_PUBKEY || secrets.MAABARIUM_UPDATE_PUBKEY }}
MAABARIUM_UPDATE_BASE_URL: ${{ vars.MAABARIUM_UPDATE_BASE_URL }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ vars.APPLE_SIGNING_IDENTITY || secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID || secrets.APPLE_TEAM_ID }}
APPLE_PROVIDER_SHORT_NAME: ${{ vars.APPLE_PROVIDER_SHORT_NAME || secrets.APPLE_PROVIDER_SHORT_NAME }}
R2_BUCKET: ${{ vars.CLOUDFLARE_R2_BUCKET }}
R2_ENDPOINT: ${{ vars.CLOUDFLARE_R2_ENDPOINT }}
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
AWS_EC2_METADATA_DISABLED: "true"
steps:
- name: Resolve release context
id: release-context
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
echo "tag=${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
echo "ref=refs/tags/${{ github.event.release.tag_name }}" >> "$GITHUB_OUTPUT"
echo "upload_release=true" >> "$GITHUB_OUTPUT"
if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then
echo "channel=beta" >> "$GITHUB_OUTPUT"
echo "publish_root_manifest=false" >> "$GITHUB_OUTPUT"
else
echo "channel=stable" >> "$GITHUB_OUTPUT"
echo "publish_root_manifest=true" >> "$GITHUB_OUTPUT"
fi
elif [[ -n "${{ inputs.release_tag || '' }}" ]]; then
echo "tag=${{ inputs.release_tag }}" >> "$GITHUB_OUTPUT"
echo "ref=refs/tags/${{ inputs.release_tag }}" >> "$GITHUB_OUTPUT"
echo "upload_release=true" >> "$GITHUB_OUTPUT"
echo "channel=${{ inputs.release_channel || 'stable' }}" >> "$GITHUB_OUTPUT"
if [[ "${{ inputs.release_channel || 'stable' }}" == "stable" ]]; then
echo "publish_root_manifest=true" >> "$GITHUB_OUTPUT"
else
echo "publish_root_manifest=false" >> "$GITHUB_OUTPUT"
fi
else
echo "tag=" >> "$GITHUB_OUTPUT"
echo "ref=${GITHUB_REF}" >> "$GITHUB_OUTPUT"
echo "upload_release=false" >> "$GITHUB_OUTPUT"
echo "channel=${{ inputs.release_channel || 'stable' }}" >> "$GITHUB_OUTPUT"
if [[ "${{ inputs.release_channel || 'stable' }}" == "stable" ]]; then
echo "publish_root_manifest=true" >> "$GITHUB_OUTPUT"
else
echo "publish_root_manifest=false" >> "$GITHUB_OUTPUT"
fi
fi
- uses: actions/checkout@v4
with:
ref: ${{ steps.release-context.outputs.ref }}
- uses: pnpm/action-setup@v4
with:
version: 10
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: crates/maabarium-desktop/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: |
. -> target
- name: Validate release configuration
shell: bash
run: |
set -euo pipefail
: "${TAURI_SIGNING_PRIVATE_KEY:?TAURI_SIGNING_PRIVATE_KEY must be configured}"
: "${MAABARIUM_UPDATE_PUBKEY:?MAABARIUM_UPDATE_PUBKEY must be configured}"
: "${MAABARIUM_UPDATE_BASE_URL:?MAABARIUM_UPDATE_BASE_URL must be configured}"
: "${APPLE_CERTIFICATE:?APPLE_CERTIFICATE must be configured}"
: "${APPLE_CERTIFICATE_PASSWORD:?APPLE_CERTIFICATE_PASSWORD must be configured}"
: "${APPLE_ID:?APPLE_ID must be configured}"
: "${APPLE_PASSWORD:?APPLE_PASSWORD must be configured}"
: "${APPLE_TEAM_ID:?APPLE_TEAM_ID must be configured}"
: "${R2_BUCKET:?CLOUDFLARE_R2_BUCKET must be configured}"
: "${R2_ENDPOINT:?CLOUDFLARE_R2_ENDPOINT must be configured}"
: "${AWS_ACCESS_KEY_ID:?CLOUDFLARE_R2_ACCESS_KEY_ID must be configured}"
: "${AWS_SECRET_ACCESS_KEY:?CLOUDFLARE_R2_SECRET_ACCESS_KEY must be configured}"
node "$GITHUB_WORKSPACE/crates/maabarium-desktop/scripts/validate-updater-pubkey.mjs" --github-env
- name: Install desktop dependencies
run: cd crates/maabarium-desktop && pnpm install --frozen-lockfile
- name: Validate updater signing keypair
shell: bash
run: |
set -euo pipefail
node "$GITHUB_WORKSPACE/crates/maabarium-desktop/scripts/validate-updater-keypair.mjs"
- name: Import Apple signing certificate into keychain
shell: bash
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/maabarium-apple-signing-cert.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/maabarium-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 24)"
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_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 "$CERT_PATH" \
-k "$KEYCHAIN_PATH" \
-P "$APPLE_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security \
-T /usr/bin/productbuild
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s \
-k "$KEYCHAIN_PASSWORD" \
"$KEYCHAIN_PATH"
EXISTING_KEYCHAINS=()
while IFS= read -r keychain; do
EXISTING_KEYCHAINS+=("${keychain//\"/}")
done < <(security list-keychains -d user)
security list-keychains -d user -s "$KEYCHAIN_PATH" "${EXISTING_KEYCHAINS[@]}"
security default-keychain -d user -s "$KEYCHAIN_PATH"
- name: Build signed Tauri bundle
env:
TAURI_SIGNING_PRIVATE_KEY_PASSWORD_SECRET: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
MAABARIUM_REQUIRE_APPLE_CLI_SIGNING: "1"
MAABARIUM_UPDATE_CHANNEL: ${{ steps.release-context.outputs.channel }}
shell: bash
run: |
set -euo pipefail
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD_SECRET:-}"
TAURI_UPDATER_PUBKEY="$(if [[ -n "${MAABARIUM_UPDATE_PUBKEY_FILE:-}" ]]; then node "$GITHUB_WORKSPACE/crates/maabarium-desktop/scripts/normalize-updater-key.mjs" --file "$MAABARIUM_UPDATE_PUBKEY_FILE" --format wrapped; else node "$GITHUB_WORKSPACE/crates/maabarium-desktop/scripts/normalize-updater-key.mjs" --value "$MAABARIUM_UPDATE_PUBKEY" --format wrapped; fi)"
export TAURI_CONFIG="$(node -e 'process.stdout.write(JSON.stringify({ productName: "Maabarium-Console", bundle: { targets: ["app"], macOS: { entitlements: "Entitlements.plist" } }, plugins: { updater: { pubkey: process.argv[1] } } }));' "$TAURI_UPDATER_PUBKEY")"
cd crates/maabarium-desktop
pnpm tauri build \
--ci \
--config "$TAURI_CONFIG"
- name: Verify macOS desktop entitlements
shell: bash
run: |
set -euo pipefail
"$GITHUB_WORKSPACE/crates/maabarium-desktop/scripts/verify-macos-entitlements.sh" \
"$GITHUB_WORKSPACE/target/release/bundle/macos/Maabarium-Console.app"
- name: Stage updater bundle and manifest
env:
RELEASE_CHANNEL: ${{ steps.release-context.outputs.channel }}
shell: bash
run: |
set -euo pipefail
DESKTOP_DIR="$GITHUB_WORKSPACE/crates/maabarium-desktop"
BUNDLE_DIR="$GITHUB_WORKSPACE/target/release/bundle/macos"
RELEASE_DIR="$DESKTOP_DIR/release"
STAGING_DIR="$RELEASE_DIR/staging"
MANIFEST_DIR="$RELEASE_DIR/$RELEASE_CHANNEL"
case "$(uname -m)" in
arm64|aarch64)
PLATFORM_KEY="darwin-aarch64"
CLI_TARGET_TRIPLE="aarch64-apple-darwin"
;;
x86_64)
PLATFORM_KEY="darwin-x86_64"
CLI_TARGET_TRIPLE="x86_64-apple-darwin"
;;
*)
echo "Unsupported macOS runner architecture: $(uname -m)" >&2
exit 1
;;
esac
UPDATER_BUNDLE="$(find "$BUNDLE_DIR" -maxdepth 1 -name "*.app.tar.gz" -print -quit)"
if [[ -z "$UPDATER_BUNDLE" ]]; then
echo "No macOS updater bundle was produced under $BUNDLE_DIR" >&2
exit 1
fi
SIG_PATH="${UPDATER_BUNDLE}.sig"
if [[ ! -f "$SIG_PATH" ]]; then
echo "Missing updater signature for $UPDATER_BUNDLE" >&2
exit 1
fi
CLI_BINARY="$GITHUB_WORKSPACE/target/cli-bundle/$CLI_TARGET_TRIPLE/release/maabarium"
if [[ ! -f "$CLI_BINARY" ]]; then
echo "No bundled CLI binary was produced at $CLI_BINARY" >&2
exit 1
fi
mkdir -p "$STAGING_DIR/$PLATFORM_KEY"
mkdir -p "$MANIFEST_DIR"
BUNDLE_NAME="$(basename "$UPDATER_BUNDLE")"
RELEASE_BUNDLE_NAME="${BUNDLE_NAME// /-}"
RELEASE_SIG_NAME="${RELEASE_BUNDLE_NAME}.sig"
CLI_ARCHIVE_NAME="maabarium-${PLATFORM_KEY}.tar.gz"
cp "$UPDATER_BUNDLE" "$STAGING_DIR/$PLATFORM_KEY/$RELEASE_BUNDLE_NAME"
cp "$SIG_PATH" "$STAGING_DIR/$PLATFORM_KEY/$RELEASE_SIG_NAME"
tar -C "$(dirname "$CLI_BINARY")" -czf "$STAGING_DIR/$PLATFORM_KEY/$CLI_ARCHIVE_NAME" "$(basename "$CLI_BINARY")"
if [[ "$RELEASE_BUNDLE_NAME" != "$BUNDLE_NAME" ]]; then
echo "Publishing updater bundle as $RELEASE_BUNDLE_NAME"
fi
cd "$DESKTOP_DIR"
pnpm build:release-manifest -- \
--base-url "$MAABARIUM_UPDATE_BASE_URL" \
--channel "$RELEASE_CHANNEL" \
--artifacts-dir "$STAGING_DIR" \
--output "$MANIFEST_DIR/latest.json" \
--platform "$PLATFORM_KEY=$PLATFORM_KEY/$RELEASE_BUNDLE_NAME" \
--cli-platform "$PLATFORM_KEY=$PLATFORM_KEY/$CLI_ARCHIVE_NAME"
- name: Generate install.sh from updater metadata base URL
shell: bash
run: |
set -euo pipefail
node "$GITHUB_WORKSPACE/scripts/release/generate-install-script.mjs" \
--base-url "$MAABARIUM_UPDATE_BASE_URL" \
--output "$GITHUB_WORKSPACE/crates/maabarium-desktop/release/install.sh"
chmod +x "$GITHUB_WORKSPACE/crates/maabarium-desktop/release/install.sh"
- name: Upload release artifacts to workflow
uses: actions/upload-artifact@v4
with:
name: desktop-release-r2-macos
path: |
crates/maabarium-desktop/release/install.sh
crates/maabarium-desktop/release/stable/latest.json
crates/maabarium-desktop/release/beta/latest.json
crates/maabarium-desktop/release/staging/**
- name: Upload desktop artifacts to GitHub Release
if: steps.release-context.outputs.upload_release == 'true'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
DESKTOP_DIR="$GITHUB_WORKSPACE/crates/maabarium-desktop"
release_assets=()
manifest_path="$DESKTOP_DIR/release/${{ steps.release-context.outputs.channel }}/latest.json"
while IFS= read -r asset_path; do
release_assets+=("$asset_path")
done < <(find "$DESKTOP_DIR/release/staging" -type f | sort)
gh release upload "${{ steps.release-context.outputs.tag }}" \
"$manifest_path" \
"$DESKTOP_DIR/release/install.sh" \
"${release_assets[@]}" \
--clobber
- name: Publish bundles and latest.json to Cloudflare R2
shell: bash
run: |
set -euo pipefail
DESKTOP_DIR="$GITHUB_WORKSPACE/crates/maabarium-desktop"
aws s3 sync "$DESKTOP_DIR/release/staging/" "s3://$R2_BUCKET/" --delete --endpoint-url "$R2_ENDPOINT"
aws s3 cp "$DESKTOP_DIR/release/${{ steps.release-context.outputs.channel }}/latest.json" "s3://$R2_BUCKET/${{ steps.release-context.outputs.channel }}/latest.json" --endpoint-url "$R2_ENDPOINT"
if [[ "${{ steps.release-context.outputs.publish_root_manifest }}" == "true" ]]; then
aws s3 cp "$DESKTOP_DIR/release/${{ steps.release-context.outputs.channel }}/latest.json" "s3://$R2_BUCKET/latest.json" --endpoint-url "$R2_ENDPOINT"
fi
aws s3 cp "$DESKTOP_DIR/release/install.sh" "s3://$R2_BUCKET/install.sh" --endpoint-url "$R2_ENDPOINT" --content-type "text/x-shellscript; charset=utf-8"