Skip to content

Commit

Permalink
ci: add changelogensets
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Mar 5, 2024
1 parent f62a9eb commit 25d19ec
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/changelogensets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Release

on:
push:
branches:
- main

permissions:
pull-requests: write
contents: write

concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.sha }}
cancel-in-progress: ${{ github.event_name != 'push' }}

jobs:
update-changelog:
if: github.repository_owner == 'nuxt' && !contains(github.event.head_commit.message, 'v1.')
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"

- name: Install dependencies
run: pnpm install

- run: pnpm jiti ./scripts/update-changelog.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@
"@vue/test-utils": "^2.4.4",
"changelogen": "^0.5.5",
"eslint": "8.57.0",
"execa": "^8.0.1",
"globby": "^14.0.1",
"happy-dom": "^13.6.2",
"ipx": "^2.1.0",
"jiti": "1.21.0",
"nuxt": "^3.10.3",
"ofetch": "^1.3.3",
"playwright-core": "^1.42.1",
"semver": "^7.6.0",
"typescript": "5.3.3",
"vitest": "^1.3.1",
"vitest-environment-nuxt": "^1.0.0",
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

129 changes: 129 additions & 0 deletions scripts/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { promises as fsp } from 'node:fs'
import { execSync } from 'node:child_process'
import { resolve } from 'pathe'
import { execaSync } from 'execa'
import { determineSemverChange, getGitDiff, loadChangelogConfig, parseCommits } from 'changelogen'

export interface Dep {
name: string,
range: string,
type: string
}

type ThenArg<T> = T extends PromiseLike<infer U> ? U : T
export type Package = ThenArg<ReturnType<typeof loadPackage>>

export async function loadPackage (dir: string) {
const pkgPath = resolve(dir, 'package.json')
const data = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'))
const save = () => fsp.writeFile(pkgPath, JSON.stringify(data, null, 2) + '\n')

const updateDeps = (reviver: (dep: Dep) => Dep | void) => {
for (const type of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
if (!data[type]) { continue }
for (const e of Object.entries(data[type])) {
const dep: Dep = { name: e[0], range: e[1] as string, type }
delete data[type][dep.name]
const updated = reviver(dep) || dep
data[updated.type] = data[updated.type] || {}
data[updated.type][updated.name] = updated.range
}
}
}

return {
dir,
data,
save,
updateDeps
}
}

export async function loadWorkspace (dir: string) {
const workspacePkg = await loadPackage(dir)

const packages = [await loadPackage(process.cwd())]

const find = (name: string) => {
const pkg = packages.find(pkg => pkg.data.name === name)
if (!pkg) {
throw new Error('Workspace package not found: ' + name)
}
return pkg
}

const rename = (from: string, to: string) => {
find(from).data._name = find(from).data.name
find(from).data.name = to
for (const pkg of packages) {
pkg.updateDeps((dep) => {
if (dep.name === from && !dep.range.startsWith('npm:')) {
dep.range = 'npm:' + to + '@' + dep.range
}
})
}
}

const setVersion = (name: string, newVersion: string, opts: { updateDeps?: boolean } = {}) => {
find(name).data.version = newVersion
if (!opts.updateDeps) { return }

for (const pkg of packages) {
pkg.updateDeps((dep) => {
if (dep.name === name) {
dep.range = newVersion
}
})
}
}

const save = () => Promise.all(packages.map(pkg => pkg.save()))

return {
dir,
workspacePkg,
packages,
save,
find,
rename,
setVersion
}
}

export async function determineBumpType () {
const config = await loadChangelogConfig(process.cwd())
const commits = await getLatestCommits()

const bumpType = determineSemverChange(commits, config)

return bumpType === 'major' ? 'minor' : bumpType
}

export async function getLatestCommits () {
const config = await loadChangelogConfig(process.cwd())
const latestTag = execaSync('git', ['describe', '--tags', '--abbrev=0']).stdout

return parseCommits(await getGitDiff(latestTag), config)
}

export async function getContributors () {
const contributors = [] as Array<{ name: string, username: string }>
const emails = new Set<string>()
const latestTag = execSync('git describe --tags --abbrev=0').toString().trim()
const rawCommits = await getGitDiff(latestTag)
for (const commit of rawCommits) {
if (emails.has(commit.author.email) || commit.author.name === 'renovate[bot]') { continue }
const { author } = await $fetch<{ author: { login: string, email: string } }>(`https://api.github.com/repos/nuxt/image/commits/${commit.shortHash}`, {
headers: {
'User-Agent': 'nuxt/image',
Accept: 'application/vnd.github.v3+json',
Authorization: `token ${process.env.GITHUB_TOKEN}`
}
})
if (!contributors.some(c => c.username === author.login)) {
contributors.push({ name: commit.author.name, username: author.login })
}
emails.add(author.email)
}
return contributors
}
84 changes: 84 additions & 0 deletions scripts/update-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { execSync } from 'node:child_process'
import { $fetch } from 'ofetch'
import { inc } from 'semver'
import { generateMarkDown, getCurrentGitBranch, loadChangelogConfig } from 'changelogen'
import { consola } from 'consola'
import { determineBumpType, getContributors, getLatestCommits, loadWorkspace } from './_utils'

async function main () {
const releaseBranch = await getCurrentGitBranch()
const workspace = await loadWorkspace(process.cwd())
const config = await loadChangelogConfig(process.cwd(), {})

const commits = await getLatestCommits().then(commits => commits.filter(
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking)
))
const bumpType = await determineBumpType()

const newVersion = inc(workspace.find('@nuxt/image').data.version, bumpType || 'patch')
const changelog = await generateMarkDown(commits, config)

// Create and push a branch with bumped versions if it has not already been created
const branchExists = execSync(`git ls-remote --heads origin v${newVersion}`).toString().trim().length > 0
if (!branchExists) {
execSync('git config --global user.email "[email protected]"')
execSync('git config --global user.name "Daniel Roe"')
execSync(`git checkout -b v${newVersion}`)

for (const pkg of workspace.packages.filter(p => !p.data.private)) {
workspace.setVersion(pkg.data.name, newVersion!)
}
await workspace.save()

execSync(`git commit -am v${newVersion}`)
execSync(`git push -u origin v${newVersion}`)
}

// Get the current PR for this release, if it exists
const [currentPR] = await $fetch(`https://api.github.com/repos/nuxt/image/pulls?head=nuxt:v${newVersion}`)
const contributors = await getContributors()

const releaseNotes = [
currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`,
'## 👉 Changelog',
changelog
.replace(/^## v.*?\n/, '')
.replace(`...${releaseBranch}`, `...v${newVersion}`)
.replace(/### ❤️ Contributors[\s\S]*$/, ''),
'### ❤️ Contributors',
contributors.map(c => `- ${c.name} (@${c.username})`).join('\n')
].join('\n')

// Create a PR with release notes if none exists
if (!currentPR) {
return await $fetch('https://api.github.com/repos/nuxt/image/pulls', {
method: 'POST',
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`
},
body: {
title: `v${newVersion}`,
head: `v${newVersion}`,
base: releaseBranch,
body: releaseNotes,
draft: true
}
})
}

// Update release notes if the pull request does exist
await $fetch(`https://api.github.com/repos/nuxt/image/pulls/${currentPR.number}`, {
method: 'PATCH',
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`
},
body: {
body: releaseNotes
}
})
}

main().catch((err) => {
consola.error(err)
process.exit(1)
})

0 comments on commit 25d19ec

Please sign in to comment.