Skip to content

Instantly share code, notes, and snippets.

@rynbyjn
Created November 20, 2020 23:37
Show Gist options
  • Save rynbyjn/465ff901a34172838c8b1d7ec121ad6e to your computer and use it in GitHub Desktop.
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
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