├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .template-lintrc.js ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── addon └── .gitkeep ├── app └── .gitkeep ├── config ├── ember-try.js └── environment.js ├── ember-cli-build.js ├── index.js ├── lib ├── notify.js └── service.js ├── package.json ├── testem.js ├── tests ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ └── application.hbs │ ├── config │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ └── robots.txt ├── helpers │ └── .gitkeep ├── index.html ├── runner.js ├── test-helper.js └── unit │ ├── .gitkeep │ ├── index-nodetest.js │ └── lib │ ├── notify-nodetest.js │ └── service-nodetest.js ├── vendor └── .gitkeep └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /bower.json.ember-try 20 | /package.json.ember-try 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true 11 | } 12 | }, 13 | env: { 14 | browser: true 15 | }, 16 | overrides: [ 17 | // node files 18 | { 19 | files: [ 20 | '.eslintrc.js', 21 | '.template-lintrc.js', 22 | 'ember-cli-build.js', 23 | 'index.js', 24 | 'testem.js', 25 | 'blueprints/*/index.js', 26 | 'config/**/*.js', 27 | 'tests/dummy/config/**/*.js' 28 | ], 29 | excludedFiles: [ 30 | 'addon/**', 31 | 'addon-test-support/**', 32 | 'app/**', 33 | 'tests/dummy/app/**' 34 | ], 35 | parserOptions: { 36 | sourceType: 'script' 37 | }, 38 | env: { 39 | browser: false, 40 | node: true 41 | }, 42 | plugins: ['node'], 43 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 44 | 'node/no-extraneous-require': 'off', 45 | 'node/no-unpublished-require': 'off' 46 | // add your custom rules and overrides for node files here 47 | }) 48 | }, { 49 | files: [ 50 | 'lib/**/*.js', 51 | 'tests/**/*-nodetest.js', 52 | ], 53 | env: { 54 | node: true, 55 | mocha: true 56 | } 57 | } 58 | ] 59 | }; 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /connect.lock 16 | /coverage/ 17 | /libpeerconnection.log 18 | /npm-debug.log* 19 | /testem.log 20 | /yarn-error.log 21 | 22 | # ember-try 23 | /.node_modules.ember-try/ 24 | /bower.json.ember-try 25 | /package.json.ember-try 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintignore 14 | /.eslintrc.js 15 | /.git/ 16 | /.gitignore 17 | /.template-lintrc.js 18 | /.travis.yml 19 | /.watchmanconfig 20 | /bower.json 21 | /config/ember-try.js 22 | /CONTRIBUTING.md 23 | /ember-cli-build.js 24 | /testem.js 25 | /tests/ 26 | /yarn.lock 27 | .gitkeep 28 | 29 | # ember-try 30 | /.node_modules.ember-try/ 31 | /bower.json.ember-try 32 | /package.json.ember-try 33 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane' 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "stable" 5 | - "10" 6 | - "12" 7 | 8 | dist: trusty 9 | 10 | addons: 11 | chrome: stable 12 | 13 | cache: 14 | yarn: true 15 | 16 | branches: 17 | only: 18 | - master 19 | # npm version tags 20 | - /^v\d+\.\d+\.\d+/ 21 | 22 | before_install: 23 | - curl -o- -L https://yarnpkg.com/install.sh | bash 24 | - export PATH=$HOME/.yarn/bin:$PATH 25 | 26 | install: 27 | - yarn install --non-interactive 28 | 29 | script: 30 | - yarn test 31 | 32 | notifications: 33 | email: 34 | on_success: never 35 | slack: 36 | rooms: 37 | secure: OOKD4ZksqzEBW/A3WRuOToODIxnDITqx+Esu7tdmmYPuQlMYgx4SUHv8j9OM9/ScFJiseeVGSkl45vJrHLLIITX9XSjO1RgiGZgw2heVujmGpF6CZNqvT6GsQuKIvMzmwF7IxuHdfV45Csr9Ou/Fg74TszR/4S2h4SOI4zhLg7A= 38 | on_success: never 39 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.4.2 2 | 3 | * ember-cli-deply-webhooks now uses rsvp for promises instead of the Ember CLI 4 | Promise implementation, see #23. 5 | 6 | # 0.4.1 7 | 8 | * Fixed a bug that caused the deployment to not fail although a webhook marked 9 | as critical did not actually succeed, see #20. 10 | 11 | # 0.4.0 12 | 13 | * Webhooks can now be defined as **critical** so that when they fail the 14 | complete deployment will fail, see #19. 15 | 16 | # 0.3.0 17 | 18 | * The Addon was renamed to ember-cli-deploy-webhooks, see #14. 19 | * Dependencies only used for testing have been moved 20 | * The Addon does now support authorization data for webhooks, see 10. 21 | 22 | # 0.2.0 23 | 24 | * The Addon can now send notifications on all deployment hooks, see #8. 25 | 26 | # 0.0.1 27 | 28 | initial release 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2020 simplabs GmbH and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-cli-deploy-webhooks [![Build Status](https://travis-ci.org/simplabs/ember-cli-deploy-webhooks.svg)](https://travis-ci.org/simplabs/ember-cli-deploy-webhooks) 2 | 3 | > An ember-cli-deploy plugin to notify external services (e.g. an error 4 | > tracking service) of successful hook executions in your deploy pipeline. 5 | 6 | [![](https://ember-cli-deploy.github.io/ember-cli-deploy-version-badges/plugins/ember-cli-deploy-webhooks.svg)](http://ember-cli-deploy.github.io/ember-cli-deploy-version-badges/) 7 | 8 | ## What is an ember-cli-deploy plugin? 9 | 10 | A plugin is an addon that can be executed as a part of the ember-cli-deploy pipeline. A plugin will implement one or more of the ember-cli-deploy's pipeline hooks. 11 | 12 | For more information on what plugins are and how they work, please refer to the [Plugin Documentation][1]. 13 | 14 | ## Quick Start 15 | 16 | To get up and running quickly, do the following: 17 | 18 | - Install this plugin 19 | 20 | ```bash 21 | $ ember install ember-cli-deploy-webhooks 22 | ``` 23 | - Place the following configuration into `config/deploy.js` 24 | 25 | 26 | ```javascript 27 | ENV.webhooks = { 28 | services: { 29 | "": { 30 | url: , 31 | headers: { 32 | // custom headers go here 33 | }, 34 | method: '', // defaults to 'POST' 35 | body: function(/*context*/) { 36 | // return any object that should be passed as request body here 37 | return { 38 | apiKey: 39 | }; 40 | }, 41 | didActivate: true 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | - Run the pipeline 48 | 49 | ```bash 50 | $ ember deploy 51 | ``` 52 | 53 | ## ember-cli-deploy Hooks Implemented 54 | 55 | For detailed information on what plugin hooks are and how they work, please refer to the [Plugin Documentation][1]. 56 | 57 | - `configure` 58 | - `setup` 59 | 60 | _Hooks that can be used for webhooks:_ 61 | 62 | - `willDeploy` 63 | - `willBuild` 64 | - `build` 65 | - `didBuild` 66 | - `willPrepare` 67 | - `prepare` 68 | - `didPrepare` 69 | - `willUpload` 70 | - `upload` 71 | - `didUpload` 72 | - `willActivate` 73 | - `activate` 74 | - `didActivate` 75 | - `teardown` 76 | - `fetchRevisions` 77 | - `displayRevisions` 78 | - `didFail` 79 | 80 | ## Configuration Options 81 | 82 | For detailed information on how configuration of plugins works, please refer to the [Plugin Documentation][1]. 83 | 84 | ### services 85 | 86 | An object that identifies all webhooks you want to notify. You will put a key for every service you want to call on deploy here. 87 | 88 | A `service` configuration needs to provide four properties as configuration for 89 | `ember-cli-deploy-webhooks` to know how to notify the service correctly: 90 | 91 | - `url` The url to call 92 | - `method` The HTTP-method to use for the call (defaults to `'POST'`) 93 | - `headers` A property to specify custom HTTP-headers (defaults to `{}`) 94 | - `body` The body of the request 95 | - `auth` used for http-authentication 96 | - `critical` if true, webhook failures will abort deploy 97 | 98 | `auth` should be a hash containing values: 99 | 100 | * `user` || `username` 101 | * `pass` || `password` 102 | 103 | Bearer authentication is also supported. Please refer to 104 | [request](https://github.com/request/request#http-authentication)'s docs for 105 | more details as `ember-cli-deploy-webhooks` uses `request` internally. 106 | 107 |
108 | **Whenever one of these properties (except `auth`) returns a _falsy_ value, the service will _not_ be 109 | called.** 110 |
111 | 112 | All these properties can return a value directly or can be implemented as 113 | a function which returns the value for this property and gets called with the 114 | deployment context. The `this` scope will be set to the service config object 115 | itself. 116 | 117 | *Example:* 118 | 119 | ```javascript 120 | ENV.webhooks = { 121 | services: { 122 | slack: { 123 | webhookURL: '', 124 | url: function() { 125 | return this.webhookURL; 126 | }, 127 | method: 'POST', 128 | headers: {}, 129 | body: function(context) { 130 | var deployer = context.deployer; 131 | 132 | return { 133 | text: deployer + ' deployed a new revision' 134 | } 135 | } 136 | } 137 | } 138 | }; 139 | ``` 140 | 141 | Additionally you have to specify on which hook to notify the service in the 142 | deploy pipeline. To do this you can simply pass a truthy value as a property 143 | named the same as the hook at which you want to notify the service. This can 144 | als be used to override the defaults that you specify on a service. 145 | 146 | *Example:* 147 | 148 | ```javascript 149 | ENV.webhooks = { 150 | services: { 151 | slack: { 152 | url: 'your-webhook-url', 153 | method: 'POST', 154 | headers: {}, 155 | body: { 156 | text: 'A new revision was activated!' 157 | }, 158 | didActivate: true 159 | didDeploy: { 160 | body: { 161 | text: 'Deployment successful!' 162 | } 163 | }, 164 | didFail: { 165 | body: { 166 | text: 'Deployment failed!' 167 | } 168 | } 169 | } 170 | } 171 | }; 172 | ``` 173 | 174 | There are two types of services you can specify in the `services` property: 175 | 176 | a) __preconfigured services__ 177 | 178 | Preconfigured services only need to be passed service specific configuration 179 | options. This depends on the service (see below) but you can also provide all 180 | other service configuration properties that were explained before to override 181 | the defaults. 182 | 183 | *Example:* 184 | 185 | ```javascript 186 | ENV.webhooks = { 187 | services: { 188 | bugsnag: { 189 | url: 'https://bugsnag.simplabs.com/deploy', 190 | apiKey: '1234', 191 | didActivate: true 192 | } 193 | } 194 | }; 195 | ``` 196 | 197 | Preconfigured services aren't very special but maintainers and contributors 198 | have already provided a base configuration that can be overridden by the 199 | plugin users. This for example is basically the default implementation that is 200 | already configured for the slack service: 201 | 202 | ```javascript 203 | ENV.webhooks.services = { 204 | // ... 205 | slack: { 206 | url: function() { 207 | return this.webhookURL; 208 | }, 209 | method: 'POST', 210 | headers: {} 211 | } 212 | }; 213 | ``` 214 | 215 | Users then only have to provide `webhookURL` and a `body`-property for the 216 | hooks that should send a message to slack. 217 | 218 | *Example:* 219 | 220 | ```javascript 221 | ENV.webhooks.services = { 222 | slack: { 223 | webhookURL: '', 224 | didActivate: { 225 | body: { 226 | text: 'A new revision was activated!' 227 | } 228 | } 229 | } 230 | }; 231 | ``` 232 | 233 | Currently available preconfigured services are: 234 | 235 | - `bugsnag` [An error-tracking service](https://bugsnag.com) 236 | - `slack` [The popular messaging app](https://slack.com/) 237 | 238 | #### bugsnag 239 | 240 | To configure bugsnag you need to at least provide an `apiKey` and specify 241 | a hook on which bugsnag should be notified of a deployment. You'll most likely 242 | want to notify bugsnag of a deployment in the `didActivate`-hook as this is the 243 | hook that actually makes a new version of your app available to your users. 244 | 245 | *Example:* 246 | 247 | ```javascript 248 | ENV.webhooks.services = { 249 | bugsnag: { 250 | apiKey: '', 251 | didActivate: true 252 | } 253 | }; 254 | ``` 255 | 256 | __Required configuration__ 257 | 258 | - `apiKey` The api-key to send as part of the request payload (identifies the 259 | application) 260 | 261 | __Default configuration__ 262 | 263 | ``` 264 | ENV.webhooks.services = { 265 | bugsnag: { 266 | url: 'http://notify.bugsnag.com/deploy', 267 | method: 'POST', 268 | headers: {}, 269 | body: function() { 270 | var apiKey = this.apiKey; 271 | 272 | if (!apiKey) { return; } 273 | 274 | return { 275 | apiKey: this.apiKey, 276 | releaseStage: process.env.DEPLOY_TARGET 277 | } 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | #### slack 284 | 285 | *Example:* 286 | 287 | ```javascript 288 | ENV.webhooks.services = { 289 | slack: { 290 | webhookURL: '', 291 | didActivate: { 292 | body: { 293 | text: 'A new revision was activated!' 294 | } 295 | } 296 | } 297 | }; 298 | ``` 299 | 300 | __Required configuration__ 301 | 302 | - `webhookURL` The [incoming webhook's](https://api.slack.com/incoming-webhooks)-url that should be called. 303 | 304 | - `body` You need to provide a payload that gets send to slack. Please refer to 305 | the [documentation](https://api.slack.com/incoming-webhooks) on how message 306 | payloads can be used to customize the appearance of a message in slack. At 307 | least you have to provide a `text` property in the payload. 308 | 309 | __Default configuration__ 310 | 311 | ```javascript 312 | ENV.webhooks.services = { 313 | // ... 314 | slack: { 315 | url: function() { 316 | return this.webhookURL; 317 | }, 318 | method: 'POST', 319 | headers: {} 320 | } 321 | }; 322 | ``` 323 | 324 | b) __custom services__ 325 | 326 | Custom services need to be configured with a `url` and `body` property. 327 | `headers` will default to `{}` and `method` will default to `'POST'`. All these 328 | options can be overridden as described before of course. 329 | 330 | *Example:* 331 | 332 | ```javascript 333 | ENV.webhooks = { 334 | services: { 335 | simplabs: { 336 | url: 'https://notify.simplabs.com/deploy', 337 | body: function(context) { 338 | var deployer = context.deployer; 339 | 340 | return { 341 | secret: 'supersecret', 342 | deployer: deployer 343 | } 344 | }, 345 | didActivate: true 346 | }, 347 | newRelic: { 348 | url: 'https://api.newrelic.com/deployments.json', 349 | headers: { 350 | "api-key": "" 351 | }, 352 | method: 'POST', 353 | body: { 354 | deployment: { 355 | // ... 356 | } 357 | }, 358 | didDeploy: true 359 | } 360 | } 361 | }; 362 | ``` 363 | 364 | ### httpClient 365 | 366 | The underlying http-library used to send requests to the specified services. This allows users to customize the library that's used for http requests which is useful in tests but might be useful to some users as well. By default the plugin uses [request](https://github.com/request/request). 367 | 368 | ## Running Tests 369 | 370 | - `yarn test` 371 | 372 | [1]: http://ember-cli-deploy.com/docs/v0.6.x/plugins-overview/ "Plugin Documentation" 373 | 374 | ## License 375 | 376 | ember-cli-deploy-webhooks is developed by and © 377 | [simplabs GmbH](http://simplabs.com) and contributors. It is released under the 378 | [MIT License](https://github.com/simplabs/ember-cli-deploy-webhooks/blob/master/LICENSE). 379 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/addon/.gitkeep -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/app/.gitkeep -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function() { 6 | return { 7 | scenarios: [ 8 | { 9 | name: 'ember-lts-3.12', 10 | npm: { 11 | devDependencies: { 12 | 'ember-source': '~3.12.0' 13 | } 14 | } 15 | }, 16 | { 17 | name: 'ember-lts-3.16', 18 | npm: { 19 | devDependencies: { 20 | 'ember-source': '~3.16.0' 21 | } 22 | } 23 | }, 24 | { 25 | name: 'ember-release', 26 | npm: { 27 | devDependencies: { 28 | 'ember-source': await getChannelURL('release') 29 | } 30 | } 31 | }, 32 | { 33 | name: 'ember-beta', 34 | npm: { 35 | devDependencies: { 36 | 'ember-source': await getChannelURL('beta') 37 | } 38 | } 39 | }, 40 | { 41 | name: 'ember-canary', 42 | npm: { 43 | devDependencies: { 44 | 'ember-source': await getChannelURL('canary') 45 | } 46 | } 47 | }, 48 | // The default `.travis.yml` runs this scenario via `npm test`, 49 | // not via `ember try`. It's still included here so that running 50 | // `ember try:each` manually or from a customized CI config will run it 51 | // along with all the other scenarios. 52 | { 53 | name: 'ember-default', 54 | npm: { 55 | devDependencies: {} 56 | } 57 | }, 58 | { 59 | name: 'ember-default-with-jquery', 60 | env: { 61 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 62 | 'jquery-integration': true 63 | }) 64 | }, 65 | npm: { 66 | devDependencies: { 67 | '@ember/jquery': '^0.5.1' 68 | } 69 | } 70 | }, 71 | { 72 | name: 'ember-classic', 73 | env: { 74 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 75 | 'application-template-wrapper': true, 76 | 'default-async-observers': false, 77 | 'template-only-glimmer-components': false 78 | }) 79 | }, 80 | npm: { 81 | ember: { 82 | edition: 'classic' 83 | } 84 | } 85 | } 86 | ] 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | var RSVP = require('rsvp'); 5 | var DeployPluginBase = require('ember-cli-deploy-plugin'); 6 | var Notify = require('./lib/notify'); 7 | var Service = require('./lib/service'); 8 | var _ = require('lodash'); 9 | var merge = _.merge; 10 | var pick = _.pick; 11 | var intersection = _.intersection; 12 | var forIn = _.forIn; 13 | 14 | function notificationHook(hookName) { 15 | return function(context) { 16 | var preConfig = this.readConfig('configuredServices'); 17 | var userConfig = this.readConfig('services'); 18 | 19 | var promises = []; 20 | 21 | for (var key in userConfig) { 22 | var defaults = preConfig[key] || {}; 23 | var user = userConfig[key] || {}; 24 | var hook = userConfig[key][hookName] || {}; 25 | 26 | var service = new Service({ 27 | defaults: defaults, 28 | user: user, 29 | hook: hook 30 | }); 31 | 32 | if (service.serviceOptions[hookName]) { 33 | var notify = new Notify({ 34 | plugin: this 35 | }); 36 | 37 | var opts = service.buildServiceCall(context); 38 | 39 | promises.push(notify.send(key, opts)); 40 | } 41 | } 42 | 43 | return RSVP.all(promises); 44 | } 45 | } 46 | 47 | module.exports = { 48 | name: require('./package').name, 49 | 50 | createDeployPlugin: function(options) { 51 | var DeployPlugin = DeployPluginBase.extend({ 52 | name: options.name, 53 | 54 | defaultConfig: { 55 | configuredServices: function(context) { 56 | return { 57 | bugsnag: { 58 | url: 'http://notify.bugsnag.com/deploy', 59 | method: 'POST', 60 | headers: {}, 61 | body: function() { 62 | var apiKey = this.apiKey; 63 | 64 | if (!apiKey) { return; } 65 | 66 | return { 67 | apiKey: this.apiKey, 68 | releaseStage: process.env.DEPLOY_TARGET 69 | } 70 | } 71 | }, 72 | 73 | slack: { 74 | url: function() { 75 | return this.webhookURL; 76 | }, 77 | method: 'POST', 78 | headers: {} 79 | } 80 | } 81 | }, 82 | 83 | httpClient: function(context) { 84 | return context.notifyHTTPClient; 85 | } 86 | }, 87 | 88 | setup: function(context) { 89 | var services = this.readConfig('services'); 90 | var hooks = [ 91 | 'willDeploy', 'willBuild', 'build', 'didBuild', 'willPrepare', 'prepare', 92 | 'didPrepare', 'willUpload', 'upload', 'didUpload', 'willActivate', 93 | 'activate', 'didActivate', 'didDeploy', 'teardown', 'fetchRevisions', 94 | 'displayRevisions', 'didFail' 95 | ]; 96 | 97 | var servicesWithNoHooksConfigured = pick(services, function(service) { 98 | return _.intersection(Object.keys(service), hooks).length === 0; 99 | }); 100 | 101 | _.forIn(servicesWithNoHooksConfigured, function(value, key) { 102 | this.log('Warning! '+key+' - Service configuration found but no hook specified in deploy configuration. Service will not be notified.', { color: 'yellow' }); 103 | }, this); 104 | }, 105 | 106 | willDeploy: notificationHook('willDeploy'), 107 | 108 | willBuild: notificationHook('willBuild'), 109 | build: notificationHook('build'), 110 | didBuild: notificationHook('didBuild'), 111 | 112 | willPrepare: notificationHook('willPrepare'), 113 | prepare: notificationHook('prepare'), 114 | didPrepare: notificationHook('didPrepare'), 115 | 116 | willUpload: notificationHook('willUpload'), 117 | upload: notificationHook('upload'), 118 | didUpload: notificationHook('didUpload'), 119 | 120 | willActivate: notificationHook('willActivate'), 121 | activate: notificationHook('activate'), 122 | didActivate: notificationHook('didActivate'), 123 | 124 | didDeploy: notificationHook('didDeploy'), 125 | 126 | teardown: notificationHook('teardown'), 127 | 128 | fetchRevisions: notificationHook('fetchRevisions'), 129 | displayRevisions: notificationHook('displayRevisions'), 130 | 131 | didFail: notificationHook('didFail') 132 | }); 133 | 134 | return new DeployPlugin(); 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /lib/notify.js: -------------------------------------------------------------------------------- 1 | const RSVP = require('rsvp'); 2 | const CoreObject = require('core-object'); 3 | const request = require('request'); 4 | const merge = require('lodash/object/merge'); 5 | 6 | function optsValid(opts) { 7 | return opts.url && opts.headers && opts.method && opts.body; 8 | } 9 | 10 | module.exports = CoreObject.extend({ 11 | init(options) { 12 | this._plugin = options.plugin; 13 | 14 | this._client = this._plugin.readConfig('httpClient') || request; 15 | }, 16 | 17 | _defaults() { 18 | return { 19 | method: 'POST', 20 | headers: {}, 21 | json: true 22 | }; 23 | }, 24 | 25 | send(serviceKey, opts = {}) { 26 | let plugin = this._plugin; 27 | let makeRequest = RSVP.denodeify(this._client); 28 | let critical = (('critical' in opts) ? delete opts.critical : false); 29 | 30 | let requestOpts = merge(this._defaults(), opts); 31 | 32 | if (optsValid(requestOpts)) { 33 | return makeRequest(requestOpts) 34 | .then(function(response) { 35 | let body = ''; 36 | 37 | if (response && response.body) { 38 | body = response.body; 39 | } 40 | 41 | if (typeof body !== 'string') { 42 | body = JSON.stringify(body); 43 | } 44 | 45 | if (critical && !(response.statusCode < 300 && response.statusCode >= 200)) { 46 | return RSVP.reject(response.statusCode); 47 | } 48 | 49 | plugin.log(`${serviceKey} => ${body}`); 50 | }) 51 | .catch(function(error) { 52 | let errorMessage = `${serviceKey} => ${error}`; 53 | 54 | if (critical) { 55 | return RSVP.reject(error); 56 | } 57 | plugin.log(errorMessage, { color: 'red' }); 58 | }); 59 | } else { 60 | let warningMessage = 'No request issued! Request options invalid! You have to specify `url`, `headers`, `method` and `body`.'; 61 | 62 | if (critical) { 63 | return RSVP.reject(warningMessage); 64 | } 65 | plugin.log(`${serviceKey} => ${warningMessage}`, { color: 'yellow', verbose: true }); 66 | return RSVP.resolve(); 67 | } 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | const CoreObject = require('core-object'); 2 | const merge = require('lodash/object/merge'); 3 | const mapValues = require('lodash/object/mapValues'); 4 | const pick = require('lodash/object/pick'); 5 | 6 | module.exports = CoreObject.extend({ 7 | init(options) { 8 | this.serviceOptions = merge(options.defaults, options.user, options.hook || {}); 9 | }, 10 | 11 | buildServiceCall(context) { 12 | let opts = mapValues(this.serviceOptions, function(value) { 13 | return typeof value === 'function' ? value.bind(this.serviceOptions)(context) : value; 14 | }.bind(this)); 15 | 16 | return pick(opts, ['url', 'method', 'headers', 'body', 'auth', 'critical']); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-cli-deploy-webhooks", 3 | "version": "1.0.0-beta.1", 4 | "description": "Ember CLI Deploy plugin for calling webhooks during deployments", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-cli-deploy-plugin" 8 | ], 9 | "repository": "https://github.com/simplabs/ember-cli-deploy-webhooks", 10 | "license": "MIT", 11 | "author": "simplabs GmbH", 12 | "directories": { 13 | "doc": "doc", 14 | "test": "tests" 15 | }, 16 | "scripts": { 17 | "build": "ember build --environment=production", 18 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*", 19 | "lint:hbs": "ember-template-lint .", 20 | "lint:js": "eslint .", 21 | "start": "ember serve", 22 | "test": "npm-run-all lint:* test:*", 23 | "test:ember": "ember test", 24 | "test:node": "node tests/runner.js" 25 | }, 26 | "dependencies": { 27 | "core-object": "^3.1.5", 28 | "ember-cli-babel": "^7.18.0", 29 | "ember-cli-deploy-plugin": "0.2.9", 30 | "ember-cli-htmlbars": "^4.2.3", 31 | "lodash": "^3.10.1", 32 | "request": "^2.88.2", 33 | "rsvp": "^4.8.4" 34 | }, 35 | "devDependencies": { 36 | "@ember/optional-features": "^1.3.0", 37 | "@glimmer/component": "^1.0.0", 38 | "@glimmer/tracking": "^1.0.0", 39 | "babel-eslint": "^10.1.0", 40 | "broccoli-asset-rev": "^3.0.0", 41 | "chai": "^3.5.0", 42 | "chai-as-promised": "^5.1.0", 43 | "ember-auto-import": "^1.5.3", 44 | "ember-cli": "~3.17.0", 45 | "ember-cli-dependency-checker": "^3.2.0", 46 | "ember-cli-inject-live-reload": "^2.0.2", 47 | "ember-cli-sri": "^2.1.1", 48 | "ember-cli-uglify": "^3.0.0", 49 | "ember-disable-prototype-extensions": "^1.1.3", 50 | "ember-export-application-global": "^2.0.1", 51 | "ember-load-initializers": "^2.1.1", 52 | "ember-maybe-import-regenerator": "^0.1.6", 53 | "ember-qunit": "^4.6.0", 54 | "ember-resolver": "^7.0.0", 55 | "ember-source": "~3.17.0", 56 | "ember-source-channel-url": "^2.0.1", 57 | "ember-template-lint": "^2.4.0", 58 | "ember-try": "^1.4.0", 59 | "eslint": "^6.8.0", 60 | "eslint-config-simplabs": "^0.4.0", 61 | "eslint-plugin-ember": "^7.10.1", 62 | "eslint-plugin-mocha": "^4.11.0", 63 | "eslint-plugin-node": "^11.0.0", 64 | "glob": "^5.0.15", 65 | "loader.js": "^4.7.0", 66 | "mocha": "^2.3.3", 67 | "nock": "^2.15.0", 68 | "npm-run-all": "^4.1.5", 69 | "qunit-dom": "^1.1.0" 70 | }, 71 | "engines": { 72 | "node": "10.* || >= 12" 73 | }, 74 | "ember": { 75 | "edition": "octane" 76 | }, 77 | "ember-addon": { 78 | "configPath": "tests/dummy/config" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: [ 7 | 'Chrome' 8 | ], 9 | launch_in_dev: [ 10 | 'Chrome' 11 | ], 12 | browser_start_timeout: 120, 13 | browser_args: { 14 | Chrome: { 15 | ci: [ 16 | // --no-sandbox is needed when running Chrome inside a container 17 | process.env.CI ? '--no-sandbox' : null, 18 | '--headless', 19 | '--disable-dev-shm-usage', 20 | '--disable-software-rasterizer', 21 | '--mute-audio', 22 | '--remote-debugging-port=0', 23 | '--window-size=1440,900' 24 | ].filter(Boolean) 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function() { 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | 3 | {{outlet}} -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | } 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let glob = require('glob'); 4 | let Mocha = require('mocha'); 5 | 6 | let mocha = new Mocha({ 7 | reporter: 'spec' 8 | }); 9 | 10 | let arg = process.argv[2]; 11 | let root = 'tests/'; 12 | 13 | function addFiles(mocha, files) { 14 | glob.sync(root + files).forEach(mocha.addFile.bind(mocha)); 15 | } 16 | 17 | addFiles(mocha, '/**/*-nodetest.js'); 18 | 19 | if (arg === 'all') { 20 | addFiles(mocha, '/**/*-nodetest-slow.js'); 21 | } 22 | 23 | mocha.run(function(failures) { 24 | process.on('exit', function() { 25 | process.exit(failures); // eslint-disable-line 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/index-nodetest.js: -------------------------------------------------------------------------------- 1 | const Promise = require('rsvp'); 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | 5 | chai.use(chaiAsPromised); 6 | 7 | const assert = chai.assert; 8 | 9 | describe('webhooks plugin', function() { 10 | let subject, plugin, context, mockUi, mockHTTP, services, serviceCalls, callbackReturnValue; 11 | 12 | let BUGSNAG_URI = 'http://notify.bugsnag.com/deploy'; 13 | 14 | before(function() { 15 | subject = require('../../index'); // eslint-disable-line node/no-missing-require 16 | }); 17 | 18 | beforeEach(function() { 19 | serviceCalls = []; 20 | 21 | plugin = subject.createDeployPlugin({ 22 | name: 'webhooks' 23 | }); 24 | 25 | callbackReturnValue = undefined; 26 | 27 | mockHTTP = function() { 28 | return function(opts, cb) { 29 | serviceCalls.push({ 30 | url: opts.url, 31 | method: opts.method, 32 | headers: opts.headers, 33 | body: opts.body || {} 34 | }); 35 | 36 | cb(callbackReturnValue); 37 | }; 38 | }; 39 | 40 | mockUi = { 41 | messages: [], 42 | write() {}, 43 | writeLine(message) { 44 | this.messages.push(message); 45 | } 46 | }; 47 | 48 | services = {}; 49 | 50 | plugin.log = function(message, opts) { 51 | opts = opts || {}; 52 | 53 | if (!opts.verbose || (opts.verbose && this.ui.verbose)) { 54 | this.ui.write('| '); 55 | this.ui.writeLine(`- ${message}`); 56 | } 57 | }; 58 | 59 | context = { 60 | ui: mockUi, 61 | 62 | config: { 63 | webhooks: { 64 | services, 65 | httpClient: mockHTTP 66 | } 67 | } 68 | }; 69 | }); 70 | 71 | it('has a name', function() { 72 | assert.equal(plugin.name, 'webhooks'); 73 | }); 74 | 75 | describe('configuring services', function() { 76 | it('warns of services that are configured but have not hook turned on', function() { 77 | services.bugsnag = { 78 | apiKey: '1234' 79 | }; 80 | 81 | services.slack = { 82 | webhookURL: '' 83 | }; 84 | 85 | plugin.beforeHook(context); 86 | plugin.configure(context); 87 | plugin.setup(context); 88 | 89 | let messages = mockUi.messages; 90 | 91 | assert.isAbove(messages.length, 0); 92 | assert.equal(messages[0], '- Warning! bugsnag - Service configuration found but no hook specified in deploy configuration. Service will not be notified.'); 93 | }); 94 | 95 | describe('preconfigured services', function() { 96 | describe('bugsnag', function() { 97 | beforeEach(function() { 98 | services.bugsnag = { 99 | didActivate: true, 100 | body: { 101 | apiKey: '1234', 102 | } 103 | }; 104 | }); 105 | 106 | it('notifies the bugsnag service correctly on `didActivate`', function() { 107 | plugin.beforeHook(context); 108 | plugin.configure(context); 109 | 110 | let promise = plugin.didActivate(context); 111 | 112 | return assert.isFulfilled(promise) 113 | .then(function() { 114 | assert.equal(serviceCalls.length, 1); 115 | 116 | let call = serviceCalls[0]; 117 | assert.equal(call.url, BUGSNAG_URI); 118 | assert.deepEqual(call.body, { apiKey: '1234' }); 119 | }); 120 | }); 121 | 122 | it('calls custom-url for preconfigured services when url is passed via config', function() { 123 | let CUSTOM_BUGSNAG_URI = 'http://bugsnag.simplabs.com/deploy'; 124 | services.bugsnag.url = CUSTOM_BUGSNAG_URI; 125 | 126 | plugin.beforeHook(context); 127 | plugin.configure(context); 128 | 129 | let promise = plugin.didActivate(context); 130 | 131 | return assert.isFulfilled(promise) 132 | .then(function() { 133 | assert.equal(serviceCalls.length, 1); 134 | 135 | let call = serviceCalls[0]; 136 | 137 | assert.equal(call.url, CUSTOM_BUGSNAG_URI); 138 | assert.deepEqual(call.body, { apiKey: '1234' }); 139 | }); 140 | }); 141 | 142 | it('is enough to specify specific properties to build the correct url', function() { 143 | services.bugsnag = { 144 | apiKey: '4321', 145 | didActivate: true 146 | }; 147 | 148 | plugin.beforeHook(context); 149 | plugin.configure(context); 150 | 151 | let promise = plugin.didActivate(context); 152 | 153 | return assert.isFulfilled(promise) 154 | .then(function() { 155 | assert.equal(serviceCalls.length, 1); 156 | 157 | let call = serviceCalls[0]; 158 | 159 | assert.equal(call.url, BUGSNAG_URI); 160 | assert.deepEqual(call.body, { apiKey: '4321' }); 161 | }); 162 | }); 163 | 164 | it('is possible to notify on different hooks', function() { 165 | services.bugsnag.didActivate = false; 166 | services.bugsnag.didDeploy = { 167 | body: { 168 | apiKey: 'hook specific' 169 | } 170 | }; 171 | 172 | plugin.beforeHook(context); 173 | plugin.configure(context); 174 | plugin.didActivate(context); 175 | 176 | let promise = plugin.didDeploy(context); 177 | 178 | return assert.isFulfilled(promise) 179 | .then(function() { 180 | assert.equal(serviceCalls.length, 1); 181 | 182 | let call = serviceCalls[0]; 183 | 184 | assert.equal(call.url, BUGSNAG_URI); 185 | assert.deepEqual(call.body, { apiKey: 'hook specific' }); 186 | }); 187 | }); 188 | }); 189 | 190 | describe('slack', function() { 191 | it('does not implement any hook by default', function() { 192 | services.slack = {}; 193 | 194 | plugin.beforeHook(context); 195 | plugin.configure(context); 196 | 197 | let promise = Promise.all([ 198 | plugin.setup(context), 199 | 200 | plugin.willDeploy(context), 201 | 202 | plugin.willBuild(context), 203 | plugin.build(context), 204 | plugin.didBuild(context), 205 | 206 | plugin.willPrepare(context), 207 | plugin.prepare(context), 208 | plugin.didPrepare(context), 209 | 210 | plugin.willUpload(context), 211 | plugin.upload(context), 212 | plugin.didUpload(context), 213 | 214 | plugin.willActivate(context), 215 | plugin.activate(context), 216 | plugin.didActivate(context), 217 | 218 | plugin.didDeploy(context), 219 | plugin.teardown(context) 220 | ]); 221 | 222 | return assert.isFulfilled(promise) 223 | .then(function() { 224 | assert.equal(serviceCalls.length, 0); 225 | }); 226 | }); 227 | 228 | it('is possible to specify hooks where slack should be notified', function() { 229 | let webhookURL = 'https://hooks.slack.com/services/my-webhook-url'; 230 | services.slack = { 231 | webhookURL, 232 | 233 | didDeploy: { 234 | body: { 235 | text: 'didDeploy' 236 | } 237 | }, 238 | 239 | didActivate: { 240 | body: { 241 | text: 'didActivate' 242 | } 243 | } 244 | }; 245 | 246 | plugin.beforeHook(context); 247 | plugin.configure(context); 248 | 249 | let promise = Promise.all([ 250 | plugin.setup(context), 251 | 252 | plugin.willDeploy(context), 253 | 254 | plugin.willBuild(context), 255 | plugin.build(context), 256 | plugin.didBuild(context), 257 | 258 | plugin.willPrepare(context), 259 | plugin.prepare(context), 260 | plugin.didPrepare(context), 261 | 262 | plugin.willUpload(context), 263 | plugin.upload(context), 264 | plugin.didUpload(context), 265 | 266 | plugin.willActivate(context), 267 | plugin.activate(context), 268 | plugin.didActivate(context), 269 | 270 | plugin.didDeploy(context), 271 | plugin.teardown(context) 272 | ]); 273 | 274 | return assert.isFulfilled(promise) 275 | .then(function() { 276 | assert.equal(serviceCalls.length, 2); 277 | 278 | let didActivateMessage = serviceCalls[0]; 279 | let didDeployMessage = serviceCalls[1]; 280 | 281 | assert.deepEqual(didActivateMessage.body, { text: 'didActivate' }); 282 | assert.deepEqual(didActivateMessage.url, webhookURL); 283 | assert.deepEqual(didActivateMessage.method, 'POST'); 284 | 285 | assert.deepEqual(didDeployMessage.body, { text: 'didDeploy' }); 286 | assert.deepEqual(didDeployMessage.url, webhookURL); 287 | assert.deepEqual(didDeployMessage.method, 'POST'); 288 | }); 289 | }); 290 | }); 291 | }); 292 | 293 | describe('user configured services', function() { 294 | it('allows to notify services that are not preconfigured', function() { 295 | let CUSTOM_URI = 'https://my-custom-hack.com/deployment-webhooks'; 296 | 297 | services.custom = { 298 | url: CUSTOM_URI, 299 | headers: {}, 300 | method: 'POST', 301 | body: { 302 | deployer: 'levelbossmike' 303 | }, 304 | didActivate: true 305 | }; 306 | 307 | plugin.beforeHook(context); 308 | plugin.configure(context); 309 | 310 | let promise = plugin.didActivate(context); 311 | 312 | return assert.isFulfilled(promise) 313 | .then(function() { 314 | assert.equal(serviceCalls.length, 1); 315 | 316 | let call = serviceCalls[0]; 317 | 318 | assert.equal(call.url, CUSTOM_URI); 319 | assert.deepEqual(call.body, { deployer: 'levelbossmike' }); 320 | }); 321 | }); 322 | }); 323 | }); 324 | }); 325 | -------------------------------------------------------------------------------- /tests/unit/lib/notify-nodetest.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiAsPromised = require('chai-as-promised'); 3 | const nock = require('nock'); 4 | 5 | chai.use(chaiAsPromised); 6 | 7 | const assert = chai.assert; 8 | 9 | describe('Notify', function() { 10 | let Notify, subject, bad_url, mockUi, plugin, url, serviceKey; 11 | 12 | before(function() { 13 | Notify = require('../../../lib/notify'); // eslint-disable-line node/no-missing-require 14 | }); 15 | 16 | beforeEach(function() { 17 | serviceKey = 'test'; 18 | 19 | mockUi = { 20 | messages: [], 21 | write() {}, 22 | writeLine(message) { 23 | this.messages.push(message); 24 | } 25 | }; 26 | plugin = { 27 | ui: mockUi, 28 | readConfig(propertyName) { 29 | if (propertyName === 'httpClient') { 30 | return undefined; 31 | } 32 | }, 33 | log(message, opts) { 34 | opts = opts || {}; 35 | 36 | if (!opts.verbose || (opts.verbose && this.ui.verbose)) { 37 | this.ui.write('| '); 38 | this.ui.writeLine(`- ${message}`); 39 | } 40 | } 41 | }; 42 | 43 | nock('http://notify.bugsnag.com') 44 | .post('/deploy', { 45 | apiKey: '1234' 46 | }) 47 | .reply(200, 'OK') 48 | .post('/deploy', { 49 | apiKey: '4321' 50 | }) 51 | .reply(200, { status: 'OK' }) 52 | .post('/deploy', { 53 | apiKey: '4321', 54 | bar: 'foo' 55 | }) 56 | .reply(500, { status: 'Internal Server Error' }); 57 | 58 | nock('http://notify.bugsnag.comm') 59 | .post('/deploy', {}) 60 | .replyWithError('Timeout'); 61 | }); 62 | 63 | describe('#send', function() { 64 | beforeEach(function() { 65 | url = 'http://notify.bugsnag.com/deploy'; 66 | bad_url = 'http://notify.bugsnag.comm/deploy'; 67 | subject = new Notify({ 68 | plugin 69 | }); 70 | }); 71 | 72 | describe('does not issue requests when `url`,`method`, `headers` or `body` is falsy in opts', function() { 73 | it('logs to the console when one of the properties is missing (verbose logging)', function() { 74 | mockUi.verbose = true; 75 | 76 | let promise = subject.send(serviceKey, { 77 | url 78 | }); 79 | 80 | return assert.isFulfilled(promise) 81 | .then(function() { 82 | let messages = mockUi.messages; 83 | 84 | // assert.isAbove(messages.length, 0); 85 | assert.equal(messages[0], `- ${serviceKey} => No request issued! Request options invalid! You have to specify \`url\`, \`headers\`, \`method\` and \`body\`.`); 86 | }); 87 | }); 88 | 89 | it('resolves immediately when no body is specified', function() { 90 | let promise = subject.send(serviceKey, { 91 | url: '/fubar' 92 | }); 93 | 94 | return assert.isFulfilled(promise); 95 | }); 96 | 97 | it('resolves immediately when no url is specified', function() { 98 | let promise = subject.send(serviceKey, { 99 | body: { 100 | apiKey: '1234' 101 | } 102 | }); 103 | 104 | return assert.isFulfilled(promise); 105 | }); 106 | 107 | it('resolves immediately when `body` is false', function() { 108 | let promise = subject.send(serviceKey, { 109 | url: false, 110 | body: { 111 | apiKey: '1234' 112 | } 113 | }); 114 | 115 | return assert.isFulfilled(promise); 116 | }); 117 | 118 | it('resolves immediately when `body` is false', function() { 119 | let promise = subject.send(serviceKey, { 120 | url: '/fubar', 121 | body: false 122 | }); 123 | 124 | return assert.isFulfilled(promise); 125 | }); 126 | 127 | it('resolves immediately when `headers` is false', function() { 128 | let promise = subject.send(serviceKey, { 129 | url: '/fubar', 130 | body: { 131 | apiKey: '1234' 132 | }, 133 | headers: false 134 | }); 135 | 136 | return assert.isFulfilled(promise); 137 | }); 138 | 139 | it('resolves immediately when `method` is false', function() { 140 | let promise = subject.send(serviceKey, { 141 | url: '/fubar', 142 | body: { 143 | apiKey: '1234' 144 | }, 145 | method: false 146 | }); 147 | 148 | return assert.isFulfilled(promise); 149 | }); 150 | }); 151 | 152 | it('calls the correct url', function() { 153 | let data = { apiKey: '1234' }; 154 | 155 | let opts = { 156 | url, 157 | body: data 158 | }; 159 | 160 | let promise = subject.send(serviceKey, opts); 161 | 162 | return assert.isFulfilled(promise); 163 | }); 164 | 165 | it('logs when a request was successful', function() { 166 | let data = { apiKey: '1234' }; 167 | 168 | let opts = { 169 | url, 170 | body: data 171 | }; 172 | 173 | let promise = subject.send(serviceKey, opts); 174 | 175 | return assert.isFulfilled(promise) 176 | .then(function() { 177 | let messages = mockUi.messages; 178 | 179 | assert.isAbove(messages.length, 0); 180 | assert.equal(messages[0], `- ${serviceKey} => OK`); 181 | }); 182 | }); 183 | 184 | it('logs when a request is an object', function() { 185 | let data = { apiKey: '4321' }; 186 | 187 | let opts = { 188 | url, 189 | body: data 190 | }; 191 | 192 | let promise = subject.send(serviceKey, opts); 193 | 194 | return assert.isFulfilled(promise) 195 | .then(function() { 196 | let messages = mockUi.messages; 197 | 198 | assert.isAbove(messages.length, 0); 199 | assert.equal(messages[0], `- ${serviceKey} => {"status":"OK"}`); 200 | }); 201 | }); 202 | 203 | it('logs when a request was successful and critical is true', function() { 204 | let opts = { 205 | url, 206 | body: { apiKey: '4321' }, 207 | critical: true 208 | }; 209 | 210 | let promise = subject.send(serviceKey, opts); 211 | 212 | return assert.isFulfilled(promise) 213 | .then(function() { 214 | let messages = mockUi.messages; 215 | 216 | assert.isAbove(messages.length, 0); 217 | assert.equal(messages[0], `- ${serviceKey} => {"status":"OK"}`); 218 | }); 219 | }); 220 | 221 | describe('when request fails', function() { 222 | it('resolves when the request fails', function() { 223 | let promise = subject.send(serviceKey, { 224 | url: bad_url, 225 | body: {} 226 | }); 227 | 228 | return assert.isFulfilled(promise); 229 | }); 230 | 231 | it('logs to the console', function() { 232 | let promise = subject.send(serviceKey, { 233 | url: bad_url, 234 | body: {} 235 | }); 236 | 237 | return assert.isFulfilled(promise) 238 | .then(function() { 239 | let messages = mockUi.messages; 240 | 241 | assert.isAbove(messages.length, 0); 242 | assert.equal(messages[0], `- ${serviceKey} => Error: Timeout`); 243 | }); 244 | }); 245 | }); 246 | 247 | describe('when request fails and critical is true', function() { 248 | it('reject when the request fails', function() { 249 | let promise = subject.send(serviceKey, { 250 | url: bad_url, 251 | body: {}, 252 | critical: true 253 | }); 254 | return assert.isRejected(promise); 255 | }); 256 | 257 | it('reject when the status code is not 2xx', function() { 258 | let promise = subject.send(serviceKey, { 259 | url, 260 | body: { 261 | apiKey: '4321', 262 | bar: 'foo' 263 | }, 264 | critical: true 265 | }); 266 | 267 | return assert.isRejected(promise); 268 | }); 269 | }); 270 | }); 271 | }); 272 | -------------------------------------------------------------------------------- /tests/unit/lib/service-nodetest.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | const chaiAsPromised = require('chai-as-promised'); 3 | 4 | chai.use(chaiAsPromised); 5 | 6 | const assert = chai.assert; 7 | 8 | describe('Service', function() { 9 | let Service; 10 | 11 | before(function() { 12 | Service = require('../../../lib/service'); // eslint-disable-line node/no-missing-require 13 | }); 14 | 15 | it('exists', function() { 16 | assert.ok(Service); 17 | }); 18 | 19 | describe('#buildServiceCall', function() { 20 | it('can use functions that will be evaluated to create config', function() { 21 | let defaults = { 22 | url: 'url', 23 | method: 'POST', 24 | headers: {}, 25 | apiKey: 'api-key', 26 | body() { 27 | return { 28 | apiKey: this.apiKey 29 | }; 30 | } 31 | }; 32 | 33 | let user = {}; 34 | 35 | let service = new Service({ defaults, user }); 36 | 37 | let serviceCallOpts = service.buildServiceCall(); 38 | 39 | let expected = { 40 | url: 'url', 41 | method: 'POST', 42 | headers: {}, 43 | body: { 44 | apiKey: 'api-key' 45 | } 46 | }; 47 | 48 | assert.deepEqual(serviceCallOpts, expected); 49 | }); 50 | 51 | it('options are overridable by user config', function() { 52 | let defaults = { 53 | url: 'url', 54 | method: 'POST', 55 | headers: {}, 56 | apiKey: 'api-key', 57 | body() { 58 | return { 59 | apiKey: this.apiKey 60 | }; 61 | } 62 | }; 63 | 64 | let user = { 65 | apiKey: 'custom' 66 | }; 67 | 68 | let service = new Service({ defaults, user }); 69 | 70 | let serviceCallOpts = service.buildServiceCall(); 71 | 72 | let expected = { 73 | url: 'url', 74 | method: 'POST', 75 | headers: {}, 76 | body: { 77 | apiKey: 'custom' 78 | } 79 | }; 80 | 81 | assert.deepEqual(serviceCallOpts, expected); 82 | }); 83 | 84 | it('calls functions in options with a context that gets passed in', function() { 85 | let defaults = { 86 | url: 'url', 87 | method: 'POST', 88 | headers: {}, 89 | apiKey: 'api-key', 90 | body() { 91 | return { 92 | apiKey: this.apiKey 93 | }; 94 | } 95 | }; 96 | 97 | let user = { 98 | body(context) { 99 | return { 100 | apiKey: context.apiKey 101 | }; 102 | } 103 | }; 104 | 105 | let context = { 106 | apiKey: 'context' 107 | }; 108 | 109 | let service = new Service({ defaults, user }); 110 | 111 | let serviceCallOpts = service.buildServiceCall(context); 112 | 113 | let expected = { 114 | url: 'url', 115 | method: 'POST', 116 | headers: {}, 117 | body: { 118 | apiKey: 'context' 119 | } 120 | }; 121 | 122 | assert.deepEqual(serviceCallOpts, expected); 123 | }); 124 | 125 | it('can override options by passing specific properties (named like deploy hooks)', function() { 126 | let defaults = { 127 | url: 'url', 128 | method: 'POST', 129 | headers: {}, 130 | apiKey: 'api-key', 131 | body() { 132 | return { 133 | apiKey: this.apiKey 134 | }; 135 | } 136 | }; 137 | 138 | let user = { 139 | url: 'lol', 140 | 141 | didActivate: { 142 | url: 'didActivate' 143 | } 144 | }; 145 | 146 | let service = new Service({ 147 | defaults, 148 | user, 149 | hook: user.didActivate 150 | }); 151 | 152 | let serviceCallOpts = service.buildServiceCall(context); 153 | 154 | let expected = { 155 | url: 'didActivate', 156 | method: 'POST', 157 | headers: {}, 158 | body: { 159 | apiKey: 'api-key' 160 | } 161 | }; 162 | 163 | assert.deepEqual(serviceCallOpts, expected); 164 | }); 165 | 166 | describe('http-authentication', function() { 167 | it('users can pass the auth property', function() { 168 | let defaults = { 169 | url: 'url', 170 | method: 'POST', 171 | headers: {}, 172 | apiKey: 'api-key', 173 | body() { 174 | return { 175 | apiKey: this.apiKey 176 | }; 177 | } 178 | }; 179 | 180 | let user = { 181 | auth: { 182 | user: 'tomster', 183 | pass: 'ember' 184 | } 185 | }; 186 | 187 | let service = new Service({ defaults, user }); 188 | 189 | let serviceCallOpts = service.buildServiceCall(); 190 | 191 | let expected = { 192 | url: 'url', 193 | method: 'POST', 194 | headers: {}, 195 | auth: { 196 | user: 'tomster', 197 | pass: 'ember' 198 | }, 199 | body: { 200 | apiKey: 'api-key' 201 | } 202 | }; 203 | 204 | assert.deepEqual(serviceCallOpts, expected); 205 | }); 206 | 207 | it('behaves like any configurable and can use a function for configuration', function() { 208 | let defaults = { 209 | url: 'url', 210 | method: 'POST', 211 | headers: {}, 212 | auth() { 213 | return { 214 | user: this.username, 215 | pass: this.password 216 | }; 217 | }, 218 | apiKey: 'api-key', 219 | body() { 220 | return { 221 | apiKey: this.apiKey 222 | }; 223 | } 224 | }; 225 | 226 | let user = { 227 | username: 'tomster', 228 | password: 'ember' 229 | }; 230 | 231 | let service = new Service({ defaults, user }); 232 | 233 | let serviceCallOpts = service.buildServiceCall(); 234 | 235 | let expected = { 236 | url: 'url', 237 | method: 'POST', 238 | headers: {}, 239 | auth: { 240 | user: 'tomster', 241 | pass: 'ember' 242 | }, 243 | body: { 244 | apiKey: 'api-key' 245 | } 246 | }; 247 | 248 | assert.deepEqual(serviceCallOpts, expected); 249 | 250 | }); 251 | }); 252 | 253 | describe('critical-webhook', function() { 254 | it('hook can be set as critical', function() { 255 | let defaults = { 256 | url: 'url', 257 | method: 'POST', 258 | headers: {}, 259 | apiKey: 'api-key', 260 | body() { 261 | return { 262 | apiKey: this.apiKey 263 | }; 264 | } 265 | }; 266 | 267 | let user = { critical: true }; 268 | 269 | let service = new Service({ defaults, user }); 270 | 271 | let serviceCallOpts = service.buildServiceCall(); 272 | 273 | let expected = { 274 | url: 'url', 275 | method: 'POST', 276 | headers: {}, 277 | critical: true, 278 | body: { 279 | apiKey: 'api-key' 280 | } 281 | }; 282 | 283 | assert.deepEqual(serviceCallOpts, expected); 284 | }); 285 | 286 | it('behaves like any configurable and can use a function for configuration', function() { 287 | let defaults = { 288 | url: 'url', 289 | method: 'POST', 290 | headers: {}, 291 | critical() { 292 | return this.isCritical; 293 | }, 294 | apiKey: 'api-key', 295 | body() { 296 | return { 297 | apiKey: this.apiKey 298 | }; 299 | } 300 | }; 301 | 302 | let user = { isCritical: true }; 303 | 304 | let service = new Service({ defaults, user }); 305 | 306 | let serviceCallOpts = service.buildServiceCall(); 307 | 308 | let expected = { 309 | url: 'url', 310 | method: 'POST', 311 | headers: {}, 312 | critical: true, 313 | body: { 314 | apiKey: 'api-key' 315 | } 316 | }; 317 | 318 | assert.deepEqual(serviceCallOpts, expected); 319 | }); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainmatter/ember-cli-deploy-webhooks/187fec9981b553c124951512377cae8935ee8590/vendor/.gitkeep --------------------------------------------------------------------------------