Repository navigation
feat(release): publish the farming analyzer as a release asset #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| #!/usr/bin/env bash | ||
| # Package the MIT farming analyzer as a deterministic release asset so | ||
| # consumers can pin it by digest instead of a mutable source archive. | ||
| set -euo pipefail | ||
|
|
||
| if [[ $# -ne 2 ]]; then | ||
| echo "usage: $0 TAG OUTPUT_DIRECTORY" >&2 | ||
| exit 2 | ||
| fi | ||
|
|
||
| tag=$1 | ||
| output_directory=$2 | ||
| if [[ ! ${tag} =~ ^v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then | ||
| echo "invalid release tag: ${tag}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| version=${BASH_REMATCH[1]} | ||
| source_commit=$(git rev-parse "${tag}^{commit}") | ||
| if ! git merge-base --is-ancestor "${source_commit}" refs/heads/main && | ||
| ! git merge-base --is-ancestor "${source_commit}" refs/remotes/origin/main; then | ||
|
Comment on lines
+20
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the release workflow runs on a supported AGENTS.md reference: AGENTS.md:L32-L33 Useful? React with 👍 / 👎. |
||
| echo "release tag is not reachable from main: ${tag}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| package=atrinik-tools-farming-analyzer-${version} | ||
| mkdir -p "${output_directory}" | ||
| # git archive records the commit time for every member; gzip -n omits the | ||
| # name and timestamp, so the same tag always yields the same bytes. | ||
| git archive --format=tar --prefix="${package}/" "${source_commit}" LICENSE farming_analyzer | | ||
| gzip -9n >"${output_directory}/${package}.tar.gz" | ||
|
Comment on lines
+30
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a release runs with a non-default repository, global, or system Useful? React with 👍 / 👎. |
||
| ( | ||
| cd "${output_directory}" | ||
| sha256sum "${package}.tar.gz" >SHA256SUMS | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| """The farming analyzer release asset is deterministic and contains only its package.""" | ||
|
|
||
| import hashlib | ||
| import shutil | ||
| import subprocess | ||
| import tarfile | ||
| import tempfile | ||
| import unittest | ||
| from pathlib import Path | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
| SCRIPT = ROOT / "scripts" / "release" / "package-farming-analyzer.sh" | ||
|
|
||
|
|
||
| @unittest.skipUnless(shutil.which("git"), "git is required") | ||
| class FarmingAnalyzerReleaseAssetTests(unittest.TestCase): | ||
| def setUp(self): | ||
| self.directory = tempfile.TemporaryDirectory() | ||
| self.repo = Path(self.directory.name) / "repo" | ||
| self.repo.mkdir() | ||
| self.git("init", "-q", "-b", "main") | ||
| (self.repo / "LICENSE").write_text("MIT\n") | ||
| (self.repo / "farming_analyzer").mkdir() | ||
| (self.repo / "farming_analyzer" / "__init__.py").write_text('"""Analyzer."""\n') | ||
| (self.repo / "map-checker-qt").mkdir() | ||
| (self.repo / "map-checker-qt" / "LICENSE").write_text("GPL\n") | ||
| self.git("add", ".") | ||
| self.git("commit", "-q", "-m", "release") | ||
| self.git("tag", "v1.2.3") | ||
|
|
||
| def tearDown(self): | ||
| self.directory.cleanup() | ||
|
|
||
| def git(self, *args): | ||
| return subprocess.run( | ||
| ["git", "-C", str(self.repo), "-c", "user.name=t", "-c", "user.email=t@example.invalid", | ||
| "-c", "commit.gpgsign=false", "-c", "tag.gpgsign=false", *args], | ||
| check=True, capture_output=True, text=True).stdout | ||
|
|
||
| def package(self, tag, output): | ||
| return subprocess.run(["bash", str(SCRIPT), tag, str(output)], cwd=self.repo, | ||
| capture_output=True, text=True) | ||
|
|
||
| def test_asset_is_deterministic_and_scoped_to_the_analyzer(self): | ||
| first, second = Path(self.directory.name) / "a", Path(self.directory.name) / "b" | ||
| for output in (first, second): | ||
| result = self.package("v1.2.3", output) | ||
| self.assertEqual(result.returncode, 0, result.stderr) | ||
| name = "atrinik-tools-farming-analyzer-1.2.3.tar.gz" | ||
| data = (first / name).read_bytes() | ||
| self.assertEqual(data, (second / name).read_bytes()) | ||
| self.assertEqual((first / "SHA256SUMS").read_text(), | ||
| f"{hashlib.sha256(data).hexdigest()} {name}\n") | ||
| with tarfile.open(first / name) as archive: | ||
| members = sorted(member.name for member in archive.getmembers() if member.isfile()) | ||
| self.assertEqual(members, [ | ||
| "atrinik-tools-farming-analyzer-1.2.3/LICENSE", | ||
| "atrinik-tools-farming-analyzer-1.2.3/farming_analyzer/__init__.py", | ||
| ]) | ||
|
|
||
| def test_rejects_malformed_and_unreleased_tags(self): | ||
| output = Path(self.directory.name) / "out" | ||
| self.assertNotEqual(self.package("1.2.3", output).returncode, 0) | ||
| self.git("checkout", "-q", "-b", "side") | ||
| (self.repo / "LICENSE").write_text("changed\n") | ||
| self.git("commit", "-q", "-am", "side") | ||
| self.git("tag", "v9.9.9") | ||
| result = self.package("v9.9.9", output) | ||
| self.assertNotEqual(result.returncode, 0) | ||
| self.assertIn("not reachable from main", result.stderr) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this packaging command fails on a mainline release—for example because
git archive,gzip, orsha256sumerrors—semantic-release 25.0.9 has already created and pushed the tag before invoking publish plugins, as shown by the pinned implementation's tag and publish ordering. On rerun that tag is selected as the last release and there are no remaining commits to publish, per the release-selection path, leaving the version without a GitHub release or assets unless a maintainer manually removes the tag. Generate the archive withprepareCmdfrom the pending release commit so packaging failures occur before tag creation.AGENTS.md reference: AGENTS.md:L32-L33
Useful? React with 👍 / 👎.