create-app-release 1.0.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/.eslintignore ADDED
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ coverage
5
+ .next
package/.eslintrc.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "es2021": true
5
+ },
6
+ "extends": ["eslint:recommended", "plugin:prettier/recommended"],
7
+ "parserOptions": {
8
+ "ecmaVersion": "latest",
9
+ "sourceType": "module"
10
+ },
11
+ "rules": {
12
+ "prettier/prettier": "error"
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env sh
2
+ . "$(dirname -- "$0")/_/husky.sh"
3
+
4
+ npx lint-staged
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ dist
3
+ build
4
+ coverage
5
+ .next
6
+ package-lock.json
package/.prettierrc ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "semi": true,
3
+ "tabWidth": 2,
4
+ "printWidth": 100,
5
+ "singleQuote": true,
6
+ "trailingComma": "es5",
7
+ "bracketSpacing": true
8
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2025-02-06
9
+
10
+ ### Added
11
+
12
+ - Initial release of `create-app-release`
13
+ - AI-powered release notes generation using GPT-4
14
+ - Interactive pull request selection
15
+ - GitHub token management through git config
16
+ - OpenAI API key management through git config
17
+ - Automatic categorization of changes (Features, Bug Fixes, Improvements)
18
+ - Professional markdown formatting for release notes
19
+ - Support for environment variables (`GITHUB_TOKEN` and `OPENAI_API_KEY`)
20
+ - Command-line interface using Commander.js
21
+ - Progress indicators and colorful console output
22
+ - Error handling and user-friendly messages
23
+ - Support for Node.js >= 14.0.0
24
+
25
+ ### Dependencies
26
+
27
+ - @octokit/rest - GitHub API client
28
+ - openai - OpenAI API client
29
+ - commander - Command-line interface
30
+ - inquirer - Interactive prompts
31
+ - chalk - Terminal styling
32
+ - ora - Terminal spinners
33
+ - dotenv - Environment variable management
34
+
35
+ [1.0.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.0.0
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # create-app-release
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.
4
+
5
+ ## Features
6
+
7
+ - 🤖 AI-powered release notes generation using GPT-4
8
+ - 📦 Zero configuration - works right out of the box
9
+ - 🔑 Secure token management through git config
10
+ - 🎯 Interactive pull request selection
11
+ - ✨ Professional markdown formatting
12
+ - 📝 Smart categorization of changes
13
+ - 🌟 User-friendly CLI interface
14
+
15
+ ## Prerequisites
16
+
17
+ - Node.js 14 or higher
18
+ - Git installed and configured
19
+ - GitHub account with repository access
20
+ - OpenAI account (for GPT-4 access)
21
+
22
+ ## Usage
23
+
24
+ Run the tool directly using npx:
25
+
26
+ ```bash
27
+ npx create-app-release
28
+ ```
29
+
30
+ On first run, the tool will guide you through:
31
+
32
+ 1. Setting up your GitHub token (stored in git config)
33
+ 2. Configuring your OpenAI API key (stored in git config)
34
+ 3. Selecting pull requests for the release
35
+ 4. Reviewing the AI-generated summary
36
+ 5. Creating the release pull request
37
+
38
+ ### Token Setup
39
+
40
+ You'll need two tokens to use this tool:
41
+
42
+ 1. **GitHub Token** - Create at [GitHub Token Settings](https://github.com/settings/tokens/new)
43
+
44
+ - Required scope: `repo`
45
+ - Will be stored in git config as `github.token`
46
+
47
+ 2. **OpenAI API Key** - Get from [OpenAI Platform](https://platform.openai.com/api-keys)
48
+ - Will be stored in git config as `openai.token`
49
+
50
+ ### Environment Variables (Optional)
51
+
52
+ Tokens can also be provided via environment variables:
53
+
54
+ ```bash
55
+ GITHUB_TOKEN=your_github_token
56
+ OPENAI_API_KEY=your_openai_api_key
57
+ ```
58
+
59
+ ## Example Output
60
+
61
+ The tool generates professional release notes in this format:
62
+
63
+ ```markdown
64
+ ### 🚀 Features
65
+
66
+ - Enhanced user authentication system
67
+ - New dashboard analytics
68
+
69
+ ### 🐛 Bug Fixes
70
+
71
+ - Fixed memory leak in background tasks
72
+ - Resolved login issues on Safari
73
+
74
+ ### 🔧 Improvements
75
+
76
+ - Optimized database queries
77
+ - Updated dependencies
78
+
79
+ ### Pull Requests
80
+
81
+ #123 - Add user authentication by [@username](https://github.com/username) (2024-02-01)
82
+ #124 - Fix memory leak by [@dev](https://github.com/dev) (2024-02-02)
83
+ ```
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Suppress the punycode deprecation warning
4
+ process.removeAllListeners('warning');
5
+
6
+ // Import and run the main script
7
+ import('../src/index.js');
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "create-app-release",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered GitHub release automation tool",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-app-release": "./bin/create-app-release"
9
+ },
10
+ "engines": {
11
+ "node": ">=14.0.0"
12
+ },
13
+ "scripts": {
14
+ "start": "node src/index.js",
15
+ "lint": "eslint .",
16
+ "lint:fix": "eslint . --fix",
17
+ "format": "prettier --write .",
18
+ "format:check": "prettier --check .",
19
+ "prepare": "husky"
20
+ },
21
+ "keywords": [
22
+ "github",
23
+ "release",
24
+ "automation",
25
+ "ai",
26
+ "openai"
27
+ ],
28
+ "author": "James Gordo",
29
+ "license": "MIT",
30
+ "lint-staged": {
31
+ "*.{js,jsx,ts,tsx}": [
32
+ "eslint --fix",
33
+ "prettier --write"
34
+ ],
35
+ "*.{json,md,yml,yaml}": [
36
+ "prettier --write"
37
+ ]
38
+ },
39
+ "dependencies": {
40
+ "@octokit/rest": "^20.0.2",
41
+ "chalk": "^5.3.0",
42
+ "commander": "^11.1.0",
43
+ "dotenv": "^16.3.1",
44
+ "inquirer": "^9.2.12",
45
+ "openai": "^4.24.1",
46
+ "ora": "^7.0.1"
47
+ },
48
+ "devDependencies": {
49
+ "eslint": "^8.56.0",
50
+ "eslint-config-prettier": "^9.1.0",
51
+ "eslint-plugin-prettier": "^5.1.3",
52
+ "husky": "^9.1.7",
53
+ "lint-staged": "^15.4.3",
54
+ "prettier": "^3.2.5"
55
+ },
56
+ "overrides": {
57
+ "uri-js": {
58
+ "punycode": "^2.3.1"
59
+ }
60
+ }
61
+ }
package/src/index.js ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { config } from 'dotenv';
5
+ import { Octokit } from '@octokit/rest';
6
+ import inquirer from 'inquirer';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+ import OpenAI from 'openai';
10
+ import { promisify } from 'util';
11
+ import { exec as execCallback } from 'child_process';
12
+ import { createRequire } from 'module';
13
+
14
+ // Initialize utilities
15
+ const exec = promisify(execCallback);
16
+ const require = createRequire(import.meta.url);
17
+
18
+ // Load environment variables
19
+ config();
20
+
21
+ // Initialize CLI program
22
+ const program = new Command();
23
+ const pkg = require('../package.json');
24
+
25
+ /**
26
+ * Get token from git config
27
+ * @param {string} key - Git config key
28
+ * @returns {Promise<string|null>} Token value or null if not found
29
+ */
30
+ async function getGitConfigToken(key) {
31
+ try {
32
+ const { stdout } = await exec(`git config --global ${key}`);
33
+ return stdout.trim() || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Configure and get API token with support for environment variables, git config, and user input
41
+ * @param {Object} config - Token configuration object
42
+ * @param {string} config.envKey - Environment variable key
43
+ * @param {string} config.gitKey - Git config key
44
+ * @param {string} config.name - Service name (e.g., 'GitHub', 'OpenAI')
45
+ * @param {string} config.createUrl - URL where users can create new tokens
46
+ * @param {string} [config.additionalInfo] - Additional information to display
47
+ * @returns {Promise<string>} The configured token
48
+ */
49
+ async function configureToken({ envKey, gitKey, name, createUrl, additionalInfo = '' }) {
50
+ const token = process.env[envKey] || (await getGitConfigToken(gitKey));
51
+
52
+ if (token) return token;
53
+
54
+ console.log(
55
+ chalk.yellow(`\nNo ${name} token found. Let's set one up.\n`) +
56
+ chalk.cyan(`Create a new token at: ${createUrl}`)
57
+ );
58
+
59
+ if (additionalInfo) {
60
+ console.log(chalk.cyan(additionalInfo));
61
+ }
62
+
63
+ const { newToken } = await inquirer.prompt([
64
+ {
65
+ type: 'password',
66
+ name: 'newToken',
67
+ message: `Enter your ${name} token:`,
68
+ validate: (input) => input.length > 0 || 'Token is required',
69
+ },
70
+ ]);
71
+
72
+ try {
73
+ await exec(`git config --global ${gitKey} "${newToken}"`);
74
+ console.log(chalk.green(`${name} token saved successfully!`));
75
+ return newToken;
76
+ } catch (error) {
77
+ console.error(chalk.red(`Failed to save ${name} token:`), error.message);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ // Initialize API clients
83
+ let octokit;
84
+ let openai;
85
+
86
+ /**
87
+ * Initialize GitHub and OpenAI tokens
88
+ * @returns {Promise<Object>} Object containing both tokens
89
+ */
90
+ async function initializeTokens() {
91
+ const githubToken = await configureToken({
92
+ envKey: 'GITHUB_TOKEN',
93
+ gitKey: 'github.token',
94
+ name: 'GitHub',
95
+ createUrl: 'https://github.com/settings/tokens/new',
96
+ additionalInfo: "Make sure to enable the 'repo' scope.",
97
+ });
98
+
99
+ const openaiToken = await configureToken({
100
+ envKey: 'OPENAI_API_KEY',
101
+ gitKey: 'openai.token',
102
+ name: 'OpenAI',
103
+ createUrl: 'https://platform.openai.com/api-keys',
104
+ });
105
+
106
+ return { githubToken, openaiToken };
107
+ }
108
+
109
+ /**
110
+ * Fetch closed pull requests from the repository
111
+ * @param {string} owner - Repository owner
112
+ * @param {string} repo - Repository name
113
+ * @returns {Promise<Array>} List of pull requests
114
+ */
115
+ async function fetchPullRequests(owner, repo) {
116
+ const spinner = ora('Fetching pull requests...').start();
117
+ try {
118
+ const { data: pulls } = await octokit.pulls.list({
119
+ owner,
120
+ repo,
121
+ state: 'closed',
122
+ sort: 'updated',
123
+ direction: 'desc',
124
+ per_page: 30,
125
+ });
126
+ spinner.succeed(`Found ${pulls.length} pull requests`);
127
+ return pulls;
128
+ } catch (error) {
129
+ spinner.fail('Failed to fetch pull requests');
130
+ console.error(chalk.red(`Error: ${error.message}`));
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Generate an AI-powered release summary from selected pull requests
137
+ * @param {Array} selectedPRs - List of selected pull requests
138
+ * @returns {Promise<string>} Generated release summary
139
+ */
140
+ async function generateSummary(selectedPRs) {
141
+ const spinner = ora('Generating release summary...').start();
142
+ try {
143
+ const prDetails = selectedPRs.map((pr) => ({
144
+ number: pr.number,
145
+ title: pr.title,
146
+ author: pr.user.login,
147
+ authorUrl: `https://github.com/${pr.user.login}`,
148
+ date: new Date(pr.created_at).toLocaleDateString(),
149
+ url: pr.html_url,
150
+ }));
151
+
152
+ const prompt = `Create a release summary for the following pull requests. The summary should have two parts:
153
+
154
+ 1. Group the changes by type (e.g., Features, Bug Fixes, Improvements) and list them down in bullet points.
155
+ 1.1 Make each type is an h3 header with a corresponding emoji prefix.
156
+ 1.2 For each type, make each bullet point concise and easy to read and understand for non-tech people.
157
+ 1.3 Don't link the bullet points to a pull requests
158
+ 2. The last section should be a list of pull requests included in the release. Format: "#<number> - <title> by [@<author>](<authorUrl>) (<date>)".
159
+
160
+ Pull Requests to summarize:
161
+ ${JSON.stringify(prDetails, null, 2)}
162
+
163
+ Keep the summary concise, clear, and focused on the user impact. Use professional but easy-to-understand language.`;
164
+
165
+ const response = await openai.chat.completions.create({
166
+ model: 'gpt-4o',
167
+ messages: [{ role: 'user', content: prompt }],
168
+ temperature: 0.7,
169
+ });
170
+
171
+ // Validate response structure
172
+ if (!response?.choices?.length || !response.choices[0]?.message?.content) {
173
+ throw new Error(
174
+ 'Invalid API response structure. Expected response.choices[0].message.content'
175
+ );
176
+ }
177
+
178
+ spinner.succeed('Summary generated successfully');
179
+ return response.choices[0].message.content;
180
+ } catch (error) {
181
+ spinner.fail('Failed to generate summary');
182
+
183
+ // Handle specific API response errors
184
+ if (error.message.includes('Invalid API response')) {
185
+ console.error(
186
+ chalk.red('Error: The AI service returned an unexpected response format.\n') +
187
+ chalk.yellow('This might be due to:') +
188
+ '\n- Service temporarily unavailable' +
189
+ '\n- Rate limiting' +
190
+ '\n- Model configuration issues\n' +
191
+ chalk.cyan('Please try again in a few moments.')
192
+ );
193
+ } else {
194
+ console.error(chalk.red(`Error: ${error.message}`));
195
+ }
196
+
197
+ // Provide fallback option
198
+ const { useFallback } = await inquirer.prompt([
199
+ {
200
+ type: 'confirm',
201
+ name: 'useFallback',
202
+ message: 'Would you like to use a simple list format instead?',
203
+ default: true,
204
+ },
205
+ ]);
206
+
207
+ if (useFallback) {
208
+ return selectedPRs
209
+ .map((pr) => {
210
+ const date = new Date(pr.created_at).toLocaleDateString();
211
+ return `#${pr.number} - ${pr.title} (by [@${pr.user.login}](https://github.com/${pr.user.login}) on ${date})`;
212
+ })
213
+ .join('\n');
214
+ }
215
+
216
+ process.exit(1);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Create a release pull request with the generated summary
222
+ * @param {string} owner - Repository owner
223
+ * @param {string} repo - Repository name
224
+ * @param {string} summary - Generated release summary
225
+ * @param {Array} selectedPRs - List of selected pull requests
226
+ * @param {string} sourceBranch - Source branch name
227
+ * @param {string} targetBranch - Target branch name
228
+ * @param {string} version - Release version
229
+ * @returns {Promise<Object>} Created pull request data
230
+ */
231
+ async function createReleasePR(
232
+ owner,
233
+ repo,
234
+ summary,
235
+ selectedPRs,
236
+ sourceBranch,
237
+ targetBranch,
238
+ version
239
+ ) {
240
+ const spinner = ora('Creating release PR...').start();
241
+ try {
242
+ const body = `# Release Summary
243
+
244
+ ${summary}`;
245
+
246
+ const { data: pr } = await octokit.pulls.create({
247
+ owner,
248
+ repo,
249
+ title: `Release: Version ${version}`,
250
+ head: sourceBranch,
251
+ base: targetBranch,
252
+ body,
253
+ draft: true,
254
+ });
255
+
256
+ spinner.succeed(`Release PR #${pr.number} created successfully`);
257
+ return pr;
258
+ } catch (error) {
259
+ spinner.fail('Failed to create release PR');
260
+ console.error(chalk.red(`Error: ${error.message}`));
261
+ process.exit(1);
262
+ }
263
+ }
264
+
265
+ async function run() {
266
+ // Initialize tokens sequentially
267
+ const { githubToken, openaiToken } = await initializeTokens();
268
+
269
+ // Initialize clients with tokens
270
+ octokit = new Octokit({
271
+ auth: githubToken,
272
+ });
273
+
274
+ openai = new OpenAI({
275
+ apiKey: openaiToken,
276
+ });
277
+
278
+ const { owner, repo } = await inquirer.prompt([
279
+ {
280
+ type: 'input',
281
+ name: 'owner',
282
+ message: 'Enter repository owner:',
283
+ validate: (input) => input.length > 0,
284
+ },
285
+ {
286
+ type: 'input',
287
+ name: 'repo',
288
+ message: 'Enter repository name:',
289
+ validate: (input) => input.length > 0,
290
+ },
291
+ ]);
292
+
293
+ const pulls = await fetchPullRequests(owner, repo);
294
+
295
+ const { selectedPRs } = await inquirer.prompt([
296
+ {
297
+ type: 'checkbox',
298
+ name: 'selectedPRs',
299
+ message: 'Select pull requests to include in the release:',
300
+ choices: pulls.map((pr) => ({
301
+ name: `#${pr.number} - ${pr.title}`,
302
+ value: pr,
303
+ })),
304
+ validate: (input) => input.length > 0,
305
+ },
306
+ ]);
307
+
308
+ const { summaryType } = await inquirer.prompt([
309
+ {
310
+ type: 'list',
311
+ name: 'summaryType',
312
+ message: 'How would you like to summarize the pull requests?',
313
+ choices: [
314
+ { name: 'Use AI to generate a summary', value: 'ai' },
315
+ { name: 'Simply list the selected pull requests', value: 'list' },
316
+ ],
317
+ },
318
+ ]);
319
+
320
+ let summary;
321
+ if (summaryType === 'ai') {
322
+ summary = await generateSummary(selectedPRs);
323
+ } else {
324
+ summary = selectedPRs
325
+ .map((pr) => {
326
+ const date = new Date(pr.created_at).toLocaleDateString();
327
+ return `#${pr.number} - ${pr.title} (by [@${pr.user.login}](https://github.com/${pr.user.login}) on ${date})`;
328
+ })
329
+ .join('\n');
330
+ }
331
+
332
+ console.log(chalk.cyan('\nSummary:'));
333
+ console.log(summary);
334
+
335
+ const { version, confirm, sourceBranch, targetBranch } = await inquirer.prompt([
336
+ {
337
+ type: 'input',
338
+ name: 'version',
339
+ message: 'Enter the version number for this release (e.g., 1.2.3):',
340
+ validate: (input) => {
341
+ // Validate semantic versioning format (x.y.z)
342
+ const semverRegex = /^\d+\.\d+\.\d+$/;
343
+ if (!semverRegex.test(input)) {
344
+ return 'Please enter a valid version number in the format x.y.z (e.g., 1.2.3)';
345
+ }
346
+ return true;
347
+ },
348
+ },
349
+ {
350
+ type: 'confirm',
351
+ name: 'confirm',
352
+ message: 'Would you like to create a release PR with this summary?',
353
+ },
354
+ {
355
+ type: 'input',
356
+ name: 'sourceBranch',
357
+ message: 'Enter source branch name:',
358
+ when: (answers) => answers.confirm,
359
+ validate: (input) => input.length > 0,
360
+ },
361
+ {
362
+ type: 'input',
363
+ name: 'targetBranch',
364
+ message: 'Enter target branch name:',
365
+ when: (answers) => answers.confirm,
366
+ validate: (input) => input.length > 0,
367
+ },
368
+ ]);
369
+
370
+ if (confirm) {
371
+ const pr = await createReleasePR(
372
+ owner,
373
+ repo,
374
+ summary,
375
+ selectedPRs,
376
+ sourceBranch,
377
+ targetBranch,
378
+ version
379
+ );
380
+ console.log(chalk.green('\nSuccess! Release PR created:'), pr.html_url);
381
+ }
382
+ }
383
+
384
+ program
385
+ .name('create-app-release')
386
+ .description('AI-powered GitHub release automation tool')
387
+ .version(pkg.version)
388
+ .action(run)
389
+ .parse(process.argv);