Release Transaction Management
Relevant source files
The following files were used as context for generating this wiki page:
The Release Transaction Management system is a critical component of the numan-plugins project designed to manage the lifecycle of plugin releases with high reliability and safety. It ensures that binary artifacts are built, verified, and published to GitHub in an atomic "draft-then-finalize" manner, preventing the publication of incomplete or corrupted releases. This system acts as the binary-delivery pipeline feeder for the numan-registry, ensuring that every published asset is immutable and tied to a verified upstream commit.
The core philosophy of the transaction management is to "fail closed" on any inconsistency. New releases are assembled as run-owned drafts, verified for asset completeness and hash integrity, and only then made public. This process prevents "half-published" releases where some platform binaries might be missing or mismatched.
Sources: README.md:1-20, REVIEW.md:30-45, scripts/release_transaction.py:1-10
Transaction Lifecycle
The release process follows a strictly ordered set of states to ensure that no release is visible to users until all assets for all platforms are successfully uploaded and verified.
1. Pre-Transaction Safety Checks
Before a transaction begins, the system verifies that the target release does not already exist. This maintains the invariant that release assets are immutable. If a Git tag or a GitHub release already exists for the intended version, the process fails immediately.
Sources: scripts/ensure_release_absent.py:38-65, REVIEW.md:35-37
2. Claiming the Release
The transaction starts with the claim operation. This creates a new Git reference (tag) pointing to a specific immutable commit and then creates a corresponding GitHub Release marked as a draft. By creating it as a draft, the release is invisible to the general public and owned by the specific CI run.
Sources: scripts/release_transaction.py:53-100, scripts/test_release_transaction.py:46-51
3. Asset Upload
Once a release is claimed and a release_id is obtained, the system uploads cross-compiled binaries. The system uses the numeric release_id for operations to avoid "softprops" issues where a tag might be briefly undiscoverable, which could otherwise lead to the creation of duplicate drafts.
Sources: scripts/release_transaction.py:120-155, scripts/test_workflow_safety.py:82-89
4. Finalization and Publication
The final step involves a rigorous verification of the draft. The system checks:
- The release is still a draft and owns the correct tag.
- The tag still points to the original expected commit SHA.
- The local assets match the remote assets exactly in terms of filename, size, and SHA-256 digest.
If all checks pass, the release is patched to draft: false, making it public.
Sources: scripts/release_transaction.py:161-197, scripts/test_release_transaction.py:55-78
Release Flow Diagram
The following diagram illustrates the sequence of operations within a release transaction.
This diagram shows the atomic nature of the release process, including the verification steps before publication and the cleanup mechanism for failures.
Sources: scripts/release_transaction.py:53-220, scripts/test_workflow_safety.py:82-89
Core Components and Commands
The release_transaction.py script provides the primary interface for managing these states.
Command Reference
| Command | Purpose | Key Parameters |
|---|---|---|
claim | Creates the tag and draft release. Records release_id to GITHUB_OUTPUT. | --repo, --tag, --commit, --name, --body |
upload | Uploads local files from a directory to the specific draft. | --repo, --release-id, --assets-dir |
finalize | Verifies tag ownership, commit integrity, and asset hashes before publishing. | --repo, --release-id, --tag, --commit, --assets-dir |
cleanup | Safely deletes the draft and tag if they are still owned by the transaction. | --repo, --release-id, --tag, --commit |
Sources: scripts/release_transaction.py:236-266
Implementation Logic for Safety
# scripts/release_transaction.py:161-197
def finalize(repo, release_id, tag, commit, assets_dir, runner):
# Verify the release is still a draft and has the right tag
release = run_gh(["api", f"repos/{repo}/releases/{release_id}"], runner)
if release.get("draft") is not True or release.get("tag_name") != tag:
raise ValueError("release is not the claimed draft")
# Verify the tag hasn't been moved to a different commit
ref = run_gh(["api", f"repos/{repo}/git/ref/tags/{quote(tag, safe='')}"], runner)
if ref.get("object", {}).get("sha") != commit:
raise ValueError("claimed release tag no longer points to the workflow commit")
# Compare local asset hashes against GitHub remote metadata
actual = {asset["name"]: {"size": asset["size"], "digest": asset.get("digest")}
for asset in release.get("assets", [])}
expected = expected_assets(assets_dir)
if actual != expected:
raise ValueError(f"release asset set mismatch")
Sources: scripts/release_transaction.py:161-197
Architecture Invariants
The transaction management system enforces several architectural invariants defined in the project's review guidelines:
- Draft-then-finalize: No release is created directly in a public state. It must be verified after the full asset set is confirmed.
- Immutable release assets: The system refuses to overwrite existing release tags or assets. Changes require a new version or build revision.
- Owner Verification: The
cleanupandfinalizemethods verify that the tag and release are still "owned" (i.e., they are still drafts and still point to the original commit) before acting on them. This prevents the CI from accidentally deleting or modifying releases it did not create. - No Private Keys: The transaction management focuses solely on building and hosting. Signing is offloaded to the
numan-registryrepository.
Sources: REVIEW.md:30-45, README.md:25-35, scripts/release_transaction.py:202-218
Error Handling and Rollback
If a transaction fails at any stage, a cleanup operation is invoked. The cleanup logic is specifically designed to be non-destructive to public data. It will only delete a release if it is still in draft status and the associated tag still points to the specific commit SHA that initiated the transaction. This prevents accidental deletion of previously successful releases if a subsequent CI run fails.
Sources: scripts/release_transaction.py:202-218, scripts/test_release_transaction.py:156-160
Transaction State Machine
This state machine shows the progression from checking existence to final publication, with the cleanup path available for any intermediate failure.
Sources: scripts/release_transaction.py:1-220, scripts/ensure_release_absent.py:38-65
Summary
Release Transaction Management in numan-plugins provides a robust, atomic method for delivering plugin binaries. By utilizing GitHub's draft release features combined with rigorous server-side and client-side verification of commits and asset hashes, the system guarantees that only complete and correct binaries are ever published to the registry. The "fail-closed" design and automated cleanup ensure the repository remains in a clean state even when cross-platform builds encounter environmental issues.