Skip to content

Commit

Permalink
add release generation script
Browse files Browse the repository at this point in the history
  • Loading branch information
dcousens committed Jul 14, 2022
1 parent 6cadee4 commit d164e4c
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 1 deletion.
34 changes: 34 additions & 0 deletions .changeset/contributors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[
"JedWatson",
"MadeByMike",
"Noviny",
"SeanDoyleGit",
"Skulek",
"Vultraz",
"allcontributors[bot]",
"bartduisters",
"bladey",
"borisno2",
"dcousens",
"dependabot[bot]",
"dominikwilkowski",
"gautamsi",
"github-actions[bot]",
"gwyneplaine",
"jesstelford",
"jordanoverbye",
"jossmac",
"keystonejs-release-bot",
"mikehazell",
"mitchellhamilton",
"molomby",
"nathsimpson",
"raveling",
"renovate",
"renovate[bot]",
"sarneaud",
"simonswiss",
"singhArmani",
"ticidesign",
"timleslie"
]
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ module.exports = {
'react/react-in-jsx-scope': 'error',
'react/self-closing-comp': 'warn',
'react/sort-prop-types': 'warn',
semi: 'error',
semi: 'off',
strict: 'off',
'no-restricted-syntax': [
'error',
Expand Down
177 changes: 177 additions & 0 deletions changelog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const { spawnSync } = require('child_process')
const { readFileSync, writeFileSync } = require('fs')
const { default: getReleasePlan } = require('@changesets/get-release-plan')
const { getInfo } = require("@changesets/get-github-info")

// TODO: move this to CI linting
const verbs = new Set([
'Adds',
'Changes',
'Fixes',
'Moves',
'Removes',
'Updates',
])

function gitCommitsSince (tag) {
const { stdout } = spawnSync('git', ['rev-list', `^${tag}`, 'HEAD'])
return stdout.toString('utf-8')
.split('\n')
.filter(x => x)
}

function firstGitCommitOf (path) {
const { stdout } = spawnSync('git', ['rev-list', 'HEAD', '--', path])
return stdout.toString('utf-8').split(' ', 1).pop().replace(/[^A-Za-z0-9]/g, '')
}

async function fetchData (tag) {
const { changesets, releases, } = await getReleasePlan('.')

// find the commits since the tag
const revs = gitCommitsSince(tag)
console.error(`${revs.length} commits since ${tag}`)
if (revs.length === 0) throw new Error('No commits')
if (revs.length > 50) throw new Error('Too many commits')

// tag changesets with their commits
for (const changeset of changesets) {
changeset.commit = firstGitCommitOf(`.changeset/${changeset.id}.md`)
if (!revs.includes(changeset.commit)) throw new Error(`Unexpected commit ${changeset.commit}`)
}

// list all the contributors
const contributors = JSON.parse(readFileSync('.changeset/contributors.json').toString('utf-8'))
const first = [] // first time contributors

const commits = {}
for (const commit of revs) {
const { user, pull } = await getInfo({ repo: 'keystonejs/keystone', commit })
console.error(`commit ${commit}, user ${user}, pull #${pull}`)
commits[commit] = { commit, user, pull }

if (contributors.includes(user)) continue
first.push({ user, pull })
contributors.push(user)
}

// join some of the changeset data with the GitHub commit information
const changes = []
for (const changeset of changesets) {
const { releases, summary, commit } = changeset

// TODO: move this to CI linting
const describedType = summary.split(' ')[0]
if (!verbs.has(describedType)) throw new Error(`${describedType} is not acceptable`)

// poor semver precedence
let type
for (const release of releases) {
if (['minor', 'patch', undefined].includes(type) && release.type === 'major') type = 'major'
if (['patch', undefined].includes(type) && release.type === 'minor') type = 'minor'
if ([undefined].includes(type) && release.type === 'patch') type = 'patch'
}
if (!type) throw new Error('Unknown type')

const { user, pull } = commits[commit]
commits[commit].changeset = changeset

// only include keystone-6 packages, then strip the namespace
const publicPackages = releases
.filter(x => x.name.startsWith('@keystone-6'))
.map(x => x.name.replace('@keystone-6/', ''))

changes.push({
packages: publicPackages,
type,
commit,
summary,
user,
pull
})
}

// only include keystone-6 packages
const packages = releases
.filter(x => x.name.startsWith('@keystone-6'))
.filter(x => x.type !== 'none')
.map(x => `${x.name}@${x.newVersion}`)

// unattributed contributions
const unattributed = Object.values(commits).filter(x => !x.changeset)
return { packages, changes, contributors, first, unattributed }
}

function formatPackagesChanged (packages) {
return `The following packages have been updated
%%%
${packages.join('\n')}
%%%`.replace(/%/g, '`')
}

function formatChange ({ packages, summary, pull, user }) {
return `- \`[${packages.join(', ')}]\` ${summary} (#${pull}) @${user}`
}

async function generateGitHubReleaseText (previousTag) {
if (!previousTag) throw new Error('Missing tag')

const { packages, changes, contributors, first, unattributed } = await fetchData(previousTag)
const output = []

output.push(formatPackagesChanged(packages))
output.push('')

const breaking = changes.filter(x => x.type === 'major')
const features = changes.filter(x => x.type === 'minor')
const fixes = changes.filter(x => x.type === 'patch')

if (breaking.length) {
output.push(...[
`#### Breaking Changes`,
...breaking.map(formatChange),
``
])
}

if (features.length) {
output.push(...[
`#### New Features`,
...features.map(formatChange),
``
])
}

if (fixes.length) {
output.push(...[
`#### Bug Fixes`,
...fixes.map(formatChange),
``
])
}

if (first.length || unattributed.length) {
const listh = unattributed.map(({ user, pull }) => `@${user} ([#${pull}](https://github.com/keystonejs/keystone/pull/${pull}))`)
const listf = first.map(({ user, pull }) => `@${user} ([#${pull}](https://github.com/keystonejs/keystone/pull/${pull}))`)

output.push(`#### Acknowledgements :blue_heart:`)
if (listh.length && listf.length) {
output.push(`Thanks to ${listf.join(', ')} for their first contributions to the project, and a shoutout to ${listh.join(', ')} for their help.`)

} else if (listh.length) {
output.push(`Thanks to ${listh.join(', ')} for their contributions to the project.`)

} else if (listf.length) {
output.push(`Thanks to ${listf.join(', ')} for making their first contributions to the project!`)
}

output.push(``)
}

const date = new Date().toISOString().slice(0, 10)
writeFileSync('./.changeset/contributors.json', JSON.stringify(contributors.sort(), null, 2))
writeFileSync(`./.changeset/release-${date}.md`, output.join('\n'))
console.error('files written to .changeset/')
}

generateGitHubReleaseText(process.argv[2])

0 comments on commit d164e4c

Please sign in to comment.