feat(release): RELEASE_TAG_PREFIX, so app tags can leave SwiftPM's namespace - #286
Merged
Merged
Conversation
…mespace
A repo that is both an app AND a SwiftPM package publishes both from one
tag namespace, because SwiftPM claims every tag that parses as a
version. So `v1.0.0+5` is simultaneously the app's release marker and
package version 1.0.0: an App Store metadata bump publishes a package
version, adopters get a bump from a commit that never mentions SwiftPM,
and the package cannot be versioned on its own merits.
Measured, not assumed. With v0.2, pkg-0.3.0, package/0.4.0 and
release/0.5.0 all tagged on a real repo, .upToNextMajor(from: "0.1.0")
resolved to 0.1.1+9 — every PREFIXED tag was ignored — while a bare v0.2
resolved as 0.2.0. SwiftPM cannot be pointed at a different tag series,
so the leverage runs the other way: move the app tags out of its view.
RELEASE_TAG_PREFIX=app/v -> app/v1.0.0+5 invisible to SwiftPM
1.2.0 a package release you cut
Unset means `v`, so a fork that ignores this is byte-for-byte unchanged.
That property is what the new tag-prefix-regression job protects, and
why strip_release_tag_prefix strips only the prefix instead of
reparsing: the old `sub(/^v/, "")` preserved a canary tag's -canary-N
suffix, and equivalence at the default was checked across all four tag
shapes before this landed.
Threaded through every reader: Bootstrap::Version.tag_prefix
(canonical), compute_release_tag, parse_tag, the Fastfile's inlined
parse_release_tag plus its GH-release tag glob and release-lane version,
ci/local-release-check.sh and ci/bump-asc-version.rb. Each strips the
configured prefix then falls back to a bare `v`, so tags cut before a
prefix change stay parseable. The tag-format check now validates the
stripped version body rather than matching ^v, which would have rejected
every tag the moment a repo set a prefix.
bin/submit.rb reads the env directly rather than through
Bootstrap::Version: it does not load version_resolver, and requiring it
there would pull spaceship into startup for one display string. Caught
by a runtime probe, not by reading.
test/tag_prefix_test.rb — 16 assertions over the default, a custom
prefix, an explicitly empty one, and prefixes that could collide with
the version body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A repo that is both an app and a SwiftPM package publishes both from one tag namespace, because SwiftPM claims every tag that parses as a version.
So
v1.0.0+5is simultaneously the app's release marker and package version1.0.0. An App Store metadata bump publishes a package version; adopters get a version bump from a commit that never mentions SwiftPM; and the package cannot be versioned on its own merits.Measured, not assumed
I tagged a real repo and resolved against it rather than reasoning about SwiftPM's rules:
.upToNextMajor(from: "0.1.0")resolved topkg-0.3.0,package/0.4.0,release/0.5.0,milestone/v0.20.1.1+9— every prefixed tag ignoredv0.20.2.0— a bare tag is claimed immediatelySo SwiftPM cannot be pointed at a different tag series — the third option people reach for does not exist. The leverage runs the other way: move the app tags where SwiftPM cannot see them.
The safety property
Unset means
v, so a fork that ignores this is byte-for-byte unchanged. That is the whole contract, and it is what the newtag-prefix-regressionjob exists to protect.It is also why
strip_release_tag_prefixstrips only the prefix rather than reparsing and reassembling. The oldsub(/^v/, "")preserved a canary tag's-canary-Nsuffix; my first attempt dropped it. Equivalence at the default was then checked across all four tag shapes before this landed:sub(/^v/,"")v1.0.0+51.0.0+51.0.0+5v2026.19.13572026.19.13572026.19.1357v0.2026.19-canary-70.2026.19-canary-70.2026.19-canary-71.0.0+51.0.0+51.0.0+5Threaded through every reader
Bootstrap::Version.tag_prefix(canonical),compute_release_tag,parse_tag, the Fastfile's inlinedparse_release_tagplus its GH-release tag glob and release-lane version,ci/local-release-check.sh, andci/bump-asc-version.rb.Two details worth calling out:
v— so tags cut before a prefix change stay parseable. A repo switching toapp/vstill hasv0.1.1+9in its history.^v, which would have rejected every tag the moment a repo set a prefix.'#{pfx}#{marketing}+*'. Left hardcoded, it would have silently found nothing and merely reported "skipping".bin/submit.rbreads the env directly rather than viaBootstrap::Version— it does not loadversion_resolver, and requiring it there would pullspaceshipinto startup for one display string. Caught by a runtime probe, not by reading: the first version raisedNameError: uninitialized constant Bootstrap::Version.Verification
test/tag_prefix_test.rb— 16/16: the default, a custom prefix, an explicitly empty one, prefixes that collide with the version body, and pre-switch tags still parsingapp/v/ empty across all four tag shapesSh.stream10/10, build number 11/11, xcconfig 51 checks, demo-account 14/14ci/check-shell.shcleanDocumented in
.bootstrap.env.examplewith the measurement, so the next reader does not have to re-derive why it exists.