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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- 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
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}
|
|
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
|
|
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
|
-
|
|
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: '
|
|
388
|
-
message: 'Enter
|
|
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: '
|
|
394
|
-
message: 'Enter
|
|
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
|
-
|
|
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
|
|
632
|
+
const { version, confirm } = await inquirer.prompt([
|
|
442
633
|
{
|
|
443
634
|
type: 'input',
|
|
444
635
|
name: 'version',
|
|
445
|
-
message:
|
|
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) {
|