Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
## Runners and toolchain

- Every Xcode job in the test and release workflows runs on the `xcode-27` runner label, a GitHub preview image, except Release Build. actionlint learns the label from `.github/actionlint.yaml`.
- Release Build runs on `mac-mini-m4pro`, a repo-level self-hosted runner with the labels `self-hosted`, `macOS`, `ARM64` and `tablepro-release`: the maintainer's M4 Pro (12 cores, 24 GB, Xcode 27.1 at `/Applications/Xcode.app`), run by launchd as the maintainer's user.
- Release Build runs on `mac-mini-m4pro`, a repo-level self-hosted runner with the labels `self-hosted`, `macOS`, `ARM64` and `tablepro-release`: the maintainer's M4 Pro (12 cores, 24 GB, the Xcode build `setup-xcode` pins, at `/Applications/Xcode.app`), run by launchd as the maintainer's user.
- No other workflow names `tablepro-release`. Release Build never takes a PR, and fork PR workflows need approval for every outside contributor.
- The Xcode version and Apple's released build of it are written once, in `.github/actions/setup-xcode/action.yml`. It takes the one non-symlink `/Applications/Xcode*.app` whose `version.plist` has that version, fails on none or several, exports `DEVELOPER_DIR` to the job's later steps and prints `xcodebuild -version`.
- The Xcode version, the build every job uses and Apple's released build are written once, in `.github/actions/setup-xcode/action.yml`. It takes the one non-symlink `/Applications/Xcode*.app` whose `version.plist` has that version and build, fails on none or several, exports `DEVELOPER_DIR` to the job's later steps and prints `xcodebuild -version`. The version alone is no pin: the 27.1 beta, 27A9269, is also 27.1.
- A workflow never selects Xcode another way, and never runs an Xcode tool under `sudo`, which drops `DEVELOPER_DIR`. A test host gets it from the Unit plan (`.claude/rules/tests.md`, Plans).
- A stable tag builds only on Apple's released Xcode: `setup-xcode` with `require-release` needs the pinned build and `licenseType` `GM` in the bundle's `LicenseInfo.plist`. The folder name is no signal: the image names the released 27A9275 `Xcode_27.1_Release_Candidate.app`.
- XcodeGen comes from `.github/actions/setup-xcodegen`, pinned by version and SHA-256.
- A stable tag builds only on Apple's released Xcode: `setup-xcode` with `require-release` needs Apple's released build and `licenseType` `GM` in the bundle's `LicenseInfo.plist`. The folder name is no signal: the image names the released 27A9275 `Xcode_27.1_Release_Candidate.app`.
- XcodeGen comes from `.github/actions/setup-xcodegen`, pinned by version and SHA-256. It installs into the runner's tool cache without `sudo` and goes first on `PATH`, ahead of a Homebrew `xcodegen`.
- `scripts/release/feed.py` runs Sparkle's `generate_appcast` and `sign_update` from `SPARKLE_BIN`, which `.github/actions/setup-sparkle` sets. The action reads the version and revision from the app's `Package.resolved` (`project.yml` pins Sparkle with `exactVersion`) and checks the archive against the checksum in Sparkle's `Package.swift` at that revision.
- The compilation cache is off on CI (`COMPILATION_CACHE_ENABLE_CACHING=NO`); `Configs/Base.xcconfig` turns it on locally.

Expand Down Expand Up @@ -67,8 +67,8 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/

