├── spec ├── testData │ ├── precommit.js │ ├── CHANGELOG.md │ ├── package_noname.json │ ├── package_precommit.json │ └── package.json ├── parseRawCommit.spec.js ├── log.spec.js ├── validateBranch.spec.js └── system.spec.js ├── config ├── testUnit │ ├── mocha.opts │ └── reporters.json ├── verify │ └── .eslintrc.js └── release │ └── commitMessageConfig.js ├── .gitignore ├── .eslintignore ├── src ├── lib │ ├── helpers.js │ ├── runScript.js │ ├── whatBump.js │ ├── writeChangelog.js │ ├── log.js │ ├── validateBranch.js │ ├── bumpUpVersion.js │ ├── getJsonCommits.js │ ├── isReleaseNecessary.js │ ├── index.js │ ├── addFilesAndCreateTag.js │ ├── getLatestTag.js │ └── parseRawCommit.js └── index.js ├── .editorconfig ├── .travis.yml ├── .nycrc ├── LICENSE ├── confit.yml ├── package.json ├── README.md └── CONTRIBUTING.md /spec/testData/precommit.js: -------------------------------------------------------------------------------- 1 | console.log('Inside precommit.js, version is', process.argv[2]); 2 | -------------------------------------------------------------------------------- /spec/testData/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.1.0 (2016-03-13) 3 | 4 | 5 | ### Features 6 | 7 | * my first feature 0429bc8 8 | -------------------------------------------------------------------------------- /config/testUnit/mocha.opts: -------------------------------------------------------------------------------- 1 | spec/**/*.spec.js 2 | --reporter mocha-multi-reporters 3 | --reporter-options configFile=./config/testUnit/reporters.json 4 | --no-timeouts 5 | --ui bdd 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | node_modules/ 3 | dist/ 4 | reports/ 5 | .idea/ 6 | .vscode/ 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | .DS_Store 13 | .history 14 | -------------------------------------------------------------------------------- /config/testUnit/reporters.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "list, mocha-junit-reporter", 3 | "mochaJunitReporterReporterOptions": { 4 | "mochaFile": "reports/junit.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/testData/package_noname.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "author": "leonardoanalista", 4 | "scripts": { 5 | "set-version": "echo 'this is my pre-commit script'" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # START_CONFIT_GENERATED_CONTENT 2 | # Common folders to ignore 3 | node_modules/* 4 | bower_components/* 5 | 6 | # Config folder (optional - you might want to lint this...) 7 | config/* 8 | 9 | # END_CONFIT_GENERATED_CONTENT 10 | -------------------------------------------------------------------------------- /src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const shell = require('shelljs'); 3 | 4 | module.exports = { 5 | 6 | isFirstRelease(latestTag) { 7 | return latestTag === 'HEAD'; 8 | }, 9 | 10 | terminateProcess(code) { 11 | if (code !== 0) { 12 | shell.exit(code); 13 | } 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /spec/testData/package_precommit.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "corp-semantic-release-test", 4 | "author": "leonardoanalista", 5 | "scripts": { 6 | "set-version": "node precommit.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://any.git.host/owner-name/repo-name.git" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor Config - generated by Confit. This file will NOT be re-overwritten by Confit 2 | # Feel free to customise it further. 3 | # http://editorconfig.org 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /src/lib/runScript.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const terminateProcess = require('./helpers').terminateProcess; 3 | const log = require('./log'); 4 | const shell = require('shelljs'); 5 | 6 | module.exports = function runScript(script, scriptType) { 7 | log.info(`>>> about to run your ${scriptType} script. Command is: ${script}`); 8 | const code = shell.exec(script).code; 9 | terminateProcess(code); 10 | }; 11 | -------------------------------------------------------------------------------- /spec/testData/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "name": "corp-semantic-release-test", 4 | "author": "leonardoanalista", 5 | "scripts": { 6 | "set-version": "echo 'this is my pre-commit script'", 7 | "do-publish": "echo 'just published via post-success script'" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://any.git.host/owner-name/repo-name.git" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - ~/.npm 6 | 7 | notifications: 8 | email: false 9 | 10 | node_js: 11 | - '8' 12 | - 'node' 13 | before_install: 14 | - npm i -g npm 15 | before_script: 16 | - npm prune 17 | 18 | script: 19 | - npm run pre-release 20 | 21 | after_success: 22 | - npm run upload-coverage 23 | - npm run travis-deploy-once "npm run semantic-release" 24 | 25 | branches: 26 | except: 27 | - /^v\d+\.\d+\.\d+$/ 28 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": false, 3 | "per-file": false, 4 | "lines": 90, 5 | "statements": 90, 6 | "functions": 85, 7 | "branches": 80, 8 | "include": [ 9 | "src/**/*.js" 10 | ], 11 | "exclude": [ 12 | "spec/**/*.spec.js" 13 | ], 14 | "reporter": [ 15 | "lcovonly", 16 | "html", 17 | "text", 18 | "cobertura", 19 | "json" 20 | ], 21 | "cache": true, 22 | "all": true, 23 | "report-dir": "./reports/coverage/" 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/whatBump.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const log = require('./log'); 3 | 4 | module.exports = function whatBump(parsedCommits) { 5 | let type; 6 | 7 | parsedCommits.every(function(commit) { 8 | if (commit.breaks.length) { 9 | type = 'major'; 10 | return false; 11 | } 12 | 13 | if (commit.type === 'feat') type = 'minor'; 14 | 15 | if (!type && commit.type === 'fix') type = 'patch'; 16 | 17 | return true; 18 | }); 19 | 20 | log.info('>>> Bump type is', type); 21 | return type; 22 | }; 23 | -------------------------------------------------------------------------------- /src/lib/writeChangelog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const log = require('./log'); 3 | const fs = require('fs'); 4 | const prependFile = require('prepend-file'); 5 | const path = require('path'); 6 | 7 | module.exports = function writeChangelog(data, replaceContents, verbose) { 8 | if (verbose === undefined) verbose = false; 9 | 10 | const fileName = path.join(process.cwd(), 'CHANGELOG.md'); 11 | if (verbose) log.info('>>> About to write/append contents to CHANGELOG.md... '); 12 | 13 | if (replaceContents) { 14 | fs.writeFileSync(fileName, data); 15 | } else { 16 | prependFile.sync(fileName, data); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const chalk = require('chalk'); 3 | 4 | /* eslint no-console: 0 */ 5 | const log = console.log; 6 | const info = console.info; 7 | 8 | const _error = chalk.bold.red; 9 | const _success = chalk.bold.green; 10 | const _announce = chalk.bold.bgGreen.white; 11 | const _info = chalk.bold.cyan; 12 | 13 | // TODO: change to spread operator 14 | module.exports = { 15 | info: (msg, obj) => log(_info(msg), obj || ''), 16 | success: (msg, obj) => log(_success(msg), obj || ''), 17 | announce: (msg, obj) => info(_announce(msg), obj || ''), 18 | error: (msg, obj) => log(_error(msg), obj || ''), 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/validateBranch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const log = require('./log'); 4 | const shell = require('shelljs'); // Make this a variable to permit mocking during testing 5 | 6 | module.exports = function validateBranch(branch) { 7 | if (branch === '*') { 8 | return null; 9 | } 10 | 11 | shell.exec('git branch').stdout; 12 | const currentBranch = shell.exec('git rev-parse --abbrev-ref HEAD').stdout.split('\n')[0]; 13 | 14 | if (branch !== currentBranch) { 15 | log.error(`You can only release from the ${branch} branch. Use option --branch to specify branch name.`); 16 | shell.exit(0); 17 | } else { 18 | log.info(`>>> Your release branch is: ${branch}`); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/bumpUpVersion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const helpers = require('./helpers'); 3 | const shell = require('shelljs'); 4 | const log = require('./log'); 5 | 6 | module.exports = function bumpUpVersion(bumpType, latestTag, tagPrefix) { 7 | log.info('>>> update version on package.json...'); 8 | let version = 'v1.0.0'; // First Release 9 | 10 | try { 11 | if (helpers.isFirstRelease(latestTag)) { 12 | shell.exec('npm version --no-git-tag-version ' + version).stdout.split('\n')[0]; 13 | } else { 14 | version = shell.exec('npm version --no-git-tag-version ' + bumpType).stdout.split('\n')[0]; 15 | } 16 | } catch (error) { 17 | log.error(error); 18 | helpers.terminateProcess(1); 19 | } 20 | if (tagPrefix !== '') { 21 | version = `${tagPrefix}${version}`; 22 | console.log(version); 23 | } 24 | return version; 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/getJsonCommits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const parseRawCommit = require('./parseRawCommit'); 3 | const shell = require('shelljs'); 4 | 5 | module.exports = function getJsonCommits(latestTag) { 6 | const rawCommits = shell.exec(`git log -E --format=%H==SPLIT==%B==END== ${latestTag}`, {silent: true}).stdout; 7 | 8 | const commits = rawCommits.split('==END==\n') 9 | .filter(function(raw) { 10 | return !!raw.trim(); 11 | }).map(function(raw) { 12 | const data = raw.split('==SPLIT=='); 13 | return { 14 | hash: data[0], 15 | message: data[1], 16 | }; 17 | }); 18 | 19 | const parsedCommits = commits.map(function(commit) { 20 | return parseRawCommit(commit.hash + '\n' + commit.message); 21 | }).filter(function(commit) { 22 | return !!commit; 23 | }); 24 | 25 | return parsedCommits; 26 | }; 27 | -------------------------------------------------------------------------------- /src/lib/isReleaseNecessary.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const log = require('./log'); 3 | 4 | module.exports = function isReleaseNecessary(bumpType, latestTag, parsedCommits, verbose) { 5 | if (verbose === undefined) verbose = false; 6 | 7 | if (!bumpType || bumpType === '') { 8 | log.success('\n\nRelease is not necessary at this point. Maybe your commits since your last tag only contains "docs", "style", "refactor", "test" and/or "chore"\n'); 9 | 10 | log.info('---> YOUR LATEST TAG: ', latestTag); 11 | if (!verbose) log.info('Run this command again with -v or --verbose to see the commit list from last tag until HEAD.'); 12 | 13 | if (verbose) { 14 | log.info('PARSED COMMIT LIST SINCE YOUR LATEST TAG:\n'); 15 | 16 | log.info('>>> parsedCommits: ', parsedCommits); 17 | } 18 | 19 | return false; 20 | } 21 | 22 | return true; 23 | }; 24 | -------------------------------------------------------------------------------- /config/verify/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // START_CONFIT_GENERATED_CONTENT 4 | let config = { 5 | extends: ['google', 'plugin:node/recommended'], 6 | plugins: ['node'], 7 | env: { 8 | commonjs: true, // For Webpack, CommonJS 9 | 'node': true, 10 | 'mocha': true, 11 | 'es6': true 12 | }, 13 | globals: { 14 | 15 | }, 16 | parser: 'espree', 17 | parserOptions: { 18 | ecmaVersion: 6, 19 | sourceType: 'module', 20 | ecmaFeatures: { 21 | globalReturn: false, 22 | impliedStrict: true, 23 | jsx: false, 24 | } 25 | }, 26 | rules: { 27 | 'max-len': ['warn', 200], // Line length 28 | 'require-jsdoc': ['off'] 29 | } 30 | }; 31 | // END_CONFIT_GENERATED_CONTENT 32 | // Customise 'config' further in this section to meet your needs... 33 | 34 | 35 | 36 | // START_CONFIT_GENERATED_CONTENT 37 | module.exports = config; 38 | // END_CONFIT_GENERATED_CONTENT 39 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const addFilesAndCreateTag = require('./addFilesAndCreateTag'); 4 | const bumpUpVersion = require('./bumpUpVersion'); 5 | const getJsonCommits = require('./getJsonCommits'); 6 | const getLatestTag = require('./getLatestTag'); 7 | const helpers = require('./helpers'); 8 | const isReleaseNecessary = require('./isReleaseNecessary'); 9 | const parseRawCommit = require('./parseRawCommit'); 10 | const runScript = require('./runScript'); 11 | const validateBranch = require('./validateBranch'); 12 | const whatBump = require('./whatBump'); 13 | const writeChangelog = require('./writeChangelog'); 14 | const log = require('./log'); 15 | 16 | module.exports = { 17 | addFilesAndCreateTag, 18 | bumpUpVersion, 19 | getJsonCommits, 20 | getLatestTag, 21 | helpers, 22 | isReleaseNecessary, 23 | parseRawCommit, 24 | runScript, 25 | validateBranch, 26 | whatBump, 27 | writeChangelog, 28 | log, 29 | }; 30 | -------------------------------------------------------------------------------- /spec/parseRawCommit.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const parseRawCommit = require('../src/lib/parseRawCommit'); 4 | const expect = require('chai').expect; 5 | 6 | describe('parseRawCommit', () => { 7 | const HASH = '012345HASH'; 8 | const SUBJECT = 'subject'; 9 | const BODY = 'body'; 10 | const FOOTER = 'footer'; 11 | 12 | it('should parse a "normal" feature commit', () => { 13 | const commit = `${HASH}\nfeat(scope): ${SUBJECT}\n\n${BODY}\n\n${FOOTER}`; 14 | 15 | const msg = parseRawCommit(commit); 16 | 17 | expect(msg).not.equal(null); 18 | expect(msg.hash).to.equal(HASH); 19 | expect(msg.subject).to.equal(SUBJECT); 20 | expect(msg.body).to.equal(`\n${BODY}\n\n${FOOTER}`); 21 | expect(msg.type).to.equal('feat'); 22 | expect(msg.component).to.equal('scope'); 23 | }); 24 | 25 | it('should not parse a feature commit with a scope which contains a "/" to be consistent with other commit message parsing tools', () => { 26 | const commit = `${HASH}\nfeat(scope/foo): ${SUBJECT}\n\n${BODY}\n\n${FOOTER}`; 27 | 28 | const msg = parseRawCommit(commit); 29 | 30 | expect(msg).to.equal(null); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Leonardo Correa 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 | -------------------------------------------------------------------------------- /spec/log.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const log = require('../src/lib/log'); 4 | const expect = require('chai').expect; 5 | const chalk = require('chalk'); 6 | const stdout = require('test-console').stdout; 7 | 8 | describe('log', () => { 9 | const testData = [ 10 | {desc: 'info', fn: log.info, colour: 'blue', colourFn: chalk.bold.cyan}, 11 | {desc: 'success', fn: log.success, colour: 'green', colourFn: chalk.bold.green}, 12 | {desc: 'announce', fn: log.announce, colour: 'white', colourFn: chalk.bold.bgGreen.white}, 13 | {desc: 'error', fn: log.error, colour: 'red', colourFn: chalk.bold.red}, 14 | ]; 15 | 16 | testData.forEach((test) => { 17 | describe(test.desc, () => { 18 | it(`should generate a ${test.colour} message`, () => { 19 | const output = stdout.inspectSync(() => { 20 | test.fn('foo'); 21 | }); 22 | expect(output[0]).to.equal(test.colourFn('foo') + ' \n'); 23 | }); 24 | 25 | it(`should generate a ${test.colour} message and append data`, () => { 26 | const output = stdout.inspectSync(() => { 27 | test.fn('foo', 'bar'); 28 | }); 29 | expect(output[0]).to.equal(test.colourFn('foo') + ' bar\n'); 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/lib/addFilesAndCreateTag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const terminateProcess = require('./helpers').terminateProcess; 3 | const shell = require('shelljs'); 4 | const log = require('./log'); 5 | 6 | module.exports = function addFilesAndCreateTag(newVersion, mockPush, ciSkip) { 7 | let code; 8 | // ###### Add edited files to git ##### 9 | log.info('>>> About to add and commit package.json and CHANGELOG...'); 10 | code = shell.exec('git add package.json CHANGELOG.md').code; 11 | terminateProcess(code); 12 | 13 | // ###### Commit files ##### 14 | const commitMessage = 'git commit -m "chore(release): ' + newVersion 15 | + (ciSkip ? ' [ci skip] ***NO_CI***' : '') + '"'; 16 | code = shell.exec(commitMessage).code; 17 | terminateProcess(code); 18 | 19 | // ###### TAG NEW VERSION ##### 20 | log.info(`>> Time to create the Semantic Tag: ${newVersion}`); 21 | code = shell.exec('git tag ' + newVersion).code; 22 | terminateProcess(code); 23 | 24 | // ###### PUSH CHANGES ##### 25 | log.info('>>...and push to remote...'); 26 | if (mockPush === undefined) { 27 | code = shell.exec('git push && git push --tags').code; 28 | } else { 29 | log.info(`mocking git push with return code ${mockPush}`); 30 | code = mockPush; 31 | } 32 | terminateProcess(code); 33 | 34 | return code; 35 | }; 36 | -------------------------------------------------------------------------------- /src/lib/getLatestTag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const semverValid = require('semver').valid; 3 | const shell = require('shelljs'); 4 | const log = require('./log'); 5 | 6 | module.exports = function getLatestTag(verbose, prefix) { 7 | if (verbose === undefined) verbose = false; 8 | 9 | const regex = /tag:\s*(.+?)[,\)]/gi; 10 | const cmd = 'git log --date-order --tags --simplify-by-decoration --pretty=format:"%d"'; 11 | const data = shell.exec(cmd, {silent: true}).stdout; 12 | let latestTag = null; 13 | 14 | data.split('\n').some(function(decorations) { 15 | let match; 16 | while (match = regex.exec(decorations)) { // eslint-disable-line no-cond-assign 17 | const tag = match[1]; 18 | if (tag.startsWith(prefix)) { 19 | const semanticVersion = tag.replace(prefix, ''); 20 | if (semverValid(semanticVersion)) { 21 | latestTag = tag; 22 | return true; 23 | } 24 | } 25 | } 26 | }); 27 | 28 | if (latestTag) { 29 | if (verbose) log.info('>> Your latest semantic tag is: ', latestTag); 30 | 31 | latestTag = `${latestTag}..HEAD`; 32 | } else { 33 | log.info('>> No SemVer tag found. It seems like your first release? Initial release will be set to v1.0.0 as per npmjs specification.'); 34 | latestTag = 'HEAD'; 35 | } 36 | 37 | return latestTag; 38 | }; 39 | -------------------------------------------------------------------------------- /config/release/commitMessageConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | types: [ 6 | {value: 'feat', name: 'feat: A new feature'}, 7 | {value: 'fix', name: 'fix: A bug fix'}, 8 | {value: 'docs', name: 'docs: Documentation only changes'}, 9 | {value: 'style', name: 'style: Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)'}, 10 | {value: 'refactor', name: 'refactor: A code change that neither fixes a bug nor adds a feature'}, 11 | {value: 'perf', name: 'perf: A code change that improves performance'}, 12 | {value: 'test', name: 'test: Adding missing tests'}, 13 | {value: 'chore', name: 'chore: Changes to the build process or auxiliary tools\n and libraries such as documentation generation'}, 14 | {value: 'revert', name: 'revert: Revert to a commit'}, 15 | {value: 'WIP', name: 'WIP: Work in progress'} 16 | ], 17 | 18 | // it needs to match the value for field type. Eg.: 'fix' 19 | scopes: [ 20 | {name: 'app'}, 21 | {name: 'build'}, 22 | {name: 'changelog'}, 23 | {name: 'contributing'}, 24 | {name: 'process'}, 25 | {name: 'readme'}, 26 | ], 27 | 28 | allowCustomScopes: false, 29 | allowBreakingChanges: ['feat', 'fix'], 30 | 31 | // Appends the branch name to the footer of the commit. Useful for tracking commits after branches have been merged 32 | appendBranchNameToCommitMessage: false 33 | }; 34 | -------------------------------------------------------------------------------- /confit.yml: -------------------------------------------------------------------------------- 1 | generator-confit: 2 | app: 3 | _version: 462ecd915fd9db1aef6a37c2b5ce8b58b80c18ba 4 | buildProfile: Latest 5 | copyrightOwner: Developers who contribute to this project 6 | license: MIT 7 | projectType: node 8 | publicRepository: true 9 | repositoryType: GitHub 10 | paths: 11 | _version: 780b129e0c7e5cab7e29c4f185bcf78524593a33 12 | config: 13 | configDir: config/ 14 | input: 15 | srcDir: src/ 16 | unitTestDir: spec/ 17 | output: 18 | prodDir: dist/ 19 | reportDir: reports/ 20 | buildJS: 21 | _version: ead8ce4280b07d696aff499a5fca1a933727582f 22 | framework: [] 23 | frameworkScripts: [] 24 | outputFormat: ES6 25 | sourceFormat: ES6 26 | entryPoint: 27 | _version: 39082c3df887fbc08744dfd088c25465e7a2e3a4 28 | entryPoints: 29 | main: 30 | - src/index.js 31 | testUnit: 32 | _version: 30eee42a88ee42cce4f1ae48fe0cbe81647d189a 33 | testDependencies: [] 34 | testFramework: mocha 35 | verify: 36 | _version: 30ae86c5022840a01fc08833e238a82c683fa1c7 37 | jsCodingStandard: Google 38 | documentation: 39 | _version: b1658da3278b16d1982212f5e8bc05348af20e0b 40 | generateDocs: false 41 | release: 42 | _version: 47f220593935b502abf17cb34a396f692e453c49 43 | checkCodeCoverage: true 44 | commitMessageFormat: Conventional 45 | useSemantic: true 46 | sampleApp: 47 | _version: 00c0a2c6fc0ed17fcccce2d548d35896121e58ba 48 | createSampleApp: false 49 | zzfinish: {} 50 | -------------------------------------------------------------------------------- /src/lib/parseRawCommit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function parseRawCommit(raw) { 4 | const COMMIT_PATTERN = /^(\w*)(\(([\w\$\.\-\* ]*)\))?\: (.*)$/; 5 | const MAX_SUBJECT_LENGTH = 80; 6 | 7 | if (!raw) { 8 | return null; 9 | } 10 | 11 | const lines = raw.split('\n'); 12 | const msg = {}; 13 | let match; 14 | 15 | msg.hash = lines.shift(); 16 | msg.subject = lines.shift(); 17 | msg.closes = []; 18 | msg.breaks = []; 19 | 20 | msg.subject = msg.subject.replace(/\s*(?:Closes|Fixes|Resolves)\s#(\d+)/ig, function(_, i) { 21 | msg.closes.push(parseInt(i, 10)); 22 | return ''; 23 | }); 24 | 25 | lines.forEach(function(line) { 26 | match = line.match(/(?:Closes|Fixes|Resolves)\s((?:#\d+(?:\,\s)?)+)/ig); 27 | 28 | if (match) { 29 | match.forEach(function(m) { 30 | if (m) { 31 | m.split(',').forEach(function(i) { 32 | const issue = i.match(/\d+/); 33 | if (issue) { 34 | msg.closes.push(parseInt(issue[0], 10)); 35 | } 36 | }); 37 | } 38 | }); 39 | } 40 | }); 41 | 42 | match = raw.match(/BREAKING CHANGE:\s([\s\S]*)/); 43 | if (match) { 44 | msg.breaks.push(match[1] + '\n'); 45 | } 46 | 47 | msg.body = lines.join('\n'); 48 | match = msg.subject.match(COMMIT_PATTERN); 49 | 50 | if (!match || !match[1] || !match[4]) { 51 | return null; 52 | } 53 | 54 | if (match[4].length > MAX_SUBJECT_LENGTH) { 55 | match[4] = match[4].substr(0, MAX_SUBJECT_LENGTH); 56 | } 57 | 58 | msg.type = match[1]; 59 | msg.component = match[3]; 60 | msg.subject = match[4]; 61 | 62 | return msg; 63 | }; 64 | -------------------------------------------------------------------------------- /spec/validateBranch.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const expect = require('chai').expect; 3 | const rewire = require('rewire'); 4 | const stdout = require('test-console').stdout; 5 | const validateBranch = rewire('../src/lib/validateBranch'); // NOTE: rewired! 6 | 7 | describe('validateBranch', () => { 8 | let revert = () => {}; 9 | 10 | afterEach(() => revert()); // revert the mocking of anything 11 | 12 | it('should show a message if the current branch is not the release branch and exit with 0', () => { 13 | let exitCode; 14 | revert = validateBranch.__set__({ 15 | shell: { 16 | exec: () => ({stdout: 'fooBranch\n'}), 17 | exit: (code) => { 18 | exitCode = code; 19 | return; 20 | }, 21 | }, 22 | }); 23 | 24 | const output = stdout.inspectSync(() => { 25 | validateBranch('bar'); 26 | }); 27 | 28 | expect(output[0]).to.include(`You can only release from the bar branch. Use option --branch to specify branch name.`); 29 | expect(exitCode).to.equal(0); 30 | }); 31 | 32 | 33 | it('should show an info message if the current branch matches the release branch', () => { 34 | let exitCalled = false; 35 | revert = validateBranch.__set__({ 36 | shell: { 37 | exec: () => ({stdout: 'fooBranch\n'}), 38 | exit: () => exitCalled = true, 39 | }, 40 | }); 41 | 42 | const output = stdout.inspectSync(() => { 43 | validateBranch('fooBranch'); 44 | }); 45 | 46 | expect(output[0]).to.include(`>>> Your release branch is: fooBranch`); 47 | expect(exitCalled).to.equal(false); // exit() is never called 48 | }); 49 | 50 | it('should allow release from any branch when option --branch is "*"', () => { 51 | let exitCalled = false; 52 | 53 | revert = validateBranch.__set__({ 54 | shell: { 55 | exit: () => exitCalled = true, 56 | }, 57 | }); 58 | 59 | const result = validateBranch('*'); 60 | 61 | expect(result).to.equal(null); 62 | expect(exitCalled).to.equal(false); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "corp-semantic-release", 3 | "description": "Semantic-release package for repositories inside private networks (e.g. corporations) or for non-GitHub repositories.", 4 | "keywords": [ 5 | "author", 6 | "automation", 7 | "changelog", 8 | "module", 9 | "package", 10 | "publish", 11 | "release", 12 | "semver", 13 | "version" 14 | ], 15 | "homepage": "https://github.com/leonardoanalista/corp-semantic-release#readme", 16 | "bugs": { 17 | "url": "https://github.com/leonardoanalista/corp-semantic-release/issues" 18 | }, 19 | "license": "MIT", 20 | "author": "leonardoanalista", 21 | "contributors": [ 22 | "leonardoanalista", 23 | "Brett Uglow ", 24 | "forstermatth" 25 | ], 26 | "files": [ 27 | "src", 28 | "*.md" 29 | ], 30 | "main": "src/index.js", 31 | "bin": { 32 | "corp-semantic-release": "src/index.js" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/leonardoanalista/corp-semantic-release.git" 37 | }, 38 | "scripts": { 39 | "pre-release": "npm-run-all verify test:coverage build", 40 | "_README": "echo Due to the fact the tests are executing in shell processes, we need to use NYC to get coverage information.", 41 | "build": "cross-env NODE_ENV=production npm run clean:prod", 42 | "clean:prod": "rimraf dist/", 43 | "commitmsg": "cz-customizable-ghooks", 44 | "dev": "cross-env NODE_ENV=development npm run verify:watch", 45 | "prepush": "npm-run-all verify test:coverage --silent", 46 | "semantic-release": "semantic-release", 47 | "start": "npm run dev", 48 | "test": "npm run test:unit", 49 | "test:check-coverage": "nyc check-coverage", 50 | "test:coverage": "npm-run-all test:unit:once test:check-coverage --silent", 51 | "test:unit": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts -w", 52 | "test:unit:once": "cross-env NODE_ENV=test nyc mocha --opts config/testUnit/mocha.opts", 53 | "upload-coverage": "cat reports/coverage/lcov.info | coveralls", 54 | "verify": "npm run verify:js --silent", 55 | "verify:js": "eslint -c config/verify/.eslintrc.js \"src/**/*.js\" \"spec/**/*.js\" && echo ✅ verify:js success", 56 | "verify:js:fix": "eslint --fix -c config/verify/.eslintrc.js \"src/**/*.js\" \"spec/**/*.js\" && echo ✅ verify:js:fix success", 57 | "verify:js:watch": "chokidar 'src/**/*.js' 'spec/**/*.js' -c 'npm run verify:js:fix' --initial --silent", 58 | "verify:watch": "npm run verify:js:watch --silent", 59 | "travis-deploy-once": "travis-deploy-once" 60 | }, 61 | "config": { 62 | "commitizen": { 63 | "path": "node_modules/cz-customizable" 64 | }, 65 | "cz-customizable": { 66 | "config": "config/release/commitMessageConfig.js" 67 | } 68 | }, 69 | "dependencies": { 70 | "async": "^2.6.2", 71 | "chalk": "^2.4.2", 72 | "commander": "^2.19.0", 73 | "conventional-changelog": "1.1.5", 74 | "conventional-changelog-angular-bitbucket": "^1.2.0", 75 | "prepend-file": "1.3.1", 76 | "semver": "^5.6.0", 77 | "shelljs": "^0.8.3", 78 | "temp": "^0.9.0", 79 | "through2": "2.0.3" 80 | }, 81 | "devDependencies": { 82 | "travis-deploy-once": "^5.0.11", 83 | "chai": "^4.2.0", 84 | "chokidar-cli": "^1.2.2", 85 | "coveralls": "^3.0.3", 86 | "cross-env": "^5.2.0", 87 | "cz-customizable": "^5.4.0", 88 | "cz-customizable-ghooks": "1.5.0", 89 | "eslint": "^5.15.1", 90 | "eslint-config-google": "^0.12.0", 91 | "eslint-friendly-formatter": "^4.0.1", 92 | "eslint-plugin-node": "^8.0.1", 93 | "husky": "^1.3.1", 94 | "mocha": "^6.0.2", 95 | "mocha-junit-reporter": "^1.18.0", 96 | "mocha-multi-reporters": "^1.1.7", 97 | "npm-run-all": "^4.1.5", 98 | "nyc": "^13.3.0", 99 | "rewire": "^4.0.1", 100 | "rimraf": "^2.6.3", 101 | "semantic-release": "^15.13.3", 102 | "test-console": "^1.1.0" 103 | }, 104 | "engines": { 105 | "node": ">= 4.0.0", 106 | "npm": ">= 2.0.0" 107 | }, 108 | "version": "6.5.0" 109 | } 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const lib = require('./lib'); 5 | const log = require('./lib').log; 6 | const shell = require('shelljs'); 7 | const program = require('commander'); 8 | const fs = require('fs'); 9 | const through = require('through2'); 10 | const changelog = require('conventional-changelog'); 11 | const async = require('async'); 12 | const stream = require('stream'); 13 | 14 | 15 | try { 16 | fs.statSync(process.cwd() + '/package.json'); 17 | } catch (e) { 18 | log.error('Cant find your package.json.'); 19 | shell.exit(1); 20 | } 21 | 22 | const pkg = require(process.cwd() + '/package.json'); 23 | const oldVersion = pkg.version; 24 | 25 | if (!pkg.name || !oldVersion) { 26 | log.error('Minimum required fields in your package.json are name and version.'); 27 | shell.exit(1); 28 | } 29 | 30 | program 31 | .version(oldVersion) 32 | .option('--ci-skip [boolean]', 'Skip Continuous Integration in CI environment. This adds "[ci skip] ***NO_CI***" to the commit message. Default is true.') 33 | .option('-d, --dryrun', 'No changes to workspace. Stops after changelog is printed.') 34 | .option('--pre-commit [npm-script]', 'Pre-commit hook. Pass the name of the npm script to run. It will run like this: npm run [pre-commit]') 35 | .option('--post-success [command]', 'Post-success hook (after git push completes successfully). Pass a command to run as the argument. Eg: --post-success "npm publish"') 36 | .option('-b, --branch [branch]', 'Branch name [branch] allowed to run release. Default is master. If you want another branch, you need to specify. Use "*" to allow any branch') 37 | .option('-v, --verbose', 'Prints debug info') 38 | .option('--changelogpreset [preset]', 'The conventional-changelog preset to use. Default is angular. angular-bitbucket' + 39 | ' is available for BitBucket repositories. Other presets can be installed: npm i conventional-changelog-jquery') 40 | .option('-r, --releasecount [number]', 'How many releases of changelog you want to generate.', parseInt) 41 | .option('--mock-push [return code]', 'Used in testing to mock git push, the mock will return [return code]', parseInt) 42 | .option('--tagPrefix [tagPrefix]', 'Tag prefix') 43 | .parse(process.argv); 44 | 45 | if (program.dryrun) { 46 | log.announce('>> YOU ARE RUNNING IN DRY RUN MODE. NO CHANGES WILL BE MADE <<'); 47 | } 48 | 49 | program.ciSkip = program.ciSkip !== 'false'; 50 | program.branch = program.branch || 'master'; 51 | program.changelogpreset = program.changelogpreset || 'angular'; 52 | // Release count defaults to 1 (generate 1 release), but could be 0 (hence logic). See https://github.com/conventional-changelog/conventional-changelog-core#releasecount 53 | program.releasecount = (program.releasecount != undefined) ? program.releasecount : 1; 54 | program.tagPrefix = program.tagPrefix || ''; 55 | 56 | 57 | // ### STEP 0 - Validate branch 58 | lib.validateBranch(program.branch); 59 | // ### STEP 1 - Work out tags 60 | const latestTag = lib.getLatestTag(program.verbose, program.tagPrefix); 61 | // ### STEP 2 - Get Commits 62 | const jsonCommits = lib.getJsonCommits(latestTag); 63 | // ### STEP 3 - find out Bump type 64 | const bumpType = lib.whatBump(jsonCommits); 65 | // ### STEP 4 - release or not? 66 | if (!lib.isReleaseNecessary(bumpType, latestTag, jsonCommits, program.verbose)) { 67 | shell.exit(0); 68 | } 69 | 70 | // ### STEP 5 - bump version in package.json (DESTRUCTIVE OPERATION - but we remember the old version and restore at the end) 71 | const version = lib.bumpUpVersion(bumpType, latestTag, program.tagPrefix); 72 | 73 | async.series([ 74 | // ### STEP 6 - get changelog contents 75 | function(callback) { 76 | const contentStream = new stream.Writable(); 77 | const data = []; 78 | 79 | contentStream._write = function(chunk) { 80 | callback(null, chunk.toString()); 81 | }; 82 | 83 | 84 | // here we can add the options in the future: 85 | // (options, context, gitRawCommitsOpts, parserOpts, writerOpts); 86 | const options = { 87 | preset: program.changelogpreset, 88 | releaseCount: program.releasecount, 89 | }; 90 | changelog(options) 91 | // Use through() to handle multiple chunks which could be returned from changelog() 92 | .pipe(through({ 93 | objectMode: true, 94 | }, function(chunk, enc, cb) { 95 | try { 96 | data.push(chunk); 97 | } catch (err) { 98 | setImmediate(function() { 99 | throw err; 100 | }); 101 | } 102 | 103 | cb(); 104 | }, function(cb) { 105 | // This function is called once ALL the object-chunks have been processed. 106 | data.push(null); 107 | 108 | cb(null, data.join('')); // Pass the data to the next stream processor 109 | })) 110 | .pipe(contentStream); 111 | }, 112 | ], 113 | function(err, results) { 114 | const changes = results[0]; 115 | 116 | // ### STEP 7 - Write or Append (DESTRUCTIVE OPERATION) 117 | if (!program.dryrun) { 118 | lib.writeChangelog(changes, program.releasecount === 0, program.verbose); // it has to run after the version has been bumped. 119 | } else { 120 | log.info('>>> Changelog contents would have been: \n\n', changes); 121 | 122 | // Ensure old package version is restored 123 | log.info('>>> Resetting package.json to original version'); 124 | shell.exec('npm version --no-git-tag-version ' + oldVersion); 125 | } 126 | 127 | // ### STEP 8 - Run if any pre commit script has been specified (DESTRUCTIVE OPERATION?) 128 | if (program.preCommit) { 129 | if (program.dryRun) { 130 | log.info('>>> Skipping pre-commit script'); 131 | } else { 132 | lib.runScript(`npm run ${program.preCommit} -- ${version}`, 'pre-commit'); 133 | } 134 | } 135 | 136 | // ### STEP 9 - Tag and push (DESTRUCTIVE OPERATION) 137 | if (!program.dryrun) { 138 | lib.addFilesAndCreateTag(version, program.mockPush, program.ciSkip); 139 | } else { 140 | log.info('>>> Skipping git push'); 141 | } 142 | 143 | // ### STEP 10 - Run after successful push (DESTRUCTIVE OPERATION) 144 | if (program.postSuccess) { 145 | if (!program.dryrun) { 146 | lib.runScript(program.postSuccess, 'post-success'); 147 | } else { 148 | log.info('>>> Skipping post-success command'); 149 | } 150 | } 151 | }); 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # corp-semantic-release 3 | 4 | 5 | 6 | > [Semantic-release](https://github.com/semantic-release/semantic-release) for repositories that are inside private networks (e.g. corporations) 7 | or for non-GitHub repositories. 8 | 9 | It has almost all expected features from [semantic-release](https://github.com/semantic-release/semantic-release) but will **not** publish to an NPM registry at the end. 10 | 11 | Obviously this project is **100%** inspired by semantic release. This module is **not** supposed to replace its parent. First try to use semantic-release. If you have special requirements under corporate network, then this module is made for you. Ironically this module uses semantic-release as I don't have any network restrictions at my home. 12 | 13 | 14 | [![NPM Version](https://img.shields.io/npm/v/corp-semantic-release.svg?style=flat-square)](http://npm.im/corp-semantic-release) 15 | [![Build Status](https://travis-ci.org/leonardoanalista/corp-semantic-release.svg)](https://travis-ci.org/leonardoanalista/corp-semantic-release) 16 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 17 | [![Coverage Status](https://coveralls.io/repos/github/leonardoanalista/corp-semantic-release/badge.svg?branch=master)](https://coveralls.io/github/leonardoanalista/corp-semantic-release?branch=master) 18 | [![Dependencies status](https://david-dm.org/leonardoanalista/corp-semantic-release/status.svg?theme=shields.io)](https://david-dm.org/leonardoanalista/corp-semantic-release#info=dependencies) 19 | [![Dev-dependencies status](https://david-dm.org/leonardoanalista/corp-semantic-release/dev-status.svg?theme=shields.io)](https://david-dm.org/leonardoanalista/corp-semantic-release#info=devDependencies) 20 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 21 | [![npm monthly downloads](https://img.shields.io/npm/dm/corp-semantic-release.svg?style=flat-square)](https://www.npmjs.com/package/corp-semantic-release) 22 | 23 | ## Actions performed 24 | 25 | * Detect if a release is required. If not, exit(0); 26 | * Figure out type of bump as usual: Major, minor or patch 27 | * Read your commits from last semantic tag and generate or append contents to `CHANGELOG.md` file. 28 | * bump your `package.json` 29 | * run `git add package.json CHANGELOG.md` 30 | * run any `pre-commit` script, if specified 31 | * run `git commit -m "chore(build): release v1.0.0"` 32 | * run `git tag v1.0.0` 33 | * run `git push` 34 | * run `git push --tags` 35 | 36 | 37 | ## Install 38 | 39 | npm install corp-semantic-release --save-dev 40 | 41 | 42 | ## Configure 43 | 44 | This tool installs itself as a `bin`. After installation, you have this command `corp-semantic-release` available from anywhere. 45 | 46 | Add this script to your `package.json` 47 | 48 | ``` 49 | "scripts": { 50 | "corp-release": "corp-semantic-release [options here]" 51 | } 52 | ``` 53 | 54 | Of course you can change `corp-release` to any name you like. 55 | 56 | 57 | ## Options 58 | * `--ci-skip [boolean]`: Skip Continuous Integration in CI environment. This adds `[ci skip] ***NO_CI***` to the commit message. Default is `true`. 59 | * `-d` or `--dryrun`: it runs in non-destructive mode. No alteration should be done in your workspace. 60 | * `--pre-commit [npm-script]`: Pre-commit hook. Pass the name of the npm script to run. It will run like this: `npm run [npm-script]`. 61 | * `--post-success [command]`: Post-success hook (after `git push` completes successfully). Pass a command to run as the argument. Eg: `--post-success "npm publish"`. 62 | * `-b [branch]` or `--branch [branch]`: Branch name allowed to run release. Default is `master`. If you want to release from another branch, you need to specify. Use "*" to allow any branch - Useful for Jenkins as git does a revision check-out. 63 | * `-v` or `--verbose`: it prints extra info such as commit list from last tag and command details. 64 | * `--changelogpreset [preset]`: The conventional-changelog preset to use. Default is `angular`. `angular-bitbucket` is available for [BitBucket repositories](https://github.com/uglow/conventional-changelog-angular-bitbucket). Other presets can be installed, e.g: `npm i conventional-changelog-jquery` then pass this flag to the command: `--changelogpreset jquery`. 65 | * `-r [num]` or `--releasecount [num]`: How many releases of changelog you want to generate. It counts from the upcoming release. Useful when you forgot to generate any previous changelog. Set to 0 to regenerate all (will overwrite any existing changelog!). 66 | * `--tagPrefix [tag name]`: Gives the tag version a prefix such as ALPHA 1.1.2. 67 | 68 | **NOTE**: If you run via `npm`, you have to add `--` before the options so npm passes all arguments to node. Eg.: `npm run corp-release -- -v -d` 69 | 70 | 71 | ## Updating other files 72 | A pretty common requirement when updating the version number is to update other files with 73 | the same version number. There are two ways you can run your own scripts to update additional files: 74 | 75 |
76 | Option 1 - NPM hook 77 | You can use NPM's built-in `(pre|post)version` [script-hook](https://docs.npmjs.com/cli/version) to run code before/just-after/after `package.json` is modified by `corp-semantic-release`. 78 | 79 | In the following example, `updateOtherFiles.js` does *NOT* receive the version as an argument but must query `package.json` to get the bumped version. 80 | ```json 81 | 82 | "scripts": { 83 | "corp-release": "corp-semantic-release", 84 | "version": "node updateOtherFiles.js" 85 | } 86 | 87 | ``` 88 |
89 | 90 | 91 |
92 | Option 2 - `--pre-commit [npm-script]` 93 | `corp-semantic-release` also provides a `--pre-commit ` option. The NPM script is passed the version 94 | number as an argument to the script. 95 | 96 | ```json 97 | 98 | "scripts": { 99 | "corp-release": "corp-semantic-release --pre-commit updateFiles", 100 | "updateFiles": "node updateOtherFiles.js" 101 | } 102 | 103 | ``` 104 |
105 | 106 | Remember to stage the files using `git add ` after modifying the files, so that when `corp-semantic-release` commits the changes, all the changed files are commited. 107 | 108 | 109 | ## Contribute 110 | 111 | Please refer to the [Contributor Guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md) and [Conduct of Code](https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md) from [AngularJs](https://github.com/angular/angular.js) project. 112 | 113 | 114 | ## TODO - Roadmap: 115 | * add option to create release on Enterprise GitHub using API v3. I am sure one day the corps will be using version 3. 116 | 117 | ## FAQ 118 | 119 | ### I work in a corporation which has a network proxy which is a pain in the azz. What should I do? 120 | This NPM module is for you! I was unable to pass the setup stage of `semantic-release` inside a corporation network. That was one of the reasons why I created this package. 121 | 122 | ### How do I setup `corp-semantic-release`? 123 | Run `npm install corp-semantic-release`. There is no setup wizard like the `semantic-release` package has. Simple! 124 | 125 | ### Can I trust 'corp-semantic-release'? 126 | Take a look at the file `test/e2e.spec.js`. It has comprehensive system tests in order to make sure it works as expected. 127 | 128 | ### Are the pipeline-of-actions different to `semantic-release`? 129 | Yes. Importantly, `corp-semantic-release` will *not* attempt to publish to an NPM registry. 130 | 131 | Actions performed: 132 | 133 | 1. Validate that the current branch is the release branch. 134 | 1. Determine the current version number (from the latest git tag). 135 | 1. Get the commit history since the latest git tag. 136 | 1. Determine the new semantic version number from the commit history. 137 | 1. Decide whether a release is required or not. If not, exit. 138 | 1. Update `package.json` with the new version number. 139 | 1. Update or create `CHANGELOG.md` with the relevant log entries (from the commit history). 140 | 1. Run pre-commit scripts. 141 | 1. Commit file changes to git, create git tag then push all changes (including tags). 142 | 143 | 144 | ### What else is different to `semantic-release`? 145 | `corp-semantic-release`, at the moment, generetes and appends changelog contents to a file called CHANGELOG.md. 146 | 147 | ### I just can't get over it - I really hate corporate proxies! 148 | 149 | 150 | I totally understand your frustrations and you are not the only one. Proxy settings is not the focus of this project 151 | but I am happy to provide some help if I can. I use `cntlm` as reverse proxy. I also **turn off ssl on npm**. 152 | This is how I get things working. If you need further instructions on cntlm, send me a message. 153 | 154 | 155 | 156 | ## License 157 | 158 | This software is licensed under the MIT Licence. See [LICENSE](LICENSE). 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | Welcome! This document explains how you can contribute to making **corp-semantic-release** even better. 5 | 6 | 7 | 8 | 9 | 10 | # Getting Started 11 | 12 | ## Installation 13 | 14 | ``` 15 | git clone 16 | npm install -g commitizen 17 | npm install -g semantic-release-cli 18 | npm install 19 | ``` 20 | 21 | 22 | 23 | 24 | 25 | ## Directory Structure 26 | 27 | Code is organised into modules which contain one-or-more components. This a great way to ensure maintainable code by encapsulation of behavior logic. A component is basically a self contained app usually in a single file or a folder with each concern as a file: style, template, specs, e2e, and component class. Here's how it looks: 28 | ``` 29 | corp-semantic-release/ 30 | ├──config/ * configuration files live here (e.g. eslint, verify, testUnit) 31 | │ 32 | ├──src/ * source code files should be here 33 | │ 34 | ├──reports/ * test reports appear here 35 | │ 36 | ├──spec/ * unit test specifications live here 37 | │ 38 | ├──confit.yml * the project config file generated by 'yo confit' 39 | ├──.nycrc * unit test coverage, reporting and threshold config 40 | ├──CONTRIBUTING.md * how to contribute to the project 41 | ├──README.md * this file 42 | └──package.json * NPM package description file 43 | ``` 44 | 45 | 46 | 47 | 48 | 49 | # GitFlow Development Process 50 | 51 | This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/index.html) workflow. 52 | 53 | ## Create a branch 54 | When you're working on a project, you're going to have a bunch of different features or ideas in progress at any given time – some of which are ready to go, and others which are not. Branching exists to help you manage this workflow. 55 | 56 | When you create a branch in your project, you're creating an environment where you can try out new ideas. Changes you make on a branch don't affect the `master` branch, so you're free to experiment and commit changes, safe in the knowledge that your branch won't be merged until it's ready to be reviewed by someone you're collaborating with. 57 | 58 | ###ProTip 59 | 60 | Branching is a core concept in Git, and the entire GitHub Flow is based upon it. There's only one rule: anything in the `master` branch is always deployable. 61 | 62 | Because of this, it's extremely important that your new branch is created off of `master` when working on a feature or a fix. Your branch name should be descriptive (e.g., `refactor-authentication`, `user-content-cache-key`, `make-retina-avatars`), so that others can see what is being worked on. 63 | 64 | ## Add commits 65 | Once your branch has been created, it's time to start making changes. Whenever you add, edit, or delete a file, you're making a commit, and adding them to your branch. This process of adding commits keeps track of your progress as you work on a feature branch. 66 | 67 | Commits also create a transparent history of your work that others can follow to understand what you've done and why. Each commit has an associated commit message, which is a description explaining why a particular change was made. Furthermore, each commit is considered a separate unit of change. This lets you roll back changes if a bug is found, or if you decide to head in a different direction. 68 | 69 | ###ProTip 70 | 71 | Commit messages are important, especially since Git tracks your changes and then displays them as commits once they're pushed to the server. By writing clear commit messages, you can make it easier for other people to follow along and provide feedback. 72 | 73 | ## Open a pull request 74 | 75 | Pull Requests initiate discussion about your commits. Because they're tightly integrated with the underlying Git repository, anyone can see exactly what changes would be merged if they accept your request. 76 | 77 | You can open a Pull Request at any point during the development process: when you have little or no code but want to share some screenshots or general ideas, when you're stuck and need help or advice, or when you're ready for someone to review your work. By using GitHub's @mention system in your Pull Request message, you can ask for feedback from specific people or teams, whether they're down the hall or ten time zones away. 78 | 79 | ###ProTip 80 | 81 | Pull Requests are useful for contributing to open source projects and for managing changes to shared repositories. If you're using a Fork & Pull Model, Pull Requests provide a way to notify project maintainers about the changes you'd like them to consider. If you're using a Shared Repository Model, Pull Requests help start code review and conversation about proposed changes before they're merged into the `master` branch. 82 | 83 | ## Discuss and review your code 84 | Once a Pull Request has been opened, the person or team reviewing your changes may have questions or comments. Perhaps the coding style doesn't match project guidelines, the change is missing unit tests, or maybe everything looks great and props are in order. Pull Requests are designed to encourage and capture this type of conversation. 85 | 86 | You can also continue to push to your branch in light of discussion and feedback about your commits. If someone comments that you forgot to do something or if there is a bug in the code, you can fix it in your branch and push up the change. GitHub will show your new commits and any additional feedback you may receive in the unified Pull Request view. 87 | 88 | ###ProTip 89 | 90 | Pull Request comments are written in Markdown, so you can embed images and emoji, use pre-formatted text blocks, and other lightweight formatting. 91 | 92 | ## Merge to `master` 93 | 94 | Once your PR has passed any the integration tests and received approval to merge, it is time to merge your code into the `master` branch. 95 | 96 | Once merged, Pull Requests preserve a record of the historical changes to your code. Because they're searchable, they let anyone go back in time to understand why and how a decision was made. 97 | 98 | ###ProTip 99 | 100 | By incorporating certain keywords into the text of your Pull Request, you can associate issues with code. When your Pull Request is merged, the related issues are also closed. For example, entering the phrase Closes #32 would close issue number 32 in the repository. For more information, check out our help article. 101 | 102 | 103 | 104 | 105 | 106 | ## Build Tasks 107 | 108 | Command | Description 109 | :------ | :---------- 110 |
npm run dev
| Run project in development mode (verify code, and re-verify when code is changed) 111 |
npm start
| Alias for `npm run dev` task 112 | 113 | 114 | 115 | 116 | 117 | 118 | ## Test Tasks 119 | 120 | Command | Description 121 | :------ | :---------- 122 |
npm test
| Alias for `npm run test:unit` task 123 |
npm run test:coverage
| Run instrumented unit tests then verify coverage meets defined thresholds
  • Returns non-zero exit code when coverage does not meet thresholds (as defined in istanbul.js)
