Skip to content

Commit f2e425f

Browse files
ksroda-saclaude
andauthored
chore(ci): add per-workflow aggregators and iOS test workflow (#158)
* chore(ci): add per-workflow aggregator jobs and iOS test workflow Each existing test workflow (test-android, test-dotnet, test-java, test-js) now ends with an `<framework> tests passed` aggregator job that needs the matrix and runs with `if: always()`. This collapses the variable number of per-sample matrix entries into one stable check name, so branch protection or rulesets only need to track 5 names regardless of how many samples exist. Adds .github/workflows/test-ios.yml — same find-projects + matrix + aggregator shape as the others, runs on macos-latest. Discovers iOS samples via project.yml (the xcodegen input is the authoritative source). The xcodebuild test step picks an iPhone simulator at runtime via xcrun simctl rather than hard-coding a device name, so the workflow keeps working as GitHub-hosted runner images get updated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): install Android cmdline-tools via shell The community android-actions/setup-android@v3 isn't on the org's third-party action allowlist. Replace with a small shell step that downloads Android command-line tools directly from Google's CDN, accepts SDK licenses, and installs the same packages we used before (platform-tools, platforms;android-36, build-tools;36.0.0). The cmdline-tools build number is pinned (Google releases new ones periodically; bump intentionally rather than chasing latest). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): switch iOS xcodebuild to ad-hoc signing CODE_SIGNING_ALLOWED=NO produced an unsigned binary with no entitlements, which caused token-refresh's RefreshTokenStoreTests to fail with errSecMissingEntitlement (osStatus -34018) when SecItemAdd/SecItemDelete checked for an application-identifier. Use CODE_SIGN_IDENTITY=- (ad-hoc) + CODE_SIGNING_REQUIRED=NO instead. Ad-hoc signing is free, available on every macOS runner without an Apple Developer account, and embeds the entitlements Keychain APIs need on the simulator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): bound sdkmanager license input to avoid pipefail trip `yes | sdkmanager --licenses` exited non-zero in CI because `yes` got SIGPIPE when sdkmanager closed its stdin after the last license, and `set -o pipefail` propagated that as a script failure (even though every license was actually accepted). Replace with `printf 'y\n%.0s' $(seq 1 50)` — a bounded producer that naturally EOFs after 50 acceptances. That's plenty for Android's ~7 licenses today, and won't ever SIGPIPE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): generate test-bundle Info.plist for iOS xcodebuild After switching to ad-hoc signing, xcodebuild now demands an Info.plist for the QuickstartTests bundle target — the project.yml in both iOS samples doesn't declare INFOPLIST_FILE for the test target (Xcode's GUI generates one implicitly, but xcodebuild doesn't unless told to). Pass GENERATE_INFOPLIST_FILE=YES so xcodebuild auto-generates a minimal plist for the test bundle, matching the GUI's implicit behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): scope test workflows' push trigger to main only Without `branches: [main]` on the push trigger, every push to a PR branch fires BOTH `push` and `pull_request` events for the same commit, producing duplicate workflow runs and duplicate status checks. Match the pattern that extract.yml already uses: push only triggers on main (catches the post-merge CI), PRs trigger pull_request only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): verify Android cmdline-tools download against pinned SHA-256 HTTPS to dl.google.com validates the connection but not the file content. Pin the canonical SHA-256 for the cmdline-tools build we use and verify with `sha256sum -c` before unzip — defense-in-depth against an origin compromise or transient CDN corruption. Bumping CMDLINE_TOOLS_ZIP requires updating CMDLINE_TOOLS_SHA256 in lock step; a mismatch fails the workflow so we never silently execute a poisoned download. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(ci): select iOS simulator by UDID instead of by name `xcrun simctl list` can return multiple devices with the same name across different iOS runtimes (common during macOS version transitions on GitHub-hosted runners). Selecting by name made `xcodebuild -destination` ambiguous, with the picked device depending on jq iteration order. Capture the UDID — globally unique — and pass `id=<UDID>` to xcodebuild so the destination is deterministic. Sort runtime keys and take `last` to pick the newest iOS runtime when multiple are installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b9f84bf commit f2e425f

5 files changed

Lines changed: 193 additions & 3 deletions

File tree

.github/workflows/test-android.yml

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Test Android Builds
22

33
on:
44
push:
5+
branches: [main]
56
paths:
67
- "samples/react-native/**"
78
- "samples/android/**"
@@ -51,9 +52,41 @@ jobs:
5152
java-version: "17"
5253

5354
- name: Set up Android SDK
54-
uses: android-actions/setup-android@v4
55-
with:
56-
packages: "platform-tools platforms;android-36 build-tools;36.0.0"
55+
# The community `android-actions/setup-android` action isn't on the org's
56+
# third-party action allowlist, so we install Android command-line tools
57+
# ourselves. The build number in the cmdline-tools URL is pinned (Google
58+
# publishes new ones periodically; bump intentionally rather than chasing).
59+
env:
60+
ANDROID_HOME: ${{ runner.temp }}/android-sdk
61+
CMDLINE_TOOLS_ZIP: commandlinetools-linux-11076708_latest.zip
62+
# SHA-256 of the build above as published by Google. Bumping the build
63+
# number requires updating this hash; mismatch fails the workflow so we
64+
# never silently run a corrupted/poisoned download.
65+
CMDLINE_TOOLS_SHA256: 2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258
66+
run: |
67+
set -euo pipefail
68+
mkdir -p "$ANDROID_HOME/cmdline-tools"
69+
cd "$ANDROID_HOME/cmdline-tools"
70+
curl -fsSL "https://dl.google.com/android/repository/$CMDLINE_TOOLS_ZIP" -o tools.zip
71+
echo "$CMDLINE_TOOLS_SHA256 tools.zip" | sha256sum -c -
72+
unzip -q tools.zip
73+
mv cmdline-tools latest
74+
rm tools.zip
75+
76+
# Expose for subsequent steps (Gradle reads ANDROID_HOME).
77+
{
78+
echo "ANDROID_HOME=$ANDROID_HOME"
79+
echo "ANDROID_SDK_ROOT=$ANDROID_HOME"
80+
} >> "$GITHUB_ENV"
81+
{
82+
echo "$ANDROID_HOME/cmdline-tools/latest/bin"
83+
echo "$ANDROID_HOME/platform-tools"
84+
} >> "$GITHUB_PATH"
85+
86+
# Accept licenses (sdkmanager errors out otherwise).
87+
printf 'y\n%.0s' $(seq 1 50) | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null
88+
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
89+
"platform-tools" "platforms;android-36" "build-tools;36.0.0"
5790
5891
# React Native samples need their JS deps installed first so autolinking
5992
# can resolve native modules from node_modules. Detect by walking up to
@@ -100,3 +133,14 @@ jobs:
100133
- name: assembleDebug
101134
working-directory: ${{ matrix.android_dir }}
102135
run: ./gradlew assembleDebug --no-daemon
136+
137+
# Stable aggregator for branch protection.
138+
all-tests-passed:
139+
name: Android tests passed
140+
needs: build
141+
if: always()
142+
runs-on: ubuntu-latest
143+
steps:
144+
- name: Verify build matrix succeeded
145+
if: needs.build.result != 'success' && needs.build.result != 'skipped'
146+
run: exit 1

.github/workflows/test-dotnet.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Test .NET Frameworks
22

33
on:
44
push:
5+
branches: [main]
56
paths:
67
- "samples/dotnet/**"
78
- ".github/workflows/test-dotnet.yml"
@@ -50,3 +51,14 @@ jobs:
5051
- name: Test
5152
working-directory: ${{ matrix.project }}
5253
run: dotnet test --no-build --configuration Release
54+
55+
# Stable aggregator for branch protection.
56+
all-tests-passed:
57+
name: .NET tests passed
58+
needs: test
59+
if: always()
60+
runs-on: ubuntu-latest
61+
steps:
62+
- name: Verify test matrix succeeded
63+
if: needs.test.result != 'success' && needs.test.result != 'skipped'
64+
run: exit 1

