Skip to content

feat(release): RELEASE_TAG_PREFIX, so app tags can leave SwiftPM's namespace - #286

Merged
prakashrj merged 1 commit into
mainfrom
upstream-feat/release-tag-prefix
Sep 3, 2026
Merged

feat(release): RELEASE_TAG_PREFIX, so app tags can leave SwiftPM's namespace#286
prakashrj merged 1 commit into
mainfrom
upstream-feat/release-tag-prefix

Conversation

@prakashrj

Copy link
Copy Markdown
Contributor

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+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 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:

tags present .upToNextMajor(from: "0.1.0") resolved to
pkg-0.3.0, package/0.4.0, release/0.5.0, milestone/v0.2 0.1.1+9 — every prefixed tag ignored
v0.2 0.2.0 — a bare tag is claimed immediately

So 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.

RELEASE_TAG_PREFIX=app/v   ->  app/v1.0.0+5   invisible to SwiftPM
                               1.2.0          a package release you chose to cut

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 new tag-prefix-regression job exists to protect.

It is also why strip_release_tag_prefix strips only the prefix rather than reparsing and reassembling. The old sub(/^v/, "") preserved a canary tag's -canary-N suffix; my first attempt dropped it. Equivalence at the default was then checked across all four tag shapes before this landed:

tag old sub(/^v/,"") new
v1.0.0+5 1.0.0+5 1.0.0+5 same
v2026.19.1357 2026.19.1357 2026.19.1357 same
v0.2026.19-canary-7 0.2026.19-canary-7 0.2026.19-canary-7 same
1.0.0+5 1.0.0+5 1.0.0+5 same

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.

Two details worth calling out:

  • Every reader strips the configured prefix and then falls back to a bare v — so tags cut before a prefix change stay parseable. A repo switching to app/v still has v0.1.1+9 in its history.
  • The tag-format check now validates the stripped version body instead of matching ^v, which would have rejected every tag the moment a repo set a prefix.
  • The GH-release lookup globs '#{pfx}#{marketing}+*'. Left hardcoded, it would have silently found nothing and merely reported "skipping".

bin/submit.rb reads the env directly rather than via 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: the first version raised NameError: uninitialized constant Bootstrap::Version.

Verification

  • test/tag_prefix_test.rb16/16: the default, a custom prefix, an explicitly empty one, prefixes that collide with the version body, and pre-switch tags still parsing
  • shell-side stripping exercised for unset / app/v / empty across all four tag shapes
  • siblings unaffected: parser 9/9, Sh.stream 10/10, build number 11/11, xcconfig 51 checks, demo-account 14/14
  • ci/check-shell.sh clean

Documented in .bootstrap.env.example with the measurement, so the next reader does not have to re-derive why it exists.

…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>
@prakashrj
prakashrj merged commit 9e1ad04 into main Sep 3, 2026
22 checks passed
@prakashrj
prakashrj deleted the upstream-feat/release-tag-prefix branch September 3, 2026 07:59
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