Created
November 20, 2020 23:37
-
-
Save rynbyjn/465ff901a34172838c8b1d7ec121ad6e to your computer and use it in GitHub Desktop.
Node script for bumping the major version for a React Native iOS app
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require('dotenv').config({ path: './.env' }) | |
const { execSync } = require('child_process') | |
const fs = require('fs') | |
const readline = require('readline') | |
// this should always be relative to this file | |
const packageFile = require('../package.json') | |
// get args passed to node | |
const args = process.argv | |
// passing the `--cleanup` arg will bail from creating the pull request and | |
// delete the created branch autommatically (good for testing updates to this) | |
const argCleanup = args.includes('--cleanup') | |
// passing `--force` will skip the questions and go straight to creating the | |
// version. this is mostly used on CI when a version branch is merged | |
const argForce = args.includes('--force') | |
// passing `--no-pr` will skip the creation of the PR | |
const argNoPR = args.includes('--no-pr') || argCleanup | |
// passing `--verbose` will print out all commands that are run | |
const argVerbose = args.includes('--verbose') | |
// passing `--version-1.x.x` will use 1.x.x instead of bumping the minor version | |
const argVersion = args.find((a) => a.includes('--version'))?.split('=')?.[1] | |
// allows us to ask questions on the command line | |
const inputInterface = readline.createInterface({ | |
input: process.stdin, | |
output: process.stdout, | |
}) | |
// helper to execute bash commands | |
const run = (command) => { | |
if (argVerbose) { | |
console.log(`Running \`${command}\`.`) | |
} | |
return execSync(command, { encoding: 'utf8' }) | |
} | |
// switch to project root so paths are correct | |
const projectRoot = run('git rev-parse --show-toplevel').trim() | |
process.chdir(projectRoot) | |
// only create new versions from latest `main` | |
const currentBranch = run('git rev-parse --abbrev-ref HEAD').trim() | |
if (currentBranch !== 'main') { | |
console.log('Switching branch to `main`.') | |
run('git checkout main') | |
} | |
console.log('Getting latest from `origin/main`.') | |
run('git pull origin main') | |
// get version information | |
const currentVersion = packageFile.version.trim() | |
const [major, minor, patch] = currentVersion.split('.') | |
let newVersion = argVersion || [major, parseInt(minor, 10) + 1, 0].join('.') | |
const newPatchVersion = [major, minor, parseInt(patch, 10) + 1].join('.') | |
const rnVersion = packageFile.dependencies['react-native'] | |
const xcVersion = run('xcodebuild -version')?.match(/(\d+\..+)/)?.[1] | |
const updatePackageVersion = () => { | |
packageFile.version = newVersion | |
fs.writeFileSync('package.json', JSON.stringify(packageFile, undefined, 2), { | |
encoding: 'utf8', | |
flag: 'w', | |
}) | |
} | |
const updateXcodeVersion = () => { | |
run(`cd ios/ && xcrun agvtool new-marketing-version ${newVersion} && cd ../`) | |
} | |
const changelogTemplate = ( | |
nV = newVersion, | |
rnV = rnVersion, | |
xcV = xcVersion, | |
) => ` | |
# ${nV} π | |
## Built With π | |
- React Native ${rnV}, Xcode ${xcV} | |
### New Features β¨ | |
- N/A | |
### Bug Fixes π | |
- N/A | |
### Chores π | |
- N/A | |
` | |
const writeChangelog = () => { | |
const changelog = fs.readFileSync('CHANGELOG.md', 'utf8') | |
const changelogArr = changelog.split('\n') | |
changelogArr[1] = changelogTemplate() | |
fs.writeFileSync('CHANGELOG.md', changelogArr.join('\n'), { | |
encoding: 'utf8', | |
flag: 'w', | |
}) | |
} | |
const commitChanges = () => { | |
console.log(`Committing updates for v${newVersion}.`) | |
const commitMsg = `v${newVersion}\n\nThis is a base branch for all features/bugs/chores that should make it into version v${newVersion}. It is huge and the code has been reviewed in the PRs that were merged into this. Reviews should consist of making sure the commits are good to go and make sense for what's in the CHANGELOG.` | |
run(`git commit -am "${commitMsg}"`) | |
} | |
// function to cleanup branch and tag when passing `--cleanup` | |
const cleanupBranch = () => { | |
console.log(`Deleting branch v${newVersion}.`) | |
// checkout main | |
run('git checkout main') | |
// delete branch if it exists | |
if (run(`git branch --list v${newVersion}`)) { | |
run(`git branch -D v${newVersion}`) | |
} | |
// delete tag if it exists | |
if (run(`git tag --list v${newVersion}`)) { | |
console.log(`Deleting tag v${newVersion}.`) | |
run(`git tag -d v${newVersion}`) | |
} | |
} | |
const createPullRequest = () => { | |
console.log(`Creating Pull Request for v${newVersion}.`) | |
run(`git push origin v${newVersion}`) | |
run('brew list gh || brew install gh') | |
const flags = [ | |
'--base main', | |
'--draft', | |
'--fill', | |
`--head v${newVersion}`, | |
'--label "Hold Merging,WIP"', | |
] | |
run(`gh pr create ${flags.join(' ')}`) | |
} | |
// function to do the version updating | |
const createNewVersion = () => { | |
console.log(`Bumping version v${currentVersion} ~> v${newVersion}.`) | |
console.log( | |
`Using React Native version ${rnVersion} and Xcode version ${xcVersion}`, | |
) | |
console.log(`Creating new branch v${newVersion}`) | |
run(`git checkout -b v${newVersion}`) | |
updatePackageVersion() | |
updateXcodeVersion() | |
writeChangelog() | |
commitChanges() | |
if (argCleanup) { | |
cleanupBranch() | |
} else if (!argNoPR) { | |
createPullRequest() | |
} | |
process.exit(0) | |
} | |
// this allows the process to exit if questions aren't answered within the | |
// timeout of 10 seconds | |
const TIMEOUT = 10 | |
const askQuestion = (question, answerFn) => { | |
const timeoutId = setTimeout(() => { | |
console.log( | |
`\n\nScript timed out, please answer within ${TIMEOUT} seconds.`, | |
) | |
inputInterface.close() | |
process.exit(0) | |
}, TIMEOUT * 1000) | |
inputInterface.question(`${question} (Y|n): `, (answer) => { | |
clearTimeout(timeoutId) | |
answerFn(answer) | |
}) | |
} | |
const checkIfBranchExists = () => { | |
const localBranchExists = run(`git branch --list v${newVersion}`) | |
const remoteBranchExists = run(`git ls-remote --heads origin v${newVersion}`) | |
if (localBranchExists || remoteBranchExists) { | |
if (remoteBranchExists) { | |
console.log( | |
`Remote branch v${newVersion} already exists! This process will have to be done manually. Exiting...`, | |
) | |
process.exit(0) | |
} else if (localBranchExists) { | |
console.log(`Local branch v${newVersion} already exists!`) | |
if (argForce) { | |
console.log('Exiting...') | |
process.exit(0) | |
} | |
askQuestion('Would you like to delete this branch?', (answer) => { | |
if (answer.toLowerCase() === 'y') { | |
run(`git branch -D v${newVersion}`) | |
inputInterface.close() | |
createNewVersion() | |
} else { | |
process.exit(0) | |
} | |
}) | |
} | |
} else { | |
createNewVersion() | |
} | |
} | |
const askToCreatePatchVersion = () => { | |
askQuestion( | |
`Would you like to create a patch from the current version? This will create version v${newPatchVersion}.`, | |
(answer) => { | |
// setTimeout to bail | |
if (answer.toLowerCase() === 'y') { | |
newVersion = newPatchVersion | |
inputInterface.close() | |
checkIfBranchExists() | |
} else { | |
console.log( | |
'You can create a specific version by passing `--version=1.x.x` to this command.', | |
) | |
process.exit(0) | |
} | |
}, | |
) | |
} | |
const askIfVersionIsCorrect = () => { | |
askQuestion( | |
`You are about to create a new minor version \`v${newVersion}\` do you wish to proceed?`, | |
(answer) => { | |
if (answer.toLowerCase() === 'y') { | |
inputInterface.close() | |
checkIfBranchExists() | |
} else { | |
askToCreatePatchVersion() | |
} | |
}, | |
) | |
} | |
if (argForce) { | |
// skip the initial questions, but don't do this if the branch exists | |
checkIfBranchExists() | |
} else { | |
askIfVersionIsCorrect() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment