@@ -3,9 +3,12 @@ import { loadConfig } from '../core/config.ts';
33import { discoverWorkspace } from '../core/workspace.ts' ;
44import { DependencyGraph } from '../core/dep-graph.ts' ;
55import { readChangesets } from '../core/changeset.ts' ;
6+ import { getChangedFiles } from '../core/git.ts' ;
67import { assembleReleasePlan } from '../core/release-plan.ts' ;
78import { runArgs , runArgsAsync , tryRunArgs } from '../utils/shell.ts' ;
8- import type { BumpyConfig , ReleasePlan , PlannedRelease } from '../types.ts' ;
9+ import { randomName } from '../utils/names.ts' ;
10+ import { detectPackageManager } from '../utils/package-manager.ts' ;
11+ import type { BumpyConfig , Changeset , PackageManager , ReleasePlan , PlannedRelease } from '../types.ts' ;
912
1013// ---- Validation helpers ----
1114
@@ -51,18 +54,29 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
5154 const config = await loadConfig ( rootDir ) ;
5255 const { packages } = await discoverWorkspace ( rootDir , config ) ;
5356 const depGraph = new DependencyGraph ( packages ) ;
54- const changesets = await readChangesets ( rootDir ) ;
57+ const allChangesets = await readChangesets ( rootDir ) ;
5558
5659 const inCI = ! ! process . env . CI ;
5760 const shouldComment = opts . comment ?? inCI ;
5861 const prNumber = detectPrNumber ( ) ;
62+ const pm = await detectPackageManager ( rootDir ) ;
63+
64+ // Filter to only changesets added/modified in this PR
65+ const changedFiles = getChangedFiles ( rootDir , config . baseBranch ) ;
66+ const prChangesetIds = new Set (
67+ changedFiles
68+ . filter ( ( f ) => / ^ \. b u m p y \/ .* \. m d $ / . test ( f ) && ! f . endsWith ( 'README.md' ) )
69+ . map ( ( f ) => f . replace ( / ^ \. b u m p y \/ / , '' ) . replace ( / \. m d $ / , '' ) ) ,
70+ ) ;
71+ const prChangesets = allChangesets . filter ( ( cs ) => prChangesetIds . has ( cs . id ) ) ;
5972
60- if ( changesets . length === 0 ) {
73+ if ( prChangesets . length === 0 ) {
6174 const msg = 'No changesets found in this PR.' ;
6275 log . info ( msg ) ;
6376
6477 if ( shouldComment && prNumber ) {
65- await postOrUpdatePrComment ( prNumber , formatNoChangesetsComment ( ) , rootDir ) ;
78+ const prBranch = detectPrBranch ( rootDir ) ;
79+ await postOrUpdatePrComment ( prNumber , formatNoChangesetsComment ( prBranch , pm ) , rootDir ) ;
6680 }
6781
6882 if ( opts . failOnMissing ) {
@@ -71,18 +85,19 @@ export async function ciCheckCommand(rootDir: string, opts: CheckOptions): Promi
7185 return ;
7286 }
7387
74- const plan = assembleReleasePlan ( changesets , packages , depGraph , config ) ;
88+ const plan = assembleReleasePlan ( prChangesets , packages , depGraph , config ) ;
7589
7690 // Pretty output for logs
77- log . bold ( `${ changesets . length } changeset(s) → ${ plan . releases . length } package(s) to release\n` ) ;
91+ log . bold ( `${ prChangesets . length } changeset(s) → ${ plan . releases . length } package(s) to release\n` ) ;
7892 for ( const r of plan . releases ) {
7993 const tag = r . isDependencyBump ? ' (dep)' : r . isCascadeBump ? ' (cascade)' : '' ;
8094 console . log ( ` ${ r . name } : ${ r . oldVersion } → ${ colorize ( r . newVersion , 'cyan' ) } ${ tag } ` ) ;
8195 }
8296
8397 // Comment on PR
8498 if ( shouldComment && prNumber ) {
85- const comment = formatReleasePlanComment ( plan , changesets . length ) ;
99+ const prBranch = detectPrBranch ( rootDir ) ;
100+ const comment = formatReleasePlanComment ( plan , prChangesets , prNumber , prBranch , pm ) ;
86101 await postOrUpdatePrComment ( prNumber , comment , rootDir ) ;
87102 }
88103}
@@ -223,47 +238,114 @@ async function createVersionPr(
223238
224239// ---- PR comment helpers ----
225240
226- function formatReleasePlanComment ( plan : ReleasePlan , changesetCount : number ) : string {
241+ const FROG_IMG_BASE = 'https://raw.githubusercontent.com/dmno-dev/bumpy/main/images' ;
242+
243+ function buildAddChangesetLink ( prBranch : string | null ) : string | null {
244+ if ( ! prBranch ) return null ;
245+ const repo = process . env . GITHUB_REPOSITORY ;
246+ if ( ! repo ) return null ;
247+
248+ const template = [ '---' , '"package-name": patch' , '---' , '' , 'Description of the change' , '' ] . join ( '\n' ) ;
249+ const filename = `.bumpy/${ randomName ( ) } .md` ;
250+ return `https://github.com/${ repo } /new/${ prBranch } ?filename=${ encodeURIComponent ( filename ) } &value=${ encodeURIComponent ( template ) } ` ;
251+ }
252+
253+ function pmRunCommand ( pm : PackageManager ) : string {
254+ if ( pm === 'bun' ) return 'bunx bumpy' ;
255+ if ( pm === 'pnpm' ) return 'pnpm exec bumpy' ;
256+ if ( pm === 'yarn' ) return 'yarn bumpy' ;
257+ return 'npx bumpy' ;
258+ }
259+
260+ function formatReleasePlanComment (
261+ plan : ReleasePlan ,
262+ changesets : Changeset [ ] ,
263+ prNumber : string ,
264+ prBranch : string | null ,
265+ pm : PackageManager ,
266+ ) : string {
267+ const repo = process . env . GITHUB_REPOSITORY ;
227268 const lines : string [ ] = [ ] ;
228- lines . push ( '## 🐸 Bumpy Release Plan\n' ) ;
229- lines . push ( `**${ changesetCount } ** changeset(s) → **${ plan . releases . length } ** package(s) to release\n` ) ;
230269
231- const groups : [ string , PlannedRelease [ ] ] [ ] = [
232- [ '🔴 Major' , plan . releases . filter ( ( r ) => r . type === 'major' ) ] ,
233- [ '🟡 Minor' , plan . releases . filter ( ( r ) => r . type === 'minor' ) ] ,
234- [ '🟢 Patch' , plan . releases . filter ( ( r ) => r . type === 'patch' ) ] ,
235- ] ;
270+ const preamble = [
271+ `<a href="${ __BUMPY_WEBSITE_URL__ } "><img src="${ FROG_IMG_BASE } /frog-talking.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>` ,
272+ '' ,
273+ '**The changes in this PR will be included in the next version bump.**' ,
274+ '<br clear="left" />' ,
275+ ] . join ( '\n' ) ;
276+ lines . push ( preamble ) ;
277+ lines . push ( '' ) ;
236278
237- for ( const [ label , group ] of groups ) {
238- if ( group . length === 0 ) continue ;
239- lines . push ( `### ${ label } \n` ) ;
240- lines . push ( '| Package | Change |' ) ;
241- lines . push ( '|---------|--------|' ) ;
242- for ( const r of group ) {
279+ // Package list grouped by bump type
280+ const groups : Record < string , PlannedRelease [ ] > = { major : [ ] , minor : [ ] , patch : [ ] } ;
281+ for ( const r of plan . releases ) {
282+ groups [ r . type ] ?. push ( r ) ;
283+ }
284+
285+ for ( const type of [ 'major' , 'minor' , 'patch' ] as const ) {
286+ const releases = groups [ type ] ;
287+ if ( ! releases || releases . length === 0 ) continue ;
288+
289+ lines . push ( bumpSectionHeader ( type ) ) ;
290+ lines . push ( '' ) ;
291+ for ( const r of releases ) {
243292 const suffix = r . isDependencyBump ? ' _(dep)_' : r . isCascadeBump ? ' _(cascade)_' : '' ;
244- lines . push ( `| \`${ r . name } \` | ${ r . oldVersion } → **${ r . newVersion } **${ suffix } | ` ) ;
293+ lines . push ( `- \`${ r . name } \` ${ r . oldVersion } → **${ r . newVersion } **${ suffix } ` ) ;
245294 }
246295 lines . push ( '' ) ;
247296 }
248297
298+ // Changeset file list with links
299+ lines . push ( `#### Changesets in this PR` ) ;
300+ lines . push ( '' ) ;
301+ for ( const cs of changesets ) {
302+ const filename = `${ cs . id } .md` ;
303+ const parts : string [ ] = [ `\`${ filename } \`` ] ;
304+ if ( repo ) {
305+ parts . push ( `([view diff](https://github.com/${ repo } /pull/${ prNumber } /files#diff-.bumpy/${ filename } ))` ) ;
306+ if ( prBranch ) {
307+ parts . push ( `([edit](https://github.com/${ repo } /edit/${ prBranch } /.bumpy/${ filename } ))` ) ;
308+ }
309+ }
310+ lines . push ( `- ${ parts . join ( ' ' ) } ` ) ;
311+ }
312+ lines . push ( '' ) ;
313+
314+ const addLink = buildAddChangesetLink ( prBranch ) ;
315+ if ( addLink ) {
316+ lines . push ( `[Click here if you want to add another changeset to this PR](${ addLink } )\n` ) ;
317+ } else {
318+ lines . push ( `To add another changeset, run \`${ pmRunCommand ( pm ) } add\`\n` ) ;
319+ }
320+
249321 lines . push ( '---' ) ;
250322 lines . push ( `_This comment is maintained by [bumpy](${ __BUMPY_WEBSITE_URL__ } )._` ) ;
251323 return lines . join ( '\n' ) ;
252324}
253325
254- function formatNoChangesetsComment ( ) : string {
255- return [
256- '## 🐸 Bumpy Release Plan\n' ,
257- 'No changesets found in this PR. If this PR should trigger a release, run:\n' ,
326+ function formatNoChangesetsComment ( prBranch : string | null , pm : PackageManager ) : string {
327+ const runCmd = pmRunCommand ( pm ) ;
328+ const lines = [
329+ `<a href="${ __BUMPY_WEBSITE_URL__ } "><img src="${ FROG_IMG_BASE } /frog-neutral.png" alt="bumpy-frog" width="60" align="left" style="image-rendering: pixelated;" title="Hi! I'm bumpy!" /></a>` ,
330+ '' ,
331+ "Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**" ,
332+ '<br clear="left" />\n' ,
333+ 'You can add a changeset by running:\n' ,
258334 '```bash' ,
259- 'bumpy add' ,
260- '```\n' ,
261- '---' ,
262- `_This comment is maintained by [bumpy](${ __BUMPY_WEBSITE_URL__ } )._` ,
263- ] . join ( '\n' ) ;
264- }
335+ `${ runCmd } add` ,
336+ '```' ,
337+ ] ;
265338
266- const FROG_IMG_BASE = 'https://raw.githubusercontent.com/dmno-dev/bumpy/main/images' ;
339+ const addLink = buildAddChangesetLink ( prBranch ) ;
340+ if ( addLink ) {
341+ lines . push ( '' ) ;
342+ lines . push ( `Or [click here to add a changeset](${ addLink } ) directly on GitHub.` ) ;
343+ }
344+
345+ lines . push ( '\n---' ) ;
346+ lines . push ( `_This comment is maintained by [bumpy](${ __BUMPY_WEBSITE_URL__ } )._` ) ;
347+ return lines . join ( '\n' ) ;
348+ }
267349
268350function bumpSectionHeader ( type : string ) : string {
269351 // I think pixelated css gets stripped but may as well leave it
@@ -288,10 +370,32 @@ function formatVersionPrBody(plan: ReleasePlan, preamble: string): string {
288370 lines . push ( bumpSectionHeader ( type ) ) ;
289371 lines . push ( '' ) ;
290372 for ( const r of releases ) {
291- const suffix = r . isDependencyBump ? ' (dep)' : r . isCascadeBump ? ' (cascade)' : '' ;
292- lines . push ( `- \`${ r . name } \` ${ r . oldVersion } → **${ r . newVersion } **${ suffix } ` ) ;
373+ const suffix = r . isDependencyBump ? ' _(dep)_' : r . isCascadeBump ? ' _(cascade)_' : '' ;
374+ lines . push ( `#### \`${ r . name } \` ${ r . oldVersion } → **${ r . newVersion } **${ suffix } ` ) ;
375+ lines . push ( '' ) ;
376+
377+ const relevantChangesets = plan . changesets . filter ( ( cs ) => r . changesets . includes ( cs . id ) ) ;
378+
379+ if ( relevantChangesets . length > 0 ) {
380+ for ( const cs of relevantChangesets ) {
381+ if ( cs . summary ) {
382+ const summaryLines = cs . summary . split ( '\n' ) ;
383+ lines . push ( `- ${ summaryLines [ 0 ] } ` ) ;
384+ for ( let i = 1 ; i < summaryLines . length ; i ++ ) {
385+ if ( summaryLines [ i ] ! . trim ( ) ) {
386+ lines . push ( ` ${ summaryLines [ i ] } ` ) ;
387+ }
388+ }
389+ }
390+ }
391+ } else if ( r . isDependencyBump ) {
392+ lines . push ( '- Updated dependencies' ) ;
393+ } else if ( r . isCascadeBump ) {
394+ lines . push ( '- Version bump via cascade rule' ) ;
395+ }
396+
397+ lines . push ( '' ) ;
293398 }
294- lines . push ( '' ) ;
295399 }
296400
297401 return lines . join ( '\n' ) ;
@@ -305,7 +409,7 @@ async function postOrUpdatePrComment(prNumber: string, body: string, rootDir: st
305409
306410 try {
307411 // Find existing bumpy comment using gh with jq
308- const jqFilter = `.comments[] | select(.body | startswith("${ COMMENT_MARKER } ")) | .id` ;
412+ const jqFilter = `.comments[] | select(.body | startswith("${ COMMENT_MARKER } ")) | .url | capture("issuecomment-(?<id>[0-9]+)$") | . id` ;
309413 const existingComment = tryRunArgs ( [ 'gh' , 'pr' , 'view' , validPr , '--json' , 'comments' , '--jq' , jqFilter ] , {
310414 cwd : rootDir ,
311415 } ) ;
@@ -315,7 +419,7 @@ async function postOrUpdatePrComment(prNumber: string, body: string, rootDir: st
315419
316420 if ( commentId ) {
317421 await runArgsAsync (
318- [ 'gh' , 'api' , `repos/{owner}/{repo}/issues/comments/${ commentId } ` , '-X' , 'PATCH' , '-f ' , 'body=@-' ] ,
422+ [ 'gh' , 'api' , `repos/{owner}/{repo}/issues/comments/${ commentId } ` , '-X' , 'PATCH' , '-F ' , 'body=@-' ] ,
319423 { cwd : rootDir , input : markedBody } ,
320424 ) ;
321425 log . dim ( ' Updated PR comment' ) ;
@@ -328,6 +432,14 @@ async function postOrUpdatePrComment(prNumber: string, body: string, rootDir: st
328432 }
329433}
330434
435+ function detectPrBranch ( rootDir : string ) : string | null {
436+ // GitHub Actions sets GITHUB_HEAD_REF for pull_request events
437+ if ( process . env . GITHUB_HEAD_REF ) return process . env . GITHUB_HEAD_REF ;
438+ // Fallback: ask gh for the PR head branch
439+ const branch = tryRunArgs ( [ 'gh' , 'pr' , 'view' , '--json' , 'headRefName' , '--jq' , '.headRefName' ] , { cwd : rootDir } ) ;
440+ return branch ?. trim ( ) || null ;
441+ }
442+
331443function detectPrNumber ( ) : string | null {
332444 // GitHub Actions
333445 if ( process . env . GITHUB_EVENT_NAME === 'pull_request' ) {
0 commit comments