Skip to content

Commit

Permalink
feat(deploy): add detailed checks and logging of configuration
Browse files Browse the repository at this point in the history
If deploying using a folder from the configurations repo, numerous checks and more logging will be performed given information about the configurations
  • Loading branch information
evansiroky committed Sep 4, 2019
1 parent 165dea0 commit 4259d66
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 107 deletions.
226 changes: 158 additions & 68 deletions bin/mastarm-deploy
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
const path = require('path')

const commander = require('commander')
const execa = require('execa')
const origin = require('git-remote-origin-url')
const repoInfo = require('git-repo-info')
const commit = require('this-commit')()
const username = require('username')

const build = require('../lib/build')
const {readFile} = require('../lib/fs-promise')
const {readFile, writeFile} = require('../lib/fs-promise')
const loadConfig = require('../lib/load-config')
const logger = require('../lib/logger')
const pkg = require('../lib/pkg')
Expand All @@ -28,86 +31,157 @@ commander
.option('--s3bucket', 'S3 Bucket to push to.')
.parse(process.argv)

const url = pkg.repository.url.replace('.git', '')
const tag = `<${url}/commit/${commit}|${pkg.name}@${commit.slice(0, 6)}>`
const config = loadConfig(process.cwd(), commander.config, commander.env)
const get = util.makeGetFn([commander, config.settings])
// each of these variables are also used in the logToMsTeams function and
// these need to be defined after potentially decoding a sops-encoded file
let cloudfront, config, env, minify, s3bucket, tag, url

if (config.env.SLACK_WEBHOOK && config.env.SLACK_WEBHOOK.length > 0) {
logger.logToSlack({
channel: config.env.SLACK_CHANNEL || '#devops',
webhook: config.env.SLACK_WEBHOOK
})
}
async function deploy () {
// get information about the config directory being used
const repoUrl = await origin(commander.config)
let configCommit, configRemoteUrl
const configRepoRoot = repoInfo(commander.config).root
const configDir = path
.resolve(commander.config)
.replace(configRepoRoot, '')

const files = util.parseEntries([...commander.args, ...(get('entries') || [])])
util.assertEntriesExist(files)
const sourceFiles = files.map(f => f[0])
const outfiles = [...files.map(f => f[1]), ...files.map(f => `${f[1]}.map`)]

const env = get('env') || 'development'
const minify = get('minify')
const buildOpts = {
config,
env,
files,
minify
}
const cloudfront = get('cloudfront')
const s3bucket = get('s3bucket')
// check if the config directory being used is a configurations repo
if (repoUrl.endsWith('/configurations.git')) {
// run some extra checks to make sure configurations repo is up to date
// modified from https://stackoverflow.com/a/3278427/269834
configRemoteUrl = repoUrl.replace('.git', '')
const { stdout: local } = await execa('git', ['-C', configRepoRoot, 'rev-parse', '@'])
const { stdout: remote } = await execa('git', ['-C', configRepoRoot, 'rev-parse', '@{u}'])
const { stdout: base } = await execa('git', ['-C', configRepoRoot, 'merge-base', '@', '@{u}'])
let configurationsOk = false
if (local === remote) {
// Up-to-date
console.log('Configurations up-to-date')
configurationsOk = true
} else if (local === base) {
console.error('Configurations out of sync: Need to pull')
} else if (remote === base) {
console.error('Configurations out of sync: Need to push')
} else {
console.error('Configurations out of sync: Diverged')
}

// make sure there are no changes to the configurations
const { stdout: status } = await execa(
'git',
[
'-C',
configRepoRoot,
'status',
'-s'
]
)
if (status === '') {
console.log('No changes to configurations repo')
} else {
console.error('Configurations out of sync: local changes exist that are not yet committed')
configurationsOk = false
}

if (!configurationsOk) process.exit(1)

configCommit = local

// decrypt env file using sops to make sure old file is overwritten with
// data from encoded sops file
const configPath = path.resolve(commander.config)
console.log('decrypting env file with sops')
const {stdout} = await execa(
'sops',
[
'-d',
path.join(configPath, 'env.enc.yml')
]
)
await writeFile(path.join(configPath, 'env.yml'), stdout)
// at this point, we can be certain that the local configurations repo
// directory matches what has been committed and pushed to the remote repo
}

url = pkg.repository.url.replace('.git', '')
tag = `<${url}/commit/${commit}|${pkg.name}@${commit.slice(0, 6)}>`
config = loadConfig(process.cwd(), commander.config, commander.env)
const get = util.makeGetFn([commander, config.settings])

if (config.env.SLACK_WEBHOOK && config.env.SLACK_WEBHOOK.length > 0) {
logger.logToSlack({
channel: config.env.SLACK_CHANNEL || '#devops',
webhook: config.env.SLACK_WEBHOOK
})
}

const files = util.parseEntries([...commander.args, ...(get('entries') || [])])
util.assertEntriesExist(files)
const sourceFiles = files.map(f => f[0])
const outfiles = [...files.map(f => f[1]), ...files.map(f => `${f[1]}.map`)]

const pushToS3 = createPushToS3({
cloudfront,
s3bucket
})
env = get('env') || 'development'
minify = get('minify')
const buildOpts = {
config,
env,
files,
minify
}
cloudfront = get('cloudfront')
s3bucket = get('s3bucket')

logger
.log(
const pushToS3 = createPushToS3({
cloudfront,
s3bucket
})

await logger.log(
`:construction: *deploying: ${tag} by <@${username.sync()}>*
:vertical_traffic_light: *mastarm:* v${mastarmVersion}
:cloud: *cloudfront:* ${cloudfront}
:hash: *commit:* ${commit}
:seedling: *env:* ${env}
:compression: *minify:* ${minify}
:package: *s3bucket:* ${s3bucket}
:hammer_and_wrench: *building:* ${sourceFiles.join(', ')}`
:vertical_traffic_light: *mastarm:* v${mastarmVersion}
:cloud: *cloudfront:* ${cloudfront}
:hash: *commit:* ${commit}
:seedling: *env:* ${env}
:compression: *minify:* ${minify}
:package: *s3bucket:* ${s3bucket}
:hammer_and_wrench: *building:* ${sourceFiles.join(', ')}`
)
.then(() =>
build(buildOpts)
.then(() =>
logger.log(`:rocket: *uploading:* ${sourceFiles.length * 2} file(s)`)
)
.then(() =>
Promise.all(
outfiles.map(outfile =>
readFile(outfile).then(body => pushToS3({body, outfile}))
)
)
)
.then(() =>
logger
.log(
`:tada: :confetti_ball: :tada: *deploy ${tag} complete* :tada: :confetti_ball: :tada:`
)
.then(() => logToMsTeams())
.then(() => process.exit(0))
)
.catch(err =>
logger
.log(
`:rotating_light: *${tag} error deploying ${tag} ${err.message || err}*`
)
.then(() => logToMsTeams(err))
.then(() => process.exit(1))

try {
await build(buildOpts)
await logger.log(`:rocket: *uploading:* ${sourceFiles.length * 2} file(s)`)
await Promise.all(
outfiles.map(outfile =>
readFile(outfile).then(body => pushToS3({body, outfile}))
)
)
)
await logger.log(
`:tada: :confetti_ball: :tada: *deploy ${tag} complete* :tada: :confetti_ball: :tada:`
)
await logToMsTeams({ configCommit, configDir, configRemoteUrl })
process.exit(0)
} catch (error) {
await logger.log(
`:rotating_light: *${tag} error deploying ${tag} ${error.message || error}*`
)
await logToMsTeams({ configCommit, configDir, configRemoteUrl, error })
process.exit(1)
}
}

deploy()

/**
* Sends a card to MS Teams with information about the deployment
* @param {[string]} configCommit hash of the commit in the configurations
* repo (if it exists)
* @param {[string]} configDir partial path to specific config directory used
* to deploy
* @param {[string]} configRemoteUrl base url for the configurations repo
* (if it exists)
* @param {[Error]} error the error, if one occurred. A falsy value indicates
* success
*/
function logToMsTeams (error) {
function logToMsTeams ({ configCommit, configDir, configRemoteUrl, error }) {
if (!config.env.MS_TEAMS_WEBHOOK) return Promise.resolve()

const potentialAction = [{
Expand All @@ -120,8 +194,24 @@ function logToMsTeams (error) {
}
]
}]
if (configCommit && configRemoteUrl) {
potentialAction.push({
'@type': 'OpenUri',
name: `View Config Commit on Github`,
targets: [
{
os: 'default',
uri: `${configRemoteUrl}/tree/${configCommit}/${configDir}`
}
]
})
}
const text = `📄 *commit:* ${pkg.name}@${commit.slice(0, 6)}\n
👤 *deployed by:* ${username.sync()}\n
${configCommit
? `🎛️ *config:* configurations@${configCommit.slice(0, 6)}\n
📂 *config folder:* ${configDir}\n` // improper indenting here needed to properly format on MS Teams
: '🎛️ *config:* unknown configuration data!\n'}
🚦 *mastarm:* v${mastarmVersion}\n
☁️ *cloudfront:* ${cloudfront}\n
🌱 *env:* ${env}\n
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@
"eslint-plugin-promise": "^4.0.1",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-standard": "^4.0.0",
"execa": "^2.0.4",
"exorcist": "^1.0.1",
"flow-bin": "0.84.0",
"flow-runtime": "^0.17.0",
"git-remote-origin-url": "^3.0.0",
"git-repo-info": "^2.1.0",
"glob": "^7.1.3",
"isomorphic-fetch": "^2.2.1",
"jest": "^24.1.0",
Expand Down
Loading

0 comments on commit 4259d66

Please sign in to comment.