rlsbl 0.3.0 → 0.4.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 +7 -5
- 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 +67 -36
- package/rlsbl/commands/status.py +1 -1
- package/rlsbl/registries/__init__.py +2 -2
- package/rlsbl/registries/go.py +124 -0
- package/rlsbl/utils.py +3 -4
- package/templates/go/VERSION.tpl +1 -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/templates/shared/pre-push-hook.sh.tpl +2 -0
- package/templates/shared/pre-release.sh.tpl +17 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# rlsbl
|
|
2
2
|
|
|
3
|
-
Release orchestration and project scaffolding CLI for npm and
|
|
3
|
+
Release orchestration and project scaffolding CLI for npm, PyPI, and Go.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ rlsbl release minor
|
|
|
25
25
|
|
|
26
26
|
## Commands
|
|
27
27
|
|
|
28
|
-
All commands work at the top level -- registries are auto-detected from project files (`package.json`, `pyproject.toml`). Use `--registry <npm|pypi>` when you need to target a specific registry.
|
|
28
|
+
All commands work at the top level -- registries are auto-detected from project files (`package.json`, `pyproject.toml`, `go.mod`). Use `--registry <npm|pypi|go>` when you need to target a specific registry.
|
|
29
29
|
|
|
30
30
|
### scaffold [--force] [--update]
|
|
31
31
|
|
|
@@ -35,6 +35,7 @@ Scaffolds CI/CD infrastructure and release tooling for all detected registries.
|
|
|
35
35
|
rlsbl scaffold
|
|
36
36
|
rlsbl scaffold --registry npm # target npm only
|
|
37
37
|
rlsbl scaffold --registry pypi --force # overwrite existing files
|
|
38
|
+
rlsbl scaffold --registry go # target Go only
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
Context-aware behavior when files already exist (without `--force`):
|
|
@@ -55,7 +56,7 @@ rlsbl release minor
|
|
|
55
56
|
rlsbl release major --dry-run --registry npm
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
The version is synced across all detected project files (`package.json`, `pyproject.toml`) regardless of which registry is primary.
|
|
59
|
+
The version is synced across all detected project files (`package.json`, `pyproject.toml`, `VERSION`) regardless of which registry is primary. Go projects use a plain `VERSION` file as the version source.
|
|
59
60
|
|
|
60
61
|
If `scripts/pre-release.sh` exists, it runs before any changes are made. A non-zero exit aborts the release.
|
|
61
62
|
|
|
@@ -122,7 +123,7 @@ The scaffolded `scripts/pre-push-hook.sh` is installed as a git pre-push hook du
|
|
|
122
123
|
|
|
123
124
|
How it works:
|
|
124
125
|
|
|
125
|
-
1. Detects project type (`package.json
|
|
126
|
+
1. Detects project type (`package.json`, `pyproject.toml`, or `VERSION`)
|
|
126
127
|
2. Extracts the current version
|
|
127
128
|
3. Checks that `CHANGELOG.md` contains a heading `## <version>`
|
|
128
129
|
4. Blocks the push with an error if the entry is missing
|
|
@@ -141,8 +142,9 @@ The first version must be published manually before CI can take over:
|
|
|
141
142
|
|---|---|---|
|
|
142
143
|
| npm | Add an `NPM_TOKEN` secret to your GitHub repo (Settings > Secrets > Actions), then push a release | CI handles subsequent publishes |
|
|
143
144
|
| PyPI | Run `uv publish` | Set up [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) on pypi.org |
|
|
145
|
+
| Go | Push to GitHub and create a release -- Go modules are published by the tag itself | No secrets needed; `pkg.go.dev` indexes automatically |
|
|
144
146
|
|
|
145
|
-
After configuration, all subsequent releases are handled by CI when `rlsbl release` creates a GitHub Release.
|
|
147
|
+
After configuration, all subsequent releases are handled by CI when `rlsbl release` creates a GitHub Release. Go projects use GoReleaser in CI (via GitHub Actions) to build cross-platform binaries.
|
|
146
148
|
|
|
147
149
|
## Requirements
|
|
148
150
|
|
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
|
+
"GoReleaser runs in CI via GitHub Actions (no local install needed)",
|
|
65
|
+
"Push to GitHub to activate the CI workflow",
|
|
66
|
+
"Run rlsbl release [patch|minor|major]",
|
|
67
|
+
],
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
|
|
@@ -55,19 +55,30 @@ def run_cmd(registry, args, flags):
|
|
|
55
55
|
current_version = reg.read_version(".")
|
|
56
56
|
log(f"Current version: {current_version}")
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
# If the current version has never been tagged, release it as-is (bootstrap)
|
|
59
|
+
current_tag = f"v{current_version}"
|
|
60
|
+
current_tag_exists = len(run("git", ["tag", "-l", current_tag])) > 0
|
|
61
|
+
|
|
62
|
+
if not current_tag_exists:
|
|
63
|
+
new_version = current_version
|
|
64
|
+
bump_type = None
|
|
65
|
+
tag = current_tag
|
|
66
|
+
if args:
|
|
67
|
+
log(f"First release: releasing {new_version} as-is (bump type ignored)")
|
|
68
|
+
else:
|
|
69
|
+
log(f"First release: {new_version}")
|
|
70
|
+
else:
|
|
71
|
+
bump_type = args[0] if args else "patch"
|
|
72
|
+
if bump_type not in VALID_BUMP_TYPES:
|
|
73
|
+
print(
|
|
74
|
+
f'Error: invalid bump type "{bump_type}". Use: {", ".join(VALID_BUMP_TYPES)}',
|
|
75
|
+
file=sys.stderr,
|
|
76
|
+
)
|
|
77
|
+
sys.exit(1)
|
|
66
78
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
log(f"New version: {new_version} ({bump_type})")
|
|
79
|
+
new_version = bump_version(current_version, bump_type)
|
|
80
|
+
tag = f"v{new_version}"
|
|
81
|
+
log(f"New version: {new_version} ({bump_type})")
|
|
71
82
|
|
|
72
83
|
# Check tag doesn't already exist
|
|
73
84
|
tag_output = run("git", ["tag", "-l", tag])
|
|
@@ -111,7 +122,10 @@ def run_cmd(registry, args, flags):
|
|
|
111
122
|
if flags.get("dry-run", False):
|
|
112
123
|
log("\n--- Dry run summary ---")
|
|
113
124
|
log(f"Registry: {registry}")
|
|
114
|
-
|
|
125
|
+
if bump_type:
|
|
126
|
+
log(f"Bump: {current_version} -> {new_version} ({bump_type})")
|
|
127
|
+
else:
|
|
128
|
+
log(f"Version: {new_version} (first release)")
|
|
115
129
|
log(f"Tag: {tag}")
|
|
116
130
|
log(f"Branch: {branch}")
|
|
117
131
|
# Show other version files that would be synced
|
|
@@ -120,7 +134,9 @@ def run_cmd(registry, args, flags):
|
|
|
120
134
|
if name == registry:
|
|
121
135
|
continue
|
|
122
136
|
if other_reg.check_project_exists("."):
|
|
123
|
-
|
|
137
|
+
other_file = other_reg.get_version_file()
|
|
138
|
+
if other_file:
|
|
139
|
+
other_files.append(other_file)
|
|
124
140
|
if other_files:
|
|
125
141
|
log(f"Sync to: {', '.join(other_files)}")
|
|
126
142
|
log(f"Changelog:\n{changelog_entry}")
|
|
@@ -129,18 +145,26 @@ def run_cmd(registry, args, flags):
|
|
|
129
145
|
|
|
130
146
|
# Pre-compute which files will be modified
|
|
131
147
|
version_file = reg.get_version_file()
|
|
132
|
-
files_to_commit = [
|
|
148
|
+
files_to_commit = []
|
|
149
|
+
if version_file:
|
|
150
|
+
files_to_commit.append(version_file)
|
|
133
151
|
for name, other_reg in REGISTRIES.items():
|
|
134
152
|
if name == registry:
|
|
135
153
|
continue
|
|
136
154
|
if other_reg.check_project_exists("."):
|
|
137
|
-
|
|
155
|
+
other_file = other_reg.get_version_file()
|
|
156
|
+
if other_file:
|
|
157
|
+
files_to_commit.append(other_file)
|
|
138
158
|
|
|
139
159
|
# Confirmation prompt (skip with --yes)
|
|
140
160
|
if not flags.get("yes"):
|
|
141
|
-
|
|
161
|
+
bump_label = f" ({bump_type})" if bump_type else ""
|
|
162
|
+
print(f"\nAbout to release {new_version}{bump_label} on {branch}")
|
|
142
163
|
print(f" Tag: {tag}")
|
|
143
|
-
|
|
164
|
+
if files_to_commit:
|
|
165
|
+
print(f" Files: {', '.join(files_to_commit)}")
|
|
166
|
+
else:
|
|
167
|
+
print(" Files: (none -- version is the git tag)")
|
|
144
168
|
try:
|
|
145
169
|
answer = input("Proceed? [y/N] ").strip().lower()
|
|
146
170
|
except (EOFError, KeyboardInterrupt):
|
|
@@ -150,26 +174,33 @@ def run_cmd(registry, args, flags):
|
|
|
150
174
|
print("Aborted.")
|
|
151
175
|
sys.exit(0)
|
|
152
176
|
|
|
153
|
-
# Write new version to
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
# Write new version to version files (skip if version didn't change, e.g. first release)
|
|
178
|
+
if new_version != current_version:
|
|
179
|
+
if version_file:
|
|
180
|
+
reg.write_version(".", new_version)
|
|
181
|
+
log(f"Updated version in {version_file}")
|
|
156
182
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
|
|
183
|
+
# Sync version to all other recognized version files
|
|
184
|
+
for name, other_reg in REGISTRIES.items():
|
|
185
|
+
if name == registry:
|
|
186
|
+
continue
|
|
187
|
+
if other_reg.check_project_exists("."):
|
|
188
|
+
other_file = other_reg.get_version_file()
|
|
189
|
+
if other_file:
|
|
190
|
+
other_reg.write_version(".", new_version)
|
|
191
|
+
log(f"Synced version to {other_file}")
|
|
192
|
+
|
|
193
|
+
# Commit version file changes (skip if version didn't change, e.g. first release)
|
|
194
|
+
if files_to_commit and new_version != current_version:
|
|
195
|
+
commit_tool = find_commit_tool()
|
|
196
|
+
if commit_tool == "safegit":
|
|
197
|
+
run(commit_tool, ["commit", "-m", tag, "--", *files_to_commit])
|
|
198
|
+
else:
|
|
199
|
+
run("git", ["add", *files_to_commit])
|
|
200
|
+
run("git", ["commit", "-m", tag])
|
|
201
|
+
log(f"Committed: {tag}")
|
|
169
202
|
else:
|
|
170
|
-
|
|
171
|
-
run("git", ["commit", "-m", tag])
|
|
172
|
-
log(f"Committed: {tag}")
|
|
203
|
+
log("No version bump to commit")
|
|
173
204
|
|
|
174
205
|
# Create local git tag
|
|
175
206
|
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,124 @@
|
|
|
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
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_template_mappings():
|
|
93
|
+
"""Returns go-specific template mappings (template file -> target path)."""
|
|
94
|
+
return [
|
|
95
|
+
{"template": "VERSION.tpl", "target": "VERSION"},
|
|
96
|
+
{"template": "ci.yml.tpl", "target": ".github/workflows/ci.yml"},
|
|
97
|
+
{"template": "publish.yml.tpl", "target": ".github/workflows/publish.yml"},
|
|
98
|
+
{"template": "goreleaser.yml.tpl", "target": ".goreleaser.yml"},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_shared_template_mappings():
|
|
103
|
+
"""Returns shared template mappings."""
|
|
104
|
+
return [
|
|
105
|
+
{"template": "CHANGELOG.md.tpl", "target": "CHANGELOG.md"},
|
|
106
|
+
{"template": "gitignore.tpl", "target": ".gitignore"},
|
|
107
|
+
{"template": "LICENSE.tpl", "target": "LICENSE"},
|
|
108
|
+
{"template": "CLAUDE.md.tpl", "target": "CLAUDE.md"},
|
|
109
|
+
{"template": "check-prs.sh.tpl", "target": "scripts/check-prs.sh"},
|
|
110
|
+
{"template": "claude-settings.json.tpl", "target": ".claude/settings.json"},
|
|
111
|
+
{"template": "record-gif.sh.tpl", "target": "scripts/record-gif.sh"},
|
|
112
|
+
{"template": "pre-release.sh.tpl", "target": "scripts/pre-release.sh"},
|
|
113
|
+
{"template": "pre-push-hook.sh.tpl", "target": "scripts/pre-push-hook.sh"},
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_project_exists(dir_path):
|
|
118
|
+
"""Returns True if a go.mod exists in the given directory."""
|
|
119
|
+
return os.path.exists(os.path.join(dir_path, "go.mod"))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_project_init_hint():
|
|
123
|
+
"""Hint for users who haven't initialized their project yet."""
|
|
124
|
+
return 'Run "go mod init <module-path>" first'
|
package/rlsbl/utils.py
CHANGED
|
@@ -93,11 +93,10 @@ def check_gh_auth():
|
|
|
93
93
|
def find_commit_tool():
|
|
94
94
|
"""Detect safegit or fall back to git for committing.
|
|
95
95
|
|
|
96
|
-
Returns
|
|
96
|
+
Returns "safegit" if available on PATH, otherwise "git".
|
|
97
97
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return safegit_path
|
|
98
|
+
if shutil.which("safegit"):
|
|
99
|
+
return "safegit"
|
|
101
100
|
return "git"
|
|
102
101
|
|
|
103
102
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{version}}
|
|
@@ -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 }}
|
|
@@ -9,6 +9,8 @@ if [ -f package.json ]; then
|
|
|
9
9
|
VERSION=$(node -e "console.log(require('./package.json').version)" 2>/dev/null) || exit 0
|
|
10
10
|
elif [ -f pyproject.toml ]; then
|
|
11
11
|
VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/.*"\(.*\)".*/\1/') || exit 0
|
|
12
|
+
elif [ -f VERSION ]; then
|
|
13
|
+
VERSION=$(tr -d '[:space:]' < VERSION) || exit 0
|
|
12
14
|
else
|
|
13
15
|
exit 0
|
|
14
16
|
fi
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Pre-release validation hook.
|
|
3
3
|
# Runs before rlsbl creates a release. Exit non-zero to abort.
|
|
4
|
-
#
|
|
4
|
+
# Detects project type and runs appropriate checks automatically.
|
|
5
5
|
|
|
6
6
|
set -euo pipefail
|
|
7
7
|
|
|
8
8
|
echo "Running pre-release checks..."
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if [ -f go.mod ]; then
|
|
11
|
+
echo "Detected Go project"
|
|
12
|
+
go vet ./...
|
|
13
|
+
go build ./...
|
|
14
|
+
go test ./... -race -short -count=1
|
|
15
|
+
elif [ -f package.json ]; then
|
|
16
|
+
echo "Detected npm project"
|
|
17
|
+
npm test
|
|
18
|
+
elif [ -f pyproject.toml ]; then
|
|
19
|
+
echo "Detected Python project"
|
|
20
|
+
if command -v uv &>/dev/null; then
|
|
21
|
+
uv run pytest
|
|
22
|
+
elif command -v pytest &>/dev/null; then
|
|
23
|
+
pytest
|
|
24
|
+
fi
|
|
25
|
+
fi
|
|
12
26
|
|
|
13
27
|
echo "Pre-release checks passed."
|