Skip to content

Commit bbef956

Browse files
authored
ci: one place for the Xcode version and one for the changed-path detector (#2359)
Claude-Session: https://claude.ai/code/session_013MEaba8K1HQcyDNeq5wEFk
1 parent a955a5f commit bbef956

6 files changed

Lines changed: 134 additions & 84 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Select Xcode
2+
description: Selects the Xcode version every TablePro build and test job uses.
3+
4+
# The version lives here and nowhere else. It used to be written out in four workflows, eight
5+
# times counting the pinned action SHA beside it, so bumping Xcode meant finding every copy and
6+
# a missed one silently built against a different toolchain.
7+
8+
inputs:
9+
xcode-version:
10+
description: Override the pinned version. Leave unset; it exists for trying a toolchain on one job.
11+
required: false
12+
default: "26.4.1"
13+
14+
runs:
15+
using: composite
16+
steps:
17+
- uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
18+
with:
19+
xcode-version: ${{ inputs.xcode-version }}
20+
21+
# Proves the pin was actually applied before a twenty minute build starts. build.yml carried
22+
# this as a separate step in both of its jobs.
23+
- shell: bash
24+
run: |
25+
echo "Active Xcode: $(xcode-select -p)"
26+
xcodebuild -version

.github/workflows/build-plugin.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,7 @@ jobs:
121121
run: git lfs pull
122122

123123
- name: Select Xcode
124-
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
125-
with:
126-
xcode-version: "26.4.1"
124+
uses: ./.github/actions/setup-xcode
127125

128126
- name: Setup XcodeGen
129127
uses: ./.github/actions/setup-xcodegen

.github/workflows/build.yml

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@ jobs:
6767
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
6868

6969
- name: Select Xcode
70-
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
71-
with:
72-
xcode-version: '26.4.1'
70+
uses: ./.github/actions/setup-xcode
7371

7472
- name: Download static libraries
7573
env:
@@ -104,14 +102,6 @@ jobs:
104102
- name: Prepare libraries
105103
run: scripts/ci/prepare-libs.sh ${{ matrix.arch }}
106104

107-
# The only thing that proves the pinned toolchain was actually selected before a twenty
108-
# minute build starts.
109-
- name: Verify Xcode
110-
run: |
111-
echo "Active Xcode:"
112-
xcode-select -p
113-
xcodebuild -version
114-
115105
- name: Create Secrets.xcconfig
116106
env:
117107
ANALYTICS_HMAC_SECRET: ${{ secrets.ANALYTICS_HMAC_SECRET }}
@@ -188,9 +178,7 @@ jobs:
188178
fetch-depth: 0
189179

190180
- name: Select Xcode
191-
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0
192-
with:
193-
xcode-version: '26.4.1'
181+
uses: ./.github/actions/setup-xcode
194182

195183
- name: Download build artifacts
196184
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1

.github/workflows/ios-tests.yml

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,15 @@ jobs:
4747
env:
4848
EVENT_NAME: ${{ github.event_name }}
4949
BASE_SHA: ${{ github.event.pull_request.base.sha }}
50+
# Shared with macos-tests.yml; see scripts/ci/detect-changed-paths.sh.
5051
run: |
5152
set -euo pipefail
52-
53-
if [ "$EVENT_NAME" != "pull_request" ]; then
54-
echo "run=true" >> "$GITHUB_OUTPUT"
55-
exit 0
56-
fi
57-
58-
# Raw NUL-separated records, read from a file. See the note in macos-tests.yml for what
59-
# core.quotePath and a pipeline each break here.
60-
git -c core.quotePath=false diff --no-renames --name-only -z \
61-
"$BASE_SHA" HEAD > "$RUNNER_TEMP/changed-files"
62-
63-
if LC_ALL=C grep -zqE '^(TableProMobile/|Configs/|Packages/TableProCore/|Libs/|\.github/workflows/ios-tests\.yml$)' "$RUNNER_TEMP/changed-files"; then
64-
echo "run=true" >> "$GITHUB_OUTPUT"
65-
else
66-
echo "run=false" >> "$GITHUB_OUTPUT"
67-
fi
53+
RUN=$(scripts/ci/detect-changed-paths.sh \
54+
--event "$EVENT_NAME" \
55+
--base "$BASE_SHA" \
56+
'TableProMobile/' 'Configs/' 'Packages/TableProCore/' 'Libs/' 'scripts/' \
57+
'\.github/actions/' '\.github/workflows/ios-tests\.yml$')
58+
echo "run=$RUN" >> "$GITHUB_OUTPUT"
6859
6960
test:
7061
name: Run iOS Tests

.github/workflows/macos-tests.yml

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -73,60 +73,24 @@ jobs:
7373
EVENT_NAME: ${{ github.event_name }}
7474
BASE_SHA: ${{ github.event.pull_request.base.sha }}
7575
REF: ${{ github.ref }}
76+
# The reasoning lives in scripts/ci/detect-changed-paths.sh. It was inline here and in
77+
# ios-tests.yml, with a comment asking a human to keep the two copies in step and nothing
78+
# enforcing it.
7679
run: |
7780
set -euo pipefail
81+
RUN=$(scripts/ci/detect-changed-paths.sh \
82+
--event "$EVENT_NAME" \
83+
--base "$BASE_SHA" \
84+
--ref "$REF" \
85+
--skip-release-commit \
86+
'TablePro/' 'Plugins/' 'Packages/' 'LocalPackages/' \
87+
'TableProTests/' 'TableProUITests/' 'Native/' 'Configs/' 'Libs/' 'scripts/' \
88+
'\.github/actions/' '\.github/macos-(ui-)?test-quarantine\.txt$' \
89+
'TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$' \
90+
'TableProMobile/project\.yml$' 'project\.yml$' \
91+
'\.github/workflows/macos-tests\.yml$')
92+
echo "run=$RUN" >> "$GITHUB_OUTPUT"
7893
79-
# A release pushes the version commit to main and then tags that same commit, so the
80-
# push and the tag both reach this workflow: once directly, once through build.yml's
81-
# workflow_call. That tests one SHA twice on two macos-26 runners, and because the
82-
# account runs five macOS jobs at a time, the duplicate is what leaves the release's
83-
# own suite queued behind it. The tag run is the one that gates the release, so the
84-
# push to main stands down. github.ref belongs to the caller, so build.yml's
85-
# workflow_call reads refs/tags/v* here and can never match this guard, which is what
86-
# keeps "a release that skipped its tests" impossible.
87-
#
88-
# The subject goes into a variable instead of a pipe into grep. grep -q exits on the
89-
# first match, and the SIGPIPE that then kills git log would surface through pipefail
90-
# as a skipped guard (the same trap scripts/check-freetds-fedauth.sh:69 documents).
91-
SUBJECT="$(git log -1 --format=%s HEAD)"
92-
if [ "$EVENT_NAME" = "push" ] && [ "$REF" = "refs/heads/main" ] &&
93-
[[ "$SUBJECT" =~ ^release:\ v[0-9] ]]; then
94-
echo "run=false" >> "$GITHUB_OUTPUT"
95-
exit 0
96-
fi
97-
98-
# Anything that is not a pull request runs the full suite: a release calls this
99-
# workflow with workflow_call, and a release that skipped its tests is the failure
100-
# this guard exists to prevent.
101-
if [ "$EVENT_NAME" != "pull_request" ]; then
102-
echo "run=true" >> "$GITHUB_OUTPUT"
103-
exit 0
104-
fi
105-
106-
# core.quotePath defaults to true, which wraps any path holding a non-ASCII byte or a
107-
# control character in double quotes and C-escapes it, so `TablePro/Café.swift` arrives
108-
# as `"TablePro/Caf\303\251.swift"` and the leading quote defeats the `^` anchor. This
109-
# detector fails open, so a misread means every suite skips behind a green gate. Reading
110-
# raw NUL-separated records removes both that and any embedded-newline trick.
111-
#
112-
# A file, not a variable: bash drops NUL bytes from "$(...)", which would run every path
113-
# together into one record. It also keeps grep out of a pipeline, so the SIGPIPE that
114-
# scripts/check-freetds-fedauth.sh:69 warns about cannot come back.
115-
git -c core.quotePath=false diff --no-renames --name-only -z \
116-
"$BASE_SHA" HEAD > "$RUNNER_TEMP/changed-files"
117-
118-
# Keep in step with the `push: paths:` list above. scripts/ and .github/actions/ are on
119-
# both because the jobs below execute them: a change to the shard tooling or to the
120-
# quarantine files used to merge behind a green gate having run nothing.
121-
if LC_ALL=C grep -zqE '^(TablePro/|Plugins/|Packages/|LocalPackages/|TableProTests/|TableProUITests/|Native/|Configs/|Libs/|scripts/|\.github/actions/|\.github/macos-(ui-)?test-quarantine\.txt$|TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$|TableProMobile/project\.yml$|project\.yml$|\.github/workflows/macos-tests\.yml$)' "$RUNNER_TEMP/changed-files"; then
122-
echo "run=true" >> "$GITHUB_OUTPUT"
123-
else
124-
echo "run=false" >> "$GITHUB_OUTPUT"
125-
fi
126-
127-
# One job for both packages rather than two. They cost 1.3 and 2.1 minutes, and a macOS
128-
# concurrency slot is the scarce resource here: the account runs five at a time, and the build
129-
# plus three UI shards plus the unit job already want four of them.
13094
packages:
13195
name: Package Tests
13296
needs: changes

scripts/ci/detect-changed-paths.sh

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Decides whether a workflow's suites need to run for this event. Prints "true" or "false".
5+
#
6+
# Usage:
7+
# detect-changed-paths.sh --event <name> [--base <sha>] [--ref <ref>] [--skip-release-commit] \
8+
# <path-regex> [<path-regex>...]
9+
#
10+
# The path arguments are alternatives in one anchored regex, so pass prefixes like "TablePro/" or
11+
# anchored files like 'project\.yml$'.
12+
#
13+
# This lived twice, inline in macos-tests.yml and ios-tests.yml, with a comment asking a human to
14+
# keep the two copies in step and nothing enforcing it. The reasoning below is the part worth not
15+
# duplicating.
16+
17+
EVENT=""
18+
BASE=""
19+
REF=""
20+
SKIP_RELEASE_COMMIT=0
21+
PATTERNS=()
22+
23+
while [ $# -gt 0 ]; do
24+
case "$1" in
25+
--event) EVENT="${2:?--event needs a value}"; shift 2 ;;
26+
--base) BASE="${2:-}"; shift 2 ;;
27+
--ref) REF="${2:-}"; shift 2 ;;
28+
--skip-release-commit) SKIP_RELEASE_COMMIT=1; shift ;;
29+
--) shift; PATTERNS+=("$@"); break ;;
30+
-*) echo "detect-changed-paths.sh: unknown flag $1" >&2; exit 2 ;;
31+
*) PATTERNS+=("$1"); shift ;;
32+
esac
33+
done
34+
35+
[ -n "$EVENT" ] || { echo "detect-changed-paths.sh: --event is required" >&2; exit 2; }
36+
[ "${#PATTERNS[@]}" -gt 0 ] || { echo "detect-changed-paths.sh: at least one path pattern is required" >&2; exit 2; }
37+
38+
# A release pushes the version commit to main and then tags that same commit, so the push and the
39+
# tag both reach the workflow: once directly, once through build.yml's workflow_call. That tests one
40+
# SHA twice, and because the account runs five macOS jobs at a time, the duplicate is what leaves
41+
# the release's own suite queued behind it. The tag run is the one that gates the release, so the
42+
# push to main stands down. github.ref belongs to the caller, so a workflow_call reads refs/tags/v*
43+
# here and can never match this guard, which is what keeps "a release that skipped its tests"
44+
# impossible.
45+
#
46+
# The subject goes into a variable instead of a pipe into grep. grep -q exits on the first match,
47+
# and the SIGPIPE that then kills git log would surface through pipefail as a skipped guard (the
48+
# same trap scripts/check-freetds-fedauth.sh documents).
49+
if [ "$SKIP_RELEASE_COMMIT" -eq 1 ] && [ "$EVENT" = "push" ] && [ "$REF" = "refs/heads/main" ]; then
50+
SUBJECT="$(git log -1 --format=%s HEAD)"
51+
if [[ "$SUBJECT" =~ ^release:\ v[0-9] ]]; then
52+
echo "false"
53+
exit 0
54+
fi
55+
fi
56+
57+
# Anything that is not a pull request runs everything: a release calls the suite with
58+
# workflow_call, and a release that skipped its tests is the failure this exists to prevent.
59+
if [ "$EVENT" != "pull_request" ]; then
60+
echo "true"
61+
exit 0
62+
fi
63+
64+
[ -n "$BASE" ] || { echo "detect-changed-paths.sh: --base is required for a pull request" >&2; exit 2; }
65+
66+
# core.quotePath defaults to true, which wraps any path holding a non-ASCII byte or a control
67+
# character in double quotes and C-escapes it, so `TablePro/Café.swift` arrives as
68+
# `"TablePro/Caf\303\251.swift"` and the leading quote defeats the `^` anchor. This detector fails
69+
# open, so a misread means every suite skips behind a green gate. Reading raw NUL-separated records
70+
# removes both that and any embedded-newline trick.
71+
#
72+
# A file, not a variable: bash drops NUL bytes from "$(...)", which would run every path together
73+
# into one record. It also keeps grep out of a pipeline, so the SIGPIPE noted above cannot come back.
74+
CHANGED="$(mktemp)"
75+
trap 'rm -f "$CHANGED"' EXIT
76+
git -c core.quotePath=false diff --no-renames --name-only -z "$BASE" HEAD > "$CHANGED"
77+
78+
JOINED="$(IFS='|'; echo "${PATTERNS[*]}")"
79+
if LC_ALL=C grep -zqE "^($JOINED)" "$CHANGED"; then
80+
echo "true"
81+
else
82+
echo "false"
83+
fi

0 commit comments

Comments
 (0)