Files
edgeever/.github/workflows/desktop-build.yml
T

601 lines
23 KiB
YAML

name: Desktop build
on:
workflow_dispatch:
inputs:
release_tag:
description: "Prepare macOS and Windows assets on this existing Draft Release (optional)"
required: false
type: string
release:
types: [published]
permissions:
actions: read
contents: write
jobs:
release-plan:
if: >-
${{
github.repository == 'tianma-if/edgeever' &&
(
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
)
}}
name: Plan desktop release asset
runs-on: ubuntu-latest
outputs:
asset_ready: ${{ steps.plan.outputs.asset_ready }}
previous_tag: ${{ steps.plan.outputs.previous_tag }}
rebuild: ${{ steps.plan.outputs.rebuild }}
steps:
- name: Check out source
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Validate Draft Release target
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
expected_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
test "$RELEASE_TAG" = "v${expected_version}"
test "$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft --jq '.isDraft')" = "true"
target_commitish="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json targetCommitish --jq '.targetCommitish')"
test "$(git rev-parse "${target_commitish}^{commit}")" = "$(git rev-parse "${GITHUB_SHA}^{commit}")"
- name: Compare with previous formal release
id: plan
env:
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
GH_TOKEN: ${{ github.token }}
HEAD_REF: ${{ github.event_name == 'release' && github.event.release.tag_name || github.sha }}
run: |
mapfile -t release_tags < <(
gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \
--jq '.[] | select(.draft == false and .prerelease == false) | .tag_name'
)
previous_tag=""
for release_tag in "${release_tags[@]}"; do
if [[ "$release_tag" != "$CURRENT_TAG" ]]; then
previous_tag="$release_tag"
break
fi
done
test -n "$previous_tag"
git fetch origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}"
echo "previous_tag=$previous_tag" >> "$GITHUB_OUTPUT"
plan_output="$(node scripts/plan-native-release.mjs desktop "$previous_tag" "$HEAD_REF")"
printf '%s\n' "$plan_output" >> "$GITHUB_OUTPUT"
rebuild="$(printf '%s\n' "$plan_output" | sed -n 's/^rebuild=//p')"
expected_version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
if [[ "$rebuild" = "true" ]]; then
root_version="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
test "$expected_version" = "$root_version"
fi
mapfile -t current_assets < <(
gh release view "$CURRENT_TAG" \
--repo "$GITHUB_REPOSITORY" \
--json assets \
--jq '.assets[].name'
)
asset_ready="$(
printf '%s\n' "${current_assets[@]}" |
node scripts/check-native-release-assets.mjs desktop "$rebuild" "$CURRENT_TAG" "$expected_version"
)"
echo "asset_ready=$asset_ready" >> "$GITHUB_OUTPUT"
if [[ "$GITHUB_EVENT_NAME" = "release" && "$asset_ready" != "true" ]]; then
echo "::error::Published Release assets are incomplete or incompatible. Restore the Release to Draft and prepare assets with workflow_dispatch."
exit 1
fi
reuse-release-desktop:
needs: release-plan
if: >-
${{
github.repository == 'tianma-if/edgeever' &&
github.event_name == 'workflow_dispatch' &&
inputs.release_tag != '' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'false' &&
needs.release-plan.outputs.rebuild == 'false'
}}
name: Reuse desktop release assets
runs-on: ubuntu-latest
steps:
- name: Copy latest compatible desktop assets without renaming
env:
CURRENT_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
GH_TOKEN: ${{ github.token }}
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
run: |
mkdir -p "$RUNNER_TEMP/native-assets"
gh release download "$PREVIOUS_TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern 'EdgeEver-*-mac-arm64.dmg' \
--pattern 'EdgeEver-*-mac-arm64.dmg.blockmap' \
--pattern 'EdgeEver-*-mac-arm64.zip' \
--pattern 'EdgeEver-*-mac-arm64.zip.blockmap' \
--pattern 'EdgeEver-*-mac-x64.dmg' \
--pattern 'EdgeEver-*-mac-x64.dmg.blockmap' \
--pattern 'EdgeEver-*-mac-x64.zip' \
--pattern 'EdgeEver-*-mac-x64.zip.blockmap' \
--pattern 'latest-mac.yml' \
--pattern 'EdgeEver-*-windows-x64.exe' \
--pattern 'latest.yml' \
--pattern 'latest-windows.json' \
--pattern 'latest-windows.json.sig' \
--pattern 'SHA256SUMS-windows.txt' \
--dir "$RUNNER_TEMP/native-assets"
test "$(find "$RUNNER_TEMP/native-assets" -type f | wc -l | tr -d ' ')" = "14"
gh release upload "$CURRENT_TAG" "$RUNNER_TEMP/native-assets/"* \
--repo "$GITHUB_REPOSITORY" \
--clobber
desktop:
needs: release-plan
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
(
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
(
github.event_name == 'workflow_dispatch' &&
inputs.release_tag != '' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'false' &&
needs.release-plan.outputs.rebuild == 'true'
)
)
}}
name: macOS ${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: arm64
runner: macos-15
shared_validation: true
- arch: x64
runner: macos-15-intel
shared_validation: false
steps:
- name: Check out source
uses: actions/checkout@v5
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
# Restoring this large cache is consistently slower than a cold install on
# the Intel runner. The arm64 job owns the shared validation and keeps the
# cache because its restore path is substantially faster.
- name: Cache Bun dependencies for shared validation
if: matrix.shared_validation
uses: actions/cache@v5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ matrix.arch }}-bun-${{ hashFiles('bun.lock') }}
- name: Cache Rust dependencies and sidecar builds
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
crates/desktop-sidecar/target
key: ${{ runner.os }}-${{ matrix.arch }}-rust-${{ hashFiles('crates/desktop-sidecar/Cargo.lock', 'crates/desktop-sidecar/**/*.rs') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-rust-
- name: Cache Electron downloads
uses: actions/cache@v5
with:
path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: ${{ runner.os }}-${{ matrix.arch }}-electron-${{ hashFiles('bun.lock', 'apps/desktop/package.json') }}
- name: Import macOS signing identity
env:
MAC_CERTIFICATE_DER_BASE64: ${{ secrets.EDGEEVER_MAC_CERTIFICATE_DER_BASE64 }}
MAC_PRIVATE_KEY_BASE64: ${{ secrets.EDGEEVER_MAC_PRIVATE_KEY_BASE64 }}
run: |
if [[ -z "$MAC_CERTIFICATE_DER_BASE64" || -z "$MAC_PRIVATE_KEY_BASE64" ]]; then
echo "::error::EDGEEVER_MAC_CERTIFICATE_DER_BASE64 and EDGEEVER_MAC_PRIVATE_KEY_BASE64 are required"
exit 1
fi
keychain_path="$RUNNER_TEMP/edgeever-build.keychain-db"
keychain_password="$(openssl rand -hex 32)"
certificate_path="$RUNNER_TEMP/edgeever-mac-signing.cer"
private_key_path="$RUNNER_TEMP/edgeever-mac-signing.pem"
traditional_key_path="$RUNNER_TEMP/edgeever-mac-signing-rsa.pem"
printf '%s' "$MAC_CERTIFICATE_DER_BASE64" | base64 -D > "$certificate_path"
printf '%s' "$MAC_PRIVATE_KEY_BASE64" | base64 -D > "$private_key_path"
openssl rsa -in "$private_key_path" -out "$traditional_key_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" \
-k "$keychain_path" \
-f x509 \
-t cert \
-T /usr/bin/codesign
security import "$traditional_key_path" \
-k "$keychain_path" \
-f openssl \
-t priv \
-P '' \
-T /usr/bin/codesign
security set-key-partition-list \
-S apple-tool:,apple: \
-s \
-k "$keychain_password" \
"$keychain_path"
security list-keychains -d user -s "$keychain_path" "$HOME/Library/Keychains/login.keychain-db"
security find-identity -v -p codesigning "$keychain_path"
echo "CSC_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV"
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build Web renderer
env:
EDGE_EVER_DESKTOP_BUILD: "1"
run: bun run build:web
- name: Verify Web precache budget
if: matrix.shared_validation
run: bun run verify:web-performance
- name: Run project type checks
if: matrix.shared_validation
run: |
bun run typecheck
bun run typecheck:mobile
- name: Build debug sidecar for integration tests
if: matrix.shared_validation
run: cargo build --manifest-path crates/desktop-sidecar/Cargo.toml
- name: Run desktop regression tests
if: matrix.shared_validation
run: bun run test:desktop
- name: Verify packaged renderer startup
if: matrix.shared_validation
run: bun run verify:desktop-renderer
- name: Validate Rust sidecar
if: matrix.shared_validation
run: |
cargo fmt --manifest-path crates/desktop-sidecar/Cargo.toml -- --check
cargo clippy --manifest-path crates/desktop-sidecar/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path crates/desktop-sidecar/Cargo.toml
- name: Build architecture-specific Rust sidecar
run: cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
- name: Prepare Apple notarization API key
env:
APPLE_API_KEY_BASE64: ${{ secrets.EDGEEVER_APPLE_API_KEY_BASE64 }}
APPLE_API_KEY_ID: ${{ secrets.EDGEEVER_APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.EDGEEVER_APPLE_API_ISSUER }}
run: |
if [[ -z "$APPLE_API_KEY_BASE64" || -z "$APPLE_API_KEY_ID" || -z "$APPLE_API_ISSUER" ]]; then
echo "::error::Apple notarization API key secrets are required"
exit 1
fi
apple_api_key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY_ID}.p8"
printf '%s' "$APPLE_API_KEY_BASE64" | base64 -D > "$apple_api_key_path"
chmod 600 "$apple_api_key_path"
echo "APPLE_API_KEY=$apple_api_key_path" >> "$GITHUB_ENV"
echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV"
- name: Package desktop installer
env:
EDGE_EVER_DESKTOP_ARCH: ${{ matrix.arch }}
run: bun scripts/run-desktop-builder.mjs --publish never
- name: Verify desktop installer
env:
EDGE_EVER_DESKTOP_ARCH: ${{ matrix.arch }}
EDGE_EVER_VERIFY_TARGET: darwin
run: bun scripts/verify-desktop-package.mjs
- name: Stage architecture-specific desktop assets
uses: actions/upload-artifact@v6
with:
name: edgeever-desktop-macOS-${{ matrix.arch }}-staging
path: |
release/desktop/EdgeEver-*-mac-${{ matrix.arch }}.dmg
release/desktop/EdgeEver-*-mac-${{ matrix.arch }}.dmg.blockmap
release/desktop/EdgeEver-*-mac-${{ matrix.arch }}.zip
release/desktop/EdgeEver-*-mac-${{ matrix.arch }}.zip.blockmap
if-no-files-found: error
retention-days: 14
windows:
needs: release-plan
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
(
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
(
github.event_name == 'workflow_dispatch' &&
inputs.release_tag != '' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'false' &&
needs.release-plan.outputs.rebuild == 'true'
)
)
}}
name: Windows x64 unsigned Preview
runs-on: windows-latest
steps:
- name: Check out source
uses: actions/checkout@v5
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Bun dependencies
uses: actions/cache@v5
with:
path: ~\AppData\Local\bun\install\cache
key: ${{ runner.os }}-x64-bun-${{ hashFiles('bun.lock') }}
- name: Cache Rust dependencies and sidecar builds
uses: actions/cache@v5
with:
path: |
~\.cargo\registry
~\.cargo\git
crates\desktop-sidecar\target
key: ${{ runner.os }}-x64-rust-${{ hashFiles('crates/desktop-sidecar/Cargo.lock', 'crates/desktop-sidecar/**/*.rs') }}
restore-keys: |
${{ runner.os }}-x64-rust-
- name: Cache Electron downloads
uses: actions/cache@v5
with:
path: |
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
key: ${{ runner.os }}-x64-electron-${{ hashFiles('bun.lock', 'apps/desktop/package.json') }}
- name: Install dependencies
shell: pwsh
run: |
for ($attempt = 1; $attempt -le 3; $attempt++) {
bun install --frozen-lockfile
$installExitCode = $LASTEXITCODE
if ($installExitCode -eq 0) { exit 0 }
if ($attempt -eq 3) { exit $installExitCode }
Write-Warning "Dependency installation failed on attempt $attempt; retrying."
Start-Sleep -Seconds (15 * $attempt)
}
- name: Build Web renderer
env:
EDGE_EVER_DESKTOP_BUILD: "1"
run: bun run build:web
- name: Build x64 Rust sidecar
run: cargo build --manifest-path crates/desktop-sidecar/Cargo.toml --release
- name: Package unsigned Windows installer
env:
EDGE_EVER_DESKTOP_ARCH: x64
EDGE_EVER_DESKTOP_TARGET: win
run: bun scripts/run-desktop-builder.mjs --publish never
- name: Verify Windows package contents and architecture
env:
EDGE_EVER_DESKTOP_ARCH: x64
EDGE_EVER_VERIFY_TARGET: win32
run: bun scripts/verify-desktop-package.mjs
- name: Run packaged Windows sidecar integration tests
shell: pwsh
run: |
$env:EDGE_EVER_SIDECAR_PATH = (Resolve-Path "release/desktop/win-unpacked/resources/sidecar/edgeever-sidecar.exe").Path
$env:EDGE_EVER_MIGRATIONS_PATH = (Resolve-Path "release/desktop/win-unpacked/resources/migrations").Path
bun scripts/test-desktop-sidecar.mjs
- name: Verify packaged Windows first launch
run: bun run verify:packaged-desktop-startup -- release/desktop/win-unpacked/EdgeEver.exe
- name: Verify that Preview executables are unsigned
shell: pwsh
run: |
$version = (Get-Content apps/desktop/package.json | ConvertFrom-Json).version
$paths = @(
"release/desktop/EdgeEver-$version-windows-x64.exe",
"release/desktop/win-unpacked/EdgeEver.exe",
"release/desktop/win-unpacked/resources/sidecar/edgeever-sidecar.exe"
)
foreach ($path in $paths) {
$signature = Get-AuthenticodeSignature -LiteralPath $path
if ($signature.Status -ne "NotSigned") {
throw "Unsigned Preview asset unexpectedly has an Authenticode signature: $path ($($signature.Status))"
}
}
- name: Create Windows update metadata
shell: pwsh
run: |
$version = (Get-Content apps/desktop/package.json | ConvertFrom-Json).version
node scripts/create-windows-update-metadata.mjs release/desktop $version
- name: Stage Windows Preview assets
uses: actions/upload-artifact@v6
with:
name: edgeever-desktop-Windows-x64-staging
path: |
release/desktop/EdgeEver-*-windows-x64.exe
release/desktop/latest.yml
release/desktop/latest-windows.json
release/desktop/SHA256SUMS-windows.txt
if-no-files-found: error
retention-days: 14
native-build-timings:
needs: [desktop, windows]
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
(needs.desktop.result != 'skipped' || needs.windows.result != 'skipped')
}}
name: Report desktop build timings
runs-on: ubuntu-latest
steps:
- name: Check out timing reporter
uses: actions/checkout@v5
- name: Collect native build timings
env:
GH_TOKEN: ${{ github.token }}
run: |
gh api \
"repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?filter=latest&per_page=100" \
> "$RUNNER_TEMP/actions-jobs.json"
node scripts/report-native-build-timings.mjs \
--input "$RUNNER_TEMP/actions-jobs.json" \
--platform desktop \
--json "$RUNNER_TEMP/native-build-timings.json" \
--markdown "$RUNNER_TEMP/native-build-timings.md"
cat "$RUNNER_TEMP/native-build-timings.md" >> "$GITHUB_STEP_SUMMARY"
- name: Upload machine-readable timing report
uses: actions/upload-artifact@v6
with:
name: edgeever-desktop-build-timings-attempt-${{ github.run_attempt }}
path: ${{ runner.temp }}/native-build-timings.json
if-no-files-found: error
retention-days: 90
desktop-assets:
needs: [release-plan, desktop, windows]
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
needs.desktop.result == 'success' &&
needs.windows.result == 'success' &&
(
(github.event_name == 'workflow_dispatch' && inputs.release_tag == '') ||
(
github.event_name == 'workflow_dispatch' &&
inputs.release_tag != '' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'false' &&
needs.release-plan.outputs.rebuild == 'true'
)
)
}}
name: Finalize desktop release assets
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v5
- name: Download platform-specific assets
uses: actions/download-artifact@v7
with:
pattern: edgeever-desktop-*-staging
path: release/desktop
merge-multiple: true
- name: Create and audit combined update metadata
run: |
version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
node scripts/create-mac-update-metadata.mjs release/desktop "$version"
asset_ready="$(
find release/desktop -maxdepth 1 -type f -exec basename {} \; |
node scripts/check-native-release-assets.mjs desktop true "v${version}" "$version" allow-missing-windows-signature
)"
test "$asset_ready" = "true"
- name: Upload verified desktop assets to Release
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
gh release upload "$RELEASE_TAG" release/desktop/* \
--repo "$GITHUB_REPOSITORY" \
--clobber
- name: Upload combined desktop installers and metadata
uses: actions/upload-artifact@v6
with:
name: edgeever-desktop-macOS-and-Windows-x64
path: release/desktop/*
if-no-files-found: error
retention-days: 14
windows-update-audit:
needs: release-plan
if: >-
${{
always() &&
github.repository == 'tianma-if/edgeever' &&
needs.release-plan.result == 'success' &&
needs.release-plan.outputs.asset_ready == 'true' &&
(
github.event_name == 'release' ||
(github.event_name == 'workflow_dispatch' && inputs.release_tag != '')
)
}}
name: Audit signed Windows update
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v5
- name: Download Windows release assets
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
run: |
mkdir -p release/desktop
gh release download "$RELEASE_TAG" \
--repo "$GITHUB_REPOSITORY" \
--pattern 'EdgeEver-*-windows-x64.exe' \
--pattern 'latest.yml' \
--pattern 'latest-windows.json' \
--pattern 'latest-windows.json.sig' \
--pattern 'SHA256SUMS-windows.txt' \
--dir release/desktop
- name: Verify signature and installer digests
run: node scripts/verify-windows-update-release.mjs release/desktop