import { fromEntries , last , sample , uniq } from 'js-fns' ; import sg from 'simple-git' ; import { Octokit } from '@octokit/core' ; import format from '../../src/format' ; const git = sg ( ) ; const gh = new Octokit ( { auth : process . env . GITHUB_TOKEN } ) ; ( async ( ) => { const changelog = await buildChangelog ( ) ; console . log ( renderChangelog ( changelog ) ) ; } ) ( ) ; function renderChangelog ( changelog ) { let markdown = `## ${ renderVersion ( changelog . version ) } - ${ format ( Date . now ( ) , 'yyyy-MM-dd' ) }

${ sample ( thanksOptions ) ! ( renderAuthors ( changelog . authors ) ) }` ; if ( changelog . fixed . length ) markdown += `

### Fixed

${ changelog . fixed . map ( renderItem ) . join ( '\n\n' ) }` ; if ( changelog . changed . length ) markdown += `

### Changed

${ changelog . changed . map ( renderItem ) . join ( '\n\n' ) }` ; if ( changelog . added . length ) markdown += `

### Added

${ changelog . added . map ( renderItem ) . join ( '\n\n' ) }` ; return markdown ; } async function buildChangelog ( ) { const lastTag = last ( ( await git . tags ( ) ) . all ) ; if ( ! lastTag ) throw new Error ( "Can't find tags" ) ; const commits = await git . log ( { from : lastTag , to : 'HEAD' } ) ; const authorsMap = fromEntries ( await Promise . all ( commits . all . map ( ( c ) => gh . request ( 'GET /repos/{owner}/{repo}/commits/{ref}' , { owner : 'date-fns' , repo : 'date-fns' , ref : c . hash , } ) . then ( ( { data } ) => [ c . hash , data . author ?. login ] as [ string , string ] ) ) ) ) ; const items = [ ] ; const authors = [ ] ; commits . all . forEach ( ( commit ) => { const author = { login : authorsMap [ commit . hash ] , email : commit . author_email , name : commit . author_name , } ; const prCaptures = commit . message . match ( /\(#(\d+)\)/ ) ; const pr = prCaptures ? parseInt ( prCaptures [ 1 ] ) : undefined ; let issues ; commit . message . match ( new RegExp ( closesRegExp , 'g' ) ) ?. forEach ( ( str ) => { const issueCaptures = str . match ( closesRegExp ) ; if ( issueCaptures ) issues = ( issues || [ ] ) . concat ( issueCaptures . slice ( 1 ) . map ( ( issue ) => parseInt ( issue ) ) ) ; } ) ; if ( ! issues ?. length ) issues = undefined ; const commitItems = extractItems ( commit . body . trim ( ) , { author , pr , issues } ) ; if ( ! authors . find ( ( a ) => a . login === author . login ) ) authors . push ( author ) ; items . push ( ... commitItems ) ; } ) ; const changed = items . filter ( ( i ) => i . type === 'changed' ) ; const fixed = items . filter ( ( i ) => i . type === 'fixed' ) ; const added = items . filter ( ( i ) => i . type === 'added' ) ; const lastVersion = parseVersion ( lastTag ) ; let version ; if ( items . find ( ( i ) => i . breaking ) ) { version = { major : lastVersion . major + 1 , minor : 0 , patch : 0 } ; } else if ( changed . length || added . length ) { version = { major : lastVersion . major , minor : lastVersion . minor + 1 , patch : 0 , } ; } else { version = { major : lastVersion . major , minor : lastVersion . minor , patch : lastVersion . patch + 1 , } ; } return { version , changed , fixed , added , authors } ; } function parseVersion ( tag ) { const captures = tag . match ( /v(\d+)\.(\d+).(\d+)/ ) ; if ( ! captures ) throw new Error ( `Can't parse version from tag "${ tag }"` ) ; return { major : parseInt ( captures [ 1 ] ) , minor : parseInt ( captures [ 2 ] ) , patch : parseInt ( captures [ 3 ] ) , } ; } function extractItems ( message , { author , pr , issues , } ) { const item = ( { type , message , } ) => { const issuesCaptures = message . match ( issuesRegExp ) ; const messageIssues = issuesCaptures ?. reduce < number [ ] > ( ( acc , capture ) => acc . concat ( ( capture . match ( /#\d+/g ) || [ ] ) . map ( ( str ) => parseInt ( str . slice ( 1 ) ) ) ) , [ ] ) ; const itemIssues = messageIssues ?. length ? uniq ( messageIssues . concat ( issues || [ ] ) ) : issues ; const breaking = /^breaking:\s?/i . test ( message ) ; return { type , author , message : message . replace ( issuesRegExp , '' ) , pr , issues : itemIssues , breaking , } ; } ; switch ( true ) { case fixedSentenceRegExp . test ( message ) : { const captures = message . match ( fixedSentenceRegExp ) ! ; return [ item ( { type : 'fixed' , message : captures [ 2 ] } ) ] ; } case fixedOneLinerRegExp . test ( message ) : { const captures = message . match ( fixedOneLinerRegExp ) ! ; return [ item ( { type : 'fixed' , message : captures [ 1 ] } ) ] ; } case changedSentenceRegExp . test ( message ) : { const captures = message . match ( changedSentenceRegExp ) ! ; return [ item ( { type : 'changed' , message : captures [ 2 ] } ) ] ; } case changedOneLinerRegExp . test ( message ) : { const captures = message . match ( changedOneLinerRegExp ) ! ; return [ item ( { type : 'changed' , message : captures [ 1 ] } ) ] ; } case addedSentenceRegExp . test ( message ) : { const captures = message . match ( addedSentenceRegExp ) ! ; return [ item ( { type : 'added' , message : captures [ 2 ] } ) ] ; } case addedOneLinerRegExp . test ( message ) : { const captures = message . match ( addedOneLinerRegExp ) ! ; return [ item ( { type : 'added' , message : captures [ 1 ] } ) ] ; } default : return [ ] ; } } function renderVersion ( { major , minor , patch } ) { return `v${ major }.${ minor }.${ patch }` ; } function renderAuthors ( authors ) { if ( authors . length > 1 ) { return ( authors . slice ( 0 , authors . length - 1 ) . map ( renderAuthor ) . join ( ', ' ) + ' and ' + renderAuthor ( last ( authors ) ! ) ) ; } else { return renderAuthor ( authors [ 0 ] ) ; } } function renderAuthor ( author ) { return `@${ author . login }` ; } function renderItem ( item ) { const message = item . pr ? `[${ item . message }](https://github.com/date-fns/date-fns/pull/${ item . pr })` : item . message ; const issues = item . issues ? ` (${ item . issues . map ( ( i ) => `[#${ i }](https://github.com/date-fns/date-fns/issues/${ i })` ) . join ( ', ' ) })` : '' ; return `- ${ message }${ issues }` ; } type ChangelogType = 'changed' | 'fixed' | 'added' ; interface Version { major : number ; minor : number ; patch : number ; } interface ChangelogItem { type : ChangelogType ; author : Author ; message : string ; pr ? : number ; issues ? : number [ ] ; breaking : boolean ; } interface ChangelogVersion { version : Version ; changed : ChangelogItem [ ] ; fixed : ChangelogItem [ ] ; added : ChangelogItem [ ] ; authors : Author [ ] ; } interface Author { login : string ; name : string ; email : string ; } const closesRegExp = /(?:closes|fixes) #(\d+)/ ; const issuesRegExp = /\s?\(((?:#\d+(?:,\s?)?)+)\)/g ; const thanksOptions = [ ( authors ) => `Kudos to ${ authors } for working on the release.` , ( authors ) => `Thanks to ${ authors } for working on the release.` , ( authors ) => `This release is brought to you by ${ authors }.` , ( authors ) => `On this release worked ${ authors }.` , ] ; const fixedSentenceRegExp = /^(breaking:\s?)?(fixed\s.+)/i ; const fixedOneLinerRegExp = /^fixed:\s(.+)/i ; const changedSentenceRegExp = /^(breaking:\s?)?(changed\s.+)/i ; const changedOneLinerRegExp = /^changed:\s(.+)/i ; const addedSentenceRegExp = /^(breaking:\s?)?(added\s.+)/i ; const addedOneLinerRegExp = /^added:\s(.+)/i ;