Skip to content

Update every Package.resolved Xcode reads, not just the .xcodeproj#5

Merged
maximbilan merged 2 commits into
mainfrom
fix/update-all-lockfiles
Jul 23, 2026
Merged

Update every Package.resolved Xcode reads, not just the .xcodeproj#5
maximbilan merged 2 commits into
mainfrom
fix/update-all-lockfiles

Conversation

@maximbilan

@maximbilan maximbilan commented Jul 23, 2026

Copy link
Copy Markdown
Owner

The bug

The action only wrote resolved pins to the lockfile embedded in the .xcodeproj:

<MyApp>.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

But Xcode keeps a separate Package.resolved per container. A project built through a .xcworkspacexcodebuild -workspace, which is what most CI/release builds run — resolves against the workspace lockfile at <MyApp>.xcworkspace/xcshareddata/swiftpm/Package.resolved. The action never touched it.

So weekly dependency bumps landed in the .xcodeproj lockfile but never reached real builds, and the two files silently drifted — the project lockfile looked current while xcodebuild -workspace kept compiling the stale pins. This was even a documented caveat in the README.

The fix

Discover every lockfile Xcode reads — the .xcodeproj-embedded one plus each sibling .xcworkspace's copy — and patch each in place, values-only:

  • Preserves each file's schema version (v2 stays v2, v3 stays v3), originHash, and byte-exact SwiftPM formatting → minimal diffs, no formatting churn.
  • Updates only pins present in the fresh resolution; never removes pins that belong solely to a workspace's local packages (e.g. a transitive dep of a local SPM package the synthetic manifest can't see).
  • Won't author a workspace lockfile from scratch (it would be incomplete — the workspace graph is a superset of the synthetic manifest's).
  • changed-count now spans all lockfiles; dependencies-changed is true if any lockfile would change.

Local packages' own lockfiles are correctly excluded.

Testing

  • test/test_merge.py (pure-Python, no Swift toolchain) — 6 tests covering target discovery and the in-place merge; wired into CI as a fast Linux unit job.
  • End-to-end through real swift package update: a stale project (v3) + stale workspace (v2) lockfile with a workspace-only pin → both updated, formats & originHash preserved, the workspace-only pin survived untouched, idempotent on re-run.

Note for consumers

Consumers pinned at @v1 won't get this until v1 is re-pointed or a new release is cut after merge.

🤖 Generated with Claude Code

The action only wrote the freshly-resolved pins to the lockfile embedded in
the .xcodeproj (project.xcworkspace/.../Package.resolved). Projects built via a
.xcworkspace resolve against the *workspace* lockfile instead, which the action
never touched -- so dependency bumps landed in the project file but never
reached `xcodebuild -workspace` builds, and the two files silently drifted
(the project lockfile looked current while real builds compiled stale pins).

Now the action discovers every lockfile Xcode reads -- the .xcodeproj-embedded
one plus each sibling .xcworkspace's copy -- and patches each in place,
values-only:

- Preserves each file's schema version (v2/v3), originHash, and byte-exact
  SwiftPM formatting, so diffs are minimal with no formatting churn.
- Only updates pins present in the fresh resolution; never removes pins that
  belong solely to a workspace's local packages (e.g. a transitive dep of a
  local SPM package the synthetic manifest never sees).
- Won't author a workspace lockfile from scratch (it would be incomplete); only
  keeps existing ones current.
- changed-count now spans all lockfiles; dependencies-changed is true if any
  lockfile would change.

Adds test/test_merge.py (pure-Python, no Swift toolchain) covering discovery
and the in-place merge, wired into CI as a fast Linux `unit` job. Updates
README and CHANGELOG; the old "project-internal Package.resolved only" caveat
is now obsolete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maximbilan
maximbilan force-pushed the fix/update-all-lockfiles branch from 95eb91c to 2bacb4f Compare July 23, 2026 13:42
Foundation's JSONEncoder (what SwiftPM/Xcode use) writes raw UTF-8; Python's
json escapes non-ASCII to \uXXXX by default, which would rewrite a package
name or URL containing non-ASCII on the first run. No-op for the common
ASCII-only case; keeps output byte-identical to Xcode's for arbitrary repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@maximbilan
maximbilan merged commit 73b6382 into main Jul 23, 2026
2 checks passed
@maximbilan
maximbilan deleted the fix/update-all-lockfiles branch July 23, 2026 14:01
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.

1 participant