Repository navigation
ci(native): build and test the native Swift packages on macOS #7984
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
|
||
| /** | ||
| * @file Routing contract for the native Swift coverage in the broad CI lane. | ||
| * | ||
| * The Swift packages of @astryxdesign/native build only on macOS. They join | ||
| * the `test` owner (AST-030 FR11), but a macOS runner is requested only when | ||
| * a Linux job finds a Swift change under packages/native, so no other pull | ||
| * request waits for one. | ||
| */ | ||
|
|
||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
|
|
||
| import {describe, expect, it} from 'vitest'; | ||
| import yaml from 'yaml'; | ||
|
|
||
| const root = path.resolve(import.meta.dirname, '../..'); | ||
| const ci = yaml.parse( | ||
| fs.readFileSync(path.join(root, '.github/workflows/ci.yml'), 'utf8'), | ||
| ); | ||
| const scope = ci.jobs['native-swift-scope']; | ||
| const swift = ci.jobs['test-native-swift']; | ||
| const step = (job, name) => | ||
| job.steps.find(candidate => candidate.name === name); | ||
|
|
||
| describe('native Swift routing', () => { | ||
| it('is joined by test', () => { | ||
| expect(ci.jobs.test.needs).toContain('test-native-swift'); | ||
| expect( | ||
| step(ci.jobs.test, 'Require expected test-owner outcomes').env, | ||
| ).toMatchObject({ | ||
| TEST_NATIVE_SWIFT_RESULT: '${{ needs.test-native-swift.result }}', | ||
| }); | ||
| }); | ||
|
|
||
| it('decides scope on Linux and asks for macOS only when Swift changed', () => { | ||
| expect(scope['runs-on']).toBe('ubuntu-slim'); | ||
| expect(swift.needs).toEqual(['check-scope', 'native-swift-scope']); | ||
| expect(swift['runs-on']).toBe( | ||
| "${{ needs.native-swift-scope.outputs.applies == 'true' && 'macos-15' || 'ubuntu-slim' }}", | ||
| ); | ||
| for (const candidate of swift.steps) { | ||
| if (candidate.uses || candidate.name?.startsWith('Build and test')) { | ||
| expect(candidate.if, candidate.name ?? candidate.uses).toBe( | ||
| "needs.native-swift-scope.outputs.applies == 'true'", | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| it('fails closed when the scope cannot be decided', () => { | ||
| expect(step(scope, 'Require successful scope classification').if).toBe( | ||
| "needs.check-scope.result != 'success'", | ||
| ); | ||
| expect(step(swift, 'Require a decided Swift scope').if).toBe( | ||
| "needs.native-swift-scope.result != 'success'", | ||
| ); | ||
| const decide = step(scope, 'Decide whether native Swift changed').run; | ||
| expect(decide).toContain( | ||
| 'git show "$BASE_SHA:.github/scripts/pr-scope-touches.sh"', | ||
| ); | ||
| // Only "out of scope" (exit 1) skips; unknown (2) runs the Swift work. | ||
| expect(decide).toContain('if [ "$scope" = 1 ]; then'); | ||
| }); | ||
|
|
||
| it('skips the Swift work on docsite-only and tooling-only changes', () => { | ||
| const decide = step(scope, 'Decide whether native Swift changed'); | ||
| expect(decide.env).toMatchObject({ | ||
| DOCSITE_ONLY: '${{ needs.check-scope.outputs.docsite_only }}', | ||
| TOOLING_ONLY: '${{ needs.check-scope.outputs.tooling_only }}', | ||
| }); | ||
| expect(decide.run).toContain( | ||
| 'if [ "$DOCSITE_ONLY" = true ] || [ "$TOOLING_ONLY" = true ]; then', | ||
| ); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -728,6 +728,119 @@ jobs: | |
| if: matrix.shard == 1 && needs.check-scope.outputs.docsite_only != 'true' && needs.check-scope.outputs.spec_only != 'true' && needs.check-scope.outputs.tooling_only != 'true' | ||
| run: node .github/scripts/theme-family-cascade.js | ||
|
|
||
| # Native Swift: the Swift packages of the experimental, canary-only | ||
| # @astryxdesign/native (AST-082) build and test only on macOS. They are | ||
| # coverage inside the shared surface (AST-030 FR11), so `test` joins | ||
| # test-native-swift. A Linux job first decides whether the change touches | ||
| # Swift under packages/native; only then does test-native-swift take a macOS | ||
| # runner, and otherwise it passes on Linux without work. | ||
| native-swift-scope: | ||
| needs: [check-scope] | ||
| if: ${{ github.event_name != 'workflow_dispatch' && always() && !cancelled() && needs.check-scope.outputs.spec_only != 'true' }} | ||
| runs-on: ubuntu-slim | ||
| permissions: | ||
| contents: read | ||
| outputs: | ||
| applies: ${{ steps.scope.outputs.applies }} | ||
| steps: | ||
| - name: Require successful scope classification | ||
| if: needs.check-scope.result != 'success' | ||
| run: | | ||
| echo "Change scope could not be classified; refusing to skip required work." | ||
| exit 1 | ||
|
|
||
| - uses: actions/checkout@v7 | ||
| if: needs.check-scope.outputs.docsite_only != 'true' && needs.check-scope.outputs.tooling_only != 'true' | ||
|
|
||
| # The helper comes from the base, since an older head lacks it. Exit 1 | ||
| # means out of scope; 0 in scope; 2 unknown, so the Swift work runs. | ||
| - name: Decide whether native Swift changed | ||
| id: scope | ||
| env: | ||
| DOCSITE_ONLY: ${{ needs.check-scope.outputs.docsite_only }} | ||
| TOOLING_ONLY: ${{ needs.check-scope.outputs.tooling_only }} | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }} | ||
| run: | | ||
| if [ "$DOCSITE_ONLY" = true ] || [ "$TOOLING_ONLY" = true ]; then | ||
| echo 'Specialized lane; no native Swift work.' | ||
| echo 'applies=false' >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| scope=2 | ||
| helper="$(mktemp)" | ||
| if [ -n "$BASE_SHA" ] \ | ||
| && git fetch --quiet --no-tags --depth=1 origin "$BASE_SHA" \ | ||
| && git show "$BASE_SHA:.github/scripts/pr-scope-touches.sh" > "$helper"; then | ||
| scope=0 | ||
| bash "$helper" "$BASE_SHA" '^packages/native/.*(\.swift|/Package\.swift)$' || scope=$? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With |
||
| fi | ||
| if [ "$scope" = 1 ]; then | ||
| echo 'No native Swift change; nothing to build.' | ||
| echo 'applies=false' >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo 'applies=true' >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| test-native-swift: | ||
| needs: [check-scope, native-swift-scope] | ||
| if: ${{ github.event_name != 'workflow_dispatch' && always() && !cancelled() && needs.check-scope.outputs.spec_only != 'true' }} | ||
| runs-on: ${{ needs.native-swift-scope.outputs.applies == 'true' && 'macos-15' || 'ubuntu-slim' }} | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Require a decided Swift scope | ||
| if: needs.native-swift-scope.result != 'success' | ||
| run: | | ||
| echo "Native Swift scope could not be decided; refusing to skip required work." | ||
| exit 1 | ||
|
|
||
| - name: Skip Swift work outside its scope | ||
| if: needs.native-swift-scope.outputs.applies != 'true' | ||
| run: echo "No native Swift change in this scope." | ||
|
|
||
| - uses: actions/checkout@v7 | ||
| if: needs.native-swift-scope.outputs.applies == 'true' | ||
|
|
||
| # One package per directory that holds a Package.swift, such as apple/ | ||
| # (generated contracts) and each versioned swift-ios-<n>/. A package that | ||
| # supports macOS builds and tests there; every package builds for the iOS | ||
| # Simulator, and an iOS-only package runs its tests there. A package | ||
| # without a test target only builds. | ||
| - name: Build and test each native Swift package | ||
| if: needs.native-swift-scope.outputs.applies == 'true' | ||
| run: | | ||
| set -euo pipefail | ||
| xcodebuild -version | ||
| packages=$(find packages/native -name Package.swift -not -path '*/.build/*' -not -path '*/node_modules/*' -exec dirname {} \; | sort) | ||
| if [ -z "$packages" ]; then | ||
| echo 'No Swift package found under packages/native.' | ||
| exit 1 | ||
| fi | ||
| for package in $packages; do | ||
| echo "::group::$package" | ||
| ( | ||
| cd "$package" | ||
| swift package dump-package > "$RUNNER_TEMP/package.json" | ||
| read -r macos tests < <(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(any(p["platformName"]=="macos" for p in d.get("platforms",[])) or not d.get("platforms"), any(t["type"]=="test" for t in d["targets"]))' "$RUNNER_TEMP/package.json") | ||
| # Xcode names the whole-package scheme <name>-Package when a | ||
| # package has several products, and after its product otherwise. | ||
| xcodebuild -list -json > "$RUNNER_TEMP/schemes.json" | ||
| scheme=$(python3 -c 'import json,sys; s=json.load(open(sys.argv[1]))["workspace"]["schemes"]; print(next((x for x in s if x.endswith("-Package")), s[0]))' "$RUNNER_TEMP/schemes.json") | ||
| if [ "$macos" = True ] && [ "$tests" = True ]; then | ||
| swift test | ||
| elif [ "$macos" = True ]; then | ||
| swift build | ||
| fi | ||
| if [ "$macos" != True ] && [ "$tests" = True ]; then | ||
| xcodebuild test -scheme "$scheme" -destination 'platform=iOS Simulator,name=iPhone 16' -quiet | ||
| else | ||
| xcodebuild build -scheme "$scheme" -destination 'generic/platform=iOS Simulator' -quiet | ||
| fi | ||
| ) | ||
| echo '::endgroup::' | ||
| done | ||
|
|
||
| # Public registry contract. Generation proves schema, dependency, route-lock, | ||
| # alias, and release-gating invariants; the final step drives every canonical | ||
| # item through the exact pinned ShadCN client and compiles every written source. | ||
|
|
@@ -804,7 +917,15 @@ jobs: | |
| # public registry contract always report or explicitly skip, so this join | ||
| # projects their combined result without creating another surface lane. | ||
| test: | ||
| needs: [check-scope, test-ui, test-node, test-build, registry-contract] | ||
| needs: | ||
| [ | ||
| check-scope, | ||
| test-ui, | ||
| test-node, | ||
| test-build, | ||
| registry-contract, | ||
| test-native-swift, | ||
| ] | ||
| if: ${{ github.event_name != 'workflow_dispatch' && always() && !cancelled() && needs.check-scope.outputs.spec_only != 'true' }} | ||
| runs-on: ubuntu-slim | ||
| permissions: | ||
|
|
@@ -822,6 +943,7 @@ jobs: | |
| TEST_NODE_RESULT: ${{ needs.test-node.result }} | ||
| TEST_BUILD_RESULT: ${{ needs.test-build.result }} | ||
| REGISTRY_CONTRACT_RESULT: ${{ needs.registry-contract.result }} | ||
| TEST_NATIVE_SWIFT_RESULT: ${{ needs.test-native-swift.result }} | ||
| run: node .github/scripts/ci-test-join.mjs | ||
|
|
||
| # Build storybook + typecheck + analyze components (parallel with test and | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This pins the comparison, but not the default
scope=2(ci.yml:769) or the pattern (ci.yml:775): changing the default toscope=1, or the pattern to one that cannot match a Swift path, leaves the whole.github/scriptssuite green while native Swift changes would skip the build. Asserting both, or running the step against a few fixture paths (Swift source,Package.swift, a newswift-ios-<n>/package, a moved-out.swift), would make the fail-closed promise mutation-sensitive.