mirror of
https://github.com/storytold/craft-common-actions.git
synced 2026-11-15 15:02:24 +00:00
* Release: scripts/cut_release.py cuts a release from green main without a bump pull request On 11 Nov 2026 the release of 14 apps took most of the day. A large part went to version-bump pull requests that changed only version strings yet waited on the full CI of a pull request (1-2 h of FreeBSD, sharded SonarCloud coverage), and to rerunning it. The script commits the bump on the newest main commit whose CI passed, merges it into release (which starts release.yml), opens the same bump for main as its own pull request, and with --publish publishes the draft once the run passed and has assets. Tests cover the bump: workspace and path-dependency versions, only the workspace's own Cargo.lock entries, CRLF files, the repository hook. * cut_release: review fixes - A trailing comment after the workspace version stays; the value alone is replaced. - Cargo.lock: only the workspace members' entries (cargo metadata --no-deps) change, not a local path dependency outside the workspace that shares the version. - Cargo.lock is checked with a resolving cargo metadata --locked, on the bump, on release after the merge and on the sync branch; --no-deps never compared it with path dependency versions. - --publish refuses a draft with fewer assets than the last release (a skipped build job leaves the run green), unless --allow-fewer-assets. - The sync PR never moves main backwards: versions compare as numbers, and a main at or past the release gets none. - A failed auto-merge says so, with the PR to merge by hand. - Arguments that reach git or gh are checked first (owner/name, version, commit hash, workflow, title), so none can be read as an option. git, gh and cargo are required up front. * Release: scripts/release_train.py releases many apps at once with cut_release.py Every app in parallel, a log per app, and one summary line each (published, pushed, or stopped and why); one app stopping doesn't stop the rest. Tested on figcraft and soundcraft with --dry-run (both done in under a minute). * tests: release_train options before -- in the test * release_train: only owner/name repos and known cut_release.py options reach the subprocess
49 lines
2.0 KiB
Python
49 lines
2.0 KiB
Python
"""scripts/release_train.py: which apps a train releases, and what it passes on."""
|
|
import importlib.util
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
spec = importlib.util.spec_from_file_location("release_train", ROOT / "scripts/release_train.py")
|
|
release_train = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(release_train)
|
|
|
|
|
|
class TrainTests(unittest.TestCase):
|
|
def run_train(self, argv, rc=0):
|
|
calls = []
|
|
|
|
def fake(repo, extra, logs):
|
|
calls.append((repo, extra))
|
|
return repo, rc, "done"
|
|
|
|
with mock.patch.object(release_train, "run_app", side_effect=fake):
|
|
code = release_train.main(["--logs", tempfile.mkdtemp()] + argv)
|
|
return code, calls
|
|
|
|
def test_all_but_the_skipped_apps_with_the_flags_passed_on(self):
|
|
code, calls = self.run_train(["--all", "--skip", "figcraft,soundcraft", "--publish", "--", "--part", "patch"])
|
|
repos = sorted(r for r, _ in calls)
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(len(repos), len(release_train.APPS) - 2)
|
|
self.assertNotIn("storytold/figcraft", repos)
|
|
self.assertTrue(all(extra == ["--part", "patch", "--publish"] for _, extra in calls))
|
|
|
|
def test_only_owner_name_repos_and_known_options_reach_cut_release(self):
|
|
release_train.check(["storytold/pdfcraft"], ["--part", "patch", "--publish", "--gate-workflow", "CI"])
|
|
for repos, extra in [(["--upload-pack=x"], []), (["pdfcraft"], []),
|
|
(["storytold/pdfcraft"], ["--commit", "abc"]), (["storytold/pdfcraft"], ["--part", "major"]),
|
|
(["storytold/pdfcraft"], ["--version"])]:
|
|
with self.assertRaises(SystemExit, msg=f"{repos} {extra}"):
|
|
release_train.check(repos, extra)
|
|
|
|
def test_one_stopped_app_fails_the_train(self):
|
|
code, _ = self.run_train(["storytold/pdfcraft"], rc=1)
|
|
self.assertEqual(code, 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|