release-with-ease 2.0.2 → 2.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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git add:*)",
5
+ "Bash(git commit -m ':*)",
6
+ "Bash(git push:*)",
7
+ "Bash(gh pr:*)"
8
+ ]
9
+ }
10
+ }
package/README.md CHANGED
@@ -25,10 +25,23 @@ The script requires these environment variables to be set:
25
25
 
26
26
  You can get a key from https://console.anthropic.com/settings/keys.
27
27
 
28
- The script also assumes that your README.md file has a `# Changelog` section.
28
+ The script also requires the `gh` CLI to be installed and authenticated (used to
29
+ create GitHub releases).
30
+
31
+ If your `README.md` has a `# Changelog` section, the script will automatically
32
+ insert the release notes there. Otherwise it skips that step and relies solely on
33
+ the GitHub release.
29
34
 
30
35
  # Changelog
31
36
 
37
+ ## 2.1.0
38
+
39
+ - Support npm publish and GitHub releases for public npm packages [by @trotzig in #5]
40
+ - Include PR number and author attribution in release notes for public packages [by @trotzig]
41
+ - Auto-detect README.md changelog section; skip insertion if absent for better compatibility
42
+ - Run `npm publish` automatically for packages without `private: true` in package.json
43
+ - Create GitHub releases automatically via `gh release create` after every push (for public packages)
44
+
32
45
  ## 1.0.1
33
46
 
34
47
  - Fix path to README and package.json
@@ -74,18 +74,79 @@ function parseCommits(raw) {
74
74
  });
75
75
  }
76
76
 
