Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ python3 scripts/release/notes.py docs X.Y.Z --ref vX.Y.Z --mdx ../tablepro-docs/

A `v*` tag runs `release.yml` and nothing else. A run is never cancelled, and every job reads the version, build and channel from `verify`.

- `verify`: `version.py check` refuses a tag that breaks the grammar or the bounds; that `origin/release/<X.Y>` does not contain; whose previous tag on the line is not its ancestor; a stable X.Y.0 with no published beta, or a beta after its X.Y.0; a stable X.Y.0 whose tree differs from its newest published beta outside `CHANGELOG.md` and `changelog.d/`; a `CHANGELOG.md` missing the stable's heading, or a published stable of this or the previous line below it; a stable whose tag still holds fragments in `changelog.d/`. Then it needs macOS Tests, iOS and Package Tests and Release Build green on the tag commit or the nearest first-parent commit that ran them, with no change to the files they read after it, waiting on a pending run. Last, it writes the notes.
- `verify`: `version.py check` refuses a tag that breaks the grammar or the bounds; that `origin/release/<X.Y>` does not contain; whose previous tag on the line is not its ancestor; a stable X.Y.0 with no published beta, or a beta after its X.Y.0; a stable whose tree differs from its line's newest published beta outside `CHANGELOG.md` and `changelog.d/`, until a stable tag on the line has matched that beta; a `CHANGELOG.md` missing the stable's heading, or a published stable of this or the previous line below it; a stable whose tag still holds fragments in `changelog.d/`. Then it needs macOS Tests, iOS and Package Tests and Release Build green on the tag commit or the nearest first-parent commit that ran them, with no change to the files they read after it, waiting on a pending run. Last, it writes the notes.
- `app` (`signing`): a GA Xcode for a stable (`setup-xcode` `require-release`), the CloudKit Production schema check, `build.sh app` (archive, export with `scripts/release/ExportOptions.plist`, universal and plugin load checks), `notarize.sh` on the app, `package.sh app`, `notarize.sh` on the DMG.
- `plugins` (`signing`): the Dameng and HANA bridges, `build.sh plugins` (each target `registry.py targets` prints, universal, signed with Developer ID, the hardened runtime and a timestamp), one notary submission for all, `package.sh plugins` (the zips, described as the manifest will read them).
- `github-release`: `actions/attest` attests `dist/*`; `gh release create` drafts, uploads and publishes; `gh release verify` and `verify-asset` check the published release.
Expand Down
38 changes: 37 additions & 1 deletion scripts/release/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,45 @@ def test_a_stable_refuses_a_change_after_its_newest_published_beta(self):
github = FakeGitHub(releases=releases)
self.assertEqual(self.check("v0.82.0", github),
["v0.82.0 differs from v0.82.0-beta.2, the newest published beta, in "
"Plugins/Third.swift, TablePro/Second.swift: ship that in a beta first"])
"Plugins/Third.swift, TablePro/Second.swift: revert them on release/0.82, "
"then tag the next number"])
self.assertEqual(github.run_queries, [])

def test_the_next_number_after_a_refused_stable_is_held_to_the_beta(self):
self.repo.git("tag", "v0.82.0-beta.1", self.cut)
pick = self.repo.commit("fix: late pick", source="TablePro/Late.swift")
refused = self.repo.commit("chore(release): v0.82.0", MAIN_CHANGELOG.replace(
"## [0.81.0]", "## [0.82.0] - 2026-10-26\n\n## [0.81.0]"))
self.repo.git("tag", "v0.82.0", refused)
patch = self.repo.commit("chore(release): v0.82.1", MAIN_CHANGELOG.replace(
"## [0.81.0]", "## [0.82.1] - 2026-10-27\n\n## [0.81.0]"))
self.repo.git("tag", "v0.82.1", patch)
self.repo.git("revert", "--no-edit", pick)
reverted = self.repo.commit("chore(release): v0.82.2", MAIN_CHANGELOG.replace(
"## [0.81.0]", "## [0.82.2] - 2026-10-28\n\n## [0.81.0]"))
self.repo.git("tag", "v0.82.2", reverted)
self.repo.line("0.82", reverted)
releases = [("v0.81.0", False), ("v0.82.0-beta.1", False)]
runs, jobs = green_on(reverted)
self.assertEqual(self.check("v0.82.1", FakeGitHub(releases=releases)),
["v0.82.1 differs from v0.82.0-beta.1, the newest published beta, in "
"TablePro/Late.swift: revert them on release/0.82, then tag the next number"])
self.assertEqual(self.check("v0.82.2", FakeGitHub(releases=releases, runs=runs, jobs=jobs)), [])

