├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── __snapshots__ ├── changelog-spec.js.snap-shot └── utils-spec.js.snap-shot ├── images └── comment.png ├── next-update-travis.sh ├── package.json └── src ├── github-post-release-spec.js ├── index.js ├── postinstall.js ├── utils-spec.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | progress=false 4 | 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: true 7 | node_js: 8 | - '6' 9 | before_script: 10 | - npm prune 11 | script: 12 | - git --version 13 | - git tag --sort version:refname || true 14 | - git tag 15 | - ./next-update-travis.sh 16 | - npm test 17 | after_success: 18 | - DEBUG=github-post-release npm run semantic-release 19 | branches: 20 | except: 21 | - /^v\d+\.\d+\.\d+$/ 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-post-release 2 | 3 | > Forms release changelog and posts a note to each referenced issues after semantic release publishes a new module version 4 | 5 | [![NPM][npm-icon] ][npm-url] 6 | 7 | [![Build status][ci-image] ][ci-url] 8 | [![semantic-release][semantic-image] ][semantic-url] 9 | [![js-standard-style][standard-image]][standard-url] 10 | [![next-update-travis badge][nut-badge]][nut-readme] 11 | 12 | ## Problem 13 | 14 | When publishing new version of your NPM package, it would be nice to comment 15 | on each GitHub issue referenced by the semantic commits, letting the user 16 | know that the fix / feature was published. 17 | 18 | This module works as a [semantic-release][sem] [generate notes][gen] plugin. 19 | It both comments on referenced issues and returns the changelog to be 20 | posted on GitHub. Each issue will get a comment like this 21 | ([example](https://github.com/bahmutov/github-post-release/issues/8#issuecomment-313786374)) 22 | 23 | ![Typical comment](images/comment.png) 24 | 25 | [sem]: https://github.com/semantic-release/semantic-release 26 | [gen]: https://github.com/semantic-release/semantic-release#generatenotes 27 | 28 | ## Install and use 29 | 30 | Requires [Node](https://nodejs.org/en/) version 6 or above. 31 | 32 | ```sh 33 | npm install --save-dev github-post-release 34 | ``` 35 | 36 | The `postinstall` script will automatically set this module to be the 37 | `generateNotes` plugin for the release. If you want you can do this manually: 38 | 39 | ```json 40 | { 41 | "release": { 42 | "generateNotes": "github-post-release" 43 | } 44 | } 45 | ``` 46 | 47 | ## Message types 48 | 49 | By default, the output message for issues will be 50 | "New version has been published to NPM ...". This might not match other 51 | scenarios when this plugin can be used. For example if you deploy a website 52 | and publish release notes to GitHub, you might like different text that does 53 | not mention NPM. There are two built-in message types: "npm", "deploy" and you 54 | can pick on to generate by configuring this plugin like this in `package.json` 55 | 56 | ```json 57 | { 58 | "release": { 59 | "generateNotes": { 60 | "path": "github-post-release", 61 | "type": "deploy" 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ## Debug 68 | 69 | To see more log messages, run this plugin with `DEBUG=github-post-release` 70 | flag. You can even demo the plugin locally (without actual GitHub updates) 71 | using `npm run demo`. 72 | 73 | ## Related 74 | 75 | It was inspired by [semantic-release-github-notifier][notifier], 76 | [release-notes-generator][notes-generator] and uses 77 | [conventional-changelog][conventional-changelog] to generate changelog text 78 | after commenting on issues. 79 | 80 | [notifier]: https://github.com/gitter-badger/semantic-release-github-notifier 81 | [notes-generator]: https://github.com/semantic-release/release-notes-generator/ 82 | [conventional-changelog]: https://github.com/conventional-changelog/conventional-changelog#readme 83 | 84 | ### Small print 85 | 86 | Author: Gleb Bahmutov <gleb.bahmutov@gmail.com> © 2017 87 | 88 | * [@bahmutov](https://twitter.com/bahmutov) 89 | * [glebbahmutov.com](https://glebbahmutov.com) 90 | * [blog](https://glebbahmutov.com/blog) 91 | 92 | License: MIT - do anything with the code, but don't blame me if it does not work. 93 | 94 | Support: if you find any problems with this module, email / tweet / 95 | [open issue](https://github.com/bahmutov/github-post-release/issues) on Github 96 | 97 | ## MIT License 98 | 99 | Copyright (c) 2017 Gleb Bahmutov <gleb.bahmutov@gmail.com> 100 | 101 | Permission is hereby granted, free of charge, to any person 102 | obtaining a copy of this software and associated documentation 103 | files (the "Software"), to deal in the Software without 104 | restriction, including without limitation the rights to use, 105 | copy, modify, merge, publish, distribute, sublicense, and/or sell 106 | copies of the Software, and to permit persons to whom the 107 | Software is furnished to do so, subject to the following 108 | conditions: 109 | 110 | The above copyright notice and this permission notice shall be 111 | included in all copies or substantial portions of the Software. 112 | 113 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 114 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 115 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 116 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 117 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 118 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 119 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 120 | OTHER DEALINGS IN THE SOFTWARE. 121 | 122 | [npm-icon]: https://nodei.co/npm/github-post-release.svg?downloads=true 123 | [npm-url]: https://npmjs.org/package/github-post-release 124 | [ci-image]: https://travis-ci.org/bahmutov/github-post-release.svg?branch=master 125 | [ci-url]: https://travis-ci.org/bahmutov/github-post-release 126 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 127 | [semantic-url]: https://github.com/semantic-release/semantic-release 128 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg 129 | [standard-url]: http://standardjs.com/ 130 | [nut-badge]: https://img.shields.io/badge/next--update--travis-weekly-green.svg 131 | [nut-readme]: https://github.com/bahmutov/next-update-travis#readme 132 | -------------------------------------------------------------------------------- /__snapshots__/changelog-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['forms changelog 1'] = "## New features 👍\n### streams\n* output stdout and stderr, fixes #1, #2 (aaaabbbbccccddddeeeeffff1111222233334444)\n\n## Bug fixes ✅\n### doc\n* updated documentation (aaaabbbbccccddddeeeeffff1111222233334444)\n\n" 2 | 3 | exports['version and public commits 1'] = "\n# 1.0.2 (2000-04-20)\n## New features 👍\n### streams\n* output stdout and stderr, fixes #1, #2 (aaaabbbbccccddddeeeeffff1111222233334444)\n\n## Bug fixes ✅\n### doc\n* updated documentation (aaaabbbbccccddddeeeeffff1111222233334444)\n\n" 4 | 5 | exports['groups 1'] = { 6 | "feat": [ 7 | { 8 | "firstLine": "feat(streams): output stdout and stderr, fixes #1, #2", 9 | "type": "feat", 10 | "scope": "streams", 11 | "subject": "output stdout and stderr, fixes #1, #2", 12 | "id": "aaaabbbbccccddddeeeeffff1111222233334444" 13 | } 14 | ], 15 | "fix": [ 16 | { 17 | "firstLine": "fix(doc): updated documentation", 18 | "type": "fix", 19 | "scope": "doc", 20 | "subject": "updated documentation", 21 | "id": "aaaabbbbccccddddeeeeffff1111222233334444" 22 | } 23 | ] 24 | } 25 | 26 | -------------------------------------------------------------------------------- /__snapshots__/utils-spec.js.snap-shot: -------------------------------------------------------------------------------- 1 | exports['finds fixed issue in 1 commit 1'] = [ 2 | 2 3 | ] 4 | 5 | exports['finds several closed issues 1'] = [ 6 | 2, 7 | 4, 8 | 3 9 | ] 10 | 11 | exports['forms url 1'] = "https://github.com/bahmutov/github-post-release/releases/tag/v1.4.0" 12 | 13 | exports['forms release text 1'] = "github-post-release/releases/tag/v1.4.0" 14 | 15 | exports['forms full message 1'] = "Version `1.4.0` has been published to NPM. The full release note can be found at [repo/releases/tag/v1.4.0](https://github.com/user/repo/releases/tag/v1.4.0).\n\n**Tip:** safely upgrade dependency my-module-name in your project using [next-update](https://github.com/bahmutov/next-update)" 16 | 17 | exports['forms full publish message 1'] = "Version `1.4.0` has been published to NPM. The full release note can be found at [repo/releases/tag/v1.4.0](https://github.com/user/repo/releases/tag/v1.4.0).\n\n**Tip:** safely upgrade dependency my-module-name in your project using [next-update](https://github.com/bahmutov/next-update)" 18 | 19 | exports['forms full deploy message 1'] = "Version `1.4.0` has been deployed. The full release note can be found at [repo/releases/tag/v1.4.0](https://github.com/user/repo/releases/tag/v1.4.0)." 20 | 21 | -------------------------------------------------------------------------------- /images/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/github-post-release/d5f0623d0edeaf756c4075e61641f621c545b13e/images/comment.png -------------------------------------------------------------------------------- /next-update-travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then 6 | if [ "$GH_TOKEN" = "" ]; then 7 | echo "" 8 | echo "⛔️ Cannot find environment variable GH_TOKEN ⛔️" 9 | echo "Please set it up for this script to be able" 10 | echo "to push results to GitHub" 11 | echo "ℹ️ The best way is to use semantic-release to set it up" 12 | echo "" 13 | echo " https://github.com/semantic-release/semantic-release" 14 | echo "" 15 | echo "npm i -g semantic-release-cli" 16 | echo "semantic-release-cli setup" 17 | echo "" 18 | exit 1 19 | fi 20 | 21 | echo "Upgrading dependencies using next-update" 22 | npm i -g next-update 23 | 24 | # you can edit options to allow only some updates 25 | # --allow major | minor | patch 26 | # --latest true | false 27 | # see all options by installing next-update 28 | # and running next-update -h 29 | next-update --allow minor --latest false 30 | 31 | git status 32 | # if package.json is modified we have 33 | # new upgrades 34 | if git diff --name-only | grep package.json > /dev/null; then 35 | echo "There are new versions of dependencies 💪" 36 | git add package.json 37 | echo "----------- package.json diff -------------" 38 | git diff --staged 39 | echo "-------------------------------------------" 40 | git config --global user.email "next-update@ci.com" 41 | git config --global user.name "next-update" 42 | git commit -m "chore(deps): upgrade dependencies using next-update" 43 | # push back to GitHub using token 44 | git remote remove origin 45 | # TODO read origin from package.json 46 | # or use github api module github 47 | # like in https://github.com/semantic-release/semantic-release/blob/caribou/src/post.js 48 | git remote add origin https://next-update:$GH_TOKEN@github.com/bahmutov/github-post-release.git 49 | git push origin HEAD:master 50 | else 51 | echo "No new versions found ✋" 52 | fi 53 | else 54 | echo "Not a cron job, normal test" 55 | fi 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-post-release", 3 | "description": "Forms release changelog and posts a note to each referenced issues after semantic release publishes a new module version", 4 | "version": "0.0.0-development", 5 | "author": "Gleb Bahmutov ", 6 | "bugs": "https://github.com/bahmutov/github-post-release/issues", 7 | "config": { 8 | "pre-git": { 9 | "commit-msg": "simple", 10 | "pre-commit": [ 11 | "npm prune", 12 | "npm run deps", 13 | "npm test", 14 | "git add src/*.js", 15 | "npm run ban" 16 | ], 17 | "pre-push": [ 18 | "npm run secure", 19 | "npm run license", 20 | "npm run ban -- --all", 21 | "npm run size" 22 | ], 23 | "post-commit": [], 24 | "post-merge": [] 25 | } 26 | }, 27 | "engines": { 28 | "node": ">=4" 29 | }, 30 | "files": [ 31 | "src/*.js", 32 | "!src/*-spec.js" 33 | ], 34 | "homepage": "https://github.com/bahmutov/github-post-release#readme", 35 | "keywords": [ 36 | "changelog", 37 | "github", 38 | "plugin", 39 | "semantic", 40 | "semantic-release" 41 | ], 42 | "license": "MIT", 43 | "main": "src/", 44 | "publishConfig": { 45 | "registry": "http://registry.npmjs.org/" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/bahmutov/github-post-release.git" 50 | }, 51 | "release": { 52 | "generateNotes": { 53 | "path": ".", 54 | "type": "npm" 55 | }, 56 | "analyzeCommits": "simple-commit-message" 57 | }, 58 | "scripts": { 59 | "ban": "ban", 60 | "deps": "deps-ok && dependency-check --no-dev . --entry src/postinstall.js", 61 | "issues": "git-issues", 62 | "license": "license-checker --production --onlyunknown --csv", 63 | "pretty": "prettier-standard 'src/*.js'", 64 | "prelint": "npm run pretty", 65 | "lint": "standard --verbose --fix src/*.js", 66 | "pretest": "npm run lint", 67 | "secure": "nsp check", 68 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";", 69 | "test": "npm run unit", 70 | "unit": "mocha src/*-spec.js", 71 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 72 | "demo": "DEBUG=github-post-release node .", 73 | "postinstall": "node src/postinstall.js" 74 | }, 75 | "devDependencies": { 76 | "ban-sensitive-files": "1.9.0", 77 | "dependency-check": "2.9.1", 78 | "deps-ok": "1.2.1", 79 | "git-issues": "1.3.1", 80 | "license-checker": "13.1.0", 81 | "mocha": "3.5.3", 82 | "mockdate": "2.0.2", 83 | "next-update-travis": "1.7.1", 84 | "nsp": "2.8.1", 85 | "pre-git": "3.15.3", 86 | "prettier-standard": "6.0.0", 87 | "semantic-release": "^6.3.6", 88 | "snap-shot": "2.17.0", 89 | "standard": "10.0.3" 90 | }, 91 | "dependencies": { 92 | "am-i-a-dependency": "1.1.2", 93 | "bluebird": "3.5.1", 94 | "check-more-types": "2.24.0", 95 | "commit-closes": "1.0.1", 96 | "common-tags": "1.4.0", 97 | "debug": "3.1.0", 98 | "github": "9.3.1", 99 | "github-url-from-git": "1.5.0", 100 | "lazy-ass": "1.6.0", 101 | "new-public-commits": "1.3.1", 102 | "parse-github-repo-url": "1.4.1", 103 | "pluralize": "6.0.0", 104 | "ramda": "0.25.0", 105 | "simple-changelog": "1.1.3", 106 | "simple-commit-message": "3.3.2" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/github-post-release-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const la = require('lazy-ass') 4 | const is = require('check-more-types') 5 | const join = require('path').join 6 | const R = require('ramda') 7 | 8 | /* global describe, it */ 9 | const githubPostRelease = require('.') 10 | const pkg = require(join(__dirname, '..', 'package.json')) 11 | 12 | describe('github-post-release', () => { 13 | it('is a function', () => { 14 | la(is.fn(githubPostRelease)) 15 | }) 16 | 17 | it('forms changelog', done => { 18 | const pluginConfig = {} 19 | const json = R.clone(pkg) 20 | json.version = '1000.0.0' 21 | const config = { 22 | pkg: json, 23 | debug: true 24 | } 25 | 26 | githubPostRelease(pluginConfig, config, (err, log) => { 27 | la(!err, 'there was an error', err) 28 | la(is.unemptyString(log), 'missing log', log) 29 | console.log(log) 30 | done() 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const bluebird = require('bluebird') 4 | const url = require('url') 5 | const GitHubApi = require('github') 6 | const parseGithubUrl = require('parse-github-repo-url') 7 | const debug = require('debug')('github-post-release') 8 | const newPublicCommits = require('new-public-commits').newPublicCommits 9 | const R = require('ramda') 10 | const pluralize = require('pluralize') 11 | const join = require('path').join 12 | const utils = require('./utils') 13 | const formChangelog = require('simple-changelog') 14 | const la = require('lazy-ass') 15 | const is = require('check-more-types') 16 | 17 | // :: -> [issue numbers] 18 | function getClosedIssues () { 19 | return newPublicCommits().then(utils.commitsToIssues) 20 | } 21 | 22 | function getGitHub (githubUrl, token) { 23 | if (!token) { 24 | throw new Error('Missing gh token') 25 | } 26 | const githubConfig = githubUrl ? url.parse(githubUrl) : {} 27 | 28 | let protocol = (githubConfig.protocol || '').split(':')[0] || null 29 | if (protocol === 'git+https') { 30 | debug('switching github protocol from %s to https', protocol) 31 | protocol = 'https' 32 | } 33 | 34 | let host = githubConfig.hostname 35 | if (host === 'github.com') { 36 | // https://github.com/mikedeboer/node-github/issues/544 37 | debug('using api.github.com host') 38 | host = 'api.github.com' 39 | } 40 | 41 | const config = { 42 | version: '3.0.0', 43 | port: githubConfig.port, 44 | protocol, 45 | host 46 | } 47 | debug('github config') 48 | debug(config) 49 | 50 | const github = new GitHubApi(config) 51 | 52 | github.authenticate({ 53 | type: 'token', 54 | token: token 55 | }) 56 | 57 | const createComment = bluebird.promisify(github.issues.createComment) 58 | return createComment 59 | } 60 | 61 | function getGitHubToken () { 62 | return process.env.GH_TOKEN 63 | } 64 | 65 | function commentOnIssues (repoUrl, message, debugMode, issues) { 66 | if (!issues) { 67 | return Promise.resolve() 68 | } 69 | if (R.isEmpty(issues)) { 70 | debug('no issues to comment on') 71 | return Promise.resolve() 72 | } 73 | 74 | const createComment = debugMode 75 | ? () => Promise.resolve() 76 | : getGitHub(repoUrl, getGitHubToken()) 77 | const parsed = parseGithubUrl(repoUrl) 78 | const owner = parsed[0] 79 | const repo = parsed[1] 80 | debug( 81 | 'commenting on %d %s: %j', 82 | issues.length, 83 | pluralize('issues', issues.length), 84 | issues 85 | ) 86 | 87 | const onPosted = number => () => { 88 | console.log('posted comment for issue', number) 89 | } 90 | 91 | const onFailed = number => err => { 92 | console.error('Could not comment on issue', number) 93 | console.error(err) 94 | } 95 | 96 | const commentPromises = issues.map(number => 97 | createComment({ owner, repo, number, body: message }) 98 | .then(onPosted(number)) 99 | .catch(onFailed(number)) 100 | ) 101 | 102 | return bluebird.all(commentPromises) 103 | } 104 | 105 | // should call "callback" function with (err, changelog) 106 | function githubPostRelease (pluginConfig, config, callback) { 107 | // debug('custom plugin config', pluginConfig) 108 | // debug('config parameter', config) 109 | 110 | // normalize message type 111 | let messageType = pluginConfig.type || 'npm' 112 | if (messageType === 'publish') { 113 | messageType = 'npm' 114 | } 115 | la( 116 | is.unemptyString(messageType), 117 | 'missing message type to form', 118 | pluginConfig 119 | ) 120 | debug('message type "%s"', messageType) 121 | 122 | const pkg = config.pkg 123 | const repoUrl = pkg.repository.url 124 | const parsedRepo = parseGithubUrl(repoUrl) 125 | 126 | debug('published version %s', pkg.version) 127 | debug('repo url %s', repoUrl) 128 | debug('debug mode?', config.debug) 129 | 130 | const onSuccess = changelog => { 131 | debug('✅ all done, with issue message:') 132 | debug(message) 133 | debug('changelog:') 134 | debug(changelog) 135 | callback(null, changelog) 136 | } 137 | 138 | const onFailure = err => { 139 | console.error('🔥 failed with error') 140 | console.error(err) 141 | callback(err) 142 | } 143 | 144 | const commentingFailed = err => { 145 | console.error('😟 commenting on related issues failed') 146 | console.error(err) 147 | } 148 | 149 | const generateChangeLog = () => { 150 | debug('generate changelog for release version %s', pkg.version) 151 | return formChangelog(pkg.version) 152 | } 153 | 154 | const owner = parsedRepo[0] 155 | const repo = parsedRepo[1] 156 | const message = utils.formMessage( 157 | messageType, 158 | owner, 159 | repo, 160 | pkg.name, 161 | pkg.version 162 | ) 163 | 164 | getClosedIssues() 165 | .then(R.partial(commentOnIssues, [repoUrl, message, config.debug])) 166 | .catch(commentingFailed) 167 | .then(generateChangeLog) 168 | .then(onSuccess, onFailure) 169 | } 170 | 171 | module.exports = githubPostRelease 172 | 173 | if (!module.parent) { 174 | console.log('demo run') 175 | const pkg = require(join(__dirname, '..', 'package.json')) 176 | const generateNotes = R.path(['release', 'generateNotes'])(pkg) 177 | const config = R.is(Object, generateNotes) ? generateNotes : {} 178 | debug('config') 179 | debug(config) 180 | 181 | githubPostRelease(config, { pkg: pkg }, (err, changelog) => { 182 | console.log('finished with') 183 | console.log('err?', err) 184 | console.log('changelog\n' + changelog) 185 | }) 186 | } 187 | -------------------------------------------------------------------------------- /src/postinstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const debug = require('debug')('github-post-release') 6 | const amIaDependency = require('am-i-a-dependency') 7 | const isForced = process.argv.some(a => a === '--force') 8 | 9 | if (!amIaDependency() && !isForced) { 10 | // top level install (we are running `npm i` in this project) 11 | debug('we are installing own dependencies') 12 | process.exit(0) 13 | } 14 | 15 | debug('installing this module as a dependency') 16 | 17 | const path = require('path') 18 | const fs = require('fs') 19 | 20 | function clientPackageJsonFilename () { 21 | return path.join(process.cwd(), '..', '..', 'package.json') 22 | } 23 | 24 | function alreadyInstalled () { 25 | const filename = clientPackageJsonFilename() 26 | const pkg = require(filename) 27 | if (!pkg.release) { 28 | return false 29 | } 30 | if (!pkg.release.generateNotes) { 31 | return false 32 | } 33 | return true 34 | } 35 | 36 | function addPlugin () { 37 | const filename = clientPackageJsonFilename() 38 | const pkg = require(filename) 39 | if (!pkg.release) { 40 | pkg.release = {} 41 | } 42 | pkg.release.generateNotes = 'github-post-release' 43 | const text = JSON.stringify(pkg, null, 2) + '\n' 44 | fs.writeFileSync(filename, text, 'utf8') 45 | console.log('✅ set generate notes plugin in', filename) 46 | } 47 | 48 | if (alreadyInstalled()) { 49 | debug('plugin generateNotes already set') 50 | process.exit(0) 51 | } 52 | 53 | console.log('⚠️ Installing release generateNotes plugin github-post-release') 54 | addPlugin() 55 | -------------------------------------------------------------------------------- /src/utils-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global describe, it */ 4 | const snapshot = require('snap-shot') 5 | 6 | describe('utils', () => { 7 | describe('utils.', () => { 8 | const utils = require('./utils') 9 | 10 | it('forms url', () => { 11 | snapshot( 12 | utils.formReleaseUrl('bahmutov', 'github-post-release', 'v1.4.0') 13 | ) 14 | }) 15 | }) 16 | 17 | describe('formReleaseText', () => { 18 | const utils = require('./utils') 19 | 20 | it('forms release text', () => { 21 | snapshot(utils.formReleaseText('github-post-release', 'v1.4.0')) 22 | }) 23 | }) 24 | 25 | describe('formMessage', () => { 26 | const utils = require('./utils') 27 | 28 | it('forms full message', () => { 29 | snapshot( 30 | utils.formMessage('npm', 'user', 'repo', 'my-module-name', '1.4.0') 31 | ) 32 | }) 33 | 34 | it('forms full publish message', () => { 35 | snapshot( 36 | utils.formMessage('publish', 'user', 'repo', 'my-module-name', '1.4.0') 37 | ) 38 | }) 39 | 40 | it('forms full deploy message', () => { 41 | snapshot( 42 | utils.formMessage('deploy', 'user', 'repo', 'my-module-name', '1.4.0') 43 | ) 44 | }) 45 | }) 46 | 47 | describe('commitsToIssues', () => { 48 | const utils = require('./utils') 49 | 50 | it('finds fixed issue in 1 commit', () => { 51 | const commits = [ 52 | { 53 | id: 'b0be94741486803ab26bb0397d8992e45bed1acd', 54 | message: 'fix(ggit): bring ggit fix #2', 55 | body: '' 56 | } 57 | ] 58 | const issues = utils.commitsToIssues(commits) 59 | snapshot(issues) 60 | }) 61 | 62 | it('finds several closed issues', () => { 63 | const commits = [ 64 | { 65 | id: 'b0be94741486803ab26bb0397d8992e45bed1acd', 66 | message: 'fix(ggit): bring ggit fix #2', 67 | body: 'and resolves #4' 68 | }, 69 | { 70 | id: 'b0be94741486803ab26b50397d8992e45bed1acd', 71 | message: 'fix(ggit): subject', 72 | body: 'closed #2, #3' 73 | } 74 | ] 75 | const issues = utils.commitsToIssues(commits) 76 | snapshot(issues) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('github-post-release') 2 | const R = require('ramda') 3 | const commitCloses = require('commit-closes') 4 | const pluralize = require('pluralize') 5 | const tags = require('common-tags') 6 | const la = require('lazy-ass') 7 | const is = require('check-more-types') 8 | 9 | function hasIssues (issues) { 10 | return issues.length > 0 11 | } 12 | 13 | function findIssues (commit) { 14 | return commitCloses(commit.message, commit.body) 15 | } 16 | 17 | function commitsToIssues (commits) { 18 | debug( 19 | 'have %d semantic %s', 20 | commits.length, 21 | pluralize('commit', commits.length) 22 | ) 23 | if (!commits.length) { 24 | return [] 25 | } 26 | debug(commits) 27 | 28 | const closedIssues = R.flatten(commits.map(findIssues).filter(hasIssues)) 29 | debug('semantic commits close the following issues') 30 | debug(closedIssues) 31 | const uniqueIssues = R.uniq(closedIssues) 32 | debug('unique closed issues', uniqueIssues) 33 | return uniqueIssues 34 | } 35 | 36 | function formReleaseUrl (user, repo, tag) { 37 | return `https://github.com/${user}/${repo}/releases/tag/${tag}` 38 | } 39 | 40 | function formReleaseText (repo, tag) { 41 | return `${repo}/releases/tag/${tag}` 42 | } 43 | 44 | function formNpmMessage (owner, repo, name, version) { 45 | la(arguments.length === 4, 'invalid arguments', arguments) 46 | 47 | const vTag = `v${version}` 48 | const releaseText = formReleaseText(repo, vTag) 49 | const releaseUrl = formReleaseUrl(owner, repo, vTag) 50 | const nextUpdateUrl = 'https://github.com/bahmutov/next-update' 51 | const message = tags.stripIndent` 52 | Version \`${version}\` has been published to NPM. The full release note can be found at [${releaseText}](${releaseUrl}). 53 | 54 | **Tip:** safely upgrade dependency ${name} in your project using [next-update](${nextUpdateUrl}) 55 | ` 56 | return message 57 | } 58 | 59 | function formDeployMessage (owner, repo, name, version) { 60 | la(arguments.length === 4, 'invalid arguments', arguments) 61 | 62 | const vTag = `v${version}` 63 | const releaseText = formReleaseText(repo, vTag) 64 | const releaseUrl = formReleaseUrl(owner, repo, vTag) 65 | const message = tags.stripIndent` 66 | Version \`${version}\` has been deployed. The full release note can be found at [${releaseText}](${releaseUrl}). 67 | ` 68 | return message 69 | } 70 | 71 | const isValidType = is.oneOf(['npm', 'publish', 'deploy']) 72 | 73 | function formMessage (type, owner, repo, name, version) { 74 | la(isValidType(type), 'invalid message type', type) 75 | const form = { 76 | npm: formNpmMessage, 77 | publish: formNpmMessage, 78 | deploy: formDeployMessage 79 | } 80 | const action = form[type] 81 | la(is.fn(action), 'cannot find action for type', type) 82 | return action(owner, repo, name, version) 83 | } 84 | 85 | module.exports = { 86 | commitsToIssues: commitsToIssues, 87 | formReleaseUrl: formReleaseUrl, 88 | formReleaseText: formReleaseText, 89 | formMessage: formMessage 90 | } 91 | --------------------------------------------------------------------------------