.github/workflows/test-ios.yml

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
name: Test iOS Builds
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "samples/ios/**"
8+
- ".github/workflows/test-ios.yml"
9+
pull_request:
10+
paths:
11+
- "samples/ios/**"
12+
- ".github/workflows/test-ios.yml"
13+
schedule:
14+
- cron: "0 10 * * 1"
15+
workflow_dispatch:
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
find-projects:
22+
runs-on: ubuntu-latest
23+
outputs:
24+
matrix: ${{ steps.find.outputs.matrix }}
25+
steps:
26+
- uses: actions/checkout@v6
27+
- id: find
28+
# Each iOS sample is identified by its project.yml (xcodegen input). We use
29+
# that as the marker rather than the generated .xcodeproj because the
30+
# project.yml is the authoritative source — generated files come and go.
31+
run: |
32+
DIRS=$(find samples/ios -mindepth 2 -maxdepth 2 -name "project.yml" -exec dirname {} \; 2>/dev/null | sort | jq -R -s -c 'split("\n") | map(select(. != ""))')
33+
echo "matrix=$DIRS" >> "$GITHUB_OUTPUT"
34+
35+
test:
36+
needs: find-projects
37+
if: ${{ needs.find-projects.outputs.matrix != '[]' }}
38+
runs-on: macos-latest
39+
strategy:
40+
fail-fast: false
41+
matrix:
42+
project: ${{ fromJson(needs.find-projects.outputs.matrix) }}
43+
steps:
44+
- uses: actions/checkout@v6
45+
46+
# Config.xcconfig is gitignored — copy the example so build settings have
47+
# placeholder values. No real CIAM workspace is needed for build + unit tests
48+
# (network-touching paths only execute in the running app).
49+
- name: Provision example xcconfig
50+
working-directory: ${{ matrix.project }}
51+
run: cp Config.example.xcconfig Config.xcconfig
52+
53+
# Pre-fetch Swift Package Manager deps (AppAuth-iOS) so the test step doesn't
54+
# also pay the resolution cost — makes failures during dep resolution easier
55+
# to spot in logs.
56+
- name: Resolve SPM dependencies
57+
working-directory: ${{ matrix.project }}
58+
run: xcodebuild -resolvePackageDependencies -project Quickstart.xcodeproj -scheme Quickstart
59+
60+
# GitHub-hosted macOS runners can ship multiple iOS runtimes (especially
61+
# during macOS version transitions), each with overlapping device names
62+
# like "iPhone 16 Pro". Capture the UDID — globally unique — rather than
63+
# the name, and pass `id=` to xcodebuild for an unambiguous destination.
64+
# We pick the newest iOS runtime by sorting runtime keys.
65+
- name: Pick iOS simulator
66+
id: sim
67+
run: |
68+
UDID=$(xcrun simctl list devices available --json | jq -r '
69+
.devices
70+
| to_entries
71+
| map(select(.key | test("com.apple.CoreSimulator.SimRuntime.iOS")))
72+
| sort_by(.key) | last
73+
| .value[]
74+
| select(.name | startswith("iPhone"))
75+
| .udid
76+
' | head -1)
77+
if [ -z "$UDID" ]; then
78+
echo "No iPhone simulator found"; exit 1
79+
fi
80+
echo "Selected UDID: $UDID"
81+
echo "udid=$UDID" >> "$GITHUB_OUTPUT"
82+
83+
- name: Build and test
84+
working-directory: ${{ matrix.project }}
85+
# - Ad-hoc signing (`CODE_SIGN_IDENTITY=-`) gives the app an
86+
# `application-identifier` entitlement so Keychain APIs work on the
87+
# simulator. `CODE_SIGNING_REQUIRED=NO` keeps things permissive.
88+
# - `GENERATE_INFOPLIST_FILE=YES` lets xcodebuild auto-generate the
89+
# QuickstartTests target's Info.plist, which the project.yml doesn't
90+
# declare explicitly (Xcode's GUI builds it implicitly, but xcodebuild
91+
# demands one once we ad-hoc sign).
92+
run: |
93+
xcodebuild test \
94+
-project Quickstart.xcodeproj \
95+
-scheme Quickstart \
96+
-destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \
97+
CODE_SIGN_IDENTITY=- \
98+
CODE_SIGNING_REQUIRED=NO \
99+
GENERATE_INFOPLIST_FILE=YES
100+
101+
# Stable aggregator for branch protection.
102+
all-tests-passed:
103+
name: iOS tests passed
104+
needs: test
105+
if: always()
106+
runs-on: ubuntu-latest
107+
steps:
108+
- name: Verify test matrix succeeded
109+
if: needs.test.result != 'success' && needs.test.result != 'skipped'
110+
run: exit 1

.github/workflows/test-java.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Test Java Frameworks
22

33
on:
44
push:
5+
branches: [main]
56
paths:
67
- "samples/java/**"
78
- ".github/workflows/test-java.yml"
@@ -46,3 +47,14 @@ jobs:
4647
- name: Build and test
4748
working-directory: ${{ matrix.project }}
4849
run: mvn -B verify
50+
51+
# Stable aggregator for branch protection.
52+
all-tests-passed:
53+
name: Java tests passed
54+
needs: test
55+
if: always()
56+
runs-on: ubuntu-latest
57+
steps:
58+
- name: Verify test matrix succeeded
59+
if: needs.test.result != 'success' && needs.test.result != 'skipped'
60+
run: exit 1

.github/workflows/test-js.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: Test JS Frameworks
22

33
on:
44
push:
5+
branches: [main]
56
paths:
67
- "samples/react/**"
78
- "samples/angular/**"
@@ -76,3 +77,14 @@ jobs:
7677
elif node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)"; then
7778
yarn test
7879
fi
80+
81+
# Stable aggregator for branch protection.
82+
all-tests-passed:
83+
name: JS tests passed
84+
needs: test
85+
if: always()
86+
runs-on: ubuntu-latest
87+
steps:
88+
- name: Verify test matrix succeeded
89+
if: needs.test.result != 'success' && needs.test.result != 'skipped'
90+
run: exit 1

0 commit comments

Comments
 (0)