Skip to content

Commit 89075df

Browse files
chenchaoyiclaude
andauthored
release: build both arches on macos-14 via universal2 + Rosetta (#47)
Stops using the macos-13 hosted runner, which has been increasingly starved through 2026 — every recent tag had the Intel job sit queued for hours before user-manual intervention. The new single-runner flow: - Swift helper builds universal2 (`swift build --arch arm64 --arch x86_64`) once per release; same .app ships in both per-arch tarballs. Gated by env var DITING_HELPER_UNIVERSAL=1 so local dev stays native-fast. - PyInstaller-frozen Python builds twice from the same macos-14 arm64 host: arm64 natively, x86_64 under Rosetta 2 via `arch -x86_64` with a separate Rosetta-installed uv that pulls x86_64 pyobjc/ifaddr/zeroconf wheels. - Both tarballs upload from the same job; shasums job is unchanged. End-user surface (release asset names, install.sh fetch path) is unchanged. Local dev surface (helper/build.sh, package_release.sh) is unchanged — universal2 path is opt-in via env var. Caveat: the x86_64 frozen binary is built under emulation on an arm64 host; no real Intel hardware smoke test yet. Noted in CHANGELOG. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8f1e9cf commit 89075df

8 files changed

Lines changed: 245 additions & 84 deletions

File tree

.github/workflows/release.yml

Lines changed: 96 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ name: release
88
# - push of a tag matching `v*` (the release flow)
99
# - manual workflow_dispatch (dry-run; uploads to a draft release
1010
# so we can inspect the artefact before cutting a real tag)
11+
#
12+
# Architecture strategy (since v1.0.8):
13+
# All builds run on macos-14 (arm64). The Swift helper is built
14+
# universal2 (single binary with both arm64 + x86_64 slices) and
15+
# shipped in both per-arch release tarballs. The PyInstaller-frozen
16+
# Python binary IS arch-specific, so we build it twice — once
17+
# natively on the arm64 host, once under Rosetta 2 (`arch -x86_64`)
18+
# for the Intel slice. Avoids the macos-13 runner queue, which
19+
# was starving so badly that the Intel tarball was effectively
20+
# never landing.
1121

1222
on:
1323
push:
@@ -20,29 +30,17 @@ on:
2030
required: false
2131
default: ""
2232

23-
# Block concurrent runs on the same ref so two tag pushes in
24-
# quick succession can't produce a racy double-upload.
2533
concurrency:
2634
group: release-${{ github.ref }}
2735
cancel-in-progress: false
2836

2937
permissions:
30-
# gh release upload needs to write to the release.
3138
contents: write
3239

3340
jobs:
3441
build:
35-
strategy:
36-
fail-fast: false
37-
matrix:
38-
include:
39-
- os: macos-14 # M1 hosted runner → arm64
40-
arch: arm64
41-
- os: macos-13 # x86_64 hosted runner → x86_64
42-
arch: x86_64
43-
44-
runs-on: ${{ matrix.os }}
45-
42+
# Both arches build from this single arm64 runner.
43+
runs-on: macos-14
4644
steps:
4745
- uses: actions/checkout@v4
4846

@@ -58,49 +56,113 @@ jobs:
5856
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
5957
echo "num_version=${VERSION#v}" >> "$GITHUB_OUTPUT"
6058
59+
- name: Ensure Rosetta 2 is installed
60+
# GitHub macos-14 runners normally have Rosetta installed,
61+
# but `softwareupdate --install-rosetta --agree-to-license`
62+
# is the official idempotent way to be sure. The flag is a
63+
# no-op if Rosetta is already there.
64+
run: softwareupdate --install-rosetta --agree-to-license || true
65+
6166
- name: Install uv
6267
uses: astral-sh/setup-uv@v5
6368
with:
6469
enable-cache: true
6570

66-
- name: Set up Python 3.11
71+
# ---- Swift helper: build once, universal2, used by both
72+
# tarballs. The helper bundle is signed ad-hoc which produces
73+
# a stable cdhash across both arches — same cdhash means the
74+
# TCC grants the user accumulates carry across arm64 / x86_64
75+
# installs of the same release.
76+
- name: Build Swift helper (universal2)
77+
env:
78+
DITING_HELPER_UNIVERSAL: "1"
79+
run: ( cd helper && ./build.sh )
80+
81+
# ---- arm64 build (native) -----------------------------------
82+
- name: Set up Python 3.11 (arm64 native)
6783
run: uv python install 3.11
6884

69-
- name: Sync release dependencies
70-
# PyInstaller lives in the [release] group. --all-groups
71-
# also pulls dev tools we don't need; --group release keeps
72-
# the build env lean.
85+
- name: Sync release deps (arm64)
7386
run: uv sync --group release --python 3.11
7487

75-
- name: Build Swift helper
76-
run: ( cd helper && ./build.sh )
77-
78-
- name: Run package_release.sh
79-
env:
80-
# Stage everything under the workspace so the upload step
81-
# can find the tarball + sha256 by glob.
82-
DITING_RELEASE_VERSION: ${{ steps.ver.outputs.num_version }}
88+
- name: Package arm64 tarball
8389
run: |
8490
uv run --python 3.11 \
8591
bash scripts/package_release.sh "${{ steps.ver.outputs.num_version }}"
8692
87-
- name: Upload tarball to release
93+
- name: Stash arm64 tarball + sha256
94+
run: |
95+
mkdir -p stash
96+
mv dist/diting-${{ steps.ver.outputs.num_version }}-darwin-arm64.tar.gz stash/
97+
mv dist/diting-${{ steps.ver.outputs.num_version }}-darwin-arm64.tar.gz.sha256 stash/
98+
99+
# ---- x86_64 build (Rosetta) ---------------------------------
100+
# PyInstaller takes the arch of the currently-running Python.
101+
# uv can install an x86_64 Python build on an arm64 host via
102+
# the --python-platform flag; we then create a separate venv
103+
# for it. All subsequent commands wrapped in `arch -x86_64`
104+
# so uv, python, and any subprocess they invoke run as x86_64
105+
# under Rosetta.
106+
- name: Set up Python 3.11 (x86_64 via Rosetta)
107+
run: |
108+
arch -x86_64 /bin/bash -c '
109+
curl -fsSL https://astral.sh/uv/install.sh | sh
110+
'
111+
# The Rosetta-installed uv lives under
112+
# ~/.local/bin/uv. Use a separate cache dir so it does not
113+
# collide with the arm64 uv's state.
114+
echo "ROSETTA_UV=$HOME/.local/bin/uv" >> $GITHUB_ENV
115+
116+
- name: Install x86_64 Python via Rosetta uv
117+
run: |
118+
arch -x86_64 "$ROSETTA_UV" python install 3.11
119+
120+
- name: Wipe arm64 venv before x86_64 sync
121+
# `uv sync` reuses .venv/ if present. The arm64 build above
122+
# populated it with arm64 wheels (pyobjc et al.); we need a
123+
# clean slate for the x86_64 install so the right wheels get
124+
# pulled.
125+
run: rm -rf .venv
126+
127+
- name: Sync release deps (x86_64)
128+
run: |
129+
arch -x86_64 "$ROSETTA_UV" sync --group release --python 3.11
130+
131+
- name: Clean previous build artefacts
132+
# PyInstaller and the staging dir from the arm64 run.
133+
run: rm -rf build dist
134+
135+
- name: Package x86_64 tarball
136+
run: |
137+
arch -x86_64 "$ROSETTA_UV" run --python 3.11 \
138+
bash scripts/package_release.sh "${{ steps.ver.outputs.num_version }}"
139+
140+
- name: Restore arm64 tarball alongside x86_64
141+
run: |
142+
mv stash/* dist/
143+
144+
# ---- Upload both tarballs -----------------------------------
145+
- name: Upload tarballs to release
88146
uses: softprops/action-gh-release@v2
89147
if: github.event_name == 'push'
90148
with:
91149
tag_name: ${{ steps.ver.outputs.version }}
92150
files: |
93-
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-${{ matrix.arch }}.tar.gz
94-
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-${{ matrix.arch }}.tar.gz.sha256
151+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-arm64.tar.gz
152+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-arm64.tar.gz.sha256
153+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-x86_64.tar.gz
154+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-x86_64.tar.gz.sha256
95155
96-
- name: Upload tarball as workflow artifact (dispatch dry-runs)
156+
- name: Upload tarballs as workflow artifacts (dispatch dry-runs)
97157
if: github.event_name == 'workflow_dispatch'
98158
uses: actions/upload-artifact@v4
99159
with:
100-
name: diting-${{ steps.ver.outputs.num_version }}-darwin-${{ matrix.arch }}
160+
name: diting-${{ steps.ver.outputs.num_version }}-darwin
101161
path: |
102-
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-${{ matrix.arch }}.tar.gz
103-
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-${{ matrix.arch }}.tar.gz.sha256
162+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-arm64.tar.gz
163+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-arm64.tar.gz.sha256
164+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-x86_64.tar.gz
165+
dist/diting-${{ steps.ver.outputs.num_version }}-darwin-x86_64.tar.gz.sha256
104166
retention-days: 14
105167

106168
shasums:

CHANGELOG.md

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ the project follows [Semantic Versioning](https://semver.org/) where
99
practical. The leading `v0.x` line is allowed to break minor
1010
behaviours between releases.
1111

12-
## [Unreleased]
12+
## [1.0.8] — 2026-05-14
1313

14-
The install-time UX got loud — three windows in two languages on
15-
top of each other, with one of the prompts showing the raw
16-
bundle filename. This release re-shapes the helper bundle around
17-
a single ordered flow and gives diting its own icon end-to-end.
14+
Two parallel pushes land together: the helper bundle gets its own
15+
icon and a single ordered install flow (no more chaotic three-window
16+
stack on first launch), and the release workflow finally ships
17+
x86_64 tarballs reliably again.
1818

19-
### Added
19+
### Added (helper bundle / install UX)
2020
- **Helper bundle ships the diting logo as its AppIcon.**
2121
Pre-rendered PNGs at every macOS iconset size live under
2222
`helper/Resources/AppIcon.iconset/` (regenerated by
@@ -34,7 +34,7 @@ a single ordered flow and gives diting its own icon end-to-end.
3434
and Bluetooth, so the watchdog can fire alerts without
3535
triggering a surprise prompt months later.
3636

37-
### Changed
37+
### Changed (helper bundle / install UX)
3838
- **Install-time permission flow is now a single ordered wizard.**
3939
`HelperAppDelegate` requests Location → Bluetooth → Notifications
4040
in sequence; each step fires only after the previous step's auth
@@ -65,12 +65,48 @@ a single ordered flow and gives diting its own icon end-to-end.
6565
Eliminates the AppleScript scroll icon that used to appear in
6666
every alert.
6767

68-
### Migration note
68+
### Changed (release flow)
69+
Intel (x86_64) releases land on every tag again. Prior tags
70+
(v1.0.0 – v1.0.7) shipped the x86_64 tarball only when the
71+
`macos-13` runner happened to be available — which during 2026 has
72+
been "almost never", since GitHub Actions has been winding down the
73+
Intel hosted-runner pool. v1.0.7's release sat with the Intel job
74+
queued for hours before the user manually unblocked arm64 by
75+
uploading just the arm64 SHASUMS entry.
76+
77+
- **Release workflow now builds both arches from a single `macos-14`
78+
(arm64) runner**:
79+
- Swift helper is built once as **universal2** (`swift build
80+
--arch arm64 --arch x86_64`) — a single .app whose binary
81+
contains both arch slices. Gated by env var
82+
`DITING_HELPER_UNIVERSAL=1` so local dev defaults to a
83+
fast native-only build.
84+
- PyInstaller's frozen Python is arch-specific (takes the running
85+
Python's arch), so we build it twice: once natively on the
86+
arm64 host, once under **Rosetta 2** via `arch -x86_64`. The
87+
Rosetta path uses a separate `uv` install (also under Rosetta)
88+
that pulls x86_64 pyobjc / ifaddr / zeroconf wheels.
89+
- Both tarballs are uploaded to the release; the `shasums` job
90+
aggregates as before. The release surface is unchanged from
91+
v1.0.7's perspective — install.sh keeps fetching
92+
`diting-<v>-darwin-<arch>.tar.gz` per `uname -m`.
93+
- **Local dev unchanged**: `helper/build.sh` defaults to native
94+
build. Set `DITING_HELPER_UNIVERSAL=1` to test the universal2 path.
95+
96+
### Migration note (breaking)
6997
The new `CFBundleIconFile` plist entry changes the bundle's cdhash.
7098
Users upgrading from v1.0.x will re-grant Location + Bluetooth once
7199
on the next install (and grant Notifications for the first time).
72100
Future installs at the same path retain grants.
73101

102+
### Caveats
103+
- Rosetta-emulated PyInstaller is ~2× slower than native on the same
104+
host — the release workflow's total wall-clock grows by ~3-5 min
105+
per release. Acceptable.
106+
- The x86_64 frozen binary is built on an arm64 host under
107+
emulation; the result has not been smoke-tested on a real Intel
108+
Mac in this change. Volunteer testers welcome.
109+
74110
## [1.0.7] — 2026-05-13
75111

76112
The macOS 26 install hang that survived v1.0.3 → v1.0.5 had a deeper

docs/RELEASE.md

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,26 @@ How to cut a new version that the curl-bash one-liner installs.
77
88
## What gets released
99

10-
Each tagged version produces three release assets on GitHub:
10+
Each tagged version produces these release assets on GitHub:
1111

1212
| Asset | Built by | Consumed by |
1313
|---|---|---|
14-
| `diting-X.Y.Z-darwin-arm64.tar.gz` | matrix job `macos-14` | install.sh on Apple Silicon |
15-
| `diting-X.Y.Z-darwin-x86_64.tar.gz` | matrix job `macos-13` | install.sh on Intel |
16-
| `SHASUMS256.txt` | `shasums` job after both matrix builds | install.sh SHA verification |
14+
| `diting-X.Y.Z-darwin-arm64.tar.gz` | `macos-14` runner (native) | install.sh on Apple Silicon |
15+
| `diting-X.Y.Z-darwin-x86_64.tar.gz` | `macos-14` runner under Rosetta 2 | install.sh on Intel |
16+
| `<arch>.tar.gz.sha256` (×2) | `package_release.sh` (one per arch) | sidecar, aggregated by `shasums` job |
17+
| `SHASUMS256.txt` | `shasums` job after both arch builds | install.sh SHA verification |
18+
19+
Both arches are built from the **same** `macos-14` (arm64) runner.
20+
The Swift helper is built once as a universal2 binary (one Mach-O
21+
with both arm64 and x86_64 slices) and shipped inside both
22+
tarballs. The PyInstaller-frozen Python is arch-specific and gets
23+
built twice: natively for arm64, under Rosetta 2 (`arch -x86_64`)
24+
for x86_64. See the `build` job in `.github/workflows/release.yml`
25+
for the full sequence.
1726

1827
Each tarball contains a PyInstaller-frozen `diting` binary plus a
19-
copy of the Swift helper bundle (`diting-tianer.app`). Layout:
28+
copy of the universal2 Swift helper bundle (`diting-tianer.app`).
29+
Layout:
2030

2131
```
2232
diting-X.Y.Z/
@@ -41,11 +51,12 @@ diting-X.Y.Z/
4151
```
4252
The tag push triggers `.github/workflows/release.yml`.
4353

44-
3. **Watch the workflow.** Both matrix jobs (`macos-14` and
45-
`macos-13`) build the helper, freeze the Python binary, run
46-
`scripts/package_release.sh`, and upload the tarball + `.sha256`
47-
sidecar to the release. The `shasums` job runs after both, pulls
48-
the tarballs back down via `gh release download`, computes
54+
3. **Watch the workflow.** The single `build` job on `macos-14`
55+
produces both arches: builds the universal2 helper once,
56+
PyInstaller-freezes Python natively for arm64, then again under
57+
Rosetta 2 for x86_64. Both tarballs + their `.sha256` sidecars
58+
are uploaded to the release. The `shasums` job runs after,
59+
pulls the tarballs back down via `gh release download`, computes
4960
`SHASUMS256.txt`, and uploads it as a sibling asset.
5061

5162
4. **Smoke-test the install.** On a clean macOS account (or a Mac
@@ -95,10 +106,13 @@ bash scripts/package_release.sh 0.10.0-rc1
95106
notarization. Until then the warning fires only once per
96107
cdhash — re-installing the same release doesn't re-trigger it.
97108

98-
- **`macos-13` runner deprecation.** GitHub will eventually retire
99-
the Intel hosted runners. When that happens we either move
100-
x86_64 builds to whatever the latest x86_64 runner is, or drop
101-
x86_64 (Apple Silicon adoption is >70% of new Macs as of 2026).
109+
- **`macos-13` runner queue.** As of v1.0.8 (2026-05-13) the
110+
workflow no longer uses `macos-13` at all. Both arches build on a
111+
single `macos-14` (arm64) runner; the helper goes universal2 and
112+
the frozen Python is built twice (native + Rosetta). If GitHub
113+
ever retires the arm64 hosted runners too, point the workflow at
114+
the current `macos-N` arm64 tier; the Rosetta 2 step keeps
115+
working as long as Apple ships Rosetta.
102116

103117
- **Upgrade users get re-prompted for Location and Bluetooth after a
104118
release.** Expected when the helper bundle's cdhash changes —

0 commit comments

Comments
 (0)