feat(desktop): wire macOS bundle updates and guarded feed publication

Route packaged macOS bundles and Light through the updater strategy.
Use electron-updater 6.8.9 and wait for native signature acceptance before
backend teardown. Keep checkout and Store ownership separate.

Share Darwin feed paths between packaging, runtime and publication.
Validate both native feeds, verify streamed artifact hashes, prevent
same-tag artifact replacement, and conditionally update the channel
pointer. Protect live feed references during canary retention.

Use one notarization owner. Require publishing credentials and validate
the stapled app. Keep Windows, Linux and Termux jobs unchanged.

Verified with updater/feed unit and transport tests, release-helper tests,
desktop typechecks, the desktop JS build, and workflow lint. No E2E,
native macOS install, release dispatch or public publication was run.
This commit is contained in:
ethernet
2026-09-06 21:27:58 -04:00
parent ff91ff0d5a
commit da236308fd
29 changed files with 1034 additions and 555 deletions

View File

@@ -76,6 +76,8 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: python3 scripts/release.py --prune-canaries --publish --remote origin
- name: Install locked feed tooling
run: npm ci --workspaces=false --ignore-scripts --no-audit --no-fund
- name: Prune R2 canary objects
env:
CLOUDFLARE_R2_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_R2_ACCOUNT_ID }}

View File

@@ -492,7 +492,7 @@ jobs:
needs: validate
runs-on: ${{ matrix.target.runner }}
environment: release-signing
timeout-minutes: 900
timeout-minutes: 180
strategy:
fail-fast: false
matrix:
@@ -513,14 +513,12 @@ jobs:
CLOUDFLARE_R2_BUCKET: ${{ vars.CLOUDFLARE_R2_BUCKET }}
CLOUDFLARE_R2_PUBLIC_URL: ${{ vars.CLOUDFLARE_R2_PUBLIC_URL }}
steps:
- name: Disable Spotlight indexing and XProtect
- name: Disable Spotlight indexing for DMG staging
# Spotlight indexes the freshly-mounted dmg staging image past
# hdiutil's detach retries (per-VM, not a cross-job race).
shell: bash
run: |
sudo mdutil -a -i off || true
sudo pkill -9 XProtect >/dev/null || true
while pgrep XProtect; do sleep 3; done
# Check out the SHA the validate job admitted — never the tag ref,
# which a force-push can move between jobs. This is the privileged
@@ -529,6 +527,7 @@ jobs:
with:
ref: ${{ needs.validate.outputs.sha }}
fetch-tags: true
fetch-depth: 0
- name: Resolve toolchain pins from pm/lock.json
id: pins
@@ -557,7 +556,7 @@ jobs:
console.log(`builder=${eb}`)
' >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v7.0.0 # immutable release. safe to pin.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ steps.pins.outputs.node }}
cache: npm
@@ -693,21 +692,12 @@ jobs:
uv tool install cmake==3.31.6
echo "$(uv tool dir --bin)" >> "$GITHUB_PATH"
- name: Derive the feed channel from the tag
id: channel
shell: bash
env:
TAG: ${{ inputs.tag }}
run: |
case "$TAG" in
*-canary.*) echo "channel=canary" >> "$GITHUB_OUTPUT" ;;
*) echo "channel=stable" >> "$GITHUB_OUTPUT" ;;
esac
- name: Build and package
shell: bash
timeout-minutes: 900
timeout-minutes: 160
env:
GITHUB_SHA: ${{ needs.validate.outputs.sha }}
GITHUB_REF_NAME: ${{ inputs.tag }}
PYTHONUTF8: '1'
# electron-osx-sign*/electron-notarize* keep the sign+notarize
# phase visible: without them NOTHING logs between "signing
@@ -747,11 +737,13 @@ jobs:
fi
for app in "${apps[@]}"; do
codesign --verify --strict --verbose=2 "$app"
xcrun stapler validate "$app"
spctl -a -vv -t exec "$app"
echo "signed + notarized: $app"
done
- name: Rename the feed yml per arch
if: inputs.upload_release == true
# electron-builder writes the channel feed yml (stable-mac.yml /
# canary-mac.yml) with the SAME name on both legs; prefix the arch
# so the publish job's merge-multiple download keeps both and
@@ -763,24 +755,33 @@ jobs:
shopt -s nullglob
MATRIX_LABEL="${{ matrix.target.label }}"
arch="${MATRIX_LABEL##*-}"
channel="${{ steps.channel.outputs.channel }}"
for f in apps/desktop/release/*-mac.yml; do
mv "$f" "apps/desktop/release/${arch}-${channel}-mac.yml"
echo "renamed: $(basename "$f") -> ${arch}-${channel}-mac.yml"
done
test -n "$(shopt -s nullglob; echo apps/desktop/release/*-mac.yml)"
channel="${{ needs.validate.outputs.channel }}"
f="apps/desktop/release/${channel}-mac.yml"
test -s "$f"
mv "$f" "apps/desktop/release/${arch}-${channel}-mac.yml"
- name: Upload artifacts
- name: Upload feed metadata
if: inputs.upload_release == true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
# The per-arch feed ymls for publish-darwin-updater to merge. The
# dmg/zip/blockmap binaries go straight to R2 from this leg.
name: hermes-bundled-${{ matrix.target.label }}-${{ inputs.tag }}
path: |
apps/desktop/release/*-mac.yml
retention-days: 30
if-no-files-found: error
- name: Retain non-publishing build artifacts
if: inputs.upload_release != true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: hermes-bundled-${{ matrix.target.label }}-${{ inputs.tag }}
path: |
apps/desktop/release/*.dmg
apps/desktop/release/*.zip
apps/desktop/release/*.blockmap
retention-days: 30
if-no-files-found: error
- name: Stage to Cloudflare R2
if: inputs.upload_release == true
shell: bash
@@ -1131,6 +1132,9 @@ jobs:
# Privileged job: pin to the SHA validate admitted, not the tag.
ref: ${{ needs.validate.outputs.sha }}
- name: Install locked feed tooling
run: npm ci --workspaces=false --ignore-scripts --no-audit --no-fund
- name: Download both darwin legs' feed ymls
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
@@ -1146,7 +1150,7 @@ jobs:
run: |
shopt -s nullglob
ymls=(staged/*-mac.yml)
if [ ${#ymls[@]} -lt 2 ]; then
if [ ${#ymls[@]} -ne 2 ]; then
echo "::error::expected the arm64 AND x64 feed ymls in the staged artifacts, found ${#ymls[@]}: ${ymls[*]}"
exit 1
fi