rlsbl 0.8.3 → 0.9.1
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/README.md +117 -112
- 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
package/rlsbl/registries/go.py
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
"""Go registry adapter for rlsbl.
|
|
2
|
-
|
|
3
|
-
Go projects use a VERSION file as the source of truth for rlsbl. GoReleaser
|
|
4
|
-
handles the build/publish step triggered by the GitHub Release that rlsbl creates.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
import re
|
|
9
|
-
|
|
10
|
-
from ..utils import run
|
|
11
|
-
|
|
12
|
-
NAME = "go"
|
|
13
|
-
|
|
14
|
-
VERSION_FILE = "VERSION"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def read_version(dir_path):
|
|
18
|
-
"""Read version from the VERSION file."""
|
|
19
|
-
version_path = os.path.join(dir_path, VERSION_FILE)
|
|
20
|
-
if not os.path.exists(version_path):
|
|
21
|
-
raise FileNotFoundError(f"No {VERSION_FILE} file found. Run 'rlsbl scaffold' first.")
|
|
22
|
-
with open(version_path, "r", encoding="utf-8") as f:
|
|
23
|
-
return f.read().strip()
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def write_version(dir_path, version):
|
|
27
|
-
"""Write the new version to the VERSION file."""
|
|
28
|
-
version_path = os.path.join(dir_path, VERSION_FILE)
|
|
29
|
-
tmp_path = version_path + ".tmp"
|
|
30
|
-
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
31
|
-
f.write(version + "\n")
|
|
32
|
-
os.replace(tmp_path, version_path)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_version_file():
|
|
36
|
-
"""Returns the filename that holds the version for this registry."""
|
|
37
|
-
return VERSION_FILE
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def get_template_dir():
|
|
41
|
-
"""Returns path to the go-specific template directory."""
|
|
42
|
-
return os.path.join(os.path.dirname(__file__), "..", "templates", "go")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_shared_template_dir():
|
|
46
|
-
"""Returns path to the shared template directory."""
|
|
47
|
-
return os.path.join(os.path.dirname(__file__), "..", "templates", "shared")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_template_vars(dir_path):
|
|
51
|
-
"""Extract template variables from go.mod."""
|
|
52
|
-
mod_path = os.path.join(dir_path, "go.mod")
|
|
53
|
-
name = ""
|
|
54
|
-
if os.path.exists(mod_path):
|
|
55
|
-
with open(mod_path) as f:
|
|
56
|
-
content = f.read()
|
|
57
|
-
match = re.search(r"^module\s+(\S+)", content, re.MULTILINE)
|
|
58
|
-
if match:
|
|
59
|
-
name = match.group(1)
|
|
60
|
-
|
|
61
|
-
# Derive short name from module path (last segment)
|
|
62
|
-
short_name = name.rsplit("/", 1)[-1] if "/" in name else name
|
|
63
|
-
|
|
64
|
-
# Derive repo name from module path (e.g. "github.com/user/repo")
|
|
65
|
-
repo_name = ""
|
|
66
|
-
repo_match = re.search(r"github\.com/([^/\s]+/[^/\s]+)", name)
|
|
67
|
-
if repo_match:
|
|
68
|
-
repo_name = repo_match.group(1)
|
|
69
|
-
|
|
70
|
-
# Author from git config
|
|
71
|
-
author = ""
|
|
72
|
-
try:
|
|
73
|
-
author = run("git", ["config", "user.name"])
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
try:
|
|
78
|
-
version = read_version(dir_path)
|
|
79
|
-
except FileNotFoundError:
|
|
80
|
-
version = "0.0.0"
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
"name": short_name,
|
|
84
|
-
"modulePath": name,
|
|
85
|
-
"version": version,
|
|
86
|
-
"author": author,
|
|
87
|
-
"repoName": repo_name,
|
|
88
|
-
"binCommand": short_name,
|
|
89
|
-
"publishSetup": "GoReleaser handles binary publishing via GitHub Actions (no secrets needed)",
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def get_template_mappings():
|
|
94
|
-
"""Returns go-specific template mappings (template file -> target path)."""
|
|
95
|
-
return [
|
|
96
|
-
{"template": "VERSION.tpl", "target": "VERSION"},
|
|
97
|
-
{"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
|
|
98
|
-
{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
|
|
99
|
-
{"template": "goreleaser.yml.tpl", "target": ".goreleaser.yml"},
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def get_shared_template_mappings():
|
|
104
|
-
"""Returns shared template mappings."""
|
|
105
|
-
return [
|
|
106
|
-
{"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
|
|
107
|
-
{"template": "gitignore.tpl", "target": ".gitignore"},
|
|
108
|
-
{"template": "LICENSE.tpl", "target": "LICENSE"},
|
|
109
|
-
{"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
|
|
110
|
-
{"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
|
|
111
|
-
{"template": "hooks/pre-release.sh.tpl", "target": ".rlsbl/hooks/pre-release.sh"},
|
|
112
|
-
{"template": "hooks/post-release.sh.tpl", "target": ".rlsbl/hooks/post-release.sh"},
|
|
113
|
-
]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def check_project_exists(dir_path):
|
|
117
|
-
"""Returns True if a go.mod exists in the given directory."""
|
|
118
|
-
return os.path.exists(os.path.join(dir_path, "go.mod"))
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def get_project_init_hint():
|
|
122
|
-
"""Hint for users who haven't initialized their project yet."""
|
|
123
|
-
return 'Run "go mod init <module-path>" first'
|
package/rlsbl/registries/npm.py
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""npm registry adapter for rlsbl."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
NAME = "npm"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def read_version(dir_path):
|
|
11
|
-
"""Read the version from package.json in the given directory."""
|
|
12
|
-
pkg_path = os.path.join(dir_path, "package.json")
|
|
13
|
-
with open(pkg_path, "r", encoding="utf-8") as f:
|
|
14
|
-
pkg = json.load(f)
|
|
15
|
-
if "version" not in pkg:
|
|
16
|
-
raise ValueError(f"No 'version' field in {pkg_path}")
|
|
17
|
-
return pkg["version"]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def write_version(dir_path, version):
|
|
21
|
-
"""Write a new version to package.json, preserving formatting."""
|
|
22
|
-
pkg_path = os.path.join(dir_path, "package.json")
|
|
23
|
-
with open(pkg_path, "r", encoding="utf-8") as f:
|
|
24
|
-
raw = f.read()
|
|
25
|
-
|
|
26
|
-
# Detect indent: look for the first indented line
|
|
27
|
-
indent_match = re.search(r'^( +|\t+)"', raw, re.MULTILINE)
|
|
28
|
-
indent = indent_match.group(1) if indent_match else " "
|
|
29
|
-
|
|
30
|
-
pkg = json.loads(raw)
|
|
31
|
-
pkg["version"] = version
|
|
32
|
-
|
|
33
|
-
# Preserve trailing newline if present
|
|
34
|
-
trailing_newline = "\n" if raw.endswith("\n") else ""
|
|
35
|
-
output = json.dumps(pkg, indent=indent, ensure_ascii=False) + trailing_newline
|
|
36
|
-
# Atomic write: write to temp file, then rename
|
|
37
|
-
tmp_path = pkg_path + ".tmp"
|
|
38
|
-
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
39
|
-
f.write(output)
|
|
40
|
-
os.replace(tmp_path, pkg_path)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def get_version_file():
|
|
44
|
-
"""Returns the filename that holds the version for this registry."""
|
|
45
|
-
return "package.json"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def get_template_dir():
|
|
49
|
-
"""Returns path to the npm-specific template directory."""
|
|
50
|
-
return os.path.join(os.path.dirname(__file__), "..", "templates", "npm")
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_shared_template_dir():
|
|
54
|
-
"""Returns path to the shared template directory."""
|
|
55
|
-
return os.path.join(os.path.dirname(__file__), "..", "templates", "shared")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def get_template_vars(dir_path):
|
|
59
|
-
"""Extract template variables from the target project's package.json."""
|
|
60
|
-
pkg_path = os.path.join(dir_path, "package.json")
|
|
61
|
-
with open(pkg_path, "r", encoding="utf-8") as f:
|
|
62
|
-
pkg = json.load(f)
|
|
63
|
-
|
|
64
|
-
# Derive binCommand from the bin field (first key if object, or package name)
|
|
65
|
-
bin_command = pkg.get("name", "")
|
|
66
|
-
bin_field = pkg.get("bin")
|
|
67
|
-
if isinstance(bin_field, dict) and bin_field:
|
|
68
|
-
bin_command = next(iter(bin_field))
|
|
69
|
-
elif isinstance(bin_field, str):
|
|
70
|
-
bin_command = pkg.get("name", "")
|
|
71
|
-
|
|
72
|
-
# Derive repoName from repository field
|
|
73
|
-
repo_name = ""
|
|
74
|
-
repository = pkg.get("repository")
|
|
75
|
-
if repository:
|
|
76
|
-
url = repository if isinstance(repository, str) else (repository.get("url") or "")
|
|
77
|
-
match = re.search(r"github\.com[/:]([^/]+/[^/.]+)", url)
|
|
78
|
-
if match:
|
|
79
|
-
repo_name = match.group(1)
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
"name": pkg.get("name", ""),
|
|
83
|
-
"version": pkg.get("version", "0.1.0"),
|
|
84
|
-
"binCommand": bin_command,
|
|
85
|
-
"author": pkg.get("author", ""),
|
|
86
|
-
"repoName": repo_name,
|
|
87
|
-
"publishSetup": "Requires NPM_TOKEN secret on GitHub (Settings > Secrets > Actions)",
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def get_template_mappings():
|
|
92
|
-
"""Returns npm-specific template mappings (template file -> target path)."""
|
|
93
|
-
return [
|
|
94
|
-
{"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
|
|
95
|
-
{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
|
|
96
|
-
]
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def get_shared_template_mappings():
|
|
100
|
-
"""Returns shared template mappings."""
|
|
101
|
-
return [
|
|
102
|
-
{"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
|
|
103
|
-
{"template": "gitignore.tpl", "target": ".gitignore"},
|
|
104
|
-
{"template": "LICENSE.tpl", "target": "LICENSE"},
|
|
105
|
-
{"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
|
|
106
|
-
{"template": "hooks/pre-release.sh.tpl", "target": ".rlsbl/hooks/pre-release.sh"},
|
|
107
|
-
{"template": "hooks/post-release.sh.tpl", "target": ".rlsbl/hooks/post-release.sh"},
|
|
108
|
-
{"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def check_project_exists(dir_path):
|
|
113
|
-
"""Returns True if a package.json exists in the given directory."""
|
|
114
|
-
return os.path.exists(os.path.join(dir_path, "package.json"))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def get_project_init_hint():
|
|
118
|
-
"""Hint for users who haven't initialized their project yet."""
|
|
119
|
-
return 'Run "npm init" first'
|
package/rlsbl/registries/pypi.py
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"""PyPI registry adapter for rlsbl."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import tomllib
|
|
6
|
-
|
|
7
|
-
from ..utils import run
|
|
8
|
-
|
|
9
|
-
NAME = "pypi"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def read_version(dir_path):
|
|
13
|
-
"""Read the version from pyproject.toml in the given directory."""
|
|
14
|
-
toml_path = os.path.join(dir_path, "pyproject.toml")
|
|
15
|
-
with open(toml_path, "rb") as f:
|
|
16
|
-
data = tomllib.load(f)
|
|
17
|
-
try:
|
|
18
|
-
return data["project"]["version"]
|
|
19
|
-
except KeyError:
|
|
20
|
-
raise ValueError(f"No [project].version in {toml_path}")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def write_version(dir_path, version):
|
|
24
|
-
"""Write a new version to pyproject.toml using regex replacement.
|
|
25
|
-
|
|
26
|
-
tomllib is read-only (no stdlib TOML writer), so we use a regex to
|
|
27
|
-
replace the version string within the [project] section only,
|
|
28
|
-
preserving all other formatting.
|
|
29
|
-
"""
|
|
30
|
-
toml_path = os.path.join(dir_path, "pyproject.toml")
|
|
31
|
-
with open(toml_path, "r", encoding="utf-8") as f:
|
|
32
|
-
content = f.read()
|
|
33
|
-
|
|
34
|
-
# Find [project] section boundaries to avoid matching version keys
|
|
35
|
-
# in other sections (e.g. [tool.something])
|
|
36
|
-
project_match = re.search(r"^\[project\]\s*$", content, re.MULTILINE)
|
|
37
|
-
if not project_match:
|
|
38
|
-
raise ValueError("No [project] section found in pyproject.toml")
|
|
39
|
-
|
|
40
|
-
section_start = project_match.end()
|
|
41
|
-
# Find next top-level section header or EOF
|
|
42
|
-
next_section = re.search(r"^\[", content[section_start:], re.MULTILINE)
|
|
43
|
-
section_end = section_start + next_section.start() if next_section else len(content)
|
|
44
|
-
|
|
45
|
-
# Replace version only within [project] section
|
|
46
|
-
section = content[section_start:section_end]
|
|
47
|
-
updated_section = re.sub(
|
|
48
|
-
r'^(version\s*=\s*)"[^"]+"',
|
|
49
|
-
rf'\g<1>"{version}"',
|
|
50
|
-
section,
|
|
51
|
-
count=1,
|
|
52
|
-
flags=re.MULTILINE,
|
|
53
|
-
)
|
|
54
|
-
updated = content[:section_start] + updated_section + content[section_end:]
|
|
55
|
-
|
|
56
|
-
# Atomic write: write to temp file, then rename
|
|
57
|
-
tmp_path = toml_path + ".tmp"
|
|
58
|
-
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
59
|
-
f.write(updated)
|
|
60
|
-
os.replace(tmp_path, toml_path)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def get_version_file():
|
|
64
|
-
"""Returns the filename that holds the version for this registry."""
|
|
65
|
-
return "pyproject.toml"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def get_template_dir():
|
|
69
|
-
"""Returns path to the pypi-specific template directory."""
|
|
70
|
-
return os.path.join(os.path.dirname(__file__), "..", "templates", "pypi")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def get_shared_template_dir():
|
|
74
|
-
"""Returns path to the shared template directory."""
|
|
75
|
-
return os.path.join(os.path.dirname(__file__), "..", "templates", "shared")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_template_vars(dir_path):
|
|
79
|
-
"""Extract template variables from the target project's pyproject.toml."""
|
|
80
|
-
toml_path = os.path.join(dir_path, "pyproject.toml")
|
|
81
|
-
with open(toml_path, "rb") as f:
|
|
82
|
-
data = tomllib.load(f)
|
|
83
|
-
|
|
84
|
-
project = data.get("project", {})
|
|
85
|
-
name = project.get("name", "")
|
|
86
|
-
version = project.get("version", "0.1.0")
|
|
87
|
-
|
|
88
|
-
# Extract author -- fall back to git config
|
|
89
|
-
author = ""
|
|
90
|
-
try:
|
|
91
|
-
author = run("git", ["config", "user.name"])
|
|
92
|
-
except Exception:
|
|
93
|
-
pass
|
|
94
|
-
|
|
95
|
-
# Extract repo name from project.urls
|
|
96
|
-
repo_name = ""
|
|
97
|
-
urls = project.get("urls", {})
|
|
98
|
-
for url in urls.values():
|
|
99
|
-
match = re.search(r"github\.com/([^/\s\"]+/[^/\s\"]+)", url)
|
|
100
|
-
if match:
|
|
101
|
-
repo_name = match.group(1).removesuffix(".git")
|
|
102
|
-
break
|
|
103
|
-
|
|
104
|
-
# Derive binCommand from project.scripts (CLI entry points)
|
|
105
|
-
bin_command = ""
|
|
106
|
-
scripts = project.get("scripts", {})
|
|
107
|
-
if scripts:
|
|
108
|
-
bin_command = next(iter(scripts)) # first script entry
|
|
109
|
-
|
|
110
|
-
# Derive the actual Python import name.
|
|
111
|
-
# 1) Check hatch build config for an explicit packages list.
|
|
112
|
-
import_name = None
|
|
113
|
-
hatch = data.get("tool", {}).get("hatch", {})
|
|
114
|
-
packages = (
|
|
115
|
-
hatch.get("build", {}).get("targets", {}).get("wheel", {}).get("packages")
|
|
116
|
-
)
|
|
117
|
-
if packages and isinstance(packages, list) and len(packages) > 0:
|
|
118
|
-
# Strip src/ prefix (common hatch src layout) and convert path to module name
|
|
119
|
-
first_pkg = packages[0]
|
|
120
|
-
import_name = first_pkg.removeprefix("src/").replace("/", ".")
|
|
121
|
-
|
|
122
|
-
# 2) Fall back to filesystem detection, then underscore convention.
|
|
123
|
-
if not import_name:
|
|
124
|
-
underscored = name.replace("-", "_")
|
|
125
|
-
if os.path.isdir(os.path.join(dir_path, underscored)):
|
|
126
|
-
import_name = underscored
|
|
127
|
-
elif os.path.isdir(os.path.join(dir_path, name)):
|
|
128
|
-
import_name = name
|
|
129
|
-
else:
|
|
130
|
-
import_name = underscored # fallback to convention
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
"name": name,
|
|
134
|
-
"version": version,
|
|
135
|
-
"binCommand": bin_command,
|
|
136
|
-
"author": author,
|
|
137
|
-
"repoName": repo_name,
|
|
138
|
-
"importName": import_name,
|
|
139
|
-
"publishSetup": "Configure Trusted Publishing on pypi.org for automated PyPI releases",
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def get_template_mappings():
|
|
144
|
-
"""Returns pypi-specific template mappings (template file -> target path)."""
|
|
145
|
-
return [
|
|
146
|
-
{"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
|
|
147
|
-
{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def get_shared_template_mappings():
|
|
152
|
-
"""Returns shared template mappings."""
|
|
153
|
-
return [
|
|
154
|
-
{"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
|
|
155
|
-
{"template": "gitignore.tpl", "target": ".gitignore"},
|
|
156
|
-
{"template": "LICENSE.tpl", "target": "LICENSE"},
|
|
157
|
-
{"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
|
|
158
|
-
{"template": "hooks/pre-release.sh.tpl", "target": ".rlsbl/hooks/pre-release.sh"},
|
|
159
|
-
{"template": "hooks/post-release.sh.tpl", "target": ".rlsbl/hooks/post-release.sh"},
|
|
160
|
-
{"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def check_project_exists(dir_path):
|
|
165
|
-
"""Returns True if a pyproject.toml exists in the given directory."""
|
|
166
|
-
return os.path.exists(os.path.join(dir_path, "pyproject.toml"))
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def get_project_init_hint():
|
|
170
|
-
"""Hint for users who haven't initialized their project yet."""
|
|
171
|
-
return 'Run "uv init" first'
|
package/rlsbl/tagging.py
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""Tagging module: inject "rlsbl" keywords into manifests and GitHub topics."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import re
|
|
6
|
-
import subprocess
|
|
7
|
-
import tomllib
|
|
8
|
-
import urllib.request
|
|
9
|
-
import urllib.error
|
|
10
|
-
|
|
11
|
-
from .utils import run
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def ensure_npm_keyword(dir_path=".", quiet=False):
|
|
15
|
-
"""Add "rlsbl" to the keywords array in package.json if not already present."""
|
|
16
|
-
pkg_path = os.path.join(dir_path, "package.json")
|
|
17
|
-
with open(pkg_path, "r", encoding="utf-8") as f:
|
|
18
|
-
raw = f.read()
|
|
19
|
-
|
|
20
|
-
# Detect indent: look for the first indented line
|
|
21
|
-
indent_match = re.search(r'^( +|\t+)"', raw, re.MULTILINE)
|
|
22
|
-
indent = indent_match.group(1) if indent_match else " "
|
|
23
|
-
|
|
24
|
-
pkg = json.loads(raw)
|
|
25
|
-
keywords = pkg.get("keywords", [])
|
|
26
|
-
|
|
27
|
-
if "rlsbl" in keywords:
|
|
28
|
-
return False
|
|
29
|
-
|
|
30
|
-
keywords.append("rlsbl")
|
|
31
|
-
pkg["keywords"] = keywords
|
|
32
|
-
|
|
33
|
-
# Preserve trailing newline if present
|
|
34
|
-
trailing_newline = "\n" if raw.endswith("\n") else ""
|
|
35
|
-
output = json.dumps(pkg, indent=indent, ensure_ascii=False) + trailing_newline
|
|
36
|
-
|
|
37
|
-
# Atomic write: write to temp file, then rename
|
|
38
|
-
tmp_path = pkg_path + ".tmp"
|
|
39
|
-
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
40
|
-
f.write(output)
|
|
41
|
-
os.replace(tmp_path, pkg_path)
|
|
42
|
-
|
|
43
|
-
if not quiet:
|
|
44
|
-
print('Tagged package.json with "rlsbl" keyword')
|
|
45
|
-
return True
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def ensure_pypi_keyword(dir_path=".", quiet=False):
|
|
49
|
-
"""Add "rlsbl" to the keywords array in pyproject.toml if not already present."""
|
|
50
|
-
toml_path = os.path.join(dir_path, "pyproject.toml")
|
|
51
|
-
with open(toml_path, "rb") as f:
|
|
52
|
-
data = tomllib.load(f)
|
|
53
|
-
|
|
54
|
-
# Check if already tagged
|
|
55
|
-
existing = data.get("project", {}).get("keywords", [])
|
|
56
|
-
if "rlsbl" in existing:
|
|
57
|
-
return False
|
|
58
|
-
|
|
59
|
-
# Read as text for regex-based editing
|
|
60
|
-
with open(toml_path, "r", encoding="utf-8") as f:
|
|
61
|
-
content = f.read()
|
|
62
|
-
|
|
63
|
-
# Find [project] section boundaries
|
|
64
|
-
project_match = re.search(r"^\[project\]\s*$", content, re.MULTILINE)
|
|
65
|
-
if not project_match:
|
|
66
|
-
raise ValueError("No [project] section found in pyproject.toml")
|
|
67
|
-
|
|
68
|
-
section_start = project_match.end()
|
|
69
|
-
next_section = re.search(r"^\[", content[section_start:], re.MULTILINE)
|
|
70
|
-
section_end = section_start + next_section.start() if next_section else len(content)
|
|
71
|
-
section = content[section_start:section_end]
|
|
72
|
-
|
|
73
|
-
# Case 1: keywords field already exists -- add "rlsbl" to the array
|
|
74
|
-
# Use DOTALL to handle multi-line arrays (e.g. keywords = [\n "foo",\n])
|
|
75
|
-
keywords_match = re.search(r'^(keywords\s*=\s*\[)(.*?)\]', section, re.MULTILINE | re.DOTALL)
|
|
76
|
-
if keywords_match:
|
|
77
|
-
prefix = keywords_match.group(1)
|
|
78
|
-
array_content = keywords_match.group(2)
|
|
79
|
-
# Detect if multi-line (contains newline between brackets)
|
|
80
|
-
if "\n" in array_content:
|
|
81
|
-
# Multi-line: insert before the closing bracket on its own line
|
|
82
|
-
# Find the indent used for existing items
|
|
83
|
-
item_indent_match = re.search(r'\n( +)"', array_content)
|
|
84
|
-
item_indent = item_indent_match.group(1) if item_indent_match else " "
|
|
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'
|
|
90
|
-
else:
|
|
91
|
-
# Single-line
|
|
92
|
-
if array_content.strip():
|
|
93
|
-
stripped_sl = array_content.rstrip().rstrip(",")
|
|
94
|
-
new_array_content = stripped_sl + ', "rlsbl"'
|
|
95
|
-
else:
|
|
96
|
-
new_array_content = '"rlsbl"'
|
|
97
|
-
new_field = prefix + new_array_content + "]"
|
|
98
|
-
updated_section = section[:keywords_match.start()] + new_field + section[keywords_match.end():]
|
|
99
|
-
else:
|
|
100
|
-
# Case 2: keywords field missing -- insert after the version line
|
|
101
|
-
version_match = re.search(r'^version\s*=\s*"[^"]*"\s*$', section, re.MULTILINE)
|
|
102
|
-
if version_match:
|
|
103
|
-
insert_pos = version_match.end()
|
|
104
|
-
else:
|
|
105
|
-
# Fallback: insert at the beginning of the section
|
|
106
|
-
insert_pos = 0
|
|
107
|
-
updated_section = (
|
|
108
|
-
section[:insert_pos] + '\nkeywords = ["rlsbl"]' + section[insert_pos:]
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
updated = content[:section_start] + updated_section + content[section_end:]
|
|
112
|
-
|
|
113
|
-
# Atomic write: write to temp file, then rename
|
|
114
|
-
tmp_path = toml_path + ".tmp"
|
|
115
|
-
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
116
|
-
f.write(updated)
|
|
117
|
-
os.replace(tmp_path, toml_path)
|
|
118
|
-
|
|
119
|
-
if not quiet:
|
|
120
|
-
print('Tagged pyproject.toml with "rlsbl" keyword')
|
|
121
|
-
return True
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def ensure_github_topic(quiet=False):
|
|
125
|
-
"""Add "rlsbl" topic to the GitHub repository if not already present."""
|
|
126
|
-
# Try to get a GitHub token (env var first, then gh CLI)
|
|
127
|
-
token = os.environ.get("GITHUB_TOKEN")
|
|
128
|
-
if not token:
|
|
129
|
-
try:
|
|
130
|
-
token = run("gh", ["auth", "token"])
|
|
131
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
132
|
-
pass
|
|
133
|
-
|
|
134
|
-
if not token:
|
|
135
|
-
if not quiet:
|
|
136
|
-
print("No GitHub token available. Run 'gh auth login' or set GITHUB_TOKEN.")
|
|
137
|
-
return False
|
|
138
|
-
|
|
139
|
-
# Detect repo name
|
|
140
|
-
repo_name = None
|
|
141
|
-
try:
|
|
142
|
-
repo_name = run("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
|
|
143
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
144
|
-
pass
|
|
145
|
-
|
|
146
|
-
if not repo_name:
|
|
147
|
-
# Fallback: parse from git remote
|
|
148
|
-
try:
|
|
149
|
-
remote_url = run("git", ["remote", "get-url", "origin"])
|
|
150
|
-
match = re.search(r"github\.com[/:]([^/]+/[^/.]+)", remote_url)
|
|
151
|
-
if match:
|
|
152
|
-
repo_name = match.group(1).removesuffix(".git")
|
|
153
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
154
|
-
pass
|
|
155
|
-
|
|
156
|
-
if not repo_name:
|
|
157
|
-
if not quiet:
|
|
158
|
-
print("Warning: could not detect GitHub repository name.")
|
|
159
|
-
return False
|
|
160
|
-
|
|
161
|
-
owner, repo = repo_name.split("/", 1)
|
|
162
|
-
api_url = f"https://api.github.com/repos/{owner}/{repo}/topics"
|
|
163
|
-
headers = {
|
|
164
|
-
"Authorization": f"token {token}",
|
|
165
|
-
"Accept": "application/vnd.github+json",
|
|
166
|
-
"User-Agent": "rlsbl-cli",
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
# GET existing topics
|
|
170
|
-
try:
|
|
171
|
-
req = urllib.request.Request(api_url, headers=headers)
|
|
172
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
173
|
-
data = json.loads(resp.read().decode("utf-8"))
|
|
174
|
-
except (urllib.error.URLError, OSError, json.JSONDecodeError) as e:
|
|
175
|
-
if not quiet:
|
|
176
|
-
print(f"Warning: failed to fetch GitHub topics: {e}")
|
|
177
|
-
return False
|
|
178
|
-
|
|
179
|
-
topics = data.get("names", [])
|
|
180
|
-
if "rlsbl" in topics:
|
|
181
|
-
return False
|
|
182
|
-
|
|
183
|
-
# PUT with merged topics list
|
|
184
|
-
topics.append("rlsbl")
|
|
185
|
-
payload = json.dumps({"names": topics}).encode("utf-8")
|
|
186
|
-
try:
|
|
187
|
-
req = urllib.request.Request(api_url, data=payload, headers=headers, method="PUT")
|
|
188
|
-
req.add_header("Content-Type", "application/json")
|
|
189
|
-
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
190
|
-
resp.read() # consume response
|
|
191
|
-
except (urllib.error.URLError, OSError) as e:
|
|
192
|
-
if not quiet:
|
|
193
|
-
print(f"Warning: failed to set GitHub topics: {e}")
|
|
194
|
-
return False
|
|
195
|
-
|
|
196
|
-
if not quiet:
|
|
197
|
-
print('Added "rlsbl" topic to GitHub repository')
|
|
198
|
-
return True
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def ensure_tags(registries, dir_path=".", quiet=False):
|
|
202
|
-
"""Tag manifests and GitHub repo based on detected registries."""
|
|
203
|
-
if "npm" in registries:
|
|
204
|
-
ensure_npm_keyword(dir_path, quiet=quiet)
|
|
205
|
-
if "pypi" in registries:
|
|
206
|
-
ensure_pypi_keyword(dir_path, quiet=quiet)
|
|
207
|
-
ensure_github_topic(quiet=quiet)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{{version}}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v6
|
|
14
|
-
- uses: actions/setup-go@v6
|
|
15
|
-
with:
|
|
16
|
-
go-version-file: go.mod
|
|
17
|
-
- run: go vet ./...
|
|
18
|
-
- run: go test ./... -race -short -timeout=10m
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
version: 2
|
|
2
|
-
|
|
3
|
-
builds:
|
|
4
|
-
- env:
|
|
5
|
-
- CGO_ENABLED=0
|
|
6
|
-
goos:
|
|
7
|
-
- linux
|
|
8
|
-
- darwin
|
|
9
|
-
- windows
|
|
10
|
-
goarch:
|
|
11
|
-
- amd64
|
|
12
|
-
- arm64
|
|
13
|
-
|
|
14
|
-
archives:
|
|
15
|
-
- format: tar.gz
|
|
16
|
-
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
|
17
|
-
format_overrides:
|
|
18
|
-
- goos: windows
|
|
19
|
-
format: zip
|
|
20
|
-
|
|
21
|
-
checksum:
|
|
22
|
-
name_template: checksums.txt
|
|
23
|
-
|
|
24
|
-
changelog:
|
|
25
|
-
use: github-native
|