#!/usr/bin/env node const childProcess = require('child_process') const path = require('path') const mkdirp = require('mkdirp') const semver = require('semver') const program = require('commander') const TMP_DIR = path.join(__dirname, '..', 'tmp') async function spawn(cmd, args, opts = {}) { return new Promise((resolve, reject) => { const env = Object.assign({}, process.env, opts.env || {}) delete opts.env const options = Object.assign({stdio: 'inherit', env}, opts); const proc = childProcess.spawn(cmd, args, options) proc.on("error", reject) proc.on("exit", resolve) }) } function exec(cmd, opts = {}) { return new Promise((resolve, reject) => { childProcess.exec(cmd, opts, (err, stdout) => { if (err) { return reject(err) } return resolve(stdout) }) }) } function git(subCmd, opts = {}) { const optsString = Object.keys(opts).reduce((prev, key) => { const optVal = opts[key] if (optVal == null) { return key.length > 1 ? `${prev} --${key}` : `${prev} -${key}` } return key.length > 1 ? `${prev} --${key}=${optVal}` : `${prev} -${key} ${optVal}` }, '') return exec(`git ${subCmd} ${optsString}`, {cwd: './'}) } async function prependToFile(filepath, string) { await exec(`echo "${string}" > ${TMP_DIR}/tmpfile`) await exec(`cat ${filepath} >> ${TMP_DIR}/tmpfile`) await exec(`mv ${TMP_DIR}/tmpfile ${filepath}`) } async function sliceFileLines(filepath, idx) { await exec(`tail -n +${1 + idx} ${filepath} > ${TMP_DIR}/tmpfile`) await exec(`mv ${TMP_DIR}/tmpfile ${filepath}`) } async function updateChangelogFile(changelogString) { mkdirp.sync(TMP_DIR) await sliceFileLines('./app/CHANGELOG.md', 2) await prependToFile('./app/CHANGELOG.md', changelogString) } function getFormattedLogs(mainLog) { const formattedMainLog = ( mainLog .filter(line => line.length > 0) .filter(line => !/^bump/i.test(line) && !/changelog/i.test(line)) .map(line => ` + ${line.replace('*', '\\*')}`) .join('\n') ) return `${formattedMainLog}\n` } function getChangelogHeader(nextVersion) { const date = new Date().toLocaleDateString() return ( `# Mailspring Changelog ### ${nextVersion} (${date}) ` ) } function validateArgs(args) { if (args.editChangelog && !process.env.EDITOR) { throw new Error(`You can't edit the changelog without a default EDITOR in your env`) } } // TODO add progress indicators with ora // TODO add options // --update-daily-channel // --notify // --quiet async function main(args) { validateArgs(args) // Pull latest changes try { await git(`checkout master`) await git(`pull --rebase`) } catch (err) { console.error(err) process.exit(1) } const pkg = require('../app/package.json') //eslint-disable-line const currentVersion = pkg.version const nextVersion = semver.inc(currentVersion, 'patch') // Make sure working directory is clean try { await exec('git diff --exit-code && git diff --cached --exit-code') } catch (err) { console.error('Git working directory is not clean!') process.exit(1) } // Make sure there is a diff to build let mainLog = ''; try { mainLog = (await git(`log ${currentVersion}..master --format='%s'`)).split('\n') if (mainLog.length <= 1) { console.error(`There are no changes to build since ${currentVersion}`) process.exit(1) } } catch (err) { console.error(err) process.exit(1) } // Update CHANGELOG try { const commitLogSinceLatestVersion = await getFormattedLogs(mainLog) const changelogHeader = getChangelogHeader(nextVersion) const changelogString = `${changelogHeader}${commitLogSinceLatestVersion}` await updateChangelogFile(changelogString) console.log(changelogString) } catch (err) { console.error('Could not update changelog file') console.error(err) process.exit(1) } // Allow editing if (args.editChangelog) { try { await spawn(process.env.EDITOR, ['./app/CHANGELOG.md'], {stdio: 'inherit'}) } catch (err) { console.error('Error editing CHANGELOG.md') console.error(err) process.exit(1) } } // Bump patch version in package.json try { await exec('npm --no-git-tag-version version patch', {cwd: './app'}) } catch (err) { console.error('Could not bump version in package.json') console.error(err) process.exit(1) } if (args.noCommit) { return } // Commit changes try { await git('add .') await git(`commit -m 'bump(version): ${nextVersion}'`) } catch (err) { console.error('Could not commit changes') console.error(err) process.exit(1) } if (args.noTag) { return } // Tag commit try { await git(`tag ${nextVersion}`) } catch (err) { console.error('Could not tag commit') console.error(err) process.exit(1) } if (args.noPush) { return } // Push changes try { await git(`push origin master --tags`) } catch (err) { console.error('Could not tag commit') console.error(err) process.exit(1) } // Build locally. This should only be used when building from our in-office // coffee machine mac mini if (args.build) { try { await spawn('git', ['clean', '-xdf']) await spawn('cp', ['-r', '../n1-keys-and-certificates', 'app/build/resources/certs']) await spawn('npm', ['install'], {env: {}}) await spawn('npm', ['run', 'build-client'], {env: {SIGN_BUILD: true}}) await spawn('codesign', ['--verify', '--deep', '--verbose=2', '"app/dist/Mailspring-darwin-x64/Mailspring.app"']) // await spawn('npm', ['run', 'upload-client']) } catch (err) { console.error('Errored while running build') console.error(err) process.exit(1) } // TODO Update `daily` channel // TODO send out notification email } console.log('Done!') } program .version('0.0.1') .usage('[options]') .description('This script will bump the version in package.json, edit the changelog with the latest\n git log (for easier editing), commit and tag the changes, and push to Github to trigger\n a build') .option('--edit-changelog', 'Open your $EDITOR to edit CHANGELOG before commiting version bump.') .option('--no-commit', 'Wether to commit changes to CHANGELOG.md and package.json') .option('--no-tag', 'Wether to tag the version bump commit (no-op if --no-commit is used)') .option('--no-push', 'Wether to push changes to the Github remote') .option('--build', 'Wether to build the app locally. This should only be used when building from our in-office Mac Mini by the coffee machine') .parse(process.argv) main(program)