Skip to content

Commit 0a411ee

Browse files
authored
add release proposal script for use locally (#4853)
1 parent 70e99bd commit 0a411ee

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ typings/
106106

107107
# End of https://www.gitignore.io/api/node,macos,visualstudiocode
108108

109+
.github/notes
109110
.next
110111
package-lock.json
111112
out

scripts/release/proposal.js

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict'
2+
3+
/* eslint-disable no-console */
4+
5+
// TODO: Support major versions.
6+
7+
const { execSync } = require('child_process')
8+
const fs = require('fs')
9+
const path = require('path')
10+
11+
// Helpers for colored output.
12+
const log = msg => console.log(msg)
13+
const success = msg => console.log(`\x1b[32m${msg}\x1b[0m`)
14+
const error = msg => console.log(`\x1b[31m${msg}\x1b[0m`)
15+
const whisper = msg => console.log(`\x1b[90m${msg}\x1b[0m`)
16+
17+
const currentBranch = capture('git branch --show-current')
18+
const releaseLine = process.argv[2]
19+
20+
// Validate release line argument.
21+
if (!releaseLine || releaseLine === 'help' || releaseLine === '--help') {
22+
log('Usage: node scripts/release/proposal <release-line> [release-type]')
23+
process.exit(0)
24+
} else if (!releaseLine?.match(/^\d+$/)) {
25+
error('Invalid release line. Must be a whole number.')
26+
process.exit(1)
27+
}
28+
29+
// Make sure the release branch is up to date to prepare for new proposal.
30+
// The main branch is not automatically pulled to avoid inconsistencies between
31+
// release lines if new commits are added to it during a release.
32+
run(`git checkout v${releaseLine}.x`)
33+
run('git pull')
34+
35+
const diffCmd = [
36+
'branch-diff',
37+
'--user DataDog',
38+
'--repo dd-trace-js',
39+
isActivePatch()
40+
? `--exclude-label=semver-major,semver-minor,dont-land-on-v${releaseLine}.x`
41+
: `--exclude-label=semver-major,dont-land-on-v${releaseLine}.x`
42+
].join(' ')
43+
44+
// Determine the new version.
45+
const [lastMajor, lastMinor, lastPatch] = require('../../package.json').version.split('.').map(Number)
46+
const lineDiff = capture(`${diffCmd} v${releaseLine}.x master`)
47+
const newVersion = lineDiff.includes('SEMVER-MINOR')
48+
? `${releaseLine}.${lastMinor + 1}.0`
49+
: `${releaseLine}.${lastMinor}.${lastPatch + 1}`
50+
51+
// Checkout new branch and output new changes.
52+
run(`git checkout v${newVersion}-proposal || git checkout -b v${newVersion}-proposal`)
53+
54+
// Get the hashes of the last version and the commits to add.
55+
const lastCommit = capture('git log -1 --pretty=%B').trim()
56+
const proposalDiff = capture(`${diffCmd} --format=sha --reverse v${newVersion}-proposal master`)
57+
.replace(/\n/g, ' ').trim()
58+
59+
if (proposalDiff) {
60+
// We have new commits to add, so revert the version commit if it exists.
61+
if (lastCommit === `v${newVersion}`) {
62+
run('git reset --hard HEAD~1')
63+
}
64+
65+
// Output new changes since last commit of the proposal branch.
66+
run(`${diffCmd} v${newVersion}-proposal master`)
67+
68+
// Cherry pick all new commits to the proposal branch.
69+
try {
70+
run(`echo "${proposalDiff}" | xargs git cherry-pick`)
71+
} catch (err) {
72+
error('Cherry-pick failed. Resolve the conflicts and run `git cherry-pick --continue` to continue.')
73+
error('When all conflicts have been resolved, run this script again.')
74+
process.exit(1)
75+
}
76+
}
77+
78+
// Update package.json with new version.
79+
run(`npm version --git-tag-version=false ${newVersion}`)
80+
run(`git commit -uno -m v${newVersion} package.json || exit 0`)
81+
82+
ready()
83+
84+
// Check if current branch is already an active patch proposal branch to avoid
85+
// creating a new minor proposal branch if new minor commits are added to the
86+
// main branch during a existing patch release.
87+
function isActivePatch () {
88+
const currentMatch = currentBranch.match(/^(\d+)\.(\d+)\.(\d+)-proposal$/)
89+
90+
if (currentMatch) {
91+
const [major, minor, patch] = currentMatch.slice(1).map(Number)
92+
93+
if (major === lastMajor && minor === lastMinor && patch > lastPatch) {
94+
return true
95+
}
96+
}
97+
98+
return false
99+
}
100+
101+
// Output a command to the terminal and execute it.
102+
function run (cmd) {
103+
whisper(`> ${cmd}`)
104+
105+
const output = execSync(cmd, {}).toString()
106+
107+
log(output)
108+
}
109+
110+
// Run a command and capture its output to return it to the caller.
111+
function capture (cmd) {
112+
return execSync(cmd, {}).toString()
113+
}
114+
115+
// Write release notes to a file that can be copied to the GitHub release.
116+
function ready () {
117+
const notesDir = path.join(__dirname, '..', '..', '.github', 'release_notes')
118+
const notesFile = path.join(notesDir, `${newVersion}.md`)
119+
const lineDiff = capture(`${diffCmd} --markdown=true v${releaseLine}.x master`)
120+
121+
fs.mkdirSync(notesDir, { recursive: true })
122+
fs.writeFileSync(notesFile, lineDiff)
123+
124+
success('Release proposal is ready.')
125+
success(`Changelog at .github/release_notes/${newVersion}.md`)
126+
127+
process.exit(0)
128+
}

0 commit comments

Comments
 (0)