diff --git a/.github/workflows/conan-center.yml b/.github/workflows/conan-center.yml deleted file mode 100644 index aeee49b..0000000 --- a/.github/workflows/conan-center.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Submit libtxms to ConanCenter -on: - workflow_call: - inputs: - tag: - required: true - type: string - secrets: - CONAN_CENTER_TOKEN: - required: true - workflow_dispatch: - inputs: - tag: - description: Published stable release tag, for example 0.1.0 - required: true - type: string -permissions: - contents: read -concurrency: - group: conan-center-libtxms - cancel-in-progress: false -jobs: - submit: - if: vars.CONAN_CENTER_FORK != '' - runs-on: ubuntu-latest - env: - TAG: ${{ inputs.tag }} - FORK: ${{ vars.CONAN_CENTER_FORK }} - steps: - - name: Validate release tag - shell: bash - run: '[[ "$TAG" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]' - - uses: actions/checkout@v4 - with: - ref: ${{ inputs.tag }} - fetch-depth: 0 - persist-credentials: false - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - run: python -m pip install 'conan>=2.12,<3' - - run: conan profile detect --force - - name: Download and verify published sources - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - run: | - gh release view "$TAG" --json isDraft,isPrerelease --jq 'if .isDraft or .isPrerelease then error("A stable published release is required") else true end' - gh release download "$TAG" --dir dist --pattern "libtxms-${TAG}.tar.gz" --pattern SHA256SUMS --pattern release.json - python tools/verify_release.py --tag "$TAG" --directory dist - - name: Prepare and test recipe - run: | - version="${TAG}" - python tools/prepare_conan_center.py --version "$version" --archive "dist/libtxms-$version.tar.gz" - python tools/check_conan_center.py --version "$version" --archive "dist/libtxms-$version.tar.gz" --recipe release-stage/recipes/libtxms/all - - name: Open or update ConanCenter pull request - env: - GH_TOKEN: ${{ secrets.CONAN_CENTER_TOKEN }} - run: python tools/submit_conan_center.py --fork "$FORK" --version "${TAG}" --stage release-stage diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bcf3034..d5b2cbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,16 +24,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' - - run: python -m pip install 'conan>=2.12,<3' - - run: conan profile detect --force - name: Verify version and archive the tested commit run: python tools/release.py --project libtxms --tag "$TAG" - - name: Prepare and test the ConanCenter download recipe - run: | - version="${TAG}" - python tools/prepare_conan_center.py --version "$version" --archive "dist/libtxms-$version.tar.gz" - python tools/check_conan_center.py --version "$version" --archive "dist/libtxms-$version.tar.gz" --recipe release-stage/recipes/libtxms/all - tar -czf "dist/libtxms-$version-conan-center.tar.gz" -C release-stage LICENSE recipes - name: Checksum all release assets run: | cd dist @@ -61,11 +53,3 @@ jobs: run: cd dist && sha256sum --check SHA256SUMS - name: Attach tested assets to the published release run: gh release upload "$TAG" dist/* - conan-center: - needs: publish - if: vars.CONAN_CENTER_FORK != '' - uses: ./.github/workflows/conan-center.yml - with: - tag: ${{ github.event.release.tag_name }} - secrets: - CONAN_CENTER_TOKEN: ${{ secrets.CONAN_CENTER_TOKEN }} diff --git a/README.md b/README.md index 31cb03c..fb9e446 100644 --- a/README.md +++ b/README.md @@ -105,49 +105,12 @@ tests run; assets are attached only after all tests and package checks pass. through GitHub's Releases page. Pushing a tag alone does not run this workflow. 3. After checking the tag/version and passing tests, the workflow uploads: - `libtxms-0.1.0.tar.gz`: source from the exact tested Git commit. - - `libtxms-0.1.0-conan-center.tar.gz`: staged ConanCenter recipe and consumer test. - `release.json`: version, commit and source checksum. - `SHA256SUMS`: checksums for all release assets. -The workflow tests static/shared ConanCenter packages from that same archive, -then uploads assets to the existing release using `GITHUB_TOKEN`. Prereleases -are skipped. Existing assets are not overwritten. No prebuilt platform libraries -are attached; Conan builds the appropriate package. - -## ConanCenter submission automation - -ConanCenter accepts recipes through reviewed pull requests; its own build -service publishes packages after merge. See the [official contribution guide](https://docs.conan.io/2/tutorial/conan_repositories/conan_center.html). -This repository prepares and submits the recipe; it cannot grant ConanCenter -approval or claim the package is already available there. - -Configure the following **in the libtxms GitHub repository**: - -| Setting | Value | -| --- | --- | -| Repository variable `CONAN_CENTER_FORK` | Your existing fork, e.g. `DataLayerHost/conan-center-index` | -| Actions secret `CONAN_CENTER_TOKEN` | Token able to push to that fork and open PRs in `conan-io/conan-center-index` | - -A classic PAT with `public_repo` is one option for public repositories; use a -service account with the required access and follow your organization's token -policy. The default `GITHUB_TOKEN` cannot write to a separate fork. Never store -the token in source files. A first-time submitter may need to sign ConanCenter's -CLA. ConanCenter maintainers also decide acceptance of the custom CORE license; -the recipe declares `LicenseRef-libtxms-CORE` and includes its complete text. - -When the fork variable is set, successful release asset upload directly calls -[conan-center.yml](.github/workflows/conan-center.yml). This avoids relying on a -second release event generated by `GITHUB_TOKEN`. The workflow downloads the -published source, checks its checksum and commit metadata, rebuilds static/shared -packages, and opens or updates a recipe PR. It preserves earlier versions and -refuses to replace an existing version's source metadata. Branches are pushed -normally, without force. If the variable is absent, the release still includes -the prepared recipe bundle and automatic PR submission is skipped. - -For a failed submission after publication, run **Actions → Submit libtxms to -ConanCenter → Run workflow**, supplying the existing tag. A missing token fails -submission without changing the published release. This manual path verifies -release assets and reruns package tests too. Until ConanCenter accepts the PR, -use `conan create .` from the matching release checkout. +The workflow uploads assets to the existing release using `GITHUB_TOKEN` after +CI succeeds. Prereleases are skipped. Existing assets are not overwritten. +No prebuilt platform libraries are attached; consumers can build from source +or use the included local Conan recipe. ConanCenter publication is disabled. Local validation details are in [tests/VALIDATION.md](tests/VALIDATION.md). diff --git a/packaging/conan-center/conanfile.py b/packaging/conan-center/conanfile.py deleted file mode 100644 index 0ba13a6..0000000 --- a/packaging/conan-center/conanfile.py +++ /dev/null @@ -1,62 +0,0 @@ -# SPDX-License-Identifier: MIT -import os -from conan import ConanFile -from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout -from conan.tools.files import copy, get, rmdir - -required_conan_version = ">=2.12" - - -class LibtxmsConan(ConanFile): - url = "https://github.com/conan-io/conan-center-index" - - name = "libtxms" - package_type = "library" - license = "LicenseRef-libtxms-CORE" - homepage = "https://github.com/DataLayerHost/libtxms" - description = "C11 TxMS transaction messaging codec" - topics = ("sms", "encoding", "blockchain") - settings = "os", "arch", "compiler", "build_type" - options = {"shared": [True, False], "fPIC": [True, False]} - default_options = {"shared": False, "fPIC": True} - - def config_options(self): - if self.settings.os == "Windows": - del self.options.fPIC - - def configure(self): - if self.options.shared: - self.options.rm_safe("fPIC") - self.settings.rm_safe("compiler.cppstd") - self.settings.rm_safe("compiler.libcxx") - - def layout(self): - cmake_layout(self) - - def generate(self): - tc = CMakeToolchain(self) - tc.variables["BUILD_TESTING"] = False - tc.variables["TXMS_SANITIZE"] = False - tc.variables["TXMS_FUZZ"] = False - tc.generate() - - def build(self): - cmake = CMake(self) - cmake.configure() - cmake.build() - - def package(self): - copy(self, "LICENSE", self.source_folder, os.path.join(self.package_folder, "licenses")) - copy(self, "NOTICE", self.source_folder, os.path.join(self.package_folder, "licenses")) - CMake(self).install() - rmdir(self, os.path.join(self.package_folder, "lib", "cmake")) - rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) - - def package_info(self): - self.cpp_info.libs = ["txms"] - self.cpp_info.set_property("cmake_file_name", "txms") - self.cpp_info.set_property("cmake_target_name", "txms::txms") - self.cpp_info.set_property("pkg_config_name", "txms") - - def source(self): - get(self, **self.conan_data["sources"][str(self.version)], strip_root=True) diff --git a/tests/VALIDATION.md b/tests/VALIDATION.md index f5ca2b4..d906f4b 100644 --- a/tests/VALIDATION.md +++ b/tests/VALIDATION.md @@ -20,11 +20,7 @@ its effect on binary round trips. Malformed escape inputs are deliberately rejec - Release tooling tests passed: tag/version matching, exact-commit archives, dirty-tree rejection and dependency metadata. - libtxms Conan 2.32.0 static/shared packages and their separate C consumer passed - on macOS arm64 and Debian 12 arm64. The staged ConanCenter download recipe also - passed both configurations on both operating systems using the same checksummed - archive served by a local HTTP server. -- ConanCenter metadata tests verify preserved older versions, idempotent updates, - refusal to replace a version checksum, and tampered-archive rejection. + on macOS arm64 and Debian 12 arm64. - macOS codec and gateway sanitizer suites passed after the CMake packaging changes. -- No release or upstream ConanCenter PR was published. Hosted Windows packaging +- No release was published during these local checks. Hosted Windows packaging and the actual GitHub publication steps remain to be exercised by CI after push. diff --git a/tests/test_release.py b/tests/test_release.py index b223440..fe0d61e 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -89,9 +89,7 @@ def test_published_release_uploads_only_after_tests(self): commands = "\n".join(step.get("run", "") for step in jobs["publish"]["steps"]) self.assertIn('gh release upload "$TAG"', commands) self.assertNotIn("gh release create", commands) - if "conan-center" in jobs: - self.assertEqual(jobs["conan-center"]["needs"], "publish") - self.assertEqual(jobs["conan-center"]["with"]["tag"], "${{ github.event.release.tag_name }}") + @unittest.skipUnless((ROOT / "LIBTXMS_REVISION").exists(), "Gateway dependency check") def test_gateway_checks_out_pinned_commit_in_every_build(self): @@ -103,46 +101,5 @@ def test_gateway_checks_out_pinned_commit_in_every_build(self): self.assertEqual(checkout["with"]["ref"], "${{ env.LIBTXMS_REF }}") self.assertTrue(any("cat LIBTXMS_REVISION" in step.get("run", "") for step in steps)) - -if (ROOT / "tools/prepare_conan_center.py").exists(): - from prepare_conan_center import prepare - from submit_conan_center import merge_recipe - from verify_release import verify - - class ConanCenterTests(unittest.TestCase): - setUp = ReleaseTests.setUp - tearDown = ReleaseTests.tearDown - git = ReleaseTests.git - def test_release_verification_rejects_tampering(self): - out = self.root / "dist" - metadata = build_release(self.root, "libtxms", "0.1.0", out) - with (out / "SHA256SUMS").open("a") as checksums: - checksums.write(hashlib.sha256((out / "release.json").read_bytes()).hexdigest() + " release.json\n") - self.assertEqual(verify(out, "0.1.0", metadata["commit"]), metadata) - (out / "libtxms-0.1.0.tar.gz").write_bytes(b"tampered") - with self.assertRaises(ValueError): - verify(out, "0.1.0", metadata["commit"]) - - def test_conan_metadata_preserves_existing_versions(self): - out = self.root / "dist" - build_release(self.root, "libtxms", "0.1.0", out) - stage = self.root / "stage" - prepare(ROOT, "0.1.0", out / "libtxms-0.1.0.tar.gz", stage) - destination = self.root / "index" - old = destination / "recipes/libtxms/all" - old.mkdir(parents=True) - (old / "conandata.yml").write_text(yaml.safe_dump({"sources": {"0.0.9": {"url": "https://example.invalid/old", "sha256": "a"*64}}})) - (old.parent / "config.yml").write_text(yaml.safe_dump({"versions": {"0.0.9": {"folder": "all"}}})) - merge_recipe(stage, destination, "0.1.0") - before = (old / "conandata.yml").read_bytes() - self.assertEqual(set(yaml.safe_load(before)["sources"]), {"0.0.9", "0.1.0"}) - merge_recipe(stage, destination, "0.1.0") - self.assertEqual(before, (old / "conandata.yml").read_bytes()) - data = yaml.safe_load((stage / "recipes/libtxms/all/conandata.yml").read_text()) - data["sources"]["0.1.0"]["sha256"] = "b"*64 - (stage / "recipes/libtxms/all/conandata.yml").write_text(yaml.safe_dump(data)) - with self.assertRaises(ValueError): - merge_recipe(stage, destination, "0.1.0") - if __name__ == "__main__": unittest.main() diff --git a/tools/check_conan_center.py b/tools/check_conan_center.py deleted file mode 100644 index 2aa1db5..0000000 --- a/tools/check_conan_center.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -"""Exercise the staged download recipe against the archive before publication.""" -import argparse -from functools import partial -import http.server -from pathlib import Path -import shutil -import subprocess -import tempfile -import threading -import yaml - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--recipe", required=True) - parser.add_argument("--archive", required=True) - parser.add_argument("--version", required=True) - args = parser.parse_args() - archive = Path(args.archive).resolve() - handler = partial(http.server.SimpleHTTPRequestHandler, directory=str(archive.parent)) - server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) - threading.Thread(target=server.serve_forever, daemon=True).start() - try: - with tempfile.TemporaryDirectory(prefix="txms-conan-recipe-") as folder: - shutil.copytree(args.recipe, folder, dirs_exist_ok=True) - data_path = Path(folder, "conandata.yml") - data = yaml.safe_load(data_path.read_text()) - data["sources"][args.version]["url"] = f"http://127.0.0.1:{server.server_port}/{archive.name}" - data_path.write_text(yaml.safe_dump(data)) - for shared in ("False", "True"): - subprocess.run(["conan", "create", folder, "--version", args.version, "--build=missing", "--no-remote", "-o", f"libtxms/*:shared={shared}"], check=True) - finally: - server.shutdown() - server.server_close() diff --git a/tools/prepare_conan_center.py b/tools/prepare_conan_center.py deleted file mode 100644 index 0cdf2f5..0000000 --- a/tools/prepare_conan_center.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -"""Stage a ConanCenter recipe with an exact release archive checksum.""" -import argparse -import hashlib -from pathlib import Path -import re -import shutil -import yaml -from release import VERSION - -def prepare(root, version, archive, output): - root, archive, output = Path(root), Path(archive), Path(output) - if not re.fullmatch(VERSION, version): - raise ValueError("Expected a stable MAJOR.MINOR.PATCH version") - if archive.name != f"libtxms-{version}.tar.gz": - raise ValueError("Archive filename does not match the version") - recipe = output / "recipes" / "libtxms" - all_dir = recipe / "all" - all_dir.mkdir(parents=True, exist_ok=True) - shutil.copy2(root / "packaging/LICENSE", output / "LICENSE") - shutil.copy2(root / "packaging/conan-center/conanfile.py", all_dir / "conanfile.py") - shutil.copytree(root / "test_package", all_dir / "test_package", dirs_exist_ok=True, - ignore=shutil.ignore_patterns("build*", "__pycache__", "CMakeUserPresets.json")) - data = {"sources": {version: { - "url": f"https://github.com/DataLayerHost/libtxms/releases/download/{version}/{archive.name}", - "sha256": hashlib.sha256(archive.read_bytes()).hexdigest(), - }}} - (all_dir / "conandata.yml").write_text(yaml.safe_dump(data, sort_keys=False)) - (recipe / "config.yml").write_text(yaml.safe_dump({"versions": {version: {"folder": "all"}}}, sort_keys=False)) - return recipe - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--version", required=True) - parser.add_argument("--archive", required=True) - parser.add_argument("--output", default="release-stage") - parser.add_argument("--root", default=".") - args = parser.parse_args() - print(prepare(args.root, args.version, args.archive, args.output)) diff --git a/tools/submit_conan_center.py b/tools/submit_conan_center.py deleted file mode 100644 index f7f909f..0000000 --- a/tools/submit_conan_center.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Open an idempotent recipe PR using an explicitly configured ConanCenter fork.""" -import argparse -import json -import os -from pathlib import Path -import re -import shutil -import subprocess -import tempfile -import yaml -from release import VERSION - -UPSTREAM = "conan-io/conan-center-index" - - -def merge_recipe(stage, destination, version): - """Preserve older versions and refuse to replace an existing source checksum.""" - source = Path(stage) / "recipes/libtxms" - target = Path(destination) / "recipes/libtxms" - data = yaml.safe_load((source / "all/conandata.yml").read_text()) - new_source = data["sources"][version] - previous_path = target / "all/conandata.yml" - previous = yaml.safe_load(previous_path.read_text()) if previous_path.exists() else {"sources": {}} - old_source = previous.setdefault("sources", {}).get(version) - if old_source is not None and old_source != new_source: - raise ValueError("Existing version has different source metadata; never overwrite a published version") - config_path = target / "config.yml" - config = yaml.safe_load(config_path.read_text()) if config_path.exists() else {"versions": {}} - if version in config.get("versions", {}) and config["versions"][version].get("folder") != "all": - raise ValueError("Existing version uses another recipe folder; manual review is required") - previous["sources"][version] = new_source - config.setdefault("versions", {})[version] = {"folder": "all"} - (target / "all").mkdir(parents=True, exist_ok=True) - shutil.copy2(source / "all/conanfile.py", target / "all/conanfile.py") - shutil.copytree(source / "all/test_package", target / "all/test_package", dirs_exist_ok=True) - previous_path.write_text(yaml.safe_dump(previous, sort_keys=False)) - config_path.write_text(yaml.safe_dump(config, sort_keys=False)) - - -def submit(fork, version, stage): - if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9_.-]+", fork) or fork.lower() == UPSTREAM.lower(): - raise ValueError("Set CONAN_CENTER_FORK to your own owner/repository fork") - if not re.fullmatch(VERSION, version): - raise ValueError("Invalid stable version") - if not os.environ.get("GH_TOKEN"): - raise ValueError("CONAN_CENTER_TOKEN is required to submit to the configured fork") - def run(*args, cwd=None): - return subprocess.check_output(args, cwd=cwd, text=True).strip() - info = json.loads(run("gh", "api", f"repos/{fork}")) - if info.get("parent", {}).get("full_name") != UPSTREAM: - raise ValueError("Configured repository is not a fork of ConanCenterIndex") - base = run("gh", "api", f"repos/{UPSTREAM}", "--jq", ".default_branch") - branch = f"automation/libtxms-{version}" - owner = fork.split("/")[0] - with tempfile.TemporaryDirectory(prefix="txms-cci-pr-") as folder: - repo = Path(folder, "index") - # gh auth configures a credential helper; the token never appears in URLs. - run("gh", "auth", "setup-git") - run("git", "clone", "--filter=blob:none", "--sparse", f"https://github.com/{fork}.git", str(repo)) - run("git", "sparse-checkout", "set", "recipes/libtxms", cwd=repo) - run("git", "remote", "add", "upstream", f"https://github.com/{UPSTREAM}.git", cwd=repo) - run("git", "fetch", "upstream", base, cwd=repo) - existing = run("git", "ls-remote", "--heads", "origin", f"refs/heads/{branch}", cwd=repo) - if existing: - run("git", "fetch", "origin", branch, cwd=repo) - run("git", "checkout", "-b", branch, "FETCH_HEAD", cwd=repo) - else: - run("git", "checkout", "-b", branch, f"upstream/{base}", cwd=repo) - merge_recipe(stage, repo, version) - run("git", "add", "--", "recipes/libtxms", cwd=repo) - if run("git", "diff", "--cached", "--name-only", cwd=repo): - run("git", "-c", "user.name=TxMS release automation", "-c", "user.email=41898282+github-actions[bot]@users.noreply.github.com", "commit", "-m", f"libtxms: add version {version}", cwd=repo) - run("git", "push", "origin", f"HEAD:refs/heads/{branch}", cwd=repo) - elif not existing: - print("ConanCenter already contains this recipe; no PR needed") - return - # REST supports organization-owned forks and owner-qualified heads. - # gh pr create/list do not support all of these combinations. - if not run("git", "diff", f"upstream/{base}", "HEAD", "--", "recipes/libtxms", cwd=repo): - print("Recipe changes are already upstream; no PR needed") - return - prs = json.loads(run("gh", "api", "--method", "GET", f"repos/{UPSTREAM}/pulls", "-f", f"head={owner}:{branch}", "-f", "state=open")) - if prs: - print(prs[0]["html_url"]) - return - body = Path(folder, "body.md") - body.write_text(f"Adds libtxms {version}, the standalone C11 TxMS codec.\n\n" - f"Source release: https://github.com/DataLayerHost/libtxms/releases/tag/{version}\n\n" - "The recipe downloads the release archive with its SHA-256 checksum. Static and shared packages were built and their C consumer was executed before submission.\n\n" - "Library license: LicenseRef-libtxms-CORE; the complete CORE license and NOTICE are packaged. Recipe and test_package are MIT licensed. Please review custom-license eligibility.\n") - request = Path(folder, "pull-request.json") - request.write_text(json.dumps({"base": base, "head": f"{owner}:{branch}", "head_repo": fork.split("/")[1], - "title": f"libtxms/{version}: add recipe", "body": body.read_text()})) - print(run("gh", "api", "--method", "POST", f"repos/{UPSTREAM}/pulls", "--input", str(request), "--jq", ".html_url")) - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--fork", required=True) - parser.add_argument("--version", required=True) - parser.add_argument("--stage", default="release-stage") - args = parser.parse_args() - submit(args.fork, args.version, Path(args.stage).resolve()) diff --git a/tools/verify_release.py b/tools/verify_release.py deleted file mode 100644 index 9def980..0000000 --- a/tools/verify_release.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -"""Verify downloaded release sources and metadata against the checked-out tag.""" -import argparse -import hashlib -import json -from pathlib import Path -import re -import subprocess -from release import VERSION - - -def verify(directory, tag, commit): - if not re.fullmatch(VERSION, tag): - raise ValueError("Invalid stable release tag") - directory = Path(directory) - archive_name = f"libtxms-{tag}.tar.gz" - checksums = {} - for line in (directory / "SHA256SUMS").read_text().splitlines(): - match = re.fullmatch(r"([0-9a-f]{64}) ([A-Za-z0-9_.-]+)", line) - if not match or match.group(2) in checksums: - raise ValueError("Malformed or duplicate checksum entry") - checksums[match.group(2)] = match.group(1) - for name in (archive_name, "release.json"): - if checksums.get(name) != hashlib.sha256((directory / name).read_bytes()).hexdigest(): - raise ValueError(f"Checksum mismatch for {name}") - metadata = json.loads((directory / "release.json").read_text()) - if metadata != {"project": "libtxms", "version": tag, "tag": tag, "commit": commit, "source_sha256": checksums[archive_name]}: - raise ValueError("Release metadata does not match the checked-out tag") - return metadata - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--tag", required=True) - parser.add_argument("--directory", default="dist") - args = parser.parse_args() - commit = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip() - tag_commit = subprocess.check_output(["git", "rev-parse", f"refs/tags/{args.tag}^{{commit}}"], text=True).strip() - if commit != tag_commit: - raise ValueError("Checkout differs from release tag") - print(json.dumps(verify(args.directory, args.tag, commit), indent=2))