genbumppush 0.0.1 → 0.0.2
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 +296 -8
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +102 -0
- package/dist/index.d.mts +71 -3
- package/dist/index.mjs +2 -6
- package/dist/release-cb3ZULZM.mjs +582 -0
- package/package.json +21 -2
package/README.md
CHANGED
|
@@ -1,23 +1,311 @@
|
|
|
1
|
-
#
|
|
1
|
+
# genbumppush
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/xcvzmoon/genbumppush/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/xcvzmoon/genbumppush/actions/workflows/release.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@xcvzmoon/genbumppush)
|
|
6
|
+
[](https://www.npmjs.com/package/@xcvzmoon/genbumppush)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
`genbumppush` handles the repetitive parts of releasing a Conventional Commit repository. It picks the next semantic version, updates the files you choose, writes the changelog, commits, tags, and can push the branch and tag together.
|
|
9
|
+
|
|
10
|
+
It supports Node packages, Nuxt applications, fixed-version monorepos, Tauri applications, and tag-driven publication/deployment. It does not publish packages or create provider releases itself; GitHub Actions or GitLab CI should handle those after the tag is pushed.
|
|
11
|
+
|
|
12
|
+
## Requirements and installation
|
|
6
13
|
|
|
7
|
-
-
|
|
14
|
+
- Node.js 20.19+
|
|
15
|
+
- Git
|
|
16
|
+
- Conventional Commits
|
|
17
|
+
- An upstream branch when pushing (unless `requireUpstream: false`)
|
|
8
18
|
|
|
9
19
|
```bash
|
|
10
|
-
|
|
20
|
+
pnpm add -D genbumppush
|
|
21
|
+
# or: npm install --save-dev genbumppush
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{ "scripts": { "release": "genbumppush" } }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The package exports `defineConfig`, `loadReleaseConfig`, `runRelease`, and release types. The `genbumppush` binary is also available directly.
|
|
29
|
+
|
|
30
|
+
## Release lifecycle
|
|
31
|
+
|
|
32
|
+
The command loads C12 configuration, validates the Git worktree, detects the release type, and previews or confirms the release. It then calculates the version, checks local/remote tag collisions, runs `before` hooks, validates every version file, writes versions and the changelog, commits, tags, atomically pushes, and runs `after` hooks.
|
|
33
|
+
|
|
34
|
+
Version-file and commit-preparation failures restore files and the index. A failed tag or network push leaves the release commit locally for inspection and retry. If no releasable commit exists, it exits successfully without mutation.
|
|
35
|
+
|
|
36
|
+
## CLI
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
genbumppush [release] [options]
|
|
11
40
|
```
|
|
12
41
|
|
|
13
|
-
|
|
42
|
+
| Type | Result from `1.2.3` |
|
|
43
|
+
| ------------ | ------------------- |
|
|
44
|
+
| `major` | `2.0.0` |
|
|
45
|
+
| `minor` | `1.3.0` |
|
|
46
|
+
| `patch` | `1.2.4` |
|
|
47
|
+
| `premajor` | `2.0.0-beta.0` |
|
|
48
|
+
| `preminor` | `1.3.0-beta.0` |
|
|
49
|
+
| `prepatch` | `1.2.4-beta.0` |
|
|
50
|
+
| `prerelease` | `1.2.4-beta.0` |
|
|
14
51
|
|
|
15
52
|
```bash
|
|
16
|
-
|
|
53
|
+
pnpm release # detect from Conventional Commits
|
|
54
|
+
pnpm release patch # force a release type
|
|
55
|
+
pnpm release preminor --preid beta # start a prerelease channel
|
|
56
|
+
pnpm release prerelease --preid beta
|
|
57
|
+
pnpm release --dry-run # preview without mutation
|
|
58
|
+
pnpm release patch --no-push # local commit and tag only
|
|
59
|
+
pnpm release patch --yes # non-interactive
|
|
60
|
+
genbumppush --cwd ../app --config release.config.ts patch
|
|
61
|
+
genbumppush --retry-gitlab v1.2.4 # retry provider release after a successful Git push
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
| Option | Meaning |
|
|
65
|
+
| ---------------------- | -------------------------------------------------------- |
|
|
66
|
+
| positional release | One supported release type |
|
|
67
|
+
| `--cwd <path>` | Repository directory; defaults to the current directory |
|
|
68
|
+
| `--config <path>` | Explicit C12 config file |
|
|
69
|
+
| `--preid <id>` | Identifier containing letters, numbers, and hyphens |
|
|
70
|
+
| `--retry-gitlab <tag>` | Retry GitLab release creation for an existing remote tag |
|
|
71
|
+
| `--dry-run` | Preview without changing files, Git, or remotes |
|
|
72
|
+
| `--no-push` | Keep commit and tag local |
|
|
73
|
+
| `--yes`, `-y` | Skip confirmation |
|
|
74
|
+
| `--help`, `-h` | Print help |
|
|
75
|
+
|
|
76
|
+
Values are resolved in this order: CLI, config file, the `genbumppush` key in `package.json`, then defaults. A CLI flag such as `--no-push` overrides the config file.
|
|
77
|
+
|
|
78
|
+
## Commit detection
|
|
79
|
+
|
|
80
|
+
Automatic detection uses `changelogen`:
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
feat: add an adapter -> minor
|
|
84
|
+
fix: handle empty input -> patch
|
|
85
|
+
feat!: remove old API -> major
|
|
86
|
+
refactor(api)!: change API -> major
|
|
87
|
+
chore(deps): update vite -> excluded by default
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`BREAKING CHANGE:` footers are supported. Set `release` to force a type. Non-breaking `chore(deps)` commits are excluded by default; set `excludeDependencyCommits: false` to include them.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
Create `genbumppush.config.ts`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { defineConfig } from 'genbumppush';
|
|
98
|
+
|
|
99
|
+
export default defineConfig({
|
|
100
|
+
// release: 'patch', // omit for automatic detection
|
|
101
|
+
preid: 'beta',
|
|
102
|
+
changelog: 'CHANGELOG.md',
|
|
103
|
+
excludeDependencyCommits: true,
|
|
104
|
+
recursive: false,
|
|
105
|
+
files: ['package.json'],
|
|
106
|
+
git: {
|
|
107
|
+
remote: 'origin',
|
|
108
|
+
push: true,
|
|
109
|
+
sign: false,
|
|
110
|
+
requireClean: true,
|
|
111
|
+
requireUpstream: true,
|
|
112
|
+
commitMessage: 'chore(release): v{{version}}',
|
|
113
|
+
tagName: 'v{{version}}',
|
|
114
|
+
tagMessage: 'v{{version}}',
|
|
115
|
+
},
|
|
116
|
+
hooks: {
|
|
117
|
+
before: ['vp check', 'vp test'],
|
|
118
|
+
after: 'echo Release complete',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The same object can live under `"genbumppush"` in `package.json`. JavaScript and TypeScript C12 files are supported; use `--config` for another location.
|
|
124
|
+
|
|
125
|
+
| Key | Type | Default | Behavior |
|
|
126
|
+
| -------------------------- | ------------------------- | ------------------------------ | -------------------------------------- |
|
|
127
|
+
| `release` | release type | detected | Force release type |
|
|
128
|
+
| `preid` | string | `beta` | Prerelease channel |
|
|
129
|
+
| `changelog` | `false \| true \| string` | `CHANGELOG.md` | Disable, default, or custom path |
|
|
130
|
+
| `excludeDependencyCommits` | boolean | `true` | Ignore non-breaking dependency commits |
|
|
131
|
+
| `files` | string[] | `['package.json']` | Version files |
|
|
132
|
+
| `recursive` | boolean | `false` | Discover nested manifests |
|
|
133
|
+
| `git.remote` | string | `origin` | Remote for checks and push |
|
|
134
|
+
| `git.push` | boolean | `true` | Push branch and tag |
|
|
135
|
+
| `git.sign` | boolean | `false` | Sign commit and tag |
|
|
136
|
+
| `git.requireClean` | boolean | `true` | Reject uncommitted changes |
|
|
137
|
+
| `git.requireUpstream` | boolean | `true` | Require upstream before pushing |
|
|
138
|
+
| `git.commitMessage` | string | `chore(release): v{{version}}` | Commit template |
|
|
139
|
+
| `git.tagName` | string | `v{{version}}` | Tag template |
|
|
140
|
+
| `git.tagMessage` | string | `v{{version}}` | Annotated tag template |
|
|
141
|
+
| `hooks.before` | string or string[] | unset | Commands before file changes |
|
|
142
|
+
| `hooks.after` | string or string[] | unset | Commands after tag/push |
|
|
143
|
+
|
|
144
|
+
`{{version}}` is replaced in commit and tag templates. Hooks run through the shell in the repository directory; only use trusted configuration.
|
|
145
|
+
|
|
146
|
+
## Version-file adapters
|
|
147
|
+
|
|
148
|
+
Every configured file is validated before writes begin. Structured files must agree with the root version; mismatches fail without partial updates.
|
|
149
|
+
|
|
150
|
+
Supported adapters:
|
|
151
|
+
|
|
152
|
+
- `package.json`: top-level `version`
|
|
153
|
+
- `package-lock.json`: root `version` and `packages['']` version
|
|
154
|
+
- Explicit JSON such as `tauri.conf.json`: top-level `version`
|
|
155
|
+
- `Cargo.toml`: `version` in `[package]`
|
|
156
|
+
- `Cargo.lock`: only the package matching the adjacent Cargo manifest
|
|
157
|
+
|
|
158
|
+
Other files use exact text replacement: the current version must occur exactly once. This suits a `VERSION` file, but not arbitrary lockfiles. There is no general pnpm-lock.yaml adapter; configure it only when the current version occurs exactly once.
|
|
159
|
+
|
|
160
|
+
### Fixed-version monorepo
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
export default defineConfig({
|
|
164
|
+
recursive: true,
|
|
165
|
+
files: ['package.json', 'package-lock.json'],
|
|
166
|
+
});
|
|
17
167
|
```
|
|
18
168
|
|
|
19
|
-
-
|
|
169
|
+
Discovery ignores `.git`, `node_modules`, `dist`, `target`, and `.output`. It assumes versioned workspaces share one version; independent-version packages need a separate strategy.
|
|
170
|
+
|
|
171
|
+
### Tauri
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
export default defineConfig({
|
|
175
|
+
files: [
|
|
176
|
+
'package.json',
|
|
177
|
+
'src-tauri/tauri.conf.json',
|
|
178
|
+
'src-tauri/Cargo.toml',
|
|
179
|
+
'src-tauri/Cargo.lock',
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Cargo dependency versions are not changed. Only the application package and matching lock entry are updated.
|
|
185
|
+
|
|
186
|
+
## Git safety and recovery
|
|
187
|
+
|
|
188
|
+
Defaults reject dirty worktrees, detached HEAD, missing upstreams, and existing local or remote tags. Branch and tag are sent with `git push --atomic`.
|
|
189
|
+
|
|
190
|
+
Use `--no-push` for a local rehearsal. If a push fails after commit/tag creation, inspect `git status`, `git log`, and `git show`, then retry the push or remove local artifacts deliberately. If GitLab release creation fails after the Git push succeeds, fix the provider or credentials and run `genbumppush --retry-gitlab <tag>`.
|
|
191
|
+
|
|
192
|
+
For deliberate exceptions, set `git.requireClean: false` or `git.requireUpstream: false`. These do not disable tag collision checks or version validation.
|
|
193
|
+
|
|
194
|
+
## GitHub Actions and npm
|
|
195
|
+
|
|
196
|
+
The included workflows split CI, release creation, and package publication into separate jobs:
|
|
197
|
+
|
|
198
|
+
- `ci.yml` runs `vp check`, `vp test`, and `vp pack` on pull requests and main pushes.
|
|
199
|
+
- `release.yml` reacts to `v*` tags and creates a GitHub Release from the changelog section.
|
|
200
|
+
- `publish.yml` reacts to `v*` tags, verifies `v${package.json.version}`, rebuilds, and publishes.
|
|
201
|
+
|
|
202
|
+
A release commit message alone does not trigger tag workflows. The tag must exist and be pushed:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
git push origin main v0.0.1
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
For npm trusted publishing, configure the npm package trusted publisher to match the repository/workflow, keep `id-token: write`, and use a current npm. The included workflow disables provenance for private GitHub source repositories because npm rejects private-source provenance bundles. Public repositories can enable provenance after trusted publishing is configured.
|
|
209
|
+
|
|
210
|
+
## GitLab CI
|
|
211
|
+
|
|
212
|
+
GitLab release creation is supported as an opt-in provider. The Git branch and tag are
|
|
213
|
+
still pushed atomically first; only then does genbumppush call the GitLab Releases API.
|
|
214
|
+
Configure a project path and provide an API token through the environment:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
export default defineConfig({
|
|
218
|
+
git: { push: true },
|
|
219
|
+
gitlab: {
|
|
220
|
+
enabled: true,
|
|
221
|
+
host: 'https://gitlab.com',
|
|
222
|
+
project: 'group/project',
|
|
223
|
+
tokenEnv: 'GITLAB_TOKEN',
|
|
224
|
+
releaseName: 'v{{version}}',
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`GITLAB_HOST` and `GITLAB_PROJECT` may be used as environment fallbacks. The token
|
|
230
|
+
must be available as the configured `tokenEnv` (default `GITLAB_TOKEN`). GitLab receives
|
|
231
|
+
the matching `CHANGELOG.md` section as the release description. If `git.push` is false,
|
|
232
|
+
the provider is rejected because GitLab cannot create a release for an unpublished tag.
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
release:
|
|
236
|
+
image: node:20
|
|
237
|
+
rules:
|
|
238
|
+
- if: '$CI_COMMIT_TAG =~ /^v/'
|
|
239
|
+
script:
|
|
240
|
+
- npm ci
|
|
241
|
+
- npx genbumppush --dry-run --yes
|
|
242
|
+
- npm run build
|
|
243
|
+
release:
|
|
244
|
+
tag_name: '$CI_COMMIT_TAG'
|
|
245
|
+
name: 'Release $CI_COMMIT_TAG'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Keep artifact publication and GitLab release creation in protected, tag-triggered jobs with their own credentials.
|
|
249
|
+
|
|
250
|
+
## Scenario guide
|
|
251
|
+
|
|
252
|
+
| Scenario | Recommended setup |
|
|
253
|
+
| ---------------------- | -------------------------------------------------- |
|
|
254
|
+
| Local release | `pnpm release`, confirm interactively |
|
|
255
|
+
| CI release | `genbumppush --yes` with protected Git credentials |
|
|
256
|
+
| Preview only | `genbumppush --dry-run --yes` |
|
|
257
|
+
| Local commit/tag only | `genbumppush patch --no-push --yes` |
|
|
258
|
+
| Nuxt or Node package | Default `package.json` adapter |
|
|
259
|
+
| npm lockfile | Add `package-lock.json` explicitly |
|
|
260
|
+
| Fixed-version monorepo | `recursive: true` |
|
|
261
|
+
| Tauri | Explicit JSON, Cargo.toml, and Cargo.lock files |
|
|
262
|
+
| Custom VERSION file | Add only if the old version occurs once |
|
|
263
|
+
| GitHub/npm publication | Push `v*`; let workflows publish |
|
|
264
|
+
| GitLab release | Run downstream jobs on `$CI_COMMIT_TAG` |
|
|
265
|
+
|
|
266
|
+
## Troubleshooting
|
|
267
|
+
|
|
268
|
+
### Tag does not match package version
|
|
20
269
|
|
|
21
270
|
```bash
|
|
271
|
+
node -p "require('./package.json').version"
|
|
272
|
+
git describe --tags --exact-match HEAD
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The publisher requires `v${package.json.version}`. Correct the release commit before creating or pushing a replacement tag.
|
|
276
|
+
|
|
277
|
+
### Dirty worktree, existing tag, or version mismatch
|
|
278
|
+
|
|
279
|
+
Run `git status --short` and inspect an existing tag with `git show <tag>`. Commit/stash changes, align every configured manifest with the root version, and never overwrite a published tag as a normal retry.
|
|
280
|
+
|
|
281
|
+
### `EBADDEVENGINES`
|
|
282
|
+
|
|
283
|
+
`devEngines.packageManager` is npm metadata. Run with the declared package manager or align the metadata with the manager used by CI; it is separate from release logic.
|
|
284
|
+
|
|
285
|
+
### Private-repository provenance failure
|
|
286
|
+
|
|
287
|
+
npm rejects provenance bundles identifying private GitHub source repositories. Use the included workflow’s `NPM_CONFIG_PROVENANCE=false` behavior, or make the source public and enable provenance after trusted publishing setup.
|
|
288
|
+
|
|
289
|
+
### Workflow did not run
|
|
290
|
+
|
|
291
|
+
Verify the tag was pushed, matches `v*`, and the workflow exists on the pushed commit:
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
git ls-remote --tags origin
|
|
295
|
+
gh run list --workflow release.yml
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Development
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
vp install
|
|
302
|
+
vp check
|
|
303
|
+
vp test
|
|
22
304
|
vp pack
|
|
23
305
|
```
|
|
306
|
+
|
|
307
|
+
The test suite covers CLI parsing, C12 configuration, SemVer edges, JSON/npm/Cargo adapters, recursive workspaces, dry runs, rollback, hooks, tag collisions, detached HEAD, upstream checks, and a real atomic push to a temporary bare Git remote.
|
|
308
|
+
|
|
309
|
+
## License
|
|
310
|
+
|
|
311
|
+
MIT
|
package/dist/bin.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as ReleaseError, t as runRelease } from "./release-cb3ZULZM.mjs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
//#region src/types.ts
|
|
5
|
+
const RELEASE_TYPES = [
|
|
6
|
+
"major",
|
|
7
|
+
"premajor",
|
|
8
|
+
"minor",
|
|
9
|
+
"preminor",
|
|
10
|
+
"patch",
|
|
11
|
+
"prepatch",
|
|
12
|
+
"prerelease"
|
|
13
|
+
];
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/cli.ts
|
|
16
|
+
const HELP_TEXT = `Usage: genbumppush [release] [options]\n\nGenerate a changelog, bump versions, create an annotated tag, and push atomically.\n\nArguments:\n release ${RELEASE_TYPES.join(" | ")}\n\nOptions:\n --cwd <path> Repository directory\n --config <path> Explicit C12 config file\n --preid <id> Prerelease identifier\n --retry-gitlab <tag> Retry GitLab release creation for a pushed tag\n --dry-run Preview without changes\n --no-push Keep commit and tag local\n --yes, -y Skip confirmation\n --help, -h Show help\n`;
|
|
17
|
+
function isType(value) {
|
|
18
|
+
return RELEASE_TYPES.some((item) => item === value);
|
|
19
|
+
}
|
|
20
|
+
function next(args, index, option) {
|
|
21
|
+
const value = args[index + 1];
|
|
22
|
+
if (value === void 0 || value.startsWith("-")) throw new ReleaseError("MISSING_OPTION_VALUE", `${option} requires a value.`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function parseCliOptions(args) {
|
|
26
|
+
const result = {
|
|
27
|
+
cwd: process.cwd(),
|
|
28
|
+
dryRun: false,
|
|
29
|
+
yes: false,
|
|
30
|
+
help: false
|
|
31
|
+
};
|
|
32
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
33
|
+
const arg = args[index];
|
|
34
|
+
if (arg === void 0) continue;
|
|
35
|
+
const equals = arg.indexOf("=");
|
|
36
|
+
const option = equals < 0 ? arg : arg.slice(0, equals);
|
|
37
|
+
const inlineValue = equals < 0 ? void 0 : arg.slice(equals + 1);
|
|
38
|
+
switch (option) {
|
|
39
|
+
case "--dry-run":
|
|
40
|
+
result.dryRun = true;
|
|
41
|
+
break;
|
|
42
|
+
case "--no-push":
|
|
43
|
+
result.push = false;
|
|
44
|
+
break;
|
|
45
|
+
case "--yes":
|
|
46
|
+
case "-y":
|
|
47
|
+
result.yes = true;
|
|
48
|
+
break;
|
|
49
|
+
case "--help":
|
|
50
|
+
case "-h":
|
|
51
|
+
result.help = true;
|
|
52
|
+
break;
|
|
53
|
+
case "--cwd": {
|
|
54
|
+
const value = inlineValue ?? next(args, index, "--cwd");
|
|
55
|
+
result.cwd = resolve(value);
|
|
56
|
+
if (inlineValue === void 0) index += 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "--config":
|
|
60
|
+
result.configFile = inlineValue ?? next(args, index, "--config");
|
|
61
|
+
if (inlineValue === void 0) index += 1;
|
|
62
|
+
break;
|
|
63
|
+
case "--preid": {
|
|
64
|
+
const value = inlineValue ?? next(args, index, "--preid");
|
|
65
|
+
if (!/^[0-9A-Za-z-]+$/.test(value)) throw new ReleaseError("INVALID_PREID", "--preid accepts letters, numbers, and hyphens.");
|
|
66
|
+
result.preid = value;
|
|
67
|
+
if (inlineValue === void 0) index += 1;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "--retry-gitlab":
|
|
71
|
+
result.gitlabRetryTag = inlineValue ?? next(args, index, "--retry-gitlab");
|
|
72
|
+
if (inlineValue === void 0) index += 1;
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
if (isType(arg) && result.release === void 0) {
|
|
76
|
+
result.release = arg;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
throw new ReleaseError("UNKNOWN_ARGUMENT", `Unknown argument: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (result.gitlabRetryTag !== void 0 && result.release !== void 0) throw new ReleaseError("CONFLICTING_ARGUMENTS", "--retry-gitlab cannot be combined with a release type.");
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/bin.ts
|
|
87
|
+
try {
|
|
88
|
+
const options = parseCliOptions(process.argv.slice(2));
|
|
89
|
+
if (options.help) console.info(HELP_TEXT);
|
|
90
|
+
else {
|
|
91
|
+
const result = await runRelease(options);
|
|
92
|
+
if (result.gitlabReleaseCreated) console.info(`GitLab release ${result.tag ?? ""} created.`);
|
|
93
|
+
else if (result.releaseType === void 0) console.info("No releasable commits found.");
|
|
94
|
+
else if (!result.dryRun) console.info(`${result.tag ?? result.releaseType} created${result.pushed ? " and pushed" : ""}.`);
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error instanceof ReleaseError) console.error(`[${error.code}] ${error.message}`);
|
|
98
|
+
else console.error(error instanceof Error ? error.message : error);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export {};
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
declare const RELEASE_TYPES: readonly ["major", "premajor", "minor", "preminor", "patch", "prepatch", "prerelease"];
|
|
3
|
+
type ReleaseType = (typeof RELEASE_TYPES)[number];
|
|
4
|
+
type GitOptions = {
|
|
5
|
+
remote?: string;
|
|
6
|
+
push?: boolean;
|
|
7
|
+
sign?: boolean;
|
|
8
|
+
requireClean?: boolean;
|
|
9
|
+
requireUpstream?: boolean;
|
|
10
|
+
commitMessage?: string;
|
|
11
|
+
tagName?: string;
|
|
12
|
+
tagMessage?: string;
|
|
13
|
+
};
|
|
14
|
+
type HookOptions = {
|
|
15
|
+
before?: string | string[];
|
|
16
|
+
after?: string | string[];
|
|
17
|
+
};
|
|
18
|
+
type GitLabOptions = {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
host?: string;
|
|
21
|
+
project?: string;
|
|
22
|
+
tokenEnv?: string;
|
|
23
|
+
releaseName?: string;
|
|
24
|
+
};
|
|
25
|
+
type GenBumpPushConfig = {
|
|
26
|
+
release?: ReleaseType;
|
|
27
|
+
preid?: string;
|
|
28
|
+
files?: string[];
|
|
29
|
+
recursive?: boolean;
|
|
30
|
+
changelog?: boolean | string;
|
|
31
|
+
excludeDependencyCommits?: boolean;
|
|
32
|
+
git?: GitOptions;
|
|
33
|
+
gitlab?: GitLabOptions;
|
|
34
|
+
hooks?: HookOptions;
|
|
35
|
+
};
|
|
36
|
+
type CliOptions = {
|
|
37
|
+
cwd: string;
|
|
38
|
+
configFile?: string;
|
|
39
|
+
gitlabRetryTag?: string;
|
|
40
|
+
release?: ReleaseType;
|
|
41
|
+
preid?: string;
|
|
42
|
+
dryRun: boolean;
|
|
43
|
+
push?: boolean;
|
|
44
|
+
yes: boolean;
|
|
45
|
+
help: boolean;
|
|
46
|
+
};
|
|
47
|
+
type ReleaseResult = {
|
|
48
|
+
currentVersion: string;
|
|
49
|
+
newVersion?: string;
|
|
50
|
+
releaseType?: ReleaseType;
|
|
51
|
+
tag?: string;
|
|
52
|
+
pushed: boolean;
|
|
53
|
+
dryRun: boolean;
|
|
54
|
+
commitCount: number;
|
|
55
|
+
gitlabReleaseCreated?: boolean;
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/config.d.ts
|
|
59
|
+
export declare const defineConfig: import("c12").DefineConfig<GenBumpPushConfig, import("c12").ConfigLayerMeta>;
|
|
60
|
+
export declare function loadReleaseConfig(cwd: string, configFile?: string, overrides?: GenBumpPushConfig): Promise<GenBumpPushConfig>;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/error.d.ts
|
|
63
|
+
export declare class ReleaseError extends Error {
|
|
64
|
+
readonly code: string;
|
|
65
|
+
constructor(code: string, message: string, options?: ErrorOptions);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/release.d.ts
|
|
69
|
+
export declare function runRelease(options: CliOptions): Promise<ReleaseResult>;
|
|
70
|
+
//#endregion
|
|
71
|
+
export type { CliOptions, GenBumpPushConfig, GitLabOptions, GitOptions, HookOptions, ReleaseResult, ReleaseType };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
return "Hello, tsdown!";
|
|
4
|
-
}
|
|
5
|
-
//#endregion
|
|
6
|
-
export { fn };
|
|
1
|
+
import { i as loadReleaseConfig, n as ReleaseError, r as defineConfig, t as runRelease } from "./release-cb3ZULZM.mjs";
|
|
2
|
+
export { ReleaseError, defineConfig, loadReleaseConfig, runRelease };
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { createDefineConfig, loadConfig } from "c12";
|
|
2
|
+
import { determineSemverChange, generateMarkDown, getGitDiff, loadChangelogConfig, parseCommits } from "changelogen";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { readFile, readdir, realpath, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
8
|
+
//#region src/config.ts
|
|
9
|
+
const defineConfig = createDefineConfig();
|
|
10
|
+
const defaults = {
|
|
11
|
+
changelog: "CHANGELOG.md",
|
|
12
|
+
excludeDependencyCommits: true,
|
|
13
|
+
recursive: false,
|
|
14
|
+
git: {
|
|
15
|
+
remote: "origin",
|
|
16
|
+
push: true,
|
|
17
|
+
sign: false,
|
|
18
|
+
requireClean: true,
|
|
19
|
+
requireUpstream: true,
|
|
20
|
+
commitMessage: "chore(release): v{{version}}",
|
|
21
|
+
tagName: "v{{version}}",
|
|
22
|
+
tagMessage: "v{{version}}"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
async function loadReleaseConfig(cwd, configFile, overrides) {
|
|
26
|
+
return (await loadConfig({
|
|
27
|
+
name: "genbumppush",
|
|
28
|
+
cwd,
|
|
29
|
+
configFile,
|
|
30
|
+
packageJson: "genbumppush",
|
|
31
|
+
defaults,
|
|
32
|
+
overrides,
|
|
33
|
+
rcFile: false,
|
|
34
|
+
globalRc: false
|
|
35
|
+
})).config;
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/error.ts
|
|
39
|
+
var ReleaseError = class extends Error {
|
|
40
|
+
code;
|
|
41
|
+
constructor(code, message, options) {
|
|
42
|
+
super(message, options);
|
|
43
|
+
this.name = "ReleaseError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/git.ts
|
|
49
|
+
function git(args, cwd) {
|
|
50
|
+
try {
|
|
51
|
+
return execFileSync("git", args, {
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: [
|
|
55
|
+
"ignore",
|
|
56
|
+
"pipe",
|
|
57
|
+
"pipe"
|
|
58
|
+
]
|
|
59
|
+
}).trim();
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new ReleaseError("GIT_COMMAND_FAILED", `git ${args.join(" ")} failed.`, { cause: error });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function tagExists(cwd, tag) {
|
|
65
|
+
return spawnSync("git", [
|
|
66
|
+
"rev-parse",
|
|
67
|
+
"--verify",
|
|
68
|
+
"--quiet",
|
|
69
|
+
`refs/tags/${tag}`
|
|
70
|
+
], {
|
|
71
|
+
cwd,
|
|
72
|
+
stdio: "ignore"
|
|
73
|
+
}).status === 0;
|
|
74
|
+
}
|
|
75
|
+
function isGitRepository(cwd) {
|
|
76
|
+
return spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
77
|
+
cwd,
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
stdio: [
|
|
80
|
+
"ignore",
|
|
81
|
+
"pipe",
|
|
82
|
+
"ignore"
|
|
83
|
+
]
|
|
84
|
+
}).stdout.trim() === "true";
|
|
85
|
+
}
|
|
86
|
+
function remoteTagExists(cwd, remote, tag) {
|
|
87
|
+
return git([
|
|
88
|
+
"ls-remote",
|
|
89
|
+
"--tags",
|
|
90
|
+
remote,
|
|
91
|
+
`refs/tags/${tag}`
|
|
92
|
+
], cwd).length > 0;
|
|
93
|
+
}
|
|
94
|
+
function runHook(command, cwd) {
|
|
95
|
+
if (spawnSync(command, {
|
|
96
|
+
cwd,
|
|
97
|
+
shell: true,
|
|
98
|
+
stdio: "inherit"
|
|
99
|
+
}).status !== 0) throw new ReleaseError("HOOK_FAILED", `Hook failed: ${command}`);
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/gitlab.ts
|
|
103
|
+
function releaseNotes(changelog, tag) {
|
|
104
|
+
const lines = changelog.split("\n");
|
|
105
|
+
const heading = `## ${tag}`;
|
|
106
|
+
const start = lines.findIndex((line) => line === heading || line.startsWith(`${heading} `));
|
|
107
|
+
if (start < 0) return "See CHANGELOG.md for release notes.";
|
|
108
|
+
const notes = [];
|
|
109
|
+
for (const line of lines.slice(start + 1)) {
|
|
110
|
+
if (line.startsWith("## ")) break;
|
|
111
|
+
notes.push(line);
|
|
112
|
+
}
|
|
113
|
+
return notes.join("\n").trim() || "See CHANGELOG.md for release notes.";
|
|
114
|
+
}
|
|
115
|
+
async function createGitLabRelease(options) {
|
|
116
|
+
const url = `${options.host.replace(/\/$/, "")}/api/v4/projects/${encodeURIComponent(options.project)}/releases`;
|
|
117
|
+
let response;
|
|
118
|
+
try {
|
|
119
|
+
response = await fetch(url, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: {
|
|
122
|
+
Authorization: `Bearer ${options.token}`,
|
|
123
|
+
"Content-Type": "application/json"
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
tag_name: options.tag,
|
|
127
|
+
name: options.name,
|
|
128
|
+
description: options.description
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new ReleaseError("GITLAB_RELEASE_FAILED", "Could not send the GitLab release request.", { cause: error });
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const body = await response.text().catch(() => "");
|
|
136
|
+
throw new ReleaseError("GITLAB_RELEASE_FAILED", `GitLab release creation failed with ${response.status}: ${body || response.statusText}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/version-files.ts
|
|
141
|
+
const IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
142
|
+
".git",
|
|
143
|
+
"node_modules",
|
|
144
|
+
"dist",
|
|
145
|
+
"target",
|
|
146
|
+
".output"
|
|
147
|
+
]);
|
|
148
|
+
function isObject$1(value) {
|
|
149
|
+
return value !== null && Object(value) === value && !Array.isArray(value);
|
|
150
|
+
}
|
|
151
|
+
function isString$1(value) {
|
|
152
|
+
return Object.prototype.toString.call(value) === "[object String]";
|
|
153
|
+
}
|
|
154
|
+
function isPackageManifest(value) {
|
|
155
|
+
return isObject$1(value) && (!("version" in value) || value.version === void 0 || isString$1(value.version));
|
|
156
|
+
}
|
|
157
|
+
function isPackageMap(value) {
|
|
158
|
+
if (!isObject$1(value)) return false;
|
|
159
|
+
const root = "" in value ? value[""] : void 0;
|
|
160
|
+
return root === void 0 || isPackageManifest(root);
|
|
161
|
+
}
|
|
162
|
+
function isPackageLock(value) {
|
|
163
|
+
return isPackageManifest(value) && (!("packages" in value) || value.packages === void 0 || isPackageMap(value.packages));
|
|
164
|
+
}
|
|
165
|
+
function isInside(root, path) {
|
|
166
|
+
const fromRoot = relative(root, path);
|
|
167
|
+
return fromRoot === "" || !fromRoot.startsWith("..") && !isAbsolute(fromRoot);
|
|
168
|
+
}
|
|
169
|
+
async function resolveRepositoryPath(cwd, configuredPath) {
|
|
170
|
+
const root = resolve(cwd);
|
|
171
|
+
const canonicalRoot = await realpath(root);
|
|
172
|
+
const path = resolve(root, configuredPath);
|
|
173
|
+
if (!isInside(root, path)) throw new ReleaseError("PATH_OUTSIDE_REPOSITORY", `${path} is outside the repository.`);
|
|
174
|
+
if (!isInside(canonicalRoot, existsSync(path) ? await realpath(path) : await realpath(dirname(path)))) throw new ReleaseError("PATH_OUTSIDE_REPOSITORY", `${path} resolves outside the repository.`);
|
|
175
|
+
return path;
|
|
176
|
+
}
|
|
177
|
+
async function findManifests(directory) {
|
|
178
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
179
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
180
|
+
if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) return findManifests(join(directory, entry.name));
|
|
181
|
+
return entry.name === "package.json" ? [join(directory, entry.name)] : [];
|
|
182
|
+
}))).flat();
|
|
183
|
+
}
|
|
184
|
+
function assertCurrent(path, actual, expected) {
|
|
185
|
+
if (actual !== expected) throw new ReleaseError("VERSION_MISMATCH", `${path} has version ${actual}; expected ${expected}.`);
|
|
186
|
+
}
|
|
187
|
+
function replaceJsonVersion(content, path, current, version) {
|
|
188
|
+
const parsed = JSON.parse(content);
|
|
189
|
+
if (!isObject$1(parsed) || !("version" in parsed) || !isString$1(parsed.version)) throw new ReleaseError("MISSING_VERSION", `${path} has no top-level version string.`);
|
|
190
|
+
assertCurrent(path, parsed.version, current);
|
|
191
|
+
const updated = content.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
192
|
+
if (updated === content) throw new ReleaseError("VERSION_UNCHANGED", `${path} was not updated.`);
|
|
193
|
+
return updated;
|
|
194
|
+
}
|
|
195
|
+
function replacePackageLockVersion(content, path, current, version) {
|
|
196
|
+
const parsed = JSON.parse(content);
|
|
197
|
+
if (!isPackageLock(parsed)) throw new ReleaseError("INVALID_LOCKFILE", `${path} is invalid.`);
|
|
198
|
+
const data = parsed;
|
|
199
|
+
if (!isString$1(data.version)) throw new ReleaseError("MISSING_VERSION", `${path} has no root version.`);
|
|
200
|
+
assertCurrent(path, data.version, current);
|
|
201
|
+
const rootVersion = data.packages?.[""]?.version;
|
|
202
|
+
if (rootVersion !== void 0 && !isString$1(rootVersion)) throw new ReleaseError("INVALID_LOCKFILE", `${path} has an invalid root package version.`);
|
|
203
|
+
if (isString$1(rootVersion)) assertCurrent(path, rootVersion, current);
|
|
204
|
+
data.version = version;
|
|
205
|
+
if (data.packages?.[""] !== void 0) data.packages[""].version = version;
|
|
206
|
+
const indent = /^\s+"/.exec(content)?.[0].length ?? 2;
|
|
207
|
+
return `${JSON.stringify(data, null, indent)}\n`;
|
|
208
|
+
}
|
|
209
|
+
function cargoSection(content) {
|
|
210
|
+
const start = content.search(/^\[package\]\s*$/m);
|
|
211
|
+
if (start < 0) throw new ReleaseError("MISSING_CARGO_PACKAGE", "Cargo.toml has no [package] section.");
|
|
212
|
+
const next = content.slice(start + 1).search(/^\[[^[]/m);
|
|
213
|
+
return {
|
|
214
|
+
start,
|
|
215
|
+
end: next < 0 ? content.length : start + 1 + next
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function cargoName(content) {
|
|
219
|
+
const section = cargoSection(content);
|
|
220
|
+
const match = /^name\s*=\s*"([^"]+)"/m.exec(content.slice(section.start, section.end));
|
|
221
|
+
if (match?.[1] === void 0) throw new ReleaseError("MISSING_CARGO_NAME", "Cargo.toml has no package name.");
|
|
222
|
+
return match[1];
|
|
223
|
+
}
|
|
224
|
+
function replaceCargoVersion(content, path, current, version) {
|
|
225
|
+
const section = cargoSection(content);
|
|
226
|
+
const body = content.slice(section.start, section.end);
|
|
227
|
+
const match = /^version\s*=\s*"([^"]*)"/m.exec(body);
|
|
228
|
+
if (match?.[1] === void 0) throw new ReleaseError("MISSING_CARGO_VERSION", "Cargo.toml has no package version.");
|
|
229
|
+
assertCurrent(path, match[1], current);
|
|
230
|
+
const updated = body.replace(/(^version\s*=\s*")[^"]*(")/m, `$1${version}$2`);
|
|
231
|
+
if (updated === body) throw new ReleaseError("MISSING_CARGO_VERSION", "Cargo.toml has no package version.");
|
|
232
|
+
return `${content.slice(0, section.start)}${updated}${content.slice(section.end)}`;
|
|
233
|
+
}
|
|
234
|
+
function replaceCargoLockVersion(content, path, name, current, version) {
|
|
235
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
236
|
+
const blocks = content.split(/(?=^\[\[package\]\]\s*$)/m);
|
|
237
|
+
let matches = 0;
|
|
238
|
+
const updated = blocks.map((block) => {
|
|
239
|
+
if (!new RegExp(`^name\\s*=\\s*"${escaped}"\\s*$`, "m").test(block)) return block;
|
|
240
|
+
matches += 1;
|
|
241
|
+
const match = /^version\s*=\s*"([^"]*)"\s*$/m.exec(block);
|
|
242
|
+
if (match?.[1] === void 0) throw new ReleaseError("MISSING_CARGO_LOCK_VERSION", `${path} package ${name} has no version.`);
|
|
243
|
+
assertCurrent(path, match[1], current);
|
|
244
|
+
return block.replace(/(^version\s*=\s*")[^"]*(")/m, `$1${version}$2`);
|
|
245
|
+
}).join("");
|
|
246
|
+
if (matches === 0) throw new ReleaseError("MISSING_CARGO_LOCK_PACKAGE", `Cargo.lock has no package named ${name}.`);
|
|
247
|
+
if (matches > 1) throw new ReleaseError("AMBIGUOUS_CARGO_LOCK_PACKAGE", `${path} contains package ${name} more than once.`);
|
|
248
|
+
return updated;
|
|
249
|
+
}
|
|
250
|
+
async function transform(path, current, version) {
|
|
251
|
+
const content = await readFile(path, "utf8");
|
|
252
|
+
const name = basename(path);
|
|
253
|
+
if (name === "package-lock.json") return replacePackageLockVersion(content, path, current, version);
|
|
254
|
+
if (name === "package.json" || name === "tauri.conf.json" || name.endsWith(".json")) return replaceJsonVersion(content, path, current, version);
|
|
255
|
+
if (name === "Cargo.toml") return replaceCargoVersion(content, path, current, version);
|
|
256
|
+
if (name === "Cargo.lock") {
|
|
257
|
+
const manifestPath = join(dirname(path), "Cargo.toml");
|
|
258
|
+
if (!existsSync(manifestPath)) throw new ReleaseError("MISSING_CARGO_MANIFEST", `${relative(process.cwd(), manifestPath)} is required.`);
|
|
259
|
+
return replaceCargoLockVersion(content, path, cargoName(await readFile(manifestPath, "utf8")), current, version);
|
|
260
|
+
}
|
|
261
|
+
const occurrences = content.split(current).length - 1;
|
|
262
|
+
if (occurrences !== 1) throw new ReleaseError("AMBIGUOUS_VERSION", `${path} must contain the current version exactly once; found ${occurrences}.`);
|
|
263
|
+
return content.replace(current, version);
|
|
264
|
+
}
|
|
265
|
+
async function planVersionChanges(cwd, current, version, config) {
|
|
266
|
+
const configured = await Promise.all((config.files ?? ["package.json"]).map((path) => resolveRepositoryPath(cwd, path)));
|
|
267
|
+
const paths = config.recursive === true ? [...configured, ...await findManifests(cwd)] : configured;
|
|
268
|
+
const unique = [...new Set(paths)];
|
|
269
|
+
return Promise.all(unique.map(async (path) => {
|
|
270
|
+
if (!existsSync(path)) throw new ReleaseError("MISSING_VERSION_FILE", `${relative(cwd, path)} does not exist.`);
|
|
271
|
+
return {
|
|
272
|
+
path,
|
|
273
|
+
before: await readFile(path, "utf8"),
|
|
274
|
+
after: await transform(path, current, version)
|
|
275
|
+
};
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
async function applyVersionChanges(changes) {
|
|
279
|
+
try {
|
|
280
|
+
await Promise.all(changes.map((change) => writeFile(change.path, change.after)));
|
|
281
|
+
} catch (error) {
|
|
282
|
+
await restoreVersionChanges(changes);
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function restoreVersionChanges(changes) {
|
|
287
|
+
await Promise.all(changes.map((change) => writeFile(change.path, change.before)));
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/version.ts
|
|
291
|
+
const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
292
|
+
function parseVersion(value) {
|
|
293
|
+
const match = VERSION_PATTERN.exec(value);
|
|
294
|
+
if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) throw new ReleaseError("INVALID_VERSION", `Invalid semantic version: ${value}`);
|
|
295
|
+
const identifiers = match[4]?.split(".");
|
|
296
|
+
if (identifiers?.some((identifier) => /^\d+$/.test(identifier) && identifier.length > 1 && identifier.startsWith("0"))) throw new ReleaseError("INVALID_VERSION", `Invalid semantic version: ${value}`);
|
|
297
|
+
const version = {
|
|
298
|
+
major: Number(match[1]),
|
|
299
|
+
minor: Number(match[2]),
|
|
300
|
+
patch: Number(match[3])
|
|
301
|
+
};
|
|
302
|
+
if (identifiers !== void 0) version.prerelease = identifiers;
|
|
303
|
+
return version;
|
|
304
|
+
}
|
|
305
|
+
function format(version) {
|
|
306
|
+
const base = `${version.major}.${version.minor}.${version.patch}`;
|
|
307
|
+
return version.prerelease === void 0 ? base : `${base}-${version.prerelease.join(".")}`;
|
|
308
|
+
}
|
|
309
|
+
function applyPrerelease(version, preid) {
|
|
310
|
+
if (version.prerelease?.[0] === preid) {
|
|
311
|
+
const identifiers = [...version.prerelease];
|
|
312
|
+
const last = identifiers.at(-1);
|
|
313
|
+
if (last !== void 0 && /^\d+$/.test(last)) identifiers[identifiers.length - 1] = String(Number(last) + 1);
|
|
314
|
+
else identifiers.push("0");
|
|
315
|
+
return {
|
|
316
|
+
...version,
|
|
317
|
+
prerelease: identifiers
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
...version,
|
|
322
|
+
prerelease: [preid, "0"]
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function bumpVersion(current, release, preid = "beta") {
|
|
326
|
+
const version = parseVersion(current);
|
|
327
|
+
if (!/^[0-9A-Za-z-]+$/.test(preid)) throw new ReleaseError("INVALID_PREID", `Invalid prerelease identifier: ${preid}`);
|
|
328
|
+
const stable = version.prerelease === void 0 ? version : {
|
|
329
|
+
...version,
|
|
330
|
+
prerelease: void 0
|
|
331
|
+
};
|
|
332
|
+
switch (release) {
|
|
333
|
+
case "major": return format(version.prerelease !== void 0 && version.minor === 0 && version.patch === 0 ? stable : {
|
|
334
|
+
major: version.major + 1,
|
|
335
|
+
minor: 0,
|
|
336
|
+
patch: 0
|
|
337
|
+
});
|
|
338
|
+
case "minor": return format(version.prerelease !== void 0 && version.patch === 0 ? stable : {
|
|
339
|
+
major: version.major,
|
|
340
|
+
minor: version.minor + 1,
|
|
341
|
+
patch: 0
|
|
342
|
+
});
|
|
343
|
+
case "patch": return format(version.prerelease === void 0 ? {
|
|
344
|
+
major: version.major,
|
|
345
|
+
minor: version.minor,
|
|
346
|
+
patch: version.patch + 1
|
|
347
|
+
} : stable);
|
|
348
|
+
case "premajor": return format(applyPrerelease({
|
|
349
|
+
major: version.major + 1,
|
|
350
|
+
minor: 0,
|
|
351
|
+
patch: 0
|
|
352
|
+
}, preid));
|
|
353
|
+
case "preminor": return format(applyPrerelease({
|
|
354
|
+
major: version.major,
|
|
355
|
+
minor: version.minor + 1,
|
|
356
|
+
patch: 0
|
|
357
|
+
}, preid));
|
|
358
|
+
case "prepatch": return format(applyPrerelease({
|
|
359
|
+
major: version.major,
|
|
360
|
+
minor: version.minor,
|
|
361
|
+
patch: version.patch + 1
|
|
362
|
+
}, preid));
|
|
363
|
+
case "prerelease": return format(applyPrerelease(version.prerelease === void 0 ? {
|
|
364
|
+
...version,
|
|
365
|
+
patch: version.patch + 1
|
|
366
|
+
} : version, preid));
|
|
367
|
+
}
|
|
368
|
+
throw new ReleaseError("INVALID_RELEASE_TYPE", "Unsupported release type.");
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/release.ts
|
|
372
|
+
const render = (value, version) => value.replaceAll("{{version}}", version);
|
|
373
|
+
const list = (value) => value === void 0 ? [] : Array.isArray(value) ? value : [value];
|
|
374
|
+
function isObject(value) {
|
|
375
|
+
return value !== null && Object(value) === value && !Array.isArray(value);
|
|
376
|
+
}
|
|
377
|
+
function isString(value) {
|
|
378
|
+
return Object.prototype.toString.call(value) === "[object String]";
|
|
379
|
+
}
|
|
380
|
+
function gitLabContext(config) {
|
|
381
|
+
if (config?.enabled !== true) throw new ReleaseError("GITLAB_RELEASE_FAILED", "Enable gitlab before creating a release.");
|
|
382
|
+
const tokenEnv = config.tokenEnv ?? "GITLAB_TOKEN";
|
|
383
|
+
const token = process.env[tokenEnv];
|
|
384
|
+
const project = config.project ?? process.env.GITLAB_PROJECT;
|
|
385
|
+
if (token === void 0 || token.length === 0) throw new ReleaseError("GITLAB_RELEASE_FAILED", `Set ${tokenEnv} to create a GitLab release.`);
|
|
386
|
+
if (project === void 0 || project.length === 0) throw new ReleaseError("GITLAB_RELEASE_FAILED", "Set gitlab.project or GITLAB_PROJECT to create a GitLab release.");
|
|
387
|
+
const context = {
|
|
388
|
+
host: config.host ?? process.env.GITLAB_HOST ?? "https://gitlab.com",
|
|
389
|
+
project,
|
|
390
|
+
token
|
|
391
|
+
};
|
|
392
|
+
if (config.releaseName !== void 0) context.releaseName = config.releaseName;
|
|
393
|
+
return context;
|
|
394
|
+
}
|
|
395
|
+
async function changelogText(cwd, config) {
|
|
396
|
+
if (config.changelog === false) return "";
|
|
397
|
+
const path = await resolveRepositoryPath(cwd, config.changelog === true ? "CHANGELOG.md" : config.changelog ?? "CHANGELOG.md");
|
|
398
|
+
return existsSync(path) ? readFile(path, "utf8") : "";
|
|
399
|
+
}
|
|
400
|
+
async function publishGitLab(context, cwd, config, tag, version) {
|
|
401
|
+
await createGitLabRelease({
|
|
402
|
+
host: context.host,
|
|
403
|
+
project: context.project,
|
|
404
|
+
token: context.token,
|
|
405
|
+
tag,
|
|
406
|
+
name: context.releaseName?.replaceAll("{{version}}", version) ?? tag,
|
|
407
|
+
description: releaseNotes(await changelogText(cwd, config), tag)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function filter(commits, config, exclude) {
|
|
411
|
+
return commits.filter((commit) => {
|
|
412
|
+
const type = config.types[commit.type.toLowerCase()];
|
|
413
|
+
return type !== void 0 && type !== false && !(exclude && commit.type === "chore" && commit.scope === "deps" && !commit.isBreaking);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async function collect(cwd, config) {
|
|
417
|
+
const changelog = await loadChangelogConfig(cwd);
|
|
418
|
+
const commits = parseCommits(await getGitDiff(changelog.from, changelog.to, cwd), changelog);
|
|
419
|
+
for (const commit of commits) commit.type = commit.type.toLowerCase();
|
|
420
|
+
return {
|
|
421
|
+
changelog,
|
|
422
|
+
commits: filter(commits, changelog, config.excludeDependencyCommits !== false)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async function confirm(message) {
|
|
426
|
+
const input = createInterface({
|
|
427
|
+
input: process.stdin,
|
|
428
|
+
output: process.stdout
|
|
429
|
+
});
|
|
430
|
+
try {
|
|
431
|
+
return /^y(es)?$/i.test((await input.question(`${message} (y/N) `)).trim());
|
|
432
|
+
} finally {
|
|
433
|
+
input.close();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function prepend(path, markdown) {
|
|
437
|
+
const current = existsSync(path) ? await readFile(path, "utf8") : "# Changelog\n";
|
|
438
|
+
const match = current.match(/^# .+$/m);
|
|
439
|
+
const offset = match?.index === void 0 ? 0 : match.index + match[0].length;
|
|
440
|
+
const prefix = current.slice(0, offset).trimEnd();
|
|
441
|
+
const suffix = current.slice(offset).trim();
|
|
442
|
+
await writeFile(path, `${prefix}${prefix ? "\n\n" : ""}${markdown.trim()}${suffix ? `\n\n${suffix}` : ""}\n`);
|
|
443
|
+
}
|
|
444
|
+
function packageVersion(cwd) {
|
|
445
|
+
const data = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8"));
|
|
446
|
+
if (!isObject(data) || !("version" in data) || !isString(data.version)) throw new ReleaseError("INVALID_PACKAGE", "package.json must contain a version string.");
|
|
447
|
+
return data.version;
|
|
448
|
+
}
|
|
449
|
+
async function runRelease(options) {
|
|
450
|
+
const overrides = {};
|
|
451
|
+
if (options.release !== void 0) overrides.release = options.release;
|
|
452
|
+
if (options.preid !== void 0) overrides.preid = options.preid;
|
|
453
|
+
if (options.push !== void 0) overrides.git = { push: options.push };
|
|
454
|
+
const config = await loadReleaseConfig(options.cwd, options.configFile, overrides);
|
|
455
|
+
const cwd = options.cwd;
|
|
456
|
+
if (!isGitRepository(cwd)) throw new ReleaseError("NOT_A_REPOSITORY", `${cwd} is not a Git repository.`);
|
|
457
|
+
const currentVersion = packageVersion(cwd);
|
|
458
|
+
if (options.gitlabRetryTag !== void 0) {
|
|
459
|
+
const context = gitLabContext(config.gitlab);
|
|
460
|
+
const remote = config.git?.remote ?? "origin";
|
|
461
|
+
if (!remoteTagExists(cwd, remote, options.gitlabRetryTag)) throw new ReleaseError("GITLAB_RELEASE_FAILED", `Tag ${options.gitlabRetryTag} does not exist on ${remote}.`);
|
|
462
|
+
await publishGitLab(context, cwd, config, options.gitlabRetryTag, currentVersion);
|
|
463
|
+
return {
|
|
464
|
+
currentVersion,
|
|
465
|
+
tag: options.gitlabRetryTag,
|
|
466
|
+
pushed: true,
|
|
467
|
+
dryRun: false,
|
|
468
|
+
commitCount: 0,
|
|
469
|
+
gitlabReleaseCreated: true
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (config.git?.requireClean !== false && git(["status", "--porcelain"], cwd) !== "") throw new ReleaseError("DIRTY_WORKTREE", "Commit or stash all changes before releasing.");
|
|
473
|
+
const branch = git(["branch", "--show-current"], cwd);
|
|
474
|
+
if (branch === "") throw new ReleaseError("DETACHED_HEAD", "Releases require a checked-out branch.");
|
|
475
|
+
const push = config.git?.push !== false;
|
|
476
|
+
if (push && config.git?.requireUpstream !== false) git([
|
|
477
|
+
"rev-parse",
|
|
478
|
+
"--abbrev-ref",
|
|
479
|
+
"--symbolic-full-name",
|
|
480
|
+
"@{upstream}"
|
|
481
|
+
], cwd);
|
|
482
|
+
const { changelog, commits } = await collect(cwd, config);
|
|
483
|
+
const detected = config.release ?? determineSemverChange(commits, changelog);
|
|
484
|
+
if (detected === null) return {
|
|
485
|
+
currentVersion,
|
|
486
|
+
pushed: false,
|
|
487
|
+
dryRun: options.dryRun,
|
|
488
|
+
commitCount: 0
|
|
489
|
+
};
|
|
490
|
+
const releaseType = detected;
|
|
491
|
+
console.info(`Release: v${currentVersion} → ${releaseType} (${commits.length} commits)`);
|
|
492
|
+
if (options.dryRun) {
|
|
493
|
+
console.info(await generateMarkDown(commits, changelog));
|
|
494
|
+
return {
|
|
495
|
+
currentVersion,
|
|
496
|
+
releaseType,
|
|
497
|
+
pushed: false,
|
|
498
|
+
dryRun: true,
|
|
499
|
+
commitCount: commits.length
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
let gitlab;
|
|
503
|
+
if (config.gitlab?.enabled === true) {
|
|
504
|
+
if (!push) throw new ReleaseError("GITLAB_RELEASE_FAILED", "GitLab release creation requires git.push to be enabled.");
|
|
505
|
+
gitlab = gitLabContext(config.gitlab);
|
|
506
|
+
}
|
|
507
|
+
if (!options.yes && !await confirm(`Create a ${releaseType} release?`)) throw new ReleaseError("CANCELLED", "Release cancelled.");
|
|
508
|
+
const plannedVersion = bumpVersion(currentVersion, releaseType, config.preid);
|
|
509
|
+
const tag = render(config.git?.tagName ?? "v{{version}}", plannedVersion);
|
|
510
|
+
const remote = config.git?.remote ?? "origin";
|
|
511
|
+
if (tagExists(cwd, tag) || push && remoteTagExists(cwd, remote, tag)) throw new ReleaseError("TAG_EXISTS", `Tag ${tag} already exists.`);
|
|
512
|
+
for (const command of list(config.hooks?.before)) runHook(command, cwd);
|
|
513
|
+
const version = plannedVersion;
|
|
514
|
+
const changes = await planVersionChanges(cwd, currentVersion, version, config);
|
|
515
|
+
const changed = changes.map((change) => change.path);
|
|
516
|
+
let changelogSnapshot;
|
|
517
|
+
if (config.changelog !== false) {
|
|
518
|
+
const path = await resolveRepositoryPath(cwd, config.changelog === true ? "CHANGELOG.md" : config.changelog ?? "CHANGELOG.md");
|
|
519
|
+
const existed = existsSync(path);
|
|
520
|
+
changelogSnapshot = {
|
|
521
|
+
path,
|
|
522
|
+
existed
|
|
523
|
+
};
|
|
524
|
+
if (existed) changelogSnapshot.content = await readFile(path, "utf8");
|
|
525
|
+
changed.push(path);
|
|
526
|
+
}
|
|
527
|
+
const indexTree = git(["write-tree"], cwd);
|
|
528
|
+
try {
|
|
529
|
+
await applyVersionChanges(changes);
|
|
530
|
+
if (changelogSnapshot !== void 0) await prepend(changelogSnapshot.path, await generateMarkDown(commits, {
|
|
531
|
+
...changelog,
|
|
532
|
+
newVersion: version
|
|
533
|
+
}));
|
|
534
|
+
git([
|
|
535
|
+
"add",
|
|
536
|
+
"--",
|
|
537
|
+
...[...new Set(changed)].map((path) => relative(cwd, path))
|
|
538
|
+
], cwd);
|
|
539
|
+
const commitArgs = ["commit"];
|
|
540
|
+
if (config.git?.sign === true) commitArgs.push("-S");
|
|
541
|
+
commitArgs.push("-m", render(config.git?.commitMessage ?? "chore(release): v{{version}}", version));
|
|
542
|
+
git(commitArgs, cwd);
|
|
543
|
+
} catch (error) {
|
|
544
|
+
await restoreVersionChanges(changes);
|
|
545
|
+
if (changelogSnapshot?.content !== void 0) await writeFile(changelogSnapshot.path, changelogSnapshot.content);
|
|
546
|
+
else if (changelogSnapshot !== void 0 && existsSync(changelogSnapshot.path)) await unlink(changelogSnapshot.path);
|
|
547
|
+
git(["read-tree", indexTree], cwd);
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
const tagArgs = ["tag", "-a"];
|
|
551
|
+
if (config.git?.sign === true) tagArgs.push("-s");
|
|
552
|
+
tagArgs.push(tag, "-m", render(config.git?.tagMessage ?? "v{{version}}", version));
|
|
553
|
+
git(tagArgs, cwd);
|
|
554
|
+
if (push) git([
|
|
555
|
+
"push",
|
|
556
|
+
"--atomic",
|
|
557
|
+
remote,
|
|
558
|
+
`HEAD:${branch}`,
|
|
559
|
+
`refs/tags/${tag}`
|
|
560
|
+
], cwd);
|
|
561
|
+
let gitlabReleaseCreated = false;
|
|
562
|
+
if (gitlab !== void 0) try {
|
|
563
|
+
await publishGitLab(gitlab, cwd, config, tag, version);
|
|
564
|
+
gitlabReleaseCreated = true;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
throw new ReleaseError("RELEASE_PUBLISHED_GITLAB_FAILED", `Git release ${tag} was pushed, but GitLab release creation failed. Retry with: genbumppush --retry-gitlab ${tag}`, { cause: error });
|
|
567
|
+
}
|
|
568
|
+
for (const command of list(config.hooks?.after)) runHook(command, cwd);
|
|
569
|
+
const result = {
|
|
570
|
+
currentVersion,
|
|
571
|
+
newVersion: version,
|
|
572
|
+
releaseType,
|
|
573
|
+
tag,
|
|
574
|
+
pushed: push,
|
|
575
|
+
dryRun: false,
|
|
576
|
+
commitCount: commits.length
|
|
577
|
+
};
|
|
578
|
+
if (gitlabReleaseCreated) result.gitlabReleaseCreated = true;
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
//#endregion
|
|
582
|
+
export { loadReleaseConfig as i, ReleaseError as n, defineConfig as r, runRelease as t };
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genbumppush",
|
|
3
3
|
"description": "Generate changelog, bump version, then push",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"author": "Mon Albert Gamil <mrgamilmonalbert@gmail.com>",
|
|
8
9
|
"homepage": "https://github.com/xcvzmoon/genbumppush#readme",
|
|
@@ -18,11 +19,26 @@
|
|
|
18
19
|
],
|
|
19
20
|
"exports": {
|
|
20
21
|
".": "./dist/index.mjs",
|
|
22
|
+
"./bin": "./dist/bin.mjs",
|
|
21
23
|
"./package.json": "./package.json"
|
|
22
24
|
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"genbumppush": "./dist/bin.mjs"
|
|
27
|
+
},
|
|
23
28
|
"publishConfig": {
|
|
24
29
|
"access": "public"
|
|
25
30
|
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.19.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"release",
|
|
36
|
+
"changelog",
|
|
37
|
+
"semver",
|
|
38
|
+
"conventional-commits",
|
|
39
|
+
"monorepo",
|
|
40
|
+
"tauri"
|
|
41
|
+
],
|
|
26
42
|
"scripts": {
|
|
27
43
|
"build": "vp pack",
|
|
28
44
|
"dev": "vp pack --watch",
|
|
@@ -31,9 +47,12 @@
|
|
|
31
47
|
"prepublishOnly": "vp run build",
|
|
32
48
|
"prepare": "vp config"
|
|
33
49
|
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"c12": "4.0.0-rc.1",
|
|
52
|
+
"changelogen": "^0.6.2"
|
|
53
|
+
},
|
|
34
54
|
"devDependencies": {
|
|
35
55
|
"@types/node": "26.5.0",
|
|
36
|
-
"bumpp": "^11.1.0",
|
|
37
56
|
"typescript": "^6.0.3",
|
|
38
57
|
"vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
|
|
39
58
|
"vite-plus": "^0.3.1"
|