124 |
npm run test:unit
| Run unit tests whenever JS source or tests change
  • Uses Mocha
  • Code coverage
  • Runs continuously (best to run in a separate window)
125 |
npm run test:unit:once
| Run unit tests once
  • Uses Mocha
  • Code coverage
126 | 127 | 128 | 129 | 130 | 131 | 132 | ## Verification (Linting) Tasks 133 | 134 | Command | Description 135 | :------ | :---------- 136 |
npm run verify
| Verify code style and syntax
  • Verifies source *and test code* aginst customisable rules (unlike Webpack loaders)
137 |
npm run verify:js
| Verify Javascript code style and syntax 138 |
npm run verify:js:fix
| Verify Javascript code style and syntax and fix any errors that can be fixed automatically 139 |
npm run verify:js:watch
| Verify Javascript code style and syntax and watch files for changes 140 |
npm run verify:watch
| Runs verify task whenever JS or CSS code is changed 141 | 142 | 143 | 144 | 145 | 146 | 147 | ## Commit Tasks 148 | 149 | Command | Description 150 | :------ | :---------- 151 |
git status
| Lists the current branch and the status of changed files 152 |
git log
| Displays the commit log (press Q to quit viewing) 153 |
git add .
| Stages all modified & untracked files, ready to be committed 154 |
git cz
| Commit changes to local repository using Commitizen.
  • Asks questions about the change to generate a valid conventional commit message
  • Can be customised by modifying [config/release/commitMessageConfig.js](config/release/commitMessageConfig.js)
155 |
git push
| Push local repository changes to remote repository 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | ## Release Tasks 168 | 169 | Command | Description 170 | :------ | :---------- 171 |
npm run commitmsg
| Git commit message hook that validates the commit message conforms to your commit message conventions. 172 |
npm run pre-release
| Verify code, run unit tests, check test coverage, build software. This task is designed to be run before 173 | the `semantic-release` task. 174 |
  • Run `semantic-release-cli setup` once you have a remote repository. See https://github.com/semantic-release/cli for details.
  • Semantic-release integrates with Travis CI (or similar tools) to generate release notes 175 | for each release (which appears in the "Releases" section in GitHub) and 176 | publishes the package to NPM (when all the tests are successful) with a semantic version number. 177 |
178 |
npm run prepush
| Git pre-push hook that verifies code and checks unit test coverage meet minimum thresholds. 179 |
npm run upload-coverage
| Uploads code-coverage metrics to Coveralls.io
  • Setup - https://coveralls.zendesk.com/hc/en-us/articles/201347419-Coveralls-currently-supports
  • Define an environment variable called COVERALLS_REPO_TOKEN in your build environment with the repo token from https://coveralls.io/github//settings
  • In your CI configuration (e.g. `travis.yml`), call `npm run upload-coverage` if the build is successful.
180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | ## Changing build-tool configuration 188 | 189 | There are 3 ways you can change the build-tool configuration for this project: 190 | 191 | 1. BEST: Modify the Confit configuration file ([confit.yml](confit.yml)) by hand, then re-run `yo confit` and tell it to use the existing configuration. 192 | 1. OK: Re-run `yo confit` and provide new answers to the questions. **Confit will attempt to overwrite your existing configuration (it will prompt for confirmation), so make sure you have committed your code to a source control (e.g. git) first**. 193 | There are certain configuration settings which can **only** be specified by hand, in which case the first approach is still best. 194 | 1. RISKY: Modify the generated build-tool config by hand. Be aware that if you re-run `yo confit` it will attempt to overwrite your changes. So commit your changes to source control first. 195 | 196 | Additionally, the **currently-generated** configuration can be extended in the following ways: 197 | 198 | - The task configuration is defined in [package.json](package.json). It is possible to change the task definitions to add your own sub-tasks. 199 | You can also use the `pre...` and `post...` script-name prefixes to run commands before (pre) and after (post) the generated commands. 200 | 201 | - The `entryPoint.entryPoints` string in [confit.yml](confit.yml) is designed to be edited manually. It represents the starting-point(s) of the application (like a `main()` function). A NodeJS application has one entry point. E.g. `src/index.js` 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | -------------------------------------------------------------------------------- /spec/system.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | NOTE: This is the End-2-end test. 5 | - before every test: it creates a git repo in a temp directory 6 | - test cases set pre-conditions. Eg.: commit messages, add files 7 | - test cases run corp-semantic-release 8 | - test cases will verify the state of Git 9 | - after every test, temp dir and git repo are deleted. 10 | */ 11 | 12 | const expect = require('chai').expect; 13 | const shell = require('shelljs'); 14 | const fs = require('fs'); 15 | const writeFileSync = fs.writeFileSync; 16 | const temp = require('temp').track(); 17 | 18 | let counter = 0; // Used to create unique content to trigger git changes, to trigger semver bumps. 19 | 20 | 21 | describe('corp-semantic-release', function() { 22 | // temp dir created for testing. 23 | // New git repo will be created here 24 | let tempDir; 25 | 26 | beforeEach(function() { 27 | tempDir = temp.path({prefix: 'corp-sem-rel-'}); 28 | 29 | shell.config.silent = !process.env.npm_config_debug; 30 | shell.rm('-rf', tempDir); 31 | shell.mkdir(tempDir); 32 | shell.cp(__dirname + '/testData/package.json', tempDir); 33 | 34 | shell.cd(tempDir); 35 | 36 | shell.exec('git init'); 37 | 38 | // travis ci needs it 39 | shell.exec('git config user.email "leonardo@example.com"'); 40 | shell.exec('git config user.name "Leonardo C"'); 41 | }); 42 | 43 | afterEach(function() { 44 | shell.cd(__dirname); 45 | shell.rm('-rf', tempDir); 46 | tempDir = null; 47 | shell.cd('../'); // Navigate back to project root directory. If we don't do this, mocha-multi-reporters complains when it looks for its options file 48 | }); 49 | 50 | 51 | it('should not change anything in dry mode', function() { 52 | commitFeat(); 53 | const result = semanticRelease(`-d -v`); 54 | 55 | expect(result.stdout).to.include('YOU ARE RUNNING IN DRY RUN MODE'); 56 | 57 | // clean work directory 58 | const gitStatus = shell.exec('git status').stdout; 59 | expect(gitStatus).to.match(/nothing to commit, working (directory|tree) clean/); 60 | }); 61 | 62 | it('should remove add no-ci message when ci-skip parameter is false', function() { 63 | commitFeat(); 64 | const result = semanticRelease(`--ci-skip false`); 65 | expect(result.code).to.be.equal(0); 66 | 67 | const expectedVersion = '1.0.0'; 68 | 69 | // check Semantic Tag 70 | expectedGitTag(expectedVersion, undefined, false); 71 | }); 72 | 73 | it('should bump minor version, create CHANGELOG.md file and semantic tag correctly', function() { 74 | commitFeat(); 75 | const result = semanticRelease(); 76 | expect(result.code).to.be.equal(0); 77 | 78 | const expectedVersion = '1.0.0'; 79 | 80 | // check Semantic Tag 81 | expectedGitTag(expectedVersion); 82 | 83 | // Verify CHANGELOG.md 84 | const changelog = shell.exec('cat CHANGELOG.md').stdout; 85 | expect(changelog).to.include(`# ${expectedVersion} (${today})`); 86 | expect(changelog).to.include('### Features\n\n'); 87 | expect(changelog).to.include('my first feature'); 88 | 89 | expectedVersionInPackageJson(expectedVersion); 90 | }); 91 | 92 | it('should allow a prefix tag with the --tagPrefix option', function() { 93 | commitFeat(); 94 | const result = semanticRelease(`--tagPrefix beta-`); 95 | expect(result.code).to.be.equal(0); 96 | 97 | const expectedVersion = '1.0.0'; 98 | const expectedPrefix = 'beta-'; 99 | 100 | // check Semantic Tag 101 | expectedGitTag(expectedVersion, expectedPrefix); 102 | }); 103 | 104 | it('should run pre-commit script and pass the version number to the npm script', function() { 105 | commitFeat(); 106 | const result = semanticRelease(`-v --pre-commit set-version`); 107 | expect(result.code).to.be.equal(0); 108 | 109 | expect(result.stdout).to.include('this is my pre-commit script v1.0.0'); 110 | }); 111 | 112 | 113 | it('should run pre-commit script and pass the version number to a node script referenced by the npm script', function() { 114 | commitFeat(); 115 | shell.exec('rm package.json'); 116 | shell.cp(__dirname + '/testData/package_precommit.json', tempDir + '/package.json'); 117 | shell.cp(__dirname + '/testData/precommit.js', tempDir); 118 | 119 | const result = semanticRelease(`-v --pre-commit set-version`); 120 | expect(result.code).to.be.equal(0); 121 | 122 | expect(result.stdout).to.include('Inside precommit.js, version is v1.0.0'); 123 | }); 124 | 125 | 126 | it('should bump Major version due to Breaking Change and append contents to CHANGELOG.md', function() { 127 | // pre-conditions 128 | shell.cp(__dirname + '/testData/CHANGELOG.md', tempDir); 129 | commitFeat(); 130 | let result = semanticRelease(); 131 | expect(result.code).to.be.equal(0); 132 | 133 | const expectedVersion = '2.0.0'; 134 | 135 | // actions 136 | commitFixWithBreakingChange(); 137 | result = semanticRelease(); 138 | expect(result.code).to.be.equal(0); 139 | 140 | // verify 141 | const changelog = shell.exec('cat CHANGELOG.md').stdout; 142 | 143 | expect(changelog).to.include('### BREAKING CHANGES\n\n* This should bump major'); 144 | expect(changelog).to.include(`# [2.0.0](https://any.git.host/owner-name/repo-name/compare/v1.0.0...v${expectedVersion}) (${today})`); 145 | expect(changelog).to.match(/\* issue in the app \(\[[a-z0-9]{7}\]\(.*\)/); 146 | 147 | expectedVersionInPackageJson(expectedVersion); 148 | }); 149 | 150 | 151 | it('should generate a changelog for BitBucket with link references', function() { 152 | // pre-conditions 153 | shell.cp(__dirname + '/testData/CHANGELOG.md', tempDir); 154 | commitFeat(); 155 | let result = semanticRelease(`--changelogpreset angular-bitbucket`); 156 | expect(result.code).to.be.equal(0); 157 | 158 | const expectedVersion = '2.0.0'; 159 | 160 | // actions 161 | commitFixWithBreakingChange(); 162 | result = semanticRelease(`--changelogpreset angular-bitbucket`); 163 | expect(result.code).to.be.equal(0); 164 | 165 | // verify 166 | const changelog = shell.exec('cat CHANGELOG.md').stdout; 167 | 168 | expect(changelog).to.include('### BREAKING CHANGES\n\n* This should bump major'); 169 | expect(changelog).to.include(`# [2.0.0](https://any.git.host/projects/owner-name/repos/repo-name/compare/diff?` + 170 | `targetBranch=refs%2Ftags%2Fv1.0.0&sourceBranch=refs%2Ftags%2Fv${expectedVersion}) (${today})`); 171 | expect(changelog).to.match(/\* issue in the app \(\[[a-z0-9]{7}\]\(.*\)/); 172 | 173 | expectedVersionInPackageJson(expectedVersion); 174 | }); 175 | 176 | 177 | it('should detect release is not necessary', function() { 178 | commitNonReleaseTypes(); 179 | const result = semanticRelease(`-v`); 180 | expect(result.code).to.be.equal(0); 181 | 182 | expect(result.stdout).to.include('Release is not necessary at this point'); 183 | 184 | // clean work directory 185 | const gitStatus = shell.exec('git status').stdout; 186 | expect(gitStatus).to.match(/nothing to commit, working (directory|tree) clean/); 187 | }); 188 | 189 | 190 | it('should NOT make any change when we run multiple times and there are no relevant commits', function() { 191 | commitWithMessage('initial commit'); 192 | let result = semanticRelease(); 193 | expect(result.code).to.be.equal(0); 194 | 195 | const expectedVersion = '0.0.1'; 196 | 197 | const gitStatus = shell.exec('git status').stdout; 198 | expect(gitStatus).to.match(/nothing to commit, working (directory|tree) clean/); 199 | 200 | // no changes expected, no tags expected 201 | const gitTag = shell.exec('git tag | cat').stdout; 202 | expect(gitTag).to.equal(''); 203 | expectedVersionInPackageJson(expectedVersion); 204 | 205 | 206 | // Then when I run again... 207 | result = semanticRelease(); 208 | expect(result.code).to.be.equal(0); 209 | 210 | expectedVersionInPackageJson(expectedVersion); 211 | expect(gitTag).to.equal(''); 212 | expectedVersionInPackageJson(expectedVersion); 213 | }); 214 | 215 | 216 | it('should NOT make any change when we run multiple times and after a first minor release', function() { 217 | let gitTag; 218 | commitWithMessage('feat(accounts): commit 1'); 219 | commitFixWithMessage('fix(exampleScope): add extra config'); 220 | 221 | let result = semanticRelease(); 222 | expect(result.code).to.be.equal(0); 223 | const expectedVersion = '1.0.0'; 224 | 225 | // version 1.0.0 expected 226 | gitTag = shell.exec('git tag | cat').stdout; 227 | expect(gitTag).to.equal(`v${expectedVersion}\n`); 228 | expectedVersionInPackageJson(expectedVersion); 229 | 230 | // then run again. The same version 1.0.0 expected 231 | result = semanticRelease(`-v`); 232 | expect(result.code).to.be.equal(0); 233 | gitTag = shell.exec('git tag | cat').stdout; 234 | expect(gitTag).to.equal(`v${expectedVersion}\n`); 235 | expectedVersionInPackageJson(expectedVersion); 236 | expect(result.stdout).to.include('Release is not necessary at this point'); 237 | 238 | // run once more. The same version 1.0.0 expected 239 | result = semanticRelease(`-v`); 240 | expect(result.code).to.be.equal(0); 241 | gitTag = shell.exec('git tag | cat').stdout; 242 | expect(gitTag).to.equal(`v${expectedVersion}\n`); 243 | expectedVersionInPackageJson(expectedVersion); 244 | expect(result.stdout).to.include('Release is not necessary at this point'); 245 | }); 246 | 247 | it('should only run if branch is master', function() { 248 | commitWithMessage('feat(accounts): commit 1'); 249 | shell.exec('git checkout -b other-branch'); 250 | 251 | const result = semanticRelease(`-v -d --post-success "echo foo"`); 252 | expect(result.code).to.be.equal(0); 253 | 254 | expect(result.stdout).not.to.include('Skipping post-success command'); 255 | expect(result.stdout).to.include('You can only release from the master branch. Use option --branch to specify branch name.'); 256 | 257 | shell.exec('git checkout master'); 258 | const resultMaster = semanticRelease(`-v -d --post-success "echo foo"`); 259 | expect(resultMaster.code).to.be.equal(0); 260 | expect(resultMaster.stdout).to.include('Skipping post-success command'); 261 | expect(resultMaster.stdout).to.include('>>> Your release branch is: master'); 262 | }); 263 | 264 | 265 | it('should inform user if package.json does not exist', function() { 266 | commitWithMessage('feat(accounts): commit 1'); 267 | shell.exec('rm package.json'); 268 | 269 | const result = semanticRelease(`-v -d`); 270 | expect(result.code).not.to.be.equal(0); 271 | 272 | expect(result.stdout).to.include('Cant find your package.json'); 273 | }); 274 | 275 | 276 | it('should inform user if name is not present in package.json', function() { 277 | commitWithMessage('feat(accounts): commit 1'); 278 | shell.exec('rm package.json'); 279 | shell.cp(__dirname + '/testData/package_noname.json', tempDir + '/package.json'); 280 | 281 | const result = semanticRelease(`-v -d`); 282 | expect(result.code).not.to.be.equal(0); 283 | 284 | expect(result.stdout).to.include('Minimum required fields in your package.json are name and version'); 285 | }); 286 | 287 | 288 | it('should generate a changelog for 1 release by default', function() { 289 | commitFeat(); 290 | let result = semanticRelease(); 291 | expect(result.code).to.be.equal(0); 292 | 293 | expectedGitTag('1.0.0'); 294 | 295 | // Verify CHANGELOG.md starts with '' 296 | let changelog = shell.exec('cat CHANGELOG.md').stdout; 297 | expect(changelog.indexOf('')).to.equal(0); 298 | 299 | // Now clear the contents of the changelog, add another feature and release. We should only see the new release in the changelog. 300 | shell.exec('echo > CHANGELOG.md'); 301 | commitFeat(); 302 | result = semanticRelease(); 303 | expect(result.code).to.be.equal(0); 304 | 305 | expectedGitTag('1.1.0'); 306 | 307 | changelog = shell.exec('cat CHANGELOG.md').stdout; 308 | expect(changelog.indexOf('')).to.equal(0); 309 | 310 | // Old information is not regenerated, which means by default only 1 release is generated 311 | expect(changelog).not.to.include(''); 312 | }); 313 | 314 | 315 | it('should allow a changelog to be generated for all releases', function() { 316 | commitFeat(); 317 | let result = semanticRelease(); 318 | expect(result.code).to.be.equal(0); 319 | 320 | let changelog = shell.exec('cat CHANGELOG.md').stdout; 321 | expect(changelog.indexOf('')).to.equal(0); // First item in file 322 | 323 | // Now clear the contents of the changelog, add another feature and re-generate all releases 324 | shell.exec('echo > CHANGELOG.md'); 325 | changelog = shell.exec('cat CHANGELOG.md').stdout; 326 | expect(changelog).to.equal('\n'); 327 | 328 | commitFeat(); 329 | result = semanticRelease(`--releasecount 0`); // regenerate ALL releases (0 = all) 330 | expect(result.code).to.be.equal(0); 331 | 332 | expectedGitTag('1.1.0'); 333 | 334 | changelog = shell.exec('cat CHANGELOG.md').stdout; 335 | expect(changelog.indexOf('')).to.equal(0); // First item in file 336 | 337 | // Old information HAS been re-generated 338 | expect(changelog).to.include(''); 339 | }); 340 | 341 | 342 | it('should replace an existing changelog when re-generating it for all releases', function() { 343 | shell.exec('echo foo bar > CHANGELOG.md'); 344 | let changelog = shell.exec('cat CHANGELOG.md').stdout; 345 | expect(changelog).to.equal('foo bar\n'); 346 | 347 | // Don't clear the contents of the changelog, add another feature and re-generate all releases 348 | commitFeat(); 349 | let result = semanticRelease(); 350 | expect(result.code).to.be.equal(0); 351 | 352 | expectedGitTag('1.0.0'); 353 | 354 | changelog = shell.exec('cat CHANGELOG.md').stdout; 355 | expect(changelog).to.include(''); 356 | expect(changelog).to.include('foo bar'); 357 | 358 | commitFeat(); 359 | result = semanticRelease(`-r 0`); // regenerate ALL releases (0 = all) 360 | expect(result.code).to.be.equal(0); 361 | 362 | expectedGitTag('1.1.0'); 363 | 364 | changelog = shell.exec('cat CHANGELOG.md').stdout; 365 | expect(changelog).to.include(''); 366 | expect(changelog).to.include(''); 367 | expect(changelog).not.to.include('foo bar'); 368 | }); 369 | 370 | 371 | it('should generate the specified number of releases', function() { 372 | // Add two features, clear the log, add another feature then generate 2 releases (this one and the previous one) 373 | commitFeat(); 374 | let result = semanticRelease(); 375 | expect(result.code).to.be.equal(0); 376 | expectedGitTag('1.0.0'); 377 | 378 | commitFeat(); 379 | result = semanticRelease(); 380 | expect(result.code).to.be.equal(0); 381 | expectedGitTag('1.1.0'); 382 | 383 | let changelog = shell.exec('cat CHANGELOG.md').stdout; 384 | expect(changelog).to.include(''); 385 | expect(changelog).to.include(''); 386 | 387 | // Replace the log with junk, then append 2 releases 388 | shell.exec('echo foo bar > CHANGELOG.md'); 389 | changelog = shell.exec('cat CHANGELOG.md').stdout; 390 | expect(changelog).to.equal('foo bar\n'); 391 | 392 | commitFeat(); 393 | result = semanticRelease(`--releasecount 2`); 394 | expect(result.code).to.be.equal(0); 395 | expectedGitTag('1.2.0'); 396 | 397 | changelog = shell.exec('cat CHANGELOG.md').stdout; 398 | expect(changelog).to.include(''); 399 | expect(changelog).to.include(''); 400 | expect(changelog).not.to.include(''); 401 | expect(changelog).to.include('foo bar'); 402 | }); 403 | 404 | 405 | it('should run post-success script when the git push command is successful', function() { 406 | commitFeat(); 407 | const result = semanticRelease(`--post-success "npm run do-publish" -v`); 408 | expect(result.code).to.be.equal(0); 409 | 410 | expect(result.stdout).to.include('just published via post-success script'); 411 | }); 412 | 413 | it('should not run post-success script when the git push command fails', function() { 414 | commitFeat(); 415 | const result = semanticRelease(`--mock-push 1 --post-success "npm run do-publish" -v`); 416 | expect(result.code).not.to.be.equal(0); 417 | 418 | expect(result.stdout).not.to.include('just published via post-success script'); 419 | }); 420 | 421 | it('should not run post-success script in dry mode', function() { 422 | commitFeat(); 423 | const result = semanticRelease(`--post-success "npm run do-publish" -d`); 424 | expect(result.code).to.be.equal(0); 425 | 426 | expect(result.stdout).to.include('Skipping post-success command'); 427 | expect(result.stdout).not.to.include('just published via post-success script'); 428 | }); 429 | 430 | it('should fail if post-success script fails', function() { 431 | commitFeat(); 432 | const result = semanticRelease(`--post-success "non-existing-command"`); 433 | 434 | expect(result.code).not.to.equal(0); 435 | }); 436 | 437 | 438 | // ####### Helpers ###### 439 | 440 | function semanticRelease(params) { 441 | return shell.exec(`node ${__dirname}/../src/index.js --mock-push 0 ${params || ''}`); 442 | } 443 | 444 | function commitFeat() { 445 | writeFileSync('feat.txt', counter++); // Produce a unique change to the file 446 | return commitWithMessage('feat: my first feature'); 447 | } 448 | 449 | function commitNonReleaseTypes() { 450 | writeFileSync('docs.txt', counter++); 451 | commitWithMessage('docs: commit 01'); 452 | 453 | writeFileSync('styles.txt', counter++); 454 | commitWithMessage('styles: commit 02'); 455 | 456 | writeFileSync('chore.txt', counter++); 457 | commitWithMessage('chore: commit 03'); 458 | } 459 | 460 | function commitFixWithMessage(msg) { 461 | writeFileSync('fix.txt', counter++); 462 | commitWithMessageMultiline(`-m "${msg}"`); 463 | } 464 | 465 | function commitFixWithBreakingChange() { 466 | writeFileSync('fix.txt', counter++); 467 | const msg = '-m "fix: issue in the app" -m "BREAKING CHANGE:" -m "This should bump major"'; 468 | 469 | commitWithMessageMultiline(msg); 470 | } 471 | 472 | function commitWithMessage(msg) { 473 | return shell.exec(`git add --all && git commit -m "${msg}"`).stdout; 474 | } 475 | 476 | function commitWithMessageMultiline(msg) { 477 | shell.exec(`git add --all && git commit ${msg}`); 478 | } 479 | 480 | const today = new Date().toISOString().substring(0, 10); 481 | 482 | 483 | function expectedGitTag(expectedVersion, expectedPrefix, ciSkip) { 484 | expectedPrefix = expectedPrefix || ''; 485 | ciSkip = ciSkip !== false; 486 | 487 | // check for new commit 488 | const gitLog = shell.exec('git log | cat').stdout; 489 | expect(gitLog).to.include(`chore(release): ${expectedPrefix}v${expectedVersion}` 490 | + (ciSkip ? ' [ci skip] ***NO_CI***' : '')); 491 | 492 | const gitTag = shell.exec('git tag | cat').stdout; 493 | expect(gitTag).to.include(expectedPrefix + 'v' + expectedVersion); 494 | } 495 | 496 | function expectedVersionInPackageJson(expectedVersion) { 497 | const newVersion = require(tempDir + '/package.json').version; 498 | expect(newVersion).to.equal(expectedVersion); 499 | } 500 | }); 501 | --------------------------------------------------------------------------------