77
- async function askClaudeForRelease(commits) {
77
+ function extractPrNumber(subject, body) {
78
+ // "(#123)" suffix — squash-merge style
79
+ const m = subject.match(/\(#(\d+)\)\s*$/);
80
+ if (m) return parseInt(m[1], 10);
81
+ // "Merge pull request #123" — merge commit style
82
+ const mm = subject.match(/Merge pull request #(\d+)/);
83
+ if (mm) return parseInt(mm[1], 10);
84
+ // Same patterns in body
85
+ const bm = (body || '').match(/\(#(\d+)\)\s*$/m);
86
+ if (bm) return parseInt(bm[1], 10);
87
+ return null;
88
+ }
89
+
90
+ function fetchGitHubMeta(commits, lastTag) {
91
+ const repoRes = safeRun('gh repo view --json nameWithOwner -q .nameWithOwner');
92
+ if (!repoRes.ok) return commits;
93
+ const [owner, repo] = repoRes.out.trim().split('/');
94
+
95
+ // SHA → GitHub login via compare API (best-effort)
96
+ const shaToLogin = {};
97
+ if (lastTag) {
98
+ const cmpRes = safeRun(
99
+ `gh api "repos/${owner}/${repo}/compare/${lastTag}...HEAD" --jq '.commits[] | [.sha, (.author.login // "")] | @tsv'`,
100
+ );
101
+ if (cmpRes.ok) {
102
+ for (const line of cmpRes.out.trim().split('\n').filter(Boolean)) {
103
+ const [sha, login] = line.split('\t');
104
+ if (sha && login) shaToLogin[sha] = login;
105
+ }
106
+ }
107
+ }
108
+
109
+ // Merge commit SHA → PR number via pr list (best-effort)
110
+ const shaToPr = {};
111
+ const prRes = safeRun(
112
+ `gh pr list --state merged --limit 100 --json number,mergeCommit --jq '.[] | select(.mergeCommit != null) | [.mergeCommit.oid, (.number | tostring)] | @tsv'`,
113
+ );
114
+ if (prRes.ok) {
115
+ for (const line of prRes.out.trim().split('\n').filter(Boolean)) {
116
+ const [sha, num] = line.split('\t');
117
+ if (sha && num) shaToPr[sha] = parseInt(num, 10);
118
+ }
119
+ }
120
+
121
+ return commits.map(c => ({
122
+ ...c,
123
+ githubLogin: shaToLogin[c.hash] || null,
124
+ prNumber: shaToPr[c.hash] ?? extractPrNumber(c.subject, c.body),
125
+ }));
126
+ }
127
+
128
+ async function askClaudeForRelease(commits, isPublicPackage = false) {
78
129
  const apiKey = process.env.ANTHROPIC_API_KEY;
79
130
  if (!apiKey) return null;
80
131
 
81
- const systemPrompt =
132
+ const basePrompt =
82
133
  'You are a release assistant. Given recent git commits, decide one of: major, minor, or patch following semver. Consider conventional commits, breaking changes, and scope. Also generate concise release notes for a public changelog. Respond with JSON containing "bump" (major/minor/patch), "reasoning" (brief explanation for version bump), and "notes" (array of 3-8 short bullet points of the most important user-facing changes). Use present tense for release notes (e.g. "Add script" not "Added script" or "Adds script"). Do not wrap the JSON in ```json or anything else.';
83
134
 
135
+ const publicExtra = isPublicPackage
136
+ ? ' Each commit may carry metadata in brackets like [by @login in #123]. When present, append that attribution verbatim at the end of the corresponding bullet point.'
137
+ : '';
138
+
139
+ const systemPrompt = basePrompt + publicExtra;
140
+
84
141
  const userContent = commits
85
142
  .map(c => {
86
143
  const body = c.body ? c.body.trim() : '';
87
144
  const truncatedBody = body.length > 500 ? body.slice(0, 500) + '…' : body;
88
- return `- ${c.subject}\n${truncatedBody}`;
145
+ const meta = [];
146
+ if (c.githubLogin) meta.push(`by @${c.githubLogin}`);
147
+ if (c.prNumber) meta.push(`in #${c.prNumber}`);
148
+ const metaStr = meta.length ? ` [${meta.join(' ')}]` : '';
149
+ return `- ${c.subject}${metaStr}\n${truncatedBody}`;
89
150
  })
90
151
  .join('\n');
91
152
 
@@ -177,6 +238,12 @@ function prompt(question) {
177
238
  });
178
239
  }
179
240
 
241
+ function hasReadmeChangelog() {
242
+ if (!fs.existsSync(readmePath)) return false;
243
+ const content = fs.readFileSync(readmePath, 'utf8');
244
+ return /^#\s*Changelog\s*$/im.test(content);
245
+ }
246
+
180
247
  function parseArgs() {
181
248
  const args = process.argv.slice(2);
182
249
  const dryRun = args.includes('--dry-run');
@@ -203,10 +270,13 @@ function parseArgs() {
203
270
  console.log('🔍 DRY RUN MODE - No changes will be made\n');
204
271
  }
205
272
 
273
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
274
+ const isPublicPackage = !pkg.private;
275
+
206
276
  fetchOriginTags();
207
277
  const lastVersionTag = getLastVersionTag();
208
278
  const raw = getCommitRange(lastVersionTag);
209
- const commits = parseCommits(raw);
279
+ let commits = parseCommits(raw);
210
280
  if (!commits.length) {
211
281
  console.log('No commits found since last tag. Aborting.');
212
282
  process.exit(1);
@@ -222,9 +292,13 @@ function parseArgs() {
222
292
  console.log(` ${shortSha} ${commit.subject}`);
223
293
  });
224
294
 
295
+ if (isPublicPackage) {
296
+ commits = fetchGitHubMeta(commits, lastVersionTag);
297
+ }
298
+
225
299
  console.log('\nWaiting for Claude to analyze commits...');
226
300
 
227
- const result = await askClaudeForRelease(commits);
301
+ const result = await askClaudeForRelease(commits, isPublicPackage);
228
302
 
229
303
  const { bump, reasoning, notes } = result;
230
304
 
@@ -242,7 +316,6 @@ function parseArgs() {
242
316
  process.exit(1);
243
317
  }
244
318
 
245
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
246
319
  const curVersion = pkg.version;
247
320
  const newVersion = bumpVersionString(curVersion, finalBump);
248
321
 
@@ -284,32 +357,48 @@ function parseArgs() {
284
357
  process.exit(1);
285
358
  }
286
359
 
360
+ const useReadmeChangelog = hasReadmeChangelog();
361
+
287
362
  if (dryRun) {
288
363
  console.log(`\n🔍 DRY RUN - Would have done the following:`);
289
- console.log(
290
- ` 1. Insert changelog entry for ${newVersion} into README.md`,
291
- );
292
- console.log(` 2. git add README.md`);
293
- console.log(` 3. git commit -m "Update changelog for ${newVersion}"`);
294
- console.log(` 4. npm version ${finalBump} -m "%s"`);
295
- console.log(` 5. git push origin main --tags`);
364
+ let step = 1;
365
+ if (useReadmeChangelog) {
366
+ console.log(
367
+ ` ${step++}. Insert changelog entry for ${newVersion} into README.md`,
368
+ );
369
+ console.log(` ${step++}. git add README.md`);
370
+ console.log(
371
+ ` ${step++}. git commit -m "Update changelog for ${newVersion}"`,
372
+ );
373
+ }
374
+ console.log(` ${step++}. npm version ${finalBump} -m "%s"`);
375
+ console.log(` ${step++}. git push origin main --tags`);
376
+ console.log(` ${step++}. gh release create v${newVersion} --title "v${newVersion}" --notes-file <entry>`);
377
+ if (isPublicPackage) {
378
+ console.log(` ${step++}. npm publish`);
379
+ }
296
380
  console.log(`\n✅ Dry run complete. Use without --dry-run to execute.`);
297
381
  fs.unlinkSync(tempEntryPath);
298
382
  return;
299
383
  }
300
384
 
301
- // Read the edited entry and insert it into README
385
+ // Read the edited entry
302
386
  const editedEntry = fs.readFileSync(tempEntryPath, 'utf8');
303
- const readme = fs.readFileSync(readmePath, 'utf8');
304
- const updatedReadme = insertChangelogEntry(
305
- readme,
306
- editedEntry.trim().split('\n'),
307
- );
308
- fs.writeFileSync(readmePath, updatedReadme);
309
- fs.unlinkSync(tempEntryPath);
310
387
 
311
- run('git add README.md');
312
- run(`git commit -m "Update changelog for ${newVersion}"`);
388
+ if (useReadmeChangelog) {
389
+ // Insert changelog entry into README.md
390
+ const readme = fs.readFileSync(readmePath, 'utf8');
391
+ const updatedReadme = insertChangelogEntry(
392
+ readme,
393
+ editedEntry.trim().split('\n'),
394
+ );
395
+ fs.writeFileSync(readmePath, updatedReadme);
396
+
397
+ run('git add README.md');
398
+ run(`git commit -m "Update changelog for ${newVersion}"`);
399
+ }
400
+
401
+ fs.unlinkSync(tempEntryPath);
313
402
 
314
403
  // Use npm version keyword per user preference
315
404
  run(`npm version ${finalBump} -m "%s"`);
@@ -317,6 +406,26 @@ function parseArgs() {
317
406
  // Push commit and tags explicitly
318
407
  run('git push origin main --tags');
319
408
 
409
+ // Create GitHub release
410
+ const ghNotesFile = path.join(
411
+ os.tmpdir(),
412
+ `release-notes-${crypto.randomBytes(8).toString('hex')}.md`,
413
+ );
414
+ fs.writeFileSync(ghNotesFile, editedEntry.trim());
415
+ try {
416
+ const releaseUrl = run(
417
+ `gh release create v${newVersion} --title "v${newVersion}" --notes-file "${ghNotesFile}"`,
418
+ ).trim();
419
+ console.log(`\n🎉 GitHub release created: ${releaseUrl}`);
420
+ } finally {
421
+ fs.unlinkSync(ghNotesFile);
422
+ }
423
+
424
+ if (isPublicPackage) {
425
+ run('npm publish');
426
+ console.log(`\n📦 Published ${pkg.name}@${newVersion} to npm.`);
427
+ }
428
+
320
429
  console.log(`\nRelease ${newVersion} created and pushed with tags.`);
321
430
  } catch (err) {
322
431
  console.error(err?.stderr?.toString?.() || err?.message || err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "release-with-ease",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "A script to bump the version of an npm library and update release notes. Uses Claude to analyze commits.",
5
5
  "main": "index.js",
6
6
  "bin": {