Skip to content

chore: raise minimum supported platform, Swift, and Xcode versions - #4268

Closed
harsh62 wants to merge 7 commits into
mainfrom
worktree-bump-min-versions-xcode26
Closed

chore: raise minimum supported platform, Swift, and Xcode versions#4268
harsh62 wants to merge 7 commits into
mainfrom
worktree-bump-min-versions-xcode26

Conversation

@harsh62

@harsh62 harsh62 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Issue

Apple has required Xcode 26 and the 26 SDKs for App Store Connect uploads since April 28, 2026. The support policy in README.md states that Amplify Swift updates its minimum Swift version within 60 days of Apple's change, so this is overdue.

This also lands ahead of AWS SDK for Swift's own proposed bump (~September 2026), so we move by plan rather than being forced by an upstream dependency.

Description

Platform floors now match Apple's published Xcode 26 support matrix, and the Swift toolchain floor matches AWS SDK for Swift.

Before After
iOS 13 15
tvOS 13 15
macOS 12 12 (unchanged)
watchOS 9 9 (unchanged)
visionOS undeclared / "Preview" 1.0, GA
swift-tools-version 5.9 6.0
Minimum Xcode 16.0 26.0

Notes on the choices:

  • watchOS stays at 9. Apple's Xcode 26 floor is watchOS 8, and the AWS SDK proposal moves to 8. We are already above that floor, so lowering it would widen support rather than raise it.
  • macOS stays at 12 for the same reason.
  • visionOS was never declared in Package.swift. It was documented as "Preview" and built in CI, but resolved implicitly via iOS compatibility. It is now explicitly declared and marked GA.

Swift 6 language mode is intentionally NOT in this PR

Raising swift-tools-version to 6.0 silently flips the default language mode to Swift 6. To keep this change behavior-neutral, swiftLanguageModes: [.v5] pins the existing behavior.

Measured separately: the Swift 6 migration is ~350 genuine strict-concurrency sites (concentrated in Storage, DataStore, and Auth). That lands in a follow-up so it can be reviewed on its own.

CI

  • Runners macos-15macos-26
  • Latest Xcode 26.326.5; minimum 16.0.026.0.1
  • Removed the Xcode 16 legs and the duplicate hardcoded 16.4.0 pin that drifted independently of the central mapping
  • Simulator devices updated to those actually present on macos-26 (iPhone 17 Pro Max, Apple Watch Series 11) — the previous iPhone 16 Pro Max / Series 10 devices do not exist on that image
  • Minimum-Xcode legs run against the 26.2 runtime: the image ships 26.0 SDKs but installs no 26.0 runtime. The Xcode version is what is being validated; the runtime only needs to be at or above our deployment floors.
  • visionOS added to the minimum-supported-version matrix, which previously never exercised the platform the docs call supported
  • New advisory nightly against the xcode-27 preview image for early warning on the next Xcode. Non-blocking (continue-on-error), so it never gates a PR.

Docs

Corrected the stale Xcode 13.4 requirement in CONTRIBUTING.md (12 major versions out of date).

Semver

Treated as a minor release, not a breaking change, consistent with how AWS SDK for Swift is handling their equivalent bump.

How did you test these changes?

Locally on Xcode 26.5 (Swift 6.3.2):

  • swift build — clean, 0 errors
  • swift test0 failures
  • xcodebuild build -scheme Amplify-Package verified end-to-end for iOS, macOS, and visionOS
  • All four floors verified to compile against the Xcode 26.5 SDKs: ios15.0, tvos15.0, watchos9.0, xros1.0
  • swiftformat --lint . --swiftversion 6.0 — clean. Three test files needed reformatting because the 6.0 bump activates the preferCountWhere rule; verified they were clean at 5.9 beforehand, so CI would have failed without this.
  • swiftlint — 0 new violations in changed files (366 pre-existing warnings elsewhere are untouched)
  • All 76 workflow/action YAML files parse

tvOS and watchOS full-package builds are left to CI, which has those simulator runtimes; the local machine does not. Compiler-level verification at those floors is included above.

Documentation

  • README.md — platform table, Swift version, Xcode requirement
  • CONTRIBUTING.md — stale Xcode version
  • AGENTS.md / CLAUDE.md
  • README-combine-support.md

