From 939c9bbae4a7003c0c69aeb66a79dd7265cce2db Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 1 Oct 2026 10:54:00 -0400 Subject: [PATCH 1/2] ci: fail PRs whose Package.resolved is out of date Xcode Cloud resolves from the committed workspace Package.resolved and won't update it, so a stale pin fails every deploy while local Xcode re-resolves silently. The flipcash2 0.14.x bump broke Deploy Flipcash this way. Resolve with -disableAutomaticPackageResolution, which applies the same restriction, on PRs that touch a Package.swift, Package.resolved, or the project file. --- .github/workflows/package-resolved.yml | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/package-resolved.yml diff --git a/.github/workflows/package-resolved.yml b/.github/workflows/package-resolved.yml new file mode 100644 index 000000000..e70fc85a9 --- /dev/null +++ b/.github/workflows/package-resolved.yml @@ -0,0 +1,64 @@ +name: Package.resolved check + +# Xcode Cloud resolves packages from the committed workspace Package.resolved and +# refuses to update it, so a Package.swift requirement the file no longer satisfies +# fails every deploy at resolution, while local Xcode re-resolves silently. That is +# how the flipcash2 0.14.x bump broke Deploy Flipcash. This resolves with the same +# restriction, so the mismatch fails the PR instead. A stale file fails in under a +# minute, before anything is cloned. + +on: + pull_request: + branches: [main] + paths: + - '**/Package.swift' + - '**/Package.resolved' + - 'Code.xcodeproj/project.pbxproj' + - '.github/workflows/package-resolved.yml' + push: + branches: [main] + paths: + - '**/Package.swift' + - '**/Package.resolved' + - 'Code.xcodeproj/project.pbxproj' + - '.github/workflows/package-resolved.yml' + +concurrency: + group: package-resolved-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + resolved: + name: Package.resolved is current + runs-on: macos-15 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + # FlipcashCore is swift-tools 6.1, which the image's default Xcode may + # predate. Same selection as vectors.yml. + - name: Select the newest installed Xcode + run: | + set -euo pipefail + XCODE=$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort -V | tail -1 || true) + if [ -n "$XCODE" ]; then + sudo xcode-select -s "$XCODE" + fi + xcodebuild -version + + - name: Resolve without updating Package.resolved + run: | + set -euo pipefail + if ! xcodebuild -resolvePackageDependencies \ + -project Code.xcodeproj \ + -scheme Flipcash \ + -disableAutomaticPackageResolution; then + echo "::error file=Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved::Package.resolved does not satisfy the Package.swift requirements. Run 'xcodebuild -resolvePackageDependencies -project Code.xcodeproj -scheme Flipcash' and commit the updated file." + exit 1 + fi + + # Resolution can succeed while still rewriting a pin, which Xcode Cloud + # would see as a change it is not allowed to make. + - name: Package.resolved is unchanged + run: | + git diff --exit-code -- Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved From 6b33f6da012ad154f31ce9082c9bb054164efdcd Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 1 Oct 2026 10:59:44 -0400 Subject: [PATCH 2/2] ci: run the Package.resolved check on every PR so it can be required A workflow-level paths filter leaves PRs that don't match waiting on a required check that never reports. Move the filter into a cheap ubuntu job and skip the macOS resolve job when no package file changed; GitHub counts a job skipped by its if: as passing. --- .github/workflows/package-resolved.yml | 45 ++++++++++++++++++++------ 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/.github/workflows/package-resolved.yml b/.github/workflows/package-resolved.yml index e70fc85a9..0c19be169 100644 --- a/.github/workflows/package-resolved.yml +++ b/.github/workflows/package-resolved.yml @@ -6,30 +6,55 @@ name: Package.resolved check # how the flipcash2 0.14.x bump broke Deploy Flipcash. This resolves with the same # restriction, so the mismatch fails the PR instead. A stale file fails in under a # minute, before anything is cloned. +# +# "Package.resolved is current" is a required check on main, so the workflow runs +# on every PR: a workflow-level `paths:` filter would leave PRs that skip it +# waiting on a check that never reports. The cheap `changes` job decides instead, +# and a job skipped by its `if:` counts as passing. on: pull_request: branches: [main] - paths: - - '**/Package.swift' - - '**/Package.resolved' - - 'Code.xcodeproj/project.pbxproj' - - '.github/workflows/package-resolved.yml' push: branches: [main] - paths: - - '**/Package.swift' - - '**/Package.resolved' - - 'Code.xcodeproj/project.pbxproj' - - '.github/workflows/package-resolved.yml' concurrency: group: package-resolved-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: + changes: + name: Package files changed + runs-on: ubuntu-latest + outputs: + packages: ${{ steps.diff.outputs.packages }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Diff against the base + id: diff + env: + BASE: ${{ github.event.pull_request.base.sha || github.event.before }} + run: | + set -euo pipefail + # A new branch push has an all-zero `before`, and a base that is not + # reachable can't be diffed; check rather than guess in both cases. + if [ -z "$BASE" ] || ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then + echo "packages=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if git diff --name-only "$BASE" HEAD | grep -qE '(^|/)Package\.(swift|resolved)$|^Code\.xcodeproj/project\.pbxproj$|^\.github/workflows/package-resolved\.yml$'; then + echo "packages=true" >> "$GITHUB_OUTPUT" + else + echo "packages=false" >> "$GITHUB_OUTPUT" + fi + resolved: name: Package.resolved is current + needs: changes + if: needs.changes.outputs.packages == 'true' runs-on: macos-15 timeout-minutes: 20 steps: