rlsbl 0.8.0 → 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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/rlsbl/__init__.py +8 -2
  3. package/rlsbl/commands/config.py +3 -3
  4. package/rlsbl/commands/discover.py +4 -2
  5. package/rlsbl/commands/init_cmd.py +1 -1
  6. package/rlsbl/commands/pre_push_check.py +8 -39
  7. package/rlsbl/commands/record_gif.py +10 -4
  8. package/rlsbl/commands/release.py +12 -0
  9. package/rlsbl/commands/undo.py +11 -3
  10. package/rlsbl/commands/watch.py +81 -74
  11. package/rlsbl/config.py +8 -5
  12. package/rlsbl/registries/go.py +2 -2
  13. package/rlsbl/registries/npm.py +3 -3
  14. package/rlsbl/registries/pypi.py +2 -2
  15. package/rlsbl/tagging.py +8 -3
  16. package/{templates → rlsbl/templates}/shared/hooks/pre-release.sh.tpl +9 -5
  17. /package/{templates → rlsbl/templates}/go/VERSION.tpl +0 -0
  18. /package/{templates → rlsbl/templates}/go/ci.yml.tpl +0 -0
  19. /package/{templates → rlsbl/templates}/go/goreleaser.yml.tpl +0 -0
  20. /package/{templates → rlsbl/templates}/go/publish.yml.tpl +0 -0
  21. /package/{templates → rlsbl/templates}/merged/publish.yml.tpl +0 -0
  22. /package/{templates → rlsbl/templates}/npm/ci.yml.tpl +0 -0
  23. /package/{templates → rlsbl/templates}/npm/publish.yml.tpl +0 -0
  24. /package/{templates → rlsbl/templates}/pypi/ci.yml.tpl +0 -0
  25. /package/{templates → rlsbl/templates}/pypi/publish.yml.tpl +0 -0
  26. /package/{templates → rlsbl/templates}/shared/CHANGELOG.md.tpl +0 -0
  27. /package/{templates → rlsbl/templates}/shared/CLAUDE.md.tpl +0 -0
  28. /package/{templates → rlsbl/templates}/shared/LICENSE.tpl +0 -0
  29. /package/{templates → rlsbl/templates}/shared/claude-settings.json.tpl +0 -0
  30. /package/{templates → rlsbl/templates}/shared/gitignore.tpl +0 -0
  31. /package/{templates → rlsbl/templates}/shared/hooks/post-release.sh.tpl +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlsbl",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Release orchestration and project scaffolding for npm and PyPI",
5
5
  "license": "MIT",
6
6
  "bin": {
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 Record a demo GIF with vhs
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(
@@ -1,7 +1,7 @@
1
1
  """Config command: show resolved project configuration."""
2
2
 
3
3
  import os
4
- from ..config import PROJECT_CONFIG, USER_CONFIG, read_json_config, should_tag
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(PROJECT_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 ({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:
@@ -47,10 +47,12 @@ def _parse_next_link(headers):
47
47
  return None
48
48
  for part in link.split(","):
49
49
  if 'rel="next"' in part:
50
- # Extract URL between < and >
51
50
  start = part.index("<") + 1
52
51
  end = part.index(">")
53
- return part[start:end]
52
+ url = part[start:end]
53
+ if not url.startswith("https://api.github.com/"):
54
+ return None
55
+ return url
54
56
  return None
55
57
 
56
58
 
@@ -480,7 +480,7 @@ def run_cmd_multi(registries_list, args, flags):
480
480
 
481
481
  # Process merged publish workflow template
482
482
  merged_tpl_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)),
483
- "..", "templates", "merged")
483
+ "templates", "merged")
484
484
  merged_created, merged_skipped, merged_warnings, merged_hashes = process_mappings(
485
485
  merged_tpl_dir,
486
486
  [{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"}],
@@ -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 project type and read the current version.
11
+ """Detect version using registry adapters.
11
12
 
12
- Returns (version_string, project_type) or (None, None) if undetectable.
13
+ Returns (version_string, registry_name) or (None, None) if undetectable.
13
14
  """
14
- if os.path.exists("go.mod"):
15
- # Go projects store version in a VERSION file
16
- version_path = "VERSION"
17
- if os.path.exists(version_path):
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 24\n"
53
- "Set Width 1200\n"
54
- "Set Height 600\n"
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 3s\n"
64
+ f"Sleep {duration}s\n"
59
65
  )
60
66
 
61
67
  tape_fd, tape_path = tempfile.mkstemp(suffix=".tape", dir=".")
@@ -210,6 +210,18 @@ def run_cmd(registry, args, flags):
210
210
  except Exception:
211
211
  pass
212
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
+
213
225
  # Commit if anything was actually modified (version bump or tagging)
214
226
  needs_commit = new_version != current_version or not is_clean_tree()
215
227
  if files_to_commit and needs_commit:
@@ -2,12 +2,20 @@
2
2
 
3
3
  import sys
4
4
 
5
- from ..utils import run, check_gh_installed, check_gh_auth, get_push_timeout
5
+ from ..utils import run, check_gh_installed, check_gh_auth, get_push_timeout, is_clean_tree
6
6
 
7
7
 
8
8
  def run_cmd(registry, args, flags):
9
- check_gh_installed()
10
- check_gh_auth()
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)
11
19
 
12
20
  # Find the latest tag
13
21
  try:
@@ -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 "{body}" with title "{title}"'],
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
- # Get commit SHA
37
- if args:
38
- commit_sha = args[0]
39
- else:
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
- commit_sha = run("git", ["rev-parse", "HEAD"])
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 a git repository and no commit SHA provided.", file=sys.stderr)
57
+ print("Error: could not get repo info. Is gh installed and authenticated?", file=sys.stderr)
44
58
  sys.exit(1)
45
59
 
46
- # Get repo info for display and URLs
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
- raw = run("gh", ["run", "list", "--commit", commit_sha,
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
- pass
76
- time.sleep(2)
77
-
78
- if not runs:
79
- print(f"rlsbl: {label}: no CI runs found after 30s", file=sys.stderr)
80
- sys.exit(1)
81
-
82
- print(f"rlsbl: {label}: found {len(runs)} CI run(s), watching...", file=sys.stderr)
83
-
84
- # Watch each run sequentially, collecting results
85
- any_failed = False
86
- for ci_run in runs:
87
- run_id = str(ci_run["databaseId"])
88
- workflow_name = ci_run.get("name", f"run {run_id}")
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
- try:
91
- # gh run watch blocks until the run completes;
92
- # --exit-status makes it exit 1 on failure; check=True raises
93
- # CalledProcessError so we can distinguish pass from fail
94
- subprocess.run(
95
- ["gh", "run", "watch", run_id, "--exit-status"],
96
- capture_output=True, text=True, timeout=3600, check=True,
97
- )
98
- print(f"rlsbl: {label}: {workflow_name} passed", file=sys.stderr)
99
- except subprocess.CalledProcessError:
100
- any_failed = True
101
- print(f"rlsbl: {label}: {workflow_name} FAILED", file=sys.stderr)
102
- if repo_slug:
103
- print(f"rlsbl: https://github.com/{repo_slug}/actions/runs/{run_id}",
104
- file=sys.stderr)
105
- except subprocess.TimeoutExpired:
106
- any_failed = True
107
- print(f"rlsbl: {label}: {workflow_name} timed out after 1h", file=sys.stderr)
108
-
109
- # Desktop notification for overall result
110
- if any_failed:
111
- _notify(f"{label}: CI FAILED", "One or more workflows failed")
112
- else:
113
- _notify(f"{label}: CI passed", "All workflows passed")
114
-
115
- sys.exit(1 if any_failed else 0)
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
- PROJECT_CONFIG = os.path.join(".rlsbl", "config.json")
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(PROJECT_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(PROJECT_CONFIG), exist_ok=True)
50
- existing = read_json_config(PROJECT_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(PROJECT_CONFIG, "w", encoding="utf-8") as f:
55
+ with open(_project_config(), "w", encoding="utf-8") as f:
53
56
  json.dump(existing, f, indent=2)
54
57
  f.write("\n")
@@ -39,12 +39,12 @@ def get_version_file():
39
39
 
40
40
  def get_template_dir():
41
41
  """Returns path to the go-specific template directory."""
42
- return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "go")
42
+ return os.path.join(os.path.dirname(__file__), "..", "templates", "go")
43
43
 
44
44
 
45
45
  def get_shared_template_dir():
46
46
  """Returns path to the shared template directory."""
47
- return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "shared")
47
+ return os.path.join(os.path.dirname(__file__), "..", "templates", "shared")
48
48
 
49
49
 
50
50
  def get_template_vars(dir_path):
@@ -32,7 +32,7 @@ def write_version(dir_path, version):
32
32
 
33
33
  # Preserve trailing newline if present
34
34
  trailing_newline = "\n" if raw.endswith("\n") else ""
35
- output = json.dumps(pkg, indent=indent) + trailing_newline
35
+ output = json.dumps(pkg, indent=indent, ensure_ascii=False) + trailing_newline
36
36
  # Atomic write: write to temp file, then rename
37
37
  tmp_path = pkg_path + ".tmp"
38
38
  with open(tmp_path, "w", encoding="utf-8") as f:
@@ -47,12 +47,12 @@ def get_version_file():
47
47
 
48
48
  def get_template_dir():
49
49
  """Returns path to the npm-specific template directory."""
50
- return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "npm")
50
+ return os.path.join(os.path.dirname(__file__), "..", "templates", "npm")
51
51
 
52
52
 
53
53
  def get_shared_template_dir():
54
54
  """Returns path to the shared template directory."""
55
- return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "shared")
55
+ return os.path.join(os.path.dirname(__file__), "..", "templates", "shared")
56
56
 
57
57
 
58
58
  def get_template_vars(dir_path):
@@ -67,12 +67,12 @@ def get_version_file():
67
67
 
68
68
  def get_template_dir():
69
69
  """Returns path to the pypi-specific template directory."""
70
- return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "pypi")
70
+ return os.path.join(os.path.dirname(__file__), "..", "templates", "pypi")
71
71
 
72
72
 
73
73
  def get_shared_template_dir():
74
74
  """Returns path to the shared template directory."""
75
- return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "shared")
75
+ return os.path.join(os.path.dirname(__file__), "..", "templates", "shared")
76
76
 
77
77
 
78
78
  def get_template_vars(dir_path):
package/rlsbl/tagging.py CHANGED
@@ -32,7 +32,7 @@ def ensure_npm_keyword(dir_path=".", quiet=False):
32
32
 
33
33
  # Preserve trailing newline if present
34
34
  trailing_newline = "\n" if raw.endswith("\n") else ""
35
- output = json.dumps(pkg, indent=indent) + trailing_newline
35
+ output = json.dumps(pkg, indent=indent, ensure_ascii=False) + trailing_newline
36
36
 
37
37
  # Atomic write: write to temp file, then rename
38
38
  tmp_path = pkg_path + ".tmp"
@@ -82,11 +82,16 @@ def ensure_pypi_keyword(dir_path=".", quiet=False):
82
82
  # Find the indent used for existing items
83
83
  item_indent_match = re.search(r'\n( +)"', array_content)
84
84
  item_indent = item_indent_match.group(1) if item_indent_match else " "
85
- new_array_content = array_content.rstrip() + f',\n{item_indent}"rlsbl"\n'
85
+ # Strip trailing comma to avoid double comma when the list
86
+ # already has a trailing comma before the closing bracket
87
+ stripped = array_content.rstrip()
88
+ stripped = stripped.rstrip(",")
89
+ new_array_content = stripped + f',\n{item_indent}"rlsbl"\n'
86
90
  else:
87
91
  # Single-line
88
92
  if array_content.strip():
89
- new_array_content = array_content.rstrip() + ', "rlsbl"'
93
+ stripped_sl = array_content.rstrip().rstrip(",")
94
+ new_array_content = stripped_sl + ', "rlsbl"'
90
95
  else:
91
96
  new_array_content = '"rlsbl"'
92
97
  new_field = prefix + new_array_content + "]"
@@ -8,15 +8,19 @@ set -euo pipefail
8
8
  echo "Running pre-release checks..."
9
9
 
10
10
  if [ -f go.mod ]; then
11
- echo "Detected Go project"
11
+ echo " Go: vet + build + test"
12
12
  go vet ./...
13
13
  go build ./...
14
14
  go test ./... -race -short -count=1
15
- elif [ -f package.json ]; then
16
- echo "Detected npm project"
15
+ fi
16
+
17
+ if [ -f package.json ] && node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)" 2>/dev/null; then
18
+ echo " npm: test"
17
19
  npm test
18
- elif [ -f pyproject.toml ]; then
19
- echo "Detected Python project"
20
+ fi
21
+
22
+ if [ -f pyproject.toml ]; then
23
+ echo " Python: pytest"
20
24
  if command -v uv &>/dev/null; then
21
25
  uv run pytest
22
26
  elif command -v pytest &>/dev/null; then
File without changes
File without changes
File without changes
File without changes