Checklist

  • PR description included
  • yarn lint passed (swiftformat + swiftlint equivalents run)
  • Unit tests added/updated — no new tests; this is a configuration change validated by building and testing every supported platform at the new floors
  • Documentation updated

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Apple has required Xcode 26 and the 26 SDKs for App Store Connect uploads
since April 28, 2026. Per the support policy in README.md, Amplify Swift
tracks the Swift version shipped with the minimum Xcode allowed for App
Store Connect uploads.

Platform floors now match Apple's published Xcode 26 support matrix, and
the Swift toolchain floor matches AWS SDK for Swift:

- iOS 13 -> 15
- tvOS 13 -> 15
- macOS 12 (unchanged)
- watchOS 9 (unchanged, already above Apple's floor of 8)
- visionOS: declared in Package.swift for the first time, promoted to GA
- swift-tools-version 5.9 -> 6.0
- Minimum Xcode 16.0 -> 26.0

Adopting the Swift 6 language mode is deliberately out of scope. Raising
swift-tools-version to 6.0 would otherwise flip the default language mode
to Swift 6, so `swiftLanguageModes: [.v5]` pins the existing behavior and
keeps this change behavior-neutral. The strict-concurrency migration is
tracked separately.

CI:
- Runners moved from macos-15 to macos-26
- Latest Xcode 26.3 -> 26.5; minimum 16.0.0 -> 26.0.1
- Removed the Xcode 16 legs and the duplicate hardcoded 16.4.0 pins
- Simulator devices updated to those present on macos-26 (iPhone 17 Pro
  Max, Apple Watch Series 11); minimum-Xcode legs use the 26.2 runtime
  because the image ships 26.0 SDKs but installs no 26.0 runtime
- visionOS added to the minimum-supported-version build matrix, which
  previously never exercised the platform the docs call supported
- Added an advisory, non-blocking nightly build against the Xcode 27
  preview image for early warning on the next Xcode release

Docs: corrected the stale Xcode 13.4 requirement in CONTRIBUTING.md.

Three test files were reformatted because raising swiftformat's
--swiftversion to 6.0 activates the preferCountWhere rule.
@harsh62
harsh62 requested review from a team as code owners August 7, 2026 14:22
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
@harsh62
harsh62 temporarily deployed to IntegrationTest August 7, 2026 14:23 — with GitHub Actions Inactive
The tvOS build failed with "'SFSpeechRecognitionResult' is unavailable in
tvOS". SFSpeechRecognitionResult is marked API_UNAVAILABLE(tvos), and the
CoreML predictions plugin was relying on `#if canImport(Speech)` to compile
itself out on tvOS.

As of the Xcode 26 SDKs, the tvOS Speech framework ships a module
(module.modulemap and Speech.swiftmodule), so canImport(Speech) now
succeeds on tvOS while the type itself remains unavailable. The guard
silently stopped excluding the platform.

Verified this is an SDK change and not a consequence of raising the tvOS
deployment floor: the same file fails to compile against the Xcode 26 tvOS
SDK at both tvos13.0 and tvos15.0.

Adding `&& !os(tvOS)` restores the pre-Xcode-26 behavior. Applied to every
file in the CoreML plugin rather than only the Speech types, because the
plugin's configure/reset/client-behavior files reference those types and
were previously compiled out as a unit.

Verified by building CoreMLPredictionsPlugin and AWSPredictionsPlugin for
arm64-apple-tvos15.0-simulator, and confirming macOS still compiles
CoreMLPredictionService so the plugin stays active where Speech is
genuinely available.
thisisabhash
thisisabhash previously approved these changes Aug 7, 2026
Jobs on this PR were being killed at exactly 30 minutes, scattered randomly
across schemes and platforms rather than clustered on any one target. The
job logs show no build output at all before the kill, and cleanup
terminating orphaned git and git-remote-http processes: the jobs were still
cloning dependencies, not compiling.

Root cause is the dependencies cache key. It embedded the Xcode version:

  amplify-packages-${{ steps.platform.outputs.xcode-version }}-<Package.resolved hash>

Raising the latest Xcode from 26.3 to 26.5 made every existing entry
unreachable, including via restore-keys. With no entry to restore, the
build cache restore is skipped too (it is gated on a dependencies cache
hit), so xcodebuild ran against an empty clonedSourcePackagesDirPath and
had to clone the full dependency graph (~34 packages: aws-sdk-swift,
smithy-swift, aws-crt-swift, swift-nio, ...). Dozens of jobs did that
concurrently and starved each other until they timed out.

These checkouts are just source and are not toolchain specific --
Package.resolved already pins them exactly -- so the Xcode version is
dropped from the key. Otherwise this breaks again on every Xcode bump.
Applied to all five workflows that share the key so they keep sharing it.

Timeouts raised from 30 to 45 minutes, including the nine callers that
hardcoded 30 and so overrode the reusable workflow default. A cold run
legitimately needs more than 30 minutes, and the cache is only seeded from
main by design, so PR runs must tolerate a cold start.

Note the dependencies cache is intentionally only written on main
(build_scheme.yml), so PRs stay consumers and cannot poison it.
Runs the Xcode 27 preview build on PRs and pushes to main instead of on a
nightly schedule, so breakages from the next Xcode surface on the PR that
introduces them rather than hours later on a run nobody is watching.

Every job keeps continue-on-error, so this reports without gating a merge.
The preview image runs a beta toolchain and can have constrained capacity,
which makes it unsuitable as a required check.

Renamed from nightly_xcode_preview.yml to build_xcode_preview.yml to match
the trigger, and the concurrency group now keys on the PR number like the
other PR workflows so superseded runs are cancelled.
Raising the job timeout from 30 to 45 minutes did not fix the killed jobs;
it only moved the wall. Jobs still die with no build output, and passing
jobs on the same run cluster at 28-43 minutes, right up against the new
ceiling. That is contention, not a slow step, and no timeout value fixes it.

The dependencies cache never populates. Every unit test, integration test
and build job only *restores* `amplify-packages-*`. The single step that
writes it lives in build_scheme.yml gated on `github.ref_name == 'main'`,
but the only workflow that reaches it with save_build_cache enabled is
build_amplify_swift_platforms.yml, which declares `branches-ignore: [main]`
and is otherwise only reachable from the deploy workflows that run on
release. The write was unreachable, so the cache has always been empty
(the API reports total_count 0).

The result is that all ~190 jobs on a run each clone the full dependency
graph (~34 packages) from github.com concurrently and starve each other. A
clean resolve takes about 5 minutes locally, so the 45 minutes of silence is
contention between jobs, not the resolve itself.

This adds a workflow that resolves dependencies on main and saves the cache,
so PR jobs restore it instead of cloning. It runs only when Package.resolved
or Package.swift change, and its concurrency group prevents two seeding runs
from racing the same key.

The bare git mirrors under repositories/ are pruned before saving: only
checkouts/ is needed once working copies exist, and consumers build with
-disableAutomaticPackageResolution on a cache hit. That takes the entry from
~4.3GB to ~830MB, which matters against the repository-wide 10GB budget.

Verified locally that resolving into the cache directory produces 31
checkouts, that resolution still succeeds after pruning repositories/, and
that a real consumer build (AWSPredictionsPlugin for macOS) succeeds against
the pruned directory with -disableAutomaticPackageResolution.
Two unit tests failed on the Xcode 27 preview while passing on Xcode 26
across all five platforms. Both were timing assumptions rather than real
regressions, so they are fixed at the source instead of being retried.

GraphQL subscribe/query/mutate and REST Combine tests waited on
expectations with a 0.05 second timeout. That is not enough budget on a
slower toolchain: testMixedSuccessAndErrorValues failed with "Exceeded
timeout of 0.05 seconds, with unfulfilled expectations:
receivedStateValueConnected". Raised to 0.5 seconds.

0.5 rather than something larger because 72 of these expectations are
isInverted, and an inverted expectation waits out its full timeout on the
happy path. A multi-second value would have added minutes to the suite for
no benefit.

AWSCloudWatchLoggingSessionControllerTests.testConsumeFailureSendsHubEvent
registered a Hub listener and immediately flushed logs. Hub registration is
asynchronous, so flushLogFailure could be published before the listener
attached, leaving the expectation unfulfilled until the 10 second timeout.
It now waits for registration using the existing
HubListenerTestUtilities.waitForListener helper, which is the pattern used
elsewhere in the repo, and asserts that registration succeeded.

Verified on Xcode 26: GraphQLSubscribeCombineTests 6/6 pass (including the
test that failed on 27), AWSCloudWatchLoggingPluginTests 60 pass,
InternalAWSPinpointUnitTests 59 pass, and the full AWSAPIPluginTests target
has no failures.
@harsh62

harsh62 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #4271, which contains the same version bump rebuilt cleanly off main without the unrelated CI changes that were tangled in here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants