rlsbl 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -5
- package/rlsbl/__init__.py +0 -247
- package/rlsbl/__main__.py +0 -4
- package/rlsbl/commands/__init__.py +0 -0
- package/rlsbl/commands/check.py +0 -229
- package/rlsbl/commands/config.py +0 -67
- package/rlsbl/commands/discover.py +0 -198
- package/rlsbl/commands/init_cmd.py +0 -518
- package/rlsbl/commands/pre_push_check.py +0 -46
- package/rlsbl/commands/record_gif.py +0 -92
- package/rlsbl/commands/release.py +0 -287
- package/rlsbl/commands/status.py +0 -76
- package/rlsbl/commands/undo.py +0 -74
- package/rlsbl/commands/watch.py +0 -125
- package/rlsbl/config.py +0 -57
- package/rlsbl/registries/__init__.py +0 -5
- package/rlsbl/registries/go.py +0 -123
- package/rlsbl/registries/npm.py +0 -119
- package/rlsbl/registries/pypi.py +0 -171
- package/rlsbl/tagging.py +0 -207
- package/rlsbl/templates/go/VERSION.tpl +0 -1
- package/rlsbl/templates/go/ci.yml.tpl +0 -18
- package/rlsbl/templates/go/goreleaser.yml.tpl +0 -25
- package/rlsbl/templates/go/publish.yml.tpl +0 -25
- package/rlsbl/templates/merged/publish.yml.tpl +0 -30
- package/rlsbl/templates/npm/ci.yml.tpl +0 -22
- package/rlsbl/templates/npm/publish.yml.tpl +0 -22
- package/rlsbl/templates/pypi/ci.yml.tpl +0 -20
- package/rlsbl/templates/pypi/publish.yml.tpl +0 -18
- package/rlsbl/templates/shared/CHANGELOG.md.tpl +0 -5
- package/rlsbl/templates/shared/CLAUDE.md.tpl +0 -20
- package/rlsbl/templates/shared/LICENSE.tpl +0 -21
- package/rlsbl/templates/shared/claude-settings.json.tpl +0 -3
- package/rlsbl/templates/shared/gitignore.tpl +0 -14
- package/rlsbl/templates/shared/hooks/post-release.sh.tpl +0 -8
- package/rlsbl/templates/shared/hooks/pre-release.sh.tpl +0 -31
- package/rlsbl/utils.py +0 -131
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
"""Release command: bump version, commit, push, create GitHub Release."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import time
|
|
6
|
-
|
|
7
|
-
from ..config import should_tag
|
|
8
|
-
from ..registries import REGISTRIES
|
|
9
|
-
from ..tagging import ensure_github_topic, ensure_npm_keyword, ensure_pypi_keyword
|
|
10
|
-
from ..utils import (
|
|
11
|
-
bump_version,
|
|
12
|
-
check_gh_auth,
|
|
13
|
-
check_gh_installed,
|
|
14
|
-
extract_changelog_entry,
|
|
15
|
-
find_commit_tool,
|
|
16
|
-
get_current_branch,
|
|
17
|
-
get_push_timeout,
|
|
18
|
-
is_clean_tree,
|
|
19
|
-
push_if_needed,
|
|
20
|
-
run,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
VALID_BUMP_TYPES = ("patch", "minor", "major")
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def run_cmd(registry, args, flags):
|
|
27
|
-
"""Release command handler.
|
|
28
|
-
|
|
29
|
-
Bumps version, commits, pushes, and creates a GitHub Release.
|
|
30
|
-
"""
|
|
31
|
-
quiet = flags.get("quiet", False)
|
|
32
|
-
|
|
33
|
-
def log(msg):
|
|
34
|
-
if not quiet:
|
|
35
|
-
print(msg)
|
|
36
|
-
|
|
37
|
-
reg = REGISTRIES[registry]
|
|
38
|
-
|
|
39
|
-
# Check prerequisites
|
|
40
|
-
if not check_gh_installed():
|
|
41
|
-
print("Error: gh CLI is not installed. Install it from https://cli.github.com", file=sys.stderr)
|
|
42
|
-
sys.exit(1)
|
|
43
|
-
if not check_gh_auth():
|
|
44
|
-
print('Error: gh CLI is not authenticated. Run "gh auth login" first.', file=sys.stderr)
|
|
45
|
-
sys.exit(1)
|
|
46
|
-
|
|
47
|
-
# Clean working tree
|
|
48
|
-
if not is_clean_tree():
|
|
49
|
-
print("Error: working tree is not clean. Commit your changes first.", file=sys.stderr)
|
|
50
|
-
sys.exit(1)
|
|
51
|
-
|
|
52
|
-
# Branch check
|
|
53
|
-
branch = get_current_branch()
|
|
54
|
-
if branch not in ("main", "master"):
|
|
55
|
-
print(f'Warning: you are on branch "{branch}", not main/master.', file=sys.stderr)
|
|
56
|
-
|
|
57
|
-
# Current version
|
|
58
|
-
current_version = reg.read_version(".")
|
|
59
|
-
log(f"Current version: {current_version}")
|
|
60
|
-
|
|
61
|
-
# If the current version has never been tagged, release it as-is (bootstrap)
|
|
62
|
-
current_tag = f"v{current_version}"
|
|
63
|
-
current_tag_exists = len(run("git", ["tag", "-l", current_tag])) > 0
|
|
64
|
-
|
|
65
|
-
if not current_tag_exists:
|
|
66
|
-
new_version = current_version
|
|
67
|
-
bump_type = None
|
|
68
|
-
tag = current_tag
|
|
69
|
-
if args:
|
|
70
|
-
log(f"First release: releasing {new_version} as-is (bump type ignored)")
|
|
71
|
-
else:
|
|
72
|
-
log(f"First release: {new_version}")
|
|
73
|
-
else:
|
|
74
|
-
bump_type = args[0] if args else "patch"
|
|
75
|
-
if bump_type not in VALID_BUMP_TYPES:
|
|
76
|
-
print(
|
|
77
|
-
f'Error: invalid bump type "{bump_type}". Use: {", ".join(VALID_BUMP_TYPES)}',
|
|
78
|
-
file=sys.stderr,
|
|
79
|
-
)
|
|
80
|
-
sys.exit(1)
|
|
81
|
-
|
|
82
|
-
new_version = bump_version(current_version, bump_type)
|
|
83
|
-
tag = f"v{new_version}"
|
|
84
|
-
log(f"New version: {new_version} ({bump_type})")
|
|
85
|
-
|
|
86
|
-
# Check tag doesn't already exist
|
|
87
|
-
tag_output = run("git", ["tag", "-l", tag])
|
|
88
|
-
if len(tag_output) > 0:
|
|
89
|
-
print(f'Error: tag "{tag}" already exists.', file=sys.stderr)
|
|
90
|
-
sys.exit(1)
|
|
91
|
-
|
|
92
|
-
# Validate changelog entry
|
|
93
|
-
changelog_path = os.path.join(".", "CHANGELOG.md")
|
|
94
|
-
if not os.path.exists(changelog_path):
|
|
95
|
-
print(
|
|
96
|
-
f"Error: CHANGELOG.md not found. Create one with a ## {new_version} section.",
|
|
97
|
-
file=sys.stderr,
|
|
98
|
-
)
|
|
99
|
-
sys.exit(1)
|
|
100
|
-
changelog_entry = extract_changelog_entry(changelog_path, new_version)
|
|
101
|
-
if not changelog_entry:
|
|
102
|
-
print(
|
|
103
|
-
f"Error: no changelog entry found for version {new_version} in CHANGELOG.md.",
|
|
104
|
-
file=sys.stderr,
|
|
105
|
-
)
|
|
106
|
-
print(f'Add a "## {new_version}" section describing the changes.', file=sys.stderr)
|
|
107
|
-
sys.exit(1)
|
|
108
|
-
if len(changelog_entry.strip()) < 10:
|
|
109
|
-
print(
|
|
110
|
-
f"Warning: changelog entry for {new_version} is very short. Consider adding more detail.",
|
|
111
|
-
file=sys.stderr,
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
# Run pre-release hook if present
|
|
115
|
-
pre_release_script = os.path.join(".", ".rlsbl", "hooks", "pre-release.sh")
|
|
116
|
-
if os.path.exists(pre_release_script):
|
|
117
|
-
log("Running pre-release hook...")
|
|
118
|
-
try:
|
|
119
|
-
run("bash", [pre_release_script])
|
|
120
|
-
except Exception:
|
|
121
|
-
print("Error: pre-release hook failed. Fix the issues and try again.", file=sys.stderr)
|
|
122
|
-
sys.exit(1)
|
|
123
|
-
|
|
124
|
-
# Dry run: print summary and return
|
|
125
|
-
if flags.get("dry-run", False):
|
|
126
|
-
log("\n--- Dry run summary ---")
|
|
127
|
-
log(f"Registry: {registry}")
|
|
128
|
-
if bump_type:
|
|
129
|
-
log(f"Bump: {current_version} -> {new_version} ({bump_type})")
|
|
130
|
-
else:
|
|
131
|
-
log(f"Version: {new_version} (first release)")
|
|
132
|
-
log(f"Tag: {tag}")
|
|
133
|
-
log(f"Branch: {branch}")
|
|
134
|
-
# Show other version files that would be synced
|
|
135
|
-
other_files = []
|
|
136
|
-
for name, other_reg in REGISTRIES.items():
|
|
137
|
-
if name == registry:
|
|
138
|
-
continue
|
|
139
|
-
if other_reg.check_project_exists("."):
|
|
140
|
-
other_file = other_reg.get_version_file()
|
|
141
|
-
if other_file:
|
|
142
|
-
other_files.append(other_file)
|
|
143
|
-
if other_files:
|
|
144
|
-
log(f"Sync to: {', '.join(other_files)}")
|
|
145
|
-
log(f"Changelog:\n{changelog_entry}")
|
|
146
|
-
log("--- No changes made ---")
|
|
147
|
-
return
|
|
148
|
-
|
|
149
|
-
# Pre-compute which files will be modified
|
|
150
|
-
version_file = reg.get_version_file()
|
|
151
|
-
files_to_commit = []
|
|
152
|
-
if version_file:
|
|
153
|
-
files_to_commit.append(version_file)
|
|
154
|
-
for name, other_reg in REGISTRIES.items():
|
|
155
|
-
if name == registry:
|
|
156
|
-
continue
|
|
157
|
-
if other_reg.check_project_exists("."):
|
|
158
|
-
other_file = other_reg.get_version_file()
|
|
159
|
-
if other_file:
|
|
160
|
-
files_to_commit.append(other_file)
|
|
161
|
-
|
|
162
|
-
# Confirmation prompt (skip with --yes)
|
|
163
|
-
if not flags.get("yes"):
|
|
164
|
-
bump_label = f" ({bump_type})" if bump_type else ""
|
|
165
|
-
print(f"\nAbout to release {new_version}{bump_label} on {branch}")
|
|
166
|
-
print(f" Tag: {tag}")
|
|
167
|
-
if files_to_commit:
|
|
168
|
-
print(f" Files: {', '.join(files_to_commit)}")
|
|
169
|
-
else:
|
|
170
|
-
print(" Files: (none -- version is the git tag)")
|
|
171
|
-
try:
|
|
172
|
-
answer = input("Proceed? [y/N] ").strip().lower()
|
|
173
|
-
except (EOFError, KeyboardInterrupt):
|
|
174
|
-
print("\nAborted.")
|
|
175
|
-
sys.exit(1)
|
|
176
|
-
if answer != "y":
|
|
177
|
-
print("Aborted.")
|
|
178
|
-
sys.exit(0)
|
|
179
|
-
|
|
180
|
-
# Write new version to version files (skip if version didn't change, e.g. first release)
|
|
181
|
-
if new_version != current_version:
|
|
182
|
-
if version_file:
|
|
183
|
-
reg.write_version(".", new_version)
|
|
184
|
-
log(f"Updated version in {version_file}")
|
|
185
|
-
|
|
186
|
-
# Sync version to all other recognized version files
|
|
187
|
-
for name, other_reg in REGISTRIES.items():
|
|
188
|
-
if name == registry:
|
|
189
|
-
continue
|
|
190
|
-
if other_reg.check_project_exists("."):
|
|
191
|
-
other_file = other_reg.get_version_file()
|
|
192
|
-
if other_file:
|
|
193
|
-
other_reg.write_version(".", new_version)
|
|
194
|
-
log(f"Synced version to {other_file}")
|
|
195
|
-
|
|
196
|
-
# Ecosystem tagging: add keyword to manifests if enabled (after confirmation)
|
|
197
|
-
if should_tag(flags):
|
|
198
|
-
try:
|
|
199
|
-
if REGISTRIES["npm"].check_project_exists("."):
|
|
200
|
-
if ensure_npm_keyword(".", quiet=quiet):
|
|
201
|
-
if "package.json" not in files_to_commit:
|
|
202
|
-
files_to_commit.append("package.json")
|
|
203
|
-
except Exception:
|
|
204
|
-
pass
|
|
205
|
-
try:
|
|
206
|
-
if REGISTRIES["pypi"].check_project_exists("."):
|
|
207
|
-
if ensure_pypi_keyword(".", quiet=quiet):
|
|
208
|
-
if "pyproject.toml" not in files_to_commit:
|
|
209
|
-
files_to_commit.append("pyproject.toml")
|
|
210
|
-
except Exception:
|
|
211
|
-
pass
|
|
212
|
-
|
|
213
|
-
# Update .rlsbl/version marker so it's included in the release commit
|
|
214
|
-
rlsbl_version_marker = os.path.join(".rlsbl", "version")
|
|
215
|
-
if os.path.exists(os.path.dirname(rlsbl_version_marker)):
|
|
216
|
-
try:
|
|
217
|
-
from .. import __version__ as rlsbl_ver
|
|
218
|
-
with open(rlsbl_version_marker, "w") as f:
|
|
219
|
-
f.write(rlsbl_ver + "\n")
|
|
220
|
-
if rlsbl_version_marker not in files_to_commit:
|
|
221
|
-
files_to_commit.append(rlsbl_version_marker)
|
|
222
|
-
except Exception:
|
|
223
|
-
pass
|
|
224
|
-
|
|
225
|
-
# Commit if anything was actually modified (version bump or tagging)
|
|
226
|
-
needs_commit = new_version != current_version or not is_clean_tree()
|
|
227
|
-
if files_to_commit and needs_commit:
|
|
228
|
-
commit_tool = find_commit_tool()
|
|
229
|
-
if commit_tool == "safegit":
|
|
230
|
-
run(commit_tool, ["commit", "-m", tag, "--", *files_to_commit])
|
|
231
|
-
else:
|
|
232
|
-
run("git", ["add", *files_to_commit])
|
|
233
|
-
run("git", ["commit", "-m", tag])
|
|
234
|
-
log(f"Committed: {tag}")
|
|
235
|
-
elif not needs_commit:
|
|
236
|
-
log("No changes to commit")
|
|
237
|
-
|
|
238
|
-
# Create local git tag
|
|
239
|
-
run("git", ["tag", tag])
|
|
240
|
-
log(f"Tagged: {tag}")
|
|
241
|
-
|
|
242
|
-
# Push commits and tag
|
|
243
|
-
push_timeout = get_push_timeout()
|
|
244
|
-
if push_timeout != 120:
|
|
245
|
-
log(f"Push timeout: {push_timeout}s (from RLSBL_PUSH_TIMEOUT)")
|
|
246
|
-
push_if_needed(branch)
|
|
247
|
-
run("git", ["push", "origin", tag], timeout=push_timeout)
|
|
248
|
-
log(f"Pushed to origin/{branch}")
|
|
249
|
-
|
|
250
|
-
# Create GitHub Release using a temp notes file
|
|
251
|
-
notes_file = f".rlsbl-notes-{int(time.time() * 1000)}.tmp"
|
|
252
|
-
writing_file = notes_file + ".writing"
|
|
253
|
-
try:
|
|
254
|
-
with open(writing_file, "w", encoding="utf-8") as f:
|
|
255
|
-
f.write(changelog_entry)
|
|
256
|
-
os.rename(writing_file, notes_file)
|
|
257
|
-
run("gh", ["release", "create", tag, "--title", tag, "--notes-file", notes_file])
|
|
258
|
-
log(f"Created GitHub Release: {tag}")
|
|
259
|
-
finally:
|
|
260
|
-
# Clean up temp files even if gh release fails
|
|
261
|
-
for tmp in (notes_file, writing_file):
|
|
262
|
-
if os.path.exists(tmp):
|
|
263
|
-
os.unlink(tmp)
|
|
264
|
-
|
|
265
|
-
# Ecosystem tagging: add GitHub topic after release is created
|
|
266
|
-
if should_tag(flags):
|
|
267
|
-
ensure_github_topic(quiet=quiet)
|
|
268
|
-
|
|
269
|
-
# Run post-release hook if present (non-fatal: release is already complete)
|
|
270
|
-
post_release_script = os.path.join(".", ".rlsbl", "hooks", "post-release.sh")
|
|
271
|
-
if os.path.exists(post_release_script):
|
|
272
|
-
log("Running post-release hook...")
|
|
273
|
-
try:
|
|
274
|
-
env = os.environ.copy()
|
|
275
|
-
env["RLSBL_VERSION"] = new_version
|
|
276
|
-
run("bash", [post_release_script], env=env)
|
|
277
|
-
except Exception as e:
|
|
278
|
-
print(f"Warning: post-release hook failed: {e}", file=sys.stderr)
|
|
279
|
-
|
|
280
|
-
# Hint: how to watch CI for this release
|
|
281
|
-
try:
|
|
282
|
-
commit_sha = run("git", ["rev-parse", "HEAD"])
|
|
283
|
-
log(f"Watch CI: rlsbl watch {commit_sha}")
|
|
284
|
-
except Exception:
|
|
285
|
-
pass
|
|
286
|
-
|
|
287
|
-
log(f"\nRelease {new_version} complete!")
|
package/rlsbl/commands/status.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""Status command: show project status summary."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
|
|
6
|
-
from ..registries import REGISTRIES
|
|
7
|
-
from ..utils import (
|
|
8
|
-
extract_changelog_entry,
|
|
9
|
-
get_current_branch,
|
|
10
|
-
is_clean_tree,
|
|
11
|
-
run,
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def run_cmd(registry, args, flags):
|
|
16
|
-
"""Status command handler.
|
|
17
|
-
|
|
18
|
-
Shows a quick 'where am I' summary: package info, git state, changelog, CI.
|
|
19
|
-
"""
|
|
20
|
-
reg = REGISTRIES[registry]
|
|
21
|
-
|
|
22
|
-
if not reg.check_project_exists("."):
|
|
23
|
-
print(f"No {registry} project found in current directory.", file=sys.stderr)
|
|
24
|
-
sys.exit(1)
|
|
25
|
-
|
|
26
|
-
version = reg.read_version(".")
|
|
27
|
-
vars_dict = reg.get_template_vars(".")
|
|
28
|
-
name = vars_dict.get("name") or "(unknown)"
|
|
29
|
-
|
|
30
|
-
print(f"Package: {name}")
|
|
31
|
-
|
|
32
|
-
# Show version info for all detected registries
|
|
33
|
-
for r_name, r_mod in REGISTRIES.items():
|
|
34
|
-
if r_mod.check_project_exists("."):
|
|
35
|
-
ver = r_mod.read_version(".")
|
|
36
|
-
file = r_mod.get_version_file() or "git tag"
|
|
37
|
-
print(f"Version: {ver} ({r_name}, {file})")
|
|
38
|
-
|
|
39
|
-
# Git info
|
|
40
|
-
try:
|
|
41
|
-
branch = get_current_branch()
|
|
42
|
-
print(f"Branch: {branch}")
|
|
43
|
-
except Exception:
|
|
44
|
-
print("Branch: (not a git repo)")
|
|
45
|
-
|
|
46
|
-
# Last tag
|
|
47
|
-
try:
|
|
48
|
-
last_tag = run("git", ["describe", "--tags", "--abbrev=0"])
|
|
49
|
-
print(f"Last tag: {last_tag}")
|
|
50
|
-
except Exception:
|
|
51
|
-
print("Last tag: (none)")
|
|
52
|
-
|
|
53
|
-
# Clean tree
|
|
54
|
-
try:
|
|
55
|
-
print(f"Clean: {'yes' if is_clean_tree() else 'no'}")
|
|
56
|
-
except Exception:
|
|
57
|
-
print("Clean: (unknown)")
|
|
58
|
-
|
|
59
|
-
# Changelog
|
|
60
|
-
changelog_path = "CHANGELOG.md"
|
|
61
|
-
if os.path.exists(changelog_path):
|
|
62
|
-
entry = extract_changelog_entry(changelog_path, version)
|
|
63
|
-
if entry:
|
|
64
|
-
print(f"Changelog: has entry for {version}")
|
|
65
|
-
else:
|
|
66
|
-
print(f"Changelog: no entry for {version}")
|
|
67
|
-
else:
|
|
68
|
-
print("Changelog: (not found)")
|
|
69
|
-
|
|
70
|
-
# CI workflows
|
|
71
|
-
ci_exists = os.path.exists(".github/workflows/ci.yml")
|
|
72
|
-
publish_exists = os.path.exists(".github/workflows/publish.yml") or os.path.exists(
|
|
73
|
-
".github/workflows/workflow.yml"
|
|
74
|
-
)
|
|
75
|
-
print(f"CI: {'yes' if ci_exists else 'missing'}")
|
|
76
|
-
print(f"Publish: {'yes' if publish_exists else 'missing'}")
|
package/rlsbl/commands/undo.py
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""Undo command: revert the last release."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
from ..utils import run, check_gh_installed, check_gh_auth, get_push_timeout, is_clean_tree
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def run_cmd(registry, args, flags):
|
|
9
|
-
if not check_gh_installed():
|
|
10
|
-
print("Error: gh CLI is not installed.", file=sys.stderr)
|
|
11
|
-
sys.exit(1)
|
|
12
|
-
if not check_gh_auth():
|
|
13
|
-
print("Error: gh CLI is not authenticated.", file=sys.stderr)
|
|
14
|
-
sys.exit(1)
|
|
15
|
-
|
|
16
|
-
if not is_clean_tree():
|
|
17
|
-
print("Error: working tree is not clean. Commit your changes first.", file=sys.stderr)
|
|
18
|
-
sys.exit(1)
|
|
19
|
-
|
|
20
|
-
# Find the latest tag
|
|
21
|
-
try:
|
|
22
|
-
tag = run("git", ["describe", "--tags", "--abbrev=0"])
|
|
23
|
-
except Exception:
|
|
24
|
-
print("Error: no tags found. Nothing to undo.", file=sys.stderr)
|
|
25
|
-
sys.exit(1)
|
|
26
|
-
|
|
27
|
-
print(f"This will undo release {tag}:")
|
|
28
|
-
print(f" - Delete git tag {tag} (local + remote)")
|
|
29
|
-
print(f" - Revert the version bump commit")
|
|
30
|
-
print(f" - Delete the GitHub Release for {tag}")
|
|
31
|
-
|
|
32
|
-
if not flags.get("yes"):
|
|
33
|
-
try:
|
|
34
|
-
answer = input("\nThis is destructive. Proceed? [y/N] ").strip().lower()
|
|
35
|
-
except (EOFError, KeyboardInterrupt):
|
|
36
|
-
print("\nAborted.")
|
|
37
|
-
sys.exit(1)
|
|
38
|
-
if answer != "y":
|
|
39
|
-
print("Aborted.")
|
|
40
|
-
sys.exit(0)
|
|
41
|
-
|
|
42
|
-
# Delete GitHub Release
|
|
43
|
-
try:
|
|
44
|
-
run("gh", ["release", "delete", tag, "--yes"])
|
|
45
|
-
print(f"Deleted GitHub Release: {tag}")
|
|
46
|
-
except Exception as e:
|
|
47
|
-
print(f"Warning: could not delete GitHub Release: {e}")
|
|
48
|
-
|
|
49
|
-
# Delete remote tag
|
|
50
|
-
try:
|
|
51
|
-
run("git", ["push", "origin", f":{tag}"], timeout=get_push_timeout())
|
|
52
|
-
print(f"Deleted remote tag: {tag}")
|
|
53
|
-
except Exception as e:
|
|
54
|
-
print(f"Warning: could not delete remote tag: {e}")
|
|
55
|
-
|
|
56
|
-
# Delete local tag
|
|
57
|
-
try:
|
|
58
|
-
run("git", ["tag", "-d", tag])
|
|
59
|
-
print(f"Deleted local tag: {tag}")
|
|
60
|
-
except Exception as e:
|
|
61
|
-
print(f"Warning: could not delete local tag: {e}")
|
|
62
|
-
|
|
63
|
-
# Revert the version bump commit (should be HEAD)
|
|
64
|
-
try:
|
|
65
|
-
head_msg = run("git", ["log", "-1", "--format=%s"])
|
|
66
|
-
if head_msg == tag:
|
|
67
|
-
run("git", ["revert", "--no-edit", "HEAD"])
|
|
68
|
-
print(f"Reverted commit: {head_msg}")
|
|
69
|
-
else:
|
|
70
|
-
print(f"Warning: HEAD commit ({head_msg}) doesn't match tag ({tag}). Skipping revert.")
|
|
71
|
-
except Exception as e:
|
|
72
|
-
print(f"Warning: could not revert commit: {e}")
|
|
73
|
-
|
|
74
|
-
print(f"\nUndo complete. Run 'git push' to sync the revert.")
|
package/rlsbl/commands/watch.py
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
"""Watch command: monitor CI runs for a commit and report results."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import shutil
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
|
-
from ..utils import run
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _notify(title, body):
|
|
13
|
-
"""Send a desktop notification. Non-fatal if unavailable."""
|
|
14
|
-
try:
|
|
15
|
-
if sys.platform == "darwin":
|
|
16
|
-
# Escape double quotes to prevent AppleScript injection
|
|
17
|
-
escaped_title = title.replace('"', '\\"')
|
|
18
|
-
escaped_body = body.replace('"', '\\"')
|
|
19
|
-
subprocess.run(
|
|
20
|
-
["osascript", "-e",
|
|
21
|
-
f'display notification "{escaped_body}" with title "{escaped_title}"'],
|
|
22
|
-
timeout=5, capture_output=True,
|
|
23
|
-
)
|
|
24
|
-
elif shutil.which("notify-send"):
|
|
25
|
-
subprocess.run(
|
|
26
|
-
["notify-send", "-u", "normal", title, body],
|
|
27
|
-
timeout=5, capture_output=True,
|
|
28
|
-
)
|
|
29
|
-
except Exception:
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def run_cmd(registry, args, flags):
|
|
34
|
-
"""Watch all CI runs for a commit until they complete.
|
|
35
|
-
|
|
36
|
-
Usage: rlsbl watch [<commit-sha>]
|
|
37
|
-
Defaults to HEAD if no commit SHA is provided.
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
# Get commit SHA (resolve short SHAs -- gh requires full 40-char)
|
|
41
|
-
if args:
|
|
42
|
-
try:
|
|
43
|
-
commit_sha = run("git", ["rev-parse", args[0]])
|
|
44
|
-
except Exception:
|
|
45
|
-
commit_sha = args[0]
|
|
46
|
-
else:
|
|
47
|
-
try:
|
|
48
|
-
commit_sha = run("git", ["rev-parse", "HEAD"])
|
|
49
|
-
except Exception:
|
|
50
|
-
print("Error: not a git repository and no commit SHA provided.", file=sys.stderr)
|
|
51
|
-
sys.exit(1)
|
|
52
|
-
|
|
53
|
-
# Get repo info for display and URLs
|
|
54
|
-
try:
|
|
55
|
-
repo_info = run("gh", ["repo", "view", "--json", "nameWithOwner,name"])
|
|
56
|
-
info = json.loads(repo_info)
|
|
57
|
-
repo_slug = info.get("nameWithOwner", "")
|
|
58
|
-
repo_name = info.get("name", "")
|
|
59
|
-
except Exception:
|
|
60
|
-
print("Error: could not get repo info. Is gh installed and authenticated?", file=sys.stderr)
|
|
61
|
-
sys.exit(1)
|
|
62
|
-
|
|
63
|
-
# Try to find a tag for this commit for nicer display
|
|
64
|
-
try:
|
|
65
|
-
tag = run("git", ["describe", "--tags", "--exact-match", commit_sha])
|
|
66
|
-
except Exception:
|
|
67
|
-
tag = commit_sha[:12]
|
|
68
|
-
|
|
69
|
-
label = f"{repo_name} {tag}" if repo_name else tag
|
|
70
|
-
|
|
71
|
-
# Poll until at least one run appears (retry up to 30s)
|
|
72
|
-
runs = []
|
|
73
|
-
for _ in range(15):
|
|
74
|
-
try:
|
|
75
|
-
raw = run("gh", ["run", "list", "--commit", commit_sha,
|
|
76
|
-
"--json", "databaseId,name,status"])
|
|
77
|
-
parsed = json.loads(raw)
|
|
78
|
-
if parsed:
|
|
79
|
-
runs = parsed
|
|
80
|
-
break
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
time.sleep(2)
|
|
84
|
-
|
|
85
|
-
if not runs:
|
|
86
|
-
print(f"rlsbl: {label}: no CI runs found after 30s", file=sys.stderr)
|
|
87
|
-
sys.exit(1)
|
|
88
|
-
|
|
89
|
-
print(f"rlsbl: {label}: found {len(runs)} CI run(s), watching...", file=sys.stderr)
|
|
90
|
-
|
|
91
|
-
# Watch each run sequentially, collecting results
|
|
92
|
-
any_failed = False
|
|
93
|
-
for ci_run in runs:
|
|
94
|
-
run_id = str(ci_run["databaseId"])
|
|
95
|
-
workflow_name = ci_run.get("name", f"run {run_id}")
|
|
96
|
-
|
|
97
|
-
try:
|
|
98
|
-
# gh run watch blocks until the run completes;
|
|
99
|
-
# --exit-status makes it exit 1 on failure; check=True raises
|
|
100
|
-
# CalledProcessError so we can distinguish pass from fail
|
|
101
|
-
subprocess.run(
|
|
102
|
-
["gh", "run", "watch", run_id, "--exit-status"],
|
|
103
|
-
capture_output=True, text=True, timeout=3600, check=True,
|
|
104
|
-
)
|
|
105
|
-
print(f"rlsbl: {label}: {workflow_name} passed", file=sys.stderr)
|
|
106
|
-
except subprocess.CalledProcessError:
|
|
107
|
-
any_failed = True
|
|
108
|
-
print(f"rlsbl: {label}: {workflow_name} FAILED", file=sys.stderr)
|
|
109
|
-
if repo_slug:
|
|
110
|
-
print(f"rlsbl: https://github.com/{repo_slug}/actions/runs/{run_id}",
|
|
111
|
-
file=sys.stderr)
|
|
112
|
-
except subprocess.TimeoutExpired:
|
|
113
|
-
any_failed = True
|
|
114
|
-
print(f"rlsbl: {label}: {workflow_name} timed out after 1h", file=sys.stderr)
|
|
115
|
-
|
|
116
|
-
# Desktop notification for overall result
|
|
117
|
-
if any_failed:
|
|
118
|
-
_notify(f"{label}: CI FAILED", "One or more workflows failed")
|
|
119
|
-
else:
|
|
120
|
-
_notify(f"{label}: CI passed", "All workflows passed")
|
|
121
|
-
|
|
122
|
-
sys.exit(1 if any_failed else 0)
|
|
123
|
-
except KeyboardInterrupt:
|
|
124
|
-
print("\nWatch cancelled.", file=sys.stderr)
|
|
125
|
-
sys.exit(130)
|
package/rlsbl/config.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""Config reading for the tag feature (ecosystem discoverability).
|
|
2
|
-
|
|
3
|
-
Precedence (highest to lowest):
|
|
4
|
-
1. CLI flag (--no-tag)
|
|
5
|
-
2. Project-level: .rlsbl/config.json
|
|
6
|
-
3. User-level: ~/.rlsbl/config.json
|
|
7
|
-
4. Default: True (tagging enabled)
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import os
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _project_config():
|
|
15
|
-
"""Resolve project config path at call time (respects cwd changes)."""
|
|
16
|
-
return os.path.join(".rlsbl", "config.json")
|
|
17
|
-
|
|
18
|
-
USER_CONFIG = os.path.expanduser("~/.rlsbl/config.json")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def read_json_config(path):
|
|
22
|
-
"""Safely read a JSON file, returning {} on missing or malformed."""
|
|
23
|
-
try:
|
|
24
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
25
|
-
return json.load(f)
|
|
26
|
-
except (OSError, json.JSONDecodeError):
|
|
27
|
-
return {}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def should_tag(flags):
|
|
31
|
-
"""Returns True if tagging is enabled, checking flag > project > user > default."""
|
|
32
|
-
# CLI flag takes highest precedence
|
|
33
|
-
if flags.get("no-tag"):
|
|
34
|
-
return False
|
|
35
|
-
|
|
36
|
-
# Project-level config
|
|
37
|
-
project = read_json_config(_project_config())
|
|
38
|
-
if "tag" in project:
|
|
39
|
-
return bool(project["tag"])
|
|
40
|
-
|
|
41
|
-
# User-level config
|
|
42
|
-
user = read_json_config(USER_CONFIG)
|
|
43
|
-
if "tag" in user:
|
|
44
|
-
return bool(user["tag"])
|
|
45
|
-
|
|
46
|
-
# Default: tagging enabled
|
|
47
|
-
return True
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def write_project_config(key, value):
|
|
51
|
-
"""Write or update a key in .rlsbl/config.json (creates dir if needed)."""
|
|
52
|
-
os.makedirs(os.path.dirname(_project_config()), exist_ok=True)
|
|
53
|
-
existing = read_json_config(_project_config())
|
|
54
|
-
existing[key] = value
|
|
55
|
-
with open(_project_config(), "w", encoding="utf-8") as f:
|
|
56
|
-
json.dump(existing, f, indent=2)
|
|
57
|
-
f.write("\n")
|