Skip to content

.github/workflows/desktop_cd.yaml #1097

.github/workflows/desktop_cd.yaml

.github/workflows/desktop_cd.yaml #1097

Workflow file for this run

# https://docs.crabnebula.dev/cloud/ci/tauri-v2-workflow/
on:
workflow_dispatch:
inputs:
channel:
description: "Release channel"
required: true
type: choice
options:
- staging
- stable
candidate_sha:
description: "Immutable candidate commit to check out"
required: false
type: string
default: ""
publish:
description: "Legacy compatibility input; stable publication must use desktop_publish"
type: boolean
default: false
include_linux:
description: "Build and include Linux beta artifacts"
type: boolean
default: true
include_windows:
description: "Build and include Windows artifacts"
type: boolean
default: true
version:
description: "Explicit semantic version required for stable releases"
required: false
type: string
default: ""
concurrency:
group: ${{ inputs.channel == 'stable' && 'desktop-stable-release' || format('{0}-{1}-{2}', github.workflow, github.ref, inputs.channel) }}
cancel-in-progress: ${{ inputs.channel != 'stable' }}
env:
CN_APPLICATION: "fastrepl/hyprnote2"
RELEASE_CHANNEL: ${{ inputs.channel }}
TAURI_CONF_PATH: ./src-tauri/tauri.conf.${{ inputs.channel }}.json
NODE_OPTIONS: "--max-old-space-size=4096"
jobs:
compute-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.candidate_sha || github.sha }}
lfs: true
fetch-depth: 0
fetch-tags: true
- run: |
git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
git fetch --tags --force
- if: ${{ inputs.channel == 'stable' }}
run: |
if [[ "${{ inputs.publish }}" == "true" ]]; then
echo "::error::desktop_cd only builds stable candidates; publish through desktop_publish after QA"
exit 1
fi
git fetch origin main --no-tags
TARGET=$(git rev-parse HEAD)
MAIN=$(git rev-parse origin/main)
if [[ "$TARGET" != "$MAIN" ]]; then
echo "::error::Stable releases must run from the current main commit ($MAIN), got $TARGET"
exit 1
fi
- uses: ./.github/actions/doxxer_install
- id: version
env:
INPUT_VERSION: ${{ inputs.version }}
RELEASE_CHANNEL: ${{ inputs.channel }}
run: |
if [[ "$RELEASE_CHANNEL" == "stable" ]]; then
if [[ ! "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Stable releases require an explicit semantic version such as 1.4.0"
exit 1
fi
VERSION="$INPUT_VERSION"
if [[ ! -f "packages/changelog/content/$VERSION.md" ]]; then
echo "::error::Missing changelog for stable version $VERSION"
exit 1
fi
elif [[ -n "$INPUT_VERSION" ]]; then
VERSION="$INPUT_VERSION"
elif [[ "$RELEASE_CHANNEL" == "staging" ]]; then
VERSION=$(doxxer --config doxxer.desktop.toml next dev)
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Computed version: $VERSION"
cn-draft:
if: ${{ inputs.channel != 'staging' }}
needs: compute-version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.candidate_sha || github.sha }}
- run: ./scripts/version.sh "./apps/desktop/src-tauri/tauri.conf.json" "${{ needs.compute-version.outputs.version }}"
- if: ${{ inputs.include_windows }}
name: Verify stable signing configuration
env:
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }}
AZURE_ARTIFACT_SIGNING_ENDPOINT: ${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME: ${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }}
AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME: ${{ vars.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
WINDOWS_SIGNER_ORGANIZATION: ${{ vars.WINDOWS_SIGNER_ORGANIZATION }}
run: |
missing=()
[[ -n "$AZURE_CLIENT_ID" ]] || missing+=("AZURE_CLIENT_ID")
[[ -n "$AZURE_TENANT_ID" ]] || missing+=("AZURE_TENANT_ID")
[[ -n "$AZURE_SUBSCRIPTION_ID" ]] || missing+=("AZURE_SUBSCRIPTION_ID")
[[ -n "$AZURE_ARTIFACT_SIGNING_ENDPOINT" ]] || missing+=("AZURE_ARTIFACT_SIGNING_ENDPOINT")
[[ -n "$AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME" ]] || missing+=("AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME")
[[ -n "$AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME" ]] || missing+=("AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME")
[[ -n "$WINDOWS_SIGNER_ORGANIZATION" ]] || missing+=("WINDOWS_SIGNER_ORGANIZATION")
if [[ ${#missing[@]} -gt 0 ]]; then
echo "::error::Stable Windows release signing configuration is missing: ${missing[*]}"
exit 1
fi
- uses: ./.github/actions/cn_release
with:
cmd: draft
app: ${{ env.CN_APPLICATION }}
key: ${{ secrets.CN_API_KEY }}
channel: ${{ env.RELEASE_CHANNEL }}
framework: tauri
working-directory: ./apps/desktop
build-macos:
needs: [compute-version, cn-draft]
if: ${{ !cancelled() && (needs.cn-draft.result == 'success' || needs.cn-draft.result == 'skipped') }}
permissions:
contents: write
runs-on: depot-macos-26
strategy:
fail-fast: true
matrix:
include: ${{ inputs.channel == 'staging' && fromJSON('[{"target":"aarch64-apple-darwin","arch":"aarch64","artifact_name":"silicon"}]') || fromJSON('[{"target":"aarch64-apple-darwin","arch":"aarch64","artifact_name":"silicon"},{"target":"x86_64-apple-darwin","arch":"x86_64","artifact_name":"intel"}]') }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.candidate_sha || github.sha }}
lfs: true
fetch-depth: 0
fetch-tags: true
submodules: recursive
- run: |
git remote set-url origin "https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git"
git fetch --tags --force
- uses: ./.github/actions/macos_tcc
- run: ./scripts/version.sh "./apps/desktop/src-tauri/tauri.conf.json" "${{ needs.compute-version.outputs.version }}"
- uses: ./.github/actions/install_desktop_deps
with:
target: macos
- uses: ./.github/actions/rust_install
with:
platform: macos
- run: echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> $GITHUB_ENV
- uses: ./.github/actions/pnpm_install
- run: pnpm -F ui build
- run: |
TAURI_ENV_TARGET_TRIPLE=${{ matrix.target }} cargo xtask prepare-binaries
./scripts/sidecar.sh "./apps/desktop/${{ env.TAURI_CONF_PATH }}" "binaries/char-chrome-native-host"
./scripts/sidecar.sh "./apps/desktop/${{ env.TAURI_CONF_PATH }}" "binaries/check-permissions"
./scripts/sidecar.sh "./apps/desktop/${{ env.TAURI_CONF_PATH }}" "resources/cli/anarlog-cli"
- uses: ./.github/actions/apple_cert
id: apple-cert
with:
apple-certificate: ${{ secrets.APPLE_CERTIFICATE }}
apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
keychain-password: ${{ secrets.KEYCHAIN_PASSWORD }}
- run: |
CLOUDSYNC_DYLIB="../../crates/cloudsync/vendor/cloudsync/macos/${{ matrix.arch }}/cloudsync.dylib"
codesign --force --sign "${{ steps.apple-cert.outputs.cert-id }}" --timestamp --options runtime "$CLOUDSYNC_DYLIB"
codesign --verify --strict --verbose=2 "$CLOUDSYNC_DYLIB"
working-directory: ./apps/desktop
- run: |
BUILD_ARGS=(--target "${{ matrix.target }}" --config "${{ env.TAURI_CONF_PATH }}" --verbose)
if [[ "${{ matrix.target }}" == "x86_64-apple-darwin" ]]; then
BUILD_ARGS+=(--config ./src-tauri/tauri.conf.macos-intel.json)
fi
if [[ "${{ inputs.channel }}" == "staging" ]]; then
BUILD_ARGS+=(--features devtools)
fi
pnpm -F desktop tauri build "${BUILD_ARGS[@]}"
env:
# https://v2.tauri.app/reference/environment-variables/
CI: false
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
VITE_POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN_HYPRNOTE_2 }}
CARGO_PROFILE_RELEASE_DEBUG: line-tables-only
APP_VERSION: ${{ needs.compute-version.outputs.version }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ steps.apple-cert.outputs.cert-id }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_APP_URL: "https://anarlog.so"
VITE_API_URL: "https://api.anarlog.so"
VITE_PRO_PRODUCT_ID: ${{ secrets.VITE_PRO_PRODUCT_ID }}
VITE_APP_VERSION: ${{ needs.compute-version.outputs.version }}
- if: ${{ inputs.channel == 'stable' }}
id: macos-symbols
name: Generate and verify macOS debug symbols
run: |
shopt -s nullglob
app_dir="apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos"
apps=("$app_dir"/*.app)
if [[ ${#apps[@]} -ne 1 ]]; then
echo "::error::Expected one macOS app for ${{ matrix.target }}, found ${#apps[@]}"
exit 1
fi
binary="${apps[0]}/Contents/MacOS/anarlog"
dsym="apps/desktop/src-tauri/target/${{ matrix.target }}/release/anarlog.dSYM"
dsymutil "$binary" -o "$dsym"
binary_uuid=$(dwarfdump --uuid "$binary" | awk '{print $2}')
dsym_uuid=$(dwarfdump --uuid "$dsym" | awk '{print $2}')
if [[ -z "$binary_uuid" || "$binary_uuid" != "$dsym_uuid" ]]; then
echo "::error::dSYM UUID does not match the packaged binary"
exit 1
fi
echo "Verified dSYM UUID $dsym_uuid"
echo "path=$dsym" >> "$GITHUB_OUTPUT"
- if: ${{ inputs.channel == 'stable' }}
uses: ./.github/actions/sentry_cli
- if: ${{ inputs.channel == 'stable' }}
name: Upload macOS debug symbols to Sentry
env:
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }}
INFISICAL_TOKEN: ${{ secrets.INFISICAL_SENTRY_TOKEN }}
run: |
response=$(
curl --fail --silent --show-error --get \
--header "Authorization: Bearer $INFISICAL_TOKEN" \
--data-urlencode "projectId=$INFISICAL_PROJECT_ID" \
--data-urlencode "environment=prod" \
--data-urlencode "secretPath=/anarlog/github" \
--data-urlencode "expandSecretReferences=false" \
"https://app.infisical.com/api/v4/secrets/SENTRY_AUTH_TOKEN"
)
if ! SENTRY_AUTH_TOKEN=$(jq -er '.secret.secretValue | select(length > 0)' <<< "$response"); then
echo "::error::Missing SENTRY_AUTH_TOKEN in Infisical prod:/anarlog/github"
exit 1
fi
export SENTRY_AUTH_TOKEN
sentry-cli debug-files upload \
--org fastrepl \
--project hyprnote2 \
--include-sources \
"${{ steps.macos-symbols.outputs.path }}"
- if: ${{ inputs.channel == 'stable' }}
name: Verify macOS updater signature
run: |
shopt -s nullglob
updater_dir="apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/macos"
apps=("$updater_dir"/*.app)
artifacts=("$updater_dir"/*.app.tar.gz)
signatures=("$updater_dir"/*.app.tar.gz.sig)
if [[ ${#apps[@]} -ne 1 || ${#artifacts[@]} -ne 1 || ${#signatures[@]} -ne 1 ]]; then
echo "Expected one macOS app, updater artifact, and signature for ${{ matrix.target }}"
exit 1
fi
app_version=$(plutil -extract CFBundleShortVersionString raw -o - "${apps[0]}/Contents/Info.plist")
if [[ "$app_version" != "${{ needs.compute-version.outputs.version }}" ]]; then
echo "Expected macOS app version ${{ needs.compute-version.outputs.version }}, got $app_version"
exit 1
fi
cargo run --locked -p updater-core --bin verify-updater-signature -- \
"${artifacts[0]}" \
"${signatures[0]}" \
apps/desktop/src-tauri/tauri.conf.json
- id: find-dmg
shell: bash
run: |
DMG_FILE=$(find "apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/" -name "*.dmg" -type f | head -1)
if [[ -z "$DMG_FILE" ]]; then
echo "::error::No DMG found in bundle output"
exit 1
fi
echo "path=$DMG_FILE" >> $GITHUB_OUTPUT
- uses: ./.github/actions/macos_notarize_dmg
with:
dmg-path: ${{ steps.find-dmg.outputs.path }}
signing-identity: ${{ steps.apple-cert.outputs.cert-id }}
apple-id: ${{ secrets.APPLE_ID }}
apple-password: ${{ secrets.APPLE_PASSWORD }}
apple-team-id: ${{ secrets.APPLE_TEAM_ID }}
- run: |
mkdir -p target/release/
find target/${{ matrix.target }}/release -type f -not -path "*/\.*" -exec cp {} target/release/ \;
shell: bash
working-directory: ./apps/desktop/src-tauri
- if: ${{ inputs.channel != 'staging' }}
uses: ./.github/actions/cn_release
with:
cmd: upload
app: ${{ env.CN_APPLICATION }}
key: ${{ secrets.CN_API_KEY }}
channel: ${{ env.RELEASE_CHANNEL }}
framework: tauri
working-directory: ./apps/desktop
- if: ${{ inputs.channel == 'staging' }}
uses: actions/upload-artifact@v4
with:
name: hyprnote-staging-macos-${{ matrix.artifact_name }}
path: apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
retention-days: 3
- if: ${{ inputs.channel == 'staging' }}
run: |
TIMESTAMP=$(date -u +"%Y%m%d%H%M%S")
COMMIT_HASH=$(git rev-parse --short HEAD)
DMG_FILE=$(find "apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/dmg/" -name "*.dmg" -type f)
FILENAME="hyprnote-staging-${TIMESTAMP}-${COMMIT_HASH}-macos-${{ matrix.arch }}.dmg"
cp "$DMG_FILE" "$FILENAME"
aws s3 cp "$FILENAME" \
"s3://hyprnote-build/desktop/staging/$FILENAME" \
--endpoint-url ${{ secrets.CLOUDFLARE_R2_ENDPOINT_URL }} \
--region auto
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
build-linux:
needs: [compute-version, cn-draft]
if: ${{ inputs.include_linux && !cancelled() && (needs.cn-draft.result == 'success' || needs.cn-draft.result == 'skipped') }}
strategy:
fail-fast: true
matrix:
include:
- runner: depot-ubuntu-24.04-8
target: x86_64-unknown-linux-gnu
rust_platform: linux-x86_64
artifact_name: x64
debian_arch: amd64
file_arch: x86-64
docker_arch: amd64
- runner: depot-ubuntu-24.04-arm-8
target: aarch64-unknown-linux-gnu
rust_platform: linux-aarch64
artifact_name: arm64
debian_arch: arm64
file_arch: ARM aarch64
docker_arch: arm64
runs-on: ${{ matrix.runner }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.candidate_sha || github.sha }}
lfs: true
fetch-depth: 0
fetch-tags: true
submodules: recursive
- run: ./scripts/version.sh "./apps/desktop/src-tauri/tauri.conf.json" "${{ needs.compute-version.outputs.version }}"
- uses: ./.github/actions/install_desktop_deps
with:
target: linux
- uses: ./.github/actions/rust_install
with:
platform: ${{ matrix.rust_platform }}
- uses: ./.github/actions/pnpm_install
- run: pnpm -F ui build
- run: |
TAURI_ENV_TARGET_TRIPLE=${{ matrix.target }} cargo xtask prepare-binaries
./scripts/sidecar.sh "./apps/desktop/${{ env.TAURI_CONF_PATH }}" "binaries/char-chrome-native-host"
./scripts/sidecar.sh "./apps/desktop/${{ env.TAURI_CONF_PATH }}" "resources/cli/anarlog-cli"
- run: |
BUILD_ARGS=(
--ci
--bundles appimage,deb
--target "${{ matrix.target }}"
--config "${{ env.TAURI_CONF_PATH }}"
--verbose
)
if [[ "${{ inputs.channel }}" == "staging" ]]; then
BUILD_ARGS+=(--features devtools)
fi
pnpm -F desktop tauri build "${BUILD_ARGS[@]}"
env:
CARGO_PROFILE_RELEASE_DEBUG: line-tables-only
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
VITE_POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN_HYPRNOTE_2 }}
APP_VERSION: ${{ needs.compute-version.outputs.version }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_APP_URL: https://anarlog.so
VITE_API_URL: https://api.anarlog.so
VITE_PRO_PRODUCT_ID: ${{ secrets.VITE_PRO_PRODUCT_ID }}
VITE_APP_VERSION: ${{ needs.compute-version.outputs.version }}
- if: ${{ inputs.channel == 'stable' }}
uses: ./.github/actions/sentry_cli
- if: ${{ inputs.channel == 'stable' }}
name: Upload Linux debug symbols to Sentry
env:
INFISICAL_PROJECT_ID: ${{ secrets.INFISICAL_PROJECT_ID }}
INFISICAL_TOKEN: ${{ secrets.INFISICAL_SENTRY_TOKEN }}
run: |
binary="apps/desktop/src-tauri/target/${{ matrix.target }}/release/anarlog"
sentry-cli debug-files check "$binary"
response=$(
curl --fail --silent --show-error --get \
--header "Authorization: Bearer $INFISICAL_TOKEN" \
--data-urlencode "projectId=$INFISICAL_PROJECT_ID" \
--data-urlencode "environment=prod" \
--data-urlencode "secretPath=/anarlog/github" \
--data-urlencode "expandSecretReferences=false" \
"https://app.infisical.com/api/v4/secrets/SENTRY_AUTH_TOKEN"
)
if ! SENTRY_AUTH_TOKEN=$(jq -er '.secret.secretValue | select(length > 0)' <<< "$response"); then
echo "::error::Missing SENTRY_AUTH_TOKEN in Infisical prod:/anarlog/github"
exit 1
fi
export SENTRY_AUTH_TOKEN
sentry-cli debug-files upload \
--org fastrepl \
--project hyprnote2 \
--include-sources \
"$binary"
- run: |
shopt -s nullglob
appimage_dir="apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/appimage"
deb_dir="apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/deb"
appimages=("$appimage_dir"/*.AppImage)
debs=("$deb_dir"/*.deb)
appimage_signatures=("$appimage_dir"/*.AppImage.sig)
deb_signatures=("$deb_dir"/*.deb.sig)
if [[ ${#appimages[@]} -ne 1 || ${#debs[@]} -ne 1 ]]; then
echo "Expected one AppImage and one .deb for ${{ matrix.target }}"
exit 1
fi
if [[ ${#appimage_signatures[@]} -ne 1 || ${#deb_signatures[@]} -ne 1 ]]; then
echo "Expected AppImage and .deb updater signatures for ${{ matrix.target }}"
exit 1
fi
package_arch=$(dpkg-deb --field "${debs[0]}" Architecture)
if [[ "$package_arch" != "${{ matrix.debian_arch }}" ]]; then
echo "Expected Debian architecture ${{ matrix.debian_arch }}, got $package_arch"
exit 1
fi
package_version=$(dpkg-deb --field "${debs[0]}" Version)
if [[ "$package_version" != "${{ needs.compute-version.outputs.version }}" ]]; then
echo "Expected Debian version ${{ needs.compute-version.outputs.version }}, got $package_version"
exit 1
fi
if [[ "$(basename "${appimages[0]}")" != *"${{ needs.compute-version.outputs.version }}"* ]]; then
echo "AppImage filename does not contain version ${{ needs.compute-version.outputs.version }}"
exit 1
fi
if ! file "${appimages[0]}" | grep -Fq "${{ matrix.file_arch }}"; then
file "${appimages[0]}"
echo "AppImage architecture does not match ${{ matrix.target }}"
exit 1
fi
if [[ "${{ inputs.channel }}" == "stable" ]]; then
cargo run --locked -p updater-core --bin verify-updater-signature -- \
"${appimages[0]}" \
"${appimage_signatures[0]}" \
apps/desktop/src-tauri/tauri.conf.json
cargo run --locked -p updater-core --bin verify-updater-signature -- \
"${debs[0]}" \
"${deb_signatures[0]}" \
apps/desktop/src-tauri/tauri.conf.json
fi
bundles=("$appimage_dir"/* "$deb_dir"/*)
for bundle in "${bundles[@]}"; do
[[ -f "$bundle" && "$bundle" != *.sha256 ]] || continue
# Record the basename, like the CI job does, so `sha256sum -c` works
# against the uploaded artifact instead of a CI-only path.
name=$(basename "$bundle")
(cd "$(dirname "$bundle")" && sha256sum "$name" > "$name.sha256")
done
- run: |
deb=$(find "apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/deb" -maxdepth 1 -name '*.deb' -type f -print -quit)
binary_name="${{ inputs.channel == 'staging' && 'anarlog-staging' || 'anarlog' }}"
docker run --rm \
--platform linux/${{ matrix.docker_arch }} \
--volume "$PWD:/workspace:ro" \
--env DEB_PATH="/workspace/$deb" \
--env BINARY_PATH="/usr/bin/$binary_name" \
ubuntu:24.04 \
bash -euxo pipefail -c '
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y "$DEB_PATH"
apt-get install -y dbus-x11 xvfb
set +e
timeout --signal=TERM 20s xvfb-run -a dbus-run-session -- "$BINARY_PATH" >/tmp/anarlog.log 2>&1
status=$?
set -e
cat /tmp/anarlog.log
if [[ $status -ne 124 ]]; then
echo "Anarlog exited before the smoke timeout with status $status"
exit 1
fi
'
- run: |
mkdir -p target/release
find target/${{ matrix.target }}/release/bundle/appimage target/${{ matrix.target }}/release/bundle/deb \
-maxdepth 1 -type f -exec cp {} target/release/ \;
working-directory: ./apps/desktop/src-tauri
- uses: actions/upload-artifact@v4
with:
name: anarlog-${{ inputs.channel }}-linux-${{ matrix.artifact_name }}
path: |
apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*
apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/deb/*
if-no-files-found: error
retention-days: 7
- if: ${{ inputs.channel != 'staging' }}
uses: ./.github/actions/cn_release
with:
cmd: upload
app: ${{ env.CN_APPLICATION }}
key: ${{ secrets.CN_API_KEY }}
channel: ${{ env.RELEASE_CHANNEL }}
framework: tauri
working-directory: ./apps/desktop
- if: ${{ inputs.channel == 'staging' }}
run: |
timestamp=$(date -u +"%Y%m%d%H%M%S")
commit_hash=$(git rev-parse --short HEAD)
prefix="anarlog-staging-${timestamp}-${commit_hash}-linux-${{ matrix.artifact_name }}"
upload_dir=$(mktemp -d)
for bundle_dir in appimage deb; do
for file in apps/desktop/src-tauri/target/${{ matrix.target }}/release/bundle/$bundle_dir/*; do
[[ -f "$file" ]] || continue
cp "$file" "$upload_dir/${prefix}-$(basename "$file")"
done
done
aws s3 cp "$upload_dir/" \
s3://hyprnote-build/desktop/staging/ \
--recursive \
--endpoint-url ${{ secrets.CLOUDFLARE_R2_ENDPOINT_URL }} \
--region auto
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}
build-windows:
needs: [compute-version, cn-draft]
if: ${{ inputs.include_windows && !cancelled() && (needs.cn-draft.result == 'success' || needs.cn-draft.result == 'skipped') }}
permissions:
contents: read
id-token: write
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.candidate_sha || github.sha }}
lfs: true
fetch-depth: 0
fetch-tags: true
submodules: recursive
- run: ./scripts/version.sh "./apps/desktop/src-tauri/tauri.conf.json" "${{ needs.compute-version.outputs.version }}"
shell: bash
- uses: ./.github/actions/rust_install
with:
platform: windows
- uses: ./.github/actions/pnpm_install
- run: pnpm -F ui build
- shell: bash
run: |
cargo build --locked --release -p anarlog-cli --target x86_64-pc-windows-msvc
cp \
target/x86_64-pc-windows-msvc/release/anarlog.exe \
apps/desktop/src-tauri/resources/cli/anarlog-cli-x86_64-pc-windows-msvc.exe
./scripts/sidecar.sh \
"./apps/desktop/${{ env.TAURI_CONF_PATH }}" \
resources/cli/anarlog-cli
- name: Build Windows app
shell: bash
run: |
BUILD_ARGS=(
--ci
--target x86_64-pc-windows-msvc
--config "${{ env.TAURI_CONF_PATH }}"
--verbose
)
if [[ "${{ inputs.channel }}" == "staging" ]]; then
BUILD_ARGS+=(--bundles nsis --features devtools --no-sign)
else
BUILD_ARGS+=(--no-bundle --no-sign)
fi
pnpm -F desktop tauri build "${BUILD_ARGS[@]}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
VITE_POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN_HYPRNOTE_2 }}
APP_VERSION: ${{ needs.compute-version.outputs.version }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }}
VITE_APP_URL: https://anarlog.so
VITE_API_URL: https://api.anarlog.so
VITE_PRO_PRODUCT_ID: ${{ secrets.VITE_PRO_PRODUCT_ID }}
VITE_APP_VERSION: ${{ needs.compute-version.outputs.version }}
- if: ${{ inputs.channel == 'stable' }}
name: Sign in to Azure
uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- if: ${{ inputs.channel == 'stable' }}
name: Sign Windows binaries
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
signing-account-name: ${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
files: |
${{ github.workspace }}\apps\desktop\src-tauri\target\x86_64-pc-windows-msvc\release\anarlog.exe
${{ github.workspace }}\apps\desktop\src-tauri\resources\cli\anarlog-cli-x86_64-pc-windows-msvc.exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- if: ${{ inputs.channel == 'stable' }}
name: Bundle signed Windows app
shell: bash
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
pnpm -F desktop tauri bundle \
--ci \
--bundles nsis \
--target x86_64-pc-windows-msvc \
--config "${{ env.TAURI_CONF_PATH }}" \
--verbose \
--no-sign
- if: ${{ inputs.channel == 'stable' }}
id: windows-installer
name: Locate Windows installer
shell: pwsh
run: |
$bundleDir = "apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis"
$installers = @(Get-ChildItem $bundleDir -Filter "*.exe")
if ($installers.Count -ne 1) {
throw "Expected one NSIS installer in $bundleDir, found $($installers.Count)"
}
"path=$($installers[0].FullName)" |
Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- if: ${{ inputs.channel == 'stable' }}
name: Sign Windows installer
uses: azure/artifact-signing-action@v2
with:
endpoint: ${{ vars.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
signing-account-name: ${{ vars.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
files: ${{ steps.windows-installer.outputs.path }}
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- if: ${{ inputs.channel == 'stable' }}
name: Sign Windows updater artifact
shell: pwsh
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: |
$installer = "${{ steps.windows-installer.outputs.path }}"
$signaturePath = "$installer.sig"
Remove-Item $signaturePath -Force -ErrorAction SilentlyContinue
pnpm -F desktop tauri signer sign $installer
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $signaturePath)) {
throw "Could not create the Windows updater signature"
}
- shell: pwsh
env:
EXPECTED_VERSION: ${{ needs.compute-version.outputs.version }}
RELEASE_CHANNEL: ${{ inputs.channel }}
WINDOWS_SIGNER_ORGANIZATION: ${{ vars.WINDOWS_SIGNER_ORGANIZATION }}
run: |
$config = Get-Content "apps/desktop/src-tauri/tauri.conf.json" | ConvertFrom-Json
if ($config.version -ne $env:EXPECTED_VERSION) {
throw "Expected app version $env:EXPECTED_VERSION, got $($config.version)"
}
$bundleDir = "apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis"
$installers = @(Get-ChildItem $bundleDir -Filter "*.exe")
if ($installers.Count -ne 1) {
throw "Expected one NSIS installer in $bundleDir, found $($installers.Count)"
}
if ($installers[0].Name -notmatch [regex]::Escape($env:EXPECTED_VERSION)) {
throw "Installer filename does not contain version $env:EXPECTED_VERSION"
}
if ($env:RELEASE_CHANNEL -eq "stable") {
$updaterSignatures = @(Get-ChildItem $bundleDir -Filter "*.exe.sig")
$expectedUpdaterSignature = "$($installers[0].FullName).sig"
if (
$updaterSignatures.Count -ne 1 -or
$updaterSignatures[0].FullName -ne $expectedUpdaterSignature
) {
throw "Expected the NSIS installer and its updater signature for the stable release"
}
$appBinary = "apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/anarlog.exe"
$sidecarBinary = "apps/desktop/src-tauri/resources/cli/anarlog-cli-x86_64-pc-windows-msvc.exe"
foreach ($signedFile in @($appBinary, $sidecarBinary, $installers[0].FullName)) {
$signature = Get-AuthenticodeSignature $signedFile
if ($signature.Status -ne "Valid") {
throw "Invalid Authenticode signature for $signedFile`: $($signature.Status)"
}
if (-not $signature.SignerCertificate) {
throw "Authenticode signer certificate is missing for $signedFile"
}
$signerOrganization = $signature.SignerCertificate.GetNameInfo(
[System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName,
$false
)
if ($signerOrganization -ne $env:WINDOWS_SIGNER_ORGANIZATION) {
throw "Unexpected Authenticode signer '$signerOrganization' for $signedFile"
}
if (-not $signature.TimeStamperCertificate) {
throw "Authenticode signature is missing a trusted timestamp for $signedFile"
}
}
}
$hash = (Get-FileHash -Algorithm SHA256 $installers[0].FullName).Hash.ToLowerInvariant()
"$hash $($installers[0].Name)" |
Out-File "$($installers[0].FullName).sha256" -Encoding ascii -NoNewline
- if: ${{ inputs.channel == 'stable' }}
name: Verify Windows updater signature
shell: bash
run: |
shopt -s nullglob
bundle_dir="apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis"
installers=("$bundle_dir"/*.exe)
cargo run --locked -p updater-core --bin verify-updater-signature -- \
"${installers[0]}" \
"${installers[0]}.sig" \
apps/desktop/src-tauri/tauri.conf.json
- shell: pwsh
run: |
$source = "apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis"
$destination = "apps/desktop/src-tauri/target/release"
New-Item -ItemType Directory -Force -Path $destination | Out-Null
Copy-Item "$source/*" $destination
- uses: actions/upload-artifact@v4
with:
name: anarlog-${{ inputs.channel }}-windows-x64
path: apps/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*
if-no-files-found: error
retention-days: 7
- if: ${{ inputs.channel != 'staging' }}
uses: ./.github/actions/cn_release
with:
cmd: upload
app: ${{ env.CN_APPLICATION }}
key: ${{ secrets.CN_API_KEY }}
channel: ${{ env.RELEASE_CHANNEL }}
framework: tauri
working-directory: ./apps/desktop
verify-cn-release:
if: ${{ inputs.channel != 'staging' && !cancelled() && needs.build-macos.result == 'success' && (!inputs.include_linux || needs.build-linux.result == 'success') && (!inputs.include_windows || needs.build-windows.result == 'success') }}
needs: [compute-version, build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.candidate_sha || github.sha }}
- id: release
uses: ./.github/actions/cn_release
with:
raw: >-
release show ${{ env.CN_APPLICATION }} ${{ needs.compute-version.outputs.version }}
--channel ${{ env.RELEASE_CHANNEL }}
key: ${{ secrets.CN_API_KEY }}
- env:
EXPECTED_VERSION: ${{ needs.compute-version.outputs.version }}
RELEASE: ${{ steps.release.outputs.raw }}
run: |
ACTUAL_VERSION=$(echo "$RELEASE" | jq -r '.version // empty')
if [[ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]]; then
echo "::error::Expected CrabNebula release $EXPECTED_VERSION, got $ACTUAL_VERSION"
exit 1
fi
EXPECTED_PLATFORMS=(
"dmg-aarch64"
"dmg-x86_64"
)
if [[ "${{ inputs.include_linux }}" == "true" ]]; then
EXPECTED_PLATFORMS+=(
"appimage-x86_64"
"debian-x86_64"
"appimage-aarch64"
"debian-aarch64"
)
fi
if [[ "${{ inputs.include_windows }}" == "true" ]]; then
EXPECTED_PLATFORMS+=("nsis-x86_64")
fi
INVALID_PLATFORMS=()
for platform in "${EXPECTED_PLATFORMS[@]}"; do
if ! echo "$RELEASE" | jq -e --arg platform "$platform" \
'[.assets[]? | select(.publicPlatform == $platform and ((.size // 0) | tonumber) > 0)] | length == 1' > /dev/null; then
INVALID_PLATFORMS+=("$platform")
fi
done
if [[ "${#INVALID_PLATFORMS[@]}" -gt 0 ]]; then
echo "::error::CrabNebula release $EXPECTED_VERSION has missing, duplicate, or empty download assets: ${INVALID_PLATFORMS[*]}"
exit 1
fi
EXPECTED_UPDATE_PLATFORMS=(
"darwin-aarch64"
"darwin-x86_64"
)
if [[ "${{ inputs.include_linux }}" == "true" ]]; then
EXPECTED_UPDATE_PLATFORMS+=(
"linux-x86_64-appimage"
"linux-x86_64-deb"
"linux-aarch64-appimage"
"linux-aarch64-deb"
)
fi
if [[ "${{ inputs.include_windows }}" == "true" ]]; then
EXPECTED_UPDATE_PLATFORMS+=("windows-x86_64-nsis")
fi
INVALID_UPDATE_PLATFORMS=()
for platform in "${EXPECTED_UPDATE_PLATFORMS[@]}"; do
if ! echo "$RELEASE" | jq -e --arg platform "$platform" \
'[.assets[]? | select(.updatePlatform == $platform and ((.signature // "") | length) > 0 and ((.size // 0) | tonumber) > 0)] | length == 1' > /dev/null; then
INVALID_UPDATE_PLATFORMS+=("$platform")
fi
done
if [[ "${#INVALID_UPDATE_PLATFORMS[@]}" -gt 0 ]]; then
echo "::error::CrabNebula release $EXPECTED_VERSION has missing, duplicate, unsigned, or empty updater assets: ${INVALID_UPDATE_PLATFORMS[*]}"
exit 1
fi
- name: Record release provenance
env:
CN_ASSET_ID: ${{ steps.release.outputs.cli-asset-id }}
CN_SHA256: ${{ steps.release.outputs.cli-sha256 }}
CN_VERSION: ${{ steps.release.outputs.cli-version }}
RELEASE: ${{ steps.release.outputs.raw }}
VERSION: ${{ needs.compute-version.outputs.version }}
run: |
mkdir -p release-provenance
printf '%s' "$RELEASE" > "$RUNNER_TEMP/crabnebula-release.json"
node scripts/desktop-release-provenance.mjs create \
--release "$RUNNER_TEMP/crabnebula-release.json" \
--output release-provenance/manifest.json \
--version "$VERSION" \
--candidate-sha "$GITHUB_SHA" \
--run-id "$GITHUB_RUN_ID" \
--cn-version "$CN_VERSION" \
--cn-asset-id "$CN_ASSET_ID" \
--cn-sha256 "$CN_SHA256"
- uses: actions/upload-artifact@v4
with:
name: desktop-release-provenance-${{ needs.compute-version.outputs.version }}-${{ github.sha }}
path: release-provenance/manifest.json
if-no-files-found: error
retention-days: 30