From b2cc424e4d026894d225bd530b397db749a3b8ea Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Tue, 27 Oct 2026 12:21:43 -0400 Subject: [PATCH] Add the migration guide beside the architecture document The architecture document describes the unified workflows but says nothing about moving a repository onto them, and the README documents only `swift_package_test.yml`, so a maintainer had no route from either to the inputs that were renamed, the checks that are now on by default, or the NIO workflows each call replaces. `migration.md` covers a move from `swift_package_test.yml` and from the `apple/swift-nio` workflows, with the input mapping, worked examples, and the customization paths. The architecture document and the README link to it, and it links back to the architecture document for the version labels, the matrix definition and the layers. --- .github/workflows/architecture.md | 4 +- .github/workflows/migration.md | 430 ++++++++++++++++++++++++++++++ README.md | 2 + 3 files changed, 435 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/migration.md diff --git a/.github/workflows/architecture.md b/.github/workflows/architecture.md index 0760c290..6c25fa1b 100644 --- a/.github/workflows/architecture.md +++ b/.github/workflows/architecture.md @@ -4,6 +4,8 @@ The Swift Package Test workflow is intended to give maintainers of open-source Swift packages a low-friction way to give contributors confidence in the code they write. This means making testing against a range of Swift versions and platforms as simple as possible, and making the results as accessible as possible. +To move an existing repository onto these workflows, see [the migration guide](migration.md). + The following key features are treated as requirements by the unified architecture. ### 1. GitHub-release-based versioning @@ -20,7 +22,7 @@ Some jobs rely on scripts to execute their functionality. This is often preferre ### 4. Custom matrix builds -Some packages need additional checks, such as their own integration tests or a custom script. Those checks often need the same matrix of builds that the recommended test workflow, `package_test.yml`, uses. Hence, a unified solution should offer lower level primitives that can execute a matrix. Furthermore, it should provide a workflow with a matrix that is already configured with the recommended Swift versions and platforms that just executes a command across them. +Some packages need additional checks, such as their own integration tests or a custom script. Those checks often need the same matrix of builds that the recommended test workflow, `package_test.yml`, uses. Hence, a unified solution should offer lower level primitives that can execute a matrix. Furthermore, it should provide a workflow with a matrix that is already configured with the recommended Swift versions and platforms that just executes a command across them. The [migration guide](migration.md#customization-calling-execute_matrixyml-directly) has worked examples. ### 5. Detect minimum version diff --git a/.github/workflows/migration.md b/.github/workflows/migration.md new file mode 100644 index 00000000..61491682 --- /dev/null +++ b/.github/workflows/migration.md @@ -0,0 +1,430 @@ +# Unified workflows migration guide + +This guide lays out how to migrate your existing GitHub Actions jobs from the existing `swiftlang/github-workflows` and `apple/swift-nio` workflows. Mostly this should be relatively straightforward as the changes are mostly additive, but some behavior and names have changes. + +Where to start: + +* If you are coming from `swiftlang/github-workflows` and `swift_package_test.yml`: read *Before your first run*, then [Migrating from `swiftlang/github-workflows`](#migrating-from-swiftlanggithub-workflows-swift_package_testyml). +* If you are coming from `apple/swift-nio`'s workflows: read *Before your first run*, then [Migrating from `apple/swift-nio` workflows](#migrating-from-appleswift-nio-workflows). +* Needing a matrix that's not a plain product of versions and operating systems: read [Calling `execute_matrix.yml` directly](#calling-execute_matrixyml-directly). + +## Before your first run + +Four things change what a repository's CI does, whichever workflow it comes from. + +**The standard checks are on by default.** The unified workflows enable a larger set of jobs and platforms by default, reflecting the expected supported Swift platforms and features. + +| Check | Turn it off with | +|---|---| +| Linux tests | `enable_linux: false` | +| Windows tests, run natively | `enable_windows: false` | +| macOS tests | `enable_macos: false` | +| macOS with a swiftly toolchain | `enable_macos_swiftly: false` | +| Static Linux SDK build | `enable_linux_static_sdk_build: false` | +| Wasm SDK build | `enable_wasm_sdk_build: false` | +| Embedded Wasm SDK build | `enable_embedded_wasm_sdk_build: false` | +| Android SDK build and emulator tests | `enable_android_sdk_build: false`; to drop only the emulator, `android_emulator_test_triples: '[]'` | +| Cxx interop check | `enable_cxx_interop: false` | +| FreeBSD tests | `enable_freebsd: false` | + +**Job names change.** Several job names change in their details, especially if you use the new relative naming, such as `Linux Swift current-1`, `Release-Build Linux Swift 6.3`, or `Android SDK Swift current NDK r30 x86_64-android28`. + +This likely requires changes in repository branch protection rules or required checks. + +**Commands and setup commands behave differently.** A build command is no longer unescaped once by the shell, and a setup command has to work with or without `sudo`. See *Commands are no longer shell-unescaped* and *Setup commands run privileged or not*. + +**A release pins the scripts as well as the workflow.** `uses: swiftlang/github-workflows/.github/workflows/package_test.yml@` runs that release's workflow YAML *and the scripts* it ships with. Previously the scripts floated on `main`, which was a bug. + +**Several workflows now default to running outside of Docker.** Previously many jobs ran in Docker containers on the GitHub runners (all in the SwiftNIO case); to speed up workflows they mostly default to running natively on the GitHub runner. + +## Migrating from `swiftlang/github-workflows` (`swift_package_test.yml`) + +### Switch the workflow name + +Replace: + +```yaml +uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11 +``` + +With: + +```yaml +uses: swiftlang/github-workflows/.github/workflows/package_test.yml@ +``` + + Most inputs have the same names, and the mapping table below lists the ones that don't. + +### What's different + +#### Changes that alter what runs + +1. **Windows now runs by default, natively.** +2. **The standard checks are on by default.** +3. **The Android emulator tests run in their own jobs.** You now specify the Android SDK triples to build and the ones to test with an emulator as separate entries. +4. **The Cxx interop check runs on every Linux version.** +5. **SDK builds no longer follow the Linux distribution.** The static-linux, wasm, and embedded-wasm SDK builds ran in a container tagged for each distribution in the Linux OS list. All four SDK builds now run on the GitHub-hosted Ubuntu runner whatever that list says, so a package pinned to `amazonlinux2` gets its tests on amazonlinux2 and its SDK builds on Ubuntu. +6. **A Windows container runs its commands under `cmd`.** `Invoke-Program` and the rest of PowerShell don't exist there. +7. **Workflows detect a package's minimum version.** The matrix infrastructure detects the minimum `swift-tools-version` and won't generate jobs below that version, since those jobs would fail. + +#### Changes that alter what you write + +7. **No `exclude_*` arrays.** The new system uses **explicit version lists**: pass the versions you want. +8. **Swift versions are relative by default**. Defaults are `["current-2", "current-1", "current", "nightly-release", "nightly-main"]` for both Linux and Windows, `["current-2", "current-1", "current"]` for macOS, and `["current", "nightly-release", "nightly-main"]` for the SDK builds, where `current` is the release the workflow version targets (see [Swift version labels](architecture.md#swift-version-labels)). A job takes its name from the label, so `Linux Swift current-1` keeps its name when a new release moves the versions under it. Write versions out (`["6.2", "6.3"]`) to name them literally. +9. **macOS uses Swift versions by default.** Instead of `macos_xcode_versions: '["16.3", "16.4", "26.0"]'`, the default is `macos_swift_versions: '["current-2", "current-1", "current"]'`. The runners have symlinks (`Xcode_swift_6.3.app`) that resolve the correct Xcode. You can still pass `macos_xcode_versions` if you prefer Xcode version numbers. +10. **`nightly-release` replaces `nightly-6.3`.** Use `nightly-release` in version lists. It resolves to the current/upcoming release branch nightly. When Swift cuts a new release branch, no workflow file needs to change. +11. **Per-version overrides are a YAML (or JSON) object.** Instead of `linux_6_1_arguments_override`, `linux_6_2_arguments_override`, etc., give the arguments every version takes under `default`, and a key for each version that differs. The keys use the same spelling as the version list: relative labels for the default list, versions if you wrote them out. + + ```yaml + linux_version_overrides: | + default: -Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error + nightly-main: --explicit-target-dependency-import-check error + ``` + +12. **Timeouts are one input.** `job_timeout` bounds each job and defaults to 90 minutes (60 for `benchmarks.yml`). `windows_job_timeout` and `freebsd_job_timeout` raise the limit for those two platforms, where installing a toolchain takes longer; `0` uses `job_timeout`. A hand-written matrix can set `timeout_minutes` on an entry. +13. **You can specify a `package_path`.** To support packages which aren't at a repository's root. + + + + +### Labeled commands + +In addition to specifying one command for a platform, say a `linux_command`, the input now also takes a map of label to command, which produces one job per toolchain *and* command, so a debug and a release pass come from one call rather than two: + +```yaml +linux_command: | + Debug-Test: swift test + Release-Build: swift build -c release +``` + +That gives `Debug-Test Linux Swift 6.3` and `Release-Build Linux Swift 6.3`. The label leads the job name, and only when the call configures more than one command, so a single labeled command leaves existing names alone. A label is one word of letters, digits, `_`, `.`, and `-`, starting with a letter or digit. A label with a space makes the whole value read as one shell command. A label may narrow itself to part of the version list: + +```yaml +linux_command: | + Debug-Test: swift test + Release-Build: + command: swift build -c release + versions: ["6.3"] +``` + + +### Commands are no longer shell-unescaped once on the way in + +The old SwiftNIO workflow interpolated a build command into a `run:` block, so the shell parsed it before the script saw it. A command referring to a variable it wanted expanded *later* had to escape the dollar: + +```yaml +linux_static_sdk_build_command: | + for triple in aarch64-swift-linux-musl x86_64-swift-linux-musl ; do + swift build --swift-sdk "\$triple" # old: one unescape happens on the way in + done +``` + +The unified workflows carry the command through the matrix as data, so there is no such step. Write the variable plainly, under the renamed input: + +```yaml +linux_static_sdk_command: | + for triple in aarch64-swift-linux-musl x86_64-swift-linux-musl ; do + swift build --swift-sdk "$triple" # new + done +``` + + +### Example migration of swift-format + +**Before:** + +```yaml +tests: + uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@0.0.11 + with: + enable_cross_pr_testing: true + linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}]" + windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]" +``` + +**After:** + +```yaml +tests: + uses: swiftlang/github-workflows/.github/workflows/package_test.yml@ + with: + enable_cross_pr_testing: true + # The package runs none of the other standard checks. + enable_macos: false + enable_macos_swiftly: false + enable_linux_static_sdk_build: false + enable_wasm_sdk_build: false + enable_embedded_wasm_sdk_build: false + enable_android_sdk_build: false + enable_cxx_interop: false + enable_freebsd: false +``` + +### Apple platforms other than macOS + +`enable_ios_checks` no longer exists. It ran `swift build --build-tests --sdk … --triple arm64-apple-ios` on a macOS runner of its own: it compiled the package and its tests for iOS but never ran anything. + +The same check now rides on the macOS entries, so it costs no extra runner: + +```yaml +# macOS is on by default +xcode_scheme: my-package-Package +xcode_targets: "[iOS]" +``` + +`xcode_targets` names the platforms to drive through `xcodebuild`. The platforms are `macOS`, `Catalyst`, `iOS`, `watchOS`, `tvOS`, and `visionOS`. A bare list asks for each named platform with every setting at its default, which is to build and not test. The map form sets them per target: + +```yaml +xcode_targets: | + iOS: {build: true, test: true} + watchOS: {test: true} + macOS: {} +``` + +The build action is `build-for-testing`, so this still type-checks test code, as `--build-tests` did. Adding `test: true` also runs the tests on a simulator, which `enable_ios_checks` couldn't do at all. + +Some consequences: + +* A scheme is now required. For a package without an Xcode project it's the SwiftPM-generated `-Package`. +* The per-Xcode-version iOS runner no longer exists: whichever macOS entries exist build iOS, so `ios_host_xcode_versions`, `ios_build_command`, and `ios_pre_build_command` no longer exist either. For iOS isolated on its own runner, use `toolchain_matrix.yml` with `execute_matrix.yml` and your own command. + +* * * + +## Migrating from `apple/swift-nio` workflows + +### Quick start + +Replace multiple workflow calls: + +```yaml +unit-tests: + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main +benchmarks: + uses: apple/swift-nio/.github/workflows/benchmarks.yml@main +cxx-interop: + uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main +release-builds: + uses: apple/swift-nio/.github/workflows/release_builds.yml@main +macos-tests: + uses: apple/swift-nio/.github/workflows/macos_tests.yml@main +static-sdk: + uses: apple/swift-nio/.github/workflows/static_sdk.yml@main +``` + +With one or two calls: + +```yaml +package-tests: + uses: swiftlang/github-workflows/.github/workflows/package_test.yml@ + with: + linux_command: | + Debug-Test: swift test + Release-Build: swift build -c release + +benchmarks: + uses: swiftlang/github-workflows/.github/workflows/benchmarks.yml@ + with: + benchmark_package_paths: '["Benchmarks"]' +``` + +The Cxx interop check, the static SDK build and macOS are on by default, so they need no input. The release-configuration build is a second labeled command, and `release_build.yml` no longer exists. + +1. **Per-version overrides.** SwiftNIO used `linux_6_1_arguments_override`, `linux_6_2_arguments_override`, etc. The unified system uses a single YAML/JSON object and a key for each version that differs. + + SwiftNIO's `pull_request.yml` passed the same arguments to five versions and different ones to `nightly-main`. It becomes one object, with the shared arguments under `default` and the one exception under its own key. `nightly_next` is now `nightly-release`, which `default` covers: + + ```yaml + package-tests: + uses: swiftlang/github-workflows/.github/workflows/package_test.yml@ + with: + linux_version_overrides: | + default: -Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error + nightly-main: --explicit-target-dependency-import-check error + ``` +2. **Version enables → version lists.** Instead of `linux_5_9_enabled: false`, leave 5.9 out of `linux_swift_versions`. Or rely on minimum version detection. A package that supports more releases than the default window of `current-2` to `current` writes its versions out, for example `["6.1", "6.2", "6.3", "6.4", "nightly-release", "nightly-main"]`, and its overrides then key on those versions. A YAML anchor states the list once for the calls in a file. +3. **macOS tests use Swift versions.** Instead of `swift_6_1_enabled: true` / `xcode_16_3_enabled: false`, pass: + + ``` + macos_swift_versions: '["6.1", "6.2", "6.3"]' + ``` + + Runners have symlinks that resolve Swift versions to Xcode installations. +4. **Runner pool is an input.** Instead of baking the pool into `macos_tests.yml`, pass `macos_runner_pool: general` (or `nightly` for main-branch CI). It's empty by default. +5. **xcodebuild platform targets.** Instead of `ios_xcode_test_enabled: true` on `macos_tests.yml`, pass: + + ``` + xcode_scheme: my-package-Package + xcode_targets: | + iOS: {test: true} + ``` + + `macOS`, `Catalyst`, `watchOS`, `tvOS`, and `visionOS` go in the same map, and the destinations match the ones `macos_tests.yml` uses. The build action is `build-for-testing` rather than `build`. +6. **Benchmarks are a separate workflow.** Now called from `swiftlang/github-workflows` instead of `apple/swift-nio`. macOS is off by default, so pass `enable_macos: true` for the macOS benchmarks. +7. **Integration tests pair `toolchain_matrix.yml` with your command.** See *An example of custom integration tests* below for the whole job pair. +8. **Container config moves under `swift_build.container`.** + +### Build and test as separate steps + +SwiftNIO's Darwin job runs `swift build --build-tests` and then `swift test`, with separate argument overrides for each. One matrix entry runs one command, so the two become one command: + +```yaml + macos_command: | + swift build --build-tests + swift test +``` + +To split them out on parallel runners rather than running in serial, make them two labeled commands instead: + +```yaml + macos_command: | + build: swift build --build-tests + test: swift test +``` + +### Default Swift versions + +By default the matrix produces jobs for and named `["current-2", "current-1", "current", "nightly-release", "nightly-main"]`, these are then mapped using a definition in the `swiftlang/github-workflows` repository, so **each tag pins them**. To adopt a new Swift version you need to adopt a new tag of the upstream workflows so each repository controls the timing. To opt out of this relative naming, you must specify the versions you want explicitly. + +### Version-keyed directories (SwiftNIO and SwiftNIO HTTP/2) + +`SWIFT_VERSION` carries the version label, and anything that stores data per version carries it in its name. SwiftNIO calls the release-branch nightly `nightly-next`. Here it's `nightly-release`, so rename every file or directory named for it once, during migration. + +`SWIFT_VERSION` carries the version a label resolves to, so with the default lists the job on `current-1` reads the files named `6.3` when `current` is 6.4. A repository that keeps thresholds for more releases than the default window holds, as SwiftNIO does, writes those versions out. + +### Example migration of SwiftNIO + +**Before:** separate calls to SwiftNIO's own workflows, one for each row of the SwiftNIO workflow mapping table. + +**After:** + +```yaml +package-tests: + uses: swiftlang/github-workflows/.github/workflows/package_test.yml@ + with: + # The four currently-supported versions explicitly-named. + linux_swift_versions: &linux_swift_versions '["6.1", "6.2", "6.3", "6.4", "nightly-release", "nightly-main"]' + + linux_version_overrides: | + default: -Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error + nightly-main: --explicit-target-dependency-import-check error + linux_command: | + Debug-Test: swift test + Release-Build: swift build -c release + + macos_runner_pool: general + + wasm_sdk_command: "swift build --target NIOCore" + + # The Android SDK builds both architectures and runs no emulator. + android_sdk_build_triples: '["aarch64-unknown-linux-android28", "x86_64-unknown-linux-android28"]' + android_emulator_test_triples: '[]' + + # Not run by this package. + enable_windows: false + enable_macos_swiftly: false + enable_embedded_wasm_sdk_build: false + enable_freebsd: false + +benchmarks: + uses: swiftlang/github-workflows/.github/workflows/benchmarks.yml@ + with: + benchmark_package_paths: '["Benchmarks"]' + linux_swift_versions: *linux_swift_versions +``` + +* * * + +## Calling `execute_matrix.yml` directly + +For full control over the matrix, bypass `package_test.yml` and call `execute_matrix.yml` with a hand-crafted or script-generated YAML matrix. The entry schema is under [The matrix definition](architecture.md#the-matrix-definition). + +### An example of custom integration tests + +Use `toolchain_matrix.yml` for the toolchains and `execute_matrix.yml` for the command. Neither needs the scripts checked out: both take them from the release you pin. + +```yaml +jobs: + toolchains: + uses: swiftlang/github-workflows/.github/workflows/toolchain_matrix.yml@ + with: + linux_swift_versions: '["6.3", "nightly-release"]' + linux_env_vars: '{"MY_VAR": "value"}' + + tests: + needs: toolchains + uses: swiftlang/github-workflows/.github/workflows/execute_matrix.yml@ + with: + name: "My custom tests" + matrix_yaml_string: ${{ needs.toolchains.outputs.matrix_yaml }} + setup_command: | + if command -v sudo >/dev/null 2>&1; then + sudo apt-get update -y -q && sudo apt-get install -y -q lsof + else + apt-get update -y -q && apt-get install -y -q lsof + fi + command: "./scripts/my_tests.sh" +``` + +An entry that carries its own `command` keeps it, so you can mix generated and hand-written entries. + +### A matrix that's not a version × OS product + +`package_test.yml` generates a rectangular matrix: every version in the list, for every OS in the list. Three shapes need the lower layers instead (`toolchain_matrix.yml` and `execute_matrix.yml`), because the toolchain matrix is plain YAML and you can reshape it with `yq` before it runs. + +**A matrix with a hole in it.** Amazon Linux 2023 images exist only for Swift 6.3 and later, so the full product would ask for images that don't exist: + +```yaml +jobs: + toolchains: + uses: swiftlang/github-workflows/.github/workflows/toolchain_matrix.yml@ + with: + linux_os: '["noble", "amazonlinux2023"]' + + filter: + needs: toolchains + runs-on: ubuntu-24.04 + outputs: + matrix_yaml: ${{ steps.filter.outputs.matrix_yaml }} + steps: + - id: filter + env: + MATRIX: ${{ needs.toolchains.outputs.matrix_yaml }} + run: | + # Keep every entry except the pre-6.3 ones on amazonlinux2023. + matrix=$(echo "$MATRIX" | yq '.config |= map( + select(((.swift_build.container.image // "") | test("amazonlinux2023") | not) + or (.swift_build.swift_version | test("^6\\.[3-9]|^nightly"))))') + { + echo "matrix_yaml<> "$GITHUB_OUTPUT" + + tests: + needs: filter + uses: swiftlang/github-workflows/.github/workflows/execute_matrix.yml@ + with: + name: "Tests" + matrix_yaml_string: ${{ needs.filter.outputs.matrix_yaml }} + command: "swift test" +``` + +The other two shapes are the same `filter` job with a different expression. + +**An image the tag scheme can't express**, such as one from a private registry. The `select` keeps it off macOS entries, which have no `swift_build` to graft it onto, and off Windows, whose images aren't the same ones: + +```yaml + matrix=$(echo "$MATRIX" | yq '.config |= map( + select(.platform == "Linux").swift_build.container.image = + "registry.example.com/swift:6.3-custom")') +``` + +**Native and containerized in one run.** `linux_use_docker` is all or nothing, so add the container to the entries that want it and leave the rest native. Every entry starts out native, including `nightly-release`, so the `select` picks out whichever ones the container is for: + +```yaml + matrix=$(echo "$MATRIX" | yq '.config |= map( + select(.platform == "Linux" and .swift_build.swift_version == "6.3") + .swift_build.container.image = "swift:6.3-noble")') +``` + +Or write the matrix yourself and pass it straight to `execute_matrix.yml`. diff --git a/README.md b/README.md index a756df0f..0330a5aa 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ jobs: ### Testing +`package_test.yml` runs the tests across Swift versions and platforms from a matrix it generates, and replaces `swift_package_test.yml` and the per-job workflows in `apple/swift-nio`. [The architecture document](.github/workflows/architecture.md) describes it, and [the migration guide](.github/workflows/migration.md) says how to move to it. The rest of this section describes `swift_package_test.yml`. + To enable pull request testing for all supported Swift versions (5.9, 5.10, 6.0, 6.1, 6.2, nightly, and nightly-6.3) on Linux and Windows, add the following code example in `.github/workflows/pull_request.yml`: