rlsbl 0.5.0 → 0.5.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 CHANGED
@@ -62,7 +62,7 @@ rlsbl release major --dry-run --registry npm
62
62
 
63
63
  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.
64
64
 
65
- If `scripts/pre-release.sh` exists, it runs before any changes are made. A non-zero exit aborts the release.
65
+ If `scripts/pre-release.sh` exists, it runs before any changes are made. A non-zero exit aborts the release. If `scripts/post-release.sh` exists, it runs after the release completes (non-fatal). See [Release flow](#release-flow) for details.
66
66
 
67
67
  ### status
68
68
 
@@ -127,6 +127,8 @@ When you run `release`, the following happens in order:
127
127
  10. Pushes the branch to `origin`
128
128
  11. Creates a GitHub Release tagged `v<new-version>` with the changelog entry as notes
129
129
  12. The GitHub Release triggers `publish.yml`, which publishes to the registry
130
+ 13. Runs `scripts/post-release.sh` if present (non-fatal -- the release is already complete). The `RLSBL_VERSION` env var is set to the released version. Useful for local install (`go install ./cmd/myapp/`), deploy, or notifications.
131
+ 14. Spawns a background process that watches CI via `gh run watch`. When CI finishes, it prints the result to stderr (so AI agents can read it) and sends a desktop notification (`notify-send` on Linux, `osascript` on macOS). On CI failure, it also prints the GitHub Actions run URL. This happens automatically -- no configuration needed.
130
132
 
131
133
  ## What scaffold creates
132
134
 
@@ -141,11 +143,14 @@ When you run `release`, the following happens in order:
141
143
  | `.claude/settings.json` | Shared | Claude Code settings |
142
144
  | `scripts/check-prs.sh` | Shared | PR review helper |
143
145
  | `scripts/pre-release.sh` | Shared | Pre-release hook (runs before each release) |
146
+ | `scripts/post-release.sh` | Shared | Post-release hook (runs after each release, non-fatal) |
144
147
  | `scripts/record-gif.sh` | Shared | Terminal recording helper |
145
148
  | `scripts/pre-push-hook.sh` | Shared | Pre-push changelog enforcement |
146
149
 
147
150
  All `.sh` files in `scripts/` are made executable automatically. The pre-push hook is installed into `.git/hooks/pre-push` during scaffold.
148
151
 
152
+ The scaffolded `.gitignore` includes a `*.local-only` pattern. Create a `.local-only/` directory or rename files with a `.local-only` suffix to keep them out of version control -- useful for local-only assets, experiments, and keeping the working tree clean for tools that check `git status`.
153
+
149
154
  ## Pre-push hook
150
155
 
151
156
  The scaffolded `scripts/pre-push-hook.sh` is installed as a git pre-push hook during `scaffold`. It prevents pushing when `CHANGELOG.md` lacks an entry for the current version.
@@ -180,6 +185,7 @@ After configuration, all subsequent releases are handled by CI when `rlsbl relea
180
185
  | Variable | Default | Description |
181
186
  |----------|---------|-------------|
182
187
  | `RLSBL_PUSH_TIMEOUT` | `120` | Timeout in seconds for `git push` operations. Increase if your pre-push hooks (e.g. test suites) take longer than 2 minutes. |
188
+ | `RLSBL_VERSION` | -- | Set automatically when running `scripts/post-release.sh`. Contains the just-released version string. |
183
189
 
184
190
  ## Requirements
185
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlsbl",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Release orchestration and project scaffolding for npm and PyPI",
5
5
  "license": "MIT",
6
6
  "bin": {
package/rlsbl/utils.py CHANGED
@@ -109,27 +109,42 @@ def find_commit_tool():
109
109
 
110
110
 
111
111
  def spawn_ci_watcher(commit_sha, tag):
112
- """Spawn a detached background process that watches CI and sends a desktop notification."""
112
+ """Spawn a detached background process that watches CI and prints results to stderr.
113
+
114
+ The spawned process inherits the parent's stderr so output appears in the
115
+ same terminal/stream -- important for AI agents that read stderr.
116
+ Desktop notifications are sent as a secondary channel when available.
117
+ """
118
+ repo_slug = ""
119
+ try:
120
+ repo_slug = run("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"])
121
+ except Exception:
122
+ pass
123
+
113
124
  repo_name = ""
114
125
  try:
115
126
  repo_name = run("gh", ["repo", "view", "--json", "name", "-q", ".name"])
116
127
  except Exception:
117
128
  pass
118
129
 
119
- notify_cmd = _notify_command()
120
- if not notify_cmd:
121
- return
122
-
123
130
  label = f"{repo_name} {tag}" if repo_name else tag
124
131
 
132
+ # Build the notification snippet based on what's available on this platform
133
+ notify_snippet = _notify_snippet()
134
+
125
135
  script = f"""
126
136
  import subprocess, sys, time
127
137
 
138
+ commit_sha = {commit_sha!r}
139
+ label = {label!r}
140
+ repo_slug = {repo_slug!r}
141
+
142
+ # Find the CI run by commit SHA (retry up to 30s)
128
143
  run_id = None
129
144
  for _ in range(15):
130
145
  try:
131
146
  r = subprocess.run(
132
- ["gh", "run", "list", "--commit", "{commit_sha}", "--limit", "1",
147
+ ["gh", "run", "list", "--commit", commit_sha, "--limit", "1",
133
148
  "--json", "databaseId", "-q", ".[0].databaseId"],
134
149
  capture_output=True, text=True, timeout=10)
135
150
  if r.returncode == 0 and r.stdout.strip():
@@ -142,43 +157,61 @@ for _ in range(15):
142
157
  if not run_id:
143
158
  sys.exit(0)
144
159
 
160
+ # Watch the run until it completes
145
161
  result = subprocess.run(
146
162
  ["gh", "run", "watch", run_id, "--exit-status"],
147
163
  capture_output=True, text=True, timeout=3600)
148
164
 
165
+ ok = result.returncode == 0
166
+
167
+ # Extract last non-empty line as summary for desktop notification
149
168
  summary = ""
150
169
  for line in reversed(result.stdout.strip().splitlines()):
151
170
  if line.strip():
152
171
  summary = line.strip()
153
172
  break
154
173
 
155
- ok = result.returncode == 0
156
- title = "{label}: CI passed" if ok else "{label}: CI FAILED"
157
- {notify_cmd}
174
+ # Print result to stderr so AI agents and terminal users can see it
175
+ if ok:
176
+ print(f"rlsbl: {{label}}: CI passed", file=sys.stderr)
177
+ else:
178
+ print(f"rlsbl: {{label}}: CI FAILED", file=sys.stderr)
179
+ if repo_slug and run_id:
180
+ print(f"rlsbl: https://github.com/{{repo_slug}}/actions/runs/{{run_id}}", file=sys.stderr)
181
+
182
+ # Desktop notification (optional, non-fatal)
183
+ title = f"{{label}}: CI passed" if ok else f"{{label}}: CI FAILED"
184
+ try:
185
+ {notify_snippet}
186
+ except Exception:
187
+ pass
158
188
  """
159
189
  subprocess.Popen(
160
190
  [sys.executable, "-c", script],
161
191
  start_new_session=True,
162
192
  stdin=subprocess.DEVNULL,
163
- stdout=subprocess.DEVNULL,
164
- stderr=subprocess.DEVNULL,
165
193
  )
166
194
 
167
195
 
168
- def _notify_command():
169
- """Return a Python code snippet for sending a desktop notification, or None."""
196
+ def _notify_snippet():
197
+ """Return an indented Python code snippet for sending a desktop notification.
198
+
199
+ Returns a pass statement if no notification tool is available.
200
+ The snippet is intended to be embedded inside a try/except block.
201
+ """
202
+ indent = " "
170
203
  if sys.platform == "darwin":
171
204
  return (
172
- 'subprocess.run(["osascript", "-e",'
173
- ' f\'display notification "{summary}" with title "{title}"\'],'
174
- ' timeout=5)'
205
+ f'{indent}subprocess.run(["osascript", "-e",\n'
206
+ f'{indent} f\'display notification "{{summary}}" with title "{{title}}"\'],\n'
207
+ f'{indent} timeout=5)'
175
208
  )
176
209
  if shutil.which("notify-send"):
177
210
  return (
178
- 'urgency = "normal" if ok else "critical"\n'
179
- 'subprocess.run(["notify-send", "-u", urgency, title, summary], timeout=5)'
211
+ f'{indent}urgency = "normal" if ok else "critical"\n'
212
+ f'{indent}subprocess.run(["notify-send", "-u", urgency, title, summary], timeout=5)'
180
213
  )
181
- return None
214
+ return f"{indent}pass"
182
215
 
183
216
 
184
217
  def bump_version(version, bump_type):