Skip to content

Fail closed on conditional SafeDI scanning #2273

Fail closed on conditional SafeDI scanning

Fail closed on conditional SafeDI scanning #2273

Workflow file for this run

name: CI
on:
push:
branches:
- main
schedule:
# Runs at 00:00 UTC every Monday to ensure that CI does not bitrot.
- cron: '0 0 * * 1'
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
update-version-check:
name: Update Version Script Check
runs-on: macos-26
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Run update-version script
run: ./Scripts/update-version.sh 99.99.99-test abc123testchecksum456
- name: Verify URL was updated
run: grep -q 'releases/download/99.99.99-test/SafeDITool.artifactbundle.zip' Package.swift
- name: Verify checksum was updated
run: grep -q 'checksum:.*abc123testchecksum456' Package.swift
- name: Verify Plugins/Shared.swift safeDIVersion was updated
# Match the literal within 3 lines after the `safeDIVersion`
# getter declaration so a future test-string in an unrelated
# spot can't satisfy a bare file-wide grep. Block scalar because
# an unquoted inline `{` trips the YAML parser.
run: |
grep -A 2 'var safeDIVersion: String {' Plugins/Shared.swift | grep -q '"99.99.99-test"'
- name: Verify MODULE.bazel version was updated
# Scope to the top-level module() block so bazel_dep lines that
# also carry `version = "X"` can't false-positive.
run: |
awk '/^module\(/,/^\)/' MODULE.bazel | grep -q 'version = "99.99.99-test"'
xcodebuild:
name: Build with xcodebuild on Xcode 26
runs-on: macos-26
strategy:
matrix:
platforms: [
'generic/platform=ios',
'platform=macOS',
'generic/platform=tvos',
'generic/platform=watchos',
'generic/platform=visionos'
]
fail-fast: false
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer
- name: Download Platform
if: matrix.platforms != 'platform=macOS'
run: |
sudo xcodebuild -runFirstLaunch
sudo xcrun simctl list
PLATFORM=$(echo "${{ matrix.platforms }}" | sed 's|generic/platform=||')
sudo xcodebuild -downloadPlatform "$PLATFORM"
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: xcrun xcodebuild -resolvePackageDependencies -skipPackagePluginValidation -skipMacroValidation -scheme SafeDI-Package
- name: Build Framework
run: xcrun xcodebuild -skipMacroValidation -skipPackagePluginValidation build -scheme SafeDI-Package -destination ${{ matrix.platforms }}
docc:
name: Build DocC on Xcode 26
runs-on: macos-26
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
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
# docbuild`, which analyzes the whole target graph.
- name: Build DocC
uses: ./.github/actions/retry
with:
command: swift package generate-documentation --target SafeDI --warnings-as-errors
# 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
runs-on: macos-26
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: xcrun swift package resolve --package-path "Examples/Example Package Integration"
- name: Build Package Integration (swift build)
run: xcrun swift build --package-path "Examples/Example Package Integration"
# Also build via xcodebuild — it exercises the plugin's in-process
# PluginScanner fallback, which `swift build` doesn't. A bug in that
# 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 "platform=macOS"; popd
spm-project-integration:
name: Build Project Integration on Xcode 26
# Pinned to 26.4 — project file declares package traits.
runs-on: macos-26
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer
- name: Replace 'main' branch with the current branch
if: github.event.pull_request.head.repo.full_name == github.repository # Only do this if the branch is from our repo.
run: sed -i '' "s#branch = main;#branch = ${{ github.head_ref || github.ref_name }};#" "Examples/ExampleProjectIntegration/ExampleProjectIntegration.xcodeproj/project.pbxproj"
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: pushd Examples/ExampleProjectIntegration && xcrun xcodebuild -resolvePackageDependencies -skipPackagePluginValidation -skipMacroValidation -scheme ExampleProjectIntegration && popd
- name: Build Project Integration
run: pushd Examples/ExampleProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleProjectIntegration; popd
spm-multi-project-integration:
name: Build Multi Project Integration on Xcode 26
# Pinned to 26.4 — project file declares package traits.
runs-on: macos-26
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
run: sudo xcode-select --switch /Applications/Xcode_26.4.app/Contents/Developer
- name: Replace 'main' branch with the current branch
if: github.event.pull_request.head.repo.full_name == github.repository # Only do this if the branch is from our repo.
run: sed -i '' "s#branch = main;#branch = ${{ github.head_ref || github.ref_name }};#" "Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration.xcodeproj/project.pbxproj"
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: pushd Examples/ExampleMultiProjectIntegration && xcrun xcodebuild -resolvePackageDependencies -skipPackagePluginValidation -skipMacroValidation -scheme ExampleMultiProjectIntegration && popd
- name: Build Project Integration
run: pushd Examples/ExampleMultiProjectIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -scheme ExampleMultiProjectIntegration; popd
spm-tuist-integration:
name: Build Tuist Integration on Xcode 26
runs-on: macos-26
permissions:
contents: read
# Required so mise / tuist install don't hit anonymous GitHub API
# rate limits when resolving release tags and artifact bundles.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
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: |
curl -fsSL https://mise.run | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "$HOME/.local/share/mise/shims" >> "$GITHUB_PATH"
"$HOME/.local/bin/mise" install -y tuist@4.183.0
"$HOME/.local/bin/mise" use -g tuist@4.183.0
- name: Tuist Install
run: pushd Examples/ExampleTuistIntegration && tuist install && popd
- name: Tuist Generate
run: pushd Examples/ExampleTuistIntegration && tuist generate --no-open && popd
- name: Build Tuist Integration
run: pushd Examples/ExampleTuistIntegration; xcrun xcodebuild build -skipPackagePluginValidation -skipMacroValidation -workspace ExampleTuistIntegration.xcworkspace -scheme ExampleTuistIntegration; popd
spm:
name: Build and Test on Xcode 26
runs-on: macos-26
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: xcrun swift package resolve
- name: Build and Test Framework
run: |
for i in {1..5}; do # Run tests a few times to ensure code-gen is stable.
xcrun swift test -c release --enable-code-coverage -Xswiftc -enable-testing --traits sourceBuild
done
- name: Prepare Coverage Reports
run: ./Scripts/prepare-coverage-reports.sh
- name: Upload Coverage Reports
if: success()
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
verbose: true
os: macos
linux:
name: Build and Test on Linux
runs-on: ubuntu-latest
container: swift:6.2
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: swift package resolve
- name: Build and Test Framework
run: swift test -c release --enable-code-coverage -Xswiftc -enable-testing --traits sourceBuild
- name: Install curl for Codecov
run: |
apt-get update
apt-get install -y --no-install-recommends curl ca-certificates
- name: Prepare Coverage Reports
run: |
llvm-cov export -format="lcov" .build/x86_64-unknown-linux-gnu/release/SafeDIPackageTests.xctest -instr-profile .build/x86_64-unknown-linux-gnu/release/codecov/default.profdata > coverage.lcov
- name: Upload Coverage Reports
if: success()
uses: codecov/codecov-action@v5
with:
fail_ci_if_error: true
verbose: true
os: linux
bazel:
name: Bazel Build on macOS
# General bazel sanity check on every PR. We can't perfectly
# mirror BCR's presubmit (https://registry.bazel.build/modules/safedi):
# BazelCI's BuildKite infrastructure is closed to non-`bazelbuild`
# projects (https://github.com/bazelbuild/continuous-integration/blob/main/.github/ISSUE_TEMPLATE/adding-your-project-to-bazel-ci.md),
# and any GH Actions runner we pick differs from BCR's image in
# ways we can't fully control (CC toolchain auto-detection,
# SDK-vs-runner-OS gap, exact Bazel point release). So treat BCR
# as the canonical macOS-on-older-OS check; this job catches
# general regressions before we publish.
#
# Keep `build_targets` aligned with `.bcr/presubmit.yml` so a
# divergence is visible in review. If you change BCR's task list,
# update this job too.
#
# 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
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Select Xcode Version
run: sudo xcode-select --switch /Applications/Xcode_26.0.app/Contents/Developer
# Wrapping `bazelisk build` in retry — not just dependency
# resolution, like the SPM jobs above — because Bazel resolves
# external repos lazily during the build itself. There's no
# equivalent to `swift package resolve` we could isolate; a
# transient HTTP failure (e.g. on a `rules_swift` transitive
# like `MobileNativeFoundation/index-import`) surfaces as a
# build failure. Bazel caches successful downloads, so a retry
# after a flake is cheap.
#
# Each command runs in a subshell so a `cd` for the example
# build doesn't leak its cwd change into subsequent retries.
- name: Build SafeDI targets (mirrors BCR `verify_safedi_build`)
uses: ./.github/actions/retry
with:
# Kept on one line — the retry composite action splices
# `${{ inputs.command }}` directly into a bash `for` loop
# and tacks ` && break` onto the same source line, so a
# multi-line YAML command leaves `&& break` orphaned.
command: bazelisk build //Sources/SafeDI:SafeDI //Sources/SafeDICore:SafeDICore //Sources/SafeDIMacros:SafeDIMacros //Sources/SafeDITool:SafeDITool
- name: Build downstream example (mirrors BCR `bcr_test_module.build_example`)
uses: ./.github/actions/retry
with:
command: (cd Examples/ExampleBazelIntegration && bazelisk build //Subproject:Subproject //ExampleBazelIntegration:ExampleBazelIntegration)
readme-validation:
name: Check Markdown links
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Link Checker
uses: AlexanderDokuchaev/md-dead-link-check@d5a37e0b14e5918605d22b34562532762ccb2e47 # v1.2.0
lint-swift:
name: Lint Swift
runs-on: ubuntu-latest
container: swift:6.3
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v6
- name: Resolve Package Dependencies
uses: ./.github/actions/retry
with:
command: swift package resolve --package-path CLI
- name: Lint Swift
run: swift run --package-path CLI swiftformat . --lint