- Nightly at 19:00 UTC on main, push to `release/**`, and dispatch; never a PR. Never cancelled. Not a required check.
- `Changes (Release)` outputs `run`. A dispatch always builds. A nightly runs the `release` detector against the head of the last nightly that ended green, read with `gh api`, so a quiet day builds nothing. A `release/**` push diffs `github.event.before`, and builds when the push created the branch.
- `Release Build` runs on the self-hosted Mac; a dispatch with `runner: github` runs it on `xcode-27`. It has a 150-minute limit and VERSION 0.0.0-dev and BUILD 9999999, so build.sh passes the version settings a tag passes.
- While the Mac is offline a run waits for it, and GitHub cancels a job still queued after 24 hours. The tag gate refuses a commit with any red run, so re-run that run once the Mac is back; a `runner: github` dispatch on the same commit does not replace it.
- `Release Build` runs on the self-hosted Mac, or on `xcode-27` when a dispatch picks `runner: github` or, for a push or nightly, the repository variable `RELEASE_BUILD_RUNNER` is `github`. It has a 150-minute limit and VERSION 0.0.0-dev and BUILD 9999999, so build.sh passes the version settings a tag passes.
- While the Mac is offline a run waits for it, and GitHub cancels a job still queued after 24 hours. The tag gate refuses a commit with any red run, and a `runner: github` dispatch on the same commit does not replace it: `RELEASING.md`, When a run fails.
- Setup as macOS Tests up to the SPM restore, which it never saves; then Go, `build-dameng.sh both` and `build-hana.sh both`.
- `scripts/release/build.sh app --unsigned` archives the `TablePro` scheme; `build.sh plugins --unsigned` builds `RegistryPlugins` and copies the publish set, the targets `scripts/release/registry.py targets` reads from `.github/plugin-registry.json`, to `build/release/plugins`. Both build universal. They run `lipo -verify_arch` once per arch (arm64, x86_64), since Xcode 27's `lipo` takes one arch per call, then `verify-plugin-loads.sh`.
- `scripts/release/package.sh app --unsigned` writes the release assets: the DMG, the `.tar.xz` (extracted and compared with the app) and the dSYMs zip; `package.sh plugins` writes one zip per plugin and the plugin dSYMs zip.
Expand Down Expand Up @@ -144,7 +144,7 @@ The required check names are in `AGENTS.md` (CI). Reading a red check: `.claude/
- `release.yml` restores that cache and never saves it, since a save in tag scope is unreadable from other refs.
- `release-build.yml` restores it and leaves the save to macOS Tests.
- There is no other Apple cache: no libs or DerivedData cache, and none in the iOS or plugin workflows. The Linux jobs cache Cargo builds through `Swatinem/rust-cache` and the Flatpak build.
- CI fits the GitHub Free and Pro plans: no paid larger runner. At most 5 hosted macOS jobs run at once, and main runs queue behind PR runs for those slots. Release Build takes none of them unless a dispatch picks `runner: github`.
- CI fits the GitHub Free and Pro plans: no paid larger runner. At most 5 hosted macOS jobs run at once, and main runs queue behind PR runs for those slots. Release Build takes none of them unless a dispatch or `RELEASE_BUILD_RUNNER` picks `github`.

## Writing workflow and lint code

Expand Down
15 changes: 9 additions & 6 deletions .github/actions/setup-xcode/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ inputs:
required: false
default: "false"

# The only place the version is written. The bundle name differs by machine
# (Xcode_27.1_Release_Candidate.app on xcode-27, Xcode.app on a Mac), so it is found by its version.
# The only place the version and build are written. The bundle name differs by machine
# (Xcode_27.1_Release_Candidate.app on xcode-27, Xcode.app on a Mac), so it is found by its version.plist.
runs:
using: composite
steps:
- shell: bash
env:
XCODE_VERSION: "27.1"
# Apple's released 27.1 (developer.apple.com/news/releases). The 27.1 beta, 27A9269, also says GM.
# The 27.1 beta, 27A9269, has the same version, so every job also checks the build.
XCODE_BUILD: 27A9275
# Apple's released 27.1 (developer.apple.com/news/releases). The 27.1 beta also says GM.
XCODE_RELEASE_BUILD: 27A9275
REQUIRE_RELEASE: ${{ inputs.require-release }}
run: |
Expand All @@ -27,13 +29,14 @@ runs:
continue
fi
version="$(plutil -extract CFBundleShortVersionString raw -o - "$app/Contents/version.plist" 2> /dev/null || true)"
installed="$installed $app ($version)"
if [ "$version" = "$XCODE_VERSION" ]; then
build="$(plutil -extract ProductBuildVersion raw -o - "$app/Contents/version.plist" 2> /dev/null || true)"
installed="$installed $app ($version $build)"
if [ "$version" = "$XCODE_VERSION" ] && [ "$build" = "$XCODE_BUILD" ]; then
matches+=("$app")
fi
done
if [ "${#matches[@]}" -ne 1 ]; then
echo "::error::Expected one Xcode $XCODE_VERSION in /Applications, found ${#matches[@]}:${installed:- no Xcode}"
echo "::error::Expected one Xcode $XCODE_VERSION ($XCODE_BUILD) in /Applications, found ${#matches[@]}:${installed:- no Xcode}"
exit 1
fi

Expand Down
24 changes: 15 additions & 9 deletions .github/actions/setup-xcodegen/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@ runs:
# generated pbxproj output differs between XcodeGen versions. install.sh copies
# both bin/xcodegen and share/xcodegen/SettingPresets; without the presets,
# generation silently drops build settings and still exits 0.
# It goes to the runner's tool cache, not /usr/local: the self-hosted Mac has no
# passwordless sudo, and the xcodegen already on its PATH is Homebrew's, unverified.
env:
XCODEGEN_VERSION: "2.46.0"
XCODEGEN_SHA256: "4d9e34b62172d645eed6457cac13fc222569974098ef4ee9c3368bedf0196806"
run: |
set -euo pipefail
if command -v xcodegen > /dev/null && [ "$(xcodegen --version | awk '{print $2}')" = "$XCODEGEN_VERSION" ]; then
echo "XcodeGen $XCODEGEN_VERSION already installed"
exit 0
prefix="${RUNNER_TOOL_CACHE:?}/xcodegen/$XCODEGEN_VERSION"
if [ "$("$prefix/bin/xcodegen" --version 2> /dev/null | awk '{print $2}')" = "$XCODEGEN_VERSION" ]; then
echo "XcodeGen $XCODEGEN_VERSION already in $prefix"
else
curl -fsSLo "$RUNNER_TEMP/xcodegen.zip" \
"https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip"
echo "${XCODEGEN_SHA256} $RUNNER_TEMP/xcodegen.zip" | shasum -a 256 -c -
rm -rf "$RUNNER_TEMP/xcodegen-dist" "$prefix"
unzip -q "$RUNNER_TEMP/xcodegen.zip" -d "$RUNNER_TEMP/xcodegen-dist"
mkdir -p "$prefix"
"$RUNNER_TEMP/xcodegen-dist/xcodegen/install.sh" "$prefix"
fi
curl -fsSLo "$RUNNER_TEMP/xcodegen.zip" \
"https://github.com/yonaskolb/XcodeGen/releases/download/${XCODEGEN_VERSION}/xcodegen.zip"
echo "${XCODEGEN_SHA256} $RUNNER_TEMP/xcodegen.zip" | shasum -a 256 -c -
unzip -q "$RUNNER_TEMP/xcodegen.zip" -d "$RUNNER_TEMP/xcodegen-dist"
sudo "$RUNNER_TEMP/xcodegen-dist/xcodegen/install.sh" /usr/local
xcodegen --version
echo "$prefix/bin" >> "$GITHUB_PATH"
"$prefix/bin/xcodegen" --version
3 changes: 2 additions & 1 deletion .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ jobs:
name: Release Build
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.run != 'false' }}
runs-on: ${{ inputs.runner == 'github' && 'xcode-27' || fromJSON('["self-hosted", "macOS", "ARM64", "tablepro-release"]') }}
# A push or nightly has no input, so it reads the RELEASE_BUILD_RUNNER repository variable; unset is the Mac.
runs-on: ${{ (inputs.runner || vars.RELEASE_BUILD_RUNNER) == 'github' && 'xcode-27' || fromJSON('["self-hosted", "macOS", "ARM64", "tablepro-release"]') }}
timeout-minutes: 150
# The tag's version settings run here first; 9999999 is the largest build at MAJOR 0 (RELEASING.md, Versions).
env:
Expand Down
1 change: 1 addition & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ A `v*` tag runs `release.yml` and nothing else. A run is never cancelled, and ev
- After it publishes: **Re-run failed jobs**. Every later step skips what is done: a published release, a feed item with the same version and build, or a manifest release with the same build and sha256.
- A stable that never published: the next release PR runs `git revert <its release commit>` before `notes.py assemble`, and the leftover draft goes (`gh release delete vX.Y.Z`).
- No GA Xcode on the runner image fails a stable in `app`. The fix is a PR pointing `.github/actions/setup-xcode` at a GA Xcode on an image that has one. Betas still ship.
- Release Build runs on the maintainer's Mac, and a tag's `verify` waits for it. While the Mac is offline, `gh variable set RELEASE_BUILD_RUNNER --body github` sends pushes and nightlies to `xcode-27`; a run already waiting needs a cancel and a re-run. `gh variable delete RELEASE_BUILD_RUNNER` once the Mac is back.

## Withdrawing a release

Expand Down
Loading