├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── bin └── changelog └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 (2017-11-15) 2 | 3 | * add compat for node 8 LTS 4 | 5 | # 0.2.0 (2015-08-12) 6 | 7 | * add support for --release [major, minor, patch] 8 | * don't automatically commit on --increment 9 | 10 | # 0.1.0 (2014-05-13) 11 | 12 | * initial 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Roman Shtylman and other contributors 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 | # changelog 2 | 3 | A tool for updating project changelogs and package.json files for new releases. 4 | 5 | ## Why? 6 | 7 | Having a changelog (or history) file in a project helps you communicate to your users (developers) which changes you find most important to a release. Obviously the commit history is always available, but is often filled with noise. The changelog is a noise free place to highlight these major changes in a more summarized and human readable form. 8 | 9 | Currently workflows do not put enough emphasis on the changelog. Developers will often make a series of commits and then just bump the version in package.json. By centering the versioning workflow around the changelog, I hope to encourage more modules to keep an updated history. 10 | 11 | ## Workflow 12 | 13 | ### install the changelog tool 14 | 15 | ```shell 16 | npm install -g defunctzombie/changelog 17 | ``` 18 | 19 | ### create an empty initial changelog 20 | 21 | ```shell 22 | changelog --init 23 | ``` 24 | 25 | A `HISTORY.md` file will be created with the following content 26 | 27 | ```md 28 | # UNRELEASED 29 | 30 | * initial 31 | ``` 32 | 33 | ### release a new version 34 | 35 | After you make some commits and add entries to the changelog, you can release a new version using the `--release` flag 36 | 37 | ```shell 38 | changelog --release 1.0.0 39 | ``` 40 | 41 | This will perform the following: 42 | * Update the first changelog `UNRELEASED` line to `# 1.0.0 (YYYY-MM-DD)`. 43 | * Set the version in `package.json` 44 | * `git commit v` 45 | * `git tag v` 46 | 47 | ### bump to UNRELEASED 48 | 49 | This will add a new `UNRELEASED` line to the start of the changelog. 50 | 51 | ```shell 52 | changelog --increment 53 | ``` 54 | 55 | ## changelog format 56 | 57 | ```md 58 | # UNRELEASED 59 | 60 | * summary item 61 | * summary item 62 | 63 | # 1.0.0 (YYYY-MM-DD) 64 | 65 | * summary item 66 | 67 | # 0.1.0 (YYYY-MM-DD) 68 | 69 | * summary item 70 | * initial 71 | ``` 72 | 73 | The reason `UNRELEASED` is used at the top of the changelog is to indicate a series of changes which have not yet been officially tagged. It may be that a particular change will case a minor or major version bump and so the version cannot be known until a release is ready. 74 | 75 | -------------------------------------------------------------------------------- /bin/changelog: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var colors = require('colors'); 6 | var program = require('commander'); 7 | var moment = require('moment'); 8 | var shell = require('shelljs'); 9 | var semver = require('semver'); 10 | 11 | program 12 | .option('--init', 'initialize a new changelog') 13 | .option('--release [version]', 'change latest UNRELEASED changes into VERSION and tag for release (supports specifying exact version or major, minor, patch)') 14 | .option('--increment', 'bump version to UNRELEASED') 15 | .parse(process.argv); 16 | 17 | var basedir = process.cwd(); 18 | 19 | // lowercase variations will be tried automatically 20 | var k_changelog_files = [ 21 | 'history.md', 22 | 'History.md', 23 | 'HISTORY.md', 24 | 'HISTORY.MD', 25 | 'changelog.md', 26 | 'Changelog.md', 27 | 'CHANGELOG.md', 28 | 'CHANGELOG.MD' 29 | ]; 30 | 31 | k_changelog_files.forEach(function(file) { 32 | k_changelog_files.push(file.toLowerCase()); 33 | }) 34 | 35 | if (program.init) { 36 | 37 | k_changelog_files.forEach(function(file) { 38 | if (fs.existsSync(path.join(basedir, file))) { 39 | console.error('Error: Cannot initialize a new Changelog. %s already exists', file); 40 | return process.exit(1); 41 | } 42 | }); 43 | 44 | var content = '# UNRELEASED\n\n * initial\n\n'; 45 | var changelog_file = path.join(basedir, 'HISTORY.md'); 46 | fs.writeFileSync(changelog_file, content, { encoding: 'utf-8' }); 47 | 48 | console.log('Initialized changelog file %s', changelog_file); 49 | return process.exit(0); 50 | } 51 | else if (program.release) { 52 | 53 | var new_version = program.release; 54 | 55 | var valid_release_versions = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease']; 56 | 57 | var files = fs.readdirSync(basedir); 58 | 59 | var changleog_file; 60 | k_changelog_files.some(function(filename) { 61 | var filepath = path.join(basedir, filename); 62 | if (fs.existsSync(filepath) && files.indexOf(filename) >= 0) { 63 | return changelog_file = filepath; 64 | } 65 | return false; 66 | }); 67 | 68 | if (!changelog_file) { 69 | console.error('no changelog file found'); 70 | return process.exit(1); 71 | } 72 | 73 | var pkg_filepath = path.join(basedir, 'package.json'); 74 | if (!fs.existsSync(pkg_filepath)) { 75 | console.error('no package.json file found'); 76 | return process.exit(1); 77 | } 78 | 79 | var content = fs.readFileSync(changelog_file).toString(); 80 | 81 | var lines = content.split('\n'); 82 | 83 | //TODO if new version is < any old version, then abort (TODO ask user to confirm override) 84 | 85 | // if first entry is not UNRELEASED then abort 86 | var first = lines.shift(); 87 | if (!first || !/UNRELEASED/.test(first)) { 88 | console.error('Cannot release. Already released.'); 89 | return process.exit(1); 90 | } 91 | 92 | // modify version in package.json 93 | // - first try to replace "version": "vvv" with new one 94 | // - second try to insert "version" field if there is no version field 95 | // - changes to package.json should be minimal diff impact 96 | // if cannot change... abort 97 | var pkg_content = fs.readFileSync(pkg_filepath, 'utf-8'); 98 | var pkg = JSON.parse(pkg_content); 99 | if (pkg.version) { 100 | // TODO check that version is newer 101 | } 102 | 103 | if (valid_release_versions.indexOf(new_version) >= 0) { 104 | if (!pkg.version) { 105 | console.error('cannot use releaes name if there is no version in package.json'); 106 | return process.exit(1); 107 | } 108 | new_version = semver.inc(pkg.version, new_version); 109 | } 110 | 111 | // new first entry with released version 112 | var date = moment(); 113 | first = '# ' + new_version + ' ' + date.format('(YYYY-MM-DD)'); 114 | lines.unshift(first); 115 | 116 | // write out new changelog 117 | var fd = fs.openSync(changelog_file, 'r+'); 118 | var buff = Buffer(lines.join('\n')); 119 | fs.writeSync(fd, buff, 0, buff.length, 0); 120 | fs.closeSync(fd); 121 | 122 | // write out new package file 123 | // TODO detect 2 or 4 spaces (or who cares?) 124 | pkg.version = new_version; 125 | var buff = Buffer(JSON.stringify(pkg, null, 2)); 126 | fs.writeFileSync(pkg_filepath, buff); 127 | 128 | // TODO if git commit fails, rollback versions 129 | 130 | // commit message and tag name 131 | var tag = 'v' + new_version; 132 | 133 | var cmd = 'git add ' + changelog_file; 134 | shell.exec(cmd); //, { silent: true }) 135 | 136 | var cmd = 'git add package.json'; 137 | shell.exec(cmd); //, { silent: true }) 138 | 139 | var cmd = 'git commit -m ' + tag; 140 | shell.exec(cmd); //, { silent: true }) 141 | 142 | var cmd = 'git tag ' + tag; 143 | shell.exec(cmd); 144 | 145 | console.log(('don\'t forget to run "git push && git push origin ' + tag + '"').yellow); 146 | 147 | return process.exit(0); 148 | } 149 | else if (program.increment) { 150 | // bump to UNRELEASED in chagnelog 151 | 152 | var files = fs.readdirSync(basedir); 153 | 154 | var changleog_file; 155 | k_changelog_files.some(function(filename) { 156 | var filepath = path.join(basedir, filename); 157 | if (fs.existsSync(filepath) && files.indexOf(filename) >= 0) { 158 | return changelog_file = filepath; 159 | } 160 | return false; 161 | }); 162 | 163 | if (!changelog_file) { 164 | console.error('no changelog file found'); 165 | return process.exit(1); 166 | } 167 | 168 | var content = fs.readFileSync(changelog_file).toString(); 169 | var lines = content.split('\n'); 170 | 171 | // check if already on unreleased 172 | var first = lines.shift(); 173 | if (!first || /UNRELEASED/.test(first)) { 174 | return process.exit(0); 175 | } 176 | 177 | lines.unshift(first); 178 | 179 | // prepend unreleased 180 | first = '# UNRELEASED'; 181 | lines.unshift(''); 182 | lines.unshift(first); 183 | 184 | // write out new changelog 185 | var fd = fs.openSync(changelog_file, 'r+'); 186 | var buff = Buffer(lines.join('\n')); 187 | fs.writeSync(fd, buff, 0, buff.length, 0); 188 | fs.closeSync(fd); 189 | 190 | // TODO option to commit just the history change? 191 | /* 192 | var cmd = 'git add ' + changelog_file; 193 | shell.exec(cmd); //, { silent: true }) 194 | 195 | // commit 196 | var cmd = 'git commit -m "now working on UNRELEASED" ' + changelog_file; 197 | shell.exec(cmd); //, { silent: true }) 198 | */ 199 | } 200 | 201 | // vim: ft=javascript 202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "changelog", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "colors": "0.6.2", 6 | "commander": "2.2.0", 7 | "moment": "2.6.0", 8 | "semver": "5.0.1", 9 | "shelljs": "0.3.0" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/defunctzombie/changelog.git" 14 | }, 15 | "bin": { 16 | "changelog": "./bin/changelog" 17 | }, 18 | "license": "MIT" 19 | } --------------------------------------------------------------------------------