@makispps/releasejet 1.0.0 → 1.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.
@@ -1,28 +1,28 @@
1
- # ReleaseJet Configuration
2
- # Copy to .releasejet.yml and customize for your project.
3
- # Or run: releasejet init
4
-
5
- # GitLab instance URL
6
- gitlab:
7
- url: https://gitlab.example.com
8
-
9
- # Client definitions (omit for single-client repos)
10
- # clients:
11
- # - prefix: mobile
12
- # label: MOBILE
13
- # - prefix: web
14
- # label: WEB
15
-
16
- # Category label mappings
17
- # Key: the label name in GitLab
18
- # Value: the section heading in release notes
19
- categories:
20
- feature: "New Features"
21
- bug: "Bug Fixes"
22
- improvement: "Improvements"
23
- breaking-change: "Breaking Changes"
24
-
25
- # How to handle uncategorized issues
26
- # "lenient" — include under "Other" with a warning (default)
27
- # "strict" — fail release generation
28
- uncategorized: lenient
1
+ # ReleaseJet Configuration
2
+ # Copy to .releasejet.yml and customize for your project.
3
+ # Or run: releasejet init
4
+
5
+ # GitLab instance URL
6
+ gitlab:
7
+ url: https://gitlab.example.com
8
+
9
+ # Client definitions (omit for single-client repos)
10
+ # clients:
11
+ # - prefix: mobile
12
+ # label: MOBILE
13
+ # - prefix: web
14
+ # label: WEB
15
+
16
+ # Category label mappings
17
+ # Key: the label name in GitLab
18
+ # Value: the section heading in release notes
19
+ categories:
20
+ feature: "New Features"
21
+ bug: "Bug Fixes"
22
+ improvement: "Improvements"
23
+ breaking-change: "Breaking Changes"
24
+
25
+ # How to handle uncategorized issues
26
+ # "lenient" — include under "Other" with a warning (default)
27
+ # "strict" — fail release generation
28
+ uncategorized: lenient
package/README.md CHANGED
@@ -1,145 +1,197 @@
1
- <img width="480" height="120" alt="lockup-light-1x" src="https://github.com/user-attachments/assets/1fd84e91-86f3-4f62-bad7-8bf4b72b517f" />
2
-
3
- Automated release notes generator for GitLab and GitHub. Collects closed issues (or merged pull requests) between Git tags, categorizes them by label, and publishes formatted release notes.
4
-
5
- ## Features
6
-
7
- - **GitLab and GitHub support** — works with both providers out of the box
8
- - **Issues or Pull Requests** — generate notes from closed issues (default) or merged PRs (GitHub)
9
- - **Multi-client repos** — filter by client label (e.g., `mobile-v1.0.0`, `web-v2.0.0`)
10
- - **Single-client repos** — just use `v<semver>` tags
11
- - **Configurable categories** — map labels to sections (features, bugs, improvements, etc.)
12
- - **CI/CD integration** — runs automatically on tag push via GitLab CI or GitHub Actions
13
- - **Strict/lenient modes** — enforce labeling or allow uncategorized issues under "Other"
14
- - **Milestone detection** — automatically links the most common milestone in release notes
15
-
16
- ## Quick Start
17
-
18
- ```bash
19
- npm install -g releasejet
20
-
21
- # Interactive setup (detects provider from git remote)
22
- releasejet init
23
-
24
- # Preview release notes
25
- releasejet generate --tag v1.0.0
26
-
27
- # Generate and publish
28
- releasejet generate --tag v1.0.0 --publish
29
- ```
30
-
31
- ## Configuration
32
-
33
- Create `.releasejet.yml` in your project root (or run `releasejet init`):
34
-
35
- ```yaml
36
- provider:
37
- type: github # 'gitlab' or 'github'
38
- url: https://github.com
39
-
40
- # GitHub-only: generate notes from issues or pull requests
41
- source: issues # 'issues' (default) or 'pull_requests'
42
-
43
- # For multi-client repos (omit for single-client)
44
- clients:
45
- - prefix: mobile
46
- label: MOBILE
47
- - prefix: web
48
- label: WEB
49
-
50
- categories:
51
- feature: "New Features"
52
- bug: "Bug Fixes"
53
- improvement: "Improvements"
54
- breaking-change: "Breaking Changes"
55
-
56
- uncategorized: lenient # or "strict" to enforce labeling
57
- ```
58
-
59
- ## CI/CD Integration
60
-
61
- ### GitHub Actions
62
-
63
- Run `releasejet init` and select CI setup, or add `.github/workflows/release-notes.yml`:
64
-
65
- ```yaml
66
- name: Release Notes
67
- on:
68
- push:
69
- tags:
70
- - '**'
71
- jobs:
72
- release-notes:
73
- runs-on: ubuntu-latest
74
- permissions:
75
- contents: write
76
- steps:
77
- - uses: actions/checkout@v4
78
- - uses: actions/setup-node@v4
79
- with:
80
- node-version: '20'
81
- - run: npm install -g releasejet
82
- - run: releasejet generate --tag "${{ github.ref_name }}" --publish
83
- env:
84
- RELEASEJET_TOKEN: ${{ secrets.RELEASEJET_TOKEN }}
85
- ```
86
-
87
- Set `RELEASEJET_TOKEN` as a repository secret (Settings > Secrets > Actions).
88
-
89
- ### GitLab CI
90
-
91
- Add to your `.gitlab-ci.yml` (or run `releasejet ci enable`):
92
-
93
- ```yaml
94
- release-notes:
95
- stage: deploy
96
- image: node:20-alpine
97
- rules:
98
- - if: $CI_COMMIT_TAG
99
- before_script:
100
- - npm install -g releasejet
101
- script:
102
- - releasejet generate --tag "$CI_COMMIT_TAG" --publish
103
- ```
104
-
105
- Set `GITLAB_API_TOKEN` (or `RELEASEJET_TOKEN`) as a CI/CD variable with `api` scope.
106
-
107
- ## Authentication
108
-
109
- Token resolution order:
110
-
111
- 1. `RELEASEJET_TOKEN` env var (works for both providers)
112
- 2. Provider-specific env var: `GITLAB_API_TOKEN` or `GITHUB_TOKEN`
113
- 3. Stored credentials from `~/.releasejet/credentials.yml`
114
-
115
- ## Tag Format
116
-
117
- | Repo type | Format | Example |
118
- |-----------|--------|---------|
119
- | Multi-client | `<prefix>-v<semver>` | `mobile-v1.2.0` |
120
- | Single-client | `v<semver>` | `v1.2.0` |
121
-
122
- ## Commands
123
-
124
- | Command | Description |
125
- |---------|-------------|
126
- | `releasejet init` | Interactive setup wizard |
127
- | `releasejet generate --tag <tag>` | Generate release notes |
128
- | `releasejet generate --tag <tag> --publish` | Generate and publish release |
129
- | `releasejet validate` | Check issues for proper labeling |
130
- | `releasejet ci enable` | Add CI configuration to `.gitlab-ci.yml` |
131
- | `releasejet ci disable` | Remove CI configuration |
132
-
133
- ### Generate Flags
134
-
135
- | Flag | Description |
136
- |------|-------------|
137
- | `--publish` | Publish as a release on the provider |
138
- | `--dry-run` | Preview without publishing |
139
- | `--format <format>` | Output format: `markdown` (default) or `json` |
140
- | `--config <path>` | Custom config file path |
141
- | `--debug` | Show debug information |
142
-
143
- ## License
144
-
145
- MIT
1
+ <img width="480" height="120" alt="lockup-light-1x" src="https://github.com/user-attachments/assets/1fd84e91-86f3-4f62-bad7-8bf4b72b517f" />
2
+
3
+ Automated release notes generator for GitLab and GitHub. Collects closed issues (or merged pull requests) between Git tags, categorizes them by label, and publishes formatted release notes.
4
+
5
+ ## Features
6
+
7
+ - **GitLab and GitHub support** — works with both providers out of the box
8
+ - **Issues or Pull Requests** — generate notes from closed issues (default) or merged PRs (GitHub)
9
+ - **Multi-client repos** — filter by client label (e.g., `mobile-v1.0.0`, `web-v2.0.0`)
10
+ - **Single-client repos** — just use `v<semver>` tags
11
+ - **Configurable categories** — map labels to sections (features, bugs, improvements, etc.)
12
+ - **CI/CD integration** — runs automatically on tag push via GitLab CI or GitHub Actions
13
+ - **Strict/lenient modes** — enforce labeling or allow uncategorized issues under "Other"
14
+ - **Milestone detection** — automatically links the most common milestone in release notes
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ npm install -g releasejet
20
+
21
+ # Interactive setup (detects provider from git remote)
22
+ releasejet init
23
+
24
+ # Preview release notes
25
+ releasejet generate --tag v1.0.0
26
+
27
+ # Generate and publish
28
+ releasejet generate --tag v1.0.0 --publish
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ Create `.releasejet.yml` in your project root (or run `releasejet init`):
34
+
35
+ ```yaml
36
+ provider:
37
+ type: github # 'gitlab' or 'github'
38
+ url: https://github.com
39
+
40
+ # GitHub-only: generate notes from issues or pull requests
41
+ source: issues # 'issues' (default) or 'pull_requests'
42
+
43
+ # For multi-client repos (omit for single-client)
44
+ clients:
45
+ - prefix: mobile
46
+ label: MOBILE
47
+ - prefix: web
48
+ label: WEB
49
+
50
+ categories:
51
+ feature: "New Features"
52
+ bug: "Bug Fixes"
53
+ improvement: "Improvements"
54
+ breaking-change: "Breaking Changes"
55
+
56
+ uncategorized: lenient # or "strict" to enforce labeling
57
+ ```
58
+
59
+ ## CI/CD Integration
60
+
61
+ ### GitHub Actions
62
+
63
+ Run `releasejet init` and select CI setup, or add `.github/workflows/release-notes.yml`:
64
+
65
+ ```yaml
66
+ name: Release Notes
67
+ on:
68
+ push:
69
+ tags:
70
+ - '**'
71
+ jobs:
72
+ release-notes:
73
+ runs-on: ubuntu-latest
74
+ permissions:
75
+ contents: write
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+ - uses: actions/setup-node@v4
79
+ with:
80
+ node-version: '20'
81
+ - run: npm install -g releasejet
82
+ - run: releasejet generate --tag "${{ github.ref_name }}" --publish
83
+ env:
84
+ RELEASEJET_TOKEN: ${{ secrets.RELEASEJET_TOKEN }}
85
+ ```
86
+
87
+ Set `RELEASEJET_TOKEN` as a repository secret (Settings > Secrets > Actions).
88
+
89
+ ### GitLab CI
90
+
91
+ Add to your `.gitlab-ci.yml` (or run `releasejet ci enable`):
92
+
93
+ ```yaml
94
+ release-notes:
95
+ stage: deploy
96
+ image: node:20-alpine
97
+ rules:
98
+ - if: $CI_COMMIT_TAG
99
+ before_script:
100
+ - npm install -g releasejet
101
+ script:
102
+ - releasejet generate --tag "$CI_COMMIT_TAG" --publish
103
+ ```
104
+
105
+ Set `GITLAB_API_TOKEN` (or `RELEASEJET_TOKEN`) as a CI/CD variable with `api` scope.
106
+
107
+ ## Authentication
108
+
109
+ Token resolution order:
110
+
111
+ 1. `RELEASEJET_TOKEN` env var (works for both providers)
112
+ 2. Provider-specific env var: `GITLAB_API_TOKEN` or `GITHUB_TOKEN`
113
+ 3. Stored credentials from `~/.releasejet/credentials.yml`
114
+
115
+ ## Tag Format
116
+
117
+ | Repo type | Format | Example |
118
+ |-----------|--------|---------|
119
+ | Multi-client | `<prefix>-v<semver>` | `mobile-v1.2.0` |
120
+ | Single-client | `v<semver>` | `v1.2.0` |
121
+
122
+ ## Commands
123
+
124
+ | Command | Description |
125
+ |---------|-------------|
126
+ | `releasejet init` | Interactive setup wizard |
127
+ | `releasejet generate --tag <tag>` | Generate release notes |
128
+ | `releasejet generate --tag <tag> --publish` | Generate and publish release |
129
+ | `releasejet validate` | Check issues for proper labeling |
130
+ | `releasejet ci enable` | Add CI configuration to `.gitlab-ci.yml` |
131
+ | `releasejet ci disable` | Remove CI configuration |
132
+
133
+ ### Generate Flags
134
+
135
+ | Flag | Description |
136
+ |------|-------------|
137
+ | `--publish` | Publish as a release on the provider |
138
+ | `--dry-run` | Preview without publishing |
139
+ | `--format <format>` | Output format: `markdown` (default) or `json` |
140
+ | `--config <path>` | Custom config file path |
141
+ | `--debug` | Show debug information |
142
+
143
+ ## Troubleshooting
144
+
145
+ ### "API token not found"
146
+
147
+ ReleaseJet checks three sources in order: `RELEASEJET_TOKEN` env var, provider-specific env var (`GITLAB_API_TOKEN` or `GITHUB_TOKEN`), and `~/.releasejet/credentials.yml`. Verify your token is set:
148
+
149
+ ```bash
150
+ echo $RELEASEJET_TOKEN
151
+ ```
152
+
153
+ To reconfigure, run `releasejet init`.
154
+
155
+ ### "Tag not found in remote repository"
156
+
157
+ The tag exists locally but hasn't been pushed. Push it first:
158
+
159
+ ```bash
160
+ git push origin <tag>
161
+ ```
162
+
163
+ ### "Invalid tag format"
164
+
165
+ Tags must match `v<semver>` (e.g., `v1.2.0`) or `<prefix>-v<semver>` (e.g., `mobile-v1.2.0`). Suffixes like `v1.2.0-beta` are supported but the core version must be valid semver.
166
+
167
+ ### Issues missing from release notes
168
+
169
+ ReleaseJet includes issues closed *between* the previous tag and the current tag (by `closedAt` timestamp, not `updatedAt`). Check that:
170
+
171
+ - The issue is **closed** (not just merged)
172
+ - The close date falls within the tag window
173
+ - The issue has the correct **client label** (multi-client repos)
174
+
175
+ Use `--debug` to see the date range and which issues were filtered.
176
+
177
+ ### "Request failed with status 401" or "403"
178
+
179
+ The API token doesn't have the required permissions. GitLab tokens need `api` scope. GitHub tokens need `repo` scope. Regenerate the token with the correct scope and update it via `releasejet init` or the `RELEASEJET_TOKEN` env var.
180
+
181
+ ### `source: pull_requests` not working
182
+
183
+ Pull request source is only supported for GitHub. GitLab projects must use `source: issues` (the default).
184
+
185
+ ### Config changes not taking effect
186
+
187
+ Run with `--debug` to see the loaded config:
188
+
189
+ ```bash
190
+ releasejet generate --tag <tag> --debug
191
+ ```
192
+
193
+ Invalid values (e.g., `uncategorized: "strictt"`) now produce clear error messages instead of being silently ignored.
194
+
195
+ ## License
196
+
197
+ MIT
@@ -1,19 +1,19 @@
1
- name: Release Notes
2
- on:
3
- push:
4
- tags:
5
- - '**'
6
- jobs:
7
- release-notes:
8
- runs-on: ubuntu-latest
9
- permissions:
10
- contents: write
11
- steps:
12
- - uses: actions/checkout@v4
13
- - uses: actions/setup-node@v4
14
- with:
15
- node-version: '20'
16
- - run: npm install -g releasejet
17
- - run: releasejet generate --tag "${{ github.ref_name }}" --publish
18
- env:
19
- RELEASEJET_TOKEN: ${{ secrets.RELEASEJET_TOKEN }}
1
+ name: Release Notes
2
+ on:
3
+ push:
4
+ tags:
5
+ - '**'
6
+ jobs:
7
+ release-notes:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-node@v4
14
+ with:
15
+ node-version: '20'
16
+ - run: npm install -g releasejet
17
+ - run: releasejet generate --tag "${{ github.ref_name }}" --publish
18
+ env:
19
+ RELEASEJET_TOKEN: ${{ secrets.RELEASEJET_TOKEN }}
@@ -1,22 +1,22 @@
1
- # ReleaseJet — Shared GitLab CI Template
2
- #
3
- # Include in your project's .gitlab-ci.yml:
4
- #
5
- # include:
6
- # - project: 'tools/releasejet'
7
- # file: '/ci/release-notes.yml'
8
- #
9
- # variables:
10
- # RELEASEJET_TOKEN: $GITLAB_API_TOKEN
11
-
12
- release-notes:
13
- stage: deploy
14
- image: node:20-alpine
15
- rules:
16
- - if: $CI_COMMIT_TAG
17
- variables:
18
- RELEASEJET_TOKEN: ${RELEASEJET_TOKEN}
19
- before_script:
20
- - npm install -g releasejet
21
- script:
22
- - releasejet generate --tag "$CI_COMMIT_TAG" --publish
1
+ # ReleaseJet — Shared GitLab CI Template
2
+ #
3
+ # Include in your project's .gitlab-ci.yml:
4
+ #
5
+ # include:
6
+ # - project: 'tools/releasejet'
7
+ # file: '/ci/release-notes.yml'
8
+ #
9
+ # variables:
10
+ # RELEASEJET_TOKEN: $GITLAB_API_TOKEN
11
+
12
+ release-notes:
13
+ stage: deploy
14
+ image: node:20-alpine
15
+ rules:
16
+ - if: $CI_COMMIT_TAG
17
+ variables:
18
+ RELEASEJET_TOKEN: ${RELEASEJET_TOKEN}
19
+ before_script:
20
+ - npm install -g releasejet
21
+ script:
22
+ - releasejet generate --tag "$CI_COMMIT_TAG" --publish
package/dist/cli.js CHANGED
@@ -37,17 +37,33 @@ async function loadConfig(configPath = ".releasejet.yml") {
37
37
  return mergeWithDefaults(raw);
38
38
  }
