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 CHANGED
@@ -1,23 +1,311 @@
1
- # vite-plus-starter
1
+ # genbumppush
2
2
 
3
- A starter for creating a Vite Plus project.
3
+ [![CI](https://img.shields.io/github/actions/workflow/status/xcvzmoon/genbumppush/ci.yml?branch=main&color=black)](https://github.com/xcvzmoon/genbumppush/actions/workflows/ci.yml)
4
+ [![Release](https://img.shields.io/github/actions/workflow/status/xcvzmoon/genbumppush/release.yml?color=black)](https://github.com/xcvzmoon/genbumppush/actions/workflows/release.yml)
5
+ [![npm version](https://img.shields.io/npm/v/%40xcvzmoon%2Fgenbumppush?color=black)](https://www.npmjs.com/package/@xcvzmoon/genbumppush)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40xcvzmoon%2Fgenbumppush?color=black)](https://www.npmjs.com/package/@xcvzmoon/genbumppush)
4
7
 
5
- ## Development
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
- - Install dependencies:
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
- vp install
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
- - Run the unit tests:
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
- vp test
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
- - Build the library:
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/index.d.ts
2
- export declare function fn(): string;
3
- //#endregion
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
- //#region src/index.ts
2
- function fn() {
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.1",
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"