├── .atomignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── .releaseflowrc
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── docs
└── assets
│ ├── full-git-flow.png
│ └── simplified.png
├── package-lock.json
├── package.json
├── src
├── Git.js
├── Phase.js
├── Release.js
├── Sequence.js
├── Step.js
├── cli.js
├── defaults
│ ├── DefaultErrorFactory.js
│ ├── DefaultLogger.js
│ ├── get-bump.js
│ └── index.js
├── execCommand.js
├── phases
│ ├── Finish.js
│ ├── Publish.js
│ └── Start.js
└── plugins
│ ├── bump-package-json
│ └── index.js
│ └── generate-changelog
│ ├── ChangelogEntry.js
│ ├── changelog-template.js
│ └── index.js
├── test
├── Git.spec.js
├── Phase.spec.js
├── Release.spec.js
├── Sequence.spec.js
├── Step.spec.js
├── defaults
│ ├── DefaultErrorFactory.spec.js
│ ├── DefaultLogger.spec.js
│ └── get-bump.spec.js
├── execCommand.spec.js
├── phases
│ ├── Finish.spec.js
│ ├── Publish.spec.js
│ └── Start.spec.js
└── plugins
│ ├── ChangelogEntry.spec.js
│ ├── bump-package-json.spec.js
│ ├── changelog-template.spec.js
│ └── generate-changelog.spec.js
└── tsconfig.json
/.atomignore:
--------------------------------------------------------------------------------
1 | .nyc_output
2 | lib
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | indent_style = space
11 | indent_size = 2
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | lib/*
3 | dist/*
4 | coverage/*
5 | .nyc_output/*
6 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:mocha/recommended"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": "latest",
14 | "sourceType": "module"
15 | },
16 | "plugins": ["@typescript-eslint", "mocha"],
17 | "rules": {
18 | "no-unused-vars": "error",
19 | "mocha/no-exclusive-tests": "error"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: ["master"]
9 | pull_request:
10 | branches: ["master"]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [12.x, 14.x, 16.x]
19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v3
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | cache: "npm"
28 | - run: npm i -g npm@8
29 | - run: npm ci
30 | - run: npm run check
31 | - run: npm run test-cov
32 | - uses: codecov/codecov-action@v3
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/node,osx,linux,bower,windows,vagrant
2 |
3 | ### Node ###
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 |
9 | # Runtime data
10 | pids
11 | *.pid
12 | *.seed
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directory
30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
31 | node_modules
32 |
33 |
34 | ### OSX ###
35 | .DS_Store
36 | .AppleDouble
37 | .LSOverride
38 |
39 | # Icon must end with two \r
40 | Icon
41 |
42 | # Thumbnails
43 | ._*
44 |
45 | # Files that might appear in the root of a volume
46 | .DocumentRevisions-V100
47 | .fseventsd
48 | .Spotlight-V100
49 | .TemporaryItems
50 | .Trashes
51 | .VolumeIcon.icns
52 |
53 | # Directories potentially created on remote AFP share
54 | .AppleDB
55 | .AppleDesktop
56 | Network Trash Folder
57 | Temporary Items
58 | .apdisk
59 |
60 |
61 | ### Linux ###
62 | *~
63 |
64 | # KDE directory preferences
65 | .directory
66 |
67 | # Linux trash folder which might appear on any partition or disk
68 | .Trash-*
69 |
70 |
71 | ### Bower ###
72 | bower_components
73 | .bower-cache
74 | .bower-registry
75 | .bower-tmp
76 |
77 |
78 | ### Windows ###
79 | # Windows image file caches
80 | Thumbs.db
81 | ehthumbs.db
82 |
83 | # Folder config file
84 | Desktop.ini
85 |
86 | # Recycle Bin used on file shares
87 | $RECYCLE.BIN/
88 |
89 | # Windows Installer files
90 | *.cab
91 | *.msi
92 | *.msm
93 | *.msp
94 |
95 | # Windows shortcuts
96 | *.lnk
97 |
98 | .nyc_output/
99 | lib/
100 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | !lib/**/*
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/*
2 | lib/*
3 | dist/*
4 | coverage/*
5 | .nyc_output/*
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.releaseflowrc:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | logLevel: "debug",
3 | developmentBranch: "master",
4 | plugins: [
5 | "bump-package-json",
6 | "generate-changelog",
7 | function () {
8 | require("child_process").execSync(`npm run compile`);
9 | },
10 | ],
11 | };
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | # [v1.2.2](https://github.com/mcasimir/release-flow/compare/v1.2.1...v1.2.2)
3 |
4 | ## Fixes
5 |
6 | - correct sort for tags history retrieving ([9f7c3a5](https://github.com/mcasimir/release-flow/commits/9f7c3a5b3c2474fc1b3bfa0a3d08e204e068932f))
7 |
8 |
9 |
10 | # [v1.2.1](https://github.com/mcasimir/release-flow/compare/v1.2.0...v1.2.1)
11 |
12 | ## Fixes
13 |
14 | - **phase** - fixed version regexp ([7f85c23](https://github.com/mcasimir/release-flow/commits/7f85c230d092c5b0a093acc4e175fe6c19e32178))
15 | - **cli** - added shabang on top of cli bin ([e54f38b](https://github.com/mcasimir/release-flow/commits/e54f38bd5d37829f81b67a7814ce2b9cad9d2f3c))
16 |
17 |
18 |
19 | # [v1.2.0](https://github.com/mcasimir/release-flow/compare/v1.1.1...v1.2.0)
20 |
21 | ## Features
22 |
23 | - **cli** - allow full release command ([ffd0b60](https://github.com/mcasimir/release-flow/commits/ffd0b60eab22183e03df5990071e6d66fb65f995))
24 |
25 | ## Fixes
26 |
27 | - **bump-package-json** - inserting newline at the end of file ([e35726e](https://github.com/mcasimir/release-flow/commits/e35726e4bb36c539920a663031701e160b74b94c), [6fc63ae](https://github.com/mcasimir/release-flow/commits/6fc63aef22c4209868f6314bbf1fb913477ef268))
28 |
29 |
30 |
31 | # [v1.1.1](https://github.com/mcasimir/release-flow/compare/v1.1.0...v1.1.1)
32 |
33 | ## Fixes
34 |
35 | - fixes compare links ([4c63ed7](https://github.com/mcasimir/release-flow/commits/4c63ed75ba3dabbeb2ea3076285cda9869c48233))
36 | - fixes commit compare link ([5743558](https://github.com/mcasimir/release-flow/commits/574355890b0a8a2eb88f6f1f184af345c477eeb3))
37 |
38 |
39 |
40 | # [v1.1.0](https://github.com/mcasimir/release-flow/compare/v1.0.1...v1.1.0)
41 |
42 | ## Features
43 |
44 | - **phase** - allow dev===prod branching model ([3a896e1](https://github.com/mcasimir/release-flow/commits/3a896e1a72e45233952ec6fa8b36dc6382a5a46c))
45 | - **phase** - implemented finish ([42020b9](https://github.com/mcasimir/release-flow/commits/42020b972586e92b9422c94fe4e03ab69399f9cb))
46 | - **phase** - validate unpushed commits on start ([4ebf9bb](https://github.com/mcasimir/release-flow/commits/4ebf9bb8cbc39792b08cfbda6bdda32f66e0de41))
47 | - **release** - implemented Publish phase ([89784ee](https://github.com/mcasimir/release-flow/commits/89784ee1ef4467f14f4df1a7f8958d66b1ed6f45))
48 | - **phase** - added replace step ([ffaf425](https://github.com/mcasimir/release-flow/commits/ffaf42502a4493eb36ada2d0334b6eb6a471b05e))
49 | - **git** - added checkout tag and merge ([6059fac](https://github.com/mcasimir/release-flow/commits/6059fac0499b5496742b538f9a4b04baf5513fb0))
50 |
51 | ## Fixes
52 |
53 | - **git** - fix hasUnpushedChanges command ([7b32c97](https://github.com/mcasimir/release-flow/commits/7b32c979f333c269834d76f3af08ccd0812c3685))
54 |
55 |
56 |
57 | # [v1.0.1](https://github.com/mcasimir/release-flow/compare/v1.0.0...v1.0.1)
58 |
59 |
60 |
61 | # [v1.0.0](https://github.com/mcasimir/release-flow/commits/v1.0.0)
62 |
63 | ## Features
64 |
65 | - open release branch ([54e859f](https://github.com/mcasimir/release-flow/commits/54e859f))
66 | - plugins interface ([f214e89](https://github.com/mcasimir/release-flow/commits/f214e89))
67 | - added cli ([24582bc](https://github.com/mcasimir/release-flow/commits/24582bc))
68 | - squash commits with same message in changelog ([1aac643](https://github.com/mcasimir/release-flow/commits/1aac643))
69 | - generate changelog ([4ed812c](https://github.com/mcasimir/release-flow/commits/4ed812c))
70 | - guess release number ([065766c](https://github.com/mcasimir/release-flow/commits/065766c))
71 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Maurizio Casimirri
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # release-flow
2 |
3 | [](https://travis-ci.org/mcasimir/release-flow) [](https://codecov.io/gh/mcasimir/release-flow)
4 |
5 | ## Git flow conventional releases
6 |
7 | `release-flow` is a command line tool that simplifies the developer side of software release process taking over tedious and error prone tasks.
8 |
9 | `release-flow` mixes [git flow releases](http://danielkummer.github.io/git-flow-cheatsheet/) with conventional commits to make release process safe and painless.
10 |
11 | ### Features
12 |
13 | - Based on commit conventions
14 | - Complements perfectly with CI tools
15 | - Flexible branching model
16 | - Pluggable design
17 | - Completely configurable and customizable
18 | - Stand-alone (gulp/grunt integration is possible)
19 | - Suitable for any kind of project and language (small apps, opensource projects, libs, enterprise applications)
20 | - Built in plugins for Changelog generation and NPM bumps
21 |
22 | ### Installation
23 |
24 | Globally (use from console)
25 |
26 | ```sh
27 | npm i -g release-flow
28 | ```
29 |
30 | As project dependency (use through npm script or programmatically)
31 |
32 | ```sh
33 | npm i --save-dev release-flow
34 | ```
35 |
36 | In your `package.json`
37 |
38 | ```json
39 | "scripts": {
40 | "release": "release-flow"
41 | }
42 | ```
43 |
44 | ### Usage
45 |
46 | #### Start a release (from your development branch)
47 |
48 | ```sh
49 | release-flow start
50 | ```
51 |
52 | Effect:
53 |
54 | - Fetches remote changes
55 | - Compute the next version bump from commits (ie. `feat commit === minor`)
56 | - Validates the operation (no uncommitted/untracked changes, no existing tag for the version)
57 | - Creates and checks out a new release branch
58 | - Commits (without pushing) any eventual changes made to start the release (ie. changelog, bump package.json)
59 |
60 | #### Publish a release (from the new release branch)
61 |
62 | ```sh
63 | release-flow publish
64 | ```
65 |
66 | #### Finalize a release (from the release branch)
67 |
68 | ```sh
69 | release-flow finish
70 | ```
71 |
72 | Effect:
73 |
74 | - Fetches remote changes
75 | - Validates the operation (no uncommitted/untracked changes)
76 | - Merges release branch on master
77 | - Tags master after the release version
78 | - Merges back to development (if different from master)
79 |
80 | #### Start/Publish/Finish with one command (from your development branch)
81 |
82 | ```sh
83 | release-flow full
84 | ```
85 |
86 | Effect:
87 |
88 | Same then issuing `release-flow start`, `release-flow publish` and `release-flow finish` in sequence.
89 |
90 | **NOTE:** This approach is especially suitable for libraries and small projects that does not require a QA phase on the release branch.
91 |
92 | #### Supported Branching model
93 |
94 | `release-flow` supports both the canonical `git-flow` branching model with develop/master and a
95 | simplified branching with just master.
96 |
97 | ##### Git flow model (default)
98 |
99 | ```js
100 | // releaseflowrc
101 | module.exports = {
102 | developmentBranch: "develop",
103 | productionBranch: "master",
104 | };
105 | ```
106 |
107 | 
108 |
109 | ##### Simplified model
110 |
111 | ```js
112 | // releaseflowrc
113 | module.exports = {
114 | developmentBranch: "master",
115 | productionBranch: "master",
116 | };
117 | ```
118 |
119 | 
120 |
121 | #### Commit conventions
122 |
123 | Release flow uses conventional commits to simplify the release process (computing next version, generating changelogs).
124 |
125 | Conventional commits are commits with a specific message format:
126 |
127 | ```
128 | type([scope]): message [BREAKING]
129 | ```
130 |
131 | ie.
132 |
133 | - fix(homepage): fixed title alignment
134 | - feat: implemented user login
135 | - feat(api): BREAKING changed endpoint to list users
136 |
137 | ##### Default bump detection logic
138 |
139 | - Has one commit whose message contains `BREAKING` → `major`
140 | - Has one commit whose type is feat → `minor`
141 | - Otherwise → `patch`
142 |
143 | #### Configuration
144 |
145 | `release-flow` loads a `releaseflowrc` javascript file to allow configuration.
146 |
147 | The following is an extract of the default configuration file:
148 |
149 | ```js
150 | export default {
151 | developmentBranch: "develop",
152 | productionBranch: "master",
153 | releaseBranchPrefix: "release/",
154 | tagPrefix: "v",
155 | remoteName: "origin",
156 | logLevel: "info",
157 | initialVersion: "1.0.0",
158 | repoHttpUrl: null,
159 | ErrorFactory: DefaultErrorFactory,
160 | Logger: DefaultLogger,
161 | repoHttpProtocol: "https",
162 | getBump: getBump,
163 | plugins: [],
164 | };
165 | ```
166 |
167 | #### Included Plugins
168 |
169 | ##### Bump package json
170 |
171 | Bumps package json version on start.
172 |
173 | ```js
174 | // releaseflowrc
175 | module.exports = {
176 | plugins: ["bump-package-json"],
177 | };
178 | ```
179 |
180 | ##### Generate changelog
181 |
182 | Generates a changelog for the release and prepend it `CHANGELOG.md` or the choosen path on start.
183 |
184 | ```js
185 | // releaseflowrc
186 | module.exports = {
187 | changelogPath: 'CHANGELOG.md'
188 | changelogTemplate: release => 'changelog contents'
189 | plugins: [
190 | 'generate-changelog'
191 | ]
192 | };
193 | ```
194 |
195 | #### Advanced usage and plugin creation
196 |
197 | A plugin is just a function of the form `install(release) => null`. To register it is enough to pass it in releaseflowrc
198 |
199 | ```js
200 | // releaseflowrc
201 | module.exports = {
202 | plugins: [
203 | (release) => {
204 | // ... do something
205 | },
206 | ],
207 | };
208 | ```
209 |
210 | Tiplcally a plugin adds some `step` to a release phase (one of start, publish or finish).
211 |
212 | A step is an object with a `name` and a `run()` function.
213 |
214 | To attach a step to a phase is possible to use array methods like `push` or `splice` on the `release.phases.[start/publish/finish].steps` array or use the `release.phases.[start/publish/finish].before` method to insert the step before another specific step:
215 |
216 | ```js
217 | // releaseflowrc
218 | module.exports = {
219 | plugins: [
220 | (release) => {
221 | let logVersion = {
222 | name: "logVersion",
223 | run(release) {
224 | console.log(release.version);
225 | },
226 | };
227 |
228 | release.phases.start.before("commit", logVersion);
229 | },
230 | ],
231 | };
232 | ```
233 |
--------------------------------------------------------------------------------
/docs/assets/full-git-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcasimir/release-flow/8a56780662f1beb49dafa896984a6cdbb8ffcb67/docs/assets/full-git-flow.png
--------------------------------------------------------------------------------
/docs/assets/simplified.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mcasimir/release-flow/8a56780662f1beb49dafa896984a6cdbb8ffcb67/docs/assets/simplified.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "release-flow",
3 | "version": "1.2.2",
4 | "description": "Git flow conventional releases",
5 | "main": "lib/Release.js",
6 | "engines": {
7 | "node": ">=12",
8 | "npm": ">=8"
9 | },
10 | "files": [
11 | "lib"
12 | ],
13 | "scripts": {
14 | "test": "mocha -r ts-node/register 'test/**/*.spec.js'",
15 | "test-cov": "nyc npm run test",
16 | "check": "depcheck . && prettier --check . && eslint . --quiet",
17 | "reformat": "prettier --write .",
18 | "clean": "rm -Rf ./lib",
19 | "release": "npm run compile && ts-node ./lib/cli.js full",
20 | "compile": "tsc -p tsconfig.json",
21 | "prepare": "npm run compile"
22 | },
23 | "bin": {
24 | "release-flow": "./lib/cli.js"
25 | },
26 | "nyc": {
27 | "extends": "@istanbuljs/nyc-config-typescript",
28 | "check-coverage": true,
29 | "all": true,
30 | "include": [
31 | "src/**/!(*.test.*).[tj]s?(x)"
32 | ],
33 | "exclude": [
34 | "src/_tests_/**/*.*"
35 | ],
36 | "reporter": [
37 | "html",
38 | "lcov",
39 | "text",
40 | "text-summary"
41 | ],
42 | "report-dir": "coverage"
43 | },
44 | "author": "mcasimir (https://github.com/mcasimir)",
45 | "repository": "mcasimir/release-flow",
46 | "license": "ISC",
47 | "dependencies": {
48 | "chalk": "1.1.3",
49 | "conventional-commits-filter": "1.0.0",
50 | "conventional-commits-parser": "1.2.2",
51 | "memoize-decorator": "1.0.2",
52 | "prepend-file": "1.3.0",
53 | "semver": "5.3.0",
54 | "yargs": "4.8.1"
55 | },
56 | "devDependencies": {
57 | "@istanbuljs/nyc-config-typescript": "^1.0.2",
58 | "@typescript-eslint/eslint-plugin": "^5.36.1",
59 | "@typescript-eslint/parser": "^5.36.1",
60 | "depcheck": "^1.4.3",
61 | "eslint": "^8.23.0",
62 | "eslint-plugin-mocha": "^10.1.0",
63 | "mocha": "^10.0.0",
64 | "nyc": "^15.1.0",
65 | "prettier": "2.7.1",
66 | "sinon": "^14.0.0",
67 | "ts-node": "^10.9.1",
68 | "typescript": "^4.8.2"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Git.js:
--------------------------------------------------------------------------------
1 | import memoize from "memoize-decorator";
2 | import conventionalCommitsFilter from "conventional-commits-filter";
3 | import conventionalCommitsParser from "conventional-commits-parser";
4 | import execCommand from "./execCommand";
5 | import { valid as semverValid, compare as semverCompare } from "semver";
6 |
7 | const COMMIT_SEPARATOR = "[----COMMIT--END----]";
8 | const HASH_DELIMITER = "-hash-";
9 | const GIT_DEFAULT_OPTIONS = {
10 | remoteName: "origin",
11 | repoHttpProtocol: "http",
12 | };
13 |
14 | const TAG_HISTORY_RE = /^([0-9a-f]{5,40})\s+\(tag: refs\/tags\/([^,)]+)/;
15 |
16 | export default class Git {
17 | constructor(options = {}) {
18 | this.options = Object.assign(GIT_DEFAULT_OPTIONS, options);
19 | this.conventionalCommitsFilter =
20 | options.conventionalCommitsFilter || conventionalCommitsFilter;
21 | this.conventionalCommitsParser =
22 | options.conventionalCommitsParser || conventionalCommitsParser;
23 | this.execCommand = options.execCommand || execCommand;
24 | }
25 |
26 | @memoize
27 | getRemoteUrl() {
28 | return this.execCommand(
29 | `git config --get remote.${this.options.remoteName}.url`
30 | );
31 | }
32 |
33 | @memoize
34 | getRepoHttpUrl() {
35 | if (this.options.repoHttpUrl) {
36 | return this.options.repoHttpUrl;
37 | }
38 |
39 | let protocol = this.options.repoHttpProtocol;
40 | let remoteUrl = this._remoteUrlToHttpUrl(this.getRemoteUrl());
41 |
42 | return `${protocol}://${remoteUrl}`;
43 | }
44 |
45 | _remoteUrlToHttpUrl(remoteUrl) {
46 | return remoteUrl
47 | .replace(/^[^@]*@/, "")
48 | .replace(/:/g, "/")
49 | .replace(/\.git$/, "");
50 | }
51 |
52 | openBranch(branchName) {
53 | this.execCommand(`git checkout -b ${branchName}`);
54 | }
55 |
56 | checkout(branchName) {
57 | this.execCommand(`git checkout ${branchName}`);
58 | }
59 |
60 | commitAll(message) {
61 | this.execCommand("git add .");
62 | this.execCommand(`git commit -m '${message}'`);
63 | }
64 |
65 | pushCurrentBranch() {
66 | this.pushRef(this.getCurrentBranch());
67 | }
68 |
69 | pushRef(refName) {
70 | this.execCommand(`git push ${this.options.remoteName} ${refName}`);
71 | }
72 |
73 | tag(refName) {
74 | this.execCommand(`git tag ${refName}`);
75 | }
76 |
77 | merge(refName) {
78 | this.execCommand(`git merge ${refName}`);
79 | }
80 |
81 | link(path) {
82 | path = (path || "").replace(/^\//, "");
83 | let base = this.getRepoHttpUrl().replace(/\/$/, "");
84 | return base + "/" + path;
85 | }
86 |
87 | commitLink(commit) {
88 | return this.link(`/commits/${commit}`);
89 | }
90 |
91 | compareLink(from, to) {
92 | return this.link(`/compare/${from}...${to}`);
93 | }
94 |
95 | fetchHeadsAndTags() {
96 | return this.execCommand(
97 | [
98 | "git fetch",
99 | this.options.remoteName,
100 | `refs/heads/*:refs/remotes/${this.options.remoteName}/*`,
101 | "+refs/tags/*:refs/tags/*",
102 | ].join(" ")
103 | );
104 | }
105 |
106 | getCurrentBranch() {
107 | return this.execCommand("git rev-parse --abbrev-ref HEAD");
108 | }
109 |
110 | hasUntrackedChanges() {
111 | return Boolean(this.execCommand("git status --porcelain").length);
112 | }
113 |
114 | hasUnpushedCommits() {
115 | let refName = this.getCurrentBranch();
116 | return Boolean(
117 | this.execCommand(
118 | [
119 | "git --no-pager cherry -v",
120 | `${this.options.remoteName}/${refName} ${refName}`,
121 | ].join(" ")
122 | ).length
123 | );
124 | }
125 |
126 | _parseTagHistoryLine(line) {
127 | let tagMatch = line.match(TAG_HISTORY_RE);
128 | if (tagMatch) {
129 | return {
130 | sha: tagMatch[1],
131 | name: tagMatch[2],
132 | };
133 | }
134 | }
135 |
136 | @memoize
137 | getLocalTags() {
138 | let tagHistory = this.execCommand(
139 | 'git log --no-walk --tags --pretty="%h %d %s" --decorate=full',
140 | {
141 | splitLines: true,
142 | }
143 | )
144 | .map((line) => {
145 | return this._parseTagHistoryLine(line);
146 | })
147 | .filter((line) => line)
148 | .filter((tag) => {
149 | return semverValid(tag.name);
150 | });
151 |
152 | return tagHistory.sort((tag1, tag2) => {
153 | return semverCompare(tag1.name, tag2.name);
154 | });
155 | }
156 |
157 | hasLocalTags() {
158 | return Boolean(this.getLocalTags().length);
159 | }
160 |
161 | @memoize
162 | getLastLocalTagSha() {
163 | let tags = this.getLocalTags();
164 | let lastTag = tags[tags.length - 1];
165 | return lastTag && lastTag.sha;
166 | }
167 |
168 | @memoize
169 | getLastLocalTagName() {
170 | let tags = this.getLocalTags();
171 | let lastTag = tags[tags.length - 1];
172 | return lastTag && lastTag.name;
173 | }
174 |
175 | hasLocalTag(tagName) {
176 | let found = this.getLocalTags().find((tag) => {
177 | return tag.name === tagName;
178 | });
179 | return Boolean(found);
180 | }
181 |
182 | isCurrentBranch(branchName) {
183 | return this.getCurrentBranch() === branchName;
184 | }
185 |
186 | getRawCommits(fromSha) {
187 | let range = fromSha ? `${fromSha}..` : "";
188 | return this.execCommand(
189 | [
190 | "git --no-pager log",
191 | `--pretty='format:%B%n${HASH_DELIMITER}%n%H${COMMIT_SEPARATOR}'`,
192 | range,
193 | ].join(" ")
194 | )
195 | .split(COMMIT_SEPARATOR)
196 | .filter((line) => line);
197 | }
198 |
199 | _parseRawCommit(rawCommit, options = {}) {
200 | let commit = this.conventionalCommitsParser.sync(rawCommit, options);
201 |
202 | if (
203 | (commit.header && commit.header.match("BREAKING")) ||
204 | (commit.footer && commit.footer.match("BREAKING"))
205 | ) {
206 | commit.breaking = true;
207 | }
208 |
209 | if (commit.hash) {
210 | commit.shortHash = commit.hash.slice(0, 7);
211 | }
212 |
213 | return commit;
214 | }
215 |
216 | conventionalCommits(fromSha, options = {}) {
217 | let rawCommits = this.getRawCommits(fromSha);
218 | let commits = rawCommits.map((rawCommit) =>
219 | this._parseRawCommit(rawCommit, options)
220 | );
221 | return this.conventionalCommitsFilter(commits);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/Phase.js:
--------------------------------------------------------------------------------
1 | import Sequence from "./Sequence";
2 |
3 | export default class Phase {
4 | constructor() {
5 | this.steps = this.constructor.steps || new Sequence();
6 | }
7 |
8 | step(step) {
9 | this.steps.push(step);
10 | }
11 |
12 | before(stepName, callback) {
13 | let idx = this.steps.findIndex(function (step) {
14 | return step.name === stepName;
15 | });
16 |
17 | if (idx !== -1) {
18 | this.steps.splice(idx, 0, callback);
19 | }
20 | }
21 |
22 | replace(stepName, callback) {
23 | let idx = this.steps.findIndex(function (step) {
24 | return step.name === stepName;
25 | });
26 |
27 | if (idx !== -1) {
28 | this.steps.splice(idx, 1, callback);
29 | }
30 | }
31 |
32 | run(...args) {
33 | return this.steps.run(...args);
34 | }
35 | }
36 |
37 | export { default as Step } from "./Step";
38 |
--------------------------------------------------------------------------------
/src/Release.js:
--------------------------------------------------------------------------------
1 | import Git from "./Git";
2 | import Start from "./phases/Start";
3 | import Publish from "./phases/Publish";
4 | import Finish from "./phases/Finish";
5 | import defaults from "./defaults";
6 |
7 | export default class Release {
8 | static plugins = {};
9 |
10 | static registerPlugin(name, fn) {
11 | this.plugins[name] = fn;
12 | }
13 |
14 | constructor(options) {
15 | options = Object.assign({}, defaults, options);
16 | this.options = options;
17 | this.phases = {
18 | start: new Start(),
19 | publish: new Publish(),
20 | finish: new Finish(),
21 | };
22 |
23 | this.logger = new options.Logger({ logLevel: options.logLevel });
24 | this.errorFactory = new options.ErrorFactory();
25 | this.git = new Git(options);
26 |
27 | for (let plugin of options.plugins) {
28 | this.plugin(plugin);
29 | }
30 | }
31 |
32 | start() {
33 | return this.phases.start.run(this);
34 | }
35 |
36 | publish() {
37 | return this.phases.publish.run(this);
38 | }
39 |
40 | finish() {
41 | return this.phases.finish.run(this);
42 | }
43 |
44 | async full() {
45 | await this.start();
46 | await this.publish();
47 | await this.finish();
48 | }
49 |
50 | error(...args) {
51 | return this.errorFactory.createError(...args);
52 | }
53 |
54 | plugin(fnOrString) {
55 | if (typeof fnOrString === "function") {
56 | fnOrString(this);
57 | } else {
58 | Release.plugins[fnOrString](this);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/Sequence.js:
--------------------------------------------------------------------------------
1 | export default class Sequence extends Array {
2 | run(args) {
3 | args = args || {};
4 | return this.reduce((acc, curr) => {
5 | return acc.then(function () {
6 | let promise;
7 |
8 | try {
9 | promise =
10 | curr.run instanceof Function
11 | ? curr.run(args)
12 | : Promise.resolve(curr(args));
13 | } catch (e) {
14 | promise = Promise.reject(e);
15 | }
16 |
17 | return promise;
18 | });
19 | }, Promise.resolve(args));
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Step.js:
--------------------------------------------------------------------------------
1 | import Sequence from "./Sequence";
2 |
3 | export default function Step(name) {
4 | return (target, methodName, descriptor) => {
5 | let stepName = name || methodName;
6 | target.constructor.steps = target.constructor.steps || new Sequence();
7 |
8 | target.constructor.steps.push({
9 | name: stepName,
10 | run: target[methodName].bind(target),
11 | });
12 |
13 | return descriptor;
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/cli.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import yargs from "yargs";
4 | import { resolve } from "path";
5 | import Release from "./Release";
6 | import bumpPackageJson from "./plugins/bump-package-json";
7 | import generateChangelog from "./plugins/generate-changelog";
8 |
9 | Release.registerPlugin("bump-package-json", bumpPackageJson);
10 | Release.registerPlugin("generate-changelog", generateChangelog);
11 |
12 | let program = yargs
13 | .options({
14 | config: {
15 | alias: ["c"],
16 | describe: "configuration file path (JSON or Javascript)",
17 | type: "string",
18 | normalize: true,
19 | default: ".releaseflowrc",
20 | },
21 | })
22 | .locale("en")
23 | .usage("Usage: $0 [options]")
24 | .command("start", "Start a release")
25 | .command("publish", "Push a release")
26 | .command("finish", "Finish a release")
27 | .command("full", "Perform the full release process at once")
28 | .demand(1)
29 | .strict()
30 | .help()
31 | .version();
32 |
33 | let argv = program.argv;
34 |
35 | if (argv._.length > 1) {
36 | program.showHelp();
37 | process.exit(1);
38 | }
39 |
40 | let options = {};
41 | let configLoadError = null;
42 |
43 | if (argv.config) {
44 | try {
45 | options = require(resolve(process.cwd(), argv.config));
46 | } catch (e) {
47 | configLoadError = e;
48 | }
49 | }
50 |
51 | let release = new Release(options);
52 |
53 | if (configLoadError) {
54 | release.logger.warn(configLoadError.message);
55 | release.logger.warn("Using default configuration");
56 | }
57 |
58 | let command = argv._[0];
59 |
60 | release[command](release)
61 | .then(function () {
62 | process.exit(0);
63 | })
64 | .catch(function (err) {
65 | release.logger.error(err.message);
66 | process.exit(1);
67 | });
68 |
--------------------------------------------------------------------------------
/src/defaults/DefaultErrorFactory.js:
--------------------------------------------------------------------------------
1 | export default class DefaultErrorFactory {
2 | createError(message, data = {}) {
3 | let error = new Error(message);
4 | Object.assign(error, data);
5 | return error;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/defaults/DefaultLogger.js:
--------------------------------------------------------------------------------
1 | import chalk from "chalk";
2 |
3 | const LEVELS = {
4 | error: 0,
5 | warn: 1,
6 | info: 2,
7 | debug: 3,
8 | };
9 |
10 | const COLORS = {
11 | error: "red",
12 | warn: "yellow",
13 | info: "white",
14 | debug: "gray",
15 | };
16 |
17 | const DEFAULT_OPTIONS = {
18 | logLevel: "info",
19 | };
20 |
21 | export default class DefaultLogger {
22 | constructor(options) {
23 | options = Object.assign({}, DEFAULT_OPTIONS, options);
24 | this.level = options.logLevel;
25 | this.console = options.console || console;
26 | }
27 |
28 | error(message, metadata) {
29 | this.log("error", message, metadata);
30 | }
31 |
32 | warn(message, metadata) {
33 | this.log("warn", message, metadata);
34 | }
35 |
36 | info(message, metadata) {
37 | this.log("info", message, metadata);
38 | }
39 |
40 | debug(message, metadata) {
41 | this.log("debug", message, metadata);
42 | }
43 |
44 | log(level, message, metadata) {
45 | if (LEVELS[this.level] >= LEVELS[level]) {
46 | let openColor = chalk.styles[COLORS[level]].open;
47 | let closeColor = chalk.styles[COLORS[level]].close;
48 |
49 | let args = [
50 | openColor + chalk.bold(`${level}:`) + ` ${message}` + closeColor,
51 | metadata,
52 | ];
53 |
54 | if (metadata === null || metadata === undefined) {
55 | args.pop();
56 | }
57 |
58 | this.console.log(...args);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/defaults/get-bump.js:
--------------------------------------------------------------------------------
1 | export default function (commits) {
2 | let bump = "patch";
3 | commits.forEach(function (commit) {
4 | if (commit.breaking) {
5 | bump = "major";
6 | } else if (commit.type === "feat" && bump === "patch") {
7 | bump = "minor";
8 | }
9 | });
10 | return bump;
11 | }
12 |
--------------------------------------------------------------------------------
/src/defaults/index.js:
--------------------------------------------------------------------------------
1 | import getBump from "./get-bump";
2 | import DefaultErrorFactory from "./DefaultErrorFactory";
3 | import DefaultLogger from "./DefaultLogger";
4 |
5 | export default {
6 | developmentBranch: "develop",
7 | productionBranch: "master",
8 | releaseBranchPrefix: "release/",
9 | tagPrefix: "v",
10 | remoteName: "origin",
11 | logLevel: "info",
12 | initialVersion: "1.0.0",
13 | repoHttpUrl: null,
14 | ErrorFactory: DefaultErrorFactory,
15 | Logger: DefaultLogger,
16 | repoHttpProtocol: "https",
17 | getBump: getBump,
18 | plugins: [],
19 | };
20 |
--------------------------------------------------------------------------------
/src/execCommand.js:
--------------------------------------------------------------------------------
1 | import { execSync } from "child_process";
2 |
3 | export default function execCommand(cmd, options = {}) {
4 | let stdout = execSync(cmd).toString().trim();
5 |
6 | if (options.splitLines) {
7 | stdout = stdout
8 | .split("\n")
9 | .map(function (line) {
10 | return line.trim();
11 | })
12 | .filter(function (line) {
13 | return line;
14 | });
15 | }
16 |
17 | return stdout;
18 | }
19 |
--------------------------------------------------------------------------------
/src/phases/Finish.js:
--------------------------------------------------------------------------------
1 | import Phase, { Step } from "../Phase";
2 |
3 | export default class Finish extends Phase {
4 | @Step()
5 | fetch(release) {
6 | release.logger.debug("fetching tags and heads");
7 | try {
8 | release.git.fetchHeadsAndTags();
9 | } catch (e) {
10 | throw release.error("Unable to fetch tags and heads");
11 | }
12 | }
13 |
14 | @Step()
15 | getReleaseInfo(release) {
16 | release.branchName = release.git.getCurrentBranch();
17 | release.name = release.branchName.slice(
18 | release.options.releaseBranchPrefix.length
19 | );
20 | release.tagName = `${release.options.tagPrefix}${release.name}`;
21 | }
22 |
23 | @Step()
24 | validateReleaseBranch(release) {
25 | let currentBranch = release.git.getCurrentBranch();
26 | if (!currentBranch.startsWith(release.options.releaseBranchPrefix)) {
27 | throw release.error(
28 | "You can only finish a release from a release branch"
29 | );
30 | }
31 |
32 | if (release.git.hasUntrackedChanges()) {
33 | throw release.error("You have untracked changes");
34 | }
35 |
36 | if (release.git.hasUnpushedCommits()) {
37 | throw release.error("You have unpushed changes");
38 | }
39 | }
40 |
41 | @Step()
42 | checkoutProduction(release) {
43 | release.git.checkout(release.options.productionBranch);
44 | }
45 |
46 | @Step()
47 | validateProductionBranch(release) {
48 | if (release.git.hasUntrackedChanges()) {
49 | throw release.error("You have untracked changes");
50 | }
51 |
52 | if (release.git.hasUnpushedCommits()) {
53 | throw release.error("You have unpushed changes");
54 | }
55 | }
56 |
57 | @Step()
58 | mergeToProduction(release) {
59 | release.git.merge(release.branchName);
60 | release.git.pushRef(release.options.productionBranch);
61 | }
62 |
63 | @Step()
64 | tagProduction(release) {
65 | release.git.tag(release.tagName);
66 | release.git.pushRef(release.tagName);
67 | }
68 |
69 | @Step()
70 | mergeBackToDevelopment(release) {
71 | if (
72 | release.options.developmentBranch !== release.options.productionBranch
73 | ) {
74 | release.git.checkout(release.options.developmentBranch);
75 | release.git.merge(release.branchName);
76 | release.git.pushRef(release.options.developmentBranch);
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/phases/Publish.js:
--------------------------------------------------------------------------------
1 | import Phase, { Step } from "../Phase";
2 |
3 | export default class Publish extends Phase {
4 | @Step()
5 | validate(release) {
6 | let currentBranch = release.git.getCurrentBranch();
7 | if (!currentBranch.startsWith(release.options.releaseBranchPrefix)) {
8 | throw release.error(
9 | "You can only publish a release from a release branch"
10 | );
11 | }
12 |
13 | if (release.git.hasUntrackedChanges()) {
14 | throw release.error("You have untracked changes");
15 | }
16 | }
17 |
18 | @Step()
19 | push(release) {
20 | release.git.pushRef(release.git.getCurrentBranch());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/phases/Start.js:
--------------------------------------------------------------------------------
1 | import semver from "semver";
2 | import Phase, { Step } from "../Phase";
3 |
4 | export default class Start extends Phase {
5 | @Step()
6 | fetch(release) {
7 | release.logger.debug("fetching tags and heads");
8 | try {
9 | release.git.fetchHeadsAndTags();
10 | } catch (e) {
11 | throw release.error("Unable to fetch tags and heads");
12 | }
13 | }
14 |
15 | @Step()
16 | getPreviousVersion(release) {
17 | release.logger.debug("finding previous version");
18 | let lastTagName = release.git.getLastLocalTagName();
19 | if (lastTagName) {
20 | let versionMatch = lastTagName.match(/\d+\.\d+\.\d+.*/);
21 | release.previousVersion = versionMatch && versionMatch[0];
22 | release.previousReleaseName = `${release.options.tagPrefix}${release.previousVersion}`;
23 | } else {
24 | release.previousVersion = null;
25 | release.previousReleaseName = null;
26 | }
27 | release.logger.debug(`previous version was ${release.previousVersion}`);
28 | }
29 |
30 | @Step()
31 | getCommits(release) {
32 | let sha = release.git.getLastLocalTagSha();
33 | release.logger.debug(`getting commits from ${sha}`);
34 | release.commits = release.git.conventionalCommits(sha);
35 | release.logger.debug(`${release.commits.length} commits found`);
36 | if (!release.commits.length) {
37 | throw release.error("Nothing to release");
38 | }
39 | }
40 |
41 | @Step()
42 | getNextVersion(release) {
43 | release.logger.debug("getting next version");
44 |
45 | if (release.previousVersion) {
46 | release.bump = release.options.getBump(release.commits);
47 | release.logger.debug(`bumping ${release.bump} based on convetions`);
48 | release.nextVersion = semver.inc(release.previousVersion, release.bump);
49 | } else {
50 | release.logger.debug("no previousVersion, assuming initialVersion");
51 | release.nextVersion = release.options.initialVersion;
52 | }
53 |
54 | release.name = `${release.options.tagPrefix}${release.nextVersion}`;
55 |
56 | release.logger.debug(`next version will be ${release.nextVersion}`);
57 | }
58 |
59 | @Step()
60 | validate(release) {
61 | if (!release.git.isCurrentBranch(release.options.developmentBranch)) {
62 | throw release.error(
63 | `Current branch should be ${release.options.developmentBranch}`
64 | );
65 | }
66 |
67 | if (release.git.hasUntrackedChanges()) {
68 | throw release.error("You have untracked changes");
69 | }
70 |
71 | if (release.git.hasUnpushedCommits(release.options.developmentBranch)) {
72 | throw release.error("You have unpushed changes");
73 | }
74 |
75 | if (release.git.hasLocalTag(release.name)) {
76 | throw release.error(`Tag ${release.name} already exists`);
77 | }
78 | }
79 |
80 | @Step()
81 | openReleaseBranch(release) {
82 | release.git.openBranch(
83 | `${release.options.releaseBranchPrefix}${release.nextVersion}`
84 | );
85 | }
86 |
87 | @Step()
88 | commit(release) {
89 | release.git.commitAll(`Release ${release.name}`);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/plugins/bump-package-json/index.js:
--------------------------------------------------------------------------------
1 | import { writeFileSync, readFileSync } from "fs";
2 | import { resolve } from "path";
3 |
4 | export default function installPlugin(release) {
5 | release.phases.start.before("commit", {
6 | name: "bumpPackageJson",
7 | run(context) {
8 | let packageJsonPath = resolve(process.cwd(), "package.json");
9 | let packageJson = JSON.parse(readFileSync(packageJsonPath));
10 | packageJson.version = context.nextVersion;
11 |
12 | writeFileSync(
13 | packageJsonPath,
14 | JSON.stringify(packageJson, null, " ") + "\n"
15 | );
16 | },
17 | });
18 | }
19 |
--------------------------------------------------------------------------------
/src/plugins/generate-changelog/ChangelogEntry.js:
--------------------------------------------------------------------------------
1 | export default class ChangelogEntry {
2 | constructor(subject, options = {}) {
3 | this.subject = subject;
4 | this.scope = options.scope;
5 | this.subjectLink = options.subjectLink;
6 | this.links = options.links || [];
7 | this.children = [];
8 | }
9 |
10 | addLink(name, url) {
11 | this.links.push({ name: name, url: url });
12 | }
13 |
14 | addChild(child) {
15 | this.children.push(child);
16 | }
17 |
18 | isLeaf() {
19 | return !this.children.length;
20 | }
21 |
22 | traverse(previsit, postvisit) {
23 | if (previsit) {
24 | previsit(this);
25 | }
26 | for (let i = 0; i < this.children.length; i++) {
27 | this.children[i].traverse(previsit, postvisit);
28 | }
29 | if (postvisit) {
30 | postvisit(this);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/plugins/generate-changelog/changelog-template.js:
--------------------------------------------------------------------------------
1 | export default function (context) {
2 | let release = context.release;
3 | let lines = [``];
4 | let level = 0;
5 |
6 | release.changes.traverse(
7 | (entry) => {
8 | level++;
9 |
10 | if (level !== 1 && !entry.isLeaf()) {
11 | lines.push("");
12 | }
13 |
14 | let titleBullet =
15 | (entry.isLeaf() && level !== 1 ? "-" : Array(level + 1).join("#")) +
16 | " ";
17 |
18 | let subject = entry.subject;
19 | let scope = entry.scope || "";
20 | let links = entry.links
21 | .map(function (link) {
22 | return `[${link.name}](${link.url})`;
23 | })
24 | .join(", ");
25 |
26 | if (scope) {
27 | scope = `**${scope}** - `;
28 | }
29 |
30 | if (links) {
31 | links = ` (${links})`;
32 | }
33 |
34 | if (entry.subjectLink) {
35 | subject = `[${subject}](${entry.subjectLink})`;
36 | }
37 |
38 | lines.push(`${titleBullet}${scope}${subject}${links}`);
39 |
40 | if (!entry.isLeaf()) {
41 | if (entry.children[0].isLeaf()) {
42 | lines.push("");
43 | }
44 | }
45 | },
46 | () => {
47 | level--;
48 | }
49 | );
50 |
51 | return lines.join("\n") + "\n\n";
52 | }
53 |
--------------------------------------------------------------------------------
/src/plugins/generate-changelog/index.js:
--------------------------------------------------------------------------------
1 | import prependFile from "prepend-file";
2 | import ChangelogEntry from "./ChangelogEntry";
3 | import { resolve } from "path";
4 | import changelogTemplate from "./changelog-template";
5 |
6 | export default function installPlugin(release) {
7 | release.options.changelogTemplate =
8 | release.options.changelogTemplate || changelogTemplate;
9 |
10 | release.options.changelogPath =
11 | release.options.changelogPath || resolve(process.cwd(), "CHANGELOG.md");
12 |
13 | release.phases.start.before("commit", {
14 | name: "getChangelogEntries",
15 | run(release) {
16 | release.logger.debug("getting changelog entries");
17 | let commits = release.commits;
18 | let changes = new ChangelogEntry(release.name);
19 |
20 | changes.subjectLink = release.previousReleaseName
21 | ? release.git.compareLink(release.previousReleaseName, release.name)
22 | : release.git.commitLink(release.name);
23 |
24 | let breaking = new ChangelogEntry("Breaking Changes");
25 | let features = new ChangelogEntry("Features");
26 | let fixes = new ChangelogEntry("Fixes");
27 |
28 | let headersMap = {};
29 |
30 | commits.forEach(function (commit) {
31 | if (headersMap[commit.header]) {
32 | let change = headersMap[commit.header];
33 | change.addLink(commit.shortHash, release.git.commitLink(commit.hash));
34 | } else if (
35 | commit.type === "feat" ||
36 | commit.type === "fix" ||
37 | commit.breaking
38 | ) {
39 | let change = new ChangelogEntry(commit.subject);
40 | headersMap[commit.header] = change;
41 |
42 | if (commit.scope) {
43 | change.scope = commit.scope;
44 | }
45 |
46 | change.addLink(commit.shortHash, release.git.commitLink(commit.hash));
47 |
48 | if (commit.type === "feat") {
49 | features.addChild(change);
50 | }
51 | if (commit.type === "fix") {
52 | fixes.addChild(change);
53 | }
54 | if (commit.breaking) {
55 | breaking.addChild(change);
56 | }
57 | }
58 | });
59 |
60 | if (!breaking.isLeaf()) {
61 | changes.addChild(breaking);
62 | }
63 |
64 | if (!features.isLeaf()) {
65 | changes.addChild(features);
66 | }
67 |
68 | if (!fixes.isLeaf()) {
69 | changes.addChild(fixes);
70 | }
71 |
72 | release.changes = changes;
73 | release.logger.debug("Changes:", release.changes);
74 | },
75 | });
76 |
77 | release.phases.start.before("commit", {
78 | name: "generateChangelogContent",
79 | run(release) {
80 | release.logger.debug("generating changelog");
81 | release.changelog = release.options.changelogTemplate({
82 | release: release,
83 | });
84 | release.logger.debug("changelog", release.changelog);
85 | },
86 | });
87 |
88 | release.phases.start.before("commit", {
89 | name: "writeChangelog",
90 | run(release) {
91 | release.logger.debug("writing changelog");
92 | prependFile.sync(release.options.changelogPath, release.changelog);
93 | },
94 | });
95 | }
96 |
--------------------------------------------------------------------------------
/test/Git.spec.js:
--------------------------------------------------------------------------------
1 | import { spy, stub } from "sinon";
2 | import assert, { equal, deepEqual } from "assert";
3 | import Git from "../src/Git";
4 |
5 | describe("Git", function () {
6 | describe("getRemoteUrl", function () {
7 | it("gets remoteUrl based on the remoteName option", function () {
8 | let git = new Git({
9 | execCommand: spy(),
10 | remoteName: "abc",
11 | });
12 |
13 | git.getRemoteUrl();
14 |
15 | assert(git.execCommand.calledWith("git config --get remote.abc.url"));
16 | });
17 | });
18 |
19 | describe("getRepoHttpUrl", function () {
20 | it("gets repoHttpUrl based on the remoteUrl", function () {
21 | let git = new Git({
22 | execCommand() {
23 | return "remoteurl.com";
24 | },
25 | remoteName: "abc",
26 | });
27 |
28 | equal(git.getRepoHttpUrl(), "http://remoteurl.com");
29 | });
30 |
31 | it("returns options.repoHttpUrl if present", function () {
32 | let git = new Git({
33 | execCommand() {
34 | return "remoteurl.com";
35 | },
36 | remoteName: "abc",
37 | repoHttpUrl: "xyz",
38 | });
39 |
40 | equal(git.getRepoHttpUrl(), "xyz");
41 | });
42 | });
43 |
44 | describe("openBranch", function () {
45 | it("runs git checkout -b on the passed branch", function () {
46 | let git = new Git({
47 | execCommand: spy(),
48 | });
49 |
50 | git.openBranch("abc");
51 |
52 | assert(git.execCommand.calledWith("git checkout -b abc"));
53 | });
54 | });
55 |
56 | describe("checkout", function () {
57 | it("runs git checkout on the passed branch", function () {
58 | let git = new Git({
59 | execCommand: spy(),
60 | });
61 |
62 | git.checkout("abc");
63 |
64 | assert(git.execCommand.calledWith("git checkout abc"));
65 | });
66 | });
67 |
68 | describe("commitAll", function () {
69 | it("runs git add", function () {
70 | let git = new Git({
71 | execCommand: spy(),
72 | });
73 |
74 | git.commitAll();
75 |
76 | assert(git.execCommand.calledWith("git add ."));
77 | });
78 |
79 | it("runs git commit with the right message", function () {
80 | let git = new Git({
81 | execCommand: spy(),
82 | });
83 |
84 | git.commitAll("xyz");
85 |
86 | assert(git.execCommand.calledWith("git commit -m 'xyz'"));
87 | });
88 | });
89 |
90 | describe("pushCurrentBranch", function () {
91 | it("pushes current branch as ref", function () {
92 | let git = new Git({
93 | execCommand: spy(),
94 | });
95 | spy(git, "pushRef");
96 | stub(git, "getCurrentBranch").callsFake(() => {
97 | return "xyz";
98 | });
99 |
100 | git.pushCurrentBranch();
101 |
102 | assert(git.pushRef.calledWith("xyz"));
103 | });
104 | });
105 |
106 | describe("pushRef", function () {
107 | it("pushes a reference on the specified remote", function () {
108 | let git = new Git({
109 | execCommand: spy(),
110 | remoteName: "abc",
111 | });
112 |
113 | git.pushRef("xyz");
114 |
115 | assert(git.execCommand.calledWith("git push abc xyz"));
116 | });
117 | });
118 |
119 | describe("link", function () {
120 | it("returns link url on repo http for the given path", function () {
121 | let git = new Git({
122 | execCommand: spy(),
123 | repoHttpUrl: "http://abc.com/",
124 | });
125 |
126 | equal(git.link("xyz"), "http://abc.com/xyz");
127 | });
128 | });
129 |
130 | describe("commitLink", function () {
131 | it("returns link for a commit", function () {
132 | let git = new Git({
133 | execCommand: spy(),
134 | repoHttpUrl: "http://abc.com/",
135 | });
136 |
137 | equal(git.commitLink("xyz"), "http://abc.com/commits/xyz");
138 | });
139 | });
140 |
141 | describe("compareLink", function () {
142 | it("returns link for comparison between references", function () {
143 | let git = new Git({
144 | execCommand: spy(),
145 | repoHttpUrl: "http://abc.com/",
146 | });
147 |
148 | equal(git.compareLink("xyz", "abc"), "http://abc.com/compare/xyz...abc");
149 | });
150 | });
151 |
152 | describe("fetchHeadsAndTags", function () {
153 | it("fetches from specified remote", function () {
154 | let git = new Git({
155 | execCommand: spy(),
156 | remoteName: "abc",
157 | });
158 |
159 | git.fetchHeadsAndTags();
160 |
161 | assert(
162 | git.execCommand.calledWith(
163 | "git fetch abc refs/heads/*:refs/remotes/abc/* +refs/tags/*:refs/tags/*"
164 | )
165 | );
166 | });
167 | });
168 |
169 | describe("getCurrentBranch", function () {
170 | it("calls git rev-parse --abbrev-ref HEAD", function () {
171 | let git = new Git({
172 | execCommand: spy(),
173 | });
174 |
175 | git.getCurrentBranch();
176 |
177 | assert(git.execCommand.calledWith("git rev-parse --abbrev-ref HEAD"));
178 | });
179 | });
180 |
181 | describe("hasUntrackedChanges", function () {
182 | it("calls git status --porcelain", function () {
183 | let git = new Git({
184 | execCommand: stub().returns(""),
185 | });
186 |
187 | git.hasUntrackedChanges();
188 |
189 | assert(git.execCommand.calledWith("git status --porcelain"));
190 | });
191 |
192 | it("returns true if git status --porcelain is not empty", function () {
193 | let git = new Git({
194 | execCommand: stub().withArgs("git status --porcelain").returns("abc"),
195 | });
196 |
197 | equal(git.hasUntrackedChanges(), true);
198 | });
199 |
200 | it("returns false if git status --porcelain is empty", function () {
201 | let git = new Git({
202 | execCommand: stub().withArgs("git status --porcelain").returns(""),
203 | });
204 |
205 | equal(git.hasUntrackedChanges(), false);
206 | });
207 | });
208 |
209 | describe("hasUnpushedCommits", function () {
210 | it("calls git --no-pager cherry -v", function () {
211 | let git = new Git({
212 | execCommand: stub().returns(""),
213 | });
214 |
215 | stub(git, "getCurrentBranch").returns("b1");
216 |
217 | git.options.remoteName = "a";
218 | git.hasUnpushedCommits();
219 |
220 | assert(git.execCommand.calledWith("git --no-pager cherry -v a/b1 b1"));
221 | });
222 |
223 | it("returns true if git --no-pager cherry -v is not empty", function () {
224 | let git = new Git({
225 | execCommand: stub().withArgs("git --no-pager cherry -v").returns("abc"),
226 | });
227 |
228 | equal(git.hasUnpushedCommits(), true);
229 | });
230 |
231 | it("returns false if git --no-pager cherry -v is empty", function () {
232 | let git = new Git({
233 | execCommand: stub().withArgs("git --no-pager cherry -v").returns(""),
234 | });
235 |
236 | equal(git.hasUnpushedCommits(), false);
237 | });
238 | });
239 |
240 | describe("_parseTagHistoryLine", function () {
241 | it("returns an object for a valid line", function () {
242 | let git = new Git();
243 |
244 | deepEqual(
245 | git._parseTagHistoryLine("d3963ed (tag: refs/tags/v1.3.0) abc bcd"),
246 | {
247 | name: "v1.3.0",
248 | sha: "d3963ed",
249 | }
250 | );
251 | });
252 | });
253 |
254 | describe("getLocalTags", function () {
255 | it("runs git log", function () {
256 | let git = new Git({
257 | execCommand: stub().returns([]),
258 | });
259 |
260 | git.getLocalTags();
261 |
262 | assert(
263 | git.execCommand.calledWith(
264 | 'git log --no-walk --tags --pretty="%h %d %s" --decorate=full'
265 | )
266 | );
267 | });
268 |
269 | it("excludes lines that are not tag refs", function () {
270 | let git = new Git({
271 | execCommand: stub()
272 | .withArgs(
273 | 'git log --no-walk --tags --pretty="%h %d %s" --decorate=full'
274 | )
275 | .returns(["d3963ed (tag: refs/heads/v1.3.0) abc bcd"]),
276 | });
277 |
278 | deepEqual(git.getLocalTags(), []);
279 | });
280 |
281 | it("includes lines that are tag refs", function () {
282 | let git = new Git({
283 | execCommand: stub()
284 | .withArgs(
285 | 'git log --no-walk --tags --pretty="%h %d %s" --decorate=full'
286 | )
287 | .returns([
288 | "d3963ed (tag: refs/tags/v1.3.0) abc bcd",
289 | "d3963ed (tag: refs/tags/v1.3.1) abc bcd",
290 | ]),
291 | });
292 |
293 | deepEqual(git.getLocalTags(), [
294 | { name: "v1.3.0", sha: "d3963ed" },
295 | { name: "v1.3.1", sha: "d3963ed" },
296 | ]);
297 | });
298 |
299 | it("filters non semantic tags", function () {
300 | let git = new Git({
301 | execCommand: stub()
302 | .withArgs(
303 | 'git log --no-walk --tags --pretty="%h %d %s" --decorate=full'
304 | )
305 | .returns([
306 | "d3963ed (tag: refs/tags/vfoo) abc bcd",
307 | "d3963ed (tag: refs/tags/vbar) abc bcd",
308 | "d3963ed (tag: refs/tags/v1.2.3) abc bcd",
309 | "d3963ed (tag: refs/tags/1.3.3) abc bcd",
310 | ]),
311 | });
312 |
313 | deepEqual(git.getLocalTags(), [
314 | { name: "v1.2.3", sha: "d3963ed" },
315 | { name: "1.3.3", sha: "d3963ed" },
316 | ]);
317 | });
318 |
319 | it("sorts by semantic version", function () {
320 | let git = new Git({
321 | execCommand: stub()
322 | .withArgs(
323 | 'git log --no-walk --tags --pretty="%h %d %s" --decorate=full'
324 | )
325 | .returns([
326 | "d3963ed (tag: refs/tags/v1.3.1) abc bcd",
327 | "d3963ed (tag: refs/tags/v1.3.0) abc bcd",
328 | ]),
329 | });
330 |
331 | deepEqual(git.getLocalTags(), [
332 | { name: "v1.3.0", sha: "d3963ed" },
333 | { name: "v1.3.1", sha: "d3963ed" },
334 | ]);
335 | });
336 | });
337 |
338 | describe("hasLocalTags", function () {
339 | it("returns true if getLocalTags length > 0", function () {
340 | let git = new Git();
341 | stub(git, "getLocalTags").returns([{ name: "v1.3.1", sha: "d3963ed" }]);
342 | equal(git.hasLocalTags(), true);
343 | });
344 |
345 | it("returns false if getLocalTags length === 0", function () {
346 | let git = new Git();
347 | stub(git, "getLocalTags").returns([]);
348 | equal(git.hasLocalTags(), false);
349 | });
350 | });
351 |
352 | describe("getLastLocalTagSha", function () {
353 | it("returns the last sha from getLocalTags", function () {
354 | let git = new Git();
355 | stub(git, "getLocalTags").returns([
356 | { name: "v1.3.1", sha: "1" },
357 | { name: "v1.3.2", sha: "2" },
358 | ]);
359 | equal(git.getLastLocalTagSha(), "2");
360 | });
361 | });
362 |
363 | describe("getLastLocalTagName", function () {
364 | it("returns the last tag name from getLocalTags", function () {
365 | let git = new Git();
366 | stub(git, "getLocalTags").returns([
367 | { name: "v1.3.1", sha: "1" },
368 | { name: "v1.3.2", sha: "2" },
369 | ]);
370 | equal(git.getLastLocalTagName(), "v1.3.2");
371 | });
372 | });
373 |
374 | describe("hasLocalTag", function () {
375 | it("returns true if tag is present", function () {
376 | let git = new Git();
377 | stub(git, "getLocalTags").returns([{ name: "v1.3.1", sha: "1" }]);
378 | equal(git.hasLocalTag("v1.3.1"), true);
379 | });
380 |
381 | it("returns false if tag is missing", function () {
382 | let git = new Git();
383 | stub(git, "getLocalTags").returns([{ name: "v1.3.1", sha: "1" }]);
384 | equal(git.hasLocalTag("v1.3.2"), false);
385 | });
386 | });
387 |
388 | describe("isCurrentBranch", function () {
389 | it("returns true if branch matches", function () {
390 | let git = new Git();
391 | stub(git, "getCurrentBranch").returns("foo");
392 | equal(git.isCurrentBranch("foo"), true);
393 | });
394 |
395 | it("returns false if branch does not match", function () {
396 | let git = new Git();
397 | stub(git, "getCurrentBranch").returns("foo");
398 | equal(git.isCurrentBranch("bar"), false);
399 | });
400 | });
401 |
402 | describe("merge", function () {
403 | it("merges a local branch", function () {
404 | let git = new Git({
405 | execCommand: spy(),
406 | });
407 |
408 | git.merge("xyz");
409 |
410 | assert(git.execCommand.calledWith("git merge xyz"));
411 | });
412 | });
413 |
414 | describe("tag", function () {
415 | it("creates an anonymous tag", function () {
416 | let git = new Git({
417 | execCommand: spy(),
418 | });
419 |
420 | git.tag("xyz");
421 |
422 | assert(git.execCommand.calledWith("git tag xyz"));
423 | });
424 | });
425 |
426 | describe("getRawCommits", function () {
427 | it("calls execCommand with right argument", function () {
428 | let git = new Git({
429 | execCommand: stub().returns(""),
430 | });
431 |
432 | git.getRawCommits("123");
433 |
434 | assert(
435 | git.execCommand.calledWith(
436 | "git --no-pager log " +
437 | "--pretty='format:%B%n-hash-%n%H[----COMMIT--END----]' 123.."
438 | )
439 | );
440 | });
441 | });
442 |
443 | describe("_parseRawCommit", function () {
444 | it("calls parser with raw commit", function () {
445 | let git = new Git({
446 | conventionalCommitsParser: {
447 | sync: stub().returns({}),
448 | },
449 | });
450 |
451 | let commit = { a: "xyz" };
452 | let options = { b: 123 };
453 |
454 | git._parseRawCommit(commit, options);
455 |
456 | assert(git.conventionalCommitsParser.sync.calledWith(commit, options));
457 | });
458 |
459 | it("sets breaking if commit header matches BREAKING", function () {
460 | let git = new Git({
461 | conventionalCommitsParser: {
462 | sync: stub().returns({
463 | header: "abc BREAKING bcd",
464 | }),
465 | },
466 | });
467 |
468 | let commit = git._parseRawCommit({});
469 |
470 | assert(commit.breaking);
471 | });
472 |
473 | it("sets breaking if commit footer matches BREAKING", function () {
474 | let git = new Git({
475 | conventionalCommitsParser: {
476 | sync: stub().returns({
477 | footer: "abc BREAKING bcd",
478 | }),
479 | },
480 | });
481 |
482 | let commit = git._parseRawCommit({});
483 |
484 | assert(commit.breaking);
485 | });
486 |
487 | it("derives shortHash from hash", function () {
488 | let git = new Git({
489 | conventionalCommitsParser: {
490 | sync: stub().returns({
491 | hash: "1234567891011121314151617181920",
492 | }),
493 | },
494 | });
495 |
496 | let commit = git._parseRawCommit({});
497 |
498 | equal(commit.shortHash, "1234567");
499 | });
500 | });
501 |
502 | describe("conventionalCommits", function () {
503 | it("calls get rawCommits with provided sha", function () {
504 | let git = new Git({
505 | conventionalCommitsFilter: spy(),
506 | });
507 |
508 | stub(git, "getRawCommits").returns([]);
509 | stub(git, "_parseRawCommit").returns({});
510 |
511 | git.conventionalCommits("xyz", {});
512 |
513 | assert(git.getRawCommits.calledWith("xyz"));
514 | });
515 |
516 | it("parses all the commits", function () {
517 | let git = new Git({
518 | conventionalCommitsFilter: spy(),
519 | });
520 |
521 | stub(git, "getRawCommits").returns([{ a: 1 }, { b: 2 }]);
522 | stub(git, "_parseRawCommit").returns({});
523 |
524 | git.conventionalCommits("xyz", {});
525 |
526 | assert(git._parseRawCommit.calledWith({ a: 1 }));
527 | assert(git._parseRawCommit.calledWith({ b: 2 }));
528 | });
529 |
530 | it("filters commits with conventionalCommitsFilter", function () {
531 | let git = new Git({
532 | conventionalCommitsFilter: spy(),
533 | });
534 |
535 | stub(git, "getRawCommits").returns([{ a: 1 }, { b: 2 }]);
536 | stub(git, "_parseRawCommit").returns({ a: 1 });
537 |
538 | git.conventionalCommits("xyz", {});
539 |
540 | assert(git.conventionalCommitsFilter.calledWith([{ a: 1 }, { a: 1 }]));
541 | });
542 |
543 | it("returns anything conventionalCommitsFilter returns", function () {
544 | let git = new Git({
545 | conventionalCommitsFilter: stub().returns(123),
546 | });
547 |
548 | stub(git, "getRawCommits").returns([]);
549 | stub(git, "_parseRawCommit");
550 | equal(git.conventionalCommits(), 123);
551 | });
552 | });
553 | });
554 |
--------------------------------------------------------------------------------
/test/Phase.spec.js:
--------------------------------------------------------------------------------
1 | import { stub } from "sinon";
2 | import assert, { deepEqual } from "assert";
3 | import Phase, { Step as PhaseStep } from "../src/Phase";
4 | import Step from "../src/Step";
5 | import Sequence from "../src/Sequence";
6 |
7 | describe("Phase", function () {
8 | it("exports Step", function () {
9 | assert(PhaseStep === Step);
10 | });
11 |
12 | describe("new Phase()", function () {
13 | it("grabs steps from extending class scope (if any)", function () {
14 | class X extends Phase {
15 | static steps = [1, 2, 3];
16 | }
17 |
18 | let x = new X();
19 |
20 | deepEqual(x.steps, X.steps);
21 | });
22 |
23 | it("creates new Sequence if steps are not found in class", function () {
24 | let phase = new Phase();
25 |
26 | assert(phase.steps instanceof Sequence);
27 | });
28 | });
29 |
30 | describe("step", function () {
31 | it("adds a step", function () {
32 | let phase = new Phase();
33 |
34 | phase.step({ name: "A" });
35 | phase.step({ name: "B" });
36 |
37 | deepEqual(phase.steps, [{ name: "A" }, { name: "B" }]);
38 | });
39 | });
40 |
41 | describe("before", function () {
42 | it("insert callback before the given step", function () {
43 | let phase = new Phase();
44 |
45 | phase.step({ name: "A" });
46 | phase.step({ name: "B" });
47 |
48 | phase.before("B", { name: "C" });
49 |
50 | deepEqual(phase.steps, [{ name: "A" }, { name: "C" }, { name: "B" }]);
51 | });
52 | });
53 |
54 | describe("replace", function () {
55 | it("replace the given step", function () {
56 | let phase = new Phase();
57 |
58 | phase.step({ name: "A" });
59 | phase.step({ name: "B" });
60 |
61 | phase.replace("B", { name: "C" });
62 |
63 | deepEqual(phase.steps, [{ name: "A" }, { name: "C" }]);
64 | });
65 | });
66 |
67 | describe("run", function () {
68 | it("calls steps run", function () {
69 | let phase = new Phase();
70 |
71 | stub(phase.steps, "run");
72 |
73 | phase.run({ x: 1 });
74 |
75 | assert(phase.steps.run.calledWith({ x: 1 }));
76 | });
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/test/Release.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { equal, deepEqual } from "assert";
2 | import { stub } from "sinon";
3 | import Release from "../src/Release";
4 |
5 | describe("Release", function () {
6 | it("has plugins map", function () {
7 | deepEqual(Release.plugins, {});
8 | });
9 |
10 | describe("static registerPlugin", function () {
11 | it("registers a plugin", function () {
12 | let pluginsBackup = Release.plugins;
13 | let modifiedPlugins = {};
14 | Release.plugins = modifiedPlugins;
15 | Release.registerPlugin("x", "y");
16 | Release.plugins = pluginsBackup;
17 | equal(modifiedPlugins.x, "y");
18 | });
19 | });
20 |
21 | describe("start", function () {
22 | it("runs start phase", function () {
23 | let release = new Release();
24 | release.phases.start.run = stub();
25 | release.start();
26 | assert(release.phases.start.run.calledWith(release));
27 | });
28 | });
29 |
30 | describe("publish", function () {
31 | it("runs publish phase", function () {
32 | let release = new Release();
33 | release.phases.publish.run = stub();
34 | release.publish();
35 | assert(release.phases.publish.run.calledWith(release));
36 | });
37 | });
38 |
39 | describe("finish", function () {
40 | it("runs finish phase", function () {
41 | let release = new Release();
42 | release.phases.finish.run = stub();
43 | release.finish();
44 | assert(release.phases.finish.run.calledWith(release));
45 | });
46 | });
47 |
48 | describe("full", function () {
49 | it("invokes start, publish and finish", async function () {
50 | let release = new Release();
51 | release.start = stub().returns(Promise.resolve());
52 | release.publish = stub().returns(Promise.resolve());
53 | release.finish = stub().returns(Promise.resolve());
54 |
55 | await release.full();
56 |
57 | assert(release.start.called);
58 | assert(release.publish.called);
59 | assert(release.finish.called);
60 | });
61 | });
62 |
63 | describe("error", function () {
64 | it("invokes error factory", function () {
65 | let release = new Release();
66 |
67 | stub(release.errorFactory, "createError");
68 |
69 | release.error(1, 2, 3);
70 |
71 | assert(release.errorFactory.createError.calledWith(1, 2, 3));
72 | });
73 | });
74 |
75 | describe("new Release()", function () {
76 | it("attaches all the plugins", function () {
77 | stub(Release.prototype, "plugin");
78 |
79 | let release = new Release({
80 | plugins: ["x", "y", "z"],
81 | });
82 |
83 | let stubbedMethod = release.plugin;
84 |
85 | Release.prototype.plugin.restore();
86 |
87 | assert(stubbedMethod.calledWith("x"));
88 | assert(stubbedMethod.calledWith("y"));
89 | assert(stubbedMethod.calledWith("z"));
90 | });
91 | });
92 |
93 | describe("plugin", function () {
94 | it("invokes anonymous plugins", function () {
95 | let release = new Release();
96 | let fn = stub();
97 | release.plugin(fn);
98 | assert(fn.calledWith(release));
99 | });
100 |
101 | it("invokes named plugins", function () {
102 | let release = new Release();
103 | let fn = stub();
104 |
105 | let pluginsBackup = Release.plugins;
106 | let modifiedPlugins = {};
107 | Release.plugins = modifiedPlugins;
108 | Release.registerPlugin("fn", fn);
109 |
110 | release.plugin("fn");
111 | Release.plugins = pluginsBackup;
112 |
113 | assert(fn.calledWith(release));
114 | });
115 | });
116 | });
117 |
--------------------------------------------------------------------------------
/test/Sequence.spec.js:
--------------------------------------------------------------------------------
1 | import { spy } from "sinon";
2 | import assert from "assert";
3 | import Sequence from "../src/Sequence";
4 |
5 | describe("Sequence", function () {
6 | describe("run", function () {
7 | it("calls all the functions", async function () {
8 | let seq = new Sequence();
9 |
10 | seq.push(spy());
11 | seq.push(spy());
12 | seq.push(spy());
13 |
14 | let [fn1, fn2, fn3] = seq;
15 |
16 | await seq.run();
17 |
18 | assert(fn1.called);
19 | assert(fn2.called);
20 | assert(fn3.called);
21 | });
22 |
23 | it("works with runnables", async function () {
24 | let seq = new Sequence();
25 |
26 | let runnable1 = { run: spy() };
27 | let runnable2 = { run: spy() };
28 | let runnable3 = { run: spy() };
29 |
30 | seq.push(runnable1);
31 | seq.push(runnable2);
32 | seq.push(runnable3);
33 |
34 | await seq.run();
35 |
36 | assert(runnable1.run.called);
37 | assert(runnable2.run.called);
38 | assert(runnable3.run.called);
39 | });
40 |
41 | it("rejects if one of the runnables throws", async function () {
42 | let seq = new Sequence();
43 | let err = new Error("success");
44 | seq.push(() => {
45 | throw err;
46 | });
47 |
48 | try {
49 | await seq.run();
50 | assert.fail("seq.run was expected to throw");
51 | } catch (e) {
52 | if (e.message !== "success") {
53 | throw e;
54 | }
55 | }
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/test/Step.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { equal } from "assert";
2 | import Step from "../src/Step";
3 | import Sequence from "../src/Sequence";
4 |
5 | describe("@Step", function () {
6 | it("Initializes step sequence", function () {
7 | class TestClass1 {
8 | @Step()
9 | doSomething() {
10 | //
11 | }
12 | }
13 |
14 | assert(TestClass1.steps instanceof Sequence);
15 | });
16 |
17 | it("Adds a named step to target class property", function () {
18 | class TestClass2 {
19 | @Step("do-something")
20 | doSomething() {
21 | //
22 | }
23 | }
24 |
25 | equal(TestClass2.steps[0].name, "do-something");
26 | });
27 |
28 | it("Adds a named step from method name to target class property", function () {
29 | class TestClass3 {
30 | @Step()
31 | doSomething() {
32 | //
33 | }
34 | }
35 |
36 | equal(TestClass3.steps[0].name, "doSomething");
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/test/defaults/DefaultErrorFactory.spec.js:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import DefaultErrorFactory from "../../src/defaults/DefaultErrorFactory";
3 |
4 | describe("DefaultErrorFactory", function () {
5 | describe("create", function () {
6 | it("Creates a generic error", function () {
7 | let errorFactory = new DefaultErrorFactory();
8 | let error = errorFactory.createError("Abc");
9 | assert(error instanceof Error);
10 | assert(error.message === "Abc");
11 | assert(error.name === "Error");
12 | });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/test/defaults/DefaultLogger.spec.js:
--------------------------------------------------------------------------------
1 | import { spy, stub } from "sinon";
2 | import assert, { equal, deepEqual } from "assert";
3 | import DefaultLogger from "../../src/defaults/DefaultLogger";
4 | import { bold, red, yellow, white, gray } from "chalk";
5 |
6 | describe("DefaultLogger", function () {
7 | before(function () {
8 | if (process.env.CI) {
9 | this.skip();
10 | }
11 | });
12 |
13 | describe("new DefaultLogger()", function () {
14 | it("has default level == info", function () {
15 | let logger = new DefaultLogger();
16 | equal(logger.level, "info");
17 | });
18 |
19 | it("allows to override default level", function () {
20 | let logger = new DefaultLogger({
21 | logLevel: "warn",
22 | });
23 | equal(logger.level, "warn");
24 | });
25 | });
26 |
27 | describe("error", function () {
28 | it("calls log with level = error", function () {
29 | let logger = new DefaultLogger();
30 | stub(logger, "log");
31 | logger.error("message", { a: 1 });
32 | assert(logger.log.calledWith("error", "message", { a: 1 }));
33 | });
34 | });
35 |
36 | describe("warn", function () {
37 | it("calls log with level = warn", function () {
38 | let logger = new DefaultLogger();
39 | stub(logger, "log");
40 | logger.warn("message", { a: 1 });
41 | assert(logger.log.calledWith("warn", "message", { a: 1 }));
42 | });
43 | });
44 |
45 | describe("info", function () {
46 | it("calls log with level = info", function () {
47 | let logger = new DefaultLogger();
48 | stub(logger, "log");
49 | logger.info("message", { a: 1 });
50 | assert(logger.log.calledWith("info", "message", { a: 1 }));
51 | });
52 | });
53 |
54 | describe("debug", function () {
55 | it("calls log with level = debug", function () {
56 | let logger = new DefaultLogger();
57 | stub(logger, "log");
58 | logger.debug("message", { a: 1 });
59 | assert(logger.log.calledWith("debug", "message", { a: 1 }));
60 | });
61 | });
62 |
63 | describe("log", function () {
64 | it("ignores log level under the current level", function () {
65 | let logger = new DefaultLogger({
66 | logLevel: "error",
67 | console: {
68 | log: spy(),
69 | },
70 | });
71 |
72 | logger.info("msg");
73 |
74 | assert(logger.console.log.called === false);
75 | });
76 |
77 | it("logs for log level equal or to the current level", function () {
78 | let logger = new DefaultLogger({
79 | logLevel: "error",
80 | console: {
81 | log: spy(),
82 | },
83 | });
84 |
85 | logger.error("err");
86 | assert(logger.console.log.called === true);
87 | });
88 |
89 | it("logs for log level above or to the current level", function () {
90 | let logger = new DefaultLogger({
91 | logLevel: "warn",
92 | console: {
93 | log: spy(),
94 | },
95 | });
96 |
97 | logger.error("err");
98 |
99 | assert(logger.console.log.called === true);
100 | });
101 |
102 | it("logs error in red", function () {
103 | let logger = new DefaultLogger({
104 | logLevel: "debug",
105 | console: {
106 | log: spy(),
107 | },
108 | });
109 |
110 | logger.error("msg");
111 |
112 | deepEqual(logger.console.log.args[0], [red(bold("error:") + " msg")]);
113 | });
114 |
115 | it("logs warn in yellow", function () {
116 | let logger = new DefaultLogger({
117 | logLevel: "debug",
118 | console: {
119 | log: spy(),
120 | },
121 | });
122 |
123 | logger.warn("msg");
124 |
125 | deepEqual(logger.console.log.args[0], [yellow(bold("warn:") + " msg")]);
126 | });
127 | });
128 |
129 | it("logs info in white", function () {
130 | let logger = new DefaultLogger({
131 | logLevel: "debug",
132 | console: {
133 | log: spy(),
134 | },
135 | });
136 |
137 | logger.info("msg");
138 |
139 | deepEqual(logger.console.log.args[0], [white(bold("info:") + " msg")]);
140 | });
141 |
142 | it("logs debug in gray", function () {
143 | let logger = new DefaultLogger({
144 | logLevel: "debug",
145 | console: {
146 | log: spy(),
147 | },
148 | });
149 |
150 | logger.debug("msg");
151 |
152 | deepEqual(logger.console.log.args[0], [gray(bold("debug:") + " msg")]);
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/test/defaults/get-bump.spec.js:
--------------------------------------------------------------------------------
1 | import getBump from "../../src/defaults/get-bump";
2 | import { equal } from "assert";
3 |
4 | describe("get-bump", function () {
5 | it("returns major if there is a breaking commit", function () {
6 | let bump = getBump([{ breaking: true }]);
7 | equal(bump, "major");
8 | });
9 |
10 | it("returns major if there is a breaking commit and a feature", function () {
11 | let bump = getBump([{ breaking: true }, { type: "feat" }]);
12 | equal(bump, "major");
13 | });
14 |
15 | it("returns minor if there is a feature", function () {
16 | let bump = getBump([{ type: "feat" }]);
17 | equal(bump, "minor");
18 | });
19 |
20 | it("returns patch by default", function () {
21 | let bump = getBump([]);
22 | equal(bump, "patch");
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/test/execCommand.spec.js:
--------------------------------------------------------------------------------
1 | import execCommand from "../src/execCommand";
2 | import { stub } from "sinon";
3 | import { equal, deepEqual } from "assert";
4 | import childProcess from "child_process";
5 |
6 | describe("execCommand", function () {
7 | it("trims execSync stdout", function () {
8 | equal(execCommand("echo '\n my string '"), "my string");
9 | });
10 |
11 | it("returns stdout as an array of lines with splitLines option", function () {
12 | stub(childProcess, "execSync").callsFake(function () {
13 | return new Buffer(" line 1\nline 2\n \nline 3");
14 | });
15 |
16 | deepEqual(execCommand("abc", { splitLines: true }), [
17 | "line 1",
18 | "line 2",
19 | "line 3",
20 | ]);
21 | });
22 |
23 | afterEach(function () {
24 | if (childProcess.execSync.restore) {
25 | childProcess.execSync.restore();
26 | }
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/test/phases/Finish.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { fail, equal, throws, doesNotThrow } from "assert";
2 | import { stub } from "sinon";
3 | import Finish from "../../src/phases/Finish";
4 | import Release from "../../src/Release";
5 |
6 | describe("Finish", function () {
7 | beforeEach(function () {
8 | this.release = new Release();
9 | stub(this.release.logger, "debug");
10 | });
11 |
12 | describe("fetch", function () {
13 | it("fetches heads and tags", function () {
14 | stub(this.release.git, "fetchHeadsAndTags");
15 |
16 | let phase = new Finish();
17 | phase.fetch(this.release);
18 |
19 | assert(this.release.git.fetchHeadsAndTags.called);
20 | });
21 |
22 | it("breaks in case of error", function () {
23 | stub(this.release.git, "fetchHeadsAndTags").throws();
24 |
25 | let phase = new Finish();
26 | try {
27 | phase.fetch(this.release);
28 | fail("Should not get here");
29 | } catch (e) {
30 | equal(e.message, "Unable to fetch tags and heads");
31 | }
32 | });
33 | });
34 |
35 | describe("getReleaseInfo", function () {
36 | it("derives info from current branch", function () {
37 | this.release.options.releaseBranchPrefix = "xyz~";
38 | this.release.options.tagPrefix = "vx";
39 |
40 | stub(this.release.git, "getCurrentBranch").returns("xyz~foo");
41 |
42 | let phase = new Finish();
43 | phase.getReleaseInfo(this.release);
44 |
45 | equal(this.release.branchName, "xyz~foo");
46 | equal(this.release.name, "foo");
47 | equal(this.release.tagName, "vxfoo");
48 | });
49 | });
50 |
51 | describe("validateReleaseBranch", function () {
52 | it("throws if currentBranch is not release branch", function () {
53 | this.release.options.releaseBranchPrefix = "xyz~";
54 |
55 | stub(this.release.git, "getCurrentBranch").returns("foo");
56 | stub(this.release.git, "hasUntrackedChanges").returns(false);
57 | stub(this.release.git, "hasUnpushedCommits").returns(false);
58 |
59 | let phase = new Finish();
60 |
61 | throws(() => {
62 | phase.validateReleaseBranch(this.release);
63 | }, /You can only finish a release from a release branch$/);
64 | });
65 |
66 | it("does not throw if currentBranch is a release branch", function () {
67 | this.release.options.releaseBranchPrefix = "xyz~";
68 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
69 | stub(this.release.git, "hasUntrackedChanges").returns(false);
70 | stub(this.release.git, "hasUnpushedCommits").returns(false);
71 |
72 | let phase = new Finish();
73 |
74 | doesNotThrow(() => {
75 | phase.validateReleaseBranch(this.release);
76 | }, /You can only finish a release from a release branch$/);
77 | });
78 |
79 | it("throws if hasUntrackedChanges", function () {
80 | this.release.options.releaseBranchPrefix = "xyz~";
81 |
82 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
83 | stub(this.release.git, "hasUntrackedChanges").returns(true);
84 | stub(this.release.git, "hasUnpushedCommits").returns(false);
85 |
86 | let phase = new Finish();
87 |
88 | throws(() => {
89 | phase.validateReleaseBranch(this.release);
90 | }, /You have untracked changes$/);
91 | });
92 |
93 | it("does not throw if has not hasUntrackedChanges", function () {
94 | this.release.options.releaseBranchPrefix = "xyz~";
95 |
96 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
97 | stub(this.release.git, "hasUntrackedChanges").returns(false);
98 | stub(this.release.git, "hasUnpushedCommits").returns(false);
99 |
100 | let phase = new Finish();
101 |
102 | doesNotThrow(() => {
103 | phase.validateReleaseBranch(this.release);
104 | }, /You have untracked changes$/);
105 | });
106 |
107 | it("throws if hasUnpushedCommits", function () {
108 | this.release.options.releaseBranchPrefix = "xyz~";
109 |
110 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
111 | stub(this.release.git, "hasUntrackedChanges").returns(false);
112 | stub(this.release.git, "hasUnpushedCommits").returns(true);
113 |
114 | let phase = new Finish();
115 |
116 | throws(() => {
117 | phase.validateReleaseBranch(this.release);
118 | }, /You have unpushed changes$/);
119 | });
120 |
121 | it("does not throw if has not hasUnpushedCommits", function () {
122 | this.release.options.releaseBranchPrefix = "xyz~";
123 |
124 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
125 | stub(this.release.git, "hasUntrackedChanges").returns(false);
126 | stub(this.release.git, "hasUnpushedCommits").returns(false);
127 |
128 | let phase = new Finish();
129 |
130 | doesNotThrow(() => {
131 | phase.validateReleaseBranch(this.release);
132 | }, /You have unpushed changes$/);
133 | });
134 | });
135 |
136 | describe("checkoutProduction", function () {
137 | it("calls checkout on production branch", function () {
138 | this.release.options.productionBranch = "prod";
139 |
140 | stub(this.release.git, "checkout");
141 |
142 | let phase = new Finish();
143 | phase.checkoutProduction(this.release);
144 |
145 | assert(this.release.git.checkout.calledWith("prod"));
146 | });
147 | });
148 |
149 | describe("validateProductionBranch", function () {
150 | it("throws if hasUntrackedChanges", function () {
151 | stub(this.release.git, "hasUntrackedChanges").returns(true);
152 | stub(this.release.git, "hasUnpushedCommits").returns(false);
153 |
154 | let phase = new Finish();
155 |
156 | throws(() => {
157 | phase.validateProductionBranch(this.release);
158 | }, /You have untracked changes$/);
159 | });
160 |
161 | it("does not throw if has not hasUntrackedChanges", function () {
162 | stub(this.release.git, "hasUntrackedChanges").returns(false);
163 | stub(this.release.git, "hasUnpushedCommits").returns(false);
164 |
165 | let phase = new Finish();
166 |
167 | doesNotThrow(() => {
168 | phase.validateProductionBranch(this.release);
169 | }, /You have untracked changes$/);
170 | });
171 |
172 | it("throws if hasUnpushedCommits", function () {
173 | stub(this.release.git, "hasUntrackedChanges").returns(false);
174 | stub(this.release.git, "hasUnpushedCommits").returns(true);
175 |
176 | let phase = new Finish();
177 |
178 | throws(() => {
179 | phase.validateProductionBranch(this.release);
180 | }, /You have unpushed changes$/);
181 | });
182 |
183 | it("does not throw if has not hasUnpushedCommits", function () {
184 | stub(this.release.git, "hasUntrackedChanges").returns(false);
185 | stub(this.release.git, "hasUnpushedCommits").returns(false);
186 |
187 | let phase = new Finish();
188 |
189 | doesNotThrow(() => {
190 | phase.validateProductionBranch(this.release);
191 | }, /You have unpushed changes$/);
192 | });
193 | });
194 |
195 | describe("mergeToProduction", function () {
196 | it("merges from derived branchName", function () {
197 | stub(this.release.git, "merge");
198 | stub(this.release.git, "pushRef");
199 | this.release.branchName = "xyz";
200 |
201 | let phase = new Finish();
202 | phase.mergeToProduction(this.release);
203 |
204 | assert(this.release.git.merge.calledWith("xyz"));
205 | });
206 |
207 | it("pushes productionBranch", function () {
208 | stub(this.release.git, "merge");
209 | stub(this.release.git, "pushRef");
210 | this.release.options.productionBranch = "prod123";
211 |
212 | let phase = new Finish();
213 | phase.mergeToProduction(this.release);
214 |
215 | assert(this.release.git.pushRef.calledWith("prod123"));
216 | });
217 | });
218 |
219 | describe("tagProduction", function () {
220 | it("tags productionBranch with tagName", function () {
221 | stub(this.release.git, "tag");
222 | stub(this.release.git, "pushRef");
223 | this.release.tagName = "tag123";
224 |
225 | let phase = new Finish();
226 | phase.tagProduction(this.release);
227 |
228 | assert(this.release.git.tag.calledWith("tag123"));
229 | });
230 |
231 | it("pushes tagName", function () {
232 | stub(this.release.git, "tag");
233 | stub(this.release.git, "pushRef");
234 | this.release.tagName = "tag123";
235 |
236 | let phase = new Finish();
237 | phase.tagProduction(this.release);
238 |
239 | assert(this.release.git.pushRef.calledWith("tag123"));
240 | });
241 | });
242 |
243 | describe("mergeBackToDevelopment", function () {
244 | it("checks out developmentBranch if dev !== prod", function () {
245 | stub(this.release.git, "checkout");
246 | stub(this.release.git, "merge");
247 | stub(this.release.git, "pushRef");
248 | this.release.options.developmentBranch = "devxyz";
249 | this.release.options.productionBranch = "prodxyz";
250 |
251 | let phase = new Finish();
252 | phase.mergeBackToDevelopment(this.release);
253 |
254 | assert(this.release.git.checkout.calledWith("devxyz"));
255 | });
256 |
257 | it("merges from derived branchName if dev !== prod", function () {
258 | stub(this.release.git, "checkout");
259 | stub(this.release.git, "merge");
260 | stub(this.release.git, "pushRef");
261 | this.release.options.developmentBranch = "devxyz";
262 | this.release.options.productionBranch = "prodxyz";
263 | this.release.branchName = "xyz";
264 |
265 | let phase = new Finish();
266 | phase.mergeBackToDevelopment(this.release);
267 |
268 | assert(this.release.git.merge.calledWith("xyz"));
269 | });
270 |
271 | it("pushes developmentBranch if dev !== prod", function () {
272 | stub(this.release.git, "checkout");
273 | stub(this.release.git, "merge");
274 | stub(this.release.git, "pushRef");
275 | this.release.options.developmentBranch = "devxyz";
276 | this.release.options.productionBranch = "prodxyz";
277 |
278 | let phase = new Finish();
279 | phase.mergeBackToDevelopment(this.release);
280 |
281 | assert(this.release.git.pushRef.calledWith("devxyz"));
282 | });
283 |
284 | it("does not run checkout, merge and pushRef if dev == prod", function () {
285 | stub(this.release.git, "checkout");
286 | stub(this.release.git, "merge");
287 | stub(this.release.git, "pushRef");
288 |
289 | this.release.options.developmentBranch = "abc";
290 | this.release.options.productionBranch = "abc";
291 |
292 | let phase = new Finish();
293 | phase.mergeBackToDevelopment(this.release);
294 |
295 | assert(!this.release.git.checkout.called);
296 | assert(!this.release.git.merge.called);
297 | assert(!this.release.git.pushRef.called);
298 | });
299 | });
300 | });
301 |
--------------------------------------------------------------------------------
/test/phases/Publish.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { throws, doesNotThrow } from "assert";
2 | import { stub } from "sinon";
3 | import Publish from "../../src/phases/Publish";
4 | import Release from "../../src/Release";
5 |
6 | describe("Publish", function () {
7 | beforeEach(function () {
8 | this.release = new Release();
9 | stub(this.release.logger, "debug");
10 | });
11 |
12 | describe("validate", function () {
13 | it("throws if currentBranch is not release branch", function () {
14 | this.release.options.releaseBranchPrefix = "xyz~";
15 |
16 | stub(this.release.git, "getCurrentBranch").returns("foo");
17 |
18 | let phase = new Publish();
19 |
20 | throws(() => {
21 | phase.validate(this.release);
22 | }, /You can only publish a release from a release branch$/);
23 | });
24 |
25 | it("does not throw if currentBranch is a release branch", function () {
26 | this.release.options.releaseBranchPrefix = "xyz~";
27 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
28 | stub(this.release.git, "hasUntrackedChanges").returns(false);
29 |
30 | let phase = new Publish();
31 |
32 | doesNotThrow(() => {
33 | phase.validate(this.release);
34 | }, /You can only publish a release from a release branch$/);
35 | });
36 |
37 | it("throws if hasUntrackedChanges", function () {
38 | this.release.options.releaseBranchPrefix = "xyz~";
39 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
40 | stub(this.release.git, "hasUntrackedChanges").returns(true);
41 |
42 | let phase = new Publish();
43 |
44 | throws(() => {
45 | phase.validate(this.release);
46 | }, /You have untracked changes$/);
47 | });
48 |
49 | it("does not throw if has not hasUntrackedChanges", function () {
50 | this.release.options.releaseBranchPrefix = "xyz~";
51 | stub(this.release.git, "getCurrentBranch").returns("xyz~1.0.0");
52 | stub(this.release.git, "hasUntrackedChanges").returns(false);
53 |
54 | let phase = new Publish();
55 |
56 | doesNotThrow(() => {
57 | phase.validate(this.release);
58 | }, /You have untracked changes$/);
59 | });
60 | });
61 |
62 | describe("push", function () {
63 | it("pushes the currentBranch", function () {
64 | stub(this.release.git, "getCurrentBranch").returns("foo");
65 | stub(this.release.git, "pushRef");
66 | let phase = new Publish();
67 |
68 | phase.push(this.release);
69 |
70 | assert(this.release.git.pushRef.calledWith("foo"));
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/test/phases/Start.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { throws, doesNotThrow, fail, equal, deepEqual } from "assert";
2 | import Start from "../../src/phases/Start";
3 | import { stub } from "sinon";
4 | import Release from "../../src/Release";
5 |
6 | describe("Start", function () {
7 | beforeEach(function () {
8 | this.release = new Release();
9 | stub(this.release.logger, "debug");
10 | });
11 |
12 | describe("fetch", function () {
13 | it("fetches heads and tags", function () {
14 | stub(this.release.git, "fetchHeadsAndTags");
15 |
16 | let phase = new Start();
17 | phase.fetch(this.release);
18 |
19 | assert(this.release.git.fetchHeadsAndTags.called);
20 | });
21 |
22 | it("breaks in case of error", function () {
23 | stub(this.release.git, "fetchHeadsAndTags").throws();
24 |
25 | let phase = new Start();
26 | try {
27 | phase.fetch(this.release);
28 | fail("Should not get here");
29 | } catch (e) {
30 | equal(e.message, "Unable to fetch tags and heads");
31 | }
32 | });
33 | });
34 |
35 | describe("getPreviousVersion", function () {
36 | it("set previousVersion if tagname", function () {
37 | stub(this.release.git, "getLastLocalTagName").returns("v1.2.3");
38 |
39 | let phase = new Start();
40 | phase.getPreviousVersion(this.release);
41 | equal(this.release.previousVersion, "1.2.3");
42 | });
43 |
44 | it("set previousReleaseName if tagname", function () {
45 | stub(this.release.git, "getLastLocalTagName").returns("v1.2.3");
46 | this.release.options.tagPrefix = "xyz";
47 | let phase = new Start();
48 | phase.getPreviousVersion(this.release);
49 | equal(this.release.previousReleaseName, "xyz1.2.3");
50 | });
51 |
52 | it("does not set previousVersion if no tagname", function () {
53 | stub(this.release.git, "getLastLocalTagName").returns(null);
54 |
55 | let phase = new Start();
56 | phase.getPreviousVersion(this.release);
57 | equal(this.release.previousVersion, null);
58 | });
59 |
60 | it("does not set previousReleaseName if no tagname", function () {
61 | stub(this.release.git, "getLastLocalTagName").returns(null);
62 | let phase = new Start();
63 | phase.getPreviousVersion(this.release);
64 | equal(this.release.previousReleaseName, null);
65 | });
66 | });
67 |
68 | describe("getCommits", function () {
69 | it("fetches conventionalCommits with lastLocalTagSha", function () {
70 | stub(this.release.git, "getLastLocalTagSha").returns("xyz");
71 | stub(this.release.git, "conventionalCommits").returns([{}]);
72 |
73 | let phase = new Start();
74 | phase.getCommits(this.release);
75 |
76 | assert(this.release.git.conventionalCommits.calledWith("xyz"));
77 | });
78 |
79 | it("sets release commits", function () {
80 | stub(this.release.git, "getLastLocalTagSha");
81 | stub(this.release.git, "conventionalCommits").returns(["123"]);
82 |
83 | let phase = new Start();
84 | phase.getCommits(this.release);
85 |
86 | deepEqual(this.release.commits, ["123"]);
87 | });
88 |
89 | it("breaks release if no commits found", function () {
90 | stub(this.release.git, "getLastLocalTagSha");
91 | stub(this.release.git, "conventionalCommits").returns([]);
92 |
93 | let phase = new Start();
94 | try {
95 | phase.getCommits(this.release);
96 | fail("Should not get here");
97 | } catch (e) {
98 | equal(e.message, "Nothing to release");
99 | }
100 | });
101 | });
102 |
103 | describe("getNextVersion", function () {
104 | it("sets initialVersion if no previousVersion", function () {
105 | this.release.options.initialVersion = "2.3.4";
106 |
107 | let phase = new Start();
108 | phase.getNextVersion(this.release);
109 |
110 | equal(this.release.nextVersion, "2.3.4");
111 | });
112 |
113 | it("if previousVersion calls getBump with commits", function () {
114 | this.release.previousVersion = "2.3.4";
115 | stub(this.release.options, "getBump").returns("patch");
116 |
117 | this.release.commits = ["abc", "bcd"];
118 |
119 | let phase = new Start();
120 | phase.getNextVersion(this.release);
121 |
122 | assert(this.release.options.getBump.calledWith(["abc", "bcd"]));
123 | });
124 |
125 | it("bumps version according to getBump", function () {
126 | this.release.previousVersion = "2.3.4";
127 | stub(this.release.options, "getBump").returns("patch");
128 |
129 | let phase = new Start();
130 | phase.getNextVersion(this.release);
131 |
132 | equal(this.release.nextVersion, "2.3.5");
133 | });
134 |
135 | it("sets release.name", function () {
136 | this.release.options.tagPrefix = "xyz";
137 | this.release.previousVersion = "2.3.4";
138 | stub(this.release.options, "getBump").returns("patch");
139 |
140 | let phase = new Start();
141 | phase.getNextVersion(this.release);
142 |
143 | equal(this.release.name, "xyz2.3.5");
144 | });
145 | });
146 |
147 | describe("validate", function () {
148 | it("throws if currentBranch is not developmentBranch", function () {
149 | this.release.options.developmentBranch = "abc";
150 |
151 | stub(this.release.git, "isCurrentBranch").returns(false);
152 | stub(this.release.git, "hasUntrackedChanges").returns(false);
153 | stub(this.release.git, "hasUnpushedCommits").returns(false);
154 | stub(this.release.git, "hasLocalTag").returns(false);
155 |
156 | let phase = new Start();
157 |
158 | throws(() => {
159 | phase.validate(this.release);
160 | }, /Current branch should be abc$/);
161 | });
162 |
163 | it("does not throw if currentBranch is developmentBranch", function () {
164 | this.release.options.developmentBranch = "abc";
165 |
166 | stub(this.release.git, "isCurrentBranch").returns(true);
167 | stub(this.release.git, "hasUntrackedChanges").returns(false);
168 | stub(this.release.git, "hasUnpushedCommits").returns(false);
169 | stub(this.release.git, "hasLocalTag").returns(false);
170 |
171 | let phase = new Start();
172 |
173 | doesNotThrow(() => {
174 | phase.validate(this.release);
175 | }, /Current branch should be abc$/);
176 | });
177 |
178 | it("throws if hasUntrackedChanges", function () {
179 | stub(this.release.git, "isCurrentBranch").returns(true);
180 | stub(this.release.git, "hasUntrackedChanges").returns(true);
181 | stub(this.release.git, "hasUnpushedCommits").returns(false);
182 | stub(this.release.git, "hasLocalTag").returns(false);
183 |
184 | let phase = new Start();
185 |
186 | throws(() => {
187 | phase.validate(this.release);
188 | }, /You have untracked changes$/);
189 | });
190 |
191 | it("does not throw if has not hasUntrackedChanges", function () {
192 | stub(this.release.git, "isCurrentBranch").returns(true);
193 | stub(this.release.git, "hasUntrackedChanges").returns(false);
194 | stub(this.release.git, "hasUnpushedCommits").returns(false);
195 | stub(this.release.git, "hasLocalTag").returns(false);
196 |
197 | let phase = new Start();
198 |
199 | doesNotThrow(() => {
200 | phase.validate(this.release);
201 | }, /You have untracked changes$/);
202 | });
203 |
204 | it("throws if hasUnpushedCommits", function () {
205 | stub(this.release.git, "isCurrentBranch").returns(true);
206 | stub(this.release.git, "hasUntrackedChanges").returns(false);
207 | stub(this.release.git, "hasUnpushedCommits").returns(true);
208 | stub(this.release.git, "hasLocalTag").returns(false);
209 |
210 | let phase = new Start();
211 |
212 | throws(() => {
213 | phase.validate(this.release);
214 | }, /You have unpushed changes$/);
215 | });
216 |
217 | it("does not throw if has not hasUnpushedCommits", function () {
218 | stub(this.release.git, "isCurrentBranch").returns(true);
219 | stub(this.release.git, "hasUntrackedChanges").returns(false);
220 | stub(this.release.git, "hasUnpushedCommits").returns(false);
221 | stub(this.release.git, "hasLocalTag").returns(false);
222 |
223 | let phase = new Start();
224 |
225 | doesNotThrow(() => {
226 | phase.validate(this.release);
227 | }, /You have unpushed changes$/);
228 | });
229 |
230 | it("throws if hasLocalTag with release name", function () {
231 | stub(this.release.git, "isCurrentBranch").returns(true);
232 | stub(this.release.git, "hasUntrackedChanges").returns(false);
233 | stub(this.release.git, "hasUnpushedCommits").returns(false);
234 | stub(this.release.git, "hasLocalTag").returns(true);
235 |
236 | this.release.name = "abc";
237 |
238 | let phase = new Start();
239 |
240 | throws(() => {
241 | phase.validate(this.release);
242 | }, /Tag abc already exists$/);
243 | });
244 |
245 | it("does not throw if has not LocalTag with release name", function () {
246 | stub(this.release.git, "isCurrentBranch").returns(true);
247 | stub(this.release.git, "hasUntrackedChanges").returns(false);
248 | stub(this.release.git, "hasUnpushedCommits").returns(false);
249 | stub(this.release.git, "hasLocalTag").returns(false);
250 |
251 | this.release.name = "abc";
252 |
253 | let phase = new Start();
254 |
255 | doesNotThrow(() => {
256 | phase.validate(this.release);
257 | }, /Tag abc already exists$/);
258 | });
259 | });
260 |
261 | describe("openReleaseBranch", function () {
262 | it("calls git.openBranch with the releaseBranchPrefix and nextVersion", function () {
263 | this.release.options.releaseBranchPrefix = "xyz~";
264 | this.release.nextVersion = "1.2.3";
265 | stub(this.release.git, "openBranch");
266 |
267 | let phase = new Start();
268 | phase.openReleaseBranch(this.release);
269 | assert(this.release.git.openBranch.calledWith("xyz~1.2.3"));
270 | });
271 | });
272 |
273 | describe("commit", function () {
274 | it("invokes git commitAll with release message", function () {
275 | this.release.name = "xyz123";
276 |
277 | stub(this.release.git, "commitAll");
278 |
279 | let phase = new Start();
280 | phase.commit(this.release);
281 |
282 | assert(this.release.git.commitAll.calledWith("Release xyz123"));
283 | });
284 | });
285 | });
286 |
--------------------------------------------------------------------------------
/test/plugins/ChangelogEntry.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { equal, deepEqual } from "assert";
2 | import ChangelogEntry from "../../src/plugins/generate-changelog/ChangelogEntry";
3 |
4 | describe("ChangelogEntry", function () {
5 | describe("new ChangelogEntry()", function () {
6 | it("sets subject", function () {
7 | let ce = new ChangelogEntry("Changed something");
8 | equal(ce.subject, "Changed something");
9 | });
10 |
11 | it("init links to empty array", function () {
12 | let ce = new ChangelogEntry("...");
13 | deepEqual(ce.links, []);
14 | });
15 |
16 | it("init children to empty array", function () {
17 | let ce = new ChangelogEntry("...");
18 | deepEqual(ce.children, []);
19 | });
20 |
21 | it("init allows to set scope", function () {
22 | let ce = new ChangelogEntry("...", {
23 | scope: "abc",
24 | });
25 | equal(ce.scope, "abc");
26 | });
27 |
28 | it("init allows to set subjectLink", function () {
29 | let ce = new ChangelogEntry("...", {
30 | subjectLink: "abc",
31 | });
32 | equal(ce.subjectLink, "abc");
33 | });
34 |
35 | it("init allows to set links", function () {
36 | let ce = new ChangelogEntry("...", {
37 | links: ["abc", "bcd"],
38 | });
39 |
40 | deepEqual(ce.links, ["abc", "bcd"]);
41 | });
42 | });
43 |
44 | describe("addLink", function () {
45 | it("adds a link", function () {
46 | let ce = new ChangelogEntry("...");
47 |
48 | ce.addLink("abc", "bcd");
49 |
50 | deepEqual(ce.links, [
51 | {
52 | name: "abc",
53 | url: "bcd",
54 | },
55 | ]);
56 | });
57 | });
58 |
59 | describe("addChild", function () {
60 | it("adds a child", function () {
61 | let ce = new ChangelogEntry("...");
62 |
63 | ce.addChild("abc");
64 |
65 | deepEqual(ce.children, ["abc"]);
66 | });
67 | });
68 |
69 | describe("isLeaf", function () {
70 | it("returns true if ChangelogEntry has no children", function () {
71 | let ce = new ChangelogEntry("...");
72 | assert(ce.isLeaf());
73 | });
74 |
75 | it("returns false if ChangelogEntry has children", function () {
76 | let ce = new ChangelogEntry("...");
77 | ce.addChild("abc");
78 | assert(!ce.isLeaf());
79 | });
80 | });
81 |
82 | describe("traverse", function () {
83 | it("calls previsit on all children in right order", function () {
84 | let root = new ChangelogEntry("A");
85 |
86 | let b = new ChangelogEntry("B");
87 | let c = new ChangelogEntry("C");
88 |
89 | root.addChild(b);
90 | root.addChild(c);
91 |
92 | b.addChild(new ChangelogEntry("D"));
93 |
94 | let visits = [];
95 | root.traverse((entry) => visits.push(entry.subject));
96 |
97 | deepEqual(visits, ["A", "B", "D", "C"]);
98 | });
99 |
100 | it("calls postvisit on all children in right order", function () {
101 | let root = new ChangelogEntry("A");
102 |
103 | let b = new ChangelogEntry("B");
104 | let c = new ChangelogEntry("C");
105 |
106 | root.addChild(b);
107 | root.addChild(c);
108 |
109 | b.addChild(new ChangelogEntry("D"));
110 |
111 | let visits = [];
112 | root.traverse(null, (entry) => visits.push(entry.subject));
113 |
114 | deepEqual(visits, ["D", "B", "C", "A"]);
115 | });
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/test/plugins/bump-package-json.spec.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import assert, { equal } from "assert";
3 | import Release from "../../src/Release";
4 | import plugin from "../../src/plugins/bump-package-json";
5 |
6 | describe("plugins", function () {
7 | describe("bumpPackageJson", function () {
8 | beforeEach(function () {
9 | fs.renameSync("package.json", "package.json.bkp");
10 | fs.writeFileSync("package.json", "{}");
11 | });
12 | afterEach(function () {
13 | fs.unlinkSync("package.json");
14 | fs.renameSync("package.json.bkp", "package.json");
15 | });
16 | it("installs a step", function () {
17 | let release = new Release({
18 | plugins: [plugin],
19 | });
20 | let step = release.phases.start.steps.find(
21 | (step) => step.name === "bumpPackageJson"
22 | );
23 | assert(step);
24 | });
25 | it("writes nextVersion on package json", function () {
26 | let release = new Release({
27 | plugins: [plugin],
28 | });
29 | release.nextVersion = "1.2.3";
30 | let step = release.phases.start.steps.find(
31 | (step) => step.name === "bumpPackageJson"
32 | );
33 | step.run(release);
34 | const packageJson = fs.readFileSync("package.json", "utf-8");
35 | equal(
36 | packageJson,
37 | `{
38 | "version": "1.2.3"
39 | }
40 | `
41 | );
42 | });
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/test/plugins/changelog-template.spec.js:
--------------------------------------------------------------------------------
1 | import ChangelogEntry from "../../src/plugins/generate-changelog/ChangelogEntry";
2 | import changelogTemplate from "../../src/plugins/generate-changelog/changelog-template";
3 |
4 | import { equal } from "assert";
5 |
6 | describe("changelog-template", function () {
7 | it("Adds an anchor for release name", function () {
8 | let context = {
9 | release: {
10 | name: "release1",
11 | changes: {
12 | traverse() {
13 | //
14 | },
15 | },
16 | },
17 | };
18 |
19 | let changelog = changelogTemplate(context);
20 |
21 | equal(
22 | changelog,
23 | `
24 |
25 | `
26 | );
27 | });
28 |
29 | it("Adds h1 entry for each top level leaf", function () {
30 | let root = new ChangelogEntry("root");
31 |
32 | let context = {
33 | release: {
34 | name: "release1",
35 | changes: root,
36 | },
37 | };
38 |
39 | let changelog = changelogTemplate(context);
40 |
41 | equal(
42 | changelog,
43 | `
44 | # root
45 |
46 | `
47 | );
48 | });
49 |
50 | it("Render subject for non leaf entries as title", function () {
51 | let root = new ChangelogEntry("root");
52 | root.addChild(new ChangelogEntry("child1"));
53 |
54 | let context = {
55 | release: {
56 | name: "release1",
57 | changes: root,
58 | },
59 | };
60 |
61 | let changelog = changelogTemplate(context);
62 |
63 | equal(
64 | changelog,
65 | `
66 | # root
67 |
68 | - child1
69 |
70 | `
71 | );
72 | });
73 |
74 | it("Adds a space before >2nd level titles", function () {
75 | let root = new ChangelogEntry("root");
76 | let child1 = new ChangelogEntry("child1");
77 | let child2 = new ChangelogEntry("child2");
78 | root.addChild(child1);
79 | child1.addChild(child2);
80 |
81 | let context = {
82 | release: {
83 | name: "release1",
84 | changes: root,
85 | },
86 | };
87 |
88 | let changelog = changelogTemplate(context);
89 |
90 | equal(
91 | changelog,
92 | `
93 | # root
94 |
95 | ## child1
96 |
97 | - child2
98 |
99 | `
100 | );
101 | });
102 |
103 | it("Does not add a space after leaf entries", function () {
104 | let root = new ChangelogEntry("root");
105 | let child1 = new ChangelogEntry("child1");
106 | let child2 = new ChangelogEntry("child2");
107 | let child3 = new ChangelogEntry("child3");
108 | root.addChild(child1);
109 | child1.addChild(child2);
110 | child1.addChild(child3);
111 |
112 | let context = {
113 | release: {
114 | name: "release1",
115 | changes: root,
116 | },
117 | };
118 |
119 | let changelog = changelogTemplate(context);
120 |
121 | equal(
122 | changelog,
123 | `
124 | # root
125 |
126 | ## child1
127 |
128 | - child2
129 | - child3
130 |
131 | `
132 | );
133 | });
134 |
135 | it("renders one link", function () {
136 | let root = new ChangelogEntry("root");
137 | root.addLink("link 1", "link-1-url");
138 |
139 | let context = {
140 | release: {
141 | name: "release1",
142 | changes: root,
143 | },
144 | };
145 |
146 | let changelog = changelogTemplate(context);
147 |
148 | equal(
149 | changelog,
150 | `
151 | # root ([link 1](link-1-url))
152 |
153 | `
154 | );
155 | });
156 |
157 | it("renders two or more link comma separated", function () {
158 | let root = new ChangelogEntry("root");
159 | root.addLink("link 1", "link-1-url");
160 | root.addLink("link 2", "link-2-url");
161 |
162 | let context = {
163 | release: {
164 | name: "release1",
165 | changes: root,
166 | },
167 | };
168 |
169 | let changelog = changelogTemplate(context);
170 |
171 | equal(
172 | changelog,
173 | `
174 | # root ([link 1](link-1-url), [link 2](link-2-url))
175 |
176 | `
177 | );
178 | });
179 |
180 | it("renders scope in bold", function () {
181 | let root = new ChangelogEntry("root");
182 | root.scope = "feat";
183 |
184 | let context = {
185 | release: {
186 | name: "release1",
187 | changes: root,
188 | },
189 | };
190 |
191 | let changelog = changelogTemplate(context);
192 |
193 | equal(
194 | changelog,
195 | `
196 | # **feat** - root
197 |
198 | `
199 | );
200 | });
201 |
202 | it("renders subject link", function () {
203 | let root = new ChangelogEntry("root");
204 | root.subjectLink = "xyz";
205 |
206 | let context = {
207 | release: {
208 | name: "release1",
209 | changes: root,
210 | },
211 | };
212 |
213 | let changelog = changelogTemplate(context);
214 |
215 | equal(
216 | changelog,
217 | `
218 | # [root](xyz)
219 |
220 | `
221 | );
222 | });
223 | });
224 |
--------------------------------------------------------------------------------
/test/plugins/generate-changelog.spec.js:
--------------------------------------------------------------------------------
1 | import assert, { deepEqual, equal } from "assert";
2 | import { stub } from "sinon";
3 | import fs from "fs";
4 |
5 | import Release from "../../src/Release";
6 | import ChangelogEntry from "../../src/plugins/generate-changelog/ChangelogEntry";
7 | import plugin from "../../src/plugins/generate-changelog";
8 |
9 | describe("plugins", function () {
10 | describe("generateChangelog", function () {
11 | afterEach(function () {
12 | if (fs.existsSync("XYZ.md")) {
13 | fs.unlinkSync("XYZ.md");
14 | }
15 | });
16 | describe("installPlugin", function () {
17 | it("installs steps in right order", function () {
18 | let release = new Release({
19 | changelogPath: "XYZ.md",
20 | plugins: [plugin],
21 | });
22 |
23 | let steps = release.phases.start.steps;
24 | let names = steps.slice(-4, -1).map((step) => step.name);
25 |
26 | deepEqual(names, [
27 | "getChangelogEntries",
28 | "generateChangelogContent",
29 | "writeChangelog",
30 | ]);
31 | });
32 | });
33 |
34 | describe("getChangelogEntries", function () {
35 | it("assign root to release.changes", function () {
36 | let release = new Release({
37 | changelogPath: "XYZ.md",
38 | plugins: [plugin],
39 | });
40 |
41 | release.commits = [];
42 |
43 | let step = release.phases.start.steps.find(
44 | (step) => step.name === "getChangelogEntries"
45 | );
46 |
47 | step.run(release);
48 | assert(release.changes instanceof ChangelogEntry);
49 | });
50 |
51 | it("set root subject after release name", function () {
52 | let release = new Release({
53 | changelogPath: "XYZ.md",
54 | plugins: [plugin],
55 | });
56 |
57 | release.name = "release123";
58 | release.commits = [];
59 |
60 | let step = release.phases.start.steps.find(
61 | (step) => step.name === "getChangelogEntries"
62 | );
63 |
64 | step.run(release);
65 | equal(release.changes.subject, "release123");
66 | });
67 |
68 | it("for first release sets root subjectLink after release name", function () {
69 | let release = new Release({
70 | changelogPath: "XYZ.md",
71 | plugins: [plugin],
72 | });
73 |
74 | release.name = "release123";
75 |
76 | stub(release.git, "commitLink").callsFake(
77 | (commit) => `commitlink://${commit}`
78 | );
79 |
80 | release.commits = [];
81 |
82 | let step = release.phases.start.steps.find(
83 | (step) => step.name === "getChangelogEntries"
84 | );
85 |
86 | step.run(release);
87 | equal(release.changes.subjectLink, "commitlink://release123");
88 | });
89 |
90 | it("sets root subjectLink as compareLink between prev and release", function () {
91 | let release = new Release({
92 | changelogPath: "XYZ.md",
93 | plugins: [plugin],
94 | });
95 |
96 | release.previousReleaseName = "release1";
97 | release.name = "release2";
98 |
99 | stub(release.git, "compareLink").callsFake(
100 | (commit1, commit2) => `comparelink://${commit1}..${commit2}`
101 | );
102 |
103 | release.commits = [];
104 |
105 | let step = release.phases.start.steps.find(
106 | (step) => step.name === "getChangelogEntries"
107 | );
108 |
109 | step.run(release);
110 | equal(release.changes.subjectLink, "comparelink://release1..release2");
111 | });
112 |
113 | it("does not add any children since no commit fits", function () {
114 | let release = new Release({
115 | changelogPath: "XYZ.md",
116 | plugins: [plugin],
117 | });
118 |
119 | release.commits = [];
120 |
121 | let step = release.phases.start.steps.find(
122 | (step) => step.name === "getChangelogEntries"
123 | );
124 |
125 | step.run(release);
126 | deepEqual(release.changes.children, []);
127 | });
128 |
129 | it("adds a list of feat entries if one is present", function () {
130 | let release = new Release({
131 | changelogPath: "XYZ.md",
132 | plugins: [plugin],
133 | });
134 |
135 | release.name = "release2";
136 |
137 | release.commits = [
138 | {
139 | header: "feat: commit1",
140 | subject: "commit1",
141 | type: "feat",
142 | },
143 | ];
144 |
145 | let step = release.phases.start.steps.find(
146 | (step) => step.name === "getChangelogEntries"
147 | );
148 |
149 | step.run(release);
150 | equal(release.changes.children[0].subject, "Features");
151 | });
152 |
153 | it("adds a list of fix entries if one is present", function () {
154 | let release = new Release({
155 | changelogPath: "XYZ.md",
156 | plugins: [plugin],
157 | });
158 |
159 | release.name = "release2";
160 |
161 | release.commits = [
162 | {
163 | header: "fix: commit1",
164 | subject: "commit1",
165 | type: "fix",
166 | },
167 | ];
168 |
169 | let step = release.phases.start.steps.find(
170 | (step) => step.name === "getChangelogEntries"
171 | );
172 |
173 | step.run(release);
174 | equal(release.changes.children[0].subject, "Fixes");
175 | });
176 |
177 | it("adds a list of breaking changes entries if one is present", function () {
178 | let release = new Release({
179 | changelogPath: "XYZ.md",
180 | plugins: [plugin],
181 | });
182 |
183 | release.name = "release2";
184 |
185 | release.commits = [
186 | {
187 | header: "breaking: commit1",
188 | subject: "commit1",
189 | breaking: true,
190 | },
191 | ];
192 |
193 | let step = release.phases.start.steps.find(
194 | (step) => step.name === "getChangelogEntries"
195 | );
196 |
197 | step.run(release);
198 | equal(release.changes.children[0].subject, "Breaking Changes");
199 | });
200 |
201 | it("adds scope to changelog if commit has one", function () {
202 | let release = new Release({
203 | changelogPath: "XYZ.md",
204 | plugins: [plugin],
205 | });
206 |
207 | release.name = "release2";
208 |
209 | release.commits = [
210 | {
211 | header: "feat: commit1",
212 | subject: "commit1",
213 | type: "feat",
214 | scope: "xyz",
215 | shortHash: "12345",
216 | hash: "12345678910",
217 | },
218 | ];
219 |
220 | let step = release.phases.start.steps.find(
221 | (step) => step.name === "getChangelogEntries"
222 | );
223 |
224 | step.run(release);
225 | equal(release.changes.children[0].children[0].scope, "xyz");
226 | });
227 |
228 | it("adds link to commit for any entry", function () {
229 | let release = new Release({
230 | changelogPath: "XYZ.md",
231 | plugins: [plugin],
232 | });
233 |
234 | release.name = "release2";
235 |
236 | release.commits = [
237 | {
238 | header: "feat: commit1",
239 | subject: "commit1",
240 | type: "feat",
241 | scope: "xyz",
242 | shortHash: "12345",
243 | hash: "12345678910",
244 | },
245 | ];
246 |
247 | let step = release.phases.start.steps.find(
248 | (step) => step.name === "getChangelogEntries"
249 | );
250 |
251 | stub(release.git, "commitLink").callsFake(
252 | (commit) => `commitlink://${commit}`
253 | );
254 |
255 | step.run(release);
256 | let commitEntry = release.changes.children[0].children[0];
257 | deepEqual(commitEntry.links[0], {
258 | name: "12345",
259 | url: "commitlink://12345678910",
260 | });
261 | });
262 |
263 | it("squashes commits with identical header", function () {
264 | let release = new Release({
265 | changelogPath: "XYZ.md",
266 | plugins: [plugin],
267 | });
268 |
269 | release.name = "release2";
270 |
271 | release.commits = [
272 | {
273 | header: "feat: commit1",
274 | subject: "commit1",
275 | type: "feat",
276 | scope: "xyz",
277 | shortHash: "12345",
278 | hash: "12345678910",
279 | },
280 | {
281 | header: "feat: commit1",
282 | subject: "commit1",
283 | type: "feat",
284 | scope: "xyz",
285 | shortHash: "67890",
286 | hash: "67890123456",
287 | },
288 | ];
289 |
290 | let step = release.phases.start.steps.find(
291 | (step) => step.name === "getChangelogEntries"
292 | );
293 |
294 | stub(release.git, "commitLink").callsFake(
295 | (commit) => `commitlink://${commit}`
296 | );
297 |
298 | step.run(release);
299 |
300 | equal(release.changes.children[0].children.length, 1);
301 |
302 | let commitEntry = release.changes.children[0].children[0];
303 |
304 | deepEqual(commitEntry.links, [
305 | {
306 | name: "12345",
307 | url: "commitlink://12345678910",
308 | },
309 | {
310 | name: "67890",
311 | url: "commitlink://67890123456",
312 | },
313 | ]);
314 | });
315 | });
316 |
317 | describe("generateChangelogContent", function () {
318 | it("processes changes through changelog template", function () {
319 | let changelogTemplateStub = stub().returns("abc");
320 |
321 | let release = new Release({
322 | changelogPath: "XYZ.md",
323 | changelogTemplate: changelogTemplateStub,
324 | plugins: [plugin],
325 | });
326 |
327 | let step = release.phases.start.steps.find(
328 | (step) => step.name === "generateChangelogContent"
329 | );
330 |
331 | step.run(release);
332 |
333 | assert(changelogTemplateStub.calledWith({ release: release }));
334 | equal(release.changelog, "abc");
335 | });
336 | });
337 |
338 | describe("writeChangelog", function () {
339 | it("prepends changelog contents to changelog", function () {
340 | let release = new Release({
341 | changelogPath: "XYZ.md",
342 | plugins: [plugin],
343 | });
344 |
345 | release.changelog = "abc";
346 |
347 | let step = release.phases.start.steps.find(
348 | (step) => step.name === "writeChangelog"
349 | );
350 |
351 | step.run(release);
352 |
353 | deepEqual(fs.readFileSync("XYZ.md", "utf8"), "abc");
354 | });
355 | });
356 | });
357 | });
358 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "lib",
4 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
5 | "module": "commonjs" /* Specify what module code is generated. */,
6 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
7 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
8 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
9 | "strict": true /* Enable all strict type-checking options. */,
10 | "skipLibCheck": true /* Skip type checking all .d.ts files. */,
11 | "experimentalDecorators": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["./src/**/*.spec.*"]
15 | }
16 |
--------------------------------------------------------------------------------