39
39
  function mergeWithDefaults(raw) {
40
- const clients = raw.clients;
41
- const categories = raw.categories;
42
- const uncategorized = raw.uncategorized;
43
- const source = raw.source;
44
40
  const providerRaw = raw.provider;
45
41
  const gitlabRaw = raw.gitlab;
42
+ const source = raw.source;
43
+ const uncategorized = raw.uncategorized;
44
+ const clientsRaw = raw.clients;
45
+ const categoriesRaw = raw.categories;
46
46
  let provider;
47
47
  if (providerRaw) {
48
+ const pType = providerRaw.type;
49
+ if (pType !== void 0 && pType !== "gitlab" && pType !== "github") {
50
+ throw new Error(
51
+ `Invalid config in .releasejet.yml
52
+
53
+ provider.type: "${pType}" is not valid. Expected "gitlab" or "github".`
54
+ );
55
+ }
56
+ const pUrl = providerRaw.url ?? "";
57
+ if (pUrl && !pUrl.startsWith("http://") && !pUrl.startsWith("https://")) {
58
+ throw new Error(
59
+ `Invalid config in .releasejet.yml
60
+
61
+ provider.url: "${pUrl}" is not valid. Must start with http:// or https://.`
62
+ );
63
+ }
48
64
  provider = {
49
- type: providerRaw.type === "github" ? "github" : "gitlab",
50
- url: providerRaw.url ?? ""
65
+ type: pType ?? "gitlab",
66
+ url: pUrl
51
67
  };
52
68
  } else if (gitlabRaw) {
53
69
  provider = {
@@ -57,12 +73,51 @@ function mergeWithDefaults(raw) {
57
73
  } else {
58
74
  provider = { type: "gitlab", url: "" };
59
75
  }
76
+ if (source !== void 0 && source !== "issues" && source !== "pull_requests") {
77
+ throw new Error(
78
+ `Invalid config in .releasejet.yml
79
+
80
+ source: "${source}" is not valid. Expected "issues" or "pull_requests".`
81
+ );
82
+ }
83
+ if (uncategorized !== void 0 && uncategorized !== "lenient" && uncategorized !== "strict") {
84
+ throw new Error(
85
+ `Invalid config in .releasejet.yml
86
+
87
+ uncategorized: "${uncategorized}" is not valid. Expected "lenient" or "strict".`
88
+ );
89
+ }
90
+ const clients = [];
91
+ if (Array.isArray(clientsRaw)) {
92
+ for (let i = 0; i < clientsRaw.length; i++) {
93
+ const entry = clientsRaw[i];
94
+ if (!entry?.prefix || !entry?.label) {
95
+ throw new Error(
96
+ `Invalid config in .releasejet.yml
97
+
98
+ clients[${i}]: "prefix" and "label" are required.`
99
+ );
100
+ }
101
+ clients.push({ prefix: entry.prefix, label: entry.label });
102
+ }
103
+ }
104
+ let categories;
105
+ if (categoriesRaw !== void 0) {
106
+ if (typeof categoriesRaw !== "object" || categoriesRaw === null || Array.isArray(categoriesRaw)) {
107
+ throw new Error(
108
+ "Invalid config in .releasejet.yml\n\n categories: expected an object mapping labels to headings."
109
+ );
110
+ }
111
+ categories = categoriesRaw;
112
+ } else {
113
+ categories = { ...DEFAULT_CATEGORIES };
114
+ }
60
115
  return {
61
116
  provider,
62
- source: source === "pull_requests" ? "pull_requests" : "issues",
63
- clients: Array.isArray(clients) ? clients : [],
64
- categories: categories ?? { ...DEFAULT_CATEGORIES },
65
- uncategorized: uncategorized === "strict" ? "strict" : "lenient"
117
+ source: source ?? "issues",
118
+ clients,
119
+ categories,
120
+ uncategorized: uncategorized ?? "lenient"
66
121
  };
67
122
  }
68
123
 
@@ -465,15 +520,55 @@ async function promptForUncategorized(issues, config) {
465
520
  }
466
521
  }
467
522
 
523
+ // src/cli/error-handler.ts
524
+ function withErrorHandler(fn) {
525
+ return async (...args) => {
526
+ try {
527
+ await fn(...args);
528
+ } catch (err) {
529
+ const message = err instanceof Error ? err.message : String(err);
530
+ const isDebug = process.argv.includes("--debug");
531
+ console.error(`
532
+ Error: ${message}`);
533
+ if (isDebug && err instanceof Error && err.stack) {
534
+ console.error(`
535
+ ${err.stack}`);
536
+ } else if (!isDebug) {
537
+ console.error("\n Re-run with --debug for more details.");
538
+ }
539
+ process.exitCode = 1;
540
+ }
541
+ };
542
+ }
543
+
544
+ // src/cli/logger.ts
545
+ function createLogger(enabled) {
546
+ return {
547
+ debug: enabled ? (...args) => console.error("[DEBUG]", ...args) : (() => {
548
+ })
549
+ };
550
+ }
551
+
468
552
  // src/cli/commands/generate.ts
553
+ import ora from "ora";
469
554
  function registerGenerateCommand(program2) {
470
- program2.command("generate").description("Generate release notes for a tag").requiredOption("--tag <tag>", "Git tag to generate release notes for").option("--publish", "Publish release", false).option("--dry-run", "Preview without publishing", false).option("--format <format>", "Output format (markdown|json)", "markdown").option("--config <path>", "Config file path", ".releasejet.yml").option("--debug", "Show debug information", false).action(async (options) => {
555
+ program2.command("generate").description("Generate release notes for a tag").requiredOption("--tag <tag>", "Git tag (e.g., v1.0.0 or mobile-v1.2.0)").option("--publish", "Publish release to provider", false).option("--dry-run", "Preview without publishing", false).option("--format <format>", "Output format (markdown|json)", "markdown").option("--config <path>", "Config file path", ".releasejet.yml").option("--debug", "Show debug information", false).addHelpText("after", `
556
+ Examples:
557
+ $ releasejet generate --tag v1.0.0 Preview release notes
558
+ $ releasejet generate --tag mobile-v2.1.0 Multi-client tag
559
+ $ releasejet generate --tag v1.0.0 --publish Publish to provider
560
+ $ releasejet generate --tag v1.0.0 --format json JSON output
561
+
562
+ Tag format:
563
+ Multi-client: <prefix>-v<semver> (e.g., mobile-v1.2.0)
564
+ Single-client: v<semver> (e.g., v1.2.0)
565
+ `).action(withErrorHandler(async (options) => {
471
566
  await runGenerate(options);
472
- });
567
+ }));
473
568
  }
474
569
  async function runGenerate(options) {
475
- const debug = options.debug ? (...args) => console.error("[DEBUG]", ...args) : () => {
476
- };
570
+ const { debug } = createLogger(options.debug ?? false);
571
+ const spinner = options.debug ? null : ora({ stream: process.stderr });
477
572
  const config = await loadConfig(options.config);
478
573
  debug("Config loaded:", JSON.stringify(config, null, 2));
479
574
  const remoteUrl = process.env.CI_SERVER_URL || process.env.GITHUB_SERVER_URL ? "" : getRemoteUrl();
@@ -485,7 +580,15 @@ async function runGenerate(options) {
485
580
  const client = createClient(config, token);
486
581
  const currentParsed = parseTag(options.tag);
487
582
  debug("Parsed tag:", JSON.stringify(currentParsed));
488
- const apiTags = await client.listTags(projectPath);
583
+ let apiTags;
584
+ try {
585
+ spinner?.start("Fetching tags...");
586
+ apiTags = await client.listTags(projectPath);
587
+ spinner?.succeed(`Fetched ${apiTags.length} tags`);
588
+ } catch (err) {
589
+ spinner?.fail("Failed to fetch tags");
590
+ throw err;
591
+ }
489
592
  debug("All remote tags:", apiTags.map((t) => `${t.name} (${t.createdAt})`).join(", "));
490
593
  const allTags = apiTags.map((t) => {
491
594
  try {
@@ -505,14 +608,24 @@ async function runGenerate(options) {
505
608
  const previousTag = findPreviousTag(allTags, currentTag);
506
609
  debug("Previous tag:", previousTag ? JSON.stringify(previousTag) : "none (first release)");
507
610
  debug("Date range:", previousTag?.createdAt ?? "beginning", "->", currentTag.createdAt);
508
- const issues = await collectIssues(
509
- client,
510
- projectPath,
511
- currentTag,
512
- previousTag,
513
- config,
514
- debug
515
- );
611
+ const sourceLabel = config.source === "pull_requests" ? "pull requests" : "issues";
612
+ let issues;
613
+ try {
614
+ spinner?.start(`Collecting ${sourceLabel}...`);
615
+ issues = await collectIssues(
616
+ client,
617
+ projectPath,
618
+ currentTag,
619
+ previousTag,
620
+ config,
621
+ debug
622
+ );
623
+ const issueCount = Object.values(issues.categorized).reduce((sum, arr) => sum + arr.length, 0) + issues.uncategorized.length;
624
+ spinner?.succeed(`Collected ${issueCount} ${sourceLabel}`);
625
+ } catch (err) {
626
+ spinner?.fail(`Failed to collect ${sourceLabel}`);
627
+ throw err;
628
+ }
516
629
  if (issues.uncategorized.length > 0) {
517
630
  if (process.stdin.isTTY) {
518
631
  await promptForUncategorized(issues, config);
@@ -548,37 +661,63 @@ async function runGenerate(options) {
548
661
  console.log(markdown);
549
662
  if (options.publish && !options.dryRun) {
550
663
  const releaseName = currentParsed.prefix ? `${currentParsed.prefix.toUpperCase()} v${currentParsed.version}` : `v${currentParsed.version}`;
551
- await client.createRelease(projectPath, {
552
- tagName: options.tag,
553
- name: releaseName,
554
- description: markdown,
555
- milestones: milestone ? [milestone] : void 0
556
- });
557
- console.log(`
558
- \u2713 Release published for ${options.tag}`);
664
+ try {
665
+ spinner?.start("Publishing release...");
666
+ await client.createRelease(projectPath, {
667
+ tagName: options.tag,
668
+ name: releaseName,
669
+ description: markdown,
670
+ milestones: milestone ? [milestone] : void 0
671
+ });
672
+ spinner?.succeed(`Release published for ${options.tag}`);
673
+ } catch (err) {
674
+ spinner?.fail("Failed to publish release");
675
+ throw err;
676
+ }
559
677
  }
560
678
  }
561
679
  }
562
680
 
563
681
  // src/cli/commands/validate.ts
682
+ import ora2 from "ora";
564
683
  function registerValidateCommand(program2) {
565
- program2.command("validate").description("Check open issues for proper labeling").option("--config <path>", "Config file path", ".releasejet.yml").action(async (options) => {
684
+ program2.command("validate").description("Check open issues for proper labeling").option("--config <path>", "Config file path", ".releasejet.yml").option("--debug", "Show debug information", false).addHelpText("after", `
685
+ Examples:
686
+ $ releasejet validate Check with default config
687
+ $ releasejet validate --config my.yml Check with custom config
688
+ `).action(withErrorHandler(async (options) => {
566
689
  await runValidate(options);
567
- });
690
+ }));
568
691
  }
569
692
  async function runValidate(options) {
693
+ const { debug } = createLogger(options.debug ?? false);
694
+ const spinner = options.debug ? null : ora2({ stream: process.stderr });
570
695
  const config = await loadConfig(options.config);
696
+ debug("Config loaded:", JSON.stringify(config, null, 2));
571
697
  const remoteUrl = getRemoteUrl();
572
698
  const hostUrl = config.provider.url || resolveHostUrl(remoteUrl);
573
699
  const projectPath = resolveProjectPath(remoteUrl);
700
+ debug("Host URL:", hostUrl);
701
+ debug("Project path:", projectPath);
574
702
  const token = await resolveToken(config.provider.type);
575
703
  const client = createClient(config, token);
576
- const issues = await client.listIssues(projectPath, {
577
- state: "opened"
578
- });
704
+ let issues;
705
+ try {
706
+ spinner?.start("Fetching open issues...");
707
+ issues = await client.listIssues(projectPath, {
708
+ state: "opened"
709
+ });
710
+ spinner?.succeed(`Fetched ${issues.length} open issues`);
711
+ } catch (err) {
712
+ spinner?.fail("Failed to fetch issues");
713
+ throw err;
714
+ }
715
+ debug("Fetched", issues.length, "open issues");
579
716
  const categoryLabels = Object.keys(config.categories);
580
717
  const clientLabels = config.clients.map((c) => c.label);
581
718
  const isMultiClient = clientLabels.length > 0;
719
+ debug("Category labels:", categoryLabels);
720
+ debug("Client labels:", clientLabels.length > 0 ? clientLabels : "none (single-client)");
582
721
  const problems = [];
583
722
  for (const issue of issues) {
584
723
  const missing = [];
@@ -589,6 +728,7 @@ async function runValidate(options) {
589
728
  missing.push("category label");
590
729
  }
591
730
  if (missing.length > 0) {
731
+ debug(` #${issue.number} "${issue.title}" labels=[${issue.labels.join(", ")}] missing=[${missing.join(", ")}]`);
592
732
  problems.push({ number: issue.number, title: issue.title, missing });
593
733
  }
594
734
  }
@@ -677,9 +817,12 @@ jobs:
677
817
  RELEASEJET_TOKEN: \${{ secrets.RELEASEJET_TOKEN }}
678
818
  `;
679
819
  function registerInitCommand(program2) {
680
- program2.command("init").description("Interactive setup for ReleaseJet").action(async () => {
820
+ program2.command("init").description("Interactive setup for ReleaseJet").addHelpText("after", `
821
+ Examples:
822
+ $ releasejet init Run the interactive setup wizard
823
+ `).action(withErrorHandler(async () => {
681
824
  await runInit();
682
- });
825
+ }));
683
826
  }
684
827
  async function runInit() {
685
828
  console.log("\u{1F680} ReleaseJet Setup\n");
@@ -901,22 +1044,32 @@ import { readFile as readFile4, writeFile as writeFile2, unlink } from "fs/promi
901
1044
  import { input as input2 } from "@inquirer/prompts";
902
1045
  var CI_FILE = ".gitlab-ci.yml";
903
1046
  function registerCiCommand(program2) {
904
- const ci = program2.command("ci").description("Manage GitLab CI/CD integration");
905
- ci.command("enable").description("Add ReleaseJet CI configuration to .gitlab-ci.yml").option("--tags <tags>", "Runner tags (comma-separated)").action(async (options) => {
1047
+ const ci = program2.command("ci").description("Manage GitLab CI/CD integration").addHelpText("after", `
1048
+ Examples:
1049
+ $ releasejet ci enable Interactive setup
1050
+ $ releasejet ci enable --tags ci,docker Non-interactive with tags
1051
+ $ releasejet ci disable Remove CI configuration
1052
+ `);
1053
+ ci.command("enable").description("Add ReleaseJet CI configuration to .gitlab-ci.yml").option("--tags <tags>", "Runner tags (comma-separated)").option("--debug", "Show debug information", false).action(withErrorHandler(async (options) => {
906
1054
  await runCiEnable(options);
907
- });
908
- ci.command("disable").description("Remove ReleaseJet CI configuration from .gitlab-ci.yml").action(async () => {
1055
+ }));
1056
+ ci.command("disable").description("Remove ReleaseJet CI configuration from .gitlab-ci.yml").action(withErrorHandler(async () => {
909
1057
  await runCiDisable();
910
- });
1058
+ }));
911
1059
  }
912
1060
  async function runCiEnable(options) {
1061
+ const { debug } = createLogger(options.debug ?? false);
913
1062
  let existing = "";
914
1063
  try {
915
1064
  existing = await readFile4(CI_FILE, "utf-8");
1065
+ debug("Existing .gitlab-ci.yml found, length:", existing.length);
916
1066
  } catch (err) {
917
1067
  if (err.code !== "ENOENT") throw err;
1068
+ debug(".gitlab-ci.yml not found, will create new file");
918
1069
  }
919
- if (hasCiBlock(existing)) {
1070
+ const hasMarkers = hasCiBlock(existing);
1071
+ debug("ReleaseJet markers found:", hasMarkers);
1072
+ if (hasMarkers) {
920
1073
  console.log("ReleaseJet CI is already enabled.");
921
1074
  return;
922
1075
  }
@@ -930,6 +1083,7 @@ async function runCiEnable(options) {
930
1083
  tags = tagsInput.trim() ? tagsInput.split(",").map((t) => t.trim()).filter(Boolean) : DEFAULT_TAGS;
931
1084
  }
932
1085
  const block = generateCiBlock(tags);
1086
+ debug("Generated CI block:\n" + block);
933
1087
  const content = appendCiBlock(existing, block);
934
1088
  await writeFile2(CI_FILE, content);
935
1089
  console.log("\u2713 ReleaseJet CI configuration added to .gitlab-ci.yml");
@@ -959,7 +1113,7 @@ async function runCiDisable() {
959
1113
 
960
1114
  // src/cli/index.ts
961
1115
  var program = new Command();
962
- program.name("releasejet").description("Automated GitLab release notes generator").version("1.0.0");
1116
+ program.name("releasejet").description("Automated release notes generator for GitLab and GitHub").version("1.0.2");
963
1117
  registerGenerateCommand(program);
964
1118
  registerValidateCommand(program);
965
1119
  registerInitCommand(program);
package/package.json CHANGED
@@ -1,59 +1,60 @@
1
- {
2
- "name": "@makispps/releasejet",
3
- "version": "1.0.0",
4
- "description": "Automated release notes generator for GitLab and GitHub",
5
- "license": "MIT",
6
- "author": "Mavroudis Papas",
7
- "homepage": "https://www.releasejet.dev/",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/makisp/releasejet.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/makisp/releasejet/issues"
14
- },
15
- "keywords": [
16
- "release-notes",
17
- "changelog",
18
- "gitlab",
19
- "github",
20
- "cli",
21
- "automation",
22
- "ci-cd"
23
- ],
24
- "type": "module",
25
- "bin": {
26
- "releasejet": "./dist/cli.js"
27
- },
28
- "files": [
29
- "dist",
30
- "ci",
31
- ".releasejet.example.yml"
32
- ],
33
- "scripts": {
34
- "build": "tsup",
35
- "test": "vitest run",
36
- "test:watch": "vitest",
37
- "dev": "tsx src/cli/index.ts",
38
- "prepublishOnly": "npm run test && npm run build"
39
- },
40
- "dependencies": {
41
- "@gitbeaker/rest": "^41.3.0",
42
- "@inquirer/prompts": "^7.2.0",
43
- "@octokit/rest": "^22.0.1",
44
- "commander": "^13.1.0",
45
- "semver": "^7.7.1",
46
- "yaml": "^2.7.0"
47
- },
48
- "devDependencies": {
49
- "@types/node": "^25.5.2",
50
- "@types/semver": "^7.5.8",
51
- "tsup": "^8.4.0",
52
- "tsx": "^4.19.0",
53
- "typescript": "^5.7.0",
54
- "vitest": "^3.1.0"
55
- },
56
- "engines": {
57
- "node": ">=20"
58
- }
59
- }
1
+ {
2
+ "name": "@makispps/releasejet",
3
+ "version": "1.0.2",
4
+ "description": "Automated release notes generator for GitLab and GitHub",
5
+ "license": "MIT",
6
+ "author": "Mavroudis Papas",
7
+ "homepage": "https://www.releasejet.dev/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/makisp/releasejet.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/makisp/releasejet/issues"
14
+ },
15
+ "keywords": [
16
+ "release-notes",
17
+ "changelog",
18
+ "gitlab",
19
+ "github",
20
+ "cli",
21
+ "automation",
22
+ "ci-cd"
23
+ ],
24
+ "type": "module",
25
+ "bin": {
26
+ "releasejet": "./dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "ci",
31
+ ".releasejet.example.yml"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
37
+ "dev": "tsx src/cli/index.ts",
38
+ "prepublishOnly": "npm run test && npm run build"
39
+ },
40
+ "dependencies": {
41
+ "@gitbeaker/rest": "^41.3.0",
42
+ "@inquirer/prompts": "^7.2.0",
43
+ "@octokit/rest": "^22.0.1",
44
+ "commander": "^13.1.0",
45
+ "ora": "^9.3.0",
46
+ "semver": "^7.7.1",
47
+ "yaml": "^2.7.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^25.5.2",
51
+ "@types/semver": "^7.5.8",
52
+ "tsup": "^8.4.0",
53
+ "tsx": "^4.19.0",
54
+ "typescript": "^5.7.0",
55
+ "vitest": "^3.1.0"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ }
60
+ }