|
| 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