import argparse
import re
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path
def get_repo_root():
script_dir = Path(__file__).parent.resolve()
return script_dir.parent.parent
def calculate_dates():
tomorrow = datetime.now() + timedelta(days=1)
release_date = tomorrow + timedelta(days=7)
return tomorrow.strftime("%Y-%m-%d"), release_date.strftime("%Y-%m-%d")
def extract_assume_valid_targets(repo_root):
assume_valid_file = repo_root / "util/constant/src/latest_assume_valid_target.rs"
if not assume_valid_file.exists():
print(f"Error: {assume_valid_file} not found", file=sys.stderr)
sys.exit(1)
content = assume_valid_file.read_text()
mainnet_match = re.search(
r'mod mainnet\s*\{.*?DEFAULT_ASSUME_VALID_TARGET.*?"(0x[0-9a-f]+)".*?\n\}',
content,
re.DOTALL,
)
testnet_match = re.search(
r'mod testnet\s*\{.*?DEFAULT_ASSUME_VALID_TARGET.*?"(0x[0-9a-f]+)".*?\n\}',
content,
re.DOTALL,
)
if not mainnet_match or not testnet_match:
print(
f"Error: Could not extract assume valid targets from {assume_valid_file}",
file=sys.stderr,
)
sys.exit(1)
return mainnet_match.group(1), testnet_match.group(1)
def get_last_release_tag(repo_root):
try:
result = subprocess.run(
["git", "tag", "--list", "v[0-9]*", "--sort=-version:refname"],
cwd=repo_root,
capture_output=True,
text=True,
check=True,
)
tags = result.stdout.strip().split("\n")
release_tags = [
tag for tag in tags if re.match(r"^v[0-9]+\.[0-9]+\.[0-9]+$", tag)
]
if not release_tags:
print("Error: Could not find last release tag", file=sys.stderr)
sys.exit(1)
return release_tags[0]
except subprocess.CalledProcessError as e:
print(f"Error running git command: {e}", file=sys.stderr)
sys.exit(1)
def read_changelog(repo_root):
changelog_file = repo_root / "CHANGELOG.md"
if not changelog_file.exists():
print(
f"Warning: {changelog_file} not found. Changelog will be empty.",
file=sys.stderr,
)
return "*No changelog available.*"
content = changelog_file.read_text()
section_pattern = re.compile(r"^##\s*\[([^\]]+)\]", re.MULTILINE)
sections = list(section_pattern.finditer(content))
if not sections:
return "*No release sections found in CHANGELOG.md.*"
def section_content_start(match):
line_end = content.find("\n", match.end())
return (line_end + 1) if line_end != -1 else match.end()
index = 0
if sections[0].group(1).strip().lower() == "unreleased" and len(sections) > 1:
index = 1
start = section_content_start(sections[index])
end = sections[index + 1].start() if index + 1 < len(sections) else len(content)
body = content[start:end].strip()
return body if body else "*No changes listed.*"
def get_current_branch(repo_root):
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=repo_root,
capture_output=True,
text=True,
check=True,
)
branch = result.stdout.strip()
if branch == "HEAD":
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=repo_root,
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
return branch
except subprocess.CalledProcessError:
return None
def get_compare_url(repo_root, last_release):
repo_url = "https://github.com/nervosnetwork/ckb"
current_branch = get_current_branch(repo_root)
if not current_branch:
return None
return f"{repo_url}/compare/{last_release}...{current_branch}"
def get_issue_title(repo_root, last_release):
try:
result = subprocess.run(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=repo_root,
capture_output=True,
text=True,
check=True,
)
branch = result.stdout.strip()
rc_match = re.match(r"rc/v([0-9]+\.[0-9]+)\.x", branch)
if rc_match:
version = rc_match.group(1)
return f"Release v{version}.0"
version_match = re.match(r"v([0-9]+)\.([0-9]+)\.([0-9]+)", last_release)
if version_match:
major, minor, patch = version_match.groups()
next_minor = int(minor) + 1
return f"Release v{major}.{next_minor} RC"
except subprocess.CalledProcessError:
pass
return "Release RC"
def create_issue_with_gh(title, body, dry_run=False):
if dry_run:
print("=" * 80, file=sys.stderr)
print("DRY RUN MODE - Issue would be created with:", file=sys.stderr)
print("=" * 80, file=sys.stderr)
print(f"\nTitle: {title}\n", file=sys.stderr)
print("Body:", file=sys.stderr)
print("-" * 80, file=sys.stderr)
print(body, file=sys.stderr)
print("-" * 80, file=sys.stderr)
return None
try:
subprocess.run(["gh", "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print(
"Error: 'gh' CLI tool not found. Please install it from https://cli.github.com/",
file=sys.stderr,
)
sys.exit(1)
try:
result = subprocess.run(
["gh", "issue", "create", "--title", title, "--body", body],
capture_output=True,
text=True,
check=True,
)
issue_url = result.stdout.strip()
print(f"Created issue: {issue_url}", file=sys.stderr)
return issue_url
except subprocess.CalledProcessError as e:
print(f"Error creating issue: {e}", file=sys.stderr)
if e.stderr:
print(e.stderr, file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Create a release issue using the template"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the issue content without creating it",
)
args = parser.parse_args()
repo_root = get_repo_root()
rc_date, release_date = calculate_dates()
mainnet_hash, testnet_hash = extract_assume_valid_targets(repo_root)
last_release = get_last_release_tag(repo_root)
changelog = read_changelog(repo_root)
compare_url = get_compare_url(repo_root, last_release)
compare_section = ""
if compare_url:
compare_section = f"\n[Compare changes]({compare_url})\n"
body = f"""- RC Date: {rc_date}
- Release Date: {release_date}
- Assume Valid Target: (Can be found in the file util/constant/src/latest_assume_valid_target.rs)
- Mainnet: [{mainnet_hash}](https://explorer.nervos.org/block/{mainnet_hash})
- Testnet: [{testnet_hash}](https://testnet.explorer.nervos.org/block/{testnet_hash})
## Changes since {last_release}{compare_section}
{changelog}
"""
title = get_issue_title(repo_root, last_release)
create_issue_with_gh(title, body, dry_run=args.dry_run)
if __name__ == "__main__":
main()