Skip to content

Commit ba2c256

Browse files
authored
Improve release proposal script (#5338)
This introduces two significant changes: 1. Don't suppress important errors. Some errors generated by the commands in the script should be ignored as they are just noise. Others however are important clues to setup and permission issues. 2. Improve requirements check, to ensure branch-diff has the expected permissions.
1 parent 9e985ff commit ba2c256

File tree

6 files changed

+99
-41
lines changed

6 files changed

+99
-41
lines changed

LICENSE-3rdparty.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, <ww
3737
dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
3838
dev,@msgpack/msgpack,ISC,Copyright 2019 The MessagePack Community
3939
dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
40+
dev,application-config-path,MIT,Copyright (c) 2015, 2023 Linus Unnebäck
4041
dev,autocannon,MIT,Copyright 2016 Matteo Collina
4142
dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4243
dev,axios,MIT,Copyright 2014-present Matt Zabriskie

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"@msgpack/msgpack": "^3.0.0-beta3",
123123
"@stylistic/eslint-plugin-js": "^3.0.1",
124124
"@types/node": "^16.0.0",
125+
"application-config-path": "^1.0.0",
125126
"autocannon": "^4.5.2",
126127
"aws-sdk": "^2.1446.0",
127128
"axios": "^1.7.4",

scripts/release/helpers/requirements.js

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22

33
/* eslint-disable @stylistic/js/max-len */
44

5+
const { join } = require('path')
6+
const { existsSync, readFileSync } = require('fs')
7+
const { default: getApplicationConfigPath } = require('application-config-path')
58
const { capture, fatal, run } = require('./terminal')
69

7-
const requiredScopes = ['public_repo', 'read:org']
8-
910
// Check that the `git` CLI is installed.
1011
function checkGit () {
1112
try {
12-
run('git --version')
13+
run('git --version', false)
1314
} catch (e) {
1415
fatal(
1516
'The "git" CLI could not be found.',
@@ -21,7 +22,7 @@ function checkGit () {
2122
// Check that the `branch-diff` CLI is installed.
2223
function checkBranchDiff () {
2324
try {
24-
run('branch-diff --version')
25+
run('branch-diff --version', false)
2526
} catch (e) {
2627
const link = [
2728
'https://datadoghq.atlassian.net/wiki/spaces/DL/pages/3125511269/Node.js+Tracer+Release+Process',
@@ -32,53 +33,72 @@ function checkBranchDiff () {
3233
`Please visit ${link} for instructions to install.`
3334
)
3435
}
36+
37+
const branchDiffConfigPath = join(getApplicationConfigPath('changelog-maker'), 'config.json')
38+
39+
if (!existsSync(branchDiffConfigPath)) {
40+
const link = 'https://github.com/nodejs/changelog-maker?tab=readme-ov-file#development'
41+
fatal(
42+
'The "branch-diff" configuration file is missing.',
43+
`Please visit ${link} for instructions to configure.`
44+
)
45+
}
46+
47+
const requiredScopes = ['public_repo']
48+
const { token } = JSON.parse(readFileSync(branchDiffConfigPath, 'utf8'))
49+
50+
checkGitHubScopes(token, requiredScopes, 'branch-diff')
3551
}
3652

3753
// Check that the `gh` CLI is installed and authenticated.
3854
function checkGitHub () {
39-
if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
55+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN
56+
const requiredScopes = ['public_repo', 'read:org']
57+
58+
if (!token) {
4059
const link = 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic'
4160

4261
fatal(
43-
'The GITHUB_TOKEN environment variable is missing.',
62+
'The GITHUB_TOKEN | GH_TOKEN environment variable is missing.',
4463
`Please visit ${link} for instructions to generate a personal access token.`,
4564
`The following scopes are required when generating the token: ${requiredScopes.join(', ')}`
4665
)
4766
}
4867

4968
try {
50-
run('gh --version')
69+
run('gh --version', false)
5170
} catch (e) {
5271
fatal(
5372
'The "gh" CLI could not be found.',
5473
'Please visit https://github.com/cli/cli#installation for instructions to install.'
5574
)
5675
}
5776

58-
checkGitHubScopes()
77+
checkGitHubScopes(token, requiredScopes, 'GITHUB_TOKEN | GH_TOKEN')
5978
}
6079

61-
// Check that the active GITHUB_TOKEN has the required scopes.
62-
function checkGitHubScopes () {
80+
function checkGitHubScopes (token, requiredScopes, source) {
6381
const url = 'https://api.github.com'
6482
const headers = [
6583
'Accept: application/vnd.github.v3+json',
66-
`Authorization: Bearer ${process.env.GITHUB_TOKEN || process.env.GH_TOKEN}`,
84+
`Authorization: Bearer ${token}`,
6785
'X-GitHub-Api-Version: 2022-11-28'
6886
].map(h => `-H "${h}"`).join(' ')
6987

70-
const lines = capture(`curl -sS -I ${headers} ${url}`).trim().split(/\r?\n/g)
88+
const lines = capture(`curl -sS -I ${headers} ${url}`).split(/\r?\n/g)
7189
const scopeLine = lines.find(line => line.startsWith('x-oauth-scopes:')) || ''
7290
const scopes = scopeLine.replace('x-oauth-scopes:', '').trim().split(', ')
73-
const link = 'https://github.com/settings/tokens'
91+
const missingScopes = []
7492

7593
for (const req of requiredScopes) {
76-
if (!scopes.includes(req)) {
77-
fatal(
78-
`Missing "${req}" scope for GITHUB_TOKEN.`,
79-
`Please visit ${link} and make sure the following scopes are enabled: ${requiredScopes.join(' ,')}.`
80-
)
81-
}
94+
if (!scopes.includes(req)) missingScopes.push(req)
95+
}
96+
97+
if (missingScopes.length !== 0) {
98+
fatal(
99+
`Missing scopes for ${source}: ${missingScopes.join(', ')}`,
100+
'Please visit https://github.com/settings/tokens and make sure they are enabled!'
101+
)
82102
}
83103
}
84104

scripts/release/helpers/terminal.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict'
22

3-
const { execSync, spawnSync } = require('child_process')
3+
const { spawnSync } = require('child_process')
44

55
const { params, flags } = parse()
66

@@ -23,19 +23,20 @@ let timer
2323
let current
2424

2525
// Output a command to the terminal and execute it.
26-
function run (cmd) {
27-
capture(cmd)
26+
function run (cmd, treatStderrAsFailure = true) {
27+
capture(cmd, treatStderrAsFailure)
2828
}
2929

3030
// Ask a question in terminal and return the response.
3131
function prompt (question) {
3232
print(`${BOLD}${CYAN}?${RESET} ${BOLD}${question}${RESET} `)
3333

34-
const child = spawnSync('bash', ['-c', 'read answer && echo $answer'], {
34+
const { stdout } = spawnSync('bash', ['-c', 'read answer && echo $answer'], {
35+
encoding: 'utf8',
3536
stdio: ['inherit']
3637
})
3738

38-
return child.stdout.toString()
39+
return stdout
3940
}
4041

4142
// Ask whether to continue and otherwise exit the process.
@@ -54,18 +55,49 @@ function checkpoint (question) {
5455
}
5556

5657
// Run a command and capture its output to return it to the caller.
57-
function capture (cmd) {
58+
function capture (cmd, treatStderrAsFailure = true) {
5859
if (flags.debug) {
5960
log(`${GRAY}> ${cmd}${RESET}`)
6061
}
6162

62-
const output = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }).toString().trim()
63+
const result = spawnSync(cmd, { encoding: 'utf8', shell: true })
64+
65+
if (result.error) throw result.error
66+
if (result.status !== 0) {
67+
const err = new Error(`Command failed: ${cmd}\n${result.stderr}`)
68+
for (const [k, v] of Object.entries(result)) {
69+
err[k] = v
70+
}
71+
throw err
72+
}
73+
74+
const stdout = result.stdout.trim()
75+
const stderr = result.stderr.trim()
6376

6477
if (flags.debug) {
65-
log(output)
78+
log(stdout)
79+
if (stderr) {
80+
if (flags['no-abort-on-error']) {
81+
log(`${RED}${stderr}${RESET}`)
82+
} else {
83+
fatal(
84+
stderr,
85+
'Aborting due to error! Use --no-abort-on-error to ignore and continue.'
86+
)
87+
}
88+
}
89+
} else if (treatStderrAsFailure && stderr) {
90+
if (flags['no-abort-on-error']) {
91+
log(`${RED}${stderr}${RESET}`)
92+
} else {
93+
fatal(
94+
stderr,
95+
'Aborting due to error! Use --no-abort-on-error to ignore and continue.'
96+
)
97+
}
6698
}
6799

68-
return output
100+
return stdout
69101
}
70102

71103
// Start an operation and show a spinner until it reports as passing or failing.

scripts/release/proposal.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,40 +50,39 @@ try {
5050
// Make sure the release branch is up to date to prepare for new proposal.
5151
// The main branch is not automatically pulled to avoid inconsistencies between
5252
// release lines if new commits are added to it during a release.
53-
run(`git checkout v${releaseLine}.x`)
54-
run('git pull --ff-only')
53+
run(`git checkout --quiet v${releaseLine}.x`)
54+
run('git pull --quiet --ff-only')
5555

5656
pass(`v${releaseLine}.x`)
5757

5858
const diffCmd = [
5959
'branch-diff',
6060
'--user DataDog',
6161
'--repo dd-trace-js',
62-
`--exclude-label=semver-major,dont-land-on-v${releaseLine}.x`
62+
`--exclude-label=semver-major,dont-land-on-v${releaseLine}.x`
6363
].join(' ')
6464

6565
start('Determine version increment')
6666

67-
const lastVersion = require('../../package.json').version
68-
const [, lastMinor, lastPatch] = lastVersion.split('.').map(Number)
67+
const { DD_MAJOR, DD_MINOR, DD_PATCH } = require('../../version')
6968
const lineDiff = capture(`${diffCmd} --markdown=true v${releaseLine}.x master`)
7069
const isMinor = flags.minor || (!flags.patch && lineDiff.includes('SEMVER-MINOR'))
7170
const newVersion = isMinor
72-
? `${releaseLine}.${lastMinor + 1}.0`
73-
: `${releaseLine}.${lastMinor}.${lastPatch + 1}`
71+
? `${releaseLine}.${DD_MINOR + 1}.0`
72+
: `${releaseLine}.${DD_MINOR}.${DD_PATCH + 1}`
7473
const notesDir = path.join(os.tmpdir(), 'release_notes')
7574
const notesFile = path.join(notesDir, `${newVersion}.md`)
7675

77-
pass(`${isMinor ? 'minor' : 'patch'} (${lastVersion} -> ${newVersion})`)
76+
pass(`${isMinor ? 'minor' : 'patch'} (${DD_MAJOR}.${DD_MINOR}.${DD_PATCH} -> ${newVersion})`)
7877

7978
start('Checkout release proposal branch')
8079

8180
// Checkout new or existing branch.
82-
run(`git checkout v${newVersion}-proposal || git checkout -b v${newVersion}-proposal`)
81+
run(`git checkout --quiet v${newVersion}-proposal &> /dev/null || git checkout --quiet -b v${newVersion}-proposal`)
8382

8483
try {
8584
// Pull latest changes in case the release was started by someone else.
86-
run(`git remote show origin | grep v${newVersion} && git pull --ff-only`)
85+
run(`git remote show origin | grep v${newVersion} && git pull --ff-only`, false)
8786
} catch (e) {
8887
// Either there is no remote to pull from or the local and remote branches
8988
// have diverged. In both cases we ignore the error and will just use our
@@ -95,7 +94,7 @@ try {
9594
start('Check for new changes')
9695

9796
// Get the hashes of the last version and the commits to add.
98-
const lastCommit = capture('git log -1 --pretty=%B').trim()
97+
const lastCommit = capture('git log -1 --pretty=%B')
9998
const proposalDiff = capture(`${diffCmd} --format=sha --reverse v${newVersion}-proposal master`)
10099
.replace(/\n/g, ' ').trim()
101100

@@ -114,7 +113,7 @@ try {
114113

115114
// Cherry pick all new commits to the proposal branch.
116115
try {
117-
run(`echo "${proposalDiff}" | xargs git cherry-pick`)
116+
run(`echo "${proposalDiff}" | xargs git cherry-pick`, false)
118117

119118
pass()
120119
} catch (err) {
@@ -148,7 +147,7 @@ try {
148147

149148
// Create or edit the PR. This will also automatically output a link to the PR.
150149
try {
151-
run(`gh pr create -d -B v${releaseLine}.x -t "v${newVersion} proposal" -F ${notesFile}`)
150+
run(`gh pr create -d -B v${releaseLine}.x -t "v${newVersion} proposal" -F ${notesFile}`, false)
152151
} catch (e) {
153152
// PR already exists so update instead.
154153
// TODO: Keep existing non-release-notes PR description if there is one.

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,11 @@ append-transform@^2.0.0:
11511151
dependencies:
11521152
default-require-extensions "^3.0.0"
11531153

1154+
application-config-path@^1.0.0:
1155+
version "1.0.0"
1156+
resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-1.0.0.tgz#9c25b8c00ac9a342db27275abd3f38c67bbe5a05"
1157+
integrity sha512-6ZDlLTlfqrTybVzZJDpX2K2ZufqyMyiTbOG06GpxmkmczFgTN+YYRGcTcMCXv/F5P5SrZijVjzzpPUE9BvheLg==
1158+
11541159
archy@^1.0.0:
11551160
version "1.0.0"
11561161
resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz"

0 commit comments

Comments
 (0)