super-release 0.1.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/README.md ADDED
@@ -0,0 +1,308 @@
1
+ # super-release
2
+
3
+ A fast [semantic-release](https://semantic-release.gitbook.io/semantic-release) alternative for monorepos, written in Rust.
4
+
5
+ Analyzes [conventional commits](https://www.conventionalcommits.org/) to determine version bumps, generate changelogs, update `package.json` files, publish to npm, and create git tags -- across all packages in a monorepo, in parallel.
6
+
7
+ ## Features
8
+
9
+ - Monorepo-first: discovers all `package.json` packages and associates commits by changed files
10
+ - Parallel commit analysis using rayon (configurable with `-j`)
11
+ - Prerelease branches (`beta`, `next`, or dynamic from branch name)
12
+ - Maintenance branches (`1.x`, `2.x`) with major-version capping
13
+ - Changelog generation powered by [git-cliff](https://git-cliff.org/)
14
+ - Plugin system: changelog, npm, git-tag (extensible)
15
+ - Configurable tag format templates
16
+ - Dependency-aware npm publish (topological order)
17
+ - Dry-run mode with pretty, truncated output
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ cargo install --path .
23
+ ```
24
+
25
+ Or build a release binary:
26
+
27
+ ```bash
28
+ cargo build --release
29
+ # Binary at target/release/super-release
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # Preview what would be released
36
+ super-release --dry-run
37
+
38
+ # Run a release
39
+ super-release
40
+
41
+ # Use 4 threads for commit analysis
42
+ super-release -j 4
43
+ ```
44
+
45
+ ## CLI Reference
46
+
47
+ ```
48
+ Usage: super-release [OPTIONS]
49
+
50
+ Options:
51
+ -n, --dry-run Show what would happen without making changes
52
+ -C, --path <PATH> Repository root [default: .]
53
+ -c, --config <CONFIG> Path to config file [default: .release.yaml]
54
+ -v, --verbose Verbose output
55
+ -j, --jobs <JOBS> Parallel jobs for commit analysis [default: 50% of CPUs]
56
+ -h, --help Print help
57
+ -V, --version Print version
58
+ ```
59
+
60
+ ## How It Works
61
+
62
+ 1. **Discover packages** -- finds all directories with a `package.json`
63
+ 2. **Resolve tags** -- finds the latest release tag per package (filtered by branch context)
64
+ 3. **Walk commits** -- only analyzes commits since the oldest tag (not the entire history)
65
+ 4. **Associate commits to packages** -- maps changed files to their owning package
66
+ 5. **Calculate versions** -- uses git-cliff's conventional commit analysis to determine bump levels
67
+ 6. **Run plugins** -- changelog, npm publish, git tag (in configured order)
68
+
69
+ ## Conventional Commits
70
+
71
+ super-release follows the [Conventional Commits](https://www.conventionalcommits.org/) specification:
72
+
73
+ | Commit | Bump |
74
+ |---|---|
75
+ | `fix: ...` | patch |
76
+ | `feat: ...` | minor |
77
+ | `feat!: ...` or `BREAKING CHANGE:` in footer | major |
78
+ | `perf: ...` | patch |
79
+ | `chore: ...`, `docs: ...`, `ci: ...` | no release |
80
+
81
+ ## Configuration
82
+
83
+ Create a `.release.yaml` (or `.release.yml`, `.super-release.yaml`) in your repository root. All fields are optional and have sensible defaults.
84
+
85
+ ### Full Example
86
+
87
+ ```yaml
88
+ # Branch configurations
89
+ branches:
90
+ # Stable branches (simple string = stable, no prerelease)
91
+ - main
92
+ - master
93
+
94
+ # Prerelease with a fixed channel name
95
+ - name: beta
96
+ prerelease: beta # -> 2.0.0-beta.1, 2.0.0-beta.2, ...
97
+
98
+ - name: next
99
+ prerelease: next # -> 2.0.0-next.1, ...
100
+
101
+ # Prerelease using the branch name as the channel (for branch patterns)
102
+ - name: "test-*"
103
+ prerelease: true # branch test-foo -> 2.0.0-test-foo.1, ...
104
+
105
+ # Maintenance branches (caps major version, breaking changes -> minor)
106
+ - name: "1.x"
107
+ maintenance: true # -> 1.5.1, 1.6.0 (never 2.0.0)
108
+
109
+ # Tag format templates (use {version} and {name} placeholders)
110
+ tag_format: "v{version}" # root package: v1.2.3
111
+ tag_format_package: "{name}/v{version}" # sub-packages: @acme/core/v1.2.3
112
+
113
+ # Packages to exclude from releasing (substring match on package name)
114
+ exclude:
115
+ - my-private-pkg
116
+
117
+ # Plugins run in order: prepare phase first, then publish phase
118
+ plugins:
119
+ - name: changelog
120
+ - name: npm
121
+ - name: git-tag
122
+ ```
123
+
124
+ ### Reference
125
+
126
+ #### `branches`
127
+
128
+ Defines which branches can produce releases and what kind.
129
+
130
+ | Form | Type | Example versions |
131
+ |---|---|---|
132
+ | `- main` | Stable | `1.0.0`, `1.1.0`, `2.0.0` |
133
+ | `- name: beta`<br>` prerelease: beta` | Prerelease (fixed channel) | `2.0.0-beta.1`, `2.0.0-beta.2` |
134
+ | `- name: "test-*"`<br>` prerelease: true` | Prerelease (branch name as channel) | `2.0.0-test-my-feature.1` |
135
+ | `- name: "1.x"`<br>` maintenance: true` | Maintenance | `1.5.1`, `1.6.0` (major capped) |
136
+
137
+ **Tag filtering by branch**: Stable branches only see stable tags. Prerelease branches see their own channel's tags plus stable tags. This prevents a `v2.0.0-beta.1` tag from affecting version calculation on `main`.
138
+
139
+ **Prerelease behavior**: If the latest tag for a package is already on the same prerelease channel (e.g. `v2.0.0-beta.3`), the next release increments the prerelease number (`v2.0.0-beta.4`). If coming from a stable version, it computes the next stable bump and appends the channel (`v1.1.0-beta.1`).
140
+
141
+ **Maintenance behavior**: Breaking changes (`feat!:`) are demoted to minor bumps so the major version never increases on a maintenance branch.
142
+
143
+ Default: `["main", "master"]`
144
+
145
+ #### `tag_format`
146
+
147
+ Template for root package tags. Placeholders:
148
+ - `{version}` -- the semver version (e.g. `1.2.3`, `2.0.0-beta.1`)
149
+ - `{name}` -- the package name from `package.json`
150
+
151
+ Default: `"v{version}"`
152
+
153
+ Examples:
154
+ ```yaml
155
+ tag_format: "v{version}" # -> v1.2.3
156
+ tag_format: "release-{version}" # -> release-1.2.3
157
+ tag_format: "{name}-v{version}" # -> my-app-v1.2.3
158
+ ```
159
+
160
+ #### `tag_format_package`
161
+
162
+ Template for sub-package tags in a monorepo.
163
+
164
+ Default: `"{name}/v{version}"`
165
+
166
+ Examples:
167
+ ```yaml
168
+ tag_format_package: "{name}/v{version}" # -> @acme/core/v1.2.3
169
+ tag_format_package: "{name}@{version}" # -> @acme/core@1.2.3 (semantic-release compat)
170
+ ```
171
+
172
+ To migrate from semantic-release's tag format, set `tag_format_package: "{name}@{version}"`.
173
+
174
+ #### `plugins`
175
+
176
+ Ordered list of plugins to execute. Each plugin runs its `prepare` phase, then its `publish` phase. Each plugin accepts an `options` object for customization.
177
+
178
+ Default: `[changelog, npm, git-commit, git-tag]`
179
+
180
+ ```yaml
181
+ plugins:
182
+ - name: changelog
183
+ options:
184
+ filename: CHANGELOG.md # output file per package (default: CHANGELOG.md)
185
+ preview_lines: 20 # max lines shown in dry-run (default: 20)
186
+
187
+ - name: npm
188
+ options:
189
+ access: public # npm access level (default: "public")
190
+ registry: https://registry.npmjs.org # custom registry URL
191
+ tag: next # dist-tag override (default: auto from prerelease channel)
192
+ publish_args: # extra args passed to the publish command
193
+ - "--otp=123456"
194
+ package_manager: yarn # force specific PM (default: auto-detect)
195
+
196
+ - name: exec
197
+ options:
198
+ # Run for all packages (omit `packages` to run for all)
199
+ prepare_cmd: "echo Releasing {name} v{version}"
200
+ # Or filter to specific packages:
201
+ # packages: ["@acme/core"]
202
+
203
+ # Multiple exec blocks for different packages:
204
+ # - name: exec
205
+ # options:
206
+ # packages: ["my-rust-project"]
207
+ # prepare_cmd: "sed -i'' -e 's/^version = .*/version = \"{version}\"/' Cargo.toml"
208
+ # - name: exec
209
+ # options:
210
+ # packages: ["@acme/*"]
211
+ # publish_cmd: "deploy.sh {name} {version}"
212
+
213
+ - name: git-commit
214
+ options:
215
+ # Commit message template. Placeholders:
216
+ # {releases} - comma-separated list: "@acme/core@1.1.0, @acme/utils@1.0.1"
217
+ # {summary} - one per line: " - @acme/core 1.0.0 -> 1.1.0"
218
+ # {count} - number of packages released
219
+ message: "chore(release): {releases} [skip ci]"
220
+ push: false # push after commit (default: false)
221
+ remote: origin # git remote (default: "origin")
222
+ paths: # paths to stage (default: ["."])
223
+ - "."
224
+
225
+ - name: git-tag
226
+ options:
227
+ push: false # push tags to remote after creation (default: false)
228
+ remote: origin # git remote to push to (default: "origin")
229
+ ```
230
+
231
+ | Plugin | Prepare | Publish |
232
+ |---|---|---|
233
+ | `changelog` | Generates/updates changelog per package (parallel) | -- |
234
+ | `npm` | Updates `package.json` versions (auto-detects npm/yarn/pnpm) | Publishes packages (parallel within dependency levels) |
235
+ | `exec` | Runs custom shell command per package | Runs custom shell command per package |
236
+ | `git-commit` | -- | Stages changed files, commits with release message, optionally pushes |
237
+ | `git-tag` | -- | Creates annotated git tags, optionally pushes |
238
+
239
+ The default plugin order ensures: changelogs and version bumps are written first, then committed, then tagged.
240
+
241
+ #### `packages`
242
+
243
+ Optional list of glob patterns to include. When set, only packages whose name matches at least one pattern are released. Supports `*`, `?`, `[...]`, and `{a,b}` alternation.
244
+
245
+ ```yaml
246
+ # Only release packages in the @acme scope
247
+ packages:
248
+ - "@acme/*"
249
+
250
+ # Release specific packages
251
+ packages:
252
+ - "@acme/core"
253
+ - "@acme/utils"
254
+
255
+ # Multiple scopes
256
+ packages:
257
+ - "{@acme/*,@tools/*}"
258
+ ```
259
+
260
+ Default: all discovered packages.
261
+
262
+ #### `exclude`
263
+
264
+ List of glob patterns to exclude from releasing. Applied after `packages`.
265
+
266
+ ```yaml
267
+ exclude:
268
+ - my-monorepo-root
269
+ - "@acme/internal-*"
270
+ ```
271
+
272
+ ## Monorepo Structure
273
+
274
+ super-release discovers packages by finding `package.json` files recursively (skipping `node_modules`, `.git`, `dist`, `build`). Each commit is associated to a package based on which files it changed.
275
+
276
+ ```
277
+ my-monorepo/
278
+ package.json <- root package (tags: v1.0.0)
279
+ .release.yaml
280
+ packages/
281
+ core/
282
+ package.json <- @acme/core (tags: @acme/core/v1.0.0)
283
+ src/
284
+ utils/
285
+ package.json <- @acme/utils (tags: @acme/utils/v1.0.0)
286
+ src/
287
+ ```
288
+
289
+ **Dependency-aware publishing**: The npm plugin builds a dependency graph from `dependencies`, `devDependencies`, and `peerDependencies`. Packages are published in topological order (dependencies before dependents), and interdependency version ranges are updated automatically (preserving `^`/`~` prefixes).
290
+
291
+ ## Performance
292
+
293
+ super-release is designed to be fast:
294
+
295
+ - **Parallel diff computation**: commit diffs are computed across multiple threads (thread-local git repo handles)
296
+ - **Tag-bounded history walk**: only walks commits since the oldest package tag, not the entire history
297
+ - **Single-pass commit collection**: commits are fetched once and partitioned per package
298
+ - **Precomputed file mapping**: file-to-package association is computed once, not per-package
299
+
300
+ Benchmark (2001 commits, 8 packages, Apple Silicon):
301
+ | Scenario | Time |
302
+ |---|---|
303
+ | All commits since initial tag | 0.11s |
304
+ | 100 commits since recent tags | 0.035s |
305
+
306
+ ## License
307
+
308
+ MIT
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from "node:child_process";
4
+ import { dirname, join } from "node:path";
5
+ import { platform } from "node:os";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const bin = join(
10
+ __dirname,
11
+ platform() === "win32" ? "super-release.exe" : "super-release"
12
+ );
13
+
14
+ try {
15
+ execFileSync(bin, process.argv.slice(2), { stdio: "inherit" });
16
+ } catch (err) {
17
+ if (err.status !== undefined) process.exit(err.status);
18
+ console.error(`Failed to run super-release: ${err.message}`);
19
+ process.exit(1);
20
+ }
package/npm/install.js ADDED
@@ -0,0 +1,69 @@
1
+ import { execSync } from "node:child_process";
2
+ import { createWriteStream, existsSync, mkdirSync, unlinkSync, chmodSync, readFileSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { tmpdir, platform, arch } from "node:os";
5
+ import { get } from "node:https";
6
+ import { pipeline } from "node:stream/promises";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const { version } = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf8"));
11
+ const REPO = "bowlingx/super-release";
12
+
13
+ const PLATFORM_MAP = {
14
+ "linux-x64": "super-release-linux-x86_64",
15
+ "linux-arm64": "super-release-linux-aarch64",
16
+ "darwin-x64": "super-release-darwin-x86_64",
17
+ "darwin-arm64": "super-release-darwin-aarch64",
18
+ };
19
+
20
+ function download(url) {
21
+ return new Promise((resolve, reject) => {
22
+ get(url, (res) => {
23
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
24
+ return download(res.headers.location).then(resolve, reject);
25
+ }
26
+ if (res.statusCode !== 200) {
27
+ reject(new Error(`Download failed: HTTP ${res.statusCode}`));
28
+ return;
29
+ }
30
+ resolve(res);
31
+ }).on("error", reject);
32
+ });
33
+ }
34
+
35
+ const key = `${platform()}-${arch()}`;
36
+ const artifact = PLATFORM_MAP[key];
37
+
38
+ if (!artifact) {
39
+ console.error(
40
+ `Unsupported platform: ${key}. Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`
41
+ );
42
+ process.exit(1);
43
+ }
44
+
45
+ const binDir = join(__dirname, "bin");
46
+ const binPath = join(binDir, "super-release");
47
+
48
+ if (existsSync(binPath)) {
49
+ process.exit(0);
50
+ }
51
+
52
+ mkdirSync(binDir, { recursive: true });
53
+
54
+ const url = `https://github.com/${REPO}/releases/download/v${version}/${artifact}.tar.gz`;
55
+ console.log(`Downloading super-release v${version} for ${key}...`);
56
+
57
+ try {
58
+ const response = await download(url);
59
+ const tmpFile = join(tmpdir(), `super-release-${Date.now()}.tar.gz`);
60
+ await pipeline(response, createWriteStream(tmpFile));
61
+ execSync(`tar xzf ${tmpFile} -C ${binDir}`, { stdio: "ignore" });
62
+ unlinkSync(tmpFile);
63
+ chmodSync(binPath, 0o755);
64
+ console.log(`Installed super-release v${version}`);
65
+ } catch (err) {
66
+ console.error(`Failed to install super-release: ${err.message}`);
67
+ console.error(`You can install manually from: https://github.com/${REPO}/releases/tag/v${version}`);
68
+ process.exit(1);
69
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "super-release",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "A fast semantic-release alternative for monorepos, written in Rust",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "super-release": "npm/bin/super-release.js"
9
+ },
10
+ "scripts": {
11
+ "postinstall": "node npm/install.js"
12
+ },
13
+ "files": [
14
+ "npm/bin",
15
+ "npm/install.js",
16
+ "README.md"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/bowlingx/super-release.git"
21
+ },
22
+ "keywords": [
23
+ "semantic-release",
24
+ "monorepo",
25
+ "release",
26
+ "changelog",
27
+ "conventional-commits"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
35
+ "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
36
+ }