from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class Decision:
action: str
reason: str
def as_github_output(self, github_output: Optional[str]) -> None:
if github_output is None:
return
dispatch = "true" if self.action == "dispatch" else "false"
with open(github_output, "a", encoding="utf-8") as handle:
handle.write(f"dispatch={dispatch}\n")
def decide_release_tag(
*,
tag_exists: bool,
tag_sha: Optional[str],
target_sha: str,
has_successful_release: bool,
has_active_release: bool,
) -> Decision:
if not target_sha:
return Decision("fail", "target SHA is empty")
if tag_exists:
if tag_sha is None:
return Decision(
"fail",
"tag exists but its SHA could not be resolved; refusing to retry",
)
if tag_sha != target_sha:
return Decision(
"fail",
(
f"version tag exists at {tag_sha} but the successful CI "
f"head is {target_sha}; version tags are immutable and "
"cannot be moved — bump the version number in Cargo.toml"
),
)
if has_successful_release:
return Decision(
"skip",
"tag already has a successful Release run",
)
if has_active_release:
return Decision(
"skip",
"tag already has an active Release run",
)
return Decision(
"dispatch",
f"retrying immutable tag at {tag_sha} with the current Release workflow",
)
return Decision(
"dispatch",
f"creating new immutable tag at {target_sha}",
)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--tag-exists", action="store_true")
parser.add_argument("--tag-sha", default=None)
parser.add_argument("--target-sha", required=True)
parser.add_argument("--has-successful-release", action="store_true")
parser.add_argument("--has-active-release", action="store_true")
parser.add_argument("--github-output", default=None)
parser.add_argument(
"--json",
action="store_true",
help="emit the decision as JSON on stdout (for tests and CI logs)",
)
args = parser.parse_args()
decision = decide_release_tag(
tag_exists=args.tag_exists,
tag_sha=args.tag_sha,
target_sha=args.target_sha,
has_successful_release=args.has_successful_release,
has_active_release=args.has_active_release,
)
if decision.action == "fail":
print(f"::error::{decision.reason}", file=sys.stderr)
if args.json:
print(json.dumps({"action": decision.action, "reason": decision.reason}))
else:
print(decision.reason)
decision.as_github_output(args.github_output)
return 0 if decision.action != "fail" else 1
if __name__ == "__main__":
raise SystemExit(main())