def test_a_fix_after_a_stable_tag_that_matched_the_beta_is_a_patch(self):
self.repo.git("tag", "v0.82.0-beta.1", self.cut)
stable = self.repo.commit("chore(release): v0.82.0", MAIN_CHANGELOG.replace(
"## [0.81.0]", "## [0.82.0] - 2026-10-26\n\n## [0.81.0]"))
self.repo.git("tag", "v0.82.0", stable)
self.repo.commit("ci: select a GA Xcode", source=".github/actions/setup-xcode/action.yml")
release_commit = self.repo.commit("chore(release): v0.82.1", MAIN_CHANGELOG.replace(
"## [0.81.0]", "## [0.82.1] - 2026-10-27\n\n## [0.81.0]"))
self.repo.line("0.82", release_commit)
self.repo.git("tag", "v0.82.1", release_commit)
runs, jobs = green_on(release_commit)
releases = [("v0.81.0", False), ("v0.82.0-beta.1", False), ("v0.82.0", True)]
self.assertEqual(self.check("v0.82.1", FakeGitHub(releases=releases, runs=runs, jobs=jobs)), [])

def test_a_stable_may_differ_from_its_newest_beta_in_the_changelog_only(self):
beta = self.repo.commit("fix: a fix", fragment="3610.fixed.md")
self.repo.git("tag", "v0.82.0-beta.1", beta)
Expand Down
21 changes: 17 additions & 4 deletions scripts/release/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,17 +224,30 @@ def beta_errors(git: Git, version: Version, releases: List[Tuple[str, bool]]) ->
return [f"{version.tag} needs a published {version.tag}-beta.N release first"]


def untested_paths(git: Git, beta: Version, tag: str) -> List[str]:
return [path for path in git.changed_paths(beta.tag, tag)
if path != "CHANGELOG.md" and not path.startswith(FRAGMENTS)]


def earlier_stables(git: Git, version: Version) -> List[Version]:
tags = (try_parse_tag(tag) for tag in git.tags(f"v{version.line}.*"))
return [tag for tag in tags
if tag is not None and not tag.is_beta and tag.line == version.line and tag.build < version.build]


def stable_tree_errors(git: Git, version: Version, releases: List[Tuple[str, bool]]) -> List[str]:
betas = published_betas(releases, version.line)
if version.is_beta or version.patch != 0 or not betas:
if version.is_beta or not betas:
return []
newest = max(betas, key=lambda beta: beta.build)
untested = [path for path in git.changed_paths(newest.tag, version.tag)
if path != "CHANGELOG.md" and not path.startswith(FRAGMENTS)]
# No beta can follow a stable tag, so a fix after one that matched the beta ships as a patch.
if any(not untested_paths(git, newest, stable.tag) for stable in earlier_stables(git, version)):
return []
untested = untested_paths(git, newest, version.tag)
if not untested:
return []
return [f"{version.tag} differs from {newest.tag}, the newest published beta, in {', '.join(untested)}: "
"ship that in a beta first"]
f"revert them on release/{version.line}, then tag the next number"]


def changelog_errors(changelog: Optional[str], version: Version, stables: List[Version]) -> List[str]:
Expand Down
Loading