Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, 'v0.') && !contains(github.event.head_commit.message, 'v1.')
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- run: corepack enable
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with:
node-version: lts/*
cache: "pnpm"

- name: Install dependencies
run: pnpm install

- run: node ./scripts/update-changelog.ts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"entry": [
"src/cli.ts",
"src/module.ts",
"build.config.ts"
"scripts/**"
]
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
"lint": "eslint .",
"nuxt-telemetry": "node ./bin/nuxt-telemetry.mjs",
"prepack": "nuxt-module-build build",
"release": "pnpm test && pnpm build && pnpm changelogen --release --push && pnpm publish",
"test": "vitest run",
"test:engines": "installed-check -d --no-workspaces",
"test:knip": "knip",
Expand All @@ -56,6 +55,7 @@
"@nuxt/module-builder": "^1.0.2",
"@nuxt/schema": "^4.3.0",
"@nuxt/test-utils": "^4.0.0",
"@types/semver": "^7.7.1",
"@vitest/coverage-v8": "^4.0.18",
"changelogen": "^0.6.2",
"eslint": "^10.0.0",
Expand All @@ -64,6 +64,7 @@
"installed-check": "^9.3.0",
"knip": "^5.83.1",
"nuxt": "^4.3.0",
"semver": "^7.7.4",
"typescript": "^5.9.3",
"unbuild": "^3.6.1",
"vitest": "^4.0.18",
Expand Down
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

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

136 changes: 136 additions & 0 deletions scripts/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { promises as fsp } from 'node:fs'
import process from 'node:process'
import { execSync } from 'node:child_process'
import { resolve } from 'node:path'
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 | undefined) => {
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 }
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
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): undefined => {
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): undefined => {
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 = execSync('git describe --tags --abbrev=0').toString().trim()

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(`https://api.github.com/repos/nuxt/fonts/commits/${commit.shortHash}`, {
headers: {
'User-Agent': 'nuxt/fonts',
'Accept': 'application/vnd.github.v3+json',
'Authorization': `token ${process.env.GITHUB_TOKEN}`,
},
}).then(r => r.json() as Promise<{ author: { login: string, email: string } }>)
if (!contributors.some(c => c.username === author.login)) {
contributors.push({ name: commit.author.name, username: author.login })
}
emails.add(author.email)
}
return contributors
}
82 changes: 82 additions & 0 deletions scripts/update-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { execSync } from 'node:child_process'
import { inc } from 'semver'
import { generateMarkDown, getCurrentGitBranch, loadChangelogConfig } from 'changelogen'
import { determineBumpType, getContributors, getLatestCommits, loadWorkspace } from './_utils.ts'

async function main() {
const releaseBranch = 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()) || 'patch'

const newVersion = inc(workspace.find('@nuxt/telemetry').data.version, bumpType)
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 "daniel@roe.dev"')
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/telemetry/pulls?head=nuxt:v${newVersion}`).then(r => r.json())
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/telemetry/pulls', {
method: 'POST',
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
body: JSON.stringify({
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/telemetry/pulls/${currentPR.number}`, {
method: 'PATCH',
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`,
},
body: JSON.stringify({
body: releaseNotes,
}),
})
}

main().catch((err) => {
console.error(err)
process.exit(1)
})
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true
}
}