Skip to content

Commit 5be1b1f

Browse files
authored
Merge pull request #290 from conveyal/dev
Minor release
2 parents 93a2cb6 + a275e69 commit 5be1b1f

File tree

11 files changed

+369
-127
lines changed

11 files changed

+369
-127
lines changed

README.md

+53-11
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ SLACK_CHANNEL: '#devops'
155155
SLACK_WEBHOOK: https://hooks.slack.com/services/fake-code
156156
```
157157

158+
#### MS Teams Notifications
159+
160+
To enable an MS Teams notification upon the completion (successful or not) of the deploy process, create a [MS Teams Webhook](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/connectors/connectors-using#setting-up-a-custom-incoming-webhook) and add the incoming webhook url as the `MS_TEAMS_WEBHOOK` key/value to your `env.yml`.
161+
162+
```
163+
MS_TEAMS_WEBHOOK: https://outlook.office.com/webhook/123...abc
164+
```
165+
158166
### `flow`
159167

160168
Run [Flow](https://flow.org/). Must have a `.flowconfig` in the current working directory and a `// @flow` annotation at the top of each file you want to check. See the Flow website for documentation.
@@ -199,7 +207,7 @@ $ mastarm prepublish lib:build
199207

200208
### `test`
201209

202-
Run the [Jest](http://facebook.github.io/jest/) test runner on your project. By default, mastarm will run Jest and generate coverage reports on all .js files in the `lib` folder of your project. The `patterns` argument will make Jest run only tests whose filename match the provided pattern.
210+
Run the [Jest](http://facebook.github.io/jest/) test runner on your project.
203211

204212
```shell
205213
$ mastarm test
@@ -210,17 +218,51 @@ Run tests using Jest
210218

211219
Options:
212220

213-
-h, --help output usage information
214-
-u, --update-snapshots Force update of snapshots. USE WITH CAUTION.
215-
--coverage Run Jest with coverage reporting
216-
--coverage-paths <paths> Extra paths to collect code coverage from
217-
--jest-cli-args <args> Extra arguments to pass directly to the Jest Cli. Make sure to encapsulate all extra arguments in quote
218-
--no-cache Run Jest without cache (defaults to using cache)
219-
--run-in-band Run all tests serially in the current process
220-
--setup-files <paths> Setup files to run before each test
221-
--test-environment <env> Jest test environment to use (Jest default is jsdom)
222-
--test-path-ignore-patterns <patterns> File patterns to ignore when scanning for test files
221+
-c, --config <path> Path to configuration files. (default: path.join(process.cwd() + '/configurations/default'))
222+
-e, --env <environment> Environment to use.
223+
-u, --update-snapshots Force update of snapshots. USE WITH CAUTION.
224+
--coverage Run Jest with coverage reporting
225+
--coverage-paths <paths> Extra paths to collect code coverage from in addition to the mastarm default of `lib/**/*.js`
226+
--custom-config-file <path> Override the Jest config with the values found in a file path relative to the current working directory
227+
--force-exit Force Jest to exit after all tests have completed running.
228+
--jest-cli-args <args> Extra arguments to pass directly to the Jest Cli. Make sure to encapsulate all extra arguments in quotes
229+
--no-cache Run Jest without cache (defaults to using cache)
230+
--run-in-band Run all tests serially in the current process. This is always set to true while running on in a continuous integration environment.
231+
--setup-files <paths> Setup files to run before each test
232+
--test-environment <env> Jest test environment to use (Jest default is jsdom)
233+
--test-path-ignore-patterns <patterns> File patterns to ignore when scanning for test files
234+
-h, --help output usage information
235+
236+
```
237+
238+
By default, mastarm will run Jest with most of the defaults in place. The defaults that mastarm adds include:
239+
240+
- some transforms needed to read certain .js files and also YAML files.
241+
- ignoring the test path directory `__tests__/test-utils`
242+
- setting the [testURL](https://jestjs.io/docs/en/configuration#testurl-string) to `http://localhost:9966`
243+
- turning on [notifications](https://jestjs.io/docs/en/configuration#notify-boolean) of test completion
244+
245+
If the `coverage` flag is set to true, mastarm will automatically generate coverage reports of all .js files in the `lib` folder and will save the reports to the `coverage` folder.
223246
247+
The `patterns` argument will make Jest run only tests whose filename match the provided pattern.
248+
249+
There are a number of ways to set the [Jest config](https://jestjs.io/docs/en/configuration). The first is by adding a `jest` object to the package.json of the project. A number of other mastarm options will override the config. And finally, it is possible to use a custom config file (either .json or .js) via the `--custom-config-file` option. The config values are set and potentially overridden in the following order:
250+
251+
1. mastarm defaults.
252+
2. Options in the `jest` object of the project's package.json file.
253+
3. The values specified in the mastarm arguments `--coverage-paths`, `--setup-files`, `--test-environment` and `--test-path-ignore-patterns`
254+
4. Options set in a custom config file specified in the mastarm argument `--custom-config-file`.
255+
256+
Here is an example of how to set the config using a custom file:
257+
258+
```shell
259+
mastarm test --custom-config-file __tests__/test-utils/mocks/mock-jest-config.json
260+
```
261+
262+
It is also possible to override any [Jest CLI Options](https://jestjs.io/docs/en/cli) by setting the `--jest-cli-args` flag. Ex:
263+
264+
```shell
265+
mastarm test --jest-cli-args "--json --outputFile e2e-test-results/results.json"
224266
```
225267
226268
### `lint-messages`

__tests__/lib/__snapshots__/jest.js.snap

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Array [
77
"--runInBand",
88
"--config",
99
Object {
10+
"collectCoverage": true,
1011
"collectCoverageFrom": Array [
1112
"lib/**/*.js",
1213
"bin",

__tests__/lib/jest.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('jest.js', () => {
99
const cfg = jestUtils.generateTestConfig(['these', 'files', 'only'], {
1010
cache: false,
1111
coveragePaths: 'bin src another-folder',
12+
customConfigFile: '__tests__/test-utils/mocks/mock-jest-config.json',
1213
runInBand: true,
1314
setupFiles: 'beforeTestsSetup.js',
1415
testEnvironment: 'node',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "collectCoverage": true }

bin/mastarm-deploy

+160-65
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
const path = require('path')
44

55
const commander = require('commander')
6+
const execa = require('execa')
7+
const gitRepoIsUpToDate = require('git-repo-is-up-to-date')
68
const commit = require('this-commit')()
79
const username = require('username')
810

911
const build = require('../lib/build')
10-
const {readFile} = require('../lib/fs-promise')
12+
const {readFile, writeFile} = require('../lib/fs-promise')
1113
const loadConfig = require('../lib/load-config')
1214
const logger = require('../lib/logger')
1315
const pkg = require('../lib/pkg')
@@ -28,74 +30,167 @@ commander
2830
.option('--s3bucket', 'S3 Bucket to push to.')
2931
.parse(process.argv)
3032

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

36-
if (config.env.SLACK_WEBHOOK && config.env.SLACK_WEBHOOK.length > 0) {
37-
logger.logToSlack({
38-
channel: config.env.SLACK_CHANNEL || '#devops',
39-
webhook: config.env.SLACK_WEBHOOK
40-
})
41-
}
37+
async function deploy () {
38+
// get information about the directory that the config is in
39+
const configRepoStatus = await gitRepoIsUpToDate(commander.config)
40+
const { remoteUrl: configRemoteUrl, repoInfo } = configRepoStatus
41+
let configCommit, configDir
42+
if (repoInfo) {
43+
configCommit = repoInfo.localCommit
44+
configDir = repoInfo.root
45+
}
4246

43-
const files = util.parseEntries([...commander.args, ...(get('entries') || [])])
44-
util.assertEntriesExist(files)
45-
const sourceFiles = files.map(f => f[0])
46-
const outfiles = [...files.map(f => f[1]), ...files.map(f => `${f[1]}.map`)]
47-
48-
const env = get('env') || 'development'
49-
const minify = get('minify')
50-
const buildOpts = {
51-
config,
52-
env,
53-
files,
54-
minify
55-
}
56-
const cloudfront = get('cloudfront')
57-
const s3bucket = get('s3bucket')
47+
// do some extra analysis if it looks like a configurations repo is being used
48+
if (configRemoteUrl && configRemoteUrl.endsWith('/configurations.git')) {
49+
if (!configRepoStatus.isUpToDate) {
50+
console.error('Configurations folder is not up-to-date! Errors:')
51+
configRepoStatus.errors.forEach(err => console.error(err))
52+
process.exit(1)
53+
}
54+
55+
// decrypt env file using sops to make sure old file is overwritten with
56+
// data from encoded sops file
57+
const configPath = path.resolve(commander.config)
58+
console.log('decrypting env file with sops')
59+
const {stdout} = await execa(
60+
'sops',
61+
[
62+
'-d',
63+
path.join(configPath, 'env.enc.yml')
64+
]
65+
)
66+
await writeFile(path.join(configPath, 'env.yml'), stdout)
67+
// at this point, we can be certain that the local configurations repo
68+
// directory matches what has been committed and pushed to the remote repo
69+
}
70+
71+
url = pkg.repository.url.replace('.git', '')
72+
tag = `<${url}/commit/${commit}|${pkg.name}@${commit.slice(0, 6)}>`
73+
config = loadConfig(process.cwd(), commander.config, commander.env)
74+
const get = util.makeGetFn([commander, config.settings])
75+
76+
if (config.env.SLACK_WEBHOOK && config.env.SLACK_WEBHOOK.length > 0) {
77+
logger.logToSlack({
78+
channel: config.env.SLACK_CHANNEL || '#devops',
79+
webhook: config.env.SLACK_WEBHOOK
80+
})
81+
}
5882

59-
const pushToS3 = createPushToS3({
60-
cloudfront,
61-
s3bucket
62-
})
83+
const files = util.parseEntries([...commander.args, ...(get('entries') || [])])
84+
util.assertEntriesExist(files)
85+
const sourceFiles = files.map(f => f[0])
86+
const outfiles = [...files.map(f => f[1]), ...files.map(f => `${f[1]}.map`)]
6387

64-
logger
65-
.log(
88+
env = get('env') || 'development'
89+
minify = get('minify')
90+
const buildOpts = {
91+
config,
92+
env,
93+
files,
94+
minify
95+
}
96+
cloudfront = get('cloudfront')
97+
s3bucket = get('s3bucket')
98+
99+
const pushToS3 = createPushToS3({
100+
cloudfront,
101+
s3bucket
102+
})
103+
104+
await logger.log(
66105
`:construction: *deploying: ${tag} by <@${username.sync()}>*
67-
:vertical_traffic_light: *mastarm:* v${mastarmVersion}
68-
:cloud: *cloudfront:* ${cloudfront}
69-
:hash: *commit:* ${commit}
70-
:seedling: *env:* ${env}
71-
:compression: *minify:* ${minify}
72-
:package: *s3bucket:* ${s3bucket}
73-
:hammer_and_wrench: *building:* ${sourceFiles.join(', ')}`
106+
:vertical_traffic_light: *mastarm:* v${mastarmVersion}
107+
:cloud: *cloudfront:* ${cloudfront}
108+
:hash: *commit:* ${commit}
109+
:seedling: *env:* ${env}
110+
:compression: *minify:* ${minify}
111+
:package: *s3bucket:* ${s3bucket}
112+
:hammer_and_wrench: *building:* ${sourceFiles.join(', ')}`
74113
)
75-
.then(() =>
76-
build(buildOpts)
77-
.then(() =>
78-
logger.log(`:rocket: *uploading:* ${sourceFiles.length * 2} file(s)`)
79-
)
80-
.then(() =>
81-
Promise.all(
82-
outfiles.map(outfile =>
83-
readFile(outfile).then(body => pushToS3({body, outfile}))
84-
)
85-
)
86-
)
87-
.then(() =>
88-
logger
89-
.log(
90-
`:tada: :confetti_ball: :tada: *deploy ${tag} complete* :tada: :confetti_ball: :tada:`
91-
)
92-
.then(() => process.exit(0))
93-
)
94-
.catch(err =>
95-
logger
96-
.log(
97-
`:rotating_light: *${tag} error deploying ${tag} ${err.message || err}*`
98-
)
99-
.then(() => process.exit(1))
114+
115+
try {
116+
await build(buildOpts)
117+
await logger.log(`:rocket: *uploading:* ${sourceFiles.length * 2} file(s)`)
118+
await Promise.all(
119+
outfiles.map(outfile =>
120+
readFile(outfile).then(body => pushToS3({body, outfile}))
100121
)
101-
)
122+
)
123+
await logger.log(
124+
`:tada: :confetti_ball: :tada: *deploy ${tag} complete* :tada: :confetti_ball: :tada:`
125+
)
126+
await logToMsTeams({ configCommit, configDir, configRemoteUrl })
127+
process.exit(0)
128+
} catch (error) {
129+
await logger.log(
130+
`:rotating_light: *${tag} error deploying ${tag} ${error.message || error}*`
131+
)
132+
await logToMsTeams({ configCommit, configDir, configRemoteUrl, error })
133+
process.exit(1)
134+
}
135+
}
136+
137+
deploy()
138+
139+
/**
140+
* Sends a card to MS Teams with information about the deployment
141+
* @param {[string]} configCommit hash of the commit in the configurations
142+
* repo (if it exists)
143+
* @param {[string]} configDir partial path to specific config directory used
144+
* to deploy
145+
* @param {[string]} configRemoteUrl base url for the configurations repo
146+
* (if it exists)
147+
* @param {[Error]} error the error, if one occurred. A falsy value indicates
148+
* success
149+
*/
150+
function logToMsTeams ({ configCommit, configDir, configRemoteUrl, error }) {
151+
if (!config.env.MS_TEAMS_WEBHOOK) return Promise.resolve()
152+
153+
const potentialAction = [{
154+
'@type': 'OpenUri',
155+
name: `View Commit on Github`,
156+
targets: [
157+
{
158+
os: 'default',
159+
uri: `${url}/commit/${commit}`
160+
}
161+
]
162+
}]
163+
if (configCommit && configRemoteUrl) {
164+
potentialAction.push({
165+
'@type': 'OpenUri',
166+
name: `View Config Commit on Github`,
167+
targets: [
168+
{
169+
os: 'default',
170+
uri: `${configRemoteUrl}/tree/${configCommit}/${configDir}`
171+
}
172+
]
173+
})
174+
}
175+
const text = `📄 **commit:** ${pkg.name}@${commit.slice(0, 6)}\n
176+
👤 **deployed by:** ${username.sync()}\n
177+
${configCommit
178+
? `🎛️ **config:** configurations@${configCommit.slice(0, 6)}\n
179+
📂 **config folder:** ${configDir}\n` // improper indenting here needed to properly format on MS Teams
180+
: '🎛️ **config:** unknown configuration data!\n'}
181+
🚦 **mastarm:** v${mastarmVersion}\n
182+
☁️ **cloudfront:** ${cloudfront}\n
183+
🌱 **env:** ${env}\n
184+
🗜️ **minify:** ${minify}\n
185+
📦 **s3bucket:** ${s3bucket}\n
186+
${error
187+
? `🚨 🚨 **error deploying ${error.message || error}**`
188+
: `🎉 🎊 🎉 **deploy successful!** 🎉 🎊 🎉`}`
189+
190+
return logger.notifyMsTeams({
191+
potentialAction,
192+
text,
193+
title: `${error ? 'Failed to deploy' : 'Successfully deployed'} ${pkg.name}`,
194+
webhook: config.env.MS_TEAMS_WEBHOOK
195+
})
196+
}

bin/mastarm-test

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ commander
2525
'--coverage-paths <paths>',
2626
'Extra paths to collect code coverage from'
2727
)
28+
.option(
29+
'--custom-config-file <path>',
30+
'Override the Jest config with the values found in a file path relative to the current working directory'
31+
)
2832
.option('--force-exit', 'Force Jest to exit after all tests have completed running.')
2933
.option(
3034
'--jest-cli-args <args>',

lib/eslintrc.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
"standard",
99
"standard-jsx",
1010
"plugin:flowtype/recommended",
11-
"plugin:jest/recommended"
11+
"plugin:jest/recommended",
12+
"plugin:jsx-a11y/strict"
1213
],
1314
"parser": "babel-eslint",
1415
"plugins": [
1516
"flowtype",
1617
"import",
17-
"jest"
18+
"jest",
19+
"jsx-a11y"
1820
],
1921
"rules": {
2022
"complexity": ["warn", 12],

0 commit comments

Comments
 (0)