innoday-blastoff 0.2.0__tar.gz
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.
- innoday_blastoff-0.2.0/LICENSE +21 -0
- innoday_blastoff-0.2.0/PKG-INFO +281 -0
- innoday_blastoff-0.2.0/README.md +248 -0
- innoday_blastoff-0.2.0/blastoff/__init__.py +2 -0
- innoday_blastoff-0.2.0/blastoff/api/__init__.py +0 -0
- innoday_blastoff-0.2.0/blastoff/api/changelog.py +75 -0
- innoday_blastoff-0.2.0/blastoff/api/claude_api.py +246 -0
- innoday_blastoff-0.2.0/blastoff/api/github_api.py +442 -0
- innoday_blastoff-0.2.0/blastoff/changelog_writer.py +66 -0
- innoday_blastoff-0.2.0/blastoff/config_loader.py +321 -0
- innoday_blastoff-0.2.0/blastoff/hotfix.py +257 -0
- innoday_blastoff-0.2.0/blastoff/main.py +23 -0
- innoday_blastoff-0.2.0/blastoff/release.py +353 -0
- innoday_blastoff-0.2.0/blastoff/stores/__init__.py +13 -0
- innoday_blastoff-0.2.0/blastoff/stores/base.py +59 -0
- innoday_blastoff-0.2.0/blastoff/stores/file_store.py +73 -0
- innoday_blastoff-0.2.0/blastoff/summarize.py +305 -0
- innoday_blastoff-0.2.0/blastoff/version.py +138 -0
- innoday_blastoff-0.2.0/blastoff/version_manager.py +514 -0
- innoday_blastoff-0.2.0/innoday_blastoff.egg-info/PKG-INFO +281 -0
- innoday_blastoff-0.2.0/innoday_blastoff.egg-info/SOURCES.txt +39 -0
- innoday_blastoff-0.2.0/innoday_blastoff.egg-info/dependency_links.txt +1 -0
- innoday_blastoff-0.2.0/innoday_blastoff.egg-info/entry_points.txt +2 -0
- innoday_blastoff-0.2.0/innoday_blastoff.egg-info/requires.txt +6 -0
- innoday_blastoff-0.2.0/innoday_blastoff.egg-info/top_level.txt +1 -0
- innoday_blastoff-0.2.0/pyproject.toml +71 -0
- innoday_blastoff-0.2.0/setup.cfg +4 -0
- innoday_blastoff-0.2.0/tests/test_changelog_writer.py +81 -0
- innoday_blastoff-0.2.0/tests/test_claude_cli_generation.py +197 -0
- innoday_blastoff-0.2.0/tests/test_debug.py +58 -0
- innoday_blastoff-0.2.0/tests/test_exception_handling.py +75 -0
- innoday_blastoff-0.2.0/tests/test_github_api_merged_prs.py +170 -0
- innoday_blastoff-0.2.0/tests/test_github_api_release_pagination.py +95 -0
- innoday_blastoff-0.2.0/tests/test_hotfix_flow_store.py +124 -0
- innoday_blastoff-0.2.0/tests/test_package_metadata.py +77 -0
- innoday_blastoff-0.2.0/tests/test_regex.py +22 -0
- innoday_blastoff-0.2.0/tests/test_release_flow_summary_changelog.py +217 -0
- innoday_blastoff-0.2.0/tests/test_summarize.py +41 -0
- innoday_blastoff-0.2.0/tests/test_utc_now_iso.py +47 -0
- innoday_blastoff-0.2.0/tests/test_version_manager_hotfix.py +136 -0
- innoday_blastoff-0.2.0/tests/test_version_store.py +118 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Haviland Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: innoday-blastoff
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Batch GitHub releases and hotfixes across repositories grouped by a topic, with pluggable version backends.
|
|
5
|
+
Author-email: Karl Haviland <me@karlhaviland.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/havilandsoftware/innoday-blastoff
|
|
8
|
+
Project-URL: Repository, https://github.com/havilandsoftware/innoday-blastoff
|
|
9
|
+
Project-URL: Issues, https://github.com/havilandsoftware/innoday-blastoff/issues
|
|
10
|
+
Keywords: release,deployment,github,cli,versioning,semver
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: python-dotenv
|
|
27
|
+
Requires-Dist: plumbum
|
|
28
|
+
Requires-Dist: requests
|
|
29
|
+
Requires-Dist: pytz
|
|
30
|
+
Requires-Dist: anthropic
|
|
31
|
+
Requires-Dist: pyyaml
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# innoday-blastoff
|
|
35
|
+
|
|
36
|
+
A small CLI to batch GitHub releases and hotfixes across multiple repositories grouped by a GitHub topic.
|
|
37
|
+
|
|
38
|
+
`blastoff` computes the next semantic version for a group of repos, tags every repository that carries a given GitHub topic, generates a per-repo changelog of merged pull requests, and (optionally) a Claude-authored release summary. Hotfixes bump the patch version, with two supported modes.
|
|
39
|
+
|
|
40
|
+
- **Install / distribution name:** `innoday-blastoff` (on PyPI)
|
|
41
|
+
- **Import / command name:** `blastoff`
|
|
42
|
+
|
|
43
|
+
## What it does
|
|
44
|
+
|
|
45
|
+
- **Batch releases by topic** — point it at a GitHub org and a topic; it finds every repo carrying that topic and creates the same release tag on each (skipping archived repos and repos that already have the tag).
|
|
46
|
+
- **Automatic version computation** — the next version is resolved from a local config by alias, so you don't hand-type tags. A release bumps the minor version; a hotfix bumps the patch.
|
|
47
|
+
- **Changelog generation** — collects merged PRs across all matched repos since the last release/hotfix and writes a structured per-repo changelog to `CHANGELOG.md`.
|
|
48
|
+
- **Optional Claude release summary** — when a Claude API key is available, an executive-facing narrative of the release is generated; otherwise blastoff falls back to PR titles.
|
|
49
|
+
- **Dry run by default** — every release/hotfix is a preview unless you add `--release`.
|
|
50
|
+
- **Pluggable version backend** — release state lives in a local file by default, but you can implement your own `VersionStore` to keep it anywhere (see [Extending](#extending--custom-version-backends)).
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
With `pip`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install innoday-blastoff
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
With [`uv`](https://docs.astral.sh/uv/):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv pip install innoday-blastoff
|
|
64
|
+
# or add it to a project
|
|
65
|
+
uv add innoday-blastoff
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The distribution is named `innoday-blastoff`, but the CLI and the import package are both `blastoff`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
blastoff --help # the CLI is on PATH after install
|
|
72
|
+
python -c "import blastoff" # import by package name
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Prerequisites
|
|
76
|
+
|
|
77
|
+
`blastoff` talks to the GitHub API and, optionally, to Claude. Configure it with environment variables (a `.env` file in the working directory is loaded automatically):
|
|
78
|
+
|
|
79
|
+
| Variable | Required | Purpose |
|
|
80
|
+
|----------|----------|---------|
|
|
81
|
+
| `GH_TOKEN` | yes | GitHub token used to list repos and create releases. Scopes: `repo`, `read:org`. |
|
|
82
|
+
| `GH_ORG` | yes* | GitHub organization to pull repositories from. |
|
|
83
|
+
| `TOPIC` | yes* | GitHub topic used to select which repos to release. |
|
|
84
|
+
| `CLAUDE_API_KEY` | no | Enables Claude-generated PR summaries (dry run) and the release narrative. Without it, blastoff falls back to PR titles / a plain summary. |
|
|
85
|
+
|
|
86
|
+
\* `GH_ORG` and `TOPIC` can also be supplied per-invocation via CLI flags, or resolved from config by alias (see below). When you resolve a release by alias (`-c <alias>`), the org and topic come from the config entry and you don't need the env vars.
|
|
87
|
+
|
|
88
|
+
## Usage
|
|
89
|
+
|
|
90
|
+
`blastoff` has four subcommands: `release`, `hotfix`, `summarize`, and `version`.
|
|
91
|
+
|
|
92
|
+
### Releases
|
|
93
|
+
|
|
94
|
+
The version and topic are resolved from config by alias. **Do not pass `-t/--tag` for a topic/alias release** — the tag is computed for you from the config's `next_version`. Passing `-t` is a one-off override that bypasses config resolution and never mutates stored version state.
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# Dry run — shows what would be released, writes nothing (safe to run anytime)
|
|
98
|
+
blastoff release -c <alias>
|
|
99
|
+
|
|
100
|
+
# Actually create the releases
|
|
101
|
+
blastoff release -c <alias> --release
|
|
102
|
+
|
|
103
|
+
# Release only a single repository
|
|
104
|
+
blastoff release -c <alias> --repo <repo-name> --release
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Useful flags:
|
|
108
|
+
|
|
109
|
+
- `--release` / `-r` — perform the release. **Omitting it is always a dry run.**
|
|
110
|
+
- `--repo <name>` — restrict to a single repository.
|
|
111
|
+
- `--summary "text"` — use this text verbatim as the release summary instead of auto-generating one.
|
|
112
|
+
- `--changelog-output <path>` — where to write the changelog (default: `CHANGELOG.md`).
|
|
113
|
+
- `-o/--org`, `-k/--token`, `-c/--topic` — override `GH_ORG`, `GH_TOKEN`, `TOPIC`.
|
|
114
|
+
|
|
115
|
+
On a real release, blastoff validates the token, creates the tag on each matching repo, writes the changelog, bumps the stored `next_version` (minor bump), and records the released version via the configured backend.
|
|
116
|
+
|
|
117
|
+
### Hotfixes
|
|
118
|
+
|
|
119
|
+
Hotfixes bump the patch version from the last released version. Two modes are supported per alias (set with the `hotfix_mode` field in config):
|
|
120
|
+
|
|
121
|
+
| Mode | Behavior | Example |
|
|
122
|
+
|------|----------|---------|
|
|
123
|
+
| `increment` (default) | Bumps the patch digit | `v1.3.0` → `v1.3.1` |
|
|
124
|
+
| `hotfix` | Appends a `-hotfix.N` suffix | `v1.3.0` → `v1.3.0-hotfix.1` |
|
|
125
|
+
|
|
126
|
+
blastoff scans existing GitHub releases in the topic to pick the next free hotfix number, so re-runs are safe.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Dry run
|
|
130
|
+
blastoff hotfix -c <alias>
|
|
131
|
+
|
|
132
|
+
# Create the hotfix releases
|
|
133
|
+
blastoff hotfix -c <alias> --release
|
|
134
|
+
|
|
135
|
+
# Hotfix a single repo
|
|
136
|
+
blastoff hotfix -c <alias> --repo <repo-name> --release
|
|
137
|
+
|
|
138
|
+
# Hotfix a single repo at a specific commit
|
|
139
|
+
blastoff hotfix -c <alias> --repo <repo-name> --commit <sha> --release
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A full release must have run first (the hotfix base is the config's `last_released_version`).
|
|
143
|
+
|
|
144
|
+
### Summaries
|
|
145
|
+
|
|
146
|
+
`summarize` produces a Markdown release document (written to `releases/` by default):
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
blastoff summarize # all configured aliases
|
|
150
|
+
blastoff summarize --alias <alias> # a single alias
|
|
151
|
+
blastoff summarize --output-dir <path> # custom output directory
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Version config
|
|
155
|
+
|
|
156
|
+
`version` inspects the resolved config:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
blastoff version print # show the version state for all configured aliases
|
|
160
|
+
blastoff version check # validate the config and report any errors
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
Version and release state is read from a local file. blastoff looks for two sources, in order, walking up from the current directory:
|
|
166
|
+
|
|
167
|
+
1. **`.innoday/project.yml`** — a project config file blastoff reads if present. Add a `release_configs` list to it (this is optional; it exists so blastoff can share a project's config file rather than requiring a separate one).
|
|
168
|
+
2. **`org-versions.json`** — the standalone fallback, holding local per-machine release state.
|
|
169
|
+
|
|
170
|
+
The resolution logic lives in `blastoff/config_loader.py`, and the default file backend is `blastoff/stores/file_store.py`.
|
|
171
|
+
|
|
172
|
+
### `org-versions.json`
|
|
173
|
+
|
|
174
|
+
Create it in your project root (or any parent directory), with one entry per alias:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"organizations": [
|
|
179
|
+
{
|
|
180
|
+
"alias": "my-app",
|
|
181
|
+
"organization": "my-github-org",
|
|
182
|
+
"label": "my-release-topic",
|
|
183
|
+
"next_version": "v1.2.0",
|
|
184
|
+
"prerelease": null,
|
|
185
|
+
"last_released": null,
|
|
186
|
+
"last_released_version": null,
|
|
187
|
+
"last_hotfix": null,
|
|
188
|
+
"hotfix_mode": "increment"
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Field reference:
|
|
195
|
+
|
|
196
|
+
| Field | Required | Meaning |
|
|
197
|
+
|-------|----------|---------|
|
|
198
|
+
| `alias` | yes | The name you pass to `-c` — how you refer to this release group. |
|
|
199
|
+
| `organization` | yes | GitHub organization the repos live in. |
|
|
200
|
+
| `label` | yes | GitHub topic that selects the repos to release. |
|
|
201
|
+
| `next_version` | yes | The version to release next (e.g. `v1.2.0`). Bumped automatically after a real release. |
|
|
202
|
+
| `prerelease` | no | Prerelease type: `alpha`, `beta`, or `rc`. |
|
|
203
|
+
| `last_released` | no | ISO-8601 timestamp of the last release (set automatically). |
|
|
204
|
+
| `last_released_version` | no | The last version released (set automatically; used as the hotfix base). |
|
|
205
|
+
| `last_hotfix` | no | The last hotfix tag created (set automatically). |
|
|
206
|
+
| `hotfix_mode` | no | `increment` (default) or `hotfix` — see the hotfix table above. |
|
|
207
|
+
|
|
208
|
+
After a successful release or hotfix, whichever source the config was loaded from is updated automatically with the new version state.
|
|
209
|
+
|
|
210
|
+
### `.innoday/project.yml` — `release_configs`
|
|
211
|
+
|
|
212
|
+
If you keep a `.innoday/project.yml`, blastoff will read a `release_configs` list from it (checked before `org-versions.json`), with the same fields per alias:
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
release_configs:
|
|
216
|
+
- alias: my-app
|
|
217
|
+
organization: my-github-org
|
|
218
|
+
label: my-release-topic
|
|
219
|
+
next_version: v1.2.0
|
|
220
|
+
prerelease: null
|
|
221
|
+
last_released: null
|
|
222
|
+
last_released_version: null
|
|
223
|
+
last_hotfix: null
|
|
224
|
+
hotfix_mode: increment
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Extending — custom version backends
|
|
228
|
+
|
|
229
|
+
Where release state lives is pluggable. blastoff computes versions and tags repos; *where* the version state is loaded from and *where* a completed release is recorded is abstracted behind the `VersionStore` interface (`blastoff.stores.VersionStore`). The shipped default is `FileVersionStore` (the file-backed backend above).
|
|
230
|
+
|
|
231
|
+
To store release state somewhere else — a database, an internal API, a service — implement the three abstract methods and inject your store into the release/hotfix commands:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from blastoff.stores import VersionStore
|
|
235
|
+
from blastoff.version_manager import OrgConfig
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class MyVersionStore(VersionStore):
|
|
239
|
+
def load_org_config(self, alias: str) -> OrgConfig:
|
|
240
|
+
"""Return the OrgConfig for `alias` (raise FileNotFoundError if none)."""
|
|
241
|
+
...
|
|
242
|
+
|
|
243
|
+
def save_org_config(self, org: OrgConfig) -> None:
|
|
244
|
+
"""Persist an updated OrgConfig back to your backend."""
|
|
245
|
+
...
|
|
246
|
+
|
|
247
|
+
def record_release(self, org, version, released_at=None, summary=None, changelog=None) -> None:
|
|
248
|
+
"""Record that `version` was released for `org` (called after tagging)."""
|
|
249
|
+
...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Both the `Release` and `Hotfix` commands expose a `version_store` attribute; set it before invoking to swap the backend. When left unset it defaults to `FileVersionStore`, giving you the standalone file-backed behavior described above. blastoff imports nothing from any consumer — the dependency arrow points one way: your code depends on blastoff and implements this interface.
|
|
253
|
+
|
|
254
|
+
## Testing
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
uv run pytest
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
The suite is hermetic — no network access required.
|
|
261
|
+
|
|
262
|
+
## Contributing
|
|
263
|
+
|
|
264
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the
|
|
265
|
+
lint conventions, and the behavioral contracts (dry-run-by-default, idempotent
|
|
266
|
+
re-runs, the `VersionStore` interface) that are easy to break by accident.
|
|
267
|
+
|
|
268
|
+
## Changelog
|
|
269
|
+
|
|
270
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
271
|
+
|
|
272
|
+
> **Renamed:** this package was published as `pixelfuel-blastoff` for 0.1.0. It is
|
|
273
|
+
> now **`innoday-blastoff`** (a separate PyPI project, since PyPI has no rename);
|
|
274
|
+
> the old distribution is yanked. The import package and CLI command are
|
|
275
|
+
> unchanged — both are still `blastoff`.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT — see [LICENSE](LICENSE).
|
|
280
|
+
|
|
281
|
+
To publish a new version to PyPI, see [PUBLISHING.md](PUBLISHING.md).
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# innoday-blastoff
|
|
2
|
+
|
|
3
|
+
A small CLI to batch GitHub releases and hotfixes across multiple repositories grouped by a GitHub topic.
|
|
4
|
+
|
|
5
|
+
`blastoff` computes the next semantic version for a group of repos, tags every repository that carries a given GitHub topic, generates a per-repo changelog of merged pull requests, and (optionally) a Claude-authored release summary. Hotfixes bump the patch version, with two supported modes.
|
|
6
|
+
|
|
7
|
+
- **Install / distribution name:** `innoday-blastoff` (on PyPI)
|
|
8
|
+
- **Import / command name:** `blastoff`
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
- **Batch releases by topic** — point it at a GitHub org and a topic; it finds every repo carrying that topic and creates the same release tag on each (skipping archived repos and repos that already have the tag).
|
|
13
|
+
- **Automatic version computation** — the next version is resolved from a local config by alias, so you don't hand-type tags. A release bumps the minor version; a hotfix bumps the patch.
|
|
14
|
+
- **Changelog generation** — collects merged PRs across all matched repos since the last release/hotfix and writes a structured per-repo changelog to `CHANGELOG.md`.
|
|
15
|
+
- **Optional Claude release summary** — when a Claude API key is available, an executive-facing narrative of the release is generated; otherwise blastoff falls back to PR titles.
|
|
16
|
+
- **Dry run by default** — every release/hotfix is a preview unless you add `--release`.
|
|
17
|
+
- **Pluggable version backend** — release state lives in a local file by default, but you can implement your own `VersionStore` to keep it anywhere (see [Extending](#extending--custom-version-backends)).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
With `pip`:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install innoday-blastoff
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With [`uv`](https://docs.astral.sh/uv/):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv pip install innoday-blastoff
|
|
31
|
+
# or add it to a project
|
|
32
|
+
uv add innoday-blastoff
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The distribution is named `innoday-blastoff`, but the CLI and the import package are both `blastoff`:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
blastoff --help # the CLI is on PATH after install
|
|
39
|
+
python -c "import blastoff" # import by package name
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Prerequisites
|
|
43
|
+
|
|
44
|
+
`blastoff` talks to the GitHub API and, optionally, to Claude. Configure it with environment variables (a `.env` file in the working directory is loaded automatically):
|
|
45
|
+
|
|
46
|
+
| Variable | Required | Purpose |
|
|
47
|
+
|----------|----------|---------|
|
|
48
|
+
| `GH_TOKEN` | yes | GitHub token used to list repos and create releases. Scopes: `repo`, `read:org`. |
|
|
49
|
+
| `GH_ORG` | yes* | GitHub organization to pull repositories from. |
|
|
50
|
+
| `TOPIC` | yes* | GitHub topic used to select which repos to release. |
|
|
51
|
+
| `CLAUDE_API_KEY` | no | Enables Claude-generated PR summaries (dry run) and the release narrative. Without it, blastoff falls back to PR titles / a plain summary. |
|
|
52
|
+
|
|
53
|
+
\* `GH_ORG` and `TOPIC` can also be supplied per-invocation via CLI flags, or resolved from config by alias (see below). When you resolve a release by alias (`-c <alias>`), the org and topic come from the config entry and you don't need the env vars.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
`blastoff` has four subcommands: `release`, `hotfix`, `summarize`, and `version`.
|
|
58
|
+
|
|
59
|
+
### Releases
|
|
60
|
+
|
|
61
|
+
The version and topic are resolved from config by alias. **Do not pass `-t/--tag` for a topic/alias release** — the tag is computed for you from the config's `next_version`. Passing `-t` is a one-off override that bypasses config resolution and never mutates stored version state.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Dry run — shows what would be released, writes nothing (safe to run anytime)
|
|
65
|
+
blastoff release -c <alias>
|
|
66
|
+
|
|
67
|
+
# Actually create the releases
|
|
68
|
+
blastoff release -c <alias> --release
|
|
69
|
+
|
|
70
|
+
# Release only a single repository
|
|
71
|
+
blastoff release -c <alias> --repo <repo-name> --release
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Useful flags:
|
|
75
|
+
|
|
76
|
+
- `--release` / `-r` — perform the release. **Omitting it is always a dry run.**
|
|
77
|
+
- `--repo <name>` — restrict to a single repository.
|
|
78
|
+
- `--summary "text"` — use this text verbatim as the release summary instead of auto-generating one.
|
|
79
|
+
- `--changelog-output <path>` — where to write the changelog (default: `CHANGELOG.md`).
|
|
80
|
+
- `-o/--org`, `-k/--token`, `-c/--topic` — override `GH_ORG`, `GH_TOKEN`, `TOPIC`.
|
|
81
|
+
|
|
82
|
+
On a real release, blastoff validates the token, creates the tag on each matching repo, writes the changelog, bumps the stored `next_version` (minor bump), and records the released version via the configured backend.
|
|
83
|
+
|
|
84
|
+
### Hotfixes
|
|
85
|
+
|
|
86
|
+
Hotfixes bump the patch version from the last released version. Two modes are supported per alias (set with the `hotfix_mode` field in config):
|
|
87
|
+
|
|
88
|
+
| Mode | Behavior | Example |
|
|
89
|
+
|------|----------|---------|
|
|
90
|
+
| `increment` (default) | Bumps the patch digit | `v1.3.0` → `v1.3.1` |
|
|
91
|
+
| `hotfix` | Appends a `-hotfix.N` suffix | `v1.3.0` → `v1.3.0-hotfix.1` |
|
|
92
|
+
|
|
93
|
+
blastoff scans existing GitHub releases in the topic to pick the next free hotfix number, so re-runs are safe.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Dry run
|
|
97
|
+
blastoff hotfix -c <alias>
|
|
98
|
+
|
|
99
|
+
# Create the hotfix releases
|
|
100
|
+
blastoff hotfix -c <alias> --release
|
|
101
|
+
|
|
102
|
+
# Hotfix a single repo
|
|
103
|
+
blastoff hotfix -c <alias> --repo <repo-name> --release
|
|
104
|
+
|
|
105
|
+
# Hotfix a single repo at a specific commit
|
|
106
|
+
blastoff hotfix -c <alias> --repo <repo-name> --commit <sha> --release
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A full release must have run first (the hotfix base is the config's `last_released_version`).
|
|
110
|
+
|
|
111
|
+
### Summaries
|
|
112
|
+
|
|
113
|
+
`summarize` produces a Markdown release document (written to `releases/` by default):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
blastoff summarize # all configured aliases
|
|
117
|
+
blastoff summarize --alias <alias> # a single alias
|
|
118
|
+
blastoff summarize --output-dir <path> # custom output directory
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Version config
|
|
122
|
+
|
|
123
|
+
`version` inspects the resolved config:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
blastoff version print # show the version state for all configured aliases
|
|
127
|
+
blastoff version check # validate the config and report any errors
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Configuration
|
|
131
|
+
|
|
132
|
+
Version and release state is read from a local file. blastoff looks for two sources, in order, walking up from the current directory:
|
|
133
|
+
|
|
134
|
+
1. **`.innoday/project.yml`** — a project config file blastoff reads if present. Add a `release_configs` list to it (this is optional; it exists so blastoff can share a project's config file rather than requiring a separate one).
|
|
135
|
+
2. **`org-versions.json`** — the standalone fallback, holding local per-machine release state.
|
|
136
|
+
|
|
137
|
+
The resolution logic lives in `blastoff/config_loader.py`, and the default file backend is `blastoff/stores/file_store.py`.
|
|
138
|
+
|
|
139
|
+
### `org-versions.json`
|
|
140
|
+
|
|
141
|
+
Create it in your project root (or any parent directory), with one entry per alias:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"organizations": [
|
|
146
|
+
{
|
|
147
|
+
"alias": "my-app",
|
|
148
|
+
"organization": "my-github-org",
|
|
149
|
+
"label": "my-release-topic",
|
|
150
|
+
"next_version": "v1.2.0",
|
|
151
|
+
"prerelease": null,
|
|
152
|
+
"last_released": null,
|
|
153
|
+
"last_released_version": null,
|
|
154
|
+
"last_hotfix": null,
|
|
155
|
+
"hotfix_mode": "increment"
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Field reference:
|
|
162
|
+
|
|
163
|
+
| Field | Required | Meaning |
|
|
164
|
+
|-------|----------|---------|
|
|
165
|
+
| `alias` | yes | The name you pass to `-c` — how you refer to this release group. |
|
|
166
|
+
| `organization` | yes | GitHub organization the repos live in. |
|
|
167
|
+
| `label` | yes | GitHub topic that selects the repos to release. |
|
|
168
|
+
| `next_version` | yes | The version to release next (e.g. `v1.2.0`). Bumped automatically after a real release. |
|
|
169
|
+
| `prerelease` | no | Prerelease type: `alpha`, `beta`, or `rc`. |
|
|
170
|
+
| `last_released` | no | ISO-8601 timestamp of the last release (set automatically). |
|
|
171
|
+
| `last_released_version` | no | The last version released (set automatically; used as the hotfix base). |
|
|
172
|
+
| `last_hotfix` | no | The last hotfix tag created (set automatically). |
|
|
173
|
+
| `hotfix_mode` | no | `increment` (default) or `hotfix` — see the hotfix table above. |
|
|
174
|
+
|
|
175
|
+
After a successful release or hotfix, whichever source the config was loaded from is updated automatically with the new version state.
|
|
176
|
+
|
|
177
|
+
### `.innoday/project.yml` — `release_configs`
|
|
178
|
+
|
|
179
|
+
If you keep a `.innoday/project.yml`, blastoff will read a `release_configs` list from it (checked before `org-versions.json`), with the same fields per alias:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
release_configs:
|
|
183
|
+
- alias: my-app
|
|
184
|
+
organization: my-github-org
|
|
185
|
+
label: my-release-topic
|
|
186
|
+
next_version: v1.2.0
|
|
187
|
+
prerelease: null
|
|
188
|
+
last_released: null
|
|
189
|
+
last_released_version: null
|
|
190
|
+
last_hotfix: null
|
|
191
|
+
hotfix_mode: increment
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Extending — custom version backends
|
|
195
|
+
|
|
196
|
+
Where release state lives is pluggable. blastoff computes versions and tags repos; *where* the version state is loaded from and *where* a completed release is recorded is abstracted behind the `VersionStore` interface (`blastoff.stores.VersionStore`). The shipped default is `FileVersionStore` (the file-backed backend above).
|
|
197
|
+
|
|
198
|
+
To store release state somewhere else — a database, an internal API, a service — implement the three abstract methods and inject your store into the release/hotfix commands:
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from blastoff.stores import VersionStore
|
|
202
|
+
from blastoff.version_manager import OrgConfig
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class MyVersionStore(VersionStore):
|
|
206
|
+
def load_org_config(self, alias: str) -> OrgConfig:
|
|
207
|
+
"""Return the OrgConfig for `alias` (raise FileNotFoundError if none)."""
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
def save_org_config(self, org: OrgConfig) -> None:
|
|
211
|
+
"""Persist an updated OrgConfig back to your backend."""
|
|
212
|
+
...
|
|
213
|
+
|
|
214
|
+
def record_release(self, org, version, released_at=None, summary=None, changelog=None) -> None:
|
|
215
|
+
"""Record that `version` was released for `org` (called after tagging)."""
|
|
216
|
+
...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Both the `Release` and `Hotfix` commands expose a `version_store` attribute; set it before invoking to swap the backend. When left unset it defaults to `FileVersionStore`, giving you the standalone file-backed behavior described above. blastoff imports nothing from any consumer — the dependency arrow points one way: your code depends on blastoff and implements this interface.
|
|
220
|
+
|
|
221
|
+
## Testing
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uv run pytest
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The suite is hermetic — no network access required.
|
|
228
|
+
|
|
229
|
+
## Contributing
|
|
230
|
+
|
|
231
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the
|
|
232
|
+
lint conventions, and the behavioral contracts (dry-run-by-default, idempotent
|
|
233
|
+
re-runs, the `VersionStore` interface) that are easy to break by accident.
|
|
234
|
+
|
|
235
|
+
## Changelog
|
|
236
|
+
|
|
237
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
238
|
+
|
|
239
|
+
> **Renamed:** this package was published as `pixelfuel-blastoff` for 0.1.0. It is
|
|
240
|
+
> now **`innoday-blastoff`** (a separate PyPI project, since PyPI has no rename);
|
|
241
|
+
> the old distribution is yanked. The import package and CLI command are
|
|
242
|
+
> unchanged — both are still `blastoff`.
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT — see [LICENSE](LICENSE).
|
|
247
|
+
|
|
248
|
+
To publish a new version to PyPI, see [PUBLISHING.md](PUBLISHING.md).
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Changelog collection for release summaries.
|
|
3
|
+
|
|
4
|
+
Builds a per-repo merged-PR inventory since the last full release or hotfix,
|
|
5
|
+
whichever is more recent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def resolve_changelog_boundary(org_config):
|
|
10
|
+
"""
|
|
11
|
+
Return the ISO timestamp marking the start of the changelog window.
|
|
12
|
+
|
|
13
|
+
org_config.last_released is refreshed to "now" on both a full release
|
|
14
|
+
(update_version_by_alias) and a hotfix (update_hotfix_by_alias), so it
|
|
15
|
+
already reflects whichever of the two happened most recently — no
|
|
16
|
+
separate comparison against last_hotfix is needed.
|
|
17
|
+
"""
|
|
18
|
+
return org_config.last_released
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def collect_merged_prs_since(repos, github, since_iso, repo_api_factory=None):
|
|
22
|
+
"""
|
|
23
|
+
Collect merged PRs since since_iso across the given repos.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
repos: list of Repo namedtuples (from GithubOrgApi.list_repos_for_topic)
|
|
27
|
+
github: Github namedtuple (owner, token)
|
|
28
|
+
since_iso: ISO 8601 timestamp string, or None to collect all merged PRs
|
|
29
|
+
repo_api_factory: optional callable(repo_name, github) -> GithubRepoApi,
|
|
30
|
+
for testing. Defaults to the real GithubRepoApi.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
[{repo: str, number, title, author, body}] flattened across all repos —
|
|
34
|
+
the single fetch that both build_changelog() and the Claude Code CLI
|
|
35
|
+
summary generator consume, so each repo's PRs are only fetched once.
|
|
36
|
+
"""
|
|
37
|
+
if repo_api_factory is None:
|
|
38
|
+
from blastoff.api.github_api import GithubRepoApi as repo_api_factory
|
|
39
|
+
|
|
40
|
+
flattened = []
|
|
41
|
+
for repo in repos:
|
|
42
|
+
repo_api = repo_api_factory(repo.name, github)
|
|
43
|
+
try:
|
|
44
|
+
merged_prs = repo_api.list_merged_pull_requests_since(since_iso)
|
|
45
|
+
except Exception:
|
|
46
|
+
merged_prs = []
|
|
47
|
+
|
|
48
|
+
for pr in merged_prs:
|
|
49
|
+
flattened.append({
|
|
50
|
+
"repo": repo.name,
|
|
51
|
+
"number": pr["number"],
|
|
52
|
+
"title": pr["title"],
|
|
53
|
+
"author": pr["author"],
|
|
54
|
+
"body": pr.get("body"),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return flattened
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_changelog(flattened_prs):
|
|
61
|
+
"""
|
|
62
|
+
Group flattened merged-PR entries (from collect_merged_prs_since) by repo.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
[{repo: str, prs: [{number, title, author}]}] — repos with no merged
|
|
66
|
+
PRs in the window are omitted. Preserves the repo order of first
|
|
67
|
+
appearance in flattened_prs.
|
|
68
|
+
"""
|
|
69
|
+
by_repo = {}
|
|
70
|
+
for pr in flattened_prs:
|
|
71
|
+
by_repo.setdefault(pr["repo"], []).append({
|
|
72
|
+
"number": pr["number"], "title": pr["title"], "author": pr["author"],
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return [{"repo": repo, "prs": prs} for repo, prs in by_repo.items()]
|