Skip to content

Commit 77d8021

Browse files
authored
Release automation (#3277)
Resolves #3056 --------- Signed-off-by: Yuri Shkuro <github@ysh.us>
1 parent 0dbd3b1 commit 77d8021

File tree

3 files changed

+191
-14
lines changed

3 files changed

+191
-14
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ draft-release:
99
wget https://raw.githubusercontent.com/jaegertracing/jaeger/main/scripts/release/draft.py -O ./scripts/draft-release.py -q
1010
chmod 755 ./scripts/draft-release.py
1111
./scripts/draft-release.py --title "Jaeger UI" --repo jaeger-ui
12+
13+
.PHONY: prepare-release
14+
prepare-release:
15+
@test $(VERSION) || (echo "VERSION is not set. Use 'make prepare-release VERSION=vX.Y.Z'"; exit 1)
16+
python3 scripts/prepare-release.py --version $(VERSION)

RELEASE.md

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22

33
<!-- BEGIN_CHECKLIST -->
44

5-
1. Create and merge, per approval, a PR which preps the release ([example](https://github.com/jaegertracing/jaeger-ui/pull/1767)).
6-
1. The PR title should match the format "Prepare release vX.Y.Z"
7-
- Apply the label `changelog:skip`
8-
2. CHANGELOG.md
9-
- Change the version of the current release from "Next (unreleased)" to "vX.Y.Z (Month D, YYYY)",
10-
where "vX.Y.Z" is the [semver](https://semver.org) for this release.
11-
- Run `make changelog` to list all changes since the last release.
12-
- Review all changes to determine how, if at all, any externally facing APIs are impacted.
13-
This includes, but is not limited to, the UI config and URL routes such as deep-linking
14-
and configuring the embedded mode.
15-
- If necessary, add a note detailing any impact to externally facing APIs.
16-
3. Update `packages/jaeger-ui/package.json#version` to refer to the version being released.
5+
1. Prepare the release.
6+
- Run `make prepare-release VERSION=vX.Y.Z` (replace `vX.Y.Z` with the actual version).
7+
- This command will:
8+
- Verify the version format.
9+
- Create a branch `release-vX.Y.Z`.
10+
- Generate release notes and update `CHANGELOG.md`.
11+
- Update `packages/jaeger-ui/package.json`.
12+
- creating a PR with `changelog:skip` label and title "Prepare release vX.Y.Z".
13+
- **Review the PR**:
14+
- Check `CHANGELOG.md` content.
15+
- Verify `package.json` version.
16+
- Merge the PR once approved.
17+
1718
2. Create a GitHub release.
1819
- Automated (requires [gh](https://cli.github.com/manual/installation)):
1920
- `make draft-release`
@@ -27,5 +28,3 @@
2728
- The tag name for the GitHub release should be the version for the release. It should include the "v", e.g. `v1.0.0`.
2829
- The title of the release match the format "Jaeger UI vX.Y.Z".
2930
- Copy the new CHANGELOG.md section into the release notes.
30-
31-

scripts/prepare-release.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import json
5+
import os
6+
import re
7+
import subprocess
8+
import sys
9+
import tempfile
10+
import time
11+
from datetime import date
12+
13+
def run_command(command, cwd=None, capture_stdout=True, capture_stderr=True):
14+
try:
15+
result = subprocess.run(
16+
command,
17+
cwd=cwd,
18+
check=True,
19+
shell=True,
20+
text=True,
21+
stdout=subprocess.PIPE if capture_stdout else None,
22+
stderr=subprocess.PIPE if capture_stderr else None
23+
)
24+
return result.stdout.strip() if capture_stdout and result.stdout else ""
25+
except subprocess.CalledProcessError as e:
26+
print(f"Error running command: {command}")
27+
if capture_stdout and e.stdout:
28+
print(f"STDOUT: {e.stdout}")
29+
if capture_stderr and e.stderr:
30+
print(f"STDERR: {e.stderr}")
31+
raise
32+
33+
def check_dependencies():
34+
try:
35+
run_command("gh --version")
36+
except:
37+
print("Error: 'gh' CLI is not installed or not in PATH.")
38+
sys.exit(1)
39+
40+
def get_gh_token():
41+
try:
42+
return run_command("gh auth token")
43+
except:
44+
print("Error: Could not retrieve GitHub token using 'gh auth token'. Please login via 'gh auth login'.")
45+
sys.exit(1)
46+
47+
def validate_version(version):
48+
if not re.match(r'^v\d+\.\d+\.\d+$', version):
49+
print(f"Error: Version '{version}' does not match format 'vX.Y.Z'")
50+
sys.exit(1)
51+
52+
def check_git_status():
53+
status = run_command("git status --porcelain")
54+
if status:
55+
print("Error: Git working directory is not clean. Please commit or stash changes.")
56+
sys.exit(1)
57+
58+
def create_branch(version, dry_run=False):
59+
branch_name = f"release-{version}-{int(time.time())}"
60+
if dry_run:
61+
print(f"[Dry Run] Would create branch {branch_name}")
62+
else:
63+
print(f"Creating branch {branch_name}...")
64+
run_command(f"git checkout -b {branch_name}")
65+
return branch_name
66+
67+
def generate_release_notes():
68+
print("Generating release notes via 'make changelog'...")
69+
# Run make -s (silent) to suppress echoing commands, capturing only the script output
70+
# Stream stderr (capture_stderr=False) to show progress bars from release-notes.py
71+
return run_command("make -s changelog", capture_stderr=False)
72+
73+
def update_changelog(version, notes, dry_run=False):
74+
print("Updating CHANGELOG.md...")
75+
changelog_path = "CHANGELOG.md"
76+
with open(changelog_path, 'r') as f:
77+
lines = f.readlines()
78+
79+
# Find insertion point: after </details>
80+
insertion_index = -1
81+
for i, line in enumerate(lines):
82+
if "</details>" in line:
83+
insertion_index = i + 1
84+
break
85+
86+
if insertion_index == -1:
87+
print("Error: Could not find insertion point (</details>) in CHANGELOG.md")
88+
sys.exit(1)
89+
90+
today = date.today().strftime("%Y-%m-%d")
91+
header = f"\n## {version} ({today})\n\n"
92+
93+
# Check if we need to add newlines to notes
94+
if not notes.endswith('\n'):
95+
notes += "\n"
96+
97+
new_content = [header, notes]
98+
99+
updated_lines = lines[:insertion_index] + new_content + lines[insertion_index:]
100+
101+
if dry_run:
102+
print("[Dry Run] Would update CHANGELOG.md with:")
103+
print("".join(new_content))
104+
else:
105+
with open(changelog_path, 'w') as f:
106+
f.writelines(updated_lines)
107+
108+
def update_package_json(version, dry_run=False):
109+
print("Updating package.json...")
110+
path = "packages/jaeger-ui/package.json"
111+
112+
# Strip 'v' for package.json
113+
semver = version[1:]
114+
115+
with open(path, 'r') as f:
116+
data = json.load(f)
117+
118+
data['version'] = semver
119+
120+
if dry_run:
121+
print(f"[Dry Run] Would update package.json version to {semver}")
122+
else:
123+
with open(path, 'w') as f:
124+
json.dump(data, f, indent=2)
125+
f.write('\n') # Add trailing newline
126+
127+
def run_prettier(dry_run=False):
128+
if dry_run:
129+
print("[Dry Run] Would run 'npm run prettier'")
130+
else:
131+
print("Running prettier...")
132+
# Run prettier on the modify files to ensure correct formatting
133+
run_command("npm run prettier -- packages/jaeger-ui/package.json")
134+
135+
def git_commit_and_pr(version, branch_name):
136+
print("Committing changes...")
137+
run_command("git add CHANGELOG.md packages/jaeger-ui/package.json")
138+
commit_msg = f"Prepare release {version}"
139+
run_command(f"git commit -s -m '{commit_msg}'")
140+
141+
print("Pushing branch...")
142+
run_command(f"git push -u origin {branch_name}")
143+
144+
print("Creating Pull Request...")
145+
pr_body = f"Prepare release {version}.\n\nAutomated release preparation."
146+
run_command(f"gh pr create --title '{commit_msg}' --body '{pr_body}' --label 'changelog:skip' --head {branch_name}", capture_stdout=False, capture_stderr=False)
147+
148+
def main():
149+
parser = argparse.ArgumentParser(description="Prepare Jaeger UI release")
150+
parser.add_argument("--version", required=True, help="Release version (e.g., v2.14.0)")
151+
parser.add_argument("--dry-run", action="store_true", help="Skip git push and PR creation")
152+
args = parser.parse_args()
153+
154+
version = args.version
155+
156+
check_dependencies()
157+
# check_git_status() # Optional: strict check, but might be annoying in dev. Uncomment if needed.
158+
validate_version(version)
159+
token = get_gh_token()
160+
161+
notes = generate_release_notes()
162+
update_changelog(version, notes, dry_run=args.dry_run)
163+
update_package_json(version, dry_run=args.dry_run)
164+
run_prettier(dry_run=args.dry_run)
165+
166+
if args.dry_run:
167+
print("Dry run finished. No changes were made.")
168+
else:
169+
branch_name = create_branch(version, dry_run=args.dry_run)
170+
git_commit_and_pr(version, branch_name)
171+
172+
if __name__ == "__main__":
173+
main()

0 commit comments

Comments
 (0)