664 lines
21 KiB
Python
Executable file
664 lines
21 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BUILD_FILE = ROOT / "app" / "build.gradle.kts"
|
|
RELEASE_OUTPUT_DIR = ROOT / "build" / "release"
|
|
APK_DIR = ROOT / "app" / "build" / "outputs" / "apk" / "release"
|
|
DEFAULT_BETA_NOTICE = (
|
|
"## This is a beta version intended for testing only. Expect breaking changes "
|
|
"in updates. Normal users are advised to wait for the stable release."
|
|
)
|
|
EXPECTED_ASSET_NAMES = [
|
|
"app-arm64-v8a-release.apk",
|
|
"app-armeabi-v7a-release.apk",
|
|
"app-x86_64-release.apk",
|
|
"app-x86-release.apk",
|
|
"app-universal-release.apk",
|
|
]
|
|
VERSION_NAME_RE = re.compile(r'(?m)^(\s*versionName\s*=\s*")([^"]+)(")')
|
|
VERSION_CODE_RE = re.compile(r"(?m)^(\s*versionCode\s*=\s*)(\d+)")
|
|
VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
|
|
PREFIX_RE = re.compile(
|
|
r"^(feat|fix|ref|refactor|perf|ui|ux|build|style|docs|test|ci|chore)"
|
|
r"(\([^)]+\))?:\s*",
|
|
re.IGNORECASE,
|
|
)
|
|
LEADING_ISSUE_RE = re.compile(
|
|
r"^(fixe?[sd]?|close[sd]?|resolve[sd]?|address(?:e[sd])?)\s*#\d+\s*,?\s*",
|
|
re.IGNORECASE,
|
|
)
|
|
DROP_PATTERNS = (
|
|
re.compile(r"^merge\b", re.IGNORECASE),
|
|
re.compile(r"^revert\b", re.IGNORECASE),
|
|
re.compile(r"^docs?[:\s]", re.IGNORECASE),
|
|
re.compile(r"^ci[:\s]", re.IGNORECASE),
|
|
re.compile(r"^test[:\s]", re.IGNORECASE),
|
|
re.compile(r"^chore[:\s]", re.IGNORECASE),
|
|
re.compile(r"^bump\b", re.IGNORECASE),
|
|
re.compile(r"^update .*translation", re.IGNORECASE),
|
|
re.compile(r"^merge .*translation", re.IGNORECASE),
|
|
re.compile(r"^close unlabeled issues workflow", re.IGNORECASE),
|
|
re.compile(r"^update issue reporting title rule", re.IGNORECASE),
|
|
re.compile(r"^merge conflict$", re.IGNORECASE),
|
|
re.compile(r"^update .* version to ", re.IGNORECASE),
|
|
re.compile(r"^update .*\.(kt|java|xml|gradle\.kts?)$", re.IGNORECASE),
|
|
re.compile(r"baselineprofile", re.IGNORECASE),
|
|
)
|
|
TRIM_TOKENS = ("cw", "wip")
|
|
WORD_REPLACEMENTS = (
|
|
(re.compile(r"\bcw\b", re.IGNORECASE), ""),
|
|
(re.compile(r"\bhomelayouts\b", re.IGNORECASE), "Home layouts"),
|
|
(re.compile(r"\bshowInHome param\b", re.IGNORECASE), "`showInHome` parameter"),
|
|
(re.compile(r"\bui\b"), "UI"),
|
|
(re.compile(r"\btrakt\b", re.IGNORECASE), "Trakt"),
|
|
(re.compile(r"\bnuvio\b", re.IGNORECASE), "Nuvio"),
|
|
)
|
|
ASSET_ORDER = {
|
|
"arm64-v8a": 0,
|
|
"armeabi-v7a": 1,
|
|
"x86_64": 2,
|
|
"x86": 3,
|
|
"universal": 4,
|
|
}
|
|
|
|
|
|
def run(
|
|
*args: str,
|
|
check: bool = True,
|
|
capture_output: bool = True,
|
|
cwd: Path = ROOT,
|
|
env: dict[str, str] | None = None,
|
|
) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
args,
|
|
cwd=cwd,
|
|
check=check,
|
|
text=True,
|
|
capture_output=capture_output,
|
|
env=env,
|
|
)
|
|
|
|
|
|
def git(*args: str, check: bool = True) -> str:
|
|
return run("git", *args, check=check).stdout.strip()
|
|
|
|
|
|
def require_tool(name: str) -> None:
|
|
if shutil.which(name) is None:
|
|
raise SystemExit(f"Missing required tool: {name}")
|
|
|
|
|
|
def read_build_file() -> str:
|
|
return BUILD_FILE.read_text(encoding="utf-8-sig")
|
|
|
|
|
|
def parse_versions(contents: str) -> tuple[str, int]:
|
|
version_match = VERSION_NAME_RE.search(contents)
|
|
code_match = VERSION_CODE_RE.search(contents)
|
|
if version_match is None or code_match is None:
|
|
raise SystemExit(f"Failed to find versionName/versionCode in {BUILD_FILE}")
|
|
return version_match.group(2), int(code_match.group(2))
|
|
|
|
|
|
def updated_build_file(contents: str, version_name: str, version_code: int) -> str:
|
|
contents, version_replacements = VERSION_NAME_RE.subn(
|
|
rf"\g<1>{version_name}\g<3>", contents, count=1
|
|
)
|
|
contents, code_replacements = VERSION_CODE_RE.subn(
|
|
rf"\g<1>{version_code}", contents, count=1
|
|
)
|
|
if version_replacements != 1 or code_replacements != 1:
|
|
raise SystemExit(f"Failed to update versionName/versionCode in {BUILD_FILE}")
|
|
return contents
|
|
|
|
|
|
def write_build_file(contents: str) -> None:
|
|
BUILD_FILE.write_text(contents, encoding="utf-8-sig")
|
|
|
|
|
|
def last_tag() -> str | None:
|
|
result = run("git", "describe", "--tags", "--abbrev=0", check=False)
|
|
tag = result.stdout.strip()
|
|
return tag or None
|
|
|
|
|
|
def release_range(previous_tag: str | None) -> str | None:
|
|
if previous_tag:
|
|
return f"{previous_tag}..HEAD"
|
|
return None
|
|
|
|
|
|
def commit_subjects(previous_tag: str | None) -> list[str]:
|
|
args = ["log", "--reverse", "--pretty=format:%s"]
|
|
revision_range = release_range(previous_tag)
|
|
if revision_range:
|
|
args.append(revision_range)
|
|
output = git(*args)
|
|
return [line.strip() for line in output.splitlines() if line.strip()]
|
|
|
|
|
|
def normalize_subject(subject: str) -> str | None:
|
|
if not subject:
|
|
return None
|
|
|
|
candidate = subject.strip()
|
|
for pattern in DROP_PATTERNS:
|
|
if pattern.search(candidate):
|
|
return None
|
|
|
|
candidate = PREFIX_RE.sub("", candidate)
|
|
while True:
|
|
updated = LEADING_ISSUE_RE.sub("", candidate)
|
|
if updated == candidate:
|
|
break
|
|
candidate = updated
|
|
|
|
candidate = candidate.strip(" .")
|
|
if not candidate:
|
|
return None
|
|
|
|
if candidate.lower().startswith(("with ", "one more ", "trying ")):
|
|
return None
|
|
|
|
words = candidate.split()
|
|
while words and words[-1].lower() in TRIM_TOKENS:
|
|
words.pop()
|
|
candidate = " ".join(words).strip()
|
|
if not candidate:
|
|
return None
|
|
|
|
for pattern, replacement in WORD_REPLACEMENTS:
|
|
candidate = pattern.sub(replacement, candidate)
|
|
candidate = re.sub(r"\s{2,}", " ", candidate).strip()
|
|
if not candidate:
|
|
return None
|
|
|
|
if candidate.islower():
|
|
candidate = candidate[:1].upper() + candidate[1:]
|
|
|
|
return candidate
|
|
|
|
|
|
def dedupe_keep_latest(items: list[str], limit: int) -> list[str]:
|
|
deduped: list[str] = []
|
|
seen: set[str] = set()
|
|
for item in items:
|
|
key = item.casefold()
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
deduped.append(item)
|
|
if limit > 0 and len(deduped) > limit:
|
|
deduped = deduped[-limit:]
|
|
return deduped
|
|
|
|
|
|
def parse_extra_notes(notes_text: str | None, extra_lines: list[str]) -> list[str]:
|
|
parsed: list[str] = []
|
|
if notes_text:
|
|
for raw_line in notes_text.splitlines():
|
|
line = raw_line.strip()
|
|
if not line:
|
|
continue
|
|
if line.startswith("- "):
|
|
line = line[2:].strip()
|
|
parsed.append(line)
|
|
parsed.extend(line.strip() for line in extra_lines if line.strip())
|
|
return parsed
|
|
|
|
|
|
def build_release_notes(
|
|
previous_tag: str | None,
|
|
max_items: int,
|
|
downloader_code: str | None,
|
|
extra_notes: str | None,
|
|
extra_lines: list[str],
|
|
custom_notes: str | None,
|
|
) -> str:
|
|
if custom_notes and custom_notes.strip():
|
|
return custom_notes.strip() + "\n"
|
|
|
|
normalized = [
|
|
note
|
|
for note in (normalize_subject(subject) for subject in commit_subjects(previous_tag))
|
|
if note
|
|
]
|
|
bullet_items = dedupe_keep_latest(normalized, max_items)
|
|
bullet_items.extend(parse_extra_notes(extra_notes, extra_lines))
|
|
|
|
if not bullet_items:
|
|
bullet_items = ["Beta maintenance update"]
|
|
|
|
lines = [DEFAULT_BETA_NOTICE, "", "### Improvements & Fixes"]
|
|
lines.extend(f"- {item}" for item in bullet_items)
|
|
if downloader_code:
|
|
lines.extend(["", f"### Downloader Code - {downloader_code.strip()}"])
|
|
return "\n".join(lines).strip() + "\n"
|
|
|
|
|
|
def release_notes_path(version_name: str) -> Path:
|
|
RELEASE_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
safe_name = version_name.replace("/", "-")
|
|
return RELEASE_OUTPUT_DIR / f"release-notes-{safe_name}.md"
|
|
|
|
|
|
def write_release_notes(version_name: str, notes: str) -> Path:
|
|
path = release_notes_path(version_name)
|
|
path.write_text(notes, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def append_job_summary(
|
|
*,
|
|
mode: str,
|
|
version_name: str,
|
|
release_tag: str,
|
|
release_title: str,
|
|
commit_message: str,
|
|
version_code: int,
|
|
previous_tag: str | None,
|
|
notes_path: Path,
|
|
assets: list[Path] | None,
|
|
) -> None:
|
|
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
|
|
if not summary_path:
|
|
return
|
|
|
|
lines = [
|
|
"## Beta Release Preview",
|
|
"",
|
|
f"- Mode: `{mode}`",
|
|
f"- Version: `{version_name}`",
|
|
f"- Tag: `{release_tag}`",
|
|
f"- Title: `{release_title}`",
|
|
f"- Commit message: `{commit_message}`",
|
|
f"- versionCode: `{version_code}`",
|
|
f"- Previous tag: `{previous_tag or 'none'}`",
|
|
f"- Notes file: `{notes_path.relative_to(ROOT)}`",
|
|
"",
|
|
"### Release Notes",
|
|
"",
|
|
notes_path.read_text(encoding="utf-8").strip(),
|
|
"",
|
|
"### Assets",
|
|
]
|
|
|
|
if assets:
|
|
lines.extend(f"- `{path.name}`" for path in assets)
|
|
else:
|
|
lines.extend(f"- `{name}`" for name in EXPECTED_ASSET_NAMES)
|
|
|
|
lines.append("")
|
|
with open(summary_path, "a", encoding="utf-8") as handle:
|
|
handle.write("\n".join(lines))
|
|
|
|
|
|
def current_branch() -> str:
|
|
branch = git("rev-parse", "--abbrev-ref", "HEAD")
|
|
if branch == "HEAD":
|
|
branch = os.environ.get("GITHUB_REF_NAME", "").strip()
|
|
if not branch:
|
|
raise SystemExit("Unable to determine the current branch for publishing")
|
|
return branch
|
|
|
|
|
|
def ensure_clean_worktree() -> None:
|
|
status = git("status", "--short")
|
|
if status:
|
|
raise SystemExit(
|
|
"Refusing to publish from a dirty worktree. Commit or stash changes first."
|
|
)
|
|
|
|
|
|
def ensure_version_available(release_tag: str) -> None:
|
|
tag_check = run(
|
|
"git", "rev-parse", "-q", "--verify", f"refs/tags/{release_tag}", check=False
|
|
)
|
|
if tag_check.returncode == 0:
|
|
raise SystemExit(f"Tag already exists: {release_tag}")
|
|
|
|
|
|
def build_release() -> list[Path]:
|
|
subprocess.run(
|
|
["./gradlew", "app:assembleRelease"],
|
|
cwd=ROOT,
|
|
check=True,
|
|
text=True,
|
|
)
|
|
assets = sorted(
|
|
APK_DIR.glob("*.apk"),
|
|
key=lambda path: next(
|
|
(order for token, order in ASSET_ORDER.items() if token in path.name),
|
|
999,
|
|
),
|
|
)
|
|
if not assets:
|
|
raise SystemExit(f"No APK assets found in {APK_DIR}")
|
|
return assets
|
|
|
|
|
|
def commit_tag_push(
|
|
release_tag: str,
|
|
release_title: str,
|
|
commit_message: str,
|
|
branch_name: str,
|
|
) -> None:
|
|
subprocess.run(["git", "add", str(BUILD_FILE.relative_to(ROOT))], cwd=ROOT, check=True)
|
|
subprocess.run(
|
|
["git", "commit", "-m", commit_message],
|
|
cwd=ROOT,
|
|
check=True,
|
|
text=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "tag", "-a", release_tag, "-m", f"Release {release_title}"],
|
|
cwd=ROOT,
|
|
check=True,
|
|
text=True,
|
|
)
|
|
subprocess.run(["git", "push", "origin", f"HEAD:{branch_name}"], cwd=ROOT, check=True)
|
|
subprocess.run(["git", "push", "origin", release_tag], cwd=ROOT, check=True)
|
|
|
|
|
|
def tag_push(
|
|
release_tag: str,
|
|
release_title: str,
|
|
branch_name: str,
|
|
) -> None:
|
|
subprocess.run(
|
|
["git", "tag", "-a", release_tag, "-m", f"Release {release_title}"],
|
|
cwd=ROOT,
|
|
check=True,
|
|
text=True,
|
|
)
|
|
subprocess.run(["git", "push", "origin", f"HEAD:{branch_name}"], cwd=ROOT, check=True)
|
|
subprocess.run(["git", "push", "origin", release_tag], cwd=ROOT, check=True)
|
|
|
|
|
|
def create_github_release(
|
|
release_tag: str,
|
|
release_title: str,
|
|
notes_path: Path,
|
|
assets: list[Path],
|
|
*,
|
|
draft: bool,
|
|
) -> None:
|
|
require_tool("gh")
|
|
command = [
|
|
"gh",
|
|
"release",
|
|
"create",
|
|
release_tag,
|
|
*[str(asset) for asset in assets],
|
|
"--title",
|
|
release_title,
|
|
"--notes-file",
|
|
str(notes_path),
|
|
]
|
|
if draft:
|
|
command.append("--draft")
|
|
else:
|
|
command.append("--latest")
|
|
subprocess.run(command, cwd=ROOT, check=True, text=True)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Preview, draft, or publish a beta GitHub release by either bumping the app "
|
|
"version or manually supplying the release tag/title, then building APKs and "
|
|
"generating release notes from recent commits."
|
|
)
|
|
)
|
|
parser.add_argument(
|
|
"version",
|
|
nargs="?",
|
|
help="Target versionName, for example 0.4.19-beta. Required unless --manual-release is set.",
|
|
)
|
|
parser.add_argument(
|
|
"--manual-release",
|
|
action="store_true",
|
|
help="Skip changing Gradle version metadata and publish using the provided tag/title.",
|
|
)
|
|
parser.add_argument(
|
|
"--version-code",
|
|
type=int,
|
|
help="Explicit versionCode to write. Defaults to current versionCode + 1. Ignored in manual mode.",
|
|
)
|
|
parser.add_argument(
|
|
"--release-tag",
|
|
help="Git tag to create for the release. Defaults to the target versionName.",
|
|
)
|
|
parser.add_argument(
|
|
"--release-title",
|
|
help="GitHub release title. Defaults to the release tag.",
|
|
)
|
|
parser.add_argument(
|
|
"--commit-message",
|
|
help="Git commit message for the version bump. Defaults to 'release: <release-tag>'.",
|
|
)
|
|
parser.add_argument(
|
|
"--custom-notes",
|
|
help="Full release notes markdown. When set, it replaces the generated notes entirely.",
|
|
)
|
|
parser.add_argument(
|
|
"--custom-notes-file",
|
|
help="Path to a markdown file whose contents replace the generated release notes entirely.",
|
|
)
|
|
parser.add_argument(
|
|
"--downloader-code",
|
|
help="Optional release-note footer value for the downloader code",
|
|
)
|
|
parser.add_argument(
|
|
"--extra-notes",
|
|
help="Optional multi-line text. Each non-empty line is appended as a bullet.",
|
|
)
|
|
parser.add_argument(
|
|
"--extra-line",
|
|
action="append",
|
|
default=[],
|
|
help="Append one extra bullet to the Improvements & Fixes list.",
|
|
)
|
|
parser.add_argument(
|
|
"--max-items",
|
|
type=int,
|
|
default=10,
|
|
help="Maximum number of generated commit bullets to keep.",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show the generated release notes without editing files, building, or publishing.",
|
|
)
|
|
parser.add_argument(
|
|
"--publish",
|
|
action="store_true",
|
|
help="Build all release APKs, commit the version bump, push tag/branch, and create the GitHub release.",
|
|
)
|
|
parser.add_argument(
|
|
"--draft",
|
|
action="store_true",
|
|
help="Build all release APKs, commit the version bump, push tag/branch, and create the GitHub release as a draft.",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
|
|
selected_modes = [args.dry_run, args.publish, args.draft]
|
|
if sum(1 for enabled in selected_modes if enabled) > 1:
|
|
raise SystemExit("Use only one of --dry-run, --draft, or --publish.")
|
|
if args.custom_notes and args.custom_notes_file:
|
|
raise SystemExit("Use either --custom-notes or --custom-notes-file, not both.")
|
|
|
|
original_contents = read_build_file()
|
|
current_version_name, current_version_code = parse_versions(original_contents)
|
|
previous_tag = last_tag()
|
|
|
|
if args.manual_release:
|
|
if args.version:
|
|
raise SystemExit("Do not provide version when --manual-release is set.")
|
|
if args.version_code is not None:
|
|
raise SystemExit("--version-code is not supported when --manual-release is set.")
|
|
if not args.release_tag:
|
|
raise SystemExit("--release-tag is required when --manual-release is set.")
|
|
if not args.release_title:
|
|
raise SystemExit("--release-title is required when --manual-release is set.")
|
|
target_version_name = current_version_name
|
|
next_version_code = current_version_code
|
|
release_tag = args.release_tag
|
|
release_title = args.release_title
|
|
commit_message = "manual release: no version bump"
|
|
notes_key = release_tag
|
|
else:
|
|
if not args.version:
|
|
raise SystemExit("version is required unless --manual-release is set.")
|
|
if not VERSION_PATTERN.match(args.version):
|
|
raise SystemExit(f"Invalid version format: {args.version}")
|
|
target_version_name = args.version
|
|
next_version_code = (
|
|
args.version_code if args.version_code is not None else current_version_code + 1
|
|
)
|
|
if next_version_code < 1:
|
|
raise SystemExit("versionCode must be a positive integer.")
|
|
release_tag = args.release_tag or target_version_name
|
|
release_title = args.release_title or release_tag
|
|
commit_message = args.commit_message or f"release: {release_tag}"
|
|
notes_key = target_version_name
|
|
|
|
custom_notes = args.custom_notes
|
|
if args.custom_notes_file:
|
|
custom_notes = Path(args.custom_notes_file).read_text(encoding="utf-8")
|
|
notes = build_release_notes(
|
|
previous_tag=previous_tag,
|
|
max_items=args.max_items,
|
|
downloader_code=args.downloader_code,
|
|
extra_notes=args.extra_notes,
|
|
extra_lines=args.extra_line,
|
|
custom_notes=custom_notes,
|
|
)
|
|
notes_path = write_release_notes(notes_key, notes)
|
|
|
|
print(f"Current versionName: {current_version_name}")
|
|
print(f"Current versionCode: {current_version_code}")
|
|
print(f"Target versionName: {target_version_name}")
|
|
print(f"Release tag: {release_tag}")
|
|
print(f"Release title: {release_title}")
|
|
print(f"Commit message: {commit_message}")
|
|
print(f"Target versionCode: {next_version_code}")
|
|
print(f"Previous tag: {previous_tag or 'none'}")
|
|
print(f"Release notes: {notes_path.relative_to(ROOT)}")
|
|
print()
|
|
print(notes.strip())
|
|
print()
|
|
|
|
mode = (
|
|
"dry-run" if args.dry_run
|
|
else "draft" if args.draft
|
|
else "publish" if args.publish
|
|
else "local-build"
|
|
)
|
|
|
|
if args.dry_run:
|
|
print("Dry run expected assets:")
|
|
for asset_name in EXPECTED_ASSET_NAMES:
|
|
print(f"- {asset_name}")
|
|
append_job_summary(
|
|
mode=mode,
|
|
version_name=target_version_name,
|
|
release_tag=release_tag,
|
|
release_title=release_title,
|
|
commit_message=commit_message,
|
|
version_code=next_version_code,
|
|
previous_tag=previous_tag,
|
|
notes_path=notes_path,
|
|
assets=None,
|
|
)
|
|
return 0
|
|
|
|
if args.publish or args.draft:
|
|
ensure_clean_worktree()
|
|
ensure_version_available(release_tag)
|
|
|
|
updated_contents = original_contents
|
|
if not args.manual_release:
|
|
updated_contents = updated_build_file(
|
|
original_contents,
|
|
version_name=target_version_name,
|
|
version_code=next_version_code,
|
|
)
|
|
write_build_file(updated_contents)
|
|
print(f"Updated {BUILD_FILE.relative_to(ROOT)}")
|
|
|
|
assets: list[Path] = []
|
|
try:
|
|
assets = build_release()
|
|
print("Built release assets:")
|
|
for asset in assets:
|
|
print(f"- {asset.relative_to(ROOT)}")
|
|
|
|
if args.publish or args.draft:
|
|
branch_name = current_branch()
|
|
if args.manual_release:
|
|
tag_push(release_tag, release_title, branch_name)
|
|
else:
|
|
commit_tag_push(
|
|
release_tag, release_title, commit_message, branch_name
|
|
)
|
|
create_github_release(
|
|
release_tag,
|
|
release_title,
|
|
notes_path,
|
|
assets,
|
|
draft=args.draft,
|
|
)
|
|
if args.draft:
|
|
print(
|
|
f"Created draft GitHub release {release_tag} "
|
|
f"({release_title}) from branch {branch_name}"
|
|
)
|
|
else:
|
|
print(
|
|
f"Published GitHub release {release_tag} "
|
|
f"({release_title}) from branch {branch_name}"
|
|
)
|
|
except Exception:
|
|
if not (args.publish or args.draft):
|
|
write_build_file(original_contents)
|
|
raise
|
|
|
|
append_job_summary(
|
|
mode=mode,
|
|
version_name=target_version_name,
|
|
release_tag=release_tag,
|
|
release_title=release_title,
|
|
commit_message=commit_message,
|
|
version_code=next_version_code,
|
|
previous_tag=previous_tag,
|
|
notes_path=notes_path,
|
|
assets=assets,
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except subprocess.CalledProcessError as exc:
|
|
if exc.stdout:
|
|
sys.stdout.write(exc.stdout)
|
|
if exc.stderr:
|
|
sys.stderr.write(exc.stderr)
|
|
raise SystemExit(exc.returncode) from exc
|