rlsbl 0.8.1 → 0.8.2
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 +1 -1
- package/rlsbl/__init__.py +8 -2
- package/rlsbl/commands/config.py +3 -3
- package/rlsbl/commands/pre_push_check.py +8 -39
- package/rlsbl/commands/record_gif.py +10 -4
- package/rlsbl/commands/watch.py +81 -74
- package/rlsbl/config.py +8 -5
package/package.json
CHANGED
package/rlsbl/__init__.py
CHANGED
|
@@ -56,7 +56,8 @@ Usage:
|
|
|
56
56
|
rlsbl discover [--mine] List rlsbl ecosystem projects
|
|
57
57
|
rlsbl watch [<commit-sha>] Watch CI runs for a commit
|
|
58
58
|
rlsbl pre-push-check Verify CHANGELOG entry for current version
|
|
59
|
-
rlsbl record-gif
|
|
59
|
+
rlsbl record-gif [--width N] [--height N] [--font-size N] [--duration N]
|
|
60
|
+
Record a demo GIF with vhs
|
|
60
61
|
|
|
61
62
|
Options:
|
|
62
63
|
--registry <npm|pypi|go> Target a specific registry (auto-detected if omitted)
|
|
@@ -86,7 +87,7 @@ def parse_args(argv):
|
|
|
86
87
|
Flags listed in VALUE_FLAGS consume the next token as their value
|
|
87
88
|
(e.g. --registry npm). All other --flags are boolean.
|
|
88
89
|
"""
|
|
89
|
-
VALUE_FLAGS = ("registry",)
|
|
90
|
+
VALUE_FLAGS = ("registry", "width", "height", "font-size", "duration")
|
|
90
91
|
raw = argv[1:]
|
|
91
92
|
positional = []
|
|
92
93
|
flags = {}
|
|
@@ -166,6 +167,11 @@ def main():
|
|
|
166
167
|
args = positional[1:]
|
|
167
168
|
registry = flags.get("registry")
|
|
168
169
|
|
|
170
|
+
# --registry was the last arg with no value following it
|
|
171
|
+
if registry is True:
|
|
172
|
+
print("Error: --registry requires a value (npm, pypi, or go).", file=sys.stderr)
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
169
175
|
# Validate --registry if provided
|
|
170
176
|
if registry and registry not in REGISTRIES:
|
|
171
177
|
print(
|
package/rlsbl/commands/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Config command: show resolved project configuration."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from ..config import
|
|
4
|
+
from ..config import _project_config, USER_CONFIG, read_json_config, should_tag
|
|
5
5
|
from ..registries import REGISTRIES
|
|
6
6
|
|
|
7
7
|
|
|
@@ -50,12 +50,12 @@ def run_cmd(registry, args, flags):
|
|
|
50
50
|
print("\nEcosystem tagging:")
|
|
51
51
|
enabled = should_tag(flags)
|
|
52
52
|
# Determine why it's enabled/disabled
|
|
53
|
-
project_cfg = read_json_config(
|
|
53
|
+
project_cfg = read_json_config(_project_config())
|
|
54
54
|
user_cfg = read_json_config(USER_CONFIG)
|
|
55
55
|
if flags.get("no-tag"):
|
|
56
56
|
source = "CLI flag"
|
|
57
57
|
elif "tag" in project_cfg:
|
|
58
|
-
source = f"project config ({
|
|
58
|
+
source = f"project config ({_project_config()})"
|
|
59
59
|
elif "tag" in user_cfg:
|
|
60
60
|
source = f"user config ({USER_CONFIG})"
|
|
61
61
|
else:
|
|
@@ -1,52 +1,21 @@
|
|
|
1
1
|
"""Pre-push-check command: verify CHANGELOG.md has an entry for the current version."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import os
|
|
5
4
|
import re
|
|
6
5
|
import sys
|
|
7
6
|
|
|
7
|
+
from ..registries import REGISTRIES
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
def _detect_version():
|
|
10
|
-
"""Detect
|
|
11
|
+
"""Detect version using registry adapters.
|
|
11
12
|
|
|
12
|
-
Returns (version_string,
|
|
13
|
+
Returns (version_string, registry_name) or (None, None) if undetectable.
|
|
13
14
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
with open(version_path, "r", encoding="utf-8") as f:
|
|
19
|
-
version = f.read().strip()
|
|
20
|
-
if version:
|
|
21
|
-
return version, "go"
|
|
22
|
-
return None, None
|
|
23
|
-
|
|
24
|
-
if os.path.exists("package.json"):
|
|
25
|
-
try:
|
|
26
|
-
with open("package.json", "r", encoding="utf-8") as f:
|
|
27
|
-
data = json.load(f)
|
|
28
|
-
version = data.get("version", "")
|
|
29
|
-
if version:
|
|
30
|
-
return version, "npm"
|
|
31
|
-
except Exception:
|
|
32
|
-
pass
|
|
33
|
-
return None, None
|
|
34
|
-
|
|
35
|
-
if os.path.exists("pyproject.toml"):
|
|
36
|
-
try:
|
|
37
|
-
try:
|
|
38
|
-
import tomllib
|
|
39
|
-
except ModuleNotFoundError:
|
|
40
|
-
import tomli as tomllib # type: ignore[no-redef]
|
|
41
|
-
with open("pyproject.toml", "rb") as f:
|
|
42
|
-
data = tomllib.load(f)
|
|
43
|
-
version = data.get("project", {}).get("version", "")
|
|
44
|
-
if version:
|
|
45
|
-
return version, "pypi"
|
|
46
|
-
except Exception:
|
|
47
|
-
pass
|
|
48
|
-
return None, None
|
|
49
|
-
|
|
15
|
+
for name in ("go", "npm", "pypi"):
|
|
16
|
+
reg = REGISTRIES[name]
|
|
17
|
+
if reg.check_project_exists("."):
|
|
18
|
+
return reg.read_version("."), name
|
|
50
19
|
return None, None
|
|
51
20
|
|
|
52
21
|
|
|
@@ -43,19 +43,25 @@ def run_cmd(registry, args, flags):
|
|
|
43
43
|
print("Ensure package.json, pyproject.toml, or go.mod exists with a CLI entry point.", file=sys.stderr)
|
|
44
44
|
sys.exit(1)
|
|
45
45
|
|
|
46
|
+
# Parse configurable VHS parameters from flags
|
|
47
|
+
width = int(flags.get("width", 1200))
|
|
48
|
+
height = int(flags.get("height", 600))
|
|
49
|
+
font_size = int(flags.get("font-size", 24))
|
|
50
|
+
duration = int(flags.get("duration", 10))
|
|
51
|
+
|
|
46
52
|
assets_dir = "assets"
|
|
47
53
|
os.makedirs(assets_dir, exist_ok=True)
|
|
48
54
|
|
|
49
55
|
# Create a temporary VHS tape file in the project directory
|
|
50
56
|
tape_content = (
|
|
51
57
|
'Set FontFamily "monospace"\n'
|
|
52
|
-
"Set FontSize
|
|
53
|
-
"Set Width
|
|
54
|
-
"Set Height
|
|
58
|
+
f"Set FontSize {font_size}\n"
|
|
59
|
+
f"Set Width {width}\n"
|
|
60
|
+
f"Set Height {height}\n"
|
|
55
61
|
"Set TypingSpeed 50ms\n"
|
|
56
62
|
f'Type "{bin_command} --help"\n'
|
|
57
63
|
"Enter\n"
|
|
58
|
-
"Sleep
|
|
64
|
+
f"Sleep {duration}s\n"
|
|
59
65
|
)
|
|
60
66
|
|
|
61
67
|
tape_fd, tape_path = tempfile.mkstemp(suffix=".tape", dir=".")
|
package/rlsbl/commands/watch.py
CHANGED
|
@@ -13,9 +13,12 @@ def _notify(title, body):
|
|
|
13
13
|
"""Send a desktop notification. Non-fatal if unavailable."""
|
|
14
14
|
try:
|
|
15
15
|
if sys.platform == "darwin":
|
|
16
|
+
# Escape double quotes to prevent AppleScript injection
|
|
17
|
+
escaped_title = title.replace('"', '\\"')
|
|
18
|
+
escaped_body = body.replace('"', '\\"')
|
|
16
19
|
subprocess.run(
|
|
17
20
|
["osascript", "-e",
|
|
18
|
-
f'display notification "{
|
|
21
|
+
f'display notification "{escaped_body}" with title "{escaped_title}"'],
|
|
19
22
|
timeout=5, capture_output=True,
|
|
20
23
|
)
|
|
21
24
|
elif shutil.which("notify-send"):
|
|
@@ -33,83 +36,87 @@ def run_cmd(registry, args, flags):
|
|
|
33
36
|
Usage: rlsbl watch [<commit-sha>]
|
|
34
37
|
Defaults to HEAD if no commit SHA is provided.
|
|
35
38
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
try:
|
|
40
|
+
# Get commit SHA
|
|
41
|
+
if args:
|
|
42
|
+
commit_sha = args[0]
|
|
43
|
+
else:
|
|
44
|
+
try:
|
|
45
|
+
commit_sha = run("git", ["rev-parse", "HEAD"])
|
|
46
|
+
except Exception:
|
|
47
|
+
print("Error: not a git repository and no commit SHA provided.", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
# Get repo info for display and URLs
|
|
40
51
|
try:
|
|
41
|
-
|
|
52
|
+
repo_info = run("gh", ["repo", "view", "--json", "nameWithOwner,name"])
|
|
53
|
+
info = json.loads(repo_info)
|
|
54
|
+
repo_slug = info.get("nameWithOwner", "")
|
|
55
|
+
repo_name = info.get("name", "")
|
|
42
56
|
except Exception:
|
|
43
|
-
print("Error: not
|
|
57
|
+
print("Error: could not get repo info. Is gh installed and authenticated?", file=sys.stderr)
|
|
44
58
|
sys.exit(1)
|
|
45
59
|
|
|
46
|
-
|
|
47
|
-
try:
|
|
48
|
-
repo_info = run("gh", ["repo", "view", "--json", "nameWithOwner,name"])
|
|
49
|
-
info = json.loads(repo_info)
|
|
50
|
-
repo_slug = info.get("nameWithOwner", "")
|
|
51
|
-
repo_name = info.get("name", "")
|
|
52
|
-
except Exception:
|
|
53
|
-
print("Error: could not get repo info. Is gh installed and authenticated?", file=sys.stderr)
|
|
54
|
-
sys.exit(1)
|
|
55
|
-
|
|
56
|
-
# Try to find a tag for this commit for nicer display
|
|
57
|
-
try:
|
|
58
|
-
tag = run("git", ["describe", "--tags", "--exact-match", commit_sha])
|
|
59
|
-
except Exception:
|
|
60
|
-
tag = commit_sha[:12]
|
|
61
|
-
|
|
62
|
-
label = f"{repo_name} {tag}" if repo_name else tag
|
|
63
|
-
|
|
64
|
-
# Poll until at least one run appears (retry up to 30s)
|
|
65
|
-
runs = []
|
|
66
|
-
for _ in range(15):
|
|
60
|
+
# Try to find a tag for this commit for nicer display
|
|
67
61
|
try:
|
|
68
|
-
|
|
69
|
-
"--json", "databaseId,name,status"])
|
|
70
|
-
parsed = json.loads(raw)
|
|
71
|
-
if parsed:
|
|
72
|
-
runs = parsed
|
|
73
|
-
break
|
|
62
|
+
tag = run("git", ["describe", "--tags", "--exact-match", commit_sha])
|
|
74
63
|
except Exception:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
64
|
+
tag = commit_sha[:12]
|
|
65
|
+
|
|
66
|
+
label = f"{repo_name} {tag}" if repo_name else tag
|
|
67
|
+
|
|
68
|
+
# Poll until at least one run appears (retry up to 30s)
|
|
69
|
+
runs = []
|
|
70
|
+
for _ in range(15):
|
|
71
|
+
try:
|
|
72
|
+
raw = run("gh", ["run", "list", "--commit", commit_sha,
|
|
73
|
+
"--json", "databaseId,name,status"])
|
|
74
|
+
parsed = json.loads(raw)
|
|
75
|
+
if parsed:
|
|
76
|
+
runs = parsed
|
|
77
|
+
break
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
time.sleep(2)
|
|
81
|
+
|
|
82
|
+
if not runs:
|
|
83
|
+
print(f"rlsbl: {label}: no CI runs found after 30s", file=sys.stderr)
|
|
84
|
+
sys.exit(1)
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
86
|
+
print(f"rlsbl: {label}: found {len(runs)} CI run(s), watching...", file=sys.stderr)
|
|
87
|
+
|
|
88
|
+
# Watch each run sequentially, collecting results
|
|
89
|
+
any_failed = False
|
|
90
|
+
for ci_run in runs:
|
|
91
|
+
run_id = str(ci_run["databaseId"])
|
|
92
|
+
workflow_name = ci_run.get("name", f"run {run_id}")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# gh run watch blocks until the run completes;
|
|
96
|
+
# --exit-status makes it exit 1 on failure; check=True raises
|
|
97
|
+
# CalledProcessError so we can distinguish pass from fail
|
|
98
|
+
subprocess.run(
|
|
99
|
+
["gh", "run", "watch", run_id, "--exit-status"],
|
|
100
|
+
capture_output=True, text=True, timeout=3600, check=True,
|
|
101
|
+
)
|
|
102
|
+
print(f"rlsbl: {label}: {workflow_name} passed", file=sys.stderr)
|
|
103
|
+
except subprocess.CalledProcessError:
|
|
104
|
+
any_failed = True
|
|
105
|
+
print(f"rlsbl: {label}: {workflow_name} FAILED", file=sys.stderr)
|
|
106
|
+
if repo_slug:
|
|
107
|
+
print(f"rlsbl: https://github.com/{repo_slug}/actions/runs/{run_id}",
|
|
108
|
+
file=sys.stderr)
|
|
109
|
+
except subprocess.TimeoutExpired:
|
|
110
|
+
any_failed = True
|
|
111
|
+
print(f"rlsbl: {label}: {workflow_name} timed out after 1h", file=sys.stderr)
|
|
112
|
+
|
|
113
|
+
# Desktop notification for overall result
|
|
114
|
+
if any_failed:
|
|
115
|
+
_notify(f"{label}: CI FAILED", "One or more workflows failed")
|
|
116
|
+
else:
|
|
117
|
+
_notify(f"{label}: CI passed", "All workflows passed")
|
|
118
|
+
|
|
119
|
+
sys.exit(1 if any_failed else 0)
|
|
120
|
+
except KeyboardInterrupt:
|
|
121
|
+
print("\nWatch cancelled.", file=sys.stderr)
|
|
122
|
+
sys.exit(130)
|
package/rlsbl/config.py
CHANGED
|
@@ -11,7 +11,10 @@ import json
|
|
|
11
11
|
import os
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
def _project_config():
|
|
15
|
+
"""Resolve project config path at call time (respects cwd changes)."""
|
|
16
|
+
return os.path.join(".rlsbl", "config.json")
|
|
17
|
+
|
|
15
18
|
USER_CONFIG = os.path.expanduser("~/.rlsbl/config.json")
|
|
16
19
|
|
|
17
20
|
|
|
@@ -31,7 +34,7 @@ def should_tag(flags):
|
|
|
31
34
|
return False
|
|
32
35
|
|
|
33
36
|
# Project-level config
|
|
34
|
-
project = read_json_config(
|
|
37
|
+
project = read_json_config(_project_config())
|
|
35
38
|
if "tag" in project:
|
|
36
39
|
return bool(project["tag"])
|
|
37
40
|
|
|
@@ -46,9 +49,9 @@ def should_tag(flags):
|
|
|
46
49
|
|
|
47
50
|
def write_project_config(key, value):
|
|
48
51
|
"""Write or update a key in .rlsbl/config.json (creates dir if needed)."""
|
|
49
|
-
os.makedirs(os.path.dirname(
|
|
50
|
-
existing = read_json_config(
|
|
52
|
+
os.makedirs(os.path.dirname(_project_config()), exist_ok=True)
|
|
53
|
+
existing = read_json_config(_project_config())
|
|
51
54
|
existing[key] = value
|
|
52
|
-
with open(
|
|
55
|
+
with open(_project_config(), "w", encoding="utf-8") as f:
|
|
53
56
|
json.dump(existing, f, indent=2)
|
|
54
57
|
f.write("\n")
|