From 11570c2c9a3467d977f9da8a5e43f163d9b699a4 Mon Sep 17 00:00:00 2001 From: Avi Sharvit Date: Mon, 31 Dec 2018 09:11:05 +0200 Subject: [PATCH] feat(semantic-release): adds option to install semantic-release --- other/roadmap.md | 2 - package.json | 3 +- readme.md | 1 + src/app/generators/github/index.js | 25 +-- .../github/templates/contributing.md | 21 ++- src/app/generators/project/index.js | 3 +- .../project/templates/_package.json | 34 +++- .../generators/project/templates/readme.md | 3 +- src/app/generators/travis/index.js | 56 ++++-- .../generators/travis/templates/_travis.yml | 22 ++- src/app/index.test.js | 58 ++++++ src/app/lib/__mocks__/github.js | 2 + src/app/lib/github.js | 25 +++ src/app/options.js | 15 +- src/app/prompter.js | 166 ++++++++++++++---- yarn.lock | 10 ++ 16 files changed, 370 insertions(+), 76 deletions(-) diff --git a/other/roadmap.md b/other/roadmap.md index 44e95d1..efe952c 100644 --- a/other/roadmap.md +++ b/other/roadmap.md @@ -6,8 +6,6 @@ We haven‘t filled this out yet though. Care to help? See [`contributing.md`](. ## Want to do - - Add option to install [semantic-release](https://github.com/semantic-release/semantic-release) - ## Might do ## Won‘t do diff --git a/package.json b/package.json index 0c470e8..c784e89 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "lodash.kebabcase": "^4.1.1", "make-dir": "^1.1.0", "npm-profile": "^4.0.1", - "opener": "^1.5.1", + "request-promise": "^4.2.2", + "uuid": "^3.3.2", "yeoman-generator": "^3.1.1" }, "devDependencies": { diff --git a/readme.md b/readme.md index 9f40d05..a0ef88d 100644 --- a/readme.md +++ b/readme.md @@ -26,6 +26,7 @@ - [Travis CI](https://travis-ci.org) configuration _(optional)_ - [Coveralls](http://coveralls.io) configuration _(optional)_ - Automatically deploy to [npm registry](https://www.npmjs.com) with [Travis CI](https://travis-ci.org) _(optional)_ +- Automates versioning releases with [semantic-release](https://github.com/semantic-release/semantic-release) and [commitizen](https://github.com/commitizen/cz-cli) _(optional)_ - Friendly for contributions using [Issue, Pull Request, and Contributing templates](https://github.com/blog/2111-issue-and-pull-request-templates) and some extras _(optional)_ ## Installation diff --git a/src/app/generators/github/index.js b/src/app/generators/github/index.js index cbec695..7899b28 100644 --- a/src/app/generators/github/index.js +++ b/src/app/generators/github/index.js @@ -21,17 +21,22 @@ export default class extends BaseGenerator { } } - async end() { + async install() { if (this.options.createGithubRepository) { - const { - data: { html_url: url }, - } = await createRepository({ - name: this.options.projectName, - description: this.options.description, - }); - - this.log('\n\n'); - this.log(`Repository created: ${url}`); + await this._createGithubRepository(); } } + + async _createGithubRepository() { + this.log('Creating github repository...'); + const { + data: { html_url: url }, + } = await createRepository({ + name: this.options.projectName, + description: this.options.description, + }); + + this.log('\n\n'); + this.log(`Repository created: ${url}`); + } } diff --git a/src/app/generators/github/templates/contributing.md b/src/app/generators/github/templates/contributing.md index 5bc3f0d..a13c757 100644 --- a/src/app/generators/github/templates/contributing.md +++ b/src/app/generators/github/templates/contributing.md @@ -38,6 +38,16 @@ yarn lint yarn lint --fix ``` +<% if (semanticRelease) { %> +Run linter to validate your commit message: + +```sh +yarn lint:commit +``` +<% } %> + +## Committing and Pushing changes + ## Committing and Pushing changes Create a branch and start hacking: @@ -48,13 +58,20 @@ git checkout -b my-branch Commit and push your changes: +<% if (semanticRelease) { %>`generator-node-mdl` uses [commitizen](https://github.com/commitizen/cz-cli) to create commit messages so [semantic-release](https://github.com/semantic-release/semantic-release) can automatically create releases.<% } %> + ```sh -git add my/changed/files +git add . +<% if (semanticRelease) { %> +yarn commit +# answer the questions +<% } else { %> git commit +<% } %> git push origin my-branch ``` -Open this project on [GitHub](https://github.com/<%= githubUsername %>/<%= projectName %>), then click “Compare & pull request”. +Open this project on [GitHub](https://github.com/sharvit/generator-node-mdl), then click “Compare & pull request”. ## Help needed diff --git a/src/app/generators/project/index.js b/src/app/generators/project/index.js index 80dc086..c98657c 100644 --- a/src/app/generators/project/index.js +++ b/src/app/generators/project/index.js @@ -65,6 +65,7 @@ export default class extends BaseGenerator { bower: false, npm: !hasYarn, yarn: hasYarn, + skipMessage: true, }); } @@ -82,7 +83,7 @@ export default class extends BaseGenerator { this.spawnCommandSync('git', [ 'commit', '-m', - 'Generated by generator-node-mdl 🔥', + 'chore(init): Generated by generator-node-mdl 🔥', '--quiet', ]); } diff --git a/src/app/generators/project/templates/_package.json b/src/app/generators/project/templates/_package.json index fccd120..86de69a 100644 --- a/src/app/generators/project/templates/_package.json +++ b/src/app/generators/project/templates/_package.json @@ -1,6 +1,7 @@ { - "name": "<%= projectName %>", - "version": "0.0.0", + "name": "<%= projectName %>",<% if (semanticRelease) { %> + "version": "0.0.0-semantic-release",<% } else { %> + "version": "0.1.0",<% } %> "description": "<%= description %>", "license": "MIT", "repository": "https://github.com/<%= githubUsername %>/<%= projectName %>", @@ -18,7 +19,11 @@ "test:watch": "jest --watch", "test": "jest --coverage",<% if (coveralls) { %> "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",<% } %> - "lint": "eslint ./src" + "lint": "eslint ./src"<% if (semanticRelease) { %>, + "lint:commit": "commitlint -e", + "lint:commit-travis": "commitlint-travis", + "commit": "git-cz", + "semantic-release": "semantic-release"<% } %> }, "main": "dist/index.js", "files": [ @@ -28,12 +33,17 @@ "devDependencies": { "@babel/cli": "^7.2.0", "@babel/core": "^7.2.0", - "@babel/preset-env": "^7.2.0", + "@babel/preset-env": "^7.2.0",<% if (semanticRelease) { %> + "@commitlint/cli": "^7.2.1", + "@commitlint/config-conventional": "^7.1.2", + "@commitlint/travis-cli": "^7.2.1",<% } %> "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.0.1", - "babel-jest": "^23.6.0", + "babel-jest": "^23.6.0",<% if (semanticRelease) { %> + "commitlint-config-cz": "^0.11.0",<% } %> "babel-plugin-add-module-exports": "^1.0.0",<% if (coveralls) { %> - "coveralls": "^3.0.2",<% } %> + "coveralls": "^3.0.2",<% } %><% if (semanticRelease) { %> + "cz-conventional-changelog": "2.1.0",<% } %> "eslint": "^5.10.0", "eslint-config-prettier": "^3.3.0", "eslint-config-standard": "^12.0.0", @@ -45,7 +55,10 @@ "jest": "^23.6.0", "prettier": "^1.15.3", "rimraf": "^2.6.2" - }, + },<% if (semanticRelease) { %> + "optionalDependencies": { + "semantic-release": "^15.13.1" + },<% } %> "jest": { "testEnvironment": "node", "transform": { @@ -54,6 +67,11 @@ "collectCoverageFrom": [ "src/**/*.js" ] - }, + },<% if (semanticRelease) { %> + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + },<% } %> "keywords": [] } diff --git a/src/app/generators/project/templates/readme.md b/src/app/generators/project/templates/readme.md index b4eded0..ccc6e47 100644 --- a/src/app/generators/project/templates/readme.md +++ b/src/app/generators/project/templates/readme.md @@ -2,7 +2,8 @@ > <%= description %> -[![Package Version](https://img.shields.io/npm/v/<%= projectName %>.svg?style=flat-square)](https://www.npmjs.com/package/<%= projectName %>) +[![Package Version](https://img.shields.io/npm/v/<%= projectName %>.svg?style=flat-square)](https://www.npmjs.com/package/<%= projectName %>)<% if (semanticRelease) { %> +[![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)<% } %> [![Downloads Status](https://img.shields.io/npm/dm/<%= projectName %>.svg?style=flat-square)](https://npm-stat.com/charts.html?package=<%= projectName %>&from=2016-04-01) <% if (travisCI) { %>[![Build Status: Linux](https://img.shields.io/travis/<%= githubUsername %>/<%= projectName %>/master.svg?style=flat-square)](https://travis-ci.org/<%= githubUsername %>/<%= projectName %>)<% } %> <% if (coveralls) { %>[![Coverage Status](https://coveralls.io/repos/github/<%= githubUsername %>/<%= projectName %>/badge.svg?branch=master)](https://coveralls.io/github/<%= githubUsername %>/<%= projectName %>?branch=master)<% } %> diff --git a/src/app/generators/travis/index.js b/src/app/generators/travis/index.js index 3ef2728..73215dd 100644 --- a/src/app/generators/travis/index.js +++ b/src/app/generators/travis/index.js @@ -12,7 +12,26 @@ export default class extends BaseGenerator { async install() { if (process.env.NODE_ENV === 'test') return; - const { repository, npmDeploy, npmToken } = this.props; + this.log('\nInstalling TravisCI...\n'); + + const { npmDeploy, semanticRelease } = this.options; + + this._installTravis(); + + if (npmDeploy) { + this._installTravisNpmToken(); + + if (semanticRelease) { + this._installTravisGithubToken(); + } + } + } + + _installTravis() { + const { repository } = this.options; + + this.log('repository'); + this.log(repository); this.spawnCommandSync('gem', [ 'install', @@ -23,16 +42,31 @@ export default class extends BaseGenerator { ]); this.spawnCommandSync('travis', ['login', '--auto']); this.spawnCommandSync('travis', ['enable', '-r', repository]); + } - if (npmDeploy) { - this.spawnCommandSync('travis', [ - 'env', - 'set', - 'NPM_TOKEN', - npmToken, - '-r', - repository, - ]); - } + _installTravisNpmToken() { + const { repository, npmToken } = this.options; + + this.spawnCommandSync('travis', [ + 'env', + 'set', + 'NPM_TOKEN', + npmToken, + '-r', + repository, + ]); + } + + _installTravisGithubToken() { + const { repository, githubToken } = this.options; + + this.spawnCommandSync('travis', [ + 'env', + 'set', + 'GH_TOKEN', + githubToken, + '-r', + repository, + ]); } } diff --git a/src/app/generators/travis/templates/_travis.yml b/src/app/generators/travis/templates/_travis.yml index 690ac9c..ef4d1a7 100644 --- a/src/app/generators/travis/templates/_travis.yml +++ b/src/app/generators/travis/templates/_travis.yml @@ -2,14 +2,26 @@ language: node_js notifications: email: false node_js: - - '10' - - '8' - - '6' -script: + - 10 + - 8 + - 6 +script:<% if (semanticRelease) { %> + - yarn lint:commit-travis<% } %> - yarn lint - yarn test - yarn build -<% if (coveralls) { %>after_success: yarn coveralls<% } %><% if (npmDeploy) { %> +<% if (coveralls) { %>after_success: yarn coveralls<% } if (npmDeploy && semanticRelease) { %> +jobs: + include: + - stage: release + if: branch = master + node_js: 10 + deploy: + provider: script + skip_cleanup: true + script: + - yarn semantic-release +<% } else if (npmDeploy) { %> deploy: skip_cleanup: true provider: npm diff --git a/src/app/index.test.js b/src/app/index.test.js index 8374075..287394e 100644 --- a/src/app/index.test.js +++ b/src/app/index.test.js @@ -176,6 +176,7 @@ describe('prompts', () => { return runAppGenerator() .withPrompts({ createGithubRepository: false, + npmDeploy: false, githubUsername: 'some-username', githubPassword: 'some-password', projectName: 'some-project-name', @@ -273,6 +274,7 @@ describe('prompts', () => { .withPrompts({ travisCI: true, npmDeploy: true, + semanticRelease: false, npmUsername: 'some-username', npmPassword: 'some-password', }) @@ -280,6 +282,28 @@ describe('prompts', () => { assert.fileContent('.travis.yml', 'deploy:'); assert.fileContent('.travis.yml', 'provider: npm'); assert.fileContent('.travis.yml', 'api_key: $NPM_TOKEN'); + + assert.noFileContent('.travis.yml', 'provider: script'); + assert.noFileContent('.travis.yml', 'yarn semantic-release'); + + assert.noFileContent( + 'package.json', + '"version": "0.0.0-semantic-release"' + ); + assert.noFileContent( + 'package.json', + '"semantic-release": "semantic-release"' + ); + assert.noFileContent('package.json', '@commitlint/cli'); + assert.noFileContent( + 'package.json', + '@commitlint/config-conventional' + ); + assert.noFileContent('package.json', '@commitlint/travis-cli'); + assert.noFileContent( + 'package.json', + '"path": "./node_modules/cz-conventional-changelog"' + ); }); }); @@ -293,5 +317,39 @@ describe('prompts', () => { assert.noFileContent('.travis.yml', 'deploy'); }); }); + + test('npmDeploy with semanticRelease', () => { + return runAppGenerator() + .withPrompts({ + travisCI: true, + npmDeploy: true, + semanticRelease: true, + npmUsername: 'some-username', + npmPassword: 'some-password', + githubUsername: 'some-username', + githubPassword: 'some-password', + }) + .then(() => { + assert.fileContent('.travis.yml', 'deploy:'); + assert.fileContent('.travis.yml', 'provider: script'); + assert.fileContent('.travis.yml', 'yarn semantic-release'); + + assert.fileContent( + 'package.json', + '"version": "0.0.0-semantic-release"' + ); + assert.fileContent( + 'package.json', + '"semantic-release": "semantic-release"' + ); + assert.fileContent('package.json', '@commitlint/cli'); + assert.fileContent('package.json', '@commitlint/config-conventional'); + assert.fileContent('package.json', '@commitlint/travis-cli'); + assert.fileContent( + 'package.json', + '"path": "./node_modules/cz-conventional-changelog"' + ); + }); + }); }); }); diff --git a/src/app/lib/__mocks__/github.js b/src/app/lib/__mocks__/github.js index d766856..c4a8fe1 100644 --- a/src/app/lib/__mocks__/github.js +++ b/src/app/lib/__mocks__/github.js @@ -8,3 +8,5 @@ export const createRepository = jest.fn(async ({ name, description }) => ({ html_url: 'some-html_url', }, })); + +export const createGithubToken = jest.fn(async () => 'some-token'); diff --git a/src/app/lib/github.js b/src/app/lib/github.js index 5e27816..832f34d 100644 --- a/src/app/lib/github.js +++ b/src/app/lib/github.js @@ -1,4 +1,6 @@ import createGithubClient from '@octokit/rest'; +import request from 'request-promise'; +import uuid from 'uuid/v4'; const github = createGithubClient(); @@ -11,3 +13,26 @@ export const login = ({ username, password }) => export const createRepository = ({ name, description }) => github.repos.createForAuthenticatedUser({ name, description }); + +export const createGithubToken = async ({ + username, + password, + repository, + scopes, +}) => { + const { token } = await request({ + method: 'POST', + url: 'https://api.github.com/authorizations', + json: true, + auth: { username, password }, + headers: { + 'User-Agent': 'semantic-release', + }, + body: { + scopes, + note: `semantic-release-${repository}-${uuid().slice(-4)}`, + }, + }); + + return token; +}; diff --git a/src/app/options.js b/src/app/options.js index 3565cfa..6fc6ffe 100644 --- a/src/app/options.js +++ b/src/app/options.js @@ -33,14 +33,27 @@ const options = { type: Boolean, desc: 'Let me create a new github repository for you?', }, - coveralls: { type: Boolean, desc: 'Connect TravisCI to Coveralls?' }, + coveralls: { + type: Boolean, + desc: 'Connect TravisCI to Coveralls?', + help: () => '\nLearn how to use Coveralls: https://coveralls.io', + }, travisCI: { type: Boolean, desc: 'Give your project super prowers using Travis CI?', + help: () => + '\nLearn how to use Travis CI: https://docs.travis-ci.com/user/tutorial/#to-get-started-with-travis-ci', }, npmDeploy: { type: Boolean, desc: 'Automatically deploy to npm using TravisCI?', + help: () => '\nNeed to have an npm account: https://www.npmjs.com/', + }, + semanticRelease: { + type: Boolean, + desc: 'Use semantic-release?', + help: () => + '\nLearn more about semantic-release: https://semantic-release.gitbook.io/semantic-release/', }, npmToken: { type: String, diff --git a/src/app/prompter.js b/src/app/prompter.js index 009b7b5..cd57417 100644 --- a/src/app/prompter.js +++ b/src/app/prompter.js @@ -1,7 +1,8 @@ import camelCase from 'lodash.camelcase'; import kebabCase from 'lodash.kebabcase'; +import chalk from 'chalk'; -import { login as githubLogin } from './lib/github'; +import { login as githubLogin, createGithubToken } from './lib/github'; import { login as npmLogin } from './lib/npm'; import options from './options'; @@ -11,6 +12,9 @@ export default class Prompter { this.generator = generator; } + /** + * Main prompter method + */ async prompt() { this.props = {}; @@ -18,10 +22,15 @@ export default class Prompter { await this._promptGithub(); await this._promptTravis(); await this._promptNpm(); + await this._promptPasswords(); return this.props; } + /* + Sub prompters + */ + async _promptGeneral() { const answers = await this.generator.prompt([ { @@ -82,10 +91,6 @@ export default class Prompter { Object.assign(this.props, answers, { repository: `${answers.githubUsername}/${this.props.projectName}`, }); - - if (answers.createGithubRepository) { - await this._loginToGithub(); - } } async _promptTravis() { @@ -95,12 +100,7 @@ export default class Prompter { message: options.travisCI.desc, type: 'confirm', default: true, - when: () => { - this.generator.log( - '\nLearn how to use Travis CI: https://docs.travis-ci.com/user/tutorial/#to-get-started-with-travis-ci' - ); - return true; - }, + when: this._logHelpWhenPrompt(options.travisCI.help), }, { name: 'coveralls', @@ -109,9 +109,7 @@ export default class Prompter { default: true, when: ({ travisCI }) => { if (travisCI) { - this.generator.log( - '\nLearn how to use Coveralls: https://coveralls.io' - ); + this.generator.log(options.coveralls.help()); return true; } @@ -132,9 +130,21 @@ export default class Prompter { default: true, when: () => { if (this.props.travisCI) { - this.generator.log( - '\nNeed to have an npm account: https://www.npmjs.com/' - ); + this.generator.log(options.npmDeploy.help()); + return true; + } + + return false; + }, + }, + { + name: 'semanticRelease', + message: options.semanticRelease.desc, + type: 'confirm', + default: true, + when: ({ npmDeploy }) => { + if (npmDeploy) { + this.generator.log(options.semanticRelease.help()); return true; } @@ -145,13 +155,66 @@ export default class Prompter { Object.assign(this.props, { npmDeploy: answers.npmDeploy, + semanticRelease: answers.semanticRelease, }); + } - if (this.props.npmDeploy) { + async _promptPasswords() { + const { createGithubRepository, semanticRelease, npmDeploy } = this.props; + + if (createGithubRepository || semanticRelease) { + this._logGithubPasswordRequired(); + this._logPasswordSafety(); + await this._loginToGithub(); + } + + if (npmDeploy) { + this._logNpmPasswordRequired(); + this._logPasswordSafety(); await this._loginToNpm(); } } + /* + Login prompters + */ + + async _loginToNpm() { + const { npmUsername, npmPassword } = await this._promptNpmLogin(); + + // generate npm-token + const { token } = await npmLogin({ + username: npmUsername, + password: npmPassword, + }); + + Object.assign(this.props, { + npmToken: token, + }); + } + + async _loginToGithub() { + const { + githubUsername: username, + semanticRelease, + repository, + } = this.props; + const { githubPassword: password } = await this._promptGithubPassword(); + + githubLogin({ + username, + password, + }); + + if (semanticRelease) { + this.props.githubToken = await this._createGithubToken({ + username, + password, + repository, + }); + } + } + _promptNpmLogin() { return this.generator.prompt([ { @@ -179,27 +242,62 @@ export default class Prompter { ]); } - async _loginToNpm() { - const { npmUsername, npmPassword } = await this._promptNpmLogin(); + /* + Loggers + */ - // generate npm-token - const { token } = await npmLogin({ - username: npmUsername, - password: npmPassword, - }); + _logHelpWhenPrompt(help) { + return () => { + this.generator.log(help()); + return true; + }; + } - Object.assign(this.props, { - npmToken: token, - }); + _logGithubPasswordRequired() { + const { createGithubRepository } = this.props; + + if (createGithubRepository) { + this.generator.log( + '\nYour github password is required so I can create a github repository for you.' + ); + } else { + this.generator.log( + '\nYour github password is required so I can install semantic-release for you.' + ); + } } - async _loginToGithub() { - const { githubUsername } = this.props; - const { githubPassword } = await this._promptGithubPassword(); + _logNpmPasswordRequired() { + this.generator.log( + '\nYour npm username and password is required so I can install an automated npm-deploy for you.' + ); + } - githubLogin({ - username: githubUsername, - password: githubPassword, + _logPasswordSafety() { + this.generator.log( + `${chalk.bold('I will never store your passwords')} 🙌 🙌 🙌 \n` + ); + } + + /* + Helpers + */ + + _createGithubToken({ username, password, repository }) { + const scopes = [ + 'repo', + 'read:org', + 'user:email', + 'repo_deployment', + 'repo:status', + 'write:repo_hook', + ]; + + return createGithubToken({ + username, + password, + repository, + scopes, }); } } diff --git a/yarn.lock b/yarn.lock index 3a897dc..4075f28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7081,6 +7081,16 @@ request-promise-native@^1.0.5: stealthy-require "^1.1.0" tough-cookie ">=2.3.3" +request-promise@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4" + integrity sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ= + dependencies: + bluebird "^3.5.0" + request-promise-core "1.1.1" + stealthy-require "^1.1.0" + tough-cookie ">=2.3.3" + request@^2.74.0, request@^2.85.0, request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"