Skip to content
This repository was archived by the owner on Mar 31, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ module.exports = {
extends: [
'plugin:github/es6'
],
parserOptions: {
ecmaVersion: 2018
},
rules: {
'no-console': 0
'no-console': 0,
'prefer-promise-reject-errors': 0
}
}
13 changes: 12 additions & 1 deletion entrypoint.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env node
const deploy = require('.')
const {DEFAULT_RETRIES} = deploy
const yargs = require('yargs')
.option('out', {
alias: 'o',
Expand All @@ -10,6 +12,16 @@ const yargs = require('yargs')
type: 'boolean',
describe: `Print the sequence of commands, but don't actually run anything`
})
.option('retries', {
alias: 'r',
type: 'number',
default: DEFAULT_RETRIES,
describe: 'Re-try deployment this number of times before giving up'
})
.option('verify', {
type: 'boolean',
describe: 'Unless provided, pass --no-verify to the Now CLI'
})
.option('help', {
alias: 'h',
type: 'boolean',
Expand All @@ -24,7 +36,6 @@ if (argv.help) {

const {promisify} = require('util')
const writeFile = promisify(require('fs').writeFile)
const deploy = require('.')

deploy(argv, argv._)
.then(res => {
Expand Down
71 changes: 62 additions & 9 deletions src/__tests__/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@ jest.mock('../now')
jest.mock('../read-json')
jest.mock('../alias-status')

now.mockImplementation(() => Promise.resolve('<mocked url>'))
aliasStatus.mockImplementation(() => Promise.resolve({}))

// the default now() mock implementation just returns a fake URL
const nowMockImpl = () => Promise.resolve('mocked-next-url.now.sh')
// we need to be sure to mock this before every test so that it
// doesn't *actually* run
now.mockImplementation(nowMockImpl)

describe('deploy()', () => {
let restoreEnv = () => {}

afterEach(() => {
restoreEnv()
aliasStatus.mockReset()
now.mockReset()
readJSON.mockReset()
now.mockReset()
})

it('calls now() once when GITHUB_REF is unset', () => {
Expand All @@ -33,7 +39,7 @@ describe('deploy()', () => {

return deploy().then(res => {
expect(now).toHaveBeenCalledTimes(1)
expect(now).toHaveBeenCalledWith([])
expect(now).toHaveBeenCalledWith(['--no-verify'])
expect(res).toEqual({name: 'foo', root, url: root})
})
})
Expand All @@ -52,7 +58,7 @@ describe('deploy()', () => {

return deploy().then(res => {
expect(now).toHaveBeenCalledTimes(2)
expect(now).toHaveBeenNthCalledWith(1, [])
expect(now).toHaveBeenNthCalledWith(1, ['--no-verify'])
expect(now).toHaveBeenNthCalledWith(2, ['alias', root, alias])
expect(res).toEqual({name: 'foo', root, alias, url: alias})
})
Expand All @@ -73,7 +79,7 @@ describe('deploy()', () => {

return deploy().then(res => {
expect(now).toHaveBeenCalledTimes(2)
expect(now).toHaveBeenNthCalledWith(1, [])
expect(now).toHaveBeenNthCalledWith(1, ['--no-verify'])
expect(now).toHaveBeenNthCalledWith(2, ['alias', root, alias])
expect(res).toEqual({name, root, url: alias})
})
Expand All @@ -95,7 +101,7 @@ describe('deploy()', () => {

return deploy().then(res => {
expect(now).toHaveBeenCalledTimes(2)
expect(now).toHaveBeenNthCalledWith(1, [])
expect(now).toHaveBeenNthCalledWith(1, ['--no-verify'])
expect(now).toHaveBeenNthCalledWith(2, ['alias', root, alias])
expect(res).toEqual({name: 'foo', root, alias, url: alias})
})
Expand All @@ -117,7 +123,7 @@ describe('deploy()', () => {

return deploy().then(res => {
expect(now).toHaveBeenCalledTimes(3)
expect(now).toHaveBeenNthCalledWith(1, [])
expect(now).toHaveBeenNthCalledWith(1, ['--no-verify'])
expect(now).toHaveBeenNthCalledWith(2, ['alias', root, alias])
expect(now).toHaveBeenNthCalledWith(3, ['alias', '-r', 'rules.json', prodAlias])
expect(res).toEqual({name: 'primer-style', root, alias, url: prodAlias})
Expand All @@ -138,7 +144,7 @@ describe('deploy()', () => {

return deploy({}, ['docs']).then(res => {
expect(now).toHaveBeenCalledTimes(2)
expect(now).toHaveBeenNthCalledWith(1, ['docs'])
expect(now).toHaveBeenNthCalledWith(1, ['--no-verify', 'docs'])
expect(now).toHaveBeenNthCalledWith(2, ['docs', 'alias', root, alias])
expect(res).toEqual({name: '@primer/css', root, alias, url: alias})
})
Expand All @@ -163,7 +169,7 @@ describe('deploy()', () => {

return deploy().then(res => {
expect(now).toHaveBeenCalledTimes(2)
expect(now).toHaveBeenNthCalledWith(1, [])
expect(now).toHaveBeenNthCalledWith(1, ['--no-verify'])
expect(now).toHaveBeenNthCalledWith(2, ['alias', root, alias])
expect(res).toEqual({name: 'derp', root, url: alias})
})
Expand All @@ -188,6 +194,53 @@ describe('deploy()', () => {
})
})

describe('resilience', () => {
it('retries up to 3 times', () => {
const url = 'third-times-a-charm.now.sh'
mockEnv({GITHUB_REF: ''})
now
.mockImplementationOnce(() => Promise.reject('simulated failure 1'))
.mockImplementationOnce(() => Promise.reject('simulated failure 2'))
.mockImplementationOnce(() => Promise.resolve(url))
return deploy().then(res => {
expect(res.url).toBe(url)
expect(now).toHaveBeenCalledTimes(3)
})
})

it('rejects after the third try', async () => {
const message = 'simulated failure'
mockEnv({GITHUB_REF: ''})
now.mockImplementation(() => Promise.reject(message))
await expect(deploy()).rejects.toBe(message)
expect(now).toHaveBeenCalledTimes(3)
})

it('respects the "retries" option', () => {
const url = 'five-times.now.sh'
mockEnv({GITHUB_REF: ''})
now
.mockImplementationOnce(() => Promise.reject('simulated failure 1'))
.mockImplementationOnce(() => Promise.reject('simulated failure 2'))
.mockImplementationOnce(() => Promise.reject('simulated failure 3'))
.mockImplementationOnce(() => Promise.reject('simulated failure 4'))
.mockImplementationOnce(() => Promise.resolve(url))
return deploy({retries: 5}).then(res => {
expect(res.url).toBe(url)
expect(now).toHaveBeenCalledTimes(5)
})
})
})

it('does *not* call `now --no-verify` when the "verify" option is truthy', () => {
mockEnv({GITHUB_REF: ''})
now.mockImplementation(nowMockImpl)
return deploy({verify: true}).then(() => {
expect(now).toHaveBeenCalledTimes(1)
expect(now).toHaveBeenNthCalledWith(1, [])
})
})

function mockEnv(env) {
restoreEnv = mockedEnv(env)
}
Expand Down
6 changes: 4 additions & 2 deletions src/__tests__/now.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const execa = require('execa')
const mockedEnv = require('mocked-env')
const now = require('../now')

const {NOW_BIN} = now

jest.mock('execa')
execa.mockImplementation(() => Promise.resolve({stderr: '', stdout: ''}))

Expand All @@ -17,13 +19,13 @@ describe('now()', () => {
it('calls `npx now --token=$NOW_TOKEN` with no additional args', () => {
mockEnv({NOW_TOKEN: 'xyz'})
now()
expect(execa).lastCalledWith('npx', ['now', '--token=xyz'], {stderr: 'inherit'})
expect(execa).lastCalledWith(NOW_BIN, ['--token=xyz'], {stderr: 'inherit'})
})

it('calls with additional arguments passed as an array', () => {
mockEnv({NOW_TOKEN: 'abc'})
now(['foo', 'bar'])
expect(execa).lastCalledWith('npx', ['now', '--token=abc', 'foo', 'bar'], {stderr: 'inherit'})
expect(execa).lastCalledWith(NOW_BIN, ['--token=abc', 'foo', 'bar'], {stderr: 'inherit'})
})

function mockEnv(env) {
Expand Down
10 changes: 9 additions & 1 deletion src/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ const getBranch = require('./get-branch')
const aliasStatus = require('./alias-status')
const getBranchAlias = require('./get-alias')
const readJSON = require('./read-json')
const retry = require('./retry')

const CONFIG_KEY = '@primer/deploy'
const DEFAULT_RETRIES = 3

module.exports = function deploy(options = {}, nowArgs = []) {
const {dryRun} = options
Expand All @@ -17,11 +19,15 @@ module.exports = function deploy(options = {}, nowArgs = []) {
const config = packageJson[CONFIG_KEY] || {}
const {releaseBranch = 'master'} = config

const configAndOptions = Object.assign({}, config, options)
const {verify = false, retries = DEFAULT_RETRIES} = configAndOptions

const name = nowJson.name || packageJson.name || dirname(process.cwd())
const branch = getBranch(name)

log(`deploying "${name}" with now...`)
return now(nowArgs)
const deployArgs = verify ? nowArgs : ['--no-verify', ...nowArgs]
return retry(() => now(deployArgs), retries)
.then(url => {
if (url) {
log(`root deployment: ${url}`)
Expand Down Expand Up @@ -71,6 +77,8 @@ module.exports = function deploy(options = {}, nowArgs = []) {
})
}

Object.assign(module.exports, {DEFAULT_RETRIES})

function log(message, ...args) {
console.warn(`[deploy] ${message}`, ...args)
}
Expand Down
11 changes: 9 additions & 2 deletions src/now.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
const execa = require('execa')

const {
bin: {now: NOW_BIN_PATH}
} = require('now/package.json')
const NOW_BIN = require.resolve(`now/${NOW_BIN_PATH}`)

module.exports = function now(args = []) {
const {NOW_TOKEN} = process.env
if (!NOW_TOKEN) {
throw new Error(`The NOW_TOKEN env var is required`)
}
const nowArgs = ['now', `--token=${NOW_TOKEN}`, ...args]
return execa('npx', nowArgs, {stderr: 'inherit'}).then(res => res.stdout)
const nowArgs = [`--token=${NOW_TOKEN}`, ...args]
return execa(NOW_BIN, nowArgs, {stderr: 'inherit'}).then(res => res.stdout)
}

Object.assign(module.exports, {NOW_BIN})
20 changes: 20 additions & 0 deletions src/retry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = function retry(fn, times) {
if (times < 0 || !isFinite(times)) {
throw new Error(`retry() expects a positive number of times; got: ${times}`)
}
let count = 0
return next()

function next() {
// console.warn(`[retry ${count}]`)
return fn().catch(error => {
if (++count < times) {
// console.warn(`[retry ${count} failed; trying again]`)
return next()
} else {
// console.warn(`[retry failed ${count} times]`)
throw error
}
})
}
}