├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── bin └── dont-break ├── index.js ├── next-update-travis.sh ├── package.json ├── src ├── banner-spec.js ├── banner.js ├── check-updates.js ├── cli-options.js ├── dont-break-spec.js ├── dont-break.js ├── install-dependency.js ├── is-repo-url.js ├── run-in-folder.js └── test │ ├── foo │ ├── .dont-break │ └── package.json │ └── run-dont-break.js └── test ├── configs ├── .dont-break.globals.json ├── .dont-break.install.json └── .dont-break.npm-link.json ├── copy ├── copy-folder.js └── foo │ └── foo.txt └── e2e.sh /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .DS_Store 4 | .idea/ 5 | yarn.lock 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | - '6' 11 | before_script: 12 | - npm prune 13 | script: 14 | - $(npm bin)/if-node-version ">=6" ./next-update-travis.sh 15 | - npm test 16 | - echo Unit tests passed, running end to end tests 17 | - npm run e2e 18 | after_success: 19 | - npm run size 20 | - TRAVIS_JOB_NUMBER=WORKAROUND.1 $(npm bin)/if-node-version ">=6" npm run semantic-release 21 | branches: 22 | except: 23 | - /^v\d+\.\d+\.\d+$/ 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dont-break 2 | 3 | Checks if the node module in the current folder breaks unit tests for specified dependent projects. 4 | 5 | [Relevant discussion at npm](https://github.com/npm/npm/issues/6510), 6 | [Do not break dependant modules](http://glebbahmutov.com/blog/do-not-break-dependant-modules/). 7 | 8 | [![Build status][dont-break-ci-image] ][dont-break-ci-url] 9 | [![npm][dont-break-npm-image]](dont-break-npm-url) 10 | [![semantic-release][semantic-image] ][semantic-url] 11 | [![next-update-travis badge][nut-badge]][nut-readme] 12 | 13 | [dont-break-icon]: https://nodei.co/npm/dont-break.svg?downloads=true 14 | [dont-break-url]: https://npmjs.org/package/dont-break 15 | [dont-break-ci-image]: https://travis-ci.org/bahmutov/dont-break.svg?branch=master 16 | [dont-break-ci-url]: https://travis-ci.org/bahmutov/dont-break 17 | [dont-break-npm-url]: https://img.shields.io/npm/dm/dont-break 18 | [dont-break-npm-image]: https://img.shields.io/npm/dm/dont-break.svg?maxAge=2592000 19 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 20 | [semantic-url]: https://github.com/semantic-release/semantic-release 21 | [nut-badge]: https://img.shields.io/badge/next--update--travis-weekly-green.svg 22 | [nut-readme]: https://github.com/bahmutov/next-update-travis#readme 23 | 24 | ## Install 25 | ``` 26 | npm install -g dont-break 27 | ``` 28 | 29 | ## Use 30 | 31 | * Create `.dont-break.json` file in the root of your package, 32 | list module names that you would like to test as an array. 33 | * Run `dont-break` any time to test latest version of each dependent module 34 | against the curent code 35 | 36 | ## Example 37 | 38 | 2 projects. 39 | 40 | 1. First project `foo` only exports single variable `module.exports = 'foo';` 41 | 2. Second project `foo-user` depends on `foo`. 42 | 43 | `foo-user` only works if it gets string `foo` from the module it depends on, like this: 44 | ```js 45 | var str = require('foo'); 46 | console.assert(str === 'foo', 'value of foo should be "foo", but is ' + str); 47 | ``` 48 | `foo` has only a single release 0.1.0 that works for `foo-user` project. 49 | 50 | The author of `foo` changes code to be `module.exports = 'bar';` and releases it as 0.2.0. 51 | `foo-user` wants to use the latest `foo` so it updates its dependency, not expecting anything 52 | bad - foo's minor version number has been upgraded. In semantic versioning it means no breaking API 53 | changes. 54 | 55 | `foo-user` is now broken! 56 | 57 | Instead, before publishing new version to NPM, project `foo` can create a file in its 58 | project folder `.dont-break.json` with names of dependent projects to test 59 | ```bash 60 | echo '["foo-user"]' > .dont-break.json 61 | ``` 62 | You can check if the current code breaks listed dependent project by running 63 | 64 | ```bash 65 | dont-break 66 | ``` 67 | 68 | This will install each dependent project from `.dont-break.json` file into `/tmp/dont-break...` folder, 69 | will run the dependent's unit tests using `npm test` to make sure they work initially, then 70 | will copy the current project into the temp folder, overwriting the previous working version. 71 | Then it will run the tests again, throwing an exception if they stopped working. 72 | 73 | In the example case, it will report something like this 74 | ```bash 75 | $ dont-break 76 | dependents [ 'foo-user' ] 77 | testing foo-user 78 | installing foo-user 79 | installed into /tmp/foo@0.0.0-against-foo-user 80 | npm test 81 | tests work in /tmp/foo@0.0.0-against-foo-user/lib/node_modules/foo-user 82 | copied /Users/gleb/git/foo/* to /tmp/foo@0.0.0-against-foo-user/lib/node_modules/foo-user/node_modules/foo 83 | npm test 84 | npm test returned 1 85 | test errors: 86 | AssertionError: value of foo should be "foo", but is bar 87 | npm ERR! Test failed. See above for more details. 88 | npm ERR! not ok code 0 89 | tests did not work in /tmp/foo@0.0.0-against-foo-user/lib/node_modules/foo-user 90 | code 1 91 | FAIL: Current version break dependents 92 | ``` 93 | The message clearly tells you that the dependent projects as they are right now cannot 94 | upgrade to the version you are about to release. 95 | 96 | ## Dependencies 97 | 98 | You can specify GitHub repos as dependencies, because they most likely will 99 | have tests. For example in `.dont-break.json` 100 | 101 | ```js 102 | // you can use JavaScript comments in this file .dont-break.json 103 | [ 104 | "https://github.com/bahmutov/dont-break-bar" 105 | ] 106 | ``` 107 | 108 | Picking projects to test manually is a judgement call. 109 | Dont-break can fetch top N most downloaded 110 | or most starred dependent modules and save the list. 111 | * run `dont-break --top-downloads ` to find top N most downloaded dependent modules, 112 | save to `.dont-break.json` and check. 113 | * run `dont-break --top-starred ` to find top N most starred dependent modules, 114 | save to `.dont-break.json` and check. 115 | 116 | The above commands overwrite `.dont-break.json` file. 117 | 118 | ## Configuration options 119 | 120 | ### Global vs. project-level configuration 121 | You can specify different configuration options on global level or on project level. Following configs are equivalent. 122 | Project level: 123 | ``` 124 | [ 125 | { 126 | "name": "project-a", 127 | "test": "grunt test" 128 | }, 129 | { 130 | "name": "https://github.com/bahmutov/dont-break-bar", 131 | "test": "grunt test" 132 | }, 133 | { 134 | "name": "project-c", 135 | "test": "npm test:special" 136 | } 137 | ] 138 | ``` 139 | Global level: 140 | ``` 141 | { 142 | "test": "grunt test", 143 | "projects": [ 144 | "project-a", 145 | "https://github.com/bahmutov/dont-break-bar", 146 | { 147 | "name": "project-c", 148 | "test": "npm test:special" 149 | } 150 | ] 151 | } 152 | ``` 153 | Global level will simplify dont-break config if dependent projects share the same options. Also, options can be 154 | overriden on project level as in case of "project-c" here. 155 | 156 | ### Execution flow overview 157 | Dont-break performs folowing steps for each dependent project: 158 | * Clone the dependent project repo into temporary dir using `git clone`, if dependent project is a Github repo url 159 | * Install the dependent project in temporary dir using the [specified command](#install-command) 160 | * Run [post-install](#post-install-command) command if [pre-test](#pre-testing-with-previous-package-version) is not disabled 161 | * [Pre-test](#pre-testing-with-previous-package-version) the dependent project if this is not disabled 162 | * [Install current module](#current-module-installation-method) into the dependent project 163 | * Run [post-install](#post-install-command) command 164 | * [Test](#test-command) the dependent project 165 | 166 | Sections below describe how you can customize these steps. 167 | 168 | ### Name 169 | 170 | Serves to identify the dependent module by either a NPM module name (possibly with scope and version range) or Github URL. 171 | 172 | ``` 173 | [ 174 | { 175 | "name": "foo-module-name" 176 | }, { 177 | "name": "@my-scope/bar-module-name@^1.0.1-pre.1" 178 | }, { 179 | "name": "https://github.com/bahmutov/dont-break-bar" 180 | } 181 | ] 182 | ``` 183 | 184 | The above config is equivalent to its shorter version: 185 | 186 | ``` 187 | [ 188 | "foo-module-name", "@my-scope/bar-module-name@^1.0.1-pre.1", "https://github.com/bahmutov/dont-break-bar" 189 | ] 190 | ``` 191 | 192 | ### Test command 193 | 194 | You can specify a custom test command per dependent module. For example, to run `grunt test` for `foo-module-name`, 195 | but default command for module `bar-name`, list in `.dont-break.json` the following: 196 | 197 | ``` 198 | [ 199 | { 200 | "name": "foo-module-name", 201 | "test": "grunt test" 202 | }, 203 | "bar-name" 204 | ] 205 | ``` 206 | 207 | ### Install command 208 | 209 | You can specify a custom install command per dependent module. By default it's `npm install`. For example, this will use 210 | `yarn add` for `foo-module-name`, but keep default `npm install` for module `bar-name`: 211 | ``` 212 | [ 213 | { 214 | "name": "foo-module-name", 215 | "install": "yarn add" 216 | }, 217 | "bar-name" 218 | ] 219 | ``` 220 | The name of dependent module will be added to given command, e.g. for above it will run `yarn add foo-module-name`. 221 | 222 | ### Post-install command 223 | 224 | Before testing the dependent package dont-break installs its dev dependencies via `npm install` command run from the 225 | dependency directory. If you need something more you can specify it via "postinstall" config parameter like this: 226 | ``` 227 | [ 228 | { 229 | "name": "packageA", 230 | "postinstall": "npm run update" 231 | }, { 232 | "name": "packageB" 233 | } 234 | ] 235 | ``` 236 | If specified this command will run first before pretesting the old version of lib (if pretest isn't disabled), then 237 | after installing current version of lib to dependent package. You can use $CURRENT_MODULE_DIR variable here which 238 | will be replaced with a path to current module: 239 | ``` 240 | [ 241 | { 242 | "name": "packageA", 243 | "postinstall": "$CURRENT_MODULE_DIR/install-all-deps.sh", 244 | } 245 | ] 246 | ``` 247 | 248 | ### Pre-testing with previous package version 249 | By default dont-break first tests dependent module with its published version of current module, to make sure that it 250 | was working before the update. If this sounds excessive to you you can disable it with {"pretest": false} option: 251 | ``` 252 | [ 253 | { 254 | "name": "foo-module-name", 255 | "test": "grunt test", 256 | "pretest": false 257 | } 258 | ] 259 | ``` 260 | Here "foo-module-name" module will be tested only once, and "bar-name" twise: first with its published version of 261 | current module, and then with the updated version. 262 | 263 | The "pretest" property can also accept custom script to run for pretesting: 264 | ``` 265 | [ 266 | { 267 | "name": "foo-module-name", 268 | "test": "grunt test", 269 | "pretest": "grunt test && ./ci/after-pretesting-by-dont-break" 270 | } 271 | ] 272 | ``` 273 | By default it equals to "test" command. 274 | 275 | ### Current module installation method 276 | 277 | To test dependent package dont-break installs current module inside the dependent package directory. By default it uses 278 | `npm install $CURRENT_MODULE_DIR`. You can enter your command there, e.g. `yarn add $CURRENT_MODULE_DIR`. There are 279 | also pre-configured options [npm-link](https://docs.npmjs.com/cli/link) and 280 | [yarn-link](https://yarnpkg.com/lang/en/docs/cli/link/). They can be helpful in some cases, e.g. if you need to use 281 | `npm install` or `yarn` in postinstall command. To use `npm link` method specify {"currentModuleInstall": "npm-link"}: 282 | ``` 283 | { 284 | "currentModuleInstall": "npm-link", 285 | "projects": ["packageA", "packageB"] 286 | } 287 | ``` 288 | 289 | ### Env vars exported to called scripts 290 | Following env vars are available for use in scripts called by executed steps: 291 | * `$CURRENT_MODULE_DIR` - directory of current module 292 | * `$CURRENT_MODULE_NAME` - name of current module as stated in its package.json 293 | 294 | ### Installation timeout 295 | You can specify a longer installation time out, in seconds, using CLI option 296 | 297 | ``` 298 | dont-break --timeout 30 299 | ``` 300 | 301 | ## Related 302 | 303 | *dont-break* is the opposite of [next-update](https://github.com/bahmutov/next-update) 304 | that one can use to safely upgrade dependencies. 305 | 306 | ## Setting up second CI for dont-break 307 | 308 | I prefer to use a separate CI service specifically to test the current code 309 | against the dependent projects using `dont-break`. For example, the project 310 | [boggle](https://www.npmjs.com/package/boggle) is setup this way. The unit tests 311 | are run on [TravisCI](https://travis-ci.org/bahmutov/boggle) using 312 | pretty standard [.travis.yml](https://github.com/bahmutov/boggle/blob/master/.travis.yml) file 313 | 314 | ```yml 315 | language: node_js 316 | node_js: 317 | - "0.12" 318 | - "4" 319 | branches: 320 | only: 321 | - master 322 | before_script: 323 | - npm install -g grunt-cli 324 | ``` 325 | 326 | Then I setup a separate build service on [CircleCi](https://circleci.com/gh/bahmutov/boggle) 327 | just to run the `npm run dont-break` command from the `package.json` 328 | 329 | ```json 330 | "scripts": { 331 | "dont-break": "dont-break --timeout 30" 332 | } 333 | ``` 334 | 335 | We are assuming a global installation of `dont-break`, and the project lists 336 | the projects to check in the 337 | [.dont-break](https://github.com/bahmutov/boggle/blob/master/.dont-break) file. 338 | At the present there is only a single dependent project 339 | [boggle-connect](https://www.npmjs.com/package/boggle-connect). 340 | 341 | To run `dont-break` on CircleCI, I created the 342 | [circle.yml](https://github.com/bahmutov/boggle/blob/master/circle.yml) file. 343 | It should be clear what it does - installs `dont-break`, and runs the npm script command. 344 | 345 | ```yml 346 | machine: 347 | node: 348 | version: "0.12" 349 | dependencies: 350 | post: 351 | - npm install -g dont-break 352 | test: 353 | override: 354 | - npm run dont-break 355 | ``` 356 | 357 | To make the status visible, I included the CircleCI badges in the README file. 358 | 359 | ```md 360 | [![Dont-break][circle-ci-image] ][circle-ci-url] 361 | [circle-ci-image]: https://circleci.com/gh/bahmutov/boggle.svg?style=svg 362 | [circle-ci-url]: https://circleci.com/gh/bahmutov/boggle 363 | ``` 364 | 365 | which produces the following: 366 | 367 | Breaking dependencies? [![Dont-break][circle-ci-image] ][circle-ci-url] using 368 | [dont-break](https://github.com/bahmutov/dont-break) 369 | 370 | [circle-ci-image]: https://circleci.com/gh/bahmutov/boggle.svg?style=svg 371 | [circle-ci-url]: https://circleci.com/gh/bahmutov/boggle 372 | 373 | ## Development and testing 374 | 375 | This project is tested end to end using two small projects: 376 | [boggle](https://www.npmjs.com/package/boggle) and its dependent 377 | [boggle-connect](https://www.npmjs.com/package/boggle-connect). 378 | 379 | To see open github issues, use command `npm run issues` 380 | 381 | To see verbose log message, run with `DEBUG=dont-break ...` environment 382 | variable. 383 | 384 | ### Small print 385 | 386 | Author: Gleb Bahmutov © 2014 387 | 388 | * [@bahmutov](https://twitter.com/bahmutov) 389 | * [glebbahmutov.com](http://glebbahmutov.com) 390 | * [blog](http://glebbahmutov.com/blog) 391 | 392 | License: MIT - do anything with the code, but don't blame me if it does not work. 393 | 394 | Spread the word: tweet, star on github, etc. 395 | 396 | Support: if you find any problems with this module, email / tweet / 397 | [open issue](https://github.com/bahmutov/dont-break/issues?state=open) on Github 398 | 399 | Implemented using [npm-registry](https://github.com/3rd-Eden/npmjs), 400 | [lazy-ass](https://github.com/bahmutov/lazy-ass) and [npm-utils](https://github.com/bahmutov/npm-utils). 401 | 402 | ## MIT License 403 | 404 | Copyright (c) 2014 Gleb Bahmutov 405 | 406 | Permission is hereby granted, free of charge, to any person 407 | obtaining a copy of this software and associated documentation 408 | files (the "Software"), to deal in the Software without 409 | restriction, including without limitation the rights to use, 410 | copy, modify, merge, publish, distribute, sublicense, and/or sell 411 | copies of the Software, and to permit persons to whom the 412 | Software is furnished to do so, subject to the following 413 | conditions: 414 | 415 | The above copyright notice and this permission notice shall be 416 | included in all copies or substantial portions of the Software. 417 | 418 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 419 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 420 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 421 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 422 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 423 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 424 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 425 | OTHER DEALINGS IN THE SOFTWARE. 426 | -------------------------------------------------------------------------------- /bin/dont-break: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var dontBreak = require('..'); 4 | 5 | console.assert(!module.parent, 'why are you using me from another module?'); 6 | 7 | require('../src/check-updates'); 8 | 9 | var join = require('path').join; 10 | var pkg = require(join(__dirname, '..', 'package.json')); 11 | console.log(pkg.name + '@' + pkg.version, '-', pkg.description); 12 | 13 | var options = require('../src/cli-options'); 14 | dontBreak(options) 15 | .catch(function (err) { 16 | console.log('error?', err); 17 | }) 18 | .then(function exitApplication(success) { 19 | console.log('finishing with success?', success); 20 | process.exit(success ? 0 : 1); 21 | }) 22 | .done(); 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var dontBreak = require('./src/dont-break'); 2 | module.exports = dontBreak; 3 | -------------------------------------------------------------------------------- /next-update-travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then 6 | if [ "$GH_TOKEN" = "" ]; then 7 | echo "" 8 | echo "⛔️ Cannot find environment variable GH_TOKEN ⛔️" 9 | echo "Please set it up for this script to be able" 10 | echo "to push results to GitHub" 11 | echo "ℹ️ The best way is to use semantic-release to set it up" 12 | echo "" 13 | echo " https://github.com/semantic-release/semantic-release" 14 | echo "" 15 | echo "npm i -g semantic-release-cli" 16 | echo "semantic-release-cli setup" 17 | echo "" 18 | exit 1 19 | fi 20 | 21 | echo "Upgrading dependencies using next-update" 22 | npm i -g next-update 23 | 24 | # you can edit options to allow only some updates 25 | # --allow major | minor | patch 26 | # --latest true | false 27 | # see all options by installing next-update 28 | # and running next-update -h 29 | next-update --allow minor --latest false 30 | 31 | git status 32 | # if package.json is modified we have 33 | # new upgrades 34 | if git diff --name-only | grep package.json > /dev/null; then 35 | echo "There are new versions of dependencies 💪" 36 | git add package.json 37 | echo "----------- package.json diff -------------" 38 | git diff --staged 39 | echo "-------------------------------------------" 40 | git config --global user.email "next-update@ci.com" 41 | git config --global user.name "next-update" 42 | git commit -m "chore(deps): upgrade dependencies using next-update" 43 | # push back to GitHub using token 44 | git remote remove origin 45 | # TODO read origin from package.json 46 | # or use github api module github 47 | # like in https://github.com/semantic-release/semantic-release/blob/caribou/src/post.js 48 | git remote add origin https://next-update:$GH_TOKEN@github.com/bahmutov/dont-break.git 49 | git push origin HEAD:master 50 | else 51 | echo "No new versions found ✋" 52 | fi 53 | else 54 | echo "Not a cron job, normal test" 55 | fi 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dont-break", 3 | "description": "Checks if the current version of your package would break dependent projects", 4 | "version": "0.0.0-development", 5 | "author": "Gleb Bahmutov ", 6 | "bin": { 7 | "dont-break": "./bin/dont-break" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/bahmutov/dont-break/issues" 11 | }, 12 | "config": { 13 | "pre-git": { 14 | "commit-msg": "simple", 15 | "pre-commit": [ 16 | "npm run mocha", 17 | "npm test" 18 | ], 19 | "pre-push": [ 20 | "npm run e2e", 21 | "npm run size" 22 | ], 23 | "post-merge": [] 24 | } 25 | }, 26 | "contributors": [], 27 | "dependencies": { 28 | "chdir-promise": "0.6.2", 29 | "check-more-types": "2.24.0", 30 | "commander": "2.20.3", 31 | "debug": "3.2.7", 32 | "fs-extra": "^3.0.1", 33 | "ggit": "2.4.12", 34 | "hr": "0.1.3", 35 | "lazy-ass": "1.6.0", 36 | "lodash": "4.17.21", 37 | "mkdirp": "0.5.5", 38 | "npm-registry": "0.1.13", 39 | "npm-utils": "1.14.1", 40 | "os-tmpdir": "1.0.2", 41 | "q": "2.0.3", 42 | "quote": "0.4.0", 43 | "rimraf": "2.7.1", 44 | "strip-json-comments": "2.0.1", 45 | "top-dependents": "1.0.0", 46 | "update-notifier": "2.5.0" 47 | }, 48 | "devDependencies": { 49 | "git-issues": "1.3.1", 50 | "github-post-release": "1.13.1", 51 | "if-node-version": "^1.1.1", 52 | "matchdep": "1.0.1", 53 | "mocha": "3.5.3", 54 | "next-update-travis": "^1.7.1", 55 | "pre-git": "3.17.1", 56 | "semantic-release": "^6.3.6", 57 | "simple-commit-message": "3.3.2", 58 | "standard": "10.0.3" 59 | }, 60 | "engines": { 61 | "node": ">= 0.12.0", 62 | "npm": ">= 3.0.0" 63 | }, 64 | "files": [ 65 | "bin", 66 | "index.js", 67 | "src/*.js", 68 | "!src/*-spec.js" 69 | ], 70 | "homepage": "https://github.com/bahmutov/dont-break", 71 | "keywords": [ 72 | "check", 73 | "dependency", 74 | "semver", 75 | "test", 76 | "update" 77 | ], 78 | "license": "MIT", 79 | "main": "index.js", 80 | "repository": { 81 | "type": "git", 82 | "url": "https://github.com/bahmutov/dont-break.git" 83 | }, 84 | "scripts": { 85 | "issues": "git-issues", 86 | "lint": "standard --verbose --fix bin/*.js src/*.js", 87 | "mocha": "mocha src/**/*-spec.js", 88 | "pretest": "npm run lint", 89 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 90 | "show-help": "./bin/dont-break --help", 91 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 92 | "test": "npm run show-help", 93 | "test-empty": "node src/test/run-dont-break.js", 94 | "e2e": "./test/e2e.sh" 95 | }, 96 | "release": { 97 | "analyzeCommits": "simple-commit-message", 98 | "generateNotes": "github-post-release" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/banner-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | var check = require('check-more-types') 3 | 4 | /* global describe, it */ 5 | describe('banner', function () { 6 | var banner = require('./banner') 7 | 8 | it('is a function', function () { 9 | la(check.fn(banner)) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/banner.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash') 2 | var hr = require('hr').hr 3 | 4 | function banner () { 5 | var args = _.toArray(arguments) 6 | hr('=') 7 | console.log(args.join(' ')) 8 | hr('-') 9 | } 10 | 11 | module.exports = banner 12 | -------------------------------------------------------------------------------- /src/check-updates.js: -------------------------------------------------------------------------------- 1 | (function checkForUpdates () { 2 | var thisPackage = require('../package.json') 3 | require('update-notifier')({ 4 | packageName: thisPackage.name, 5 | packageVersion: thisPackage.version 6 | }).notify() 7 | }()) 8 | -------------------------------------------------------------------------------- /src/cli-options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var debug = require('debug')('dont-break') 4 | function list (val) { 5 | return val.split(',') 6 | } 7 | 8 | var program = require('commander') 9 | program 10 | .option('-t, --top-downloads ', 11 | 'Fetch N most downloaded dependent modules, save and check', parseInt) 12 | .option('-s, --top-starred ', 13 | 'Fetch N most starred dependent modules, save and check', parseInt) 14 | .option('-d, --dep ', 15 | 'Check if current code breaks given dependent project(s)', list) 16 | .option('--timeout ', 17 | 'Wait for N seconds when installing a package', parseInt) 18 | .parse(process.argv) 19 | 20 | debug('command line options') 21 | debug(program) 22 | 23 | module.exports = program 24 | -------------------------------------------------------------------------------- /src/dont-break-spec.js: -------------------------------------------------------------------------------- 1 | var la = require('lazy-ass') 2 | 3 | /* global describe, it */ 4 | describe('dont-break utils', function () { 5 | var join = require('path').join 6 | 7 | it('can join wildcards', function () { 8 | var result = join('foo', 'bar', '*') 9 | la(result === 'foo/bar/*', result) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /src/dont-break.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var check = require('check-more-types') 5 | var path = require('path') 6 | var osTmpdir = require('os-tmpdir') 7 | var join = path.join 8 | var quote = require('quote') 9 | var chdir = require('chdir-promise') 10 | var banner = require('./banner') 11 | var debug = require('debug')('dont-break') 12 | var isRepoUrl = require('./is-repo-url') 13 | 14 | var _ = require('lodash') 15 | 16 | var npmTest = require('npm-utils').test 17 | la(check.fn(npmTest), 'npm test should be a function', npmTest) 18 | 19 | var fs = require('fs-extra') 20 | var read = fs.readFileSync 21 | var exists = fs.existsSync 22 | 23 | var stripComments = require('strip-json-comments') 24 | // write found dependencies into a hidden file 25 | var dontBreakFilename = './.dont-break.json' 26 | 27 | var NAME_COMMAND_SEPARATOR = ':' 28 | var DEFAULT_TEST_COMMAND = 'npm test' 29 | var INSTALL_TIMEOUT_SECONDS = 3 * 60 30 | 31 | var install = require('./install-dependency') 32 | var runInFolder = require('./run-in-folder') 33 | 34 | function readJSON (filename) { 35 | la(exists(filename), 'cannot find JSON file to load', filename) 36 | return JSON.parse(read(filename)) 37 | } 38 | 39 | var npm = require('top-dependents') 40 | la(check.schema({ 41 | downloads: check.fn, 42 | sortedByDownloads: check.fn, 43 | topDependents: check.fn 44 | }, npm), 'invalid npm methods', npm) 45 | 46 | function saveTopDependents (name, metric, n) { 47 | la(check.unemptyString(name), 'invalid package name', name) 48 | la(check.unemptyString(metric), 'invalid metric', metric) 49 | la(check.positiveNumber(n), 'invalid top number', n) 50 | 51 | var fetchTop = _.partial(npm.downloads, metric) 52 | return npm.topDependents(name, n) 53 | .then(fetchTop) 54 | .then(npm.sortedByDownloads) 55 | .then(function (dependents) { 56 | la(check.array(dependents), 'cannot select top n, not a list', dependents) 57 | console.log('limiting top downloads to first', n, 'from the list of', dependents.length) 58 | return _.take(dependents, n) 59 | }) 60 | .then(function saveToFile (topDependents) { 61 | la(check.arrayOfStrings(topDependents), 'expected list of top strings', topDependents) 62 | // TODO use template library instead of manual concat 63 | var str = '// top ' + n + ' most dependent modules by ' + metric + ' for ' + name + '\n' 64 | str += '// data from NPM registry on ' + (new Date()).toDateString() + '\n' 65 | str += JSON.stringify(topDependents, null, 2) + '\n' 66 | return fs.writeFile(dontBreakFilename, str, 'utf-8').then(function () { 67 | console.log('saved top', n, 'dependents for', name, 'by', metric, 'to', dontBreakFilename) 68 | return topDependents 69 | }) 70 | }) 71 | } 72 | 73 | function getDependentsFromFile () { 74 | return fs.readFile(dontBreakFilename, 'utf-8') 75 | .then(stripComments) 76 | .then(function (text) { 77 | debug('loaded dependencies file', text) 78 | return text 79 | }) 80 | .then(JSON.parse) 81 | .catch(function (err) { 82 | // the file does not exist probably 83 | console.log(err && err.message) 84 | console.log('could not find file', quote(dontBreakFilename), 'in', quote(process.cwd())) 85 | console.log('no dependent projects, maybe query NPM for projects that depend on this one.') 86 | return [] 87 | }) 88 | } 89 | 90 | var currentPackageName = _.memoize(function () { 91 | var pkg = require(join(process.cwd(), 'package.json')) 92 | return pkg.name 93 | }) 94 | 95 | function getDependents (options, name) { 96 | options = options || {} 97 | var forName = name 98 | 99 | if (!name) { 100 | forName = currentPackageName() 101 | } 102 | 103 | var firstStep 104 | 105 | var metric, n 106 | if (check.number(options.topDownloads)) { 107 | metric = 'downloads' 108 | n = options.topDownloads 109 | } else if (check.number(options.topStarred)) { 110 | metric = 'starred' 111 | n = options.topStarred 112 | } 113 | if (check.unemptyString(metric) && check.number(n)) { 114 | firstStep = saveTopDependents(forName, metric, n) 115 | } else { 116 | firstStep = Promise.resolve() 117 | } 118 | 119 | return firstStep.then(getDependentsFromFile) 120 | } 121 | 122 | function testInFolder (testCommand, folder) { 123 | return runInFolder(folder, testCommand, { 124 | missing: 'missing test command', 125 | success: 'tests work', 126 | failure: 'tests did not work' 127 | }) 128 | } 129 | 130 | var linkCurrentModule = _.memoize(function (thisFolder, linkCmd) { 131 | return runInFolder(thisFolder, linkCmd, { 132 | success: 'linking current module succeeded', 133 | failure: 'linking current module failed' 134 | }) 135 | }) 136 | 137 | function getDependencyName (dependent) { 138 | if (isRepoUrl(dependent)) { 139 | debug('dependent is git repo url %s', dependent) 140 | return dependent 141 | } 142 | const nameParts = dependent.split(NAME_COMMAND_SEPARATOR) 143 | la(nameParts.length, 'expected at least module name', dependent) 144 | const moduleName = nameParts[0].trim() 145 | return moduleName 146 | } 147 | 148 | function getDependentVersion (pkg, name) { 149 | if (check.object(pkg.dependencies) && pkg.dependencies[name]) { 150 | return pkg.dependencies[name] 151 | } 152 | if (check.object(pkg.devDependencies) && pkg.devDependencies[name]) { 153 | return pkg.devDependencies[name] 154 | } 155 | } 156 | 157 | function testDependent (options, dependent, config) { 158 | var moduleTestCommand 159 | var modulePostinstallCommand 160 | var testWithPreviousVersion 161 | var currentModuleInstallMethod 162 | if (check.string(dependent)) { 163 | dependent = {name: dependent.trim()} 164 | } 165 | 166 | dependent = Object.assign({pretest: true, currentModuleInstall: 'npm install $CURRENT_MODULE_DIR'}, config, dependent) 167 | moduleTestCommand = dependent.test 168 | modulePostinstallCommand = dependent.postinstall || 'npm install' 169 | testWithPreviousVersion = dependent.pretest 170 | currentModuleInstallMethod = dependent.currentModuleInstall 171 | var dependentInstall = dependent.install 172 | 173 | dependent = dependent.name 174 | 175 | la(check.unemptyString(dependent), 'invalid dependent', dependent) 176 | banner(' testing', quote(dependent)) 177 | 178 | const moduleName = getDependencyName(dependent) 179 | 180 | function formFullFolderName () { 181 | if (isRepoUrl(dependent)) { 182 | // simple repo installation 183 | return toFolder 184 | } else { 185 | let scoped = moduleName.startsWith('@') 186 | let idx = scoped ? 1 : 0 187 | let moduleDir = moduleName.split('@')[idx] 188 | moduleDir = scoped ? `@${moduleDir}` : moduleDir 189 | return join(toFolder, 'node_modules', moduleDir) 190 | } 191 | } 192 | 193 | // var nameParts = dependent.split(NAME_COMMAND_SEPARATOR) 194 | // la(nameParts.length, 'expected at least module name', dependent) 195 | // var moduleName = nameParts[0].trim() 196 | // var moduleTestCommand = nameParts[1] || DEFAULT_TEST_COMMAND 197 | moduleTestCommand = moduleTestCommand || DEFAULT_TEST_COMMAND 198 | var testModuleInFolder = _.partial(testInFolder, moduleTestCommand) 199 | 200 | var cwd = process.cwd() 201 | var pkg = require(join(cwd, 'package.json')) 202 | process.env.CURRENT_MODULE_NAME = pkg.name 203 | process.env.CURRENT_MODULE_DIR = cwd 204 | 205 | function expandCommandVars (command) { 206 | if (!command) { 207 | return command 208 | } 209 | command = command.replace('$CURRENT_MODULE_DIR', cwd) 210 | command = command.replace('$CURRENT_MODULE_NAME', pkg.name) 211 | return command 212 | } 213 | 214 | function postInstallInFolder (dependentFolder, command) { 215 | if (command) { 216 | command = expandCommandVars(command) 217 | return runInFolder(dependentFolder, command, { 218 | success: 'postinstall succeeded', 219 | failure: 'postinstall did not work' 220 | }) 221 | } else { 222 | return dependentFolder 223 | } 224 | } 225 | 226 | function installCurrentModuleToDependent (sourceFolder, dependentFolder, currentModuleInstallMethod) { 227 | la(check.unemptyString(dependentFolder), 'expected dependent folder', dependentFolder) 228 | 229 | debug('testing the current module in %s', dependentFolder) 230 | debug('current module folder %s', sourceFolder) 231 | 232 | var pkgName = currentPackageName() 233 | if (_.includes(['yarn-link', 'npm-link'], currentModuleInstallMethod)) { 234 | var linkCmd = currentModuleInstallMethod.replace('-', ' ') 235 | return linkCurrentModule(sourceFolder, linkCmd) 236 | .then(function () { 237 | return runInFolder(dependentFolder, `${linkCmd} ${pkgName}`, { 238 | success: `linked ${pkgName}`, 239 | failure: `linking ${pkgName} failed` 240 | }) 241 | }) 242 | .finally(chdir.from) 243 | .then(function () { 244 | return dependentFolder 245 | }) 246 | } else { 247 | currentModuleInstallMethod = expandCommandVars(currentModuleInstallMethod) 248 | return runInFolder(dependentFolder, `${currentModuleInstallMethod}`, { 249 | success: `installed ${pkgName}`, 250 | failure: `installing ${pkgName} failed` 251 | }) 252 | } 253 | } 254 | 255 | var depName = pkg.name + '-v' + pkg.version + '-against-' + moduleName 256 | var safeName = _.kebabCase(_.deburr(depName)) 257 | debug('original name "%s", safe "%s"', depName, safeName) 258 | var toFolder = join(osTmpdir(), safeName) 259 | console.log('testing folder %s', quote(toFolder)) 260 | 261 | var timeoutSeconds = options.timeout || INSTALL_TIMEOUT_SECONDS 262 | la(check.positiveNumber(timeoutSeconds), 'wrong timeout', timeoutSeconds, options) 263 | 264 | var installOptions = { 265 | name: moduleName, 266 | prefix: toFolder, 267 | cmd: expandCommandVars(dependentInstall) 268 | } 269 | 270 | var postInstallModuleInFolder = _.partialRight(postInstallInFolder, modulePostinstallCommand) 271 | 272 | var res = install(installOptions) 273 | .timeout(timeoutSeconds * 1000, 'install timed out for ' + moduleName) 274 | .then(formFullFolderName) 275 | .then(function checkInstalledFolder (folder) { 276 | la(check.unemptyString(folder), 'expected folder', folder) 277 | la(exists(folder), 'expected folder to exist', folder) 278 | return folder 279 | }) 280 | .then(function printMessage (folder) { 281 | var installedPackage = readJSON(join(folder, 'package.json')) 282 | var moduleVersion = installedPackage.version 283 | var currentVersion = getDependentVersion(installedPackage, pkg.name) 284 | var usageMessage = currentVersion 285 | ? '\ncurrently uses ' + pkg.name + '@' + currentVersion 286 | : '\ncurrently not (directly) using ' + pkg.name 287 | banner('installed', moduleName + '@' + moduleVersion, 288 | '\ninto', folder, 289 | usageMessage, 290 | '\nwill test', pkg.name + '@' + pkg.version) 291 | return folder 292 | }) 293 | 294 | if (testWithPreviousVersion) { 295 | var modulePretestCommand 296 | if (check.type('string', testWithPreviousVersion)) { 297 | modulePretestCommand = testWithPreviousVersion 298 | } else { 299 | modulePretestCommand = moduleTestCommand 300 | } 301 | var pretestModuleInFolder = _.partial(testInFolder, modulePretestCommand) 302 | res = res 303 | .then(postInstallModuleInFolder) 304 | .then(pretestModuleInFolder) 305 | } 306 | 307 | return res 308 | .then(function (folder) { return installCurrentModuleToDependent(cwd, folder, currentModuleInstallMethod) }) 309 | .then(postInstallModuleInFolder) 310 | .then(testModuleInFolder) 311 | .finally(function () { 312 | console.log('restoring original directory', cwd) 313 | process.chdir(cwd) 314 | }) 315 | } 316 | 317 | function testDependents (options, config) { 318 | la(check.array(config.projects), 'expected dependents', config.projects) 319 | 320 | // TODO switch to parallel testing! 321 | return config.projects.reduce(function (prev, dependent) { 322 | return prev.then(function () { 323 | return testDependent(options, dependent, config) 324 | }) 325 | }, Promise.resolve(true)) 326 | } 327 | 328 | function dontBreakDependents (options, dependents) { 329 | if (check.arrayOf(check.object, dependents) || check.arrayOfStrings(dependents)) { 330 | dependents = { 331 | projects: dependents 332 | } 333 | } 334 | la(check.arrayOf(function (item) { 335 | return check.object(item) || check.string(item) 336 | }, dependents.projects), 'invalid dependents', dependents.projects) 337 | debug('dependents', dependents) 338 | if (check.empty(dependents)) { 339 | return Promise.resolve() 340 | } 341 | 342 | banner(' testing the following dependents\n ' + JSON.stringify(dependents)) 343 | 344 | var logSuccess = function logSuccess () { 345 | console.log('all dependents tested') 346 | } 347 | 348 | return testDependents(options, dependents) 349 | .then(logSuccess) 350 | } 351 | 352 | function dontBreak (options) { 353 | if (check.unemptyString(options)) { 354 | options = { 355 | folder: options 356 | } 357 | } 358 | options = options || {} 359 | options.folder = options.folder || process.cwd() 360 | 361 | debug('working in folder %s', options.folder) 362 | var start = chdir.to(options.folder) 363 | 364 | if (check.arrayOfStrings(options.dep)) { 365 | start = start.then(function () { 366 | return options.dep 367 | }) 368 | } else { 369 | start = start.then(function () { 370 | debug('getting dependents') 371 | return getDependents(options) 372 | }) 373 | } 374 | 375 | var logPass = function logPass () { 376 | console.log('PASS: Current version does not break dependents') 377 | return true 378 | } 379 | 380 | var logFail = function logFail (err) { 381 | console.log('FAIL: Current version breaks dependents') 382 | if (err && err.message) { 383 | console.error('REPORTED ERROR:', err.message) 384 | if (err.stack) { 385 | console.error(err.stack) 386 | } 387 | } 388 | return false 389 | } 390 | 391 | return start 392 | .then(_.partial(dontBreakDependents, options)) 393 | .then(logPass, logFail) 394 | .finally(chdir.from) 395 | } 396 | 397 | module.exports = dontBreak 398 | -------------------------------------------------------------------------------- /src/install-dependency.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var q = require('q') 4 | var isRepoUrl = require('./is-repo-url') 5 | var debug = require('debug')('dont-break') 6 | var exists = require('fs').existsSync 7 | var rimraf = require('rimraf') 8 | 9 | var cloneRepo = require('ggit').cloneRepo 10 | var runInFolder = require('./run-in-folder') 11 | var mkdirp = require('mkdirp') 12 | 13 | function removeFolder (folder) { 14 | if (exists(folder)) { 15 | debug('removing folder %s', folder) 16 | rimraf.sync(folder) 17 | } 18 | } 19 | 20 | function install (options) { 21 | let cmd = options.cmd || 'npm install' 22 | let res 23 | if (!exists(options.prefix)) { 24 | mkdirp.sync(options.prefix) 25 | } 26 | if (isRepoUrl(options.name)) { 27 | debug('installing repo %s', options.name) 28 | removeFolder(options.prefix) 29 | res = q(cloneRepo({ 30 | url: options.name, 31 | folder: options.prefix 32 | })).then(function () { 33 | console.log('cloned %s', options.name) 34 | }) 35 | .catch(function (err) { 36 | console.error('smth went wrong', err) 37 | throw err 38 | }) 39 | } else { 40 | if (options.name) { 41 | cmd = `${cmd} ${options.name}` 42 | } 43 | res = q([]) 44 | } 45 | 46 | return res.then(function () { 47 | return runInFolder(options.prefix, cmd, { 48 | success: 'installing dependent module succeeded', 49 | failure: 'installing dependent module failed' 50 | }) 51 | }) 52 | } 53 | 54 | module.exports = install 55 | 56 | if (!module.parent) { 57 | // quick and dirty test of module install 58 | var join = require('path').join 59 | var osTmpdir = require('os-tmpdir') 60 | var folder = join(osTmpdir(), 'test-install') 61 | console.log('tmp folder for testing') 62 | console.log(folder) 63 | 64 | install({ 65 | // name: 'boggle-connect', 66 | name: 'https://github.com/bahmutov/dont-break-bar', 67 | prefix: folder 68 | }) 69 | .then(function () { 70 | console.log('all done') 71 | }, function (err) { 72 | console.error('Could not install') 73 | console.error(err) 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /src/is-repo-url.js: -------------------------------------------------------------------------------- 1 | var is = require('check-more-types') 2 | 3 | function isGitHub (s) { 4 | return s.indexOf('github') !== -1 5 | } 6 | 7 | function isRepoUrl (s) { 8 | return (is.git(s) || is.url(s)) && isGitHub(s) 9 | } 10 | 11 | module.exports = isRepoUrl 12 | -------------------------------------------------------------------------------- /src/run-in-folder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var la = require('lazy-ass') 4 | var check = require('check-more-types') 5 | var npmTest = require('npm-utils').test 6 | var chdir = require('chdir-promise') 7 | 8 | la(check.fn(npmTest), 'npm test should be a function', npmTest) 9 | 10 | function runInFolder (folder, command, options) { 11 | la(check.unemptyString(command), options.missing, command) 12 | la(check.unemptyString(folder), 'expected folder', folder) 13 | 14 | return chdir.to(folder) 15 | .then(function () { 16 | console.log(`running "${command}" from ${folder}`) 17 | return npmTest(command) 18 | }) 19 | .then(function () { 20 | console.log(`${options.success} in ${folder}`) 21 | return folder 22 | }) 23 | .catch(function (errors) { 24 | console.error(`${options.failure} in ${folder}`) 25 | console.error('code', errors.code) 26 | throw errors 27 | }) 28 | .finally(chdir.from) 29 | } 30 | 31 | module.exports = runInFolder 32 | -------------------------------------------------------------------------------- /src/test/foo/.dont-break: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/dont-break/1506368bd9da55f4f9cb856763d177677edd1ed4/src/test/foo/.dont-break -------------------------------------------------------------------------------- /src/test/foo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo", 3 | "version": "1.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /src/test/run-dont-break.js: -------------------------------------------------------------------------------- 1 | var dontBreak = require('../dont-break'); 2 | var foo = require('path').join(__dirname, 'foo'); 3 | dontBreak(foo).done(); 4 | -------------------------------------------------------------------------------- /test/configs/.dont-break.globals.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "error", 3 | "pretest": false, 4 | "projects": [ 5 | { 6 | "name": "https://github.com/bahmutov/dont-break-bar", 7 | "postinstall": "rm package.json", 8 | "test": "node index.js" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /test/configs/.dont-break.install.json: -------------------------------------------------------------------------------- 1 | { 2 | "pretest": false, 3 | "install": "npm install", 4 | "projects": ["https://github.com/bahmutov/dont-break-bar"] 5 | } 6 | -------------------------------------------------------------------------------- /test/configs/.dont-break.npm-link.json: -------------------------------------------------------------------------------- 1 | { 2 | "pretest": false, 3 | "currentModuleInstall": "npm-link", 4 | "projects": ["https://github.com/bahmutov/dont-break-bar", "https://github.com/bahmutov/dont-break-bar"] 5 | } -------------------------------------------------------------------------------- /test/copy/copy-folder.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | 3 | cp('-r', './foo/*', './foo-copy'); 4 | console.log('copied foo'); 5 | -------------------------------------------------------------------------------- /test/copy/foo/foo.txt: -------------------------------------------------------------------------------- 1 | this is foo 2 | -------------------------------------------------------------------------------- /test/e2e.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Testing dont-break" 6 | 7 | npm link 8 | echo "Linked current dont-break to global" 9 | 10 | BASEDIR=$(pwd) 11 | echo "Creating test folder" 12 | folder=/tmp/test-dont-break 13 | rm -rf $folder 14 | mkdir $folder 15 | cd $folder 16 | echo "Created test folder $folder" 17 | 18 | echo "Cloning first module" 19 | git clone https://github.com/bahmutov/dont-break-foo.git 20 | cd dont-break-foo 21 | npm install --prod 22 | npm run dont-break 23 | cp -f $BASEDIR/test/configs/.dont-break.globals.json ./.dont-break.json 24 | npm run dont-break 25 | cp -f $BASEDIR/test/configs/.dont-break.npm-link.json ./.dont-break.json 26 | npm run dont-break 27 | cp -f $BASEDIR/test/configs/.dont-break.install.json ./.dont-break.json 28 | npm run dont-break 29 | echo "dont-break is working" 30 | --------------------------------------------------------------------------------