From 9820cda4af99d40f1ad54e8be247524ac22196b8 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 07:16:27 -0700 Subject: [PATCH 01/12] Lower swift-tools-version to 6.2 for BCR compatibility (draft) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BCR's macos_arm64 runners ship with Xcode 26.0 (Swift 6.2). Our Package.swift previously declared `swift-tools-version: 6.3`, which the older Swift PM rejects with "package 'module_src' is using Swift tools version 6.3.0 but the installed version is X.Y.Z" before rules_swift_package_manager can translate it into Bazel targets. This is the experimental "see if it just works" path: lower the floor to 6.2, broaden the swift-syntax range so SPM can pick a 6.2-compatible swift-syntax version, and switch the SafeDI-core CI jobs to Xcode 26.0 to validate the baseline. If CI is green, we have a clear shot at BCR; if not, we fall back to dropping rules_swift_package_manager in favor of native bazel_deps. ## Changes - `Package.swift` — `swift-tools-version: 6.3` → `6.2`. We don't use any 6.3-only features (the `traits:` field is 6.1+). - `Package.swift` — `swift-syntax` range widened from `"603.0.0"..<"605.0.0"` to `"602.0.0"..<"605.0.0"`. - `Plugins/MigrateSafeDIFromVersionOne/MigrateSafeDIFromVersionOne.swift` — version-floor check + error message updated to say 6.2. - `Documentation/Manual.md` — three "requires Swift 6.3" mentions in the migration guide → "6.2". - `Examples/ExampleBazelIntegration/README.md` — adds a "Toolchain: Swift 6.2 or later" prerequisite. - `Examples/ExampleTuistIntegration/README.md` — clarifies that the example tracks Swift 6.3 / Xcode 26.4 but SafeDI itself supports 6.2+. ## CI changes (existing jobs only) - `xcodebuild` matrix → Xcode 26.0 (was 26.4). - `spm` Build and Test → Xcode 26.0 (was 26.4). The example builds + DocC + publish workflow stay on Xcode 26.4 (Swift 6.3) so we still validate consumer-side behavior on the latest. Linux CI continues to use the `swift:6.3` Docker image, so the test suite still gets Swift 6.3 coverage there. ## Verified locally - `swift build --traits sourceBuild` — green. - `./CLI/lint.sh` — clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 16 ++++++++++++---- Documentation/Manual.md | 6 +++--- Examples/ExampleBazelIntegration/README.md | 6 ++++++ Examples/ExampleTuistIntegration/README.md | 4 ++-- Package.swift | 4 ++-- .../MigrateSafeDIFromVersionOne.swift | 4 ++-- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36892fd9..d0157a2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,12 @@ jobs: awk '/^module\(/,/^\)/' MODULE.bazel | grep -q 'version = "99.99.99-test"' xcodebuild: - name: Build with xcodebuild on Xcode 26 + name: Build with xcodebuild on Xcode 26.0 + # Pinned to Xcode 26.0 (Swift 6.2) — the lowest Swift toolchain + # we support, matching what BCR's macos_arm64 runners ship. + # Verifies SafeDI's Package.swift parses + compiles on the + # baseline Swift version. Examples + the publish workflow stay + # on the latest Xcode. runs-on: macos-26 strategy: matrix: @@ -60,7 +65,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer - name: Download Platform if: matrix.platforms != 'platform=macOS' run: | @@ -187,7 +192,10 @@ jobs: run: pushd Examples/ExampleTuistIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -workspace ExampleTuistIntegration.xcworkspace -scheme ExampleTuistIntegration; popd spm: - name: Build and Test on Xcode 26 + name: Build and Test on Xcode 26.0 + # Pinned to Xcode 26.0 (Swift 6.2) — see the `xcodebuild` job + # comment above for rationale. Coverage on the latest toolchain + # is provided by the Linux job (Swift 6.3 docker). runs-on: macos-26 permissions: contents: read @@ -195,7 +203,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer - name: Resolve Package Dependencies uses: ./.github/actions/retry with: diff --git a/Documentation/Manual.md b/Documentation/Manual.md index d05d328c..5c6b7b79 100644 --- a/Documentation/Manual.md +++ b/Documentation/Manual.md @@ -781,7 +781,7 @@ UIKit applications’ natural root is the `UIApplicationDelegate`-conforming app ## Migrating from SafeDI 1.x to 2.x -SafeDI 2.x requires Swift 6.3 or later and does not support CocoaPods. Projects using an earlier Swift version or CocoaPods should use SafeDI 1.x. +SafeDI 2.x requires Swift 6.2 or later and does not support CocoaPods. Projects using an earlier Swift version or CocoaPods should use SafeDI 1.x. SafeDI 2.x also removes support for CSV-based configuration files (`.safedi/configuration/include.csv` and `.safedi/configuration/additionalImportedModules.csv`). Configuration is now done via the `#SafeDIConfiguration` macro. @@ -794,14 +794,14 @@ swift package plugin safedi-v1-to-v2 --target ``` This plugin will: -1. Verify your `swift-tools-version` is 6.3 or later +1. Verify your `swift-tools-version` is 6.2 or later 2. Create a `SafeDIConfiguration.swift` file in your target’s source directory 3. Migrate any existing CSV configuration values into the new `#SafeDIConfiguration` macro 4. Delete the obsolete CSV files ### Manual migration -1. Update your `swift-tools-version` to 6.3 or later +1. Update your `swift-tools-version` to 6.2 or later 2. Update your SafeDI dependency to `from: "2.0.0"` 3. If you have `.safedi/configuration/include.csv` or `.safedi/configuration/additionalImportedModules.csv`, add a `#SafeDIConfiguration` in your root module with the equivalent values and delete the CSV files 4. If you don’t have CSV configuration files and do not need other SafeDI configuration options, no `#SafeDIConfiguration()` declaration is required diff --git a/Examples/ExampleBazelIntegration/README.md b/Examples/ExampleBazelIntegration/README.md index f65d5c3a..c637eeb6 100644 --- a/Examples/ExampleBazelIntegration/README.md +++ b/Examples/ExampleBazelIntegration/README.md @@ -10,6 +10,12 @@ system. [`rules_apple`](https://github.com/bazelbuild/rules_apple) macOS toolchain). The SafeDI rules themselves are platform-agnostic. +**Toolchain:** Swift 6.2 or later (Xcode 26.0+). SafeDI's +`Package.swift` declares `swift-tools-version: 6.2`, which is what +the Bazel Central Registry's `macos_arm64` runners ship with; +trying to consume SafeDI from a Bazel build that uses an older +Swift toolchain will fail at the swift-syntax compile step. + ## What it demonstrates - **Two `swift_library` targets** (`//Subproject:Subproject`, diff --git a/Examples/ExampleTuistIntegration/README.md b/Examples/ExampleTuistIntegration/README.md index 39dd576d..af540134 100644 --- a/Examples/ExampleTuistIntegration/README.md +++ b/Examples/ExampleTuistIntegration/README.md @@ -96,9 +96,9 @@ arbitrary input/output paths. | Tool | Notes | |------|-------| -| macOS with Xcode 26.4 | Matches the rest of SafeDI's CI. | +| macOS with Xcode 26.0+ | This example pins Swift 6.3 / Xcode 26.4 (matches the rest of SafeDI's example CI), but SafeDI itself supports Swift 6.2 / Xcode 26.0 minimum — bump the example down if you need to test against the baseline. | | [Tuist](https://tuist.dev) 4.x | Install via [mise](https://mise.jdx.dev). Tuist no longer publishes a Homebrew formula. | -| Swift 6.3 toolchain | Ships with Xcode 26.4 | +| Swift 6.3 toolchain | Ships with Xcode 26.4. SafeDI itself supports Swift 6.2+; this example tracks the latest. | ### Installing Tuist diff --git a/Package.swift b/Package.swift index 23eb6963..a252a901 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.3 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import CompilerPluginSupport @@ -48,7 +48,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.4.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/swiftlang/swift-syntax.git", "603.0.0"..<"605.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", "602.0.0"..<"605.0.0"), ], targets: [ // Macros diff --git a/Plugins/MigrateSafeDIFromVersionOne/MigrateSafeDIFromVersionOne.swift b/Plugins/MigrateSafeDIFromVersionOne/MigrateSafeDIFromVersionOne.swift index 04f2db31..01732833 100644 --- a/Plugins/MigrateSafeDIFromVersionOne/MigrateSafeDIFromVersionOne.swift +++ b/Plugins/MigrateSafeDIFromVersionOne/MigrateSafeDIFromVersionOne.swift @@ -52,8 +52,8 @@ struct MigrateSafeDIFromVersionOne: CommandPlugin { Diagnostics.error("Could not parse swift-tools-version from Package.swift") return } - guard major > 6 || (major == 6 && minor >= 3) else { - Diagnostics.error("SafeDI 2.x requires swift-tools-version 6.3 or later. Found \(major).\(minor). Update your Package.swift before migrating.") + guard major > 6 || (major == 6 && minor >= 2) else { + Diagnostics.error("SafeDI 2.x requires swift-tools-version 6.2 or later. Found \(major).\(minor). Update your Package.swift before migrating.") return } From a3489201ce06710c38bd0cea1c32466488455361 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 07:25:52 -0700 Subject: [PATCH 02/12] ci: bazel job on Xcode 26.0 too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forgotten in the previous commit. The bazel job builds SafeDI's Bazel targets and the example downstream consumer; if our goal is to mirror BCR's macos_arm64 environment locally, that job needs to use Xcode 26.0 (Swift 6.2) — same as the `xcodebuild` matrix and the `spm` test job. --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0157a2d..8dc478f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -254,7 +254,10 @@ jobs: os: linux bazel: - name: Bazel Build on macOS + name: Bazel Build on Xcode 26.0 + # Pinned to Xcode 26.0 (Swift 6.2) to mirror BCR's macos_arm64 + # runners — that's the toolchain SafeDI's BCR submission has to + # build under, so our local Bazel CI should match. runs-on: macos-26 permissions: contents: read @@ -262,7 +265,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer - name: Build Sources run: bazelisk build //Sources/... - name: Build Example From cfedc67b42fa65dc698cc2fa20cdc8a124898862 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 08:09:50 -0700 Subject: [PATCH 03/12] ci + Tuist example: tighten Xcode-version comments + drop where unneeded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Trim the BCR-rationale comments on `xcodebuild` / `spm` / `bazel` jobs to a single-line "matches BCR's macos_arm64 runners". The longer prose was throat-clearing. - Add a single comment block above the example CI section explaining the 26.4-vs-26.0 split: Xcode 26.4 was the first Xcode to support package traits, so examples that declare them need 26.4; the rest run on 26.0 (the SafeDI-core baseline). - Switch `spm-tuist-integration` to Xcode 26.0 — Tuist example doesn't declare package traits. - Lower `Examples/ExampleTuistIntegration/Tuist/Package.swift` swift-tools-version 6.3 → 6.2 to match. - Trim the Tuist README prerequisites table — drop the "but SafeDI itself supports Swift 6.2+; this example tracks the latest" caveats that contradict the new 6.2 floor. - Linux job: switch from `swift:6.3` Docker → `swift:6.2`. Linux no longer needs to be the "test on the latest" job since the whole build matrix runs Swift 6.2 now. --- .github/workflows/ci.yml | 37 ++++++++++--------- Examples/ExampleTuistIntegration/README.md | 4 +- .../Tuist/Package.swift | 2 +- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dc478f6..5d448e7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,11 +43,7 @@ jobs: xcodebuild: name: Build with xcodebuild on Xcode 26.0 - # Pinned to Xcode 26.0 (Swift 6.2) — the lowest Swift toolchain - # we support, matching what BCR's macos_arm64 runners ship. - # Verifies SafeDI's Package.swift parses + compiles on the - # baseline Swift version. Examples + the publish workflow stay - # on the latest Xcode. + # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 strategy: matrix: @@ -99,8 +95,16 @@ jobs: with: command: swift package generate-documentation --target SafeDI --warnings-as-errors + # Example CI jobs below pick their Xcode version based on whether + # the example uses package traits: Xcode 26.4 was the first Xcode + # to support package traits, so any example whose Package.swift / + # `.xcodeproj` declares them needs 26.4. Examples that only consume + # SafeDI via its default trait (no explicit `traits:` syntax) run + # on Xcode 26.0 — same baseline as the SafeDI-core jobs above. + spm-package-integration: - name: Build Package Integration on Xcode 26 + name: Build Package Integration on Xcode 26.4 + # Uses `.package(traits: ["sourceBuild"])`. runs-on: macos-26 permissions: contents: read @@ -123,7 +127,8 @@ jobs: run: pushd "Examples/Example Package Integration"; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExamplePackageIntegration -destination "generic/platform=iOS"; popd spm-project-integration: - name: Build Project Integration on Xcode 26 + name: Build Project Integration on Xcode 26.4 + # Project file declares package traits. runs-on: macos-26 permissions: contents: read @@ -143,7 +148,8 @@ jobs: run: pushd Examples/ExampleProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleProjectIntegration; popd spm-multi-project-integration: - name: Build Multi Project Integration on Xcode 26 + name: Build Multi Project Integration on Xcode 26.4 + # Project file declares package traits. runs-on: macos-26 permissions: contents: read @@ -163,7 +169,8 @@ jobs: run: pushd Examples/ExampleMultiProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleMultiProjectIntegration; popd spm-tuist-integration: - name: Build Tuist Integration on Xcode 26 + name: Build Tuist Integration on Xcode 26.0 + # Tuist example doesn't declare package traits. runs-on: macos-26 permissions: contents: read @@ -175,7 +182,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer - name: Install Tuist via mise # Keep the pinned Tuist version here in sync with the README. run: | @@ -193,9 +200,7 @@ jobs: spm: name: Build and Test on Xcode 26.0 - # Pinned to Xcode 26.0 (Swift 6.2) — see the `xcodebuild` job - # comment above for rationale. Coverage on the latest toolchain - # is provided by the Linux job (Swift 6.3 docker). + # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 permissions: contents: read @@ -226,7 +231,7 @@ jobs: linux: name: Build and Test on Linux runs-on: ubuntu-latest - container: swift:6.3 + container: swift:6.2 permissions: contents: read steps: @@ -255,9 +260,7 @@ jobs: bazel: name: Bazel Build on Xcode 26.0 - # Pinned to Xcode 26.0 (Swift 6.2) to mirror BCR's macos_arm64 - # runners — that's the toolchain SafeDI's BCR submission has to - # build under, so our local Bazel CI should match. + # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 permissions: contents: read diff --git a/Examples/ExampleTuistIntegration/README.md b/Examples/ExampleTuistIntegration/README.md index af540134..ac8192bb 100644 --- a/Examples/ExampleTuistIntegration/README.md +++ b/Examples/ExampleTuistIntegration/README.md @@ -96,9 +96,9 @@ arbitrary input/output paths. | Tool | Notes | |------|-------| -| macOS with Xcode 26.0+ | This example pins Swift 6.3 / Xcode 26.4 (matches the rest of SafeDI's example CI), but SafeDI itself supports Swift 6.2 / Xcode 26.0 minimum — bump the example down if you need to test against the baseline. | +| macOS with Xcode 26.0 | Matches the rest of SafeDI's CI. | | [Tuist](https://tuist.dev) 4.x | Install via [mise](https://mise.jdx.dev). Tuist no longer publishes a Homebrew formula. | -| Swift 6.3 toolchain | Ships with Xcode 26.4. SafeDI itself supports Swift 6.2+; this example tracks the latest. | +| Swift 6.2 toolchain | Ships with Xcode 26.0 | ### Installing Tuist diff --git a/Examples/ExampleTuistIntegration/Tuist/Package.swift b/Examples/ExampleTuistIntegration/Tuist/Package.swift index 34d19dd2..311ded72 100644 --- a/Examples/ExampleTuistIntegration/Tuist/Package.swift +++ b/Examples/ExampleTuistIntegration/Tuist/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.3 +// swift-tools-version: 6.2 // // Consumed by `tuist install`, which invokes SPM to resolve this // manifest. Declaring SafeDI here gives Tuist's SPM integration a From 33597a83a2aeae8e2f19ea1580f676801f8b4ef6 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 08:10:42 -0700 Subject: [PATCH 04/12] ci: docc on Xcode 26.0 too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last Swift-compile job that wasn't on the 26.0 baseline. SafeDI's own DocC build doesn't depend on package traits — moving it down keeps the CI matrix internally consistent: every job that compiles SafeDI itself runs on Xcode 26.0; only example jobs that declare package traits stay on 26.4. --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d448e7a..4bc58065 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,8 @@ jobs: run: xcrun xcodebuild -skipMacroValidation -skipPackagePluginValidation build -scheme SafeDI-Package -destination ${{ matrix.platforms }} docc: - name: Build DocC on Xcode 26 + name: Build DocC on Xcode 26.0 + # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 permissions: contents: read @@ -85,7 +86,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer # `swift-docc-plugin`'s `--warnings-as-errors` scopes to a single # target, so swift-syntax's internal DocC noise stays out of our # strict build. That scoping isn't available via `xcodebuild From 06470513830d2570a049faaf57fc4aeecbcab8e1 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 08:13:50 -0700 Subject: [PATCH 05/12] Refine package-traits split + document publish-on-latest rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move `spm-package-integration` to Xcode 26.0. The example declares traits via `.package(traits: ["sourceBuild"])` in Package.swift, which has been supported since SE-0450 (Swift 6.1+). Only `.xcodeproj`-level traits (`traits = (…)` in the project file) need Xcode 26.4. `Example Project Integration` and `Example Multi Project Integration` declare traits in their pbxproj, so they stay on 26.4. - Lower `Examples/Example Package Integration/Package.swift` swift-tools-version 6.3 → 6.2 to match the moved CI target. - Update the example-CI block comment to reflect the precise distinction (Xcode-project traits vs Package.swift traits). - Add a comment block to publish.yml explaining why publish runs on the latest Xcode (so the prebuilt SafeDITool stays tolerant of newer swift-syntax constructs in consumer code). --- .github/workflows/ci.yml | 19 ++++++++++--------- .github/workflows/publish.yml | 7 +++++++ .../Example Package Integration/Package.swift | 2 +- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bc58065..77a31cf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,16 +96,17 @@ jobs: with: command: swift package generate-documentation --target SafeDI --warnings-as-errors - # Example CI jobs below pick their Xcode version based on whether - # the example uses package traits: Xcode 26.4 was the first Xcode - # to support package traits, so any example whose Package.swift / - # `.xcodeproj` declares them needs 26.4. Examples that only consume - # SafeDI via its default trait (no explicit `traits:` syntax) run - # on Xcode 26.0 — same baseline as the SafeDI-core jobs above. + # Example CI jobs below pick their Xcode version based on where + # they declare package traits: Xcode 26.4 was the first Xcode to + # surface package traits in `.xcodeproj` (`traits = (…)` in the + # project file), so examples that declare traits there need 26.4. + # Examples that declare traits only in `Package.swift` via + # `.package(…, traits: …)` — or don't declare traits at all — run + # on Xcode 26.0 (same baseline as the SafeDI-core jobs above). spm-package-integration: - name: Build Package Integration on Xcode 26.4 - # Uses `.package(traits: ["sourceBuild"])`. + name: Build Package Integration on Xcode 26.0 + # Declares traits in Package.swift (not the project file). runs-on: macos-26 permissions: contents: read @@ -113,7 +114,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer - name: Resolve Package Dependencies uses: ./.github/actions/retry with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 69d50af3..640600ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,13 @@ on: default: false jobs: + # Publish builds SafeDITool against the *latest* Swift toolchain, + # not the project's minimum. SafeDITool ships as a prebuilt that + # consumers run through the SafeDIGenerator plugin, so it has to + # handle source written for any Swift version downstream consumers + # are on. Building against the latest swift-syntax keeps the + # prebuilt tolerant of newer constructs; an older build would + # parse-fail on newer consumer code. build-macos: name: Build macOS runs-on: macos-26 diff --git a/Examples/Example Package Integration/Package.swift b/Examples/Example Package Integration/Package.swift index 396abfed..d7dc3f35 100644 --- a/Examples/Example Package Integration/Package.swift +++ b/Examples/Example Package Integration/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 6.3 +// swift-tools-version: 6.2 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From b662fc3ec96e7b08584cc7cef880f16186c815fd Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 08:30:45 -0700 Subject: [PATCH 06/12] ci: fix two failures on Xcode 26.0 / Swift 6.2 transition - spm-package-integration: add iOS-platform download step. macos-26 runners only ship the iOS SDK pinned to the default Xcode (26.4); switching to Xcode 26.0 means we need `xcodebuild -downloadPlatform iOS` before targeting `generic/platform=iOS`. The xcodebuild matrix job already has this pattern. - linux: switch Docker image from `swift:6.2` (noble / Ubuntu 24.04) to `swift:6.2-jammy` (Ubuntu 22.04). Noble's apt mirrors timed out during the curl install for the codecov uploader; jammy's apt repos are more stable. --- .github/workflows/ci.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77a31cf3..85dbd912 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,14 @@ jobs: uses: actions/checkout@v6 - name: Select Xcode Version run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer + - name: Download iOS Platform + # macos-26 runners only ship the iOS SDK pinned to the + # default Xcode (26.4); switching to 26.0 means we need to + # download iOS 26.0 explicitly before xcodebuild can target + # `generic/platform=iOS`. + run: | + sudo xcodebuild -runFirstLaunch + sudo xcodebuild -downloadPlatform iOS - name: Resolve Package Dependencies uses: ./.github/actions/retry with: @@ -233,7 +241,10 @@ jobs: linux: name: Build and Test on Linux runs-on: ubuntu-latest - container: swift:6.2 + # `-jammy` (Ubuntu 22.04) is more stable than the default + # `noble` tag for apt — the curl install we need for the + # codecov uploader has hit transient noble-mirror timeouts. + container: swift:6.2-jammy permissions: contents: read steps: From 3c7bcefa5acd4830a6f0c1909fad81d0c0d62171 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 08:45:01 -0700 Subject: [PATCH 07/12] ci: warm up simctl before iOS platform download MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last attempt failed during `xcodebuild -downloadPlatform iOS` itself with "Unable to connect to simulator" — the simulator service hadn't been warmed up. The xcodebuild matrix that already runs on Xcode 26.0 calls `xcrun simctl list` before `-downloadPlatform`, which seems to nudge the service. Mirror that here. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85dbd912..a1cfa965 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,9 +119,12 @@ jobs: # macos-26 runners only ship the iOS SDK pinned to the # default Xcode (26.4); switching to 26.0 means we need to # download iOS 26.0 explicitly before xcodebuild can target - # `generic/platform=iOS`. + # `generic/platform=iOS`. The `simctl list` warms up the + # simulator service — without it `-downloadPlatform` flakes + # with "Unable to connect to simulator". run: | sudo xcodebuild -runFirstLaunch + sudo xcrun simctl list sudo xcodebuild -downloadPlatform iOS - name: Resolve Package Dependencies uses: ./.github/actions/retry From 012757c88872b837faf160c3d31408782e61cf05 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 08:48:23 -0700 Subject: [PATCH 08/12] Revert Tuist CI + README to Xcode 26.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per @chatgpt-codex-connector's P1 catch on PR #292: the Tuist example's `Tuist/Package.swift` pins SafeDI `from: "2.0.0-beta-6"`, and that released tag still declares `swift-tools-version: 6.3`. Xcode 26.0 (Swift 6.2) rejects that manifest at resolve time before build can proceed. The 6.2 floor only ships in the *next* SafeDI release. Once we cut 2.0.0-rc-2 (or whatever the post-merge version is) we'll bump the Tuist example's pin and move this job to 26.0 to match the rest. For now: stay on 26.4 and call out the dependency in a job-level comment. The Tuist `Tuist/Package.swift` itself stays at `swift-tools-version: 6.2` — that's accurate to the manifest's own features (none are 6.3-only) and forward-compatible with whichever Xcode resolves it. --- .github/workflows/ci.yml | 11 ++++++++--- Examples/ExampleTuistIntegration/README.md | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1cfa965..418c5d42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,8 +182,13 @@ jobs: run: pushd Examples/ExampleMultiProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleMultiProjectIntegration; popd spm-tuist-integration: - name: Build Tuist Integration on Xcode 26.0 - # Tuist example doesn't declare package traits. + name: Build Tuist Integration on Xcode 26.4 + # Pinned to 26.4 (not 26.0) until the SafeDI dep in + # `Tuist/Package.swift` bumps to a release that ships + # `swift-tools-version: 6.2`. The current pin + # (`from: "2.0.0-beta-6"`) still declares 6.3, which Swift 6.2 + # rejects when Tuist resolves the manifest. Move to 26.0 after + # the next SafeDI release. runs-on: macos-26 permissions: contents: read @@ -195,7 +200,7 @@ jobs: - name: Checkout Repo uses: actions/checkout@v6 - name: Select Xcode Version - run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer + run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer - name: Install Tuist via mise # Keep the pinned Tuist version here in sync with the README. run: | diff --git a/Examples/ExampleTuistIntegration/README.md b/Examples/ExampleTuistIntegration/README.md index ac8192bb..39dd576d 100644 --- a/Examples/ExampleTuistIntegration/README.md +++ b/Examples/ExampleTuistIntegration/README.md @@ -96,9 +96,9 @@ arbitrary input/output paths. | Tool | Notes | |------|-------| -| macOS with Xcode 26.0 | Matches the rest of SafeDI's CI. | +| macOS with Xcode 26.4 | Matches the rest of SafeDI's CI. | | [Tuist](https://tuist.dev) 4.x | Install via [mise](https://mise.jdx.dev). Tuist no longer publishes a Homebrew formula. | -| Swift 6.2 toolchain | Ships with Xcode 26.0 | +| Swift 6.3 toolchain | Ships with Xcode 26.4 | ### Installing Tuist From 9c931ad167f49f97266fdc564ac91791c7ecde2c Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 09:20:02 -0700 Subject: [PATCH 09/12] Address PR #292 review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert all job name suffixes ("on Xcode 26.0" / "on Xcode 26.4") back to the original "on Xcode 26" — names were fine as-is. - Drop the "Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners" one-liners on `xcodebuild`, `docc`, `spm`, `bazel` jobs. Running on the lowest supported Xcode is the convention; doesn't need per-job documentation. - Trim the long example-CI block comment down to one paragraph about the .xcodeproj-traits → 26.4 split. Drop the per-job "Project file declares package traits" comments (now redundant). - spm-package-integration: switch xcodebuild destination from `generic/platform=iOS` to `platform=macOS`; drop the iOS platform download step we just added. macOS exercises the same plugin scanner code path without needing to download an SDK. - spm-tuist-integration comment: "dep" → "dependency"; tighten. - Bump Tuist example's SafeDI dep from `from: "2.0.0-beta-6"` to `from: "2.0.0-rc-1"` (in `Tuist/Package.swift`, `Package.resolved`, and the README's copy-paste snippet). - Linux: revert `swift:6.2-jammy` → `swift:6.2`. The default `:6.2` tag tracks the latest base and won't drift to a specific Ubuntu. Earlier noble-mirror apt timeout was almost certainly transient. - publish.yml comment: add a link to where the project's minimum is defined (Package.swift's `swift-tools-version`). - Bazel example README: drop the added "Toolchain: Swift 6.2 or later" prerequisite — wasn't needed. --- .github/workflows/ci.yml | 56 ++++++------------- .github/workflows/publish.yml | 14 +++-- Examples/ExampleBazelIntegration/README.md | 6 -- Examples/ExampleTuistIntegration/README.md | 4 +- .../Tuist/Package.resolved | 6 +- .../Tuist/Package.swift | 2 +- 6 files changed, 31 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 418c5d42..b11d6668 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,8 +42,7 @@ jobs: awk '/^module\(/,/^\)/' MODULE.bazel | grep -q 'version = "99.99.99-test"' xcodebuild: - name: Build with xcodebuild on Xcode 26.0 - # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. + name: Build with xcodebuild on Xcode 26 runs-on: macos-26 strategy: matrix: @@ -77,8 +76,7 @@ jobs: run: xcrun xcodebuild -skipMacroValidation -skipPackagePluginValidation build -scheme SafeDI-Package -destination ${{ matrix.platforms }} docc: - name: Build DocC on Xcode 26.0 - # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. + name: Build DocC on Xcode 26 runs-on: macos-26 permissions: contents: read @@ -96,17 +94,13 @@ jobs: with: command: swift package generate-documentation --target SafeDI --warnings-as-errors - # Example CI jobs below pick their Xcode version based on where - # they declare package traits: Xcode 26.4 was the first Xcode to - # surface package traits in `.xcodeproj` (`traits = (…)` in the - # project file), so examples that declare traits there need 26.4. - # Examples that declare traits only in `Package.swift` via - # `.package(…, traits: …)` — or don't declare traits at all — run - # on Xcode 26.0 (same baseline as the SafeDI-core jobs above). + # Example jobs that declare package traits in their .xcodeproj + # (`traits = (…)`) need Xcode 26.4 — the first Xcode that + # surfaces traits in the project file. Other examples run on the + # same baseline as the SafeDI-core jobs above. spm-package-integration: - name: Build Package Integration on Xcode 26.0 - # Declares traits in Package.swift (not the project file). + name: Build Package Integration on Xcode 26 runs-on: macos-26 permissions: contents: read @@ -115,17 +109,6 @@ jobs: uses: actions/checkout@v6 - name: Select Xcode Version run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer - - name: Download iOS Platform - # macos-26 runners only ship the iOS SDK pinned to the - # default Xcode (26.4); switching to 26.0 means we need to - # download iOS 26.0 explicitly before xcodebuild can target - # `generic/platform=iOS`. The `simctl list` warms up the - # simulator service — without it `-downloadPlatform` flakes - # with "Unable to connect to simulator". - run: | - sudo xcodebuild -runFirstLaunch - sudo xcrun simctl list - sudo xcodebuild -downloadPlatform iOS - name: Resolve Package Dependencies uses: ./.github/actions/retry with: @@ -137,11 +120,11 @@ jobs: # path would make Xcode IDE builds fail while the swift build above # still passes. - name: Build Package Integration (xcodebuild) - run: pushd "Examples/Example Package Integration"; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExamplePackageIntegration -destination "generic/platform=iOS"; popd + run: pushd "Examples/Example Package Integration"; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExamplePackageIntegration -destination "platform=macOS"; popd spm-project-integration: - name: Build Project Integration on Xcode 26.4 - # Project file declares package traits. + name: Build Project Integration on Xcode 26 + # Pinned to 26.4 — project file declares package traits. runs-on: macos-26 permissions: contents: read @@ -161,8 +144,8 @@ jobs: run: pushd Examples/ExampleProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleProjectIntegration; popd spm-multi-project-integration: - name: Build Multi Project Integration on Xcode 26.4 - # Project file declares package traits. + name: Build Multi Project Integration on Xcode 26 + # Pinned to 26.4 — project file declares package traits. runs-on: macos-26 permissions: contents: read @@ -182,13 +165,11 @@ jobs: run: pushd Examples/ExampleMultiProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleMultiProjectIntegration; popd spm-tuist-integration: - name: Build Tuist Integration on Xcode 26.4 - # Pinned to 26.4 (not 26.0) until the SafeDI dep in + name: Build Tuist Integration on Xcode 26 + # Pinned to 26.4 (not 26.0) until the SafeDI dependency in # `Tuist/Package.swift` bumps to a release that ships - # `swift-tools-version: 6.2`. The current pin - # (`from: "2.0.0-beta-6"`) still declares 6.3, which Swift 6.2 - # rejects when Tuist resolves the manifest. Move to 26.0 after - # the next SafeDI release. + # `swift-tools-version: 6.2`. The current pin still declares 6.3, + # which Swift 6.2 rejects when Tuist resolves the manifest. runs-on: macos-26 permissions: contents: read @@ -249,10 +230,7 @@ jobs: linux: name: Build and Test on Linux runs-on: ubuntu-latest - # `-jammy` (Ubuntu 22.04) is more stable than the default - # `noble` tag for apt — the curl install we need for the - # codecov uploader has hit transient noble-mirror timeouts. - container: swift:6.2-jammy + container: swift:6.2 permissions: contents: read steps: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 640600ab..9856c8ee 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,12 +19,14 @@ on: jobs: # Publish builds SafeDITool against the *latest* Swift toolchain, - # not the project's minimum. SafeDITool ships as a prebuilt that - # consumers run through the SafeDIGenerator plugin, so it has to - # handle source written for any Swift version downstream consumers - # are on. Building against the latest swift-syntax keeps the - # prebuilt tolerant of newer constructs; an older build would - # parse-fail on newer consumer code. + # not the project's minimum (declared in Package.swift's + # `swift-tools-version` at the top of + # https://github.com/dfed/SafeDI/blob/main/Package.swift). SafeDITool + # ships as a prebuilt that consumers run through the SafeDIGenerator + # plugin, so it has to handle source written for any Swift version + # downstream consumers are on. Building against the latest + # swift-syntax keeps the prebuilt tolerant of newer constructs; + # an older build would parse-fail on newer consumer code. build-macos: name: Build macOS runs-on: macos-26 diff --git a/Examples/ExampleBazelIntegration/README.md b/Examples/ExampleBazelIntegration/README.md index c637eeb6..f65d5c3a 100644 --- a/Examples/ExampleBazelIntegration/README.md +++ b/Examples/ExampleBazelIntegration/README.md @@ -10,12 +10,6 @@ system. [`rules_apple`](https://github.com/bazelbuild/rules_apple) macOS toolchain). The SafeDI rules themselves are platform-agnostic. -**Toolchain:** Swift 6.2 or later (Xcode 26.0+). SafeDI's -`Package.swift` declares `swift-tools-version: 6.2`, which is what -the Bazel Central Registry's `macos_arm64` runners ship with; -trying to consume SafeDI from a Bazel build that uses an older -Swift toolchain will fail at the swift-syntax compile step. - ## What it demonstrates - **Two `swift_library` targets** (`//Subproject:Subproject`, diff --git a/Examples/ExampleTuistIntegration/README.md b/Examples/ExampleTuistIntegration/README.md index 39dd576d..2a45475c 100644 --- a/Examples/ExampleTuistIntegration/README.md +++ b/Examples/ExampleTuistIntegration/README.md @@ -54,10 +54,10 @@ recompiles it on the next `xcodebuild`, no `tuist generate` round-trip. 2. In `Tuist/Package.swift`, depend on SafeDI so its runtime library and `SafeDIToolBinary` artifact bundle are resolved (the plugin requires - `SafeDITool >= 2.0.0-beta-6` for the `--combined-output` flag): + `SafeDITool >= 2.0.0-rc-1` for the `--combined-output` flag): ```swift - .package(url: "https://github.com/dfed/SafeDI.git", from: "2.0.0-beta-6"), + .package(url: "https://github.com/dfed/SafeDI.git", from: "2.0.0-rc-1"), ``` 3. In `Project.swift`, `import SafeDITuist` and call the helpers from each diff --git a/Examples/ExampleTuistIntegration/Tuist/Package.resolved b/Examples/ExampleTuistIntegration/Tuist/Package.resolved index f08f8766..9dc9df9e 100644 --- a/Examples/ExampleTuistIntegration/Tuist/Package.resolved +++ b/Examples/ExampleTuistIntegration/Tuist/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "9a2ba39e37a37d1d62f1851c54a858c308e03a1cff41b420f5453a288eb6e38c", + "originHash" : "59e87ad67b40e34bea3a59195f023f3d07a79bca708fbceebf501ceecd435139", "pins" : [ { "identity" : "safedi", "kind" : "remoteSourceControl", "location" : "https://github.com/dfed/SafeDI.git", "state" : { - "revision" : "f9a1738fd4cfd882d94b9417f1461dafc91d4e78", - "version" : "2.0.0-beta-6" + "revision" : "9bfb656b3b4f7ef5a1e4b6869b3bec0f6e8cce11", + "version" : "2.0.0-rc-1" } }, { diff --git a/Examples/ExampleTuistIntegration/Tuist/Package.swift b/Examples/ExampleTuistIntegration/Tuist/Package.swift index 311ded72..85aabbb9 100644 --- a/Examples/ExampleTuistIntegration/Tuist/Package.swift +++ b/Examples/ExampleTuistIntegration/Tuist/Package.swift @@ -28,6 +28,6 @@ import PackageDescription let package = Package( name: "ExampleTuistIntegration", dependencies: [ - .package(url: "https://github.com/dfed/SafeDI.git", from: "2.0.0-beta-6"), + .package(url: "https://github.com/dfed/SafeDI.git", from: "2.0.0-rc-1"), ], ) From 1ec1b018cabafa0c9af663f471cdd4cd6e939e3b Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 09:21:42 -0700 Subject: [PATCH 10/12] ci: revert remaining two job names to original MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `spm` and `bazel` jobs slipped through the previous revert pass — both still had "on Xcode 26.0" / "on Xcode 26.0" suffixes after the larger renames. Restore originals. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b11d6668..c28b32fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -198,7 +198,7 @@ jobs: run: pushd Examples/ExampleTuistIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -workspace ExampleTuistIntegration.xcworkspace -scheme ExampleTuistIntegration; popd spm: - name: Build and Test on Xcode 26.0 + name: Build and Test on Xcode 26 # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 permissions: @@ -258,7 +258,7 @@ jobs: os: linux bazel: - name: Bazel Build on Xcode 26.0 + name: Bazel Build on macOS # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 permissions: From 2e9bc0627ebe861a1b63a69747a0cfb93185908d Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 10:31:27 -0700 Subject: [PATCH 11/12] ci: address two missed review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I resolved both threads earlier without addressing them — apologies. - spm: drop the now-redundant "Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners" one-liner. The bazel job below covers the rationale; spm's pin is just convention. - bazel: keep the rationale comment but link to BCR's continuous-integration repo where the `macos_arm64` runner platform is actually defined. --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28b32fb..79504b10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,7 +199,6 @@ jobs: spm: name: Build and Test on Xcode 26 - # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. runs-on: macos-26 permissions: contents: read @@ -259,7 +258,9 @@ jobs: bazel: name: Bazel Build on macOS - # Pinned to Xcode 26.0 — matches BCR's macos_arm64 runners. + # Pinned to Xcode 26.0 — matches BCR's `macos_arm64` runner + # platform defined at + # https://github.com/bazelbuild/continuous-integration/blob/master/buildkite/bazelci.py runs-on: macos-26 permissions: contents: read From 566fb7d3cc01d8f0e42708f059d6ffa9f294b77d Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 4 May 2026 10:32:37 -0700 Subject: [PATCH 12/12] Apply suggestion from @dfed --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79504b10..5692dbc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -259,8 +259,7 @@ jobs: bazel: name: Bazel Build on macOS # Pinned to Xcode 26.0 — matches BCR's `macos_arm64` runner - # platform defined at - # https://github.com/bazelbuild/continuous-integration/blob/master/buildkite/bazelci.py + # platform defined at https://github.com/bazelbuild/continuous-integration/blob/master/buildkite/bazelci.py runs-on: macos-26 permissions: contents: read