Track close separately from exit, preserve probe outcomes on cleanup errors, and cover inherited pipes and Windows cwd locks. Add cross-platform CI with the official Codex ACP process chain smoke check. Refs #523
920 lines
36 KiB
YAML
920 lines
36 KiB
YAML
name: Desktop build
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_tag:
|
|
description: "Prepare macOS, Windows, and Linux 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
|
|
|
|
# `gh release view --json assets` stays empty after a draft is published.
|
|
# The API URL is addressed by release id and still lists the uploads.
|
|
release_api="$(gh release view "$CURRENT_TAG" --repo "$GITHUB_REPOSITORY" --json apiUrl --jq .apiUrl)"
|
|
mapfile -t current_assets < <(
|
|
gh api "$release_api" --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' \
|
|
--pattern 'EdgeEver-*-linux-x64.AppImage' \
|
|
--pattern 'latest-linux.yml' \
|
|
--pattern 'SHA256SUMS-linux.txt' \
|
|
--dir "$RUNNER_TEMP/native-assets"
|
|
test "$(find "$RUNNER_TEMP/native-assets" -type f | wc -l | tr -d ' ')" = "17"
|
|
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
|
|
|
|
- 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: |
|
|
for attempt in 1 2 3; do
|
|
if bun install --frozen-lockfile; then
|
|
exit 0
|
|
fi
|
|
if [[ "$attempt" = "3" ]]; then
|
|
exit 1
|
|
fi
|
|
sleep "$((15 * attempt))"
|
|
done
|
|
|
|
- name: Build Web renderer
|
|
env:
|
|
EDGE_EVER_DESKTOP_BUILD: "1"
|
|
run: bun run build:web
|
|
|
|
- name: Verify Web performance 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 renderer origin storage migration
|
|
if: matrix.shared_validation
|
|
run: bun run verify:renderer-origin-migration
|
|
|
|
- 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 packaged macOS cross-version startup
|
|
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
|
|
run: |
|
|
previous_directory="$RUNNER_TEMP/edgeever-previous-${{ matrix.arch }}"
|
|
mkdir -p "$previous_directory/archive" "$previous_directory/app"
|
|
gh release download "$PREVIOUS_TAG" \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--pattern 'EdgeEver-*-mac-${{ matrix.arch }}.zip' \
|
|
--dir "$previous_directory/archive"
|
|
previous_archive="$(find "$previous_directory/archive" -maxdepth 1 -type f -name '*.zip' -print -quit)"
|
|
test -n "$previous_archive"
|
|
ditto -x -k "$previous_archive" "$previous_directory/app"
|
|
previous_executable="$previous_directory/app/EdgeEver.app/Contents/MacOS/EdgeEver"
|
|
unpacked_directory="mac-${{ matrix.arch }}"
|
|
if [[ "${{ matrix.arch }}" == "x64" ]]; then
|
|
unpacked_directory="mac"
|
|
fi
|
|
current_executable="release/desktop/$unpacked_directory/EdgeEver.app/Contents/MacOS/EdgeEver"
|
|
bun run verify:desktop-cross-version-startup -- "$previous_executable" "$current_executable"
|
|
|
|
- name: Verify packaged macOS first launch
|
|
env:
|
|
EDGE_EVER_DESKTOP_ARCH: ${{ matrix.arch }}
|
|
run: |
|
|
unpacked_directory="mac-${{ matrix.arch }}"
|
|
if [[ "${{ matrix.arch }}" == "x64" ]]; then
|
|
unpacked_directory="mac"
|
|
fi
|
|
bun run verify:packaged-desktop-startup -- "release/desktop/$unpacked_directory/EdgeEver.app/Contents/MacOS/EdgeEver"
|
|
|
|
- name: Verify packaged macOS private protocol file flows
|
|
env:
|
|
EDGE_EVER_DESKTOP_ARCH: ${{ matrix.arch }}
|
|
run: |
|
|
unpacked_directory="mac-${{ matrix.arch }}"
|
|
if [[ "${{ matrix.arch }}" == "x64" ]]; then
|
|
unpacked_directory="mac"
|
|
fi
|
|
bun run verify:desktop-protocol-e2e -- "release/desktop/$unpacked_directory/EdgeEver.app/Contents/MacOS/EdgeEver"
|
|
|
|
- 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
|
|
|
|
- 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: Test ACP process and workspace cleanup
|
|
run: bun test --cwd apps/desktop src/main/acp-cleanup.test.mjs
|
|
|
|
- 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 packaged Windows cross-version startup
|
|
if: github.event_name == 'workflow_dispatch' && inputs.release_tag != ''
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
|
|
run: |
|
|
$previousDirectory = Join-Path $env:RUNNER_TEMP "edgeever-previous-windows-x64"
|
|
New-Item -ItemType Directory -Force -Path $previousDirectory | Out-Null
|
|
gh release download $env:PREVIOUS_TAG `
|
|
--repo $env:GITHUB_REPOSITORY `
|
|
--pattern 'EdgeEver-*-windows-x64.exe' `
|
|
--dir $previousDirectory
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
$previousInstaller = Get-ChildItem -LiteralPath $previousDirectory -Filter '*.exe' | Select-Object -First 1
|
|
if (-not $previousInstaller) { throw "Previous Windows installer was not downloaded" }
|
|
$installerProcess = Start-Process -FilePath $previousInstaller.FullName -ArgumentList '/S' -Wait -PassThru
|
|
if ($installerProcess.ExitCode -ne 0) {
|
|
throw "Previous Windows installer exited with code $($installerProcess.ExitCode)"
|
|
}
|
|
Get-Process -Name EdgeEver -ErrorAction SilentlyContinue | Stop-Process -Force
|
|
$previousExecutable = Get-ChildItem -LiteralPath (Join-Path $env:LOCALAPPDATA 'Programs') `
|
|
-Filter 'EdgeEver.exe' -File -Recurse | Select-Object -First 1
|
|
if (-not $previousExecutable) { throw "Previous installed EdgeEver executable was not found" }
|
|
bun run verify:desktop-cross-version-startup -- `
|
|
$previousExecutable.FullName `
|
|
(Resolve-Path 'release/desktop/win-unpacked/EdgeEver.exe').Path
|
|
|
|
- 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 packaged Windows private protocol file flows
|
|
run: bun run verify:desktop-protocol-e2e -- 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
|
|
|
|
linux:
|
|
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: Linux x64 AppImage Preview
|
|
# The Rust sidecar is dynamically linked. Pinning the oldest supported
|
|
# Ubuntu runner keeps its glibc requirement at or below 2.35.
|
|
runs-on: ubuntu-22.04
|
|
steps:
|
|
- name: Check out source
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Set up Bun
|
|
uses: oven-sh/setup-bun@v2
|
|
|
|
- name: Set up Rust
|
|
uses: dtolnay/rust-toolchain@stable
|
|
|
|
- name: Install AppImage runtime dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install --yes libfuse2
|
|
|
|
- name: Cache Bun dependencies
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: ~/.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: |
|
|
~/.cache/electron
|
|
~/.cache/electron-builder
|
|
key: ${{ runner.os }}-x64-electron-${{ hashFiles('bun.lock', 'apps/desktop/package.json') }}
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
for attempt in 1 2 3; do
|
|
if bun install --frozen-lockfile; then
|
|
exit 0
|
|
fi
|
|
if [[ "$attempt" = "3" ]]; then
|
|
exit 1
|
|
fi
|
|
sleep "$((15 * attempt))"
|
|
done
|
|
|
|
- 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: Build Linux automatic update predecessor
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PREVIOUS_TAG: ${{ needs.release-plan.outputs.previous_tag }}
|
|
run: |
|
|
if [[ -n "$PREVIOUS_TAG" ]]; then
|
|
git fetch --depth=1 origin "refs/tags/${PREVIOUS_TAG}:refs/tags/${PREVIOUS_TAG}"
|
|
if git show "${PREVIOUS_TAG}:apps/desktop/src/main/index.mjs" | grep -q 'linuxUpdateTestMode'; then
|
|
previous_directory="$RUNNER_TEMP/edgeever-previous-linux-x64"
|
|
mkdir -p "$previous_directory"
|
|
gh release download "$PREVIOUS_TAG" \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--pattern 'EdgeEver-*-linux-x64.AppImage' \
|
|
--dir "$previous_directory"
|
|
previous_app_image="$(find "$previous_directory" -maxdepth 1 -name 'EdgeEver-*-linux-x64.AppImage' -type f -print -quit)"
|
|
test -n "$previous_app_image"
|
|
test "$(find "$previous_directory" -maxdepth 1 -name 'EdgeEver-*-linux-x64.AppImage' -type f | wc -l | tr -d ' ')" = "1"
|
|
mv "$previous_app_image" "$RUNNER_TEMP/EdgeEver-linux-update-source.AppImage"
|
|
chmod +x "$RUNNER_TEMP/EdgeEver-linux-update-source.AppImage"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# The first release that enables Linux updates has no update-capable
|
|
# predecessor in the wild. Build the same packaged updater at an older
|
|
# version so the bootstrap release still exercises a real replacement.
|
|
package_backup="$RUNNER_TEMP/edgeever-desktop-package.json"
|
|
cp apps/desktop/package.json "$package_backup"
|
|
trap 'cp "$package_backup" apps/desktop/package.json' EXIT
|
|
node -e "const fs=require('fs');const path='apps/desktop/package.json';const value=JSON.parse(fs.readFileSync(path,'utf8'));value.version='0.0.1';fs.writeFileSync(path, JSON.stringify(value, null, 2)+'\n')"
|
|
EDGE_EVER_DESKTOP_ARCH=x64 EDGE_EVER_DESKTOP_TARGET=linux \
|
|
bun scripts/run-desktop-builder.mjs --publish never
|
|
mv release/desktop/EdgeEver-0.0.1-linux-x64.AppImage \
|
|
"$RUNNER_TEMP/EdgeEver-linux-update-source.AppImage"
|
|
cp "$package_backup" apps/desktop/package.json
|
|
trap - EXIT
|
|
|
|
- name: Package Linux AppImage
|
|
env:
|
|
EDGE_EVER_DESKTOP_ARCH: x64
|
|
EDGE_EVER_DESKTOP_TARGET: linux
|
|
run: bun scripts/run-desktop-builder.mjs --publish never
|
|
|
|
- name: Verify Linux package contents, architecture, and glibc baseline
|
|
env:
|
|
EDGE_EVER_DESKTOP_ARCH: x64
|
|
EDGE_EVER_VERIFY_TARGET: linux
|
|
run: bun scripts/verify-desktop-package.mjs
|
|
|
|
- name: Run packaged Linux sidecar integration tests
|
|
env:
|
|
EDGE_EVER_SIDECAR_PATH: ${{ github.workspace }}/release/desktop/linux-unpacked/resources/sidecar/edgeever-sidecar
|
|
EDGE_EVER_MIGRATIONS_PATH: ${{ github.workspace }}/release/desktop/linux-unpacked/resources/migrations
|
|
run: bun scripts/test-desktop-sidecar.mjs
|
|
|
|
- name: Verify packaged Linux first launch
|
|
run: |
|
|
executable=""
|
|
for candidate in release/desktop/linux-unpacked/EdgeEver release/desktop/linux-unpacked/edgeever; do
|
|
if [[ -x "$candidate" ]]; then
|
|
executable="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
test -n "$executable"
|
|
xvfb-run -a bun run verify:packaged-desktop-startup -- "$executable"
|
|
|
|
- name: Verify packaged Linux private protocol file flows
|
|
run: |
|
|
executable=""
|
|
for candidate in release/desktop/linux-unpacked/EdgeEver release/desktop/linux-unpacked/edgeever; do
|
|
if [[ -x "$candidate" ]]; then
|
|
executable="$candidate"
|
|
break
|
|
fi
|
|
done
|
|
test -n "$executable"
|
|
xvfb-run -a bun run verify:desktop-protocol-e2e -- "$executable"
|
|
|
|
- name: Create Linux checksum
|
|
run: |
|
|
version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
|
|
cd release/desktop
|
|
sha256sum "EdgeEver-${version}-linux-x64.AppImage" > SHA256SUMS-linux.txt
|
|
sha256sum --check SHA256SUMS-linux.txt
|
|
|
|
- name: Verify Linux automatic update metadata
|
|
run: |
|
|
version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
|
|
node scripts/verify-linux-update-release.mjs release/desktop "$version"
|
|
|
|
- name: Verify real Linux AppImage automatic update
|
|
run: |
|
|
version="$(node -p "JSON.parse(require('fs').readFileSync('apps/desktop/package.json', 'utf8')).version")"
|
|
xvfb-run -a bun scripts/verify-linux-appimage-update.mjs \
|
|
"$RUNNER_TEMP/EdgeEver-linux-update-source.AppImage" \
|
|
"release/desktop/EdgeEver-${version}-linux-x64.AppImage" \
|
|
"$version"
|
|
|
|
- name: Stage Linux Preview assets
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: edgeever-desktop-Linux-x64-staging
|
|
path: |
|
|
release/desktop/EdgeEver-*-linux-x64.AppImage
|
|
release/desktop/latest-linux.yml
|
|
release/desktop/SHA256SUMS-linux.txt
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
|
|
native-build-timings:
|
|
needs: [desktop, windows, linux]
|
|
if: >-
|
|
${{
|
|
always() &&
|
|
github.repository == 'tianma-if/edgeever' &&
|
|
(needs.desktop.result != 'skipped' || needs.windows.result != 'skipped' || needs.linux.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, linux]
|
|
if: >-
|
|
${{
|
|
always() &&
|
|
github.repository == 'tianma-if/edgeever' &&
|
|
needs.desktop.result == 'success' &&
|
|
needs.windows.result == 'success' &&
|
|
needs.linux.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"
|
|
node scripts/verify-linux-update-release.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-Windows-and-Linux-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
|
|
node scripts/download-release-assets.mjs \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--tag "$RELEASE_TAG" \
|
|
--dir release/desktop \
|
|
--pattern 'EdgeEver-*-windows-x64.exe' \
|
|
--pattern 'latest.yml' \
|
|
--pattern 'latest-windows.json' \
|
|
--pattern 'latest-windows.json.sig' \
|
|
--pattern 'SHA256SUMS-windows.txt'
|
|
|
|
- name: Verify signature and installer digests
|
|
run: node scripts/verify-windows-update-release.mjs release/desktop
|
|
|
|
linux-release-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 Linux Preview asset
|
|
runs-on: ubuntu-22.04
|
|
steps:
|
|
- name: Check out source
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Download Linux release assets
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ inputs.release_tag || github.event.release.tag_name }}
|
|
run: |
|
|
mkdir -p release/desktop
|
|
node scripts/download-release-assets.mjs \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--tag "$RELEASE_TAG" \
|
|
--dir release/desktop \
|
|
--pattern 'EdgeEver-*-linux-x64.AppImage' \
|
|
--pattern 'latest-linux.yml' \
|
|
--pattern 'SHA256SUMS-linux.txt'
|
|
|
|
- name: Verify Linux automatic update release
|
|
run: node scripts/verify-linux-update-release.mjs release/desktop
|