Move the Photos bridge types into the desktop contract, expose rakazoDesktop.media.photos from the preload, connect the sync runner in the main process through an authenticated oRPC client, package and sign the helper, and build and test it on macOS CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138YqDj3ttfayRwuMKKrqVj
361 lines
15 KiB
YAML
361 lines
15 KiB
YAML
name: release-desktop
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
# Stable releases must validate and publish serially. Per-tag concurrency would let an older
|
|
# release validate before a newer one publishes, then overwrite GitHub's "latest" pointer.
|
|
group: release-desktop-stable
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
validate:
|
|
name: Validate stable release
|
|
runs-on: [self-hosted, evoo-ci, Linux]
|
|
outputs:
|
|
tag: ${{ steps.release.outputs.tag }}
|
|
version: ${{ steps.release.outputs.version }}
|
|
sha: ${{ steps.release.outputs.sha }}
|
|
platforms: ${{ steps.platforms.outputs.include }}
|
|
windows: ${{ steps.platforms.outputs.windows }}
|
|
steps:
|
|
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
- name: Validate tag, version, ancestry, and monotonicity
|
|
id: release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_TAG: ${{ github.ref_name }}
|
|
RELEASE_REF_TYPE: ${{ github.ref_type }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "$RELEASE_REF_TYPE" != "tag" ]]; then
|
|
echo "Desktop releases must run from a tag, including manual dispatches." >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! "$RELEASE_TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
|
echo "Desktop releases use the stable vMAJOR.MINOR.PATCH channel." >&2
|
|
exit 1
|
|
fi
|
|
|
|
version="$(node -p "require('./apps/desktop/package.json').version")"
|
|
if [[ "$RELEASE_TAG" != "v$version" ]]; then
|
|
echo "Tag $RELEASE_TAG does not match desktop version $version." >&2
|
|
exit 1
|
|
fi
|
|
|
|
tag_sha="$(git rev-parse "${RELEASE_TAG}^{commit}")"
|
|
if [[ "$tag_sha" != "$GITHUB_SHA" ]]; then
|
|
echo "The workflow revision does not match the tagged commit." >&2
|
|
exit 1
|
|
fi
|
|
git fetch --no-tags origin main
|
|
if ! git merge-base --is-ancestor "$tag_sha" origin/main; then
|
|
echo "Desktop releases must point to a commit already on main." >&2
|
|
exit 1
|
|
fi
|
|
if gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
|
|
echo "A release already exists for $RELEASE_TAG; refusing to replace it." >&2
|
|
exit 1
|
|
fi
|
|
|
|
latest_tag="$(
|
|
gh api --paginate "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --jq '.[] | select(.draft == false and .prerelease == false) | .tag_name' |
|
|
sed -nE '/^v[0-9]+\.[0-9]+\.[0-9]+$/p' |
|
|
sort -V |
|
|
tail -n 1
|
|
)"
|
|
if [[ -n "$latest_tag" ]]; then
|
|
highest="$(printf '%s\n%s\n' "$latest_tag" "$RELEASE_TAG" | sort -V | tail -n 1)"
|
|
if [[ "$highest" != "$RELEASE_TAG" || "$latest_tag" == "$RELEASE_TAG" ]]; then
|
|
echo "$RELEASE_TAG must be newer than published release $latest_tag." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
echo "sha=$tag_sha" >> "$GITHUB_OUTPUT"
|
|
- name: Choose release platforms
|
|
id: platforms
|
|
env:
|
|
# Windows builds only when an Authenticode certificate is configured; macOS and Linux always build.
|
|
WINDOWS_SIGNING: ${{ secrets.DESKTOP_WIN_CSC_LINK != '' }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
include='[{"runner":["self-hosted","evoo-ci","macOS"],"artifact":"macos"},{"runner":["self-hosted","evoo-ci","Linux"],"artifact":"linux"}]'
|
|
if [[ "$WINDOWS_SIGNING" == "true" ]]; then
|
|
# No self-hosted Windows runner; keep GitHub-hosted only when signing credentials exist.
|
|
include="$(jq -c '. + [{"runner":"windows-2022","artifact":"windows"}]' <<<"$include")"
|
|
fi
|
|
echo "include=$include" >> "$GITHUB_OUTPUT"
|
|
echo "windows=$WINDOWS_SIGNING" >> "$GITHUB_OUTPUT"
|
|
|
|
build:
|
|
name: Build signed ${{ matrix.artifact }} artifacts
|
|
needs: validate
|
|
runs-on: ${{ matrix.runner }}
|
|
timeout-minutes: 60
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include: ${{ fromJSON(needs.validate.outputs.platforms) }}
|
|
steps:
|
|
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
with:
|
|
ref: ${{ needs.validate.outputs.sha }}
|
|
persist-credentials: false
|
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
with:
|
|
node-version: 22
|
|
- run: pnpm install --frozen-lockfile
|
|
- name: Build renderer and Electron main process
|
|
run: pnpm --filter @rakazo/web build && pnpm --filter @rakazo/desktop build
|
|
env:
|
|
RAKAZO_ALLOW_DEV_SECRETS: "1"
|
|
|
|
- name: Require macOS signing and notarization credentials
|
|
if: runner.os == 'macOS'
|
|
shell: bash
|
|
env:
|
|
CSC_LINK: ${{ secrets.DESKTOP_MAC_CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_MAC_CSC_KEY_PASSWORD }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
|
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
|
|
run: |
|
|
set -euo pipefail
|
|
test -n "$CSC_LINK"
|
|
test -n "$CSC_KEY_PASSWORD"
|
|
test -n "$APPLE_API_KEY_ID"
|
|
test -n "$APPLE_API_ISSUER"
|
|
test -n "$APPLE_API_KEY_P8"
|
|
umask 077
|
|
printf '%s\n' "$APPLE_API_KEY_P8" > "$APPLE_API_KEY"
|
|
chmod 600 "$APPLE_API_KEY"
|
|
- name: Build and test the universal Photos helper
|
|
if: runner.os == 'macOS'
|
|
working-directory: apps/desktop/native/photos-helper
|
|
run: |
|
|
set -euo pipefail
|
|
swift test
|
|
swift build -c release --arch arm64 --arch x86_64
|
|
lipo -verify_arch .build/apple/Products/Release/photos-helper arm64 x86_64
|
|
- name: Package signed and notarized universal macOS app
|
|
if: runner.os == 'macOS'
|
|
env:
|
|
CSC_LINK: ${{ secrets.DESKTOP_MAC_CSC_LINK }}
|
|
CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_MAC_CSC_KEY_PASSWORD }}
|
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
|
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
|
|
run: >-
|
|
pnpm --filter @rakazo/desktop exec electron-builder
|
|
--mac --universal --publish never -c.forceCodeSigning=true
|
|
- name: Remove the App Store Connect API key from the runner
|
|
if: always() && runner.os == 'macOS'
|
|
shell: bash
|
|
env:
|
|
APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8
|
|
run: rm -f "$APPLE_API_KEY"
|
|
- name: Verify macOS signature, notarization ticket, and update feed
|
|
if: runner.os == 'macOS'
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
app="apps/desktop/out/mac-universal/Rakazo.app"
|
|
codesign --verify --deep --strict --verbose=2 "$app"
|
|
xcrun stapler validate "$app"
|
|
spctl --assess --type execute --verbose=2 "$app"
|
|
grep -Fqx "provider: github" "$app/Contents/Resources/app-update.yml"
|
|
grep -Fqx "owner: elie222" "$app/Contents/Resources/app-update.yml"
|
|
grep -Fqx "repo: rakazo" "$app/Contents/Resources/app-update.yml"
|
|
helper="$app/Contents/Resources/bin/photos-helper"
|
|
codesign --verify --strict --verbose=2 "$helper"
|
|
codesign -d --entitlements - --xml "$helper" | grep -Fq "com.apple.security.personal-information.photos-library"
|
|
/usr/libexec/PlistBuddy -c 'Print :NSPhotoLibraryUsageDescription' "$app/Contents/Info.plist"
|
|
|
|
- name: Require Windows signing credentials
|
|
if: runner.os == 'Windows'
|
|
shell: bash
|
|
env:
|
|
WIN_CSC_LINK: ${{ secrets.DESKTOP_WIN_CSC_LINK }}
|
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_WIN_CSC_KEY_PASSWORD }}
|
|
run: |
|
|
set -euo pipefail
|
|
test -n "$WIN_CSC_LINK"
|
|
test -n "$WIN_CSC_KEY_PASSWORD"
|
|
- name: Package signed x64 Windows app
|
|
if: runner.os == 'Windows'
|
|
env:
|
|
WIN_CSC_LINK: ${{ secrets.DESKTOP_WIN_CSC_LINK }}
|
|
WIN_CSC_KEY_PASSWORD: ${{ secrets.DESKTOP_WIN_CSC_KEY_PASSWORD }}
|
|
run: >-
|
|
pnpm --filter @rakazo/desktop exec electron-builder
|
|
--win --x64 --publish never -c.forceCodeSigning=true
|
|
- name: Verify Windows Authenticode signature and publisher-bound update feed
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = "Stop"
|
|
$executables = @(
|
|
Get-ChildItem "apps/desktop/out/*.exe"
|
|
Get-ChildItem "apps/desktop/out/win-unpacked/*.exe"
|
|
)
|
|
if ($executables.Count -lt 2) { throw "Windows installer or application was not created." }
|
|
foreach ($executable in $executables) {
|
|
$signature = Get-AuthenticodeSignature $executable.FullName
|
|
if ($signature.Status -ne "Valid") {
|
|
throw "$($executable.Name) signature is $($signature.Status)."
|
|
}
|
|
}
|
|
$config = "apps/desktop/out/win-unpacked/resources/app-update.yml"
|
|
if (-not (Select-String -Path $config -Pattern '^publisherName:' -Quiet)) {
|
|
throw "Windows update config is not bound to the signing publisher."
|
|
}
|
|
$feed = Get-Content -Raw $config
|
|
foreach ($expected in @("provider: github", "owner: elie222", "repo: rakazo")) {
|
|
if ($feed -notmatch "(?m)^$([regex]::Escape($expected))\r?$") {
|
|
throw "Windows update config missing '$expected'."
|
|
}
|
|
}
|
|
|
|
- name: Package x64 Linux AppImage
|
|
if: runner.os == 'Linux'
|
|
run: >-
|
|
pnpm --filter @rakazo/desktop exec electron-builder
|
|
--linux --x64 --publish never
|
|
- name: Verify Linux update feed is pinned to the official GitHub channel
|
|
if: runner.os == 'Linux'
|
|
shell: bash
|
|
env:
|
|
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
config="apps/desktop/out/linux-unpacked/resources/app-update.yml"
|
|
if [[ ! -f "$config" ]]; then
|
|
config="$(find apps/desktop/out -name app-update.yml -print -quit)"
|
|
fi
|
|
test -n "$config"
|
|
test -f "$config"
|
|
grep -Fqx "provider: github" "$config"
|
|
grep -Fqx "owner: elie222" "$config"
|
|
grep -Fqx "repo: rakazo" "$config"
|
|
test -f apps/desktop/out/latest-linux.yml
|
|
grep -Fqx "version: $RELEASE_VERSION" apps/desktop/out/latest-linux.yml
|
|
|
|
- name: Retain installers and updater metadata
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: desktop-${{ matrix.artifact }}-${{ needs.validate.outputs.sha }}
|
|
path: |
|
|
apps/desktop/out/*.AppImage
|
|
apps/desktop/out/*.blockmap
|
|
apps/desktop/out/*.dmg
|
|
apps/desktop/out/*.exe
|
|
apps/desktop/out/latest*.yml
|
|
apps/desktop/out/*.zip
|
|
if-no-files-found: error
|
|
compression-level: 0
|
|
retention-days: 7
|
|
|
|
publish:
|
|
name: Attest and publish complete release
|
|
needs: [validate, build]
|
|
runs-on: [self-hosted, evoo-ci, Linux]
|
|
timeout-minutes: 15
|
|
permissions:
|
|
actions: read
|
|
attestations: write
|
|
contents: write
|
|
id-token: write
|
|
steps:
|
|
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
ref: ${{ needs.validate.outputs.sha }}
|
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
pattern: desktop-*-${{ needs.validate.outputs.sha }}
|
|
path: release-artifacts
|
|
merge-multiple: true
|
|
- name: Verify the complete stable update feed
|
|
env:
|
|
RELEASE_SHA: ${{ needs.validate.outputs.sha }}
|
|
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
|
|
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
|
|
WINDOWS_BUILT: ${{ needs.validate.outputs.windows }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --force origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
|
|
if [[ "$(git rev-parse "${RELEASE_TAG}^{commit}")" != "$RELEASE_SHA" ]]; then
|
|
echo "The release tag moved after the build started." >&2
|
|
exit 1
|
|
fi
|
|
|
|
test -f release-artifacts/latest-mac.yml
|
|
test -f release-artifacts/latest-linux.yml
|
|
compgen -G 'release-artifacts/*.dmg' >/dev/null
|
|
compgen -G 'release-artifacts/*.zip' >/dev/null
|
|
compgen -G 'release-artifacts/*.AppImage' >/dev/null
|
|
feeds=(
|
|
release-artifacts/latest-mac.yml
|
|
release-artifacts/latest-linux.yml
|
|
)
|
|
if [[ "$WINDOWS_BUILT" == "true" ]]; then
|
|
test -f release-artifacts/latest.yml
|
|
compgen -G 'release-artifacts/*.exe' >/dev/null
|
|
feeds+=(release-artifacts/latest.yml)
|
|
fi
|
|
for feed in "${feeds[@]}"; do
|
|
grep -Fqx "version: $RELEASE_VERSION" "$feed"
|
|
done
|
|
|
|
(
|
|
cd release-artifacts
|
|
find . -maxdepth 1 -type f ! -name SHA256SUMS -print0 |
|
|
sort -z |
|
|
xargs -0 sha256sum > SHA256SUMS
|
|
)
|
|
- name: Attest release provenance
|
|
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
|
with:
|
|
subject-path: release-artifacts/*
|
|
- name: Create draft and upload every platform
|
|
id: create_release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
|
|
run: >-
|
|
gh release create "$RELEASE_TAG" release-artifacts/*
|
|
--draft --generate-notes --title "Rakazo $RELEASE_TAG" --verify-tag
|
|
- name: Publish the completed release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
|
|
run: gh release edit "$RELEASE_TAG" --draft=false --latest
|
|
- name: Remove an incomplete draft
|
|
if: failure() && steps.create_release.outcome != 'skipped'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_TAG: ${{ needs.validate.outputs.tag }}
|
|
shell: bash
|
|
run: |
|
|
if [[ "$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" --jq .draft 2>/dev/null)" == "true" ]]; then
|
|
gh release delete "$RELEASE_TAG" --yes
|
|
fi
|