genbumppush 0.0.2 → 0.0.3
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 +41 -27
- package/dist/bin.mjs +11 -3
- package/dist/index.d.mts +11 -1
- package/dist/index.mjs +1 -1
- package/dist/{release-cb3ZULZM.mjs → release-CVQ5WSKG.mjs} +310 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,23 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/xcvzmoon/genbumppush/actions/workflows/ci.yml)
|
|
4
4
|
[](https://github.com/xcvzmoon/genbumppush/actions/workflows/release.yml)
|
|
5
|
-
[](https://www.npmjs.com/package/genbumppush)
|
|
6
|
+
[](https://www.npmjs.com/package/genbumppush)
|
|
7
7
|
|
|
8
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
9
|
|
|
10
|
-
It supports Node packages,
|
|
11
|
-
|
|
12
|
-
## Requirements and installation
|
|
13
|
-
|
|
14
|
-
- Node.js 20.19+
|
|
15
|
-
- Git
|
|
16
|
-
- Conventional Commits
|
|
17
|
-
- An upstream branch when pushing (unless `requireUpstream: false`)
|
|
10
|
+
It supports Node packages, web applications (React, Vue, Solid, Svelte, Astro, Next, Nuxt, etc.), 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.
|
|
18
11
|
|
|
19
12
|
```bash
|
|
20
|
-
|
|
21
|
-
# or: npm install --save-dev genbumppush
|
|
13
|
+
npm install --save-dev genbumppush
|
|
22
14
|
```
|
|
23
15
|
|
|
24
16
|
```json
|
|
@@ -50,15 +42,15 @@ genbumppush [release] [options]
|
|
|
50
42
|
| `prerelease` | `1.2.4-beta.0` |
|
|
51
43
|
|
|
52
44
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
npm run release # detect from Conventional Commits
|
|
46
|
+
npm run release patch # force a release type
|
|
47
|
+
npm run release preminor --preid beta # start a prerelease channel
|
|
48
|
+
npm run release prerelease --preid beta
|
|
49
|
+
npm run release --dry-run # preview without mutation
|
|
50
|
+
npm run release patch --no-push # local commit and tag only
|
|
51
|
+
npm run release patch --yes # non-interactive
|
|
60
52
|
genbumppush --cwd ../app --config release.config.ts patch
|
|
61
|
-
genbumppush --retry-gitlab v1.2.4
|
|
53
|
+
genbumppush --retry-gitlab v1.2.4 # retry provider release after a successful Git push
|
|
62
54
|
```
|
|
63
55
|
|
|
64
56
|
| Option | Meaning |
|
|
@@ -68,6 +60,7 @@ genbumppush --retry-gitlab v1.2.4 # retry provider release after a successful G
|
|
|
68
60
|
| `--config <path>` | Explicit C12 config file |
|
|
69
61
|
| `--preid <id>` | Identifier containing letters, numbers, and hyphens |
|
|
70
62
|
| `--retry-gitlab <tag>` | Retry GitLab release creation for an existing remote tag |
|
|
63
|
+
| `--retry-github <tag>` | Retry GitHub release creation for an existing remote tag |
|
|
71
64
|
| `--dry-run` | Preview without changing files, Git, or remotes |
|
|
72
65
|
| `--no-push` | Keep commit and tag local |
|
|
73
66
|
| `--yes`, `-y` | Skip confirmation |
|
|
@@ -140,6 +133,12 @@ The same object can live under `"genbumppush"` in `package.json`. JavaScript and
|
|
|
140
133
|
| `git.tagMessage` | string | `v{{version}}` | Annotated tag template |
|
|
141
134
|
| `hooks.before` | string or string[] | unset | Commands before file changes |
|
|
142
135
|
| `hooks.after` | string or string[] | unset | Commands after tag/push |
|
|
136
|
+
| `github.enabled` | boolean | `false` | Create a GitHub release after push |
|
|
137
|
+
| `github.host` | string | `github.com` | github.com or GHES host |
|
|
138
|
+
| `github.repo` | string | `GITHUB_REPOSITORY` / remote | `owner/name` |
|
|
139
|
+
| `github.tokenEnv` | string | `GITHUB_TOKEN` | Token environment variable |
|
|
140
|
+
| `github.releaseName` | string | tag | Release title template |
|
|
141
|
+
| `gitlab.enabled` | boolean | `false` | Create a GitLab release after push |
|
|
143
142
|
|
|
144
143
|
`{{version}}` is replaced in commit and tag templates. Hooks run through the shell in the repository directory; only use trusted configuration.
|
|
145
144
|
|
|
@@ -205,13 +204,28 @@ A release commit message alone does not trigger tag workflows. The tag must exis
|
|
|
205
204
|
git push origin main v0.0.1
|
|
206
205
|
```
|
|
207
206
|
|
|
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
|
|
207
|
+
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 enables provenance for public repositories. Private source repositories should set `NPM_CONFIG_PROVENANCE=false` because npm rejects private-source provenance bundles.
|
|
208
|
+
|
|
209
|
+
## GitHub and GitLab releases
|
|
210
|
+
|
|
211
|
+
GitHub Releases are supported as an opt-in provider after the Git branch and tag are pushed atomically. genbumppush uses the exact `git.tagName` string (so custom templates work) and creates or updates the release via the GitHub API (including GHES).
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
export default defineConfig({
|
|
215
|
+
git: { push: true },
|
|
216
|
+
github: {
|
|
217
|
+
enabled: true,
|
|
218
|
+
// host: 'github.com', // or a GHES host
|
|
219
|
+
// repo: 'group/project', // or GITHUB_REPOSITORY / package.json repository
|
|
220
|
+
tokenEnv: 'GITHUB_TOKEN', // also honors GH_TOKEN and CHANGELOGEN_TOKENS_GITHUB
|
|
221
|
+
releaseName: 'v{{version}}',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
```
|
|
209
225
|
|
|
210
|
-
|
|
226
|
+
If GitHub release creation fails after the Git push succeeds, fix credentials and run `genbumppush --retry-github <tag>`.
|
|
211
227
|
|
|
212
|
-
GitLab release creation
|
|
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:
|
|
228
|
+
GitLab release creation remains supported the same way. Configure a project path and provide an API token through the environment:
|
|
215
229
|
|
|
216
230
|
```ts
|
|
217
231
|
export default defineConfig({
|
|
@@ -251,7 +265,7 @@ Keep artifact publication and GitLab release creation in protected, tag-triggere
|
|
|
251
265
|
|
|
252
266
|
| Scenario | Recommended setup |
|
|
253
267
|
| ---------------------- | -------------------------------------------------- |
|
|
254
|
-
| Local release | `
|
|
268
|
+
| Local release | `npm run release`, confirm interactively |
|
|
255
269
|
| CI release | `genbumppush --yes` with protected Git credentials |
|
|
256
270
|
| Preview only | `genbumppush --dry-run --yes` |
|
|
257
271
|
| Local commit/tag only | `genbumppush patch --no-push --yes` |
|
|
@@ -284,7 +298,7 @@ Run `git status --short` and inspect an existing tag with `git show <tag>`. Comm
|
|
|
284
298
|
|
|
285
299
|
### Private-repository provenance failure
|
|
286
300
|
|
|
287
|
-
npm rejects provenance bundles identifying private GitHub source repositories.
|
|
301
|
+
npm rejects provenance bundles identifying private GitHub source repositories. Set `NPM_CONFIG_PROVENANCE=false` in the publish workflow, or make the source public and keep provenance enabled.
|
|
288
302
|
|
|
289
303
|
### Workflow did not run
|
|
290
304
|
|
package/dist/bin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as ReleaseError, t as runRelease } from "./release-
|
|
2
|
+
import { n as ReleaseError, t as runRelease } from "./release-CVQ5WSKG.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
//#region src/types.ts
|
|
5
5
|
const RELEASE_TYPES = [
|
|
@@ -13,7 +13,7 @@ const RELEASE_TYPES = [
|
|
|
13
13
|
];
|
|
14
14
|
//#endregion
|
|
15
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`;
|
|
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 --retry-github <tag> Retry GitHub 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
17
|
function isType(value) {
|
|
18
18
|
return RELEASE_TYPES.some((item) => item === value);
|
|
19
19
|
}
|
|
@@ -71,6 +71,10 @@ function parseCliOptions(args) {
|
|
|
71
71
|
result.gitlabRetryTag = inlineValue ?? next(args, index, "--retry-gitlab");
|
|
72
72
|
if (inlineValue === void 0) index += 1;
|
|
73
73
|
break;
|
|
74
|
+
case "--retry-github":
|
|
75
|
+
result.githubRetryTag = inlineValue ?? next(args, index, "--retry-github");
|
|
76
|
+
if (inlineValue === void 0) index += 1;
|
|
77
|
+
break;
|
|
74
78
|
default:
|
|
75
79
|
if (isType(arg) && result.release === void 0) {
|
|
76
80
|
result.release = arg;
|
|
@@ -80,6 +84,8 @@ function parseCliOptions(args) {
|
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
if (result.gitlabRetryTag !== void 0 && result.release !== void 0) throw new ReleaseError("CONFLICTING_ARGUMENTS", "--retry-gitlab cannot be combined with a release type.");
|
|
87
|
+
if (result.githubRetryTag !== void 0 && result.release !== void 0) throw new ReleaseError("CONFLICTING_ARGUMENTS", "--retry-github cannot be combined with a release type.");
|
|
88
|
+
if (result.gitlabRetryTag !== void 0 && result.githubRetryTag !== void 0) throw new ReleaseError("CONFLICTING_ARGUMENTS", "--retry-gitlab and --retry-github cannot be combined.");
|
|
83
89
|
return result;
|
|
84
90
|
}
|
|
85
91
|
//#endregion
|
|
@@ -89,8 +95,10 @@ try {
|
|
|
89
95
|
if (options.help) console.info(HELP_TEXT);
|
|
90
96
|
else {
|
|
91
97
|
const result = await runRelease(options);
|
|
92
|
-
if (result.
|
|
98
|
+
if (result.githubReleaseCreated) console.info(`GitHub release ${result.tag ?? ""} created.`);
|
|
99
|
+
else if (result.gitlabReleaseCreated) console.info(`GitLab release ${result.tag ?? ""} created.`);
|
|
93
100
|
else if (result.releaseType === void 0) console.info("No releasable commits found.");
|
|
101
|
+
else if (result.dryRun) console.info(`Dry run: would create ${result.tag ?? result.releaseType} from ${result.currentVersion}${result.newVersion !== void 0 ? ` to v${result.newVersion}` : ""}.`);
|
|
94
102
|
else if (!result.dryRun) console.info(`${result.tag ?? result.releaseType} created${result.pushed ? " and pushed" : ""}.`);
|
|
95
103
|
}
|
|
96
104
|
} catch (error) {
|
package/dist/index.d.mts
CHANGED
|
@@ -22,6 +22,13 @@ type GitLabOptions = {
|
|
|
22
22
|
tokenEnv?: string;
|
|
23
23
|
releaseName?: string;
|
|
24
24
|
};
|
|
25
|
+
type GitHubOptions = {
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
host?: string;
|
|
28
|
+
repo?: string;
|
|
29
|
+
tokenEnv?: string;
|
|
30
|
+
releaseName?: string;
|
|
31
|
+
};
|
|
25
32
|
type GenBumpPushConfig = {
|
|
26
33
|
release?: ReleaseType;
|
|
27
34
|
preid?: string;
|
|
@@ -31,12 +38,14 @@ type GenBumpPushConfig = {
|
|
|
31
38
|
excludeDependencyCommits?: boolean;
|
|
32
39
|
git?: GitOptions;
|
|
33
40
|
gitlab?: GitLabOptions;
|
|
41
|
+
github?: GitHubOptions;
|
|
34
42
|
hooks?: HookOptions;
|
|
35
43
|
};
|
|
36
44
|
type CliOptions = {
|
|
37
45
|
cwd: string;
|
|
38
46
|
configFile?: string;
|
|
39
47
|
gitlabRetryTag?: string;
|
|
48
|
+
githubRetryTag?: string;
|
|
40
49
|
release?: ReleaseType;
|
|
41
50
|
preid?: string;
|
|
42
51
|
dryRun: boolean;
|
|
@@ -53,6 +62,7 @@ type ReleaseResult = {
|
|
|
53
62
|
dryRun: boolean;
|
|
54
63
|
commitCount: number;
|
|
55
64
|
gitlabReleaseCreated?: boolean;
|
|
65
|
+
githubReleaseCreated?: boolean;
|
|
56
66
|
};
|
|
57
67
|
//#endregion
|
|
58
68
|
//#region src/config.d.ts
|
|
@@ -68,4 +78,4 @@ export declare class ReleaseError extends Error {
|
|
|
68
78
|
//#region src/release.d.ts
|
|
69
79
|
export declare function runRelease(options: CliOptions): Promise<ReleaseResult>;
|
|
70
80
|
//#endregion
|
|
71
|
-
export type { CliOptions, GenBumpPushConfig, GitLabOptions, GitOptions, HookOptions, ReleaseResult, ReleaseType };
|
|
81
|
+
export type { CliOptions, GenBumpPushConfig, GitHubOptions, GitLabOptions, GitOptions, HookOptions, ReleaseResult, ReleaseType };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as loadReleaseConfig, n as ReleaseError, r as defineConfig, t as runRelease } from "./release-
|
|
1
|
+
import { i as loadReleaseConfig, n as ReleaseError, r as defineConfig, t as runRelease } from "./release-CVQ5WSKG.mjs";
|
|
2
2
|
export { ReleaseError, defineConfig, loadReleaseConfig, runRelease };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createDefineConfig, loadConfig } from "c12";
|
|
2
|
-
import { determineSemverChange, generateMarkDown, getGitDiff, loadChangelogConfig, parseCommits } from "changelogen";
|
|
2
|
+
import { determineSemverChange, generateMarkDown, getGitDiff, loadChangelogConfig, parseCommits, resolveRepoConfig } from "changelogen";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { readFile, readdir, realpath, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -73,7 +73,7 @@ function tagExists(cwd, tag) {
|
|
|
73
73
|
}).status === 0;
|
|
74
74
|
}
|
|
75
75
|
function isGitRepository(cwd) {
|
|
76
|
-
|
|
76
|
+
const result = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
77
77
|
cwd,
|
|
78
78
|
encoding: "utf8",
|
|
79
79
|
stdio: [
|
|
@@ -81,7 +81,9 @@ function isGitRepository(cwd) {
|
|
|
81
81
|
"pipe",
|
|
82
82
|
"ignore"
|
|
83
83
|
]
|
|
84
|
-
})
|
|
84
|
+
});
|
|
85
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== "string") return false;
|
|
86
|
+
return result.stdout.trim() === "true";
|
|
85
87
|
}
|
|
86
88
|
function remoteTagExists(cwd, remote, tag) {
|
|
87
89
|
return git([
|
|
@@ -99,6 +101,105 @@ function runHook(command, cwd) {
|
|
|
99
101
|
}).status !== 0) throw new ReleaseError("HOOK_FAILED", `Hook failed: ${command}`);
|
|
100
102
|
}
|
|
101
103
|
//#endregion
|
|
104
|
+
//#region src/github.ts
|
|
105
|
+
function normalizeHost(host) {
|
|
106
|
+
return host.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
107
|
+
}
|
|
108
|
+
function apiBase(host) {
|
|
109
|
+
const normalized = normalizeHost(host);
|
|
110
|
+
if (normalized === "github.com" || normalized === "api.github.com") return "https://api.github.com";
|
|
111
|
+
return `https://${normalized}/api/v3`;
|
|
112
|
+
}
|
|
113
|
+
function encodeRepoPath(repo) {
|
|
114
|
+
return repo.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
115
|
+
}
|
|
116
|
+
function resolveGitHubToken(tokenEnv) {
|
|
117
|
+
if (tokenEnv !== void 0) {
|
|
118
|
+
const token = process.env[tokenEnv];
|
|
119
|
+
return token !== void 0 && token.length > 0 ? token : void 0;
|
|
120
|
+
}
|
|
121
|
+
for (const name of [
|
|
122
|
+
"GITHUB_TOKEN",
|
|
123
|
+
"GH_TOKEN",
|
|
124
|
+
"CHANGELOGEN_TOKENS_GITHUB"
|
|
125
|
+
]) {
|
|
126
|
+
const token = process.env[name];
|
|
127
|
+
if (token !== void 0 && token.length > 0) return token;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function resolveGitHubRepo(cwd, source) {
|
|
131
|
+
const host = normalizeHost(source.host ?? process.env.GITHUB_API_URL ?? "github.com");
|
|
132
|
+
const explicit = source.repo ?? process.env.GITHUB_REPOSITORY;
|
|
133
|
+
if (explicit !== void 0 && explicit.length > 0) return {
|
|
134
|
+
host,
|
|
135
|
+
repo: explicit
|
|
136
|
+
};
|
|
137
|
+
const resolved = await resolveRepoConfig(cwd);
|
|
138
|
+
if (resolved?.provider === "github" && resolved.repo) return {
|
|
139
|
+
host: normalizeHost(resolved.domain ?? host),
|
|
140
|
+
repo: resolved.repo
|
|
141
|
+
};
|
|
142
|
+
throw new ReleaseError("GITHUB_RELEASE_FAILED", "Set github.repo or GITHUB_REPOSITORY to create a GitHub release.");
|
|
143
|
+
}
|
|
144
|
+
async function githubFetch(options, path, init) {
|
|
145
|
+
const url = `${apiBase(options.host)}/repos/${encodeRepoPath(options.repo)}${path}`;
|
|
146
|
+
const headers = new Headers({
|
|
147
|
+
Accept: "application/vnd.github+json",
|
|
148
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
149
|
+
Authorization: `Bearer ${options.token}`,
|
|
150
|
+
"Content-Type": "application/json"
|
|
151
|
+
});
|
|
152
|
+
if (init?.headers) new Headers(init.headers).forEach((value, key) => {
|
|
153
|
+
headers.set(key, value);
|
|
154
|
+
});
|
|
155
|
+
let response;
|
|
156
|
+
try {
|
|
157
|
+
response = await fetch(url, {
|
|
158
|
+
method: init?.method,
|
|
159
|
+
body: init?.body,
|
|
160
|
+
headers,
|
|
161
|
+
signal: init?.signal ?? AbortSignal.timeout(3e4)
|
|
162
|
+
});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new ReleaseError("GITHUB_RELEASE_FAILED", "Could not send the GitHub release request.", { cause: error });
|
|
165
|
+
}
|
|
166
|
+
return response;
|
|
167
|
+
}
|
|
168
|
+
function parseReleaseId(body) {
|
|
169
|
+
if (typeof body !== "object" || body === null || !("id" in body)) return void 0;
|
|
170
|
+
const id = body.id;
|
|
171
|
+
return typeof id === "number" ? id : void 0;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Create or update a GitHub (or GHES) release for an existing tag.
|
|
175
|
+
* Uses the exact tag string so custom genbumppush tag templates stay valid.
|
|
176
|
+
*/
|
|
177
|
+
async function createGitHubRelease(options) {
|
|
178
|
+
const existing = await githubFetch(options, `/releases/tags/${encodeURIComponent(options.tag)}`, { method: "GET" });
|
|
179
|
+
let existingId;
|
|
180
|
+
if (existing.ok) existingId = parseReleaseId(await existing.json().catch(() => ({})));
|
|
181
|
+
else if (existing.status !== 404) {
|
|
182
|
+
const text = await existing.text().catch(() => "");
|
|
183
|
+
throw new ReleaseError("GITHUB_RELEASE_FAILED", `GitHub release lookup failed with ${existing.status}: ${text || existing.statusText}`);
|
|
184
|
+
}
|
|
185
|
+
const payload = JSON.stringify({
|
|
186
|
+
tag_name: options.tag,
|
|
187
|
+
name: options.name,
|
|
188
|
+
body: options.description
|
|
189
|
+
});
|
|
190
|
+
const response = existingId === void 0 ? await githubFetch(options, "/releases", {
|
|
191
|
+
method: "POST",
|
|
192
|
+
body: payload
|
|
193
|
+
}) : await githubFetch(options, `/releases/${existingId}`, {
|
|
194
|
+
method: "PATCH",
|
|
195
|
+
body: payload
|
|
196
|
+
});
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const text = await response.text().catch(() => "");
|
|
199
|
+
throw new ReleaseError("GITHUB_RELEASE_FAILED", `GitHub release creation failed with ${response.status}: ${text || response.statusText}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
102
203
|
//#region src/gitlab.ts
|
|
103
204
|
function releaseNotes(changelog, tag) {
|
|
104
205
|
const lines = changelog.split("\n");
|
|
@@ -112,12 +213,14 @@ function releaseNotes(changelog, tag) {
|
|
|
112
213
|
}
|
|
113
214
|
return notes.join("\n").trim() || "See CHANGELOG.md for release notes.";
|
|
114
215
|
}
|
|
216
|
+
const PROVIDER_FETCH_TIMEOUT_MS = 3e4;
|
|
115
217
|
async function createGitLabRelease(options) {
|
|
116
218
|
const url = `${options.host.replace(/\/$/, "")}/api/v4/projects/${encodeURIComponent(options.project)}/releases`;
|
|
117
219
|
let response;
|
|
118
220
|
try {
|
|
119
221
|
response = await fetch(url, {
|
|
120
222
|
method: "POST",
|
|
223
|
+
signal: AbortSignal.timeout(PROVIDER_FETCH_TIMEOUT_MS),
|
|
121
224
|
headers: {
|
|
122
225
|
Authorization: `Bearer ${options.token}`,
|
|
123
226
|
"Content-Type": "application/json"
|
|
@@ -184,11 +287,139 @@ async function findManifests(directory) {
|
|
|
184
287
|
function assertCurrent(path, actual, expected) {
|
|
185
288
|
if (actual !== expected) throw new ReleaseError("VERSION_MISMATCH", `${path} has version ${actual}; expected ${expected}.`);
|
|
186
289
|
}
|
|
290
|
+
function parseJsonString(raw) {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(raw);
|
|
293
|
+
return isString$1(parsed) ? parsed : void 0;
|
|
294
|
+
} catch {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function readJsonString(content, start) {
|
|
299
|
+
let index = start + 1;
|
|
300
|
+
let escape = false;
|
|
301
|
+
while (index < content.length) {
|
|
302
|
+
const char = content[index];
|
|
303
|
+
if (char === void 0) break;
|
|
304
|
+
if (escape) escape = false;
|
|
305
|
+
else if (char === "\\") escape = true;
|
|
306
|
+
else if (char === "\"") {
|
|
307
|
+
index += 1;
|
|
308
|
+
return {
|
|
309
|
+
end: index,
|
|
310
|
+
raw: content.slice(start, index)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
index += 1;
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
end: content.length,
|
|
317
|
+
raw: content.slice(start)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function objectKeyPath(stack, propertyKey) {
|
|
321
|
+
const path = [];
|
|
322
|
+
for (const frame of stack) if (frame.kind === "object" && frame.introKey !== void 0) path.push(frame.introKey);
|
|
323
|
+
path.push(propertyKey);
|
|
324
|
+
return path;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Locate a JSON string property by object-key path without reformatting the file.
|
|
328
|
+
* Nested keys with the same name are never selected.
|
|
329
|
+
*/
|
|
330
|
+
function findJsonStringProperty(content, keyPath) {
|
|
331
|
+
if (keyPath.length === 0) return void 0;
|
|
332
|
+
const stack = [];
|
|
333
|
+
let index = 0;
|
|
334
|
+
let pendingKey;
|
|
335
|
+
let afterColon = false;
|
|
336
|
+
const skipWhitespace = () => {
|
|
337
|
+
while (index < content.length && /\s/.test(content[index] ?? "")) index += 1;
|
|
338
|
+
};
|
|
339
|
+
while (index < content.length) {
|
|
340
|
+
skipWhitespace();
|
|
341
|
+
const char = content[index];
|
|
342
|
+
if (char === void 0) break;
|
|
343
|
+
if (char === "{") {
|
|
344
|
+
stack.push({
|
|
345
|
+
kind: "object",
|
|
346
|
+
introKey: pendingKey
|
|
347
|
+
});
|
|
348
|
+
pendingKey = void 0;
|
|
349
|
+
afterColon = false;
|
|
350
|
+
index += 1;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if (char === "}") {
|
|
354
|
+
stack.pop();
|
|
355
|
+
pendingKey = void 0;
|
|
356
|
+
afterColon = false;
|
|
357
|
+
index += 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (char === "[") {
|
|
361
|
+
stack.push({ kind: "array" });
|
|
362
|
+
pendingKey = void 0;
|
|
363
|
+
afterColon = false;
|
|
364
|
+
index += 1;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (char === "]") {
|
|
368
|
+
stack.pop();
|
|
369
|
+
pendingKey = void 0;
|
|
370
|
+
afterColon = false;
|
|
371
|
+
index += 1;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (char === ",") {
|
|
375
|
+
pendingKey = void 0;
|
|
376
|
+
afterColon = false;
|
|
377
|
+
index += 1;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (char === ":") {
|
|
381
|
+
afterColon = true;
|
|
382
|
+
index += 1;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (char === "\"") {
|
|
386
|
+
const { end, raw } = readJsonString(content, index);
|
|
387
|
+
index = end;
|
|
388
|
+
const decoded = parseJsonString(raw);
|
|
389
|
+
if (decoded === void 0) continue;
|
|
390
|
+
if (stack.at(-1)?.kind === "object" && !afterColon) {
|
|
391
|
+
pendingKey = decoded;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (afterColon && pendingKey !== void 0) {
|
|
395
|
+
const path = objectKeyPath(stack, pendingKey);
|
|
396
|
+
if (path.length === keyPath.length && path.every((segment, i) => segment === keyPath[i])) return {
|
|
397
|
+
value: decoded,
|
|
398
|
+
valueStart: end - raw.length + 1,
|
|
399
|
+
valueEnd: end - 1
|
|
400
|
+
};
|
|
401
|
+
pendingKey = void 0;
|
|
402
|
+
afterColon = false;
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (afterColon) {
|
|
407
|
+
pendingKey = void 0;
|
|
408
|
+
afterColon = false;
|
|
409
|
+
}
|
|
410
|
+
index += 1;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function replaceJsonStringProperty(content, span, value) {
|
|
414
|
+
return `${content.slice(0, span.valueStart)}${JSON.stringify(value).slice(1, -1)}${content.slice(span.valueEnd)}`;
|
|
415
|
+
}
|
|
187
416
|
function replaceJsonVersion(content, path, current, version) {
|
|
188
417
|
const parsed = JSON.parse(content);
|
|
189
418
|
if (!isObject$1(parsed) || !("version" in parsed) || !isString$1(parsed.version)) throw new ReleaseError("MISSING_VERSION", `${path} has no top-level version string.`);
|
|
190
419
|
assertCurrent(path, parsed.version, current);
|
|
191
|
-
const
|
|
420
|
+
const span = findJsonStringProperty(content, ["version"]);
|
|
421
|
+
if (span === void 0) throw new ReleaseError("MISSING_VERSION", `${path} has no top-level version string.`);
|
|
422
|
+
const updated = replaceJsonStringProperty(content, span, version);
|
|
192
423
|
if (updated === content) throw new ReleaseError("VERSION_UNCHANGED", `${path} was not updated.`);
|
|
193
424
|
return updated;
|
|
194
425
|
}
|
|
@@ -201,10 +432,20 @@ function replacePackageLockVersion(content, path, current, version) {
|
|
|
201
432
|
const rootVersion = data.packages?.[""]?.version;
|
|
202
433
|
if (rootVersion !== void 0 && !isString$1(rootVersion)) throw new ReleaseError("INVALID_LOCKFILE", `${path} has an invalid root package version.`);
|
|
203
434
|
if (isString$1(rootVersion)) assertCurrent(path, rootVersion, current);
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
435
|
+
const rootSpan = findJsonStringProperty(content, ["version"]);
|
|
436
|
+
if (rootSpan === void 0) throw new ReleaseError("MISSING_VERSION", `${path} has no root version.`);
|
|
437
|
+
let updated = replaceJsonStringProperty(content, rootSpan, version);
|
|
438
|
+
if (isString$1(rootVersion)) {
|
|
439
|
+
const packagesSpan = findJsonStringProperty(updated, [
|
|
440
|
+
"packages",
|
|
441
|
+
"",
|
|
442
|
+
"version"
|
|
443
|
+
]);
|
|
444
|
+
if (packagesSpan === void 0) throw new ReleaseError("MISSING_VERSION", `${path} has no packages[""].version string to update.`);
|
|
445
|
+
updated = replaceJsonStringProperty(updated, packagesSpan, version);
|
|
446
|
+
}
|
|
447
|
+
if (updated === content) throw new ReleaseError("VERSION_UNCHANGED", `${path} was not updated.`);
|
|
448
|
+
return updated;
|
|
208
449
|
}
|
|
209
450
|
function cargoSection(content) {
|
|
210
451
|
const start = content.search(/^\[package\]\s*$/m);
|
|
@@ -392,6 +633,23 @@ function gitLabContext(config) {
|
|
|
392
633
|
if (config.releaseName !== void 0) context.releaseName = config.releaseName;
|
|
393
634
|
return context;
|
|
394
635
|
}
|
|
636
|
+
function resolveGitHubContext(config, cwd) {
|
|
637
|
+
if (config?.enabled !== true) throw new ReleaseError("GITHUB_RELEASE_FAILED", "Enable github before creating a release.");
|
|
638
|
+
const token = resolveGitHubToken(config.tokenEnv);
|
|
639
|
+
if (token === void 0) throw new ReleaseError("GITHUB_RELEASE_FAILED", `Set ${config.tokenEnv ?? "GITHUB_TOKEN"} to create a GitHub release.`);
|
|
640
|
+
return resolveGitHubRepo(cwd, {
|
|
641
|
+
host: config.host,
|
|
642
|
+
repo: config.repo
|
|
643
|
+
}).then(({ host, repo }) => {
|
|
644
|
+
const context = {
|
|
645
|
+
host,
|
|
646
|
+
repo,
|
|
647
|
+
token
|
|
648
|
+
};
|
|
649
|
+
if (config.releaseName !== void 0) context.releaseName = config.releaseName;
|
|
650
|
+
return context;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
395
653
|
async function changelogText(cwd, config) {
|
|
396
654
|
if (config.changelog === false) return "";
|
|
397
655
|
const path = await resolveRepositoryPath(cwd, config.changelog === true ? "CHANGELOG.md" : config.changelog ?? "CHANGELOG.md");
|
|
@@ -407,6 +665,16 @@ async function publishGitLab(context, cwd, config, tag, version) {
|
|
|
407
665
|
description: releaseNotes(await changelogText(cwd, config), tag)
|
|
408
666
|
});
|
|
409
667
|
}
|
|
668
|
+
async function publishGitHub(context, cwd, config, tag, version) {
|
|
669
|
+
await createGitHubRelease({
|
|
670
|
+
host: context.host,
|
|
671
|
+
repo: context.repo,
|
|
672
|
+
token: context.token,
|
|
673
|
+
tag,
|
|
674
|
+
name: context.releaseName?.replaceAll("{{version}}", version) ?? tag,
|
|
675
|
+
description: releaseNotes(await changelogText(cwd, config), tag)
|
|
676
|
+
});
|
|
677
|
+
}
|
|
410
678
|
function filter(commits, config, exclude) {
|
|
411
679
|
return commits.filter((commit) => {
|
|
412
680
|
const type = config.types[commit.type.toLowerCase()];
|
|
@@ -469,6 +737,20 @@ async function runRelease(options) {
|
|
|
469
737
|
gitlabReleaseCreated: true
|
|
470
738
|
};
|
|
471
739
|
}
|
|
740
|
+
if (options.githubRetryTag !== void 0) {
|
|
741
|
+
const context = await resolveGitHubContext(config.github, cwd);
|
|
742
|
+
const remote = config.git?.remote ?? "origin";
|
|
743
|
+
if (!remoteTagExists(cwd, remote, options.githubRetryTag)) throw new ReleaseError("GITHUB_RELEASE_FAILED", `Tag ${options.githubRetryTag} does not exist on ${remote}.`);
|
|
744
|
+
await publishGitHub(context, cwd, config, options.githubRetryTag, currentVersion);
|
|
745
|
+
return {
|
|
746
|
+
currentVersion,
|
|
747
|
+
tag: options.githubRetryTag,
|
|
748
|
+
pushed: true,
|
|
749
|
+
dryRun: false,
|
|
750
|
+
commitCount: 0,
|
|
751
|
+
githubReleaseCreated: true
|
|
752
|
+
};
|
|
753
|
+
}
|
|
472
754
|
if (config.git?.requireClean !== false && git(["status", "--porcelain"], cwd) !== "") throw new ReleaseError("DIRTY_WORKTREE", "Commit or stash all changes before releasing.");
|
|
473
755
|
const branch = git(["branch", "--show-current"], cwd);
|
|
474
756
|
if (branch === "") throw new ReleaseError("DETACHED_HEAD", "Releases require a checked-out branch.");
|
|
@@ -489,11 +771,16 @@ async function runRelease(options) {
|
|
|
489
771
|
};
|
|
490
772
|
const releaseType = detected;
|
|
491
773
|
console.info(`Release: v${currentVersion} → ${releaseType} (${commits.length} commits)`);
|
|
774
|
+
const plannedVersion = bumpVersion(currentVersion, releaseType, config.preid);
|
|
775
|
+
const plannedTag = render(config.git?.tagName ?? "v{{version}}", plannedVersion);
|
|
492
776
|
if (options.dryRun) {
|
|
777
|
+
console.info(`Dry run: v${currentVersion} → v${plannedVersion} (${plannedTag})`);
|
|
493
778
|
console.info(await generateMarkDown(commits, changelog));
|
|
494
779
|
return {
|
|
495
780
|
currentVersion,
|
|
781
|
+
newVersion: plannedVersion,
|
|
496
782
|
releaseType,
|
|
783
|
+
tag: plannedTag,
|
|
497
784
|
pushed: false,
|
|
498
785
|
dryRun: true,
|
|
499
786
|
commitCount: commits.length
|
|
@@ -504,13 +791,17 @@ async function runRelease(options) {
|
|
|
504
791
|
if (!push) throw new ReleaseError("GITLAB_RELEASE_FAILED", "GitLab release creation requires git.push to be enabled.");
|
|
505
792
|
gitlab = gitLabContext(config.gitlab);
|
|
506
793
|
}
|
|
794
|
+
let github;
|
|
795
|
+
if (config.github?.enabled === true) {
|
|
796
|
+
if (!push) throw new ReleaseError("GITHUB_RELEASE_FAILED", "GitHub release creation requires git.push to be enabled.");
|
|
797
|
+
github = await resolveGitHubContext(config.github, cwd);
|
|
798
|
+
}
|
|
507
799
|
if (!options.yes && !await confirm(`Create a ${releaseType} release?`)) throw new ReleaseError("CANCELLED", "Release cancelled.");
|
|
508
|
-
const
|
|
509
|
-
const tag =
|
|
800
|
+
const version = plannedVersion;
|
|
801
|
+
const tag = plannedTag;
|
|
510
802
|
const remote = config.git?.remote ?? "origin";
|
|
511
803
|
if (tagExists(cwd, tag) || push && remoteTagExists(cwd, remote, tag)) throw new ReleaseError("TAG_EXISTS", `Tag ${tag} already exists.`);
|
|
512
804
|
for (const command of list(config.hooks?.before)) runHook(command, cwd);
|
|
513
|
-
const version = plannedVersion;
|
|
514
805
|
const changes = await planVersionChanges(cwd, currentVersion, version, config);
|
|
515
806
|
const changed = changes.map((change) => change.path);
|
|
516
807
|
let changelogSnapshot;
|
|
@@ -565,6 +856,13 @@ async function runRelease(options) {
|
|
|
565
856
|
} catch (error) {
|
|
566
857
|
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
858
|
}
|
|
859
|
+
let githubReleaseCreated = false;
|
|
860
|
+
if (github !== void 0) try {
|
|
861
|
+
await publishGitHub(github, cwd, config, tag, version);
|
|
862
|
+
githubReleaseCreated = true;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
throw new ReleaseError("RELEASE_PUBLISHED_GITHUB_FAILED", `Git release ${tag} was pushed, but GitHub release creation failed. Retry with: genbumppush --retry-github ${tag}`, { cause: error });
|
|
865
|
+
}
|
|
568
866
|
for (const command of list(config.hooks?.after)) runHook(command, cwd);
|
|
569
867
|
const result = {
|
|
570
868
|
currentVersion,
|
|
@@ -576,6 +874,7 @@ async function runRelease(options) {
|
|
|
576
874
|
commitCount: commits.length
|
|
577
875
|
};
|
|
578
876
|
if (gitlabReleaseCreated) result.gitlabReleaseCreated = true;
|
|
877
|
+
if (githubReleaseCreated) result.githubReleaseCreated = true;
|
|
579
878
|
return result;
|
|
580
879
|
}
|
|
581
880
|
//#endregion
|