Skip to content

Commit 15c230a

Browse files
chore: fix changelog
1 parent 3b500ce commit 15c230a

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

scripts/_utils.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { promises as fsp } from 'node:fs'
2+
import process from 'node:process'
3+
import { resolve } from 'pathe'
4+
import { execaSync } from 'execa'
5+
import {
6+
determineSemverChange,
7+
getGitDiff,
8+
loadChangelogConfig,
9+
parseCommits,
10+
} from 'changelogen'
11+
12+
export interface Dep {
13+
name: string
14+
range: string
15+
type: string
16+
}
17+
18+
type ThenArg<T> = T extends PromiseLike<infer U> ? U : T
19+
export type Package = ThenArg<ReturnType<typeof loadPackage>>
20+
21+
export async function loadPackage(dir: string) {
22+
const pkgPath = resolve(dir, 'package.json')
23+
const data = JSON.parse(
24+
await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'),
25+
)
26+
const save = () =>
27+
fsp.writeFile(pkgPath, `${JSON.stringify(data, null, 2)}\n`)
28+
29+
const updateDeps = (reviver: (dep: Dep) => Dep | void) => {
30+
for (const type of [
31+
'dependencies',
32+
'devDependencies',
33+
'optionalDependencies',
34+
'peerDependencies',
35+
]) {
36+
if (!data[type])
37+
continue
38+
for (const e of Object.entries(data[type])) {
39+
const dep: Dep = { name: e[0], range: e[1] as string, type }
40+
delete data[type][dep.name]
41+
const updated = reviver(dep) || dep
42+
data[updated.type] = data[updated.type] || {}
43+
data[updated.type][updated.name] = updated.range
44+
}
45+
}
46+
}
47+
48+
return {
49+
dir,
50+
data,
51+
save,
52+
updateDeps,
53+
}
54+
}
55+
56+
export async function loadWorkspace(dir: string) {
57+
const workspacePkg = await loadPackage(dir)
58+
59+
const rename = (from: string, to: string) => {
60+
workspacePkg.data._name = workspacePkg.data.name
61+
workspacePkg.data.name = to
62+
workspacePkg.updateDeps((dep) => {
63+
if (dep.name === from && !dep.range.startsWith('npm:'))
64+
dep.range = `npm:${to}@${dep.range}`
65+
})
66+
}
67+
68+
const setVersion = (
69+
newVersion: string,
70+
// eslint-disable-next-line unused-imports/no-unused-vars
71+
opts: { updateDeps?: boolean } = {},
72+
) => {
73+
workspacePkg.data.version = newVersion
74+
}
75+
76+
const save = () => workspacePkg.save()
77+
78+
return {
79+
dir,
80+
workspacePkg,
81+
save,
82+
rename,
83+
setVersion,
84+
}
85+
}
86+
87+
export async function determineBumpType() {
88+
const config = await loadChangelogConfig(process.cwd())
89+
const commits = await getLatestCommits()
90+
91+
const bumpType = determineSemverChange(commits, config)
92+
93+
return bumpType === 'major' ? 'minor' : bumpType
94+
}
95+
96+
export async function getLatestCommits() {
97+
const config = await loadChangelogConfig(process.cwd())
98+
const latestTag = execaSync('git', [
99+
'describe',
100+
'--tags',
101+
'--abbrev=0',
102+
]).stdout
103+
104+
return parseCommits(await getGitDiff(latestTag), config)
105+
}

scripts/update-changelog.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { execSync } from 'node:child_process'
2+
import process from 'node:process'
3+
import { $fetch } from 'ofetch'
4+
import { inc } from 'semver'
5+
import { generateMarkDown, loadChangelogConfig } from 'changelogen'
6+
import { determineBumpType, getLatestCommits, loadWorkspace } from './_utils'
7+
8+
async function main() {
9+
const workspace = await loadWorkspace(process.cwd())
10+
const config = await loadChangelogConfig(process.cwd())
11+
12+
const commits = await getLatestCommits().then(commits => commits.filter(
13+
c => config.types[c.type] && !(c.type === 'chore' && c.scope === 'deps' && !c.isBreaking),
14+
))
15+
const bumpType = await determineBumpType()
16+
17+
const newVersion = inc(workspace.workspacePkg.data.version, bumpType || 'patch')
18+
const changelog = await generateMarkDown(commits, config)
19+
20+
// Create and push a branch with bumped versions if it has not already been created
21+
const branchExists = execSync(`git ls-remote --heads origin v${newVersion}`).toString().trim().length > 0
22+
if (!branchExists) {
23+
execSync('git config --global user.email "hi@productdevbook.com"')
24+
execSync('git config --global user.name "productdevbook"')
25+
execSync(`git checkout -b v${newVersion}`)
26+
27+
workspace.setVersion(newVersion!)
28+
29+
await workspace.save()
30+
31+
execSync(`git commit -am v${newVersion}`)
32+
execSync(`git push -u origin v${newVersion}`)
33+
}
34+
35+
// Get the current PR for this release, if it exists
36+
const [currentPR] = await $fetch(`https://api.github.com/repos/oku-ui/primitives/pulls?head=oku-ui:v${newVersion}`)
37+
38+
const releaseNotes = [
39+
currentPR?.body.replace(/## 👉 Changelog[\s\S]*$/, '') || `> ${newVersion} is the next ${bumpType} release.\n>\n> **Timetable**: to be announced.`,
40+
'## 👉 Changelog',
41+
changelog.replace(/^## v.*?\n/, '').replace('...main', `...v${newVersion}`),
42+
].join('\n')
43+
44+
// Create a PR with release notes if none exists
45+
if (!currentPR) {
46+
return await $fetch('https://api.github.com/repos/oku-ui/primitives/pulls', {
47+
method: 'POST',
48+
headers: {
49+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
50+
},
51+
body: {
52+
title: `v${newVersion}`,
53+
head: `v${newVersion}`,
54+
base: 'main',
55+
body: releaseNotes,
56+
draft: true,
57+
},
58+
})
59+
}
60+
61+
// Update release notes if the pull request does exist
62+
await $fetch(`https://api.github.com/repos/oku-ui/primitives/pulls/${currentPR.number}`, {
63+
method: 'PATCH',
64+
headers: {
65+
Authorization: `token ${process.env.GITHUB_TOKEN}`,
66+
},
67+
body: {
68+
body: releaseNotes,
69+
},
70+
})
71+
}
72+
73+
main().catch((err) => {
74+
console.error(err)
75+
process.exit(1)
76+
})

0 commit comments

Comments
 (0)