rlsbl 0.3.0 → 0.4.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 +1 -1
- package/rlsbl/__init__.py +12 -9
- package/rlsbl/commands/check.py +39 -3
- package/rlsbl/commands/config.py +2 -1
- package/rlsbl/commands/init_cmd.py +5 -0
- package/rlsbl/commands/release.py +29 -14
- package/rlsbl/commands/status.py +1 -1
- package/rlsbl/registries/__init__.py +2 -2
- package/rlsbl/registries/go.py +119 -0
- package/templates/go/ci.yml.tpl +21 -0
- package/templates/go/goreleaser.yml.tpl +25 -0
- package/templates/go/publish.yml.tpl +25 -0
package/package.json
CHANGED
package/rlsbl/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""rlsbl: Release orchestration and project scaffolding for npm and
|
|
1
|
+
"""rlsbl: Release orchestration and project scaffolding for npm, PyPI, and Go."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
@@ -9,12 +9,12 @@ try:
|
|
|
9
9
|
except Exception:
|
|
10
10
|
__version__ = "unknown"
|
|
11
11
|
|
|
12
|
-
REGISTRIES = ("npm", "pypi")
|
|
12
|
+
REGISTRIES = ("npm", "pypi", "go")
|
|
13
13
|
COMMANDS = ("release", "status", "scaffold", "check", "config", "undo")
|
|
14
14
|
COMMAND_ALIASES = {"init": "scaffold"}
|
|
15
15
|
|
|
16
16
|
HELP = f"""\
|
|
17
|
-
rlsbl v{__version__} -- Release orchestration and project scaffolding for npm and
|
|
17
|
+
rlsbl v{__version__} -- Release orchestration and project scaffolding for npm, PyPI, and Go
|
|
18
18
|
|
|
19
19
|
Usage:
|
|
20
20
|
rlsbl release [patch|minor|major] [--dry-run] [--yes] [--quiet] Orchestrate a release
|
|
@@ -25,7 +25,7 @@ Usage:
|
|
|
25
25
|
rlsbl undo [--yes] Revert the last release
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
|
-
--registry <npm|pypi> Target a specific registry (auto-detected if omitted)
|
|
28
|
+
--registry <npm|pypi|go> Target a specific registry (auto-detected if omitted)
|
|
29
29
|
--help, -h Show this help
|
|
30
30
|
--version, -v Show version"""
|
|
31
31
|
|
|
@@ -40,6 +40,8 @@ def detect_registries():
|
|
|
40
40
|
found.append("npm")
|
|
41
41
|
if os.path.exists("pyproject.toml"):
|
|
42
42
|
found.append("pypi")
|
|
43
|
+
if os.path.exists("go.mod"):
|
|
44
|
+
found.append("go")
|
|
43
45
|
return found
|
|
44
46
|
|
|
45
47
|
|
|
@@ -144,9 +146,10 @@ def main():
|
|
|
144
146
|
if registry:
|
|
145
147
|
handler.run_cmd(registry, args, flags)
|
|
146
148
|
else:
|
|
147
|
-
|
|
149
|
+
all_registries = ["npm", "pypi", "go"]
|
|
150
|
+
for i, r in enumerate(all_registries):
|
|
148
151
|
handler.run_cmd(r, args, flags)
|
|
149
|
-
if i < 1:
|
|
152
|
+
if i < len(all_registries) - 1:
|
|
150
153
|
print("")
|
|
151
154
|
elif command == "scaffold":
|
|
152
155
|
if registry:
|
|
@@ -154,7 +157,7 @@ def main():
|
|
|
154
157
|
else:
|
|
155
158
|
regs = detect_registries()
|
|
156
159
|
if not regs:
|
|
157
|
-
print("Error: no package.json
|
|
160
|
+
print("Error: no package.json, pyproject.toml, or go.mod found.", file=sys.stderr)
|
|
158
161
|
sys.exit(1)
|
|
159
162
|
if len(regs) > 1:
|
|
160
163
|
handler.run_cmd_multi(regs, args, flags)
|
|
@@ -169,7 +172,7 @@ def main():
|
|
|
169
172
|
if not registry:
|
|
170
173
|
regs = detect_registries()
|
|
171
174
|
if not regs:
|
|
172
|
-
print("Error: no package.json
|
|
175
|
+
print("Error: no package.json, pyproject.toml, or go.mod found.", file=sys.stderr)
|
|
173
176
|
sys.exit(1)
|
|
174
177
|
registry = regs[0]
|
|
175
178
|
handler.run_cmd(registry, args, flags)
|
|
@@ -178,7 +181,7 @@ def main():
|
|
|
178
181
|
if not registry:
|
|
179
182
|
regs = detect_registries()
|
|
180
183
|
if not regs:
|
|
181
|
-
print("Error: no package.json
|
|
184
|
+
print("Error: no package.json, pyproject.toml, or go.mod found.", file=sys.stderr)
|
|
182
185
|
sys.exit(1)
|
|
183
186
|
registry = regs[0]
|
|
184
187
|
handler.run_cmd(registry, args, flags)
|
package/rlsbl/commands/check.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Check command: check package name availability on npm or
|
|
1
|
+
"""Check command: check package name availability on npm, PyPI, or Go (pkg.go.dev)."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import subprocess
|
|
@@ -165,10 +165,44 @@ def _check_name_pypi(name):
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
def check_go_availability(name):
|
|
169
|
+
"""Check if a Go module path exists on pkg.go.dev.
|
|
170
|
+
|
|
171
|
+
Returns {"status": "available"|"taken"|"error", "message"?: str}.
|
|
172
|
+
"""
|
|
173
|
+
url = f"https://pkg.go.dev/{name}"
|
|
174
|
+
try:
|
|
175
|
+
req = urllib.request.Request(url, method="GET")
|
|
176
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
177
|
+
if resp.status == 200:
|
|
178
|
+
return {"status": "taken"}
|
|
179
|
+
return {"status": "error", "message": f"Unexpected status {resp.status}"}
|
|
180
|
+
except urllib.error.HTTPError as e:
|
|
181
|
+
if e.code == 404:
|
|
182
|
+
return {"status": "available"}
|
|
183
|
+
return {"status": "error", "message": f"Unexpected status {e.code}"}
|
|
184
|
+
except Exception as e:
|
|
185
|
+
return {"status": "error", "message": str(e) or "Network error"}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _check_name_go(name):
|
|
189
|
+
"""Check Go module path availability on pkg.go.dev."""
|
|
190
|
+
print(f'Checking pkg.go.dev for "{name}"...')
|
|
191
|
+
|
|
192
|
+
result = check_go_availability(name)
|
|
193
|
+
if result["status"] == "error":
|
|
194
|
+
print(f"Error checking pkg.go.dev: {result['message']}", file=sys.stderr)
|
|
195
|
+
sys.exit(1)
|
|
196
|
+
if result["status"] == "available":
|
|
197
|
+
print(f'"{name}" is available on pkg.go.dev.')
|
|
198
|
+
else:
|
|
199
|
+
print(f'"{name}" already exists on pkg.go.dev.')
|
|
200
|
+
|
|
201
|
+
|
|
168
202
|
def run_cmd(registry, args, flags):
|
|
169
203
|
"""Check command handler.
|
|
170
204
|
|
|
171
|
-
Checks package name availability on npm or
|
|
205
|
+
Checks package name availability on npm, PyPI, or Go, and warns about similar names.
|
|
172
206
|
"""
|
|
173
207
|
name = args[0] if args else None
|
|
174
208
|
if not name:
|
|
@@ -180,5 +214,7 @@ def run_cmd(registry, args, flags):
|
|
|
180
214
|
|
|
181
215
|
if registry == "npm":
|
|
182
216
|
_check_name_npm(name)
|
|
183
|
-
|
|
217
|
+
elif registry == "pypi":
|
|
184
218
|
_check_name_pypi(name)
|
|
219
|
+
elif registry == "go":
|
|
220
|
+
_check_name_go(name)
|
package/rlsbl/commands/config.py
CHANGED
|
@@ -9,7 +9,8 @@ def run_cmd(registry, args, flags):
|
|
|
9
9
|
for name, reg in REGISTRIES.items():
|
|
10
10
|
if reg.check_project_exists("."):
|
|
11
11
|
version = reg.read_version(".")
|
|
12
|
-
|
|
12
|
+
vfile = reg.get_version_file() or "git tag"
|
|
13
|
+
print(f" {name}: {vfile} (v{version})")
|
|
13
14
|
else:
|
|
14
15
|
print(f" {name}: not found")
|
|
15
16
|
|
|
@@ -60,6 +60,11 @@ NEXT_STEPS = {
|
|
|
60
60
|
"Configure Trusted Publishing on pypi.org",
|
|
61
61
|
"Run rlsbl release [patch|minor|major]",
|
|
62
62
|
],
|
|
63
|
+
"go": [
|
|
64
|
+
"Install GoReleaser (https://goreleaser.com/install/)",
|
|
65
|
+
"Push to GitHub to activate the CI workflow",
|
|
66
|
+
"Run rlsbl release [patch|minor|major]",
|
|
67
|
+
],
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
|
|
@@ -120,7 +120,9 @@ def run_cmd(registry, args, flags):
|
|
|
120
120
|
if name == registry:
|
|
121
121
|
continue
|
|
122
122
|
if other_reg.check_project_exists("."):
|
|
123
|
-
|
|
123
|
+
other_file = other_reg.get_version_file()
|
|
124
|
+
if other_file:
|
|
125
|
+
other_files.append(other_file)
|
|
124
126
|
if other_files:
|
|
125
127
|
log(f"Sync to: {', '.join(other_files)}")
|
|
126
128
|
log(f"Changelog:\n{changelog_entry}")
|
|
@@ -129,18 +131,25 @@ def run_cmd(registry, args, flags):
|
|
|
129
131
|
|
|
130
132
|
# Pre-compute which files will be modified
|
|
131
133
|
version_file = reg.get_version_file()
|
|
132
|
-
files_to_commit = [
|
|
134
|
+
files_to_commit = []
|
|
135
|
+
if version_file:
|
|
136
|
+
files_to_commit.append(version_file)
|
|
133
137
|
for name, other_reg in REGISTRIES.items():
|
|
134
138
|
if name == registry:
|
|
135
139
|
continue
|
|
136
140
|
if other_reg.check_project_exists("."):
|
|
137
|
-
|
|
141
|
+
other_file = other_reg.get_version_file()
|
|
142
|
+
if other_file:
|
|
143
|
+
files_to_commit.append(other_file)
|
|
138
144
|
|
|
139
145
|
# Confirmation prompt (skip with --yes)
|
|
140
146
|
if not flags.get("yes"):
|
|
141
147
|
print(f"\nAbout to release {new_version} ({bump_type}) on {branch}")
|
|
142
148
|
print(f" Tag: {tag}")
|
|
143
|
-
|
|
149
|
+
if files_to_commit:
|
|
150
|
+
print(f" Files: {', '.join(files_to_commit)}")
|
|
151
|
+
else:
|
|
152
|
+
print(" Files: (none -- version is the git tag)")
|
|
144
153
|
try:
|
|
145
154
|
answer = input("Proceed? [y/N] ").strip().lower()
|
|
146
155
|
except (EOFError, KeyboardInterrupt):
|
|
@@ -151,25 +160,31 @@ def run_cmd(registry, args, flags):
|
|
|
151
160
|
sys.exit(0)
|
|
152
161
|
|
|
153
162
|
# Write new version to the primary registry file
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
if version_file:
|
|
164
|
+
reg.write_version(".", new_version)
|
|
165
|
+
log(f"Updated version in {version_file}")
|
|
156
166
|
|
|
157
167
|
# Sync version to all other recognized version files
|
|
158
168
|
for name, other_reg in REGISTRIES.items():
|
|
159
169
|
if name == registry:
|
|
160
170
|
continue
|
|
161
171
|
if other_reg.check_project_exists("."):
|
|
162
|
-
other_reg.
|
|
163
|
-
|
|
172
|
+
other_file = other_reg.get_version_file()
|
|
173
|
+
if other_file:
|
|
174
|
+
other_reg.write_version(".", new_version)
|
|
175
|
+
log(f"Synced version to {other_file}")
|
|
164
176
|
|
|
165
177
|
# Commit all bumped version files together
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
if files_to_commit:
|
|
179
|
+
commit_tool = find_commit_tool()
|
|
180
|
+
if commit_tool == "safegit":
|
|
181
|
+
run(commit_tool, ["commit", "-m", tag, "--", *files_to_commit])
|
|
182
|
+
else:
|
|
183
|
+
run("git", ["add", *files_to_commit])
|
|
184
|
+
run("git", ["commit", "-m", tag])
|
|
185
|
+
log(f"Committed: {tag}")
|
|
169
186
|
else:
|
|
170
|
-
|
|
171
|
-
run("git", ["commit", "-m", tag])
|
|
172
|
-
log(f"Committed: {tag}")
|
|
187
|
+
log("No version files to commit (version is the git tag)")
|
|
173
188
|
|
|
174
189
|
# Create local git tag
|
|
175
190
|
run("git", ["tag", tag])
|
package/rlsbl/commands/status.py
CHANGED
|
@@ -33,7 +33,7 @@ def run_cmd(registry, args, flags):
|
|
|
33
33
|
for r_name, r_mod in REGISTRIES.items():
|
|
34
34
|
if r_mod.check_project_exists("."):
|
|
35
35
|
ver = r_mod.read_version(".")
|
|
36
|
-
file = r_mod.get_version_file()
|
|
36
|
+
file = r_mod.get_version_file() or "git tag"
|
|
37
37
|
print(f"Version: {ver} ({r_name}, {file})")
|
|
38
38
|
|
|
39
39
|
# Git info
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Go registry adapter for rlsbl.
|
|
2
|
+
|
|
3
|
+
Go modules are versioned by git tags, not version files. GoReleaser handles
|
|
4
|
+
the build/publish step. rlsbl's role: changelog validation, tagging, GitHub
|
|
5
|
+
Release creation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from ..utils import run
|
|
12
|
+
|
|
13
|
+
NAME = "go"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_version(dir_path):
|
|
17
|
+
"""Read version from the latest git tag.
|
|
18
|
+
|
|
19
|
+
Go modules have no version file -- the version IS the git tag.
|
|
20
|
+
Returns "0.0.0" if no tags exist yet.
|
|
21
|
+
"""
|
|
22
|
+
try:
|
|
23
|
+
tag = run("git", ["describe", "--tags", "--abbrev=0"])
|
|
24
|
+
return tag.lstrip("v")
|
|
25
|
+
except Exception:
|
|
26
|
+
return "0.0.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_version(dir_path, version):
|
|
30
|
+
"""No-op: Go versions are git tags, not file fields."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_version_file():
|
|
35
|
+
"""Go has no version file -- version is the git tag."""
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_template_dir():
|
|
40
|
+
"""Returns path to the go-specific template directory."""
|
|
41
|
+
return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "go")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_shared_template_dir():
|
|
45
|
+
"""Returns path to the shared template directory."""
|
|
46
|
+
return os.path.join(os.path.dirname(__file__), "..", "..", "templates", "shared")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_template_vars(dir_path):
|
|
50
|
+
"""Extract template variables from go.mod."""
|
|
51
|
+
mod_path = os.path.join(dir_path, "go.mod")
|
|
52
|
+
name = ""
|
|
53
|
+
if os.path.exists(mod_path):
|
|
54
|
+
with open(mod_path) as f:
|
|
55
|
+
content = f.read()
|
|
56
|
+
match = re.search(r"^module\s+(\S+)", content, re.MULTILINE)
|
|
57
|
+
if match:
|
|
58
|
+
name = match.group(1)
|
|
59
|
+
|
|
60
|
+
# Derive short name from module path (last segment)
|
|
61
|
+
short_name = name.rsplit("/", 1)[-1] if "/" in name else name
|
|
62
|
+
|
|
63
|
+
# Derive repo name from module path (e.g. "github.com/user/repo")
|
|
64
|
+
repo_name = ""
|
|
65
|
+
repo_match = re.search(r"github\.com/([^/\s]+/[^/\s]+)", name)
|
|
66
|
+
if repo_match:
|
|
67
|
+
repo_name = repo_match.group(1)
|
|
68
|
+
|
|
69
|
+
# Author from git config
|
|
70
|
+
author = ""
|
|
71
|
+
try:
|
|
72
|
+
author = run("git", ["config", "user.name"])
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
version = read_version(dir_path)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"name": short_name,
|
|
80
|
+
"modulePath": name,
|
|
81
|
+
"version": version,
|
|
82
|
+
"author": author,
|
|
83
|
+
"repoName": repo_name,
|
|
84
|
+
"binCommand": short_name,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_template_mappings():
|
|
89
|
+
"""Returns go-specific template mappings (template file -> target path)."""
|
|
90
|
+
return [
|
|
91
|
+
{"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
|
|
92
|
+
{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
|
|
93
|
+
{"template": "goreleaser.yml.tpl", "target": ".goreleaser.yml"},
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_shared_template_mappings():
|
|
98
|
+
"""Returns shared template mappings."""
|
|
99
|
+
return [
|
|
100
|
+
{"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
|
|
101
|
+
{"template": "gitignore.tpl", "target": ".gitignore"},
|
|
102
|
+
{"template": "LICENSE.tpl", "target": "LICENSE"},
|
|
103
|
+
{"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
|
|
104
|
+
{"template": "check-prs.sh.tpl", "target": "scripts/check-prs.sh"},
|
|
105
|
+
{"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
|
|
106
|
+
{"template": "record-gif.sh.tpl", "target": "scripts/record-gif.sh"},
|
|
107
|
+
{"template": "pre-release.sh.tpl", "target": "scripts/pre-release.sh"},
|
|
108
|
+
{"template": "pre-push-hook.sh.tpl", "target": "scripts/pre-push-hook.sh"},
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_project_exists(dir_path):
|
|
113
|
+
"""Returns True if a go.mod exists in the given directory."""
|
|
114
|
+
return os.path.exists(os.path.join(dir_path, "go.mod"))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_project_init_hint():
|
|
118
|
+
"""Hint for users who haven't initialized their project yet."""
|
|
119
|
+
return 'Run "go mod init <module-path>" first'
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
go-version: ["1.22", "1.23", "1.24"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
- uses: actions/setup-go@v5
|
|
18
|
+
with:
|
|
19
|
+
go-version: ${{ matrix.go-version }}
|
|
20
|
+
- run: go test ./...
|
|
21
|
+
- run: go vet ./...
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
goreleaser:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
- uses: actions/setup-go@v5
|
|
18
|
+
with:
|
|
19
|
+
go-version-file: go.mod
|
|
20
|
+
- uses: goreleaser/goreleaser-action@v6
|
|
21
|
+
with:
|
|
22
|
+
version: latest
|
|
23
|
+
args: release --clean
|
|
24
|
+
env:
|
|
25
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|