create-app-release 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2025-02-12
9
+
10
+ ### Added
11
+
12
+ - Added support for multiple LLM providers:
13
+ - OpenAI models (default)
14
+ - Deepseek models
15
+ - QwenAI models
16
+ - Local LLM deployments
17
+ - New command-line options for LLM configuration:
18
+ - `--openai-key`: Set API key directly
19
+ - `--openai-model`: Choose model (e.g., gpt-4o, deepseek-r1, qwen2.5)
20
+ - `--openai-base-url`: Set custom API base URL for different providers
21
+ - Enhanced help information with detailed options and provider-specific examples
22
+
23
+ ## [1.0.2] - 2025-02-09
24
+
25
+ ### Changed
26
+
27
+ - Added smart filtering to exclude PRs already included in previous releases
28
+
8
29
  ## [1.0.1] - 2025-02-07
9
30
 
10
31
  ### Changed
package/README.md CHANGED
@@ -1,10 +1,18 @@
1
1
  # create-app-release
2
2
 
3
- An AI-powered GitHub release automation tool that helps you create release pull requests with automatically generated summaries using OpenAI's GPT-4 model. The tool intelligently groups your changes and creates professional release notes, making the release process smoother and more efficient.
3
+ [![NPM Version](https://img.shields.io/npm/v/create-app-release.svg)](https://www.npmjs.com/package/create-app-release)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ An AI-powered GitHub release automation tool that helps you create release pull requests with automatically generated summaries using various LLM providers. The tool intelligently groups your changes and creates professional release notes, making the release process smoother and more efficient.
4
7
 
5
8
  ## Features
6
9
 
7
10
  - 🤖 AI-powered release notes generation using GPT-4
11
+ - 🔄 Flexible LLM support:
12
+ - OpenAI models (GPT-4o, GPT-3.5-turbo)
13
+ - Deepseek models
14
+ - QwenAI models
15
+ - Local LLM deployments
8
16
  - 📦 Zero configuration - works right out of the box
9
17
  - 🔑 Secure token management through git config
10
18
  - 🎯 Interactive pull request selection
@@ -47,6 +55,38 @@ You'll need two tokens to use this tool:
47
55
  2. **OpenAI API Key** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
48
56
  - Will be stored in git config as `openai.token`
49
57
 
58
+ ### Command-Line Options
59
+
60
+ Customize the tool's behavior using these command-line options:
61
+
62
+ ```bash
63
+ # Set OpenAI API key directly (alternative to env/git config)
64
+ --openai-key <key>
65
+
66
+ # Choose OpenAI model (default: "gpt-4o")
67
+ --openai-model <model>
68
+ # Examples: gpt-4o, gpt-3.5-turbo, deepseek-r1, qwen2.5
69
+
70
+ # Set custom OpenAI API base URL
71
+ --openai-base-url <url>
72
+ # Examples:
73
+ # - Deepseek: https://api.deepseek.com/v1
74
+ # - QwenAI: https://api.qwen.ai/v1
75
+ # - Local: http://localhost:8000/v1
76
+ # - Custom: https://custom-openai-endpoint.com/v1
77
+
78
+ # Full example with different providers:
79
+
80
+ # Using Deepseek
81
+ npx create-app-release --openai-base-url https://api.deepseek.com/v1 --openai-key your_deepseek_key --openai-model deepseek-chat
82
+
83
+ # Using QwenAI
84
+ npx create-app-release --openai-base-url https://api.qwen.ai/v1 --openai-key your_qwen_key --openai-model qwen-14b-chat
85
+
86
+ # Using Local LLM
87
+ npx create-app-release --openai-base-url http://localhost:8000/v1 --openai-model local-model
88
+ ```
89
+
50
90
  ### Environment Variables (Optional)
51
91
 
52
92
  Tokens can also be provided via environment variables:
@@ -85,3 +125,7 @@ The tool generates professional release notes in this format:
85
125
  ## License
86
126
 
87
127
  MIT
128
+
129
+ ## Author
130
+
131
+ [James Gordo](https://github.com/jamesgordo)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-app-release",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered GitHub release automation tool",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -27,6 +27,14 @@
27
27
  ],
28
28
  "author": "James Gordo",
29
29
  "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/jamesgordo/create-app-release.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/jamesgordo/create-app-release/issues"
36
+ },
37
+ "homepage": "https://github.com/jamesgordo/create-app-release#readme",
30
38
  "lint-staged": {
31
39
  "*.{js,jsx,ts,tsx}": [
32
40
  "eslint --fix",
package/src/index.js CHANGED
@@ -18,6 +18,22 @@ const require = createRequire(import.meta.url);
18
18
  // Load environment variables
19
19
  config();
20
20
 
21
+ // Setup graceful exit handlers
22
+ process.stdin.setRawMode(true);
23
+ process.stdin.resume();
24
+ process.stdin.setEncoding('utf8');
25
+
26
+ // Display exit instructions
27
+ console.log(chalk.cyan('Press Ctrl+C or q to exit at any time'));
28
+
29
+ process.stdin.on('data', (key) => {
30
+ // Ctrl+C or 'q' to exit
31
+ if (key === '\u0003' || key.toLowerCase() === 'q') {
32
+ console.log(chalk.yellow('\nExiting gracefully...'));
33
+ process.exit(0);
34
+ }
35
+ });
36
+
21
37
  // Initialize CLI program
22
38
  const program = new Command();
23
39
  const pkg = require('../package.json');
@@ -106,17 +122,70 @@ async function initializeTokens() {
106
122
  return { githubToken, openaiToken };
107
123
  }
108
124
 
125
+ /**
126
+ * Get the latest release pull request
127
+ * @param {string} owner - Repository owner
128
+ * @param {string} repo - Repository name
129
+ * @returns {Promise<Object|null>} Latest release PR or null
130
+ */
131
+ async function getLatestReleasePR(owner, repo) {
132
+ try {
133
+ const iterator = octokit.paginate.iterator(octokit.rest.pulls.list, {
134
+ owner,
135
+ repo,
136
+ state: 'closed',
137
+ sort: 'updated',
138
+ direction: 'desc',
139
+ per_page: 100,
140
+ });
141
+
142
+ for await (const { data } of iterator) {
143
+ // SemVer regex pattern: matches X.Y.Z with optional pre-release and build metadata
144
+ const semverPattern =
145
+ /\b(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?\b/;
146
+
147
+ const recentReleasePR = data.find((pr) => semverPattern.test(pr.title));
148
+ if (recentReleasePR) {
149
+ return recentReleasePR;
150
+ }
151
+ }
152
+ return null;
153
+ } catch (error) {
154
+ console.error(chalk.red('Failed to fetch latest release PR:', error.message));
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Extract PR numbers from release PR description
161
+ * @param {string} description - PR description
162
+ * @returns {Set<number>} Set of PR numbers
163
+ */
164
+ function extractPRNumbersFromDescription(description) {
165
+ if (!description) return new Set();
166
+
167
+ // Match PR numbers in various formats like #123, (#123), or just plain 123 in PR lists
168
+ const prMatches = description.match(/#\d+|\(#\d+\)|(?<=PR:?\s*)\d+/g) || [];
169
+
170
+ return new Set(prMatches.map((match) => parseInt(match.replace(/[^0-9]/g, ''))));
171
+ }
172
+
109
173
  /**
110
174
  * Fetch closed pull requests from the repository
111
175
  * @param {string} owner - Repository owner
112
176
  * @param {string} repo - Repository name
177
+ * @param {string} targetBranch - Target branch name
113
178
  * @returns {Promise<Array>} List of pull requests
114
179
  */
115
180
  async function fetchPullRequests(owner, repo) {
116
181
  const spinner = ora('Fetching pull requests...').start();
117
182
  try {
183
+ // Get the latest release PR first
184
+ const latestReleasePR = await getLatestReleasePR(owner, repo);
185
+
186
+ const includedPRNumbers = extractPRNumbersFromDescription(latestReleasePR?.body);
118
187
  const pulls = [];
119
- const iterator = octokit.paginate.iterator(octokit.pulls.list, {
188
+ const iterator = octokit.paginate.iterator(octokit.rest.pulls.list, {
120
189
  owner,
121
190
  repo,
122
191
  state: 'closed',
@@ -126,9 +195,29 @@ async function fetchPullRequests(owner, repo) {
126
195
  });
127
196
 
128
197
  for await (const { data } of iterator) {
129
- pulls.push(...data);
198
+ // Filter PRs that are merged after the last release and not included in it
199
+ const relevantPRs = data.filter((pr) => {
200
+ if (!pr.merged_at) return false;
201
+
202
+ const isAfterLastRelease = latestReleasePR
203
+ ? new Date(pr.merged_at) >= new Date(latestReleasePR.merged_at)
204
+ : true;
205
+
206
+ return (
207
+ isAfterLastRelease &&
208
+ !includedPRNumbers.has(`#${pr.number}`) &&
209
+ pr.id !== latestReleasePR?.id
210
+ );
211
+ });
212
+ pulls.push(...relevantPRs);
130
213
  }
131
- spinner.succeed(`Found ${pulls.length} pull requests`);
214
+
215
+ const excludedCount = includedPRNumbers.size;
216
+ const message = latestReleasePR
217
+ ? `Found ${pulls.length} new merged pull requests (excluding ${excludedCount} PRs from last release)`
218
+ : `Found ${pulls.length} merged pull requests`;
219
+
220
+ spinner.succeed(message);
132
221
  return pulls;
133
222
  } catch (error) {
134
223
  spinner.fail('Failed to fetch pull requests');
@@ -167,8 +256,9 @@ ${JSON.stringify(prDetails, null, 2)}
167
256
 
168
257
  Keep the summary concise, clear, and focused on the user impact. Use professional but easy-to-understand language.`;
169
258
 
259
+ const model = program.opts().openaiModel || 'gpt-4o';
170
260
  const response = await openai.chat.completions.create({
171
- model: 'gpt-4o',
261
+ model,
172
262
  messages: [{ role: 'user', content: prompt }],
173
263
  temperature: 0.7,
174
264
  });
@@ -268,8 +358,18 @@ ${summary}`;
268
358
  }
269
359
 
270
360
  async function run() {
271
- // Initialize tokens sequentially
272
- const { githubToken, openaiToken } = await initializeTokens();
361
+ // Get command line options
362
+ const options = program.opts();
363
+
364
+ // Initialize GitHub token
365
+ const { githubToken } = await initializeTokens();
366
+
367
+ // Get OpenAI token from command line or fallback to configuration
368
+ let openaiToken = options.openaiKey;
369
+ if (!openaiToken) {
370
+ const tokens = await initializeTokens();
371
+ openaiToken = tokens.openaiToken;
372
+ }
273
373
 
274
374
  // Initialize clients with tokens
275
375
  octokit = new Octokit({
@@ -278,6 +378,7 @@ async function run() {
278
378
 
279
379
  openai = new OpenAI({
280
380
  apiKey: openaiToken,
381
+ baseURL: options.openaiBaseUrl,
281
382
  });
282
383
 
283
384
  const { owner, repo } = await inquirer.prompt([
@@ -386,9 +487,30 @@ async function run() {
386
487
  }
387
488
  }
388
489
 
490
+ const description = `AI-powered GitHub release automation tool
491
+
492
+ Options:
493
+ --openai-key <key> Set OpenAI API key directly (alternative to env/git config)
494
+ --openai-model <model> Set OpenAI model to use (default: "gpt-4")
495
+ Examples: gpt-4, gpt-3.5-turbo
496
+ --openai-base-url <url> Set custom OpenAI API base URL
497
+ Example: https://custom-openai-endpoint.com/v1
498
+
499
+ Environment Variables:
500
+ GITHUB_TOKEN GitHub personal access token
501
+ OPENAI_API_KEY OpenAI API key (if not using --openai-key)
502
+
503
+ Git Config:
504
+ github.token GitHub token in git config
505
+ openai.token OpenAI token in git config (if not using --openai-key)
506
+ `;
507
+
389
508
  program
390
509
  .name('create-app-release')
391
- .description('AI-powered GitHub release automation tool')
510
+ .description(description)
392
511
  .version(pkg.version)
512
+ .option('--openai-base-url <url>', 'Set custom OpenAI API base URL')
513
+ .option('--openai-model <model>', 'Set OpenAI model to use (default: "gpt-4")')
514
+ .option('--openai-key <key>', 'Set OpenAI API key directly (alternative to env/git config)')
393
515
  .action(run)
394
516
  .parse(process.argv);