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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlsbl",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Release orchestration and project scaffolding for npm and PyPI",
5
5
  "license": "MIT",
6
6
  "bin": {
package/rlsbl/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- """rlsbl: Release orchestration and project scaffolding for npm and PyPI."""
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 PyPI
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
- for i, r in enumerate(["npm", "pypi"]):
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 or pyproject.toml found.", file=sys.stderr)
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 or pyproject.toml found.", file=sys.stderr)
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 or pyproject.toml found.", file=sys.stderr)
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)
@@ -1,4 +1,4 @@
1
- """Check command: check package name availability on npm or PyPI."""
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 PyPI, and warns about similar names.
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
- else:
217
+ elif registry == "pypi":
184
218
  _check_name_pypi(name)
219
+ elif registry == "go":
220
+ _check_name_go(name)
@@ -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
- print(f" {name}: {reg.get_version_file()} (v{version})")
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
- other_files.append(other_reg.get_version_file())
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 = [version_file]
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
- files_to_commit.append(other_reg.get_version_file())
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
- print(f" Files: {', '.join(files_to_commit)}")
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
- reg.write_version(".", new_version)
155
- log(f"Updated version in {version_file}")
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.write_version(".", new_version)
163
- log(f"Synced version to {other_reg.get_version_file()}")
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
- commit_tool = find_commit_tool()
167
- if commit_tool == "safegit":
168
- run(commit_tool, ["commit", "-m", tag, "--", *files_to_commit])
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
- run("git", ["add", *files_to_commit])
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])
@@ -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
@@ -1,5 +1,5 @@
1
1
  """Registry lookup for rlsbl."""
2
2
 
3
- from . import npm, pypi
3
+ from . import go, npm, pypi
4
4
 
5
- REGISTRIES = {"npm": npm, "pypi": pypi}
5
+ REGISTRIES = {"npm": npm, "pypi": pypi, "go": go}
@@ -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 }}