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
6 changes: 4 additions & 2 deletions .claude/rules/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ 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. actionlint learns the label from `.github/actionlint.yaml`. A stable tag builds only on a GA Xcode: `setup-xcode` with `require-release` fails on a beta or Release Candidate.
- The Xcode version is written once, in `.github/actions/setup-xcode/action.yml`, which runs `xcode-select --switch` and prints `xcodebuild -version`. A workflow never selects Xcode another way.
- Every Xcode job in the test and release workflows runs on the `xcode-27` runner label, a GitHub preview image. actionlint learns the label from `.github/actionlint.yaml`.
- 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`.
- 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.
- `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
3 changes: 2 additions & 1 deletion .claude/rules/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ UI flows: `.claude/rules/ui-tests.md`. Running tests and reading verdicts: `.cla
- `TestPlans/Unit.xctestplan` is the `TablePro` scheme's default plan. `TestPlans/UI.xctestplan` holds `TableProUITests` alone.
- Unit holds `TableProTests`, the hostless plugin bundles, the Editor and Grammars package targets and the TableProMacCore package targets.
- TableProCore and TableProOracle test targets are in no plan and run under `swift test`. Some depend on the `TableProPluginKit` package, and a plan target that does makes the app and plugins compile against that module instead of the resilient framework. A package joins the Unit plan only when nothing it builds depends on that package; the top of `Packages/TableProMacCore/Package.swift` says so.
- Unit plan settings: language `en`, region `US`, timeouts on with a 60 s default and a 120 s maximum per case, no code coverage, the sandbox variable (Isolation below). UI plan settings are in `.claude/rules/ui-tests.md`.
- Unit plan settings: language `en`, region `US`, timeouts on with a 60 s default and a 120 s maximum per case, no code coverage, the sandbox variable (Isolation below) and `DEVELOPER_DIR` set to `$(DEVELOPER_DIR)`. UI plan settings are in `.claude/rules/ui-tests.md`.
- The host gets no shell environment, so without that entry a `/usr/bin` shim a test runs (`python3`) resolves through the Mac's selected Xcode, not the one running the tests. On `xcode-27` that is 27.0, and the shim asked to install the Command Line Tools instead.
- The Unit plan never retries. A red case is a bug, or a flake to fix.
- The Unit plan runs serially. Its Xcode targets carry `"parallelizable": false`; the package targets carry no key, so their suites run in parallel in-process. A new Xcode target entry stays serial too.
- Why serial: in-process parallelism starts every `TableProTests` suite at once and starves the main actor, so time limits kill cases and then the host. Parallel clones never split the Swift Testing suites of a hosted target: they all run in one clone. The plugin bundles in parallel save about 1% of the Unit step.
Expand Down
58 changes: 35 additions & 23 deletions .github/actions/setup-xcode/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,51 @@ description: Selects the one Xcode every TablePro build and test job uses.

inputs:
require-release:
description: true fails the job unless the selected Xcode is a GA release, as a stable build requires.
description: true fails the job unless the selected Xcode is Apple's released build, as a stable build requires.
required: false
default: "false"

# The only place the version is written. runs-on names the image (xcode-27); this picks the build
# inside it, the one developers install locally. The image has no Xcode 26.
# 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.
runs:
using: composite
steps:
- shell: bash
env:
XCODE: /Applications/Xcode_27.1.app
XCODE_VERSION: "27.1"
# Apple's released 27.1 (developer.apple.com/news/releases). The 27.1 beta, 27A9269, also says GM.
XCODE_RELEASE_BUILD: 27A9275
REQUIRE_RELEASE: ${{ inputs.require-release }}
run: |
set -euo pipefail
sudo xcode-select --switch "$XCODE"
matches=()
installed=""
for app in /Applications/Xcode*.app; do
if [ -L "$app" ] || [ ! -d "$app" ]; then
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
matches+=("$app")
fi
done
if [ "${#matches[@]}" -ne 1 ]; then
echo "::error::Expected one Xcode $XCODE_VERSION in /Applications, found ${#matches[@]}:${installed:- no Xcode}"
exit 1
fi

app="${matches[0]}"
export DEVELOPER_DIR="$app/Contents/Developer"
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
xcodebuild -version
[ "$REQUIRE_RELEASE" = true ] || exit 0

# The image links Xcode_<version>.app to whichever build it carries, a beta or RC included.
app="$(cd "$XCODE" && pwd -P)"
case "$(basename "$app")" in
*_beta* | *_Release_Candidate*)
echo "::error::$XCODE is $app, not a GA Xcode; a stable release builds only on a GA Xcode"
exit 1
;;
esac
license="$app/Contents/Resources/LicenseInfo.plist"
type="$(plutil -extract licenseType raw -o - "$license" 2> /dev/null || true)"
case "$(tr '[:upper:]' '[:lower:]' <<< "$type")" in
*beta*)
echo "::error::$app has the license type '$type', not a GA Xcode; a stable release builds only on a GA Xcode"
exit 1
;;
esac
echo "$app is a GA Xcode"
build="$(plutil -extract ProductBuildVersion raw -o - "$app/Contents/version.plist")"
license="$(plutil -extract licenseType raw -o - "$app/Contents/Resources/LicenseInfo.plist" 2> /dev/null || true)"
echo "$app: build $build, licenseType ${license:-missing}"
[ "$REQUIRE_RELEASE" = true ] || exit 0
if [ "$build" != "$XCODE_RELEASE_BUILD" ] || [ "$license" != GM ]; then
echo "::error::$app is $build with licenseType '$license', not Apple's released Xcode $XCODE_VERSION ($XCODE_RELEASE_BUILD, GM); a stable release builds only on it"
exit 1
fi
echo "$app is Apple's released Xcode $XCODE_VERSION"
2 changes: 1 addition & 1 deletion .github/workflows/ios-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
}
udid="$(find_simulator)"
if [ -z "$udid" ]; then
sudo xcodebuild -downloadPlatform iOS
xcodebuild -downloadPlatform iOS
udid="$(find_simulator)"
fi
if [ -z "$udid" ]; then
Expand Down
4 changes: 4 additions & 0 deletions TestPlans/Unit.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"codeCoverage" : false,
"defaultTestExecutionTimeAllowance" : 60,
"environmentVariableEntries" : [
{
"key" : "DEVELOPER_DIR",
"value" : "$(DEVELOPER_DIR)"
},
{
"key" : "TABLEPRO_UI_TEST_SANDBOX",
"value" : "$(TARGET_TEMP_DIR)\/UnitTestStorage"
Expand Down
Loading