create-app-release 1.1.0 → 1.2.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/src/index.js +203 -25
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ 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.2.0] - 2025-03-18
9
+
10
+ ### Added
11
+
12
+ - Auto suggest repositories based on recent activities
13
+ - Added filter for listed pull requests for easy tracking
14
+ - Dynamic release version suggestion based on previous releases
15
+ - Added suggestion for source and target branches
16
+
17
+ ### Changed
18
+
19
+ - Updated release summary generation to exclude heading
20
+
8
21
  ## [1.1.0] - 2025-02-12
9
22
 
10
23
  ### Added
@@ -59,4 +72,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
59
72
  - ora - Terminal spinners
60
73
  - dotenv - Environment variable management
61
74
 
75
+ [1.2.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.2.0
76
+ [1.1.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.1.0
62
77
  [1.0.0]: https://github.com/jamesgordo/create-app-release/releases/tag/v1.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-app-release",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "AI-powered GitHub release automation tool",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -170,14 +170,130 @@ function extractPRNumbersFromDescription(description) {
170
170
  return new Set(prMatches.map((match) => parseInt(match.replace(/[^0-9]/g, ''))));
171
171
  }
172
172
 
173
+ /**
174
+ * Fetch repositories the user has contributed to, including personal and organization repos
175
+ * @returns {Promise<Array>} List of repositories
176
+ */
177
+ async function fetchUserRepositories() {
178
+ const spinner = ora('Fetching your repositories...').start();
179
+ try {
180
+ // Get authenticated user info
181
+ const { data: user } = await octokit.rest.users.getAuthenticated();
182
+ const username = user.login;
183
+
184
+ // Get user's repositories (both owned and contributed to)
185
+ const repos = [];
186
+
187
+ // Fetch user's own repositories
188
+ const userReposIterator = octokit.paginate.iterator(
189
+ octokit.rest.repos.listForAuthenticatedUser,
190
+ {
191
+ per_page: 100,
192
+ sort: 'updated',
193
+ direction: 'desc',
194
+ }
195
+ );
196
+
197
+ for await (const { data: userRepos } of userReposIterator) {
198
+ repos.push(
199
+ ...userRepos.map((repo) => ({
200
+ name: `${repo.owner.login}/${repo.name}`,
201
+ fullName: `${repo.owner.login}/${repo.name}`,
202
+ owner: repo.owner.login,
203
+ repoName: repo.name,
204
+ updatedAt: new Date(repo.updated_at),
205
+ pushedAt: new Date(repo.pushed_at || repo.updated_at),
206
+ isPersonal: repo.owner.login === username,
207
+ activityScore: 0, // Will be calculated based on user activity
208
+ }))
209
+ );
210
+
211
+ // Limit to 100 repositories to avoid excessive API calls
212
+ if (repos.length >= 100) break;
213
+ }
214
+
215
+ // Get recent user activity for each repository (limited to top 15 to avoid API rate limits)
216
+ const topRepos = repos.slice(0, 15);
217
+ spinner.text = 'Analyzing your recent activity...';
218
+
219
+ // Process repositories in parallel with rate limiting
220
+ await Promise.all(
221
+ topRepos.map(async (repo, index) => {
222
+ // Add delay to avoid hitting rate limits
223
+ await new Promise((resolve) => setTimeout(resolve, index * 100));
224
+
225
+ try {
226
+ // Check for user's recent commits
227
+ const { data: commits } = await octokit.rest.repos
228
+ .listCommits({
229
+ owner: repo.owner,
230
+ repo: repo.repoName,
231
+ author: username,
232
+ per_page: 100,
233
+ })
234
+ .catch(() => ({ data: [] }));
235
+
236
+ // Check for user's recent PRs
237
+ const { data: prs } = await octokit.rest.pulls
238
+ .list({
239
+ owner: repo.owner,
240
+ repo: repo.repoName,
241
+ state: 'all',
242
+ per_page: 100,
243
+ })
244
+ .catch(() => ({ data: [] }));
245
+
246
+ // Calculate activity score based on recency and count
247
+ const now = new Date();
248
+ let score = 0;
249
+
250
+ // Add points for recent commits
251
+ commits.forEach((commit) => {
252
+ const daysAgo = (now - new Date(commit.commit.author.date)) / (1000 * 60 * 60 * 24);
253
+ score += Math.max(30 - daysAgo, 0); // More points for more recent commits
254
+ });
255
+
256
+ // Add points for PRs authored or reviewed by user
257
+ prs.forEach((pr) => {
258
+ if (pr.user.login === username) {
259
+ const daysAgo = (now - new Date(pr.updated_at)) / (1000 * 60 * 60 * 24);
260
+ score += Math.max(20 - daysAgo, 0); // Points for authoring PRs
261
+ }
262
+ });
263
+
264
+ // Update the repository's activity score
265
+ repo.activityScore = score;
266
+ } catch (error) {
267
+ // Silently continue if we hit API limits or other issues
268
+ }
269
+ })
270
+ );
271
+
272
+ // Sort repositories by activity score first, then by pushed date
273
+ repos.sort((a, b) => {
274
+ if (a.activityScore !== b.activityScore) {
275
+ return b.activityScore - a.activityScore; // Higher score first
276
+ }
277
+ return b.pushedAt - a.pushedAt; // Then by most recent push
278
+ });
279
+
280
+ spinner.succeed(`Found ${repos.length} repositories, sorted by your recent activity`);
281
+ return repos;
282
+ } catch (error) {
283
+ spinner.fail('Failed to fetch repositories');
284
+ console.error(chalk.red(`Error: ${error.message}`));
285
+ return [];
286
+ }
287
+ }
288
+
173
289
  /**
174
290
  * Fetch closed pull requests from the repository
175
291
  * @param {string} owner - Repository owner
176
292
  * @param {string} repo - Repository name
177
- * @param {string} targetBranch - Target branch name
293
+ * @param {string} baseBranch - Base branch name
178
294
  * @returns {Promise<Array>} List of pull requests
179
295
  */
180
- async function fetchPullRequests(owner, repo) {
296
+ async function fetchPullRequests(owner, repo, baseBranch) {
181
297
  const spinner = ora('Fetching pull requests...').start();
182
298
  try {
183
299
  // Get the latest release PR first
@@ -195,10 +311,14 @@ async function fetchPullRequests(owner, repo) {
195
311
  });
196
312
 
197
313
  for await (const { data } of iterator) {
198
- // Filter PRs that are merged after the last release and not included in it
314
+ // Filter PRs that are merged after the last release, not included in it, and merged to the target branch
199
315
  const relevantPRs = data.filter((pr) => {
316
+ // Skip PRs that aren't merged
200
317
  if (!pr.merged_at) return false;
201
318
 
319
+ // Skip PRs that aren't targeting the specified branch
320
+ if (pr.base && pr.base.ref !== baseBranch) return false;
321
+
202
322
  const isAfterLastRelease = latestReleasePR
203
323
  ? new Date(pr.merged_at) >= new Date(latestReleasePR.merged_at)
204
324
  : true;
@@ -250,6 +370,7 @@ async function generateSummary(selectedPRs) {
250
370
  1.2 For each type, make each bullet point concise and easy to read and understand for non-tech people.
251
371
  1.3 Don't link the bullet points to a pull requests
252
372
  2. The last section should be a list of pull requests included in the release. Format: "#<number> - <title> by [@<author>](<authorUrl>) (<date>)".
373
+ 3. Don't add Release Summary title/heading.
253
374
 
254
375
  Pull Requests to summarize:
255
376
  ${JSON.stringify(prDetails, null, 2)}
@@ -381,22 +502,92 @@ async function run() {
381
502
  baseURL: options.openaiBaseUrl,
382
503
  });
383
504
 
384
- const { owner, repo } = await inquirer.prompt([
505
+ // Fetch repositories the user has contributed to
506
+ const userRepos = await fetchUserRepositories();
507
+
508
+ // Prepare repository choices
509
+ const repoChoices =
510
+ userRepos.length > 0
511
+ ? userRepos.map((repo) => ({
512
+ name: repo.fullName + (repo.isPersonal ? ' (personal)' : ''),
513
+ value: { owner: repo.owner, repo: repo.repoName },
514
+ }))
515
+ : [];
516
+
517
+ // Add option for manual entry
518
+ repoChoices.push({ name: '-- Enter repository manually --', value: 'manual' });
519
+
520
+ let repoInfo = { owner: '', repo: '' };
521
+ const { repoSelection } = await inquirer.prompt([
522
+ {
523
+ type: 'list',
524
+ name: 'repoSelection',
525
+ message: 'Select a repository:',
526
+ choices: repoChoices,
527
+ pageSize: 5,
528
+ },
529
+ ]);
530
+
531
+ // Handle manual repository entry
532
+ if (repoSelection === 'manual') {
533
+ const manualEntry = await inquirer.prompt([
534
+ {
535
+ type: 'input',
536
+ name: 'owner',
537
+ message: 'Enter repository owner:',
538
+ validate: (input) => input.length > 0,
539
+ },
540
+ {
541
+ type: 'input',
542
+ name: 'repo',
543
+ message: 'Enter repository name:',
544
+ validate: (input) => input.length > 0,
545
+ },
546
+ ]);
547
+ repoInfo = manualEntry;
548
+ } else {
549
+ repoInfo = repoSelection;
550
+ }
551
+
552
+ const { owner, repo } = repoInfo;
553
+
554
+ const { sourceBranch, targetBranch } = await inquirer.prompt([
385
555
  {
386
556
  type: 'input',
387
- name: 'owner',
388
- message: 'Enter repository owner:',
557
+ name: 'sourceBranch',
558
+ message: 'Enter source branch name:',
559
+ default: 'staging',
389
560
  validate: (input) => input.length > 0,
390
561
  },
391
562
  {
392
563
  type: 'input',
393
- name: 'repo',
394
- message: 'Enter repository name:',
564
+ name: 'targetBranch',
565
+ message: 'Enter target branch name:',
566
+ default: 'main',
395
567
  validate: (input) => input.length > 0,
396
568
  },
397
569
  ]);
398
570
 
399
- const pulls = await fetchPullRequests(owner, repo);
571
+ // Get the latest release version for the repository
572
+ let suggestedVersion = '1.0.0';
573
+ try {
574
+ const { data: releases } = await octokit.rest.repos.listReleases({
575
+ owner,
576
+ repo,
577
+ per_page: 100,
578
+ });
579
+
580
+ if (releases && releases.length > 0) {
581
+ // Extract version from tag_name (removing any 'v' prefix)
582
+ const latestTag = releases[0].tag_name.replace(/^v/, '');
583
+ const [major, minor, patch] = latestTag.split('.');
584
+ suggestedVersion = `${major}.${minor}.${parseInt(patch) + 1}`;
585
+ }
586
+ } catch (error) {
587
+ console.log(chalk.yellow(`Could not fetch latest release version: ${error.message}`));
588
+ }
589
+
590
+ const pulls = await fetchPullRequests(owner, repo, sourceBranch);
400
591
 
401
592
  const { selectedPRs } = await inquirer.prompt([
402
593
  {
@@ -438,11 +629,12 @@ async function run() {
438
629
  console.log(chalk.cyan('\nSummary:'));
439
630
  console.log(summary);
440
631
 
441
- const { version, confirm, sourceBranch, targetBranch } = await inquirer.prompt([
632
+ const { version, confirm } = await inquirer.prompt([
442
633
  {
443
634
  type: 'input',
444
635
  name: 'version',
445
- message: 'Enter the version number for this release (e.g., 1.2.3):',
636
+ message: `Enter the version number for this release (suggested: ${suggestedVersion}):`,
637
+ default: suggestedVersion,
446
638
  validate: (input) => {
447
639
  // Validate semantic versioning format (x.y.z)
448
640
  const semverRegex = /^\d+\.\d+\.\d+$/;
@@ -457,20 +649,6 @@ async function run() {
457
649
  name: 'confirm',
458
650
  message: 'Would you like to create a release PR with this summary?',
459
651
  },
460
- {
461
- type: 'input',
462
- name: 'sourceBranch',
463
- message: 'Enter source branch name:',
464
- when: (answers) => answers.confirm,
465
- validate: (input) => input.length > 0,
466
- },
467
- {
468
- type: 'input',
469
- name: 'targetBranch',
470
- message: 'Enter target branch name:',
471
- when: (answers) => answers.confirm,
472
- validate: (input) => input.length > 0,
473
- },
474
652
  ]);
475
653
 
476
654
  if (confirm) {