├── nca ├── test ├── spec │ ├── data │ │ ├── doc │ │ │ ├── abc.txt │ │ │ └── xyz.txt │ │ └── .autorelease.yml │ └── lib │ │ ├── gh-pages-creator.js │ │ ├── releasability-checker.js │ │ ├── autorelease-yml.js │ │ └── release-executor.js └── mocha.opts ├── src ├── decls │ └── extern.js ├── bin │ ├── nca-gh-pages.js │ ├── nca.js │ ├── nca-run.js │ ├── nca-update-modules.js │ ├── nca-notice.js │ ├── nca-generate.js │ ├── nca-init.js │ ├── nca-release.js │ └── nca-bmp.js ├── lib │ ├── package-json-loader.js │ ├── working-directory.js │ ├── gh-pages-creator.js │ ├── releasability-checker.js │ ├── release-executor.js │ ├── circle-yml.js │ └── autorelease-yml.js └── util │ └── exec.js ├── .bmp.yml ├── .flowconfig ├── .editorconfig ├── .babelrc ├── .gitignore ├── .autorelease.yml ├── .releaseignore ├── circle.yml ├── .dev-decls └── mocha.js ├── .eslintrc.yml ├── LICENSE ├── package.json └── README.md /nca: -------------------------------------------------------------------------------- 1 | dist/bin/nca.js -------------------------------------------------------------------------------- /test/spec/data/doc/abc.txt: -------------------------------------------------------------------------------- 1 | sample text 2 | -------------------------------------------------------------------------------- /test/spec/data/doc/xyz.txt: -------------------------------------------------------------------------------- 1 | sample text 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | -------------------------------------------------------------------------------- /src/decls/extern.js: -------------------------------------------------------------------------------- 1 | declare type primitive = string | number | boolean 2 | -------------------------------------------------------------------------------- /.bmp.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.2.4 3 | commit: Bump to version v%.%.% 4 | files: 5 | package.json: '"version": "%.%.%",' 6 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | src/decls 7 | .dev-decls 8 | 9 | [options] 10 | esproposal.class_static_fields=enable 11 | unsafe.enable_getters_and_setters=true 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.json] 11 | indent_size = 2 12 | 13 | [*.yml] 14 | indent_size = 2 15 | 16 | [.babelrc] 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [ 4 | "transform-object-assign", 5 | "array-includes", 6 | ["typecheck", { "only": ["enabled"] }], 7 | "syntax-flow", 8 | "transform-flow-strip-types" 9 | ], 10 | "env": { 11 | "development": { 12 | "presets": [ 13 | "power-assert" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary directories 2 | .tmp 3 | 4 | # distributable files 5 | dist 6 | 7 | # automatically generated docs 8 | doc/reference* 9 | 10 | # npm https://www.npmjs.com 11 | node_modules 12 | 13 | # avn settings https://github.com/wbyoung/avn 14 | .envrc 15 | 16 | # debug logs 17 | *.log 18 | 19 | # vim 20 | *.swp 21 | : 22 | 23 | # sublime text 24 | *.sublime-* 25 | -------------------------------------------------------------------------------- /.autorelease.yml: -------------------------------------------------------------------------------- 1 | config: 2 | git_user_name: CircleCI 3 | git_user_email: circleci@example.com 4 | npm_update_depth: 5 5 | version_prefix: v 6 | create_branch: false 7 | npm_shrinkwrap: false 8 | create_gh_pages: false 9 | gh_pages_dir: null 10 | circle: 11 | machine: 12 | node: 13 | version: 6.2.0 14 | environment: 15 | PATH: '.' 16 | dependencies: 17 | override: 18 | - npm install && npm run babel 19 | -------------------------------------------------------------------------------- /.releaseignore: -------------------------------------------------------------------------------- 1 | # symlink, only for development 2 | /nca 3 | 4 | 5 | 6 | ### 7 | # basic files 8 | ### 9 | # dot files 10 | .* 11 | # npm https://www.npmjs.com 12 | node_modules 13 | 14 | 15 | # documentations 16 | /doc 17 | 18 | # source files 19 | /src 20 | 21 | # test files 22 | /test 23 | 24 | # development tools 25 | /tools 26 | 27 | # CircleCI cetting https://circleci.com 28 | circle.yml 29 | 30 | # debug logs 31 | *.log 32 | 33 | # vim 34 | *.swp 35 | : 36 | 37 | # sublime text 38 | *.sublime-*node_modules 39 | npm-debug.log 40 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | general: 2 | branches: 3 | ignore: 4 | - gh-pages 5 | - /release.*/ 6 | machine: 7 | environment: 8 | PATH: '.:./node_modules/.bin:$PATH' 9 | pre: 10 | - git config --global user.name "CircleCI" 11 | - git config --global user.email "circleci@example.com" 12 | node: 13 | version: 6.2.0 14 | dependencies: 15 | post: 16 | - nca run nca update-modules --depth 5 17 | override: 18 | - npm install && npm run babel 19 | deployment: 20 | create_release_branch: 21 | branch: 22 | - master 23 | commands: 24 | - nca release --prefix v 25 | - nca run nca notice gh-pages 26 | -------------------------------------------------------------------------------- /test/spec/data/.autorelease.yml: -------------------------------------------------------------------------------- 1 | 2 | hooks: 3 | 4 | update_modules: 5 | pre: echo "running before updating npm" 6 | 7 | release: 8 | post: echo "running before releasing the tag" 9 | 10 | gh_pages: 11 | pre: echo "running before building gh-pages branch" 12 | 13 | 14 | config: 15 | git_user_name: CircleCI, 16 | git_user_email: circleci@cureapp.jp, 17 | version_prefix: v, 18 | create_branch: true, 19 | create_gh_pages: false, 20 | gh_pages_dir: doc, 21 | npm_shrinkwrap: true, 22 | npm_update_depth: 4 23 | 24 | 25 | circle: 26 | machine: 27 | environment: 28 | PATH: "$PATH:$HOME/$CIRCLE_PROJECT_REPONAME/bin" 29 | -------------------------------------------------------------------------------- /src/bin/nca-gh-pages.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import program from 'commander' 5 | import GhPagesCreator from '../lib/gh-pages-creator' 6 | 7 | 8 | export default function run() { 9 | 10 | program 11 | .arguments('', /[pmjr]/) 12 | .option('--dir ', 'directory to host') 13 | .parse(process.argv) 14 | 15 | const {dir} = program 16 | 17 | if (!dir) { 18 | console.log(HOW_TO_HOST_SPECIFIC_DIR) 19 | } 20 | 21 | new GhPagesCreator().create(dir) 22 | } 23 | 24 | const HOW_TO_HOST_SPECIFIC_DIR = ` 25 | All files in master branch will be added to gh-pages. 26 | Set 'config.gh_pages_dir' in .autorelease.yml. 27 | Then, only the contents of the directory are added to gh-pages. 28 | ` 29 | 30 | if (require.main === module) run() 31 | -------------------------------------------------------------------------------- /src/lib/package-json-loader.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs' 3 | 4 | /** 5 | * Loader for package.json 6 | */ 7 | export default class PackageJSONLoader { 8 | 9 | /** 10 | * load package.json of the given path 11 | * @param cwd project root dir 12 | */ 13 | static load(cwd: string) { 14 | 15 | const path = cwd + '/package.json' 16 | 17 | if (!fs.existsSync(path)) { 18 | throw new Error(path + ' is not found.') 19 | } 20 | 21 | try { 22 | return JSON.parse(fs.readFileSync(path, 'utf8')) 23 | 24 | } catch (e) { 25 | throw new Error(path + ': parse error.\n' + e.message) 26 | } 27 | } 28 | 29 | 30 | static save(cwd, content) { 31 | 32 | const path = cwd + '/package.json' 33 | 34 | return fs.writeFileSync(path, JSON.stringify(content, null, 2) + '\n') 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.dev-decls/mocha.js: -------------------------------------------------------------------------------- 1 | declare function describe(name:string, callback?: Function):void; 2 | declare function xdescribe(name:string, callback?: Function):void; 3 | 4 | declare function before(callback?: Function):void; 5 | declare function xbefore(callback?: Function):void; 6 | 7 | declare function beforeEach(callback?: Function):void; 8 | declare function xbeforeEach(callback?: Function):void; 9 | 10 | declare function after(callback?: Function):void; 11 | declare function xafter(callback?: Function):void; 12 | 13 | declare function afterEach(callback?: Function):void; 14 | declare function xafterEach(callback?: Function):void; 15 | 16 | declare function it(name:string, callback?: Function):void; 17 | declare function xit(name:string, callback?: Function):void; 18 | 19 | declare function context(name:string, callback?: Function):void; 20 | declare function xcontext(name:string, callback?: Function):void; 21 | 22 | declare function assert(bool: any):void; 23 | -------------------------------------------------------------------------------- /src/lib/working-directory.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | /** 6 | * Getting current working directory 7 | * 8 | */ 9 | export default class WorkingDirectory { 10 | 11 | path: string; 12 | 13 | constructor() { 14 | this.path = process.cwd() 15 | } 16 | 17 | 18 | /** 19 | * get current project root 20 | * @public 21 | */ 22 | resolve(): string { 23 | if (this.inNodeModules() && this.upperPackageJSON()) { 24 | this.path = path.normalize(this.path + '/../..') 25 | } 26 | 27 | return this.path 28 | } 29 | 30 | 31 | inNodeModules() { 32 | 33 | return path.basename(path.normalize(this.path + '/..')) === 'node_modules' 34 | } 35 | 36 | 37 | upperPackageJSON() { 38 | 39 | const upperPackagePath = path.normalize(this.path + '/../../package.json') 40 | 41 | return fs.existsSync(upperPackagePath) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | commonjs: true 4 | es6: true 5 | extends: 'eslint:recommended' 6 | installedESLint: true 7 | parser: babel-eslint 8 | parserOptions: 9 | ecmaFeatures: 10 | experimentalObjectRestSpread: true 11 | jsx: true 12 | sourceType: module 13 | plugins: 14 | - react 15 | - flowtype 16 | - flow-vars 17 | rules: 18 | comma-dangle: off 19 | no-unused-vars: warn 20 | indent: 21 | - error 22 | - 4 23 | linebreak-style: 24 | - error 25 | - unix 26 | quotes: 27 | - error 28 | - single 29 | semi: 30 | - error 31 | - never 32 | prefer-const: warn 33 | 34 | flowtype/require-return-type: 0 35 | 36 | flowtype/space-after-type-colon: 37 | - 1 38 | - always 39 | flowtype/space-before-type-colon: 40 | - 1 41 | - never 42 | flowtype/type-id-match: 43 | - 1 44 | - "^([A-Z][a-z0-9]+)+Type$" 45 | 46 | flow-vars/define-flow-type: 1 47 | flow-vars/use-flow-type: 1 48 | 49 | settings: 50 | flowtype: 51 | onlyFilesWithFlowAnnotation: false 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 CureApp.Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/util/exec.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | import {exec as shellJSExec} from 'shelljs' 4 | import chalk from 'chalk' 5 | 6 | const CHECK_OK = '✓' 7 | const CHECK_NG = '✖' 8 | 9 | export default function exec(command: string, options?: {silent?: boolean} = {}): Object { 10 | 11 | const dryRun = !!process.env.DRY_RUN 12 | 13 | const log = options.silent ? function(){} : console.log.bind(console) 14 | const error = options.silent ? function(){} : console.error.bind(console) 15 | 16 | if (dryRun) { 17 | log(chalk.yellow(`[DRY RUN]: ${command}`)) 18 | return {command, stdout: '[DRY RUN]', stderr: '[DRY RUN]', code: 0} 19 | } 20 | else { 21 | const result = shellJSExec(command, {silent: true}) 22 | const succeeded = result.code === 0 23 | const color = succeeded ? 'green': 'red' 24 | const check = succeeded ? CHECK_OK : CHECK_NG 25 | log(chalk[color](` ${check} ${command}`)) 26 | 27 | if (!succeeded) { 28 | log('\tSTDOUT: ') 29 | log(chalk.red(result.stdout)) 30 | error('\tSTDERR: ') 31 | error(chalk.red(result.stderr)) 32 | } 33 | 34 | return result 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/bin/nca.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /*eslint no-console: 0 */ 3 | // @flow 4 | 5 | import fs from 'fs' 6 | import PackageJSONLoader from '../lib/package-json-loader' 7 | import program from 'commander' 8 | 9 | const version = PackageJSONLoader.load(__dirname + '/../..').version 10 | program.version(version) 11 | 12 | const subcommands = { 13 | 'init' : 'add .autorelease.yml to your project', 14 | 'generate' : 'generate circle.yml', 15 | 'bmp' : 'generate circle.yml and bumping version', 16 | 'update-modules' : 'update node modules', 17 | 'release' : 'release current version', 18 | 'gh-pages' : 'create "gh-pages" branch for documentation', 19 | 'notice' : 'show notice', 20 | 'run' : 'execute commands at releasable timings', 21 | } 22 | 23 | Object.keys(subcommands) 24 | .filter(sub => fs.existsSync(__dirname + '/nca-' + sub + '.js')) 25 | .forEach(sub => program.command(sub, subcommands[sub])) 26 | 27 | 28 | export function run(args: Array) { 29 | const argv = args.slice() 30 | argv.unshift(process.execPath, __filename) 31 | program.parse(argv) 32 | } 33 | 34 | if (require.main === module) run(process.argv.slice(2)) 35 | -------------------------------------------------------------------------------- /src/bin/nca-run.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import {spawn} from 'child_process' 5 | import exec from '../util/exec' 6 | import ReleasabilityChecker from '../lib/releasability-checker' 7 | 8 | export default function run(argv: Array) { 9 | 10 | const command = argv.join(' ') 11 | 12 | if (!isReleasable()) { 13 | console.log(`Non-releasable state, skip command: "${command}"`) 14 | return process.exit(0) 15 | } 16 | 17 | console.log(`executing "${command}"`) 18 | 19 | const [bin, ...args] = argv 20 | spawn(bin, args, {stdio: 'inherit'}) 21 | } 22 | 23 | function isReleasable(): boolean { 24 | 25 | if (isReleaseFinished()) { 26 | return true 27 | } 28 | 29 | const checker = new ReleasabilityChecker() 30 | return checker.isReleasable 31 | } 32 | 33 | // Currently, we regard it as "release finished" that 34 | // the pushed branch name differs from current one. 35 | function isReleaseFinished(): boolean { 36 | const currentBranch = exec('git rev-parse --abbrev-ref HEAD', {silent: true}).stdout.trim() 37 | return process.env.CIRCLECI && process.env.CIRCLE_BRANCH != currentBranch 38 | } 39 | 40 | 41 | 42 | if (require.main === module) run(process.argv.slice(2)) 43 | -------------------------------------------------------------------------------- /src/bin/nca-update-modules.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import exec from '../util/exec' 5 | import program from 'commander' 6 | 7 | export default function run() { 8 | 9 | program 10 | .option('--depth ', 'depth of npm modules to update', parseInt) 11 | .parse(process.argv) 12 | 13 | const {depth} = program 14 | 15 | if (!depth) return process.exit(0) 16 | 17 | if (getMajorNpmVersion() < 3) { 18 | console.log(WHY_NPM2_IS_NOT_RECOMMENDED) 19 | exec(`npm update --depth ${depth}`) 20 | } 21 | else { 22 | exec(`npm update --dev --depth ${depth}`) 23 | } 24 | } 25 | 26 | function getMajorNpmVersion() { 27 | return Number(exec('npm -v', {silent: true}).stdout.split('.')[0]) 28 | } 29 | 30 | const WHY_NPM2_IS_NOT_RECOMMENDED = ` 31 | --------------------------------------------------------------- 32 | To update node_modules, npm version should be 3 or more. 33 | 34 | This is because npm v2 has a bug that it tries to install 35 | devDependencies of submodules when --dev option is set. 36 | 37 | https://github.com/npm/npm/issues/5554 38 | 39 | As a workaround, we omit updating devDependencies in npm v2. 40 | ---------------------------------------------------------------- 41 | ` 42 | 43 | 44 | if (require.main === module) run() 45 | -------------------------------------------------------------------------------- /src/bin/nca-notice.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import program from 'commander' 5 | 6 | export default function run() { 7 | 8 | program 9 | .arguments('') 10 | .parse(process.argv) 11 | 12 | switch (program.args[0]) { 13 | case 'gh-pages': 14 | console.log(GH_PAGES_BRANCH_WAS_NOT_CREATED) 15 | return 16 | 17 | case 'update-modules': 18 | console.log(UPDATE_MODULES_WERE_NOT_EXECUTED) 19 | return 20 | } 21 | 22 | 23 | } 24 | 25 | const GH_PAGES_BRANCH_WAS_NOT_CREATED = ` 26 | -------------------------------------------------------- 27 | Branch "gh-pages" was not created. 28 | If you would like to create it, edit .autorelease.yml 29 | like the code below. 30 | 31 | config: 32 | create_gh_pages: true 33 | gh_pages_dir: doc # directory to host in gh-pages 34 | 35 | -------------------------------------------------------- 36 | ` 37 | 38 | const UPDATE_MODULES_WERE_NOT_EXECUTED = ` 39 | -------------------------------------------------------- 40 | Dependent node_modules were not updated. 41 | If you would like to update them, edit .autorelease.yml 42 | like the code below. 43 | 44 | config: 45 | npm_update_depth: 4 # greater than 0 -> updated 46 | 47 | -------------------------------------------------------- 48 | ` 49 | 50 | 51 | if (require.main === module) run() 52 | -------------------------------------------------------------------------------- /src/lib/gh-pages-creator.js: -------------------------------------------------------------------------------- 1 | 2 | import fs from 'fs' 3 | import {ls} from 'shelljs' 4 | import exec from '../util/exec' 5 | import yaml from 'js-yaml' 6 | 7 | /** 8 | * Creator for gh-pages 9 | */ 10 | export default class GhPagesCreator { 11 | 12 | 13 | /** 14 | * @public 15 | */ 16 | create(dir ?: string, 17 | remote ?: string = 'origin') { 18 | 19 | this.exec('git checkout --orphan gh-pages') 20 | 21 | if (dir) { 22 | 23 | this.exec('git reset') 24 | this.exec(`git add -f ${dir}`) 25 | this.exec('git clean -fdx') 26 | 27 | ls(dir).forEach(file => { 28 | this.exec(`git mv ${dir}/${file} .`) 29 | }) 30 | } 31 | this.addCircleYml() 32 | 33 | this.exec('git commit -m "gh-pages"') 34 | this.exec(`git push --force ${remote} gh-pages`) 35 | } 36 | 37 | /** 38 | * Add circle.yml for gh-pages 39 | * @private 40 | */ 41 | addCircleYml() { 42 | const ignoreGhPagesYml = { 43 | general: {branches: {ignore: ['gh-pages']}} 44 | } 45 | const ymlStr = yaml.dump(ignoreGhPagesYml, {indent: 2, lineWidth: 120}) 46 | this.write('circle.yml', ymlStr) 47 | this.exec('git add -f circle.yml') 48 | } 49 | 50 | /** 51 | * Write a file with the content 52 | * @private 53 | */ 54 | write(filename: string, content: string) { 55 | fs.writeFileSync(process.cwd() + '/' + filename, content) 56 | } 57 | 58 | /** 59 | * execute a given command 60 | * @private 61 | */ 62 | exec(...args: Array): Object { 63 | return exec(...args) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/bin/nca-generate.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import fs from 'fs' 5 | import {join} from 'path' 6 | import chalk from 'chalk' 7 | import WorkingDirectory from '../lib/working-directory' 8 | import AutoreleaseYml from '../lib/autorelease-yml' 9 | import CircleYml from '../lib/circle-yml' 10 | 11 | export default function run(skipShowingWhatToDoNext?: boolean = false) { 12 | 13 | const rootDir = new WorkingDirectory().resolve() 14 | 15 | const arYml = AutoreleaseYml.loadFromDir(rootDir) 16 | 17 | arYml.checkFormat() 18 | 19 | const ymlStr = CircleYml.generate(arYml) 20 | 21 | const filename = join(rootDir, 'circle.yml') 22 | 23 | if (process.env.DRY_RUN) { 24 | console.log(chalk.yellow('[DRY RUN]: generating circle.yml')) 25 | console.log(chalk.yellow(ymlStr)) 26 | } 27 | else { 28 | fs.writeFileSync(filename, ymlStr) 29 | console.log(chalk.green('circle.yml was successfully generated!')) 30 | } 31 | 32 | if (!skipShowingWhatToDoNext) console.log(WHAT_TO_DO_NEXT) 33 | } 34 | 35 | 36 | const WHAT_TO_DO_NEXT = ` 37 | ----------------------------------------------------------------- 38 | What you do next: 39 | 40 | 1. check your circle.yml 41 | 42 | $EDITOR circle.yml 43 | 44 | 2. commit the changes 45 | 46 | git add -A 47 | git commit -m "add circle.yml" 48 | 49 | 3. version bumping 50 | 51 | $(npm bin)/nca bmp p # patch level version up 52 | $(npm bin)/nca bmp m # minor level version up 53 | $(npm bin)/nca bmp j # major level version up 54 | 55 | ----------------------------------------------------------------- 56 | ` 57 | 58 | 59 | 60 | if (require.main === module) run() 61 | -------------------------------------------------------------------------------- /src/lib/releasability-checker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import exec from '../util/exec' 4 | 5 | 6 | const NON_RELEASE_COMMIT_MESSAGE = ` 7 | ---------------------------------------------------------------- 8 | No release process is going to start, because 9 | the latest commit log is not valid. 10 | Run one of the following command to get valid commit log. 11 | 12 | $(npm bin)/nca bmp p # patch level (0.0.1) 13 | $(npm bin)/nca bmp m # minor level (0.1.0) 14 | $(npm bin)/nca bmp j # major level (1.0.0) 15 | $(npm bin)/nca bmp r # re-release (0.0.0) 16 | 17 | Valid commit log message formats are the followings. 18 | These are automatically set via the commands above. 19 | 20 | release X.Y.Z 21 | re-release X.Y.Z 22 | 23 | ---------------------------------------------------------------- 24 | ` 25 | 26 | /** 27 | * Checker for releasability 28 | */ 29 | export default class ReleasabilityChecker { 30 | __commitMsg: ?string; 31 | 32 | constructor() { 33 | this.__commitMsg = null // cache 34 | } 35 | 36 | /** 37 | * @public 38 | */ 39 | get isReleasable(): ?boolean { 40 | return this.logVersion != null 41 | } 42 | 43 | /** 44 | * @public 45 | */ 46 | get warnMessage(): ?string { 47 | if (!this.logVersion) { return NON_RELEASE_COMMIT_MESSAGE } 48 | } 49 | 50 | get commitMsg(): string { 51 | return this.__commitMsg || this.exec('git log --pretty=format:"%s" -1', {silent: true}).stdout 52 | } 53 | 54 | /** 55 | * @public 56 | */ 57 | get logVersion(): ?string { 58 | if (! this.commitMsg.match(/^(re-)?release +[0-9]+\./)) { 59 | return null 60 | } 61 | 62 | return this.commitMsg.split(/release +/)[1] 63 | } 64 | 65 | 66 | exec(...args: Array): Object { 67 | return exec(...args) 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-circleci-autorelease", 3 | "version": "2.2.4", 4 | "description": "create release tags at CircleCI", 5 | "main": "dist/bin/nca", 6 | "bin": { 7 | "nca": "dist/bin/nca.js" 8 | }, 9 | "engines": { 10 | "node": ">=4", 11 | "npm": ">=3" 12 | }, 13 | "scripts": { 14 | "babel": "babel src -d dist", 15 | "doc": "documentation build src/**/*.js --format html --output doc/reference", 16 | "doc:watch": "documentation serve src/**/*.js -w", 17 | "lint": "eslint $(find src -type f ! -path '*/decls/*')", 18 | "parse": "npm run lint && flow", 19 | "parse:watch": "nodemon --watch src --watch test --exec npm run parse", 20 | "flow": "flow", 21 | "test": "npm run lint && flow && npm run test:spec", 22 | "test:spec": "mocha $(find test/spec -type f ! -path '*/data/*')", 23 | "test:watch": "nodemon --watch src --watch test --exec npm test" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/CureApp/node-circleci-autorelease.git" 28 | }, 29 | "author": "CureApp, Inc.", 30 | "license": "MIT", 31 | "dependencies": { 32 | "chalk": "^1.1.3", 33 | "commander": "^2.9.0", 34 | "deepmerge": "^0.2.10", 35 | "js-yaml": "^3.4.3", 36 | "shelljs": "^0.7.0" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "^6.9.0", 40 | "babel-eslint": "^6.0.4", 41 | "babel-plugin-array-includes": "^2.0.3", 42 | "babel-plugin-syntax-flow": "^6.8.0", 43 | "babel-plugin-transform-flow-strip-types": "^6.8.0", 44 | "babel-plugin-transform-function-bind": "^6.8.0", 45 | "babel-plugin-transform-object-assign": "^6.8.0", 46 | "babel-plugin-typecheck": "^3.9.0", 47 | "babel-preset-es2015": "^6.9.0", 48 | "babel-preset-power-assert": "^1.0.0", 49 | "babel-preset-stage-0": "^6.5.0", 50 | "babel-register": "^6.9.0", 51 | "documentation": "^4.0.0-beta2", 52 | "eslint": "^2.10.2", 53 | "eslint-plugin-flow-vars": "^0.4.0", 54 | "eslint-plugin-flowtype": "^2.2.7", 55 | "eslint-plugin-react": "^5.1.1", 56 | "flow-bin": "^0.25.0", 57 | "mocha": "^2.4.5", 58 | "power-assert": "^1.4.1" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/spec/lib/gh-pages-creator.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import GhPagesCreator from '../../../src/lib/gh-pages-creator' 3 | import assert from 'power-assert' 4 | 5 | import fs from 'fs' 6 | import {resolve} from 'path' 7 | import {exec} from 'shelljs' 8 | import yaml from 'js-yaml' 9 | const ymlStr = yaml.dump({general: {branches: {ignore: ['gh-pages']}}}, {indent: 2}) 10 | 11 | describe('GhPagesCreator', function() { 12 | 13 | beforeEach(function() { 14 | 15 | this.executedCommands = [] 16 | this.creator = new GhPagesCreator() 17 | 18 | this.creator.write = (filename, content) => { 19 | assert(filename === 'circle.yml') 20 | assert(content === ymlStr) 21 | } 22 | 23 | this.creator.exec = x => { 24 | this.executedCommands.push(x) 25 | return { 26 | stdout: 'stdout mock', 27 | stderr: 'stderr mock', 28 | code: 0 29 | } 30 | } 31 | }) 32 | 33 | describe('create', function() { 34 | 35 | context('when no dir is given,', function() { 36 | 37 | it('should release gh-pages branch the same as master', function() { 38 | this.creator.create() 39 | assert.deepEqual(this.executedCommands, [ 40 | 'git checkout --orphan gh-pages', 41 | 'git add -f circle.yml', 42 | 'git commit -m "gh-pages"', 43 | 'git push --force origin gh-pages', 44 | ]) 45 | }) 46 | }) 47 | 48 | context('when dir is given,', function() { 49 | 50 | it('should release gh-pages only the given dir', function() { 51 | this.creator.create('test/spec/data/doc') 52 | 53 | assert.deepEqual(this.executedCommands, [ 54 | 'git checkout --orphan gh-pages', 55 | 'git reset', 56 | 'git add -f test/spec/data/doc', 57 | 'git clean -fdx', 58 | 'git mv test/spec/data/doc/abc.txt .', 59 | 'git mv test/spec/data/doc/xyz.txt .', 60 | 'git add -f circle.yml', 61 | 'git commit -m "gh-pages"', 62 | 'git push --force origin gh-pages' 63 | ]) 64 | }) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/bin/nca-init.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import fs from 'fs' 5 | import chalk from 'chalk' 6 | import program from 'commander' 7 | import AutoreleaseYml from '../lib/autorelease-yml' 8 | import WorkingDirectory from '../lib/working-directory' 9 | 10 | const {filename} = AutoreleaseYml 11 | 12 | export default function run() { 13 | 14 | program 15 | .option('-n, --node', 'attach current node.js information') 16 | .parse(process.argv) 17 | 18 | const rootDir = new WorkingDirectory().resolve() 19 | 20 | generateReleaseIgnoreFile(rootDir) 21 | 22 | generateYml(rootDir) 23 | 24 | if (!process.env.DRY_RUN) console.log(WHAT_TO_DO_NEXT) 25 | 26 | } 27 | 28 | function generateReleaseIgnoreFile(rootDir) { 29 | 30 | if (fs.existsSync(rootDir + '/.releaseignore')) { 31 | console.log(chalk.yellow('.releaseignore already exists.')) 32 | return 33 | } 34 | 35 | const contents = (fs.existsSync(rootDir + '/.gitignore')) 36 | ? fs.readFileSync(rootDir + '/.gitignore', 'utf8') 37 | : '#write down files to ignore in release tags.\n' 38 | 39 | if (process.env.DRY_RUN) { 40 | console.log(chalk.yellow('[DRY RUN]: generating .releaseignore')) 41 | console.log(chalk.yellow(contents)) 42 | } 43 | else { 44 | fs.writeFileSync(rootDir + '/.releaseignore', contents) 45 | console.log(chalk.green('.releaseignore was successfully generated!')) 46 | } 47 | } 48 | 49 | function generateYml(rootDir: string) { 50 | 51 | const arYml = AutoreleaseYml.loadFromDir(rootDir) 52 | 53 | if (arYml.loaded) { 54 | console.log(chalk.yellow(`${filename} already exists.`)) 55 | return 56 | } 57 | 58 | if (program.node) { 59 | arYml.setNodeVersion(process.version.slice(1)) // slice(1): strip 'v' 60 | } 61 | 62 | if (process.env.DRY_RUN) { 63 | console.log(chalk.yellow(`[DRY RUN]: generating ${filename}`)) 64 | console.log(chalk.yellow(arYml.toString())) 65 | } 66 | else { 67 | arYml.saveTo(rootDir) 68 | console.log(chalk.green(`${filename} was successfully generated!`)) 69 | } 70 | } 71 | 72 | const WHAT_TO_DO_NEXT = ` 73 | ----------------------------------------------------------------- 74 | What you do next: 75 | 76 | 1. Edit the setting 77 | 78 | $EDITOR ${filename} 79 | 80 | You can remove all the meaningless 'echo' commands in hooks. 81 | 82 | see https://github.com/CureApp/node-circleci-autorelease 83 | 84 | 2. Reflect it to circle.yml 85 | 86 | $(npm bin)/nca generate 87 | 88 | ----------------------------------------------------------------- 89 | ` 90 | 91 | if (require.main === module) run() 92 | -------------------------------------------------------------------------------- /test/spec/lib/releasability-checker.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ReleasabilityChecker from '../../../src/lib/releasability-checker' 3 | import assert from 'power-assert' 4 | 5 | // TODO: https://github.com/facebook/flow/issues/396 6 | class ReleasabilityChecker4t extends ReleasabilityChecker { 7 | exec: Function; 8 | } 9 | 10 | 11 | describe('ReleasabilityChecker', function() { 12 | 13 | describe('warnMessage', function() { 14 | 15 | it('should return notice message of no release when commit message is incompatible', function() { 16 | const checker = new ReleasabilityChecker4t() 17 | checker.exec = ()=> { return { stdout: 'fix typo' } } 18 | assert(checker.warnMessage && checker.warnMessage.match(/No release process/)) 19 | }) 20 | }) 21 | 22 | describe('logVersion',function() { 23 | 24 | it('should return 1.2.3 when commit log is "release 1.2.3"', function() { 25 | const checker = new ReleasabilityChecker4t() 26 | checker.exec = ()=> { return { stdout: 'release 1.2.3' } } 27 | assert(checker.logVersion === '1.2.3') 28 | }) 29 | 30 | it('should return 13.0.3b when commit log is "re-release 13.0.3b"', function() { 31 | const checker = new ReleasabilityChecker4t() 32 | checker.exec = ()=> { return { stdout: 're-release 13.0.3b' } } 33 | assert(checker.logVersion === '13.0.3b') 34 | }) 35 | 36 | 37 | it('should return null when commit log is "release v1.2.3"', function() { 38 | const checker = new ReleasabilityChecker4t() 39 | checker.exec = ()=> { return { stdout: 'release v1.2.3' } } 40 | assert(checker.logVersion === null) 41 | }) 42 | 43 | 44 | it('should return null when commit log is "re-release v1.2.3"', function() { 45 | const checker = new ReleasabilityChecker4t() 46 | checker.exec = ()=> { return { stdout: 're-release v1.2.3' } } 47 | assert(checker.logVersion === null) 48 | }) 49 | 50 | }) 51 | 52 | describe('isReleasable',function() { 53 | 54 | it('should return true when commit log is "release 1.2.3"', function() { 55 | const checker = new ReleasabilityChecker4t() 56 | checker.exec = ()=> { return { stdout: 'release 1.2.3' } } 57 | assert(checker.isReleasable === true) 58 | }) 59 | 60 | it('should return false when commit log is "re-release v1.2.3"', function() { 61 | const checker = new ReleasabilityChecker4t() 62 | checker.exec = ()=> { return { stdout: 're-release v1.2.3' } } 63 | assert(checker.isReleasable === false) 64 | }) 65 | 66 | 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/bin/nca-release.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import program from 'commander' 5 | import chalk from 'chalk' 6 | import ReleasabilityChecker from '../lib/releasability-checker' 7 | import ReleaseExecutor from '../lib/release-executor' 8 | 9 | 10 | export default function run() { 11 | 12 | program 13 | .option('--prefix ', 'version prefix') 14 | .option('--branch', 'create branch') 15 | .option('--shrinkwrap', 'make shrinkwrap.json') 16 | .parse(process.argv) 17 | 18 | const {prefix, branch, shrinkwrap} = program 19 | 20 | const checker = new ReleasabilityChecker() 21 | if (!checker.isReleasable) { 22 | console.error(chalk.yellow(checker.warnMessage)) 23 | return process.exit(0) 24 | } 25 | const version = prefix + checker.logVersion 26 | 27 | // release 28 | const executor = new ReleaseExecutor(console.log) 29 | const result = executor.release(version, shrinkwrap, branch) 30 | if (result) { 31 | console.log(chalk.green(`The tag "${version}" was successfully released.`)) 32 | } 33 | else { 34 | const { CIRCLE_PROJECT_USERNAME, CIRCLE_PROJECT_REPONAME } = process.env 35 | console.log(chalk.red(SHOW_HOW_TO_RELEASE_IN_CIRCLE_CI(CIRCLE_PROJECT_USERNAME, CIRCLE_PROJECT_REPONAME))) 36 | process.exit(1) 37 | } 38 | 39 | // npm publish 40 | const {NPM_EMAIL, NPM_AUTH} = process.env 41 | if (NPM_EMAIL && NPM_AUTH) { 42 | const npmVersion = executor.publishNpm(NPM_EMAIL, NPM_AUTH) 43 | if (npmVersion) { 44 | console.log(chalk.green(`npm publish "${npmVersion}" succeeded.`)) 45 | } 46 | else { 47 | console.log(chalk.red('npm publish failed.')) 48 | } 49 | } 50 | else { 51 | const { CIRCLE_PROJECT_USERNAME, CIRCLE_PROJECT_REPONAME } = process.env 52 | console.log(SHOW_HOW_TO_NPM_PUBLISH(CIRCLE_PROJECT_USERNAME, CIRCLE_PROJECT_REPONAME)) 53 | } 54 | } 55 | 56 | 57 | const SHOW_HOW_TO_RELEASE_IN_CIRCLE_CI = (userName: string, repoName: string): string => ` 58 | ---------------------------------------------------------------------- 59 | Release failed. 60 | 61 | In most cases, it is due to the ssh key registered in CircleCI. 62 | Check the key have permission to write to github. 63 | 64 | https://circleci.com/gh/${userName}/${repoName}/edit#checkout 65 | 66 | ---------------------------------------------------------------------- 67 | ` 68 | 69 | 70 | const SHOW_HOW_TO_NPM_PUBLISH = (userName: string, repoName: string): string => ` 71 | ----------------------------------------------------------------------------------------------------- 72 | 'npm publish' was not executed as $NPM_AUTH and $NPM_EMAIL environment variables does not exist. 73 | 74 | Set it at 75 | https://circleci.com/gh/${userName}/${repoName}/edit#env-vars 76 | 77 | Name: NPM_AUTH 78 | Value: (value of '_auth' at your .npmrc after 'npm login') 79 | 80 | Name: NPM_EMAIL 81 | Value: (your email registered to npm) 82 | ----------------------------------------------------------------------------------------------------- 83 | ` 84 | 85 | if (require.main === module) run() 86 | -------------------------------------------------------------------------------- /src/bin/nca-bmp.js: -------------------------------------------------------------------------------- 1 | /*eslint no-console: 0 */ 2 | // @flow 3 | 4 | import {which} from 'shelljs' 5 | import exec from '../util/exec' 6 | import program from 'commander' 7 | import fs from 'fs' 8 | import chalk from 'chalk' 9 | import WorkingDirectory from '../lib/working-directory' 10 | import PackageJSONLoader from '../lib/package-json-loader' 11 | import AutoreleaseYml from '../lib/autorelease-yml' 12 | import generateCircleYml from './nca-generate' 13 | 14 | const COMMAND_DESC = ` 15 | bump-level: 16 | p: patch level (0.0.1) 17 | m: minor level (0.1.0) 18 | j: major level (1.0.0) 19 | r: re-release (0.0.0) 20 | ` 21 | 22 | const optionNames = { 23 | p: 'patch', 24 | m: 'minor', 25 | j: 'major', 26 | } 27 | 28 | 29 | export default function run() { 30 | 31 | program 32 | .arguments('', /[pmjr]/) 33 | .option('-s, --skipCircle', 'No generation of circle.yml') 34 | .description(COMMAND_DESC) 35 | .parse(process.argv) 36 | 37 | const arg = program.args[0] 38 | 39 | if (!arg) { 40 | program.help() 41 | } 42 | 43 | const bin = getBmpBin() 44 | 45 | if (!bin) { 46 | console.log(chalk.red(HOW_TO_INSTALL_BMP_OR_YANGPAO)) 47 | process.exit(1) 48 | } 49 | 50 | if (program.skipCircle) { 51 | console.log('skip generating circle.yml') 52 | } 53 | else { 54 | generateCircleYml(true) // skip showing what to do next 55 | console.log(HOW_TO_SKIP_GENERATION_OF_CIRCLE_YML) 56 | } 57 | 58 | const verb = arg === 'r' ? 're-release' : 'release' 59 | 60 | const optionName = optionNames[arg] 61 | 62 | if (optionName) { 63 | const rootDir = new WorkingDirectory().resolve() 64 | const arYml = AutoreleaseYml.loadFromDir(rootDir) 65 | 66 | arYml.bmpHooks('pre').forEach(cmd => exec(cmd)) 67 | 68 | exec(`${bin} --${optionName}`) 69 | 70 | arYml.bmpHooks('post').forEach(cmd => exec(cmd)) 71 | } 72 | 73 | const version = getCurrentVersion() 74 | 75 | exec('git add -A') 76 | exec(`git commit --allow-empty -m "${verb} ${version}"`) 77 | console.log(NOTICE_AFTER_BUMPING) 78 | 79 | return 0 80 | } 81 | 82 | 83 | /** 84 | * Get the current version 85 | * @private 86 | */ 87 | function getCurrentVersion(): string { 88 | const cwd = new WorkingDirectory().resolve() 89 | return PackageJSONLoader.load(cwd).version 90 | } 91 | 92 | /** 93 | * Get bmp|yangpao full path 94 | * @private 95 | */ 96 | function getBmpBin(): string { 97 | const cwd = new WorkingDirectory().resolve() 98 | 99 | if (fs.existsSync(cwd + '/.yangpao.toml')) { 100 | return which('yangpao') 101 | } 102 | if (fs.existsSync(cwd + '/.bmp.yml')) { 103 | return which('bmp') 104 | } 105 | } 106 | 107 | 108 | const HOW_TO_SKIP_GENERATION_OF_CIRCLE_YML = ` 109 | 110 | To skip circle.yml generation, 111 | run with --skipCircle (or -s) option. 112 | 113 | ` 114 | 115 | const HOW_TO_INSTALL_BMP_OR_YANGPAO = ` 116 | You need to install one of the version-bumping tools of the followings, 117 | 118 | - [bmp](https://github.com/kt3k/bmp) 119 | - [yangpao](https://github.com/januswel/yangpao) 120 | 121 | ## install bmp 122 | \tgem install bmp 123 | 124 | ## install yangpao 125 | \tgo get github.com/januswel/yangpao 126 | 127 | Make sure to run this command on project root. 128 | ` 129 | 130 | /** 131 | * Show notice after git commit 132 | * @private 133 | */ 134 | const NOTICE_AFTER_BUMPING = ` 135 | ---------------------------------------------------------------------------- 136 | Before pushing to github, make sure the following settings have been done. 137 | 138 | 1. Checkout SSH keys [required] 139 | Confirm CircleCI setting if your user key is registered. 140 | 141 | 2. Set Environment variables for publishing npm [optional] 142 | Name: NPM_AUTH 143 | Value: (value of '_auth' at your .npmrc after 'npm login') 144 | 145 | Name: NPM_EMAIL 146 | Value: (your email registered to npm) 147 | 148 | If you mistakenly ran this command, you can reset by 149 | 150 | git reset HEAD^ 151 | Don't be upset :) 152 | ---------------------------------------------------------------------------- 153 | ` 154 | 155 | if (require.main === module) run() 156 | -------------------------------------------------------------------------------- /src/lib/release-executor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs' 4 | import exec from '../util/exec' 5 | 6 | /** 7 | * Executes release process 8 | */ 9 | export default class ReleaseExecutor { 10 | log: (a: string) => void; 11 | 12 | 13 | constructor(log: (a: string) => void) { 14 | this.log = log 15 | } 16 | 17 | /** 18 | * Release the version 19 | * @public 20 | * @param version version name formatted as X.Y.Z 21 | * @param shrinkwrap [] whether or not to run `npm shrinkwrap` 22 | * @param branch whether or not to release branch 23 | */ 24 | release(version: string, 25 | shrinkwrap: boolean = false, 26 | branch: boolean = false, 27 | remote: string = 'origin'): boolean { 28 | 29 | this.exec(`git checkout -b release-${version}`) 30 | this.ignoreFiles() 31 | 32 | if (shrinkwrap) { 33 | this.addShrinkwrap() 34 | } 35 | 36 | this.exec('git add -A') 37 | this.exec(`git commit -m ${version}`) 38 | this.exec(`git tag ${version}`) 39 | const {code} = this.exec(`git push --force ${remote} ${version}`) 40 | if (!this.isPushSucceeded(code)) { 41 | return false 42 | } 43 | 44 | if (branch) { 45 | this.pushReleaseBranch(version, remote) 46 | } 47 | 48 | // re-install dev-dependent modules 49 | if (shrinkwrap) { 50 | this.log('---- re-installing node_modules after shrinkwrap ----') 51 | this.exec('npm install') 52 | } 53 | return true 54 | } 55 | 56 | /** 57 | * publish npm 58 | * @public 59 | */ 60 | publishNpm(email: string, 61 | auth: string, 62 | path?: string = '.npmrc'): ?string { 63 | 64 | const npmrc = `_auth=${auth}\nemail=${email}\n` 65 | this.write(path, npmrc) 66 | this.exec('cp .releaseignore .npmignore') 67 | const {stdout, code} = this.exec('npm publish') 68 | this.exec('rm .npmignore') 69 | this.exec('rm .npmrc') 70 | 71 | return code === 0 ? stdout.trim().split('@')[1] : null 72 | } 73 | 74 | /** 75 | * ignore files in .releaseignore 76 | * @private 77 | */ 78 | ignoreFiles() { 79 | this.exec('cp .releaseignore .git/info/exclude') 80 | this.exec('git rm .gitignore') 81 | 82 | const filesToRemove = this.exec('git ls-files --full-name -i --exclude-from .releaseignore').stdout 83 | 84 | if (filesToRemove) { 85 | for (const file of filesToRemove.trim().split('\n')) { 86 | this.exec(`git rm --cached ${file}`) 87 | } 88 | } 89 | } 90 | 91 | /** 92 | * Add shrinkwrap.json before release 93 | * 94 | * On npm v2, 95 | * 96 | * - `npm shrinkwrap` omits dev-dependent modules 97 | * - wrongly omits dev-dependent modules which are sub-dependent 98 | * 99 | * On npm v3, 100 | * - `npm shrinkwrap` includes some (not all) of dev-dependent modules 101 | * - after `npm prune --production`, it's ok 102 | * 103 | * Currenlty, the following process is the only way to get the correct result on npm >=v2. 104 | * 105 | * ```sh 106 | * rm -rf node_modules 107 | * npm install --production 108 | * npm shrinkwrap 109 | * ``` 110 | * @see https://github.com/npm/npm/issues/11189 111 | * 112 | * @private 113 | */ 114 | addShrinkwrap() { 115 | this.exec('rm -rf node_modules') 116 | this.exec('npm install --production') 117 | this.exec('npm shrinkwrap') 118 | } 119 | 120 | /** 121 | * @check if push succeeded 122 | * @private 123 | */ 124 | isPushSucceeded(code: number): boolean { 125 | return code === 0 126 | } 127 | 128 | /** 129 | * push release branch after pushing tag 130 | * @private 131 | */ 132 | pushReleaseBranch(version: string, remote: string) { 133 | this.exec('git add -f circle.yml') 134 | this.exec('git commit --allow-empty -m "add circle.yml for release"') 135 | this.exec(`git push --force ${remote} release-${version}`) 136 | } 137 | 138 | 139 | /** 140 | * Write a file with the content 141 | * @private 142 | */ 143 | write(filename: string, content: string) { 144 | fs.writeFileSync(process.cwd() + '/' + filename, content) 145 | } 146 | 147 | /** 148 | * execute a given command 149 | * @private 150 | */ 151 | exec(...args: Array): Object { 152 | return exec(...args) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/lib/circle-yml.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import merge from 'deepmerge' 4 | import yaml from 'js-yaml' 5 | import type AutoreleaseYml from './autorelease-yml' 6 | 7 | /** 8 | * Generator for circle.yml 9 | */ 10 | export default class CircleYml { 11 | 12 | /** 13 | * @public 14 | */ 15 | static generate(arYml: AutoreleaseYml): string { 16 | 17 | const standard = this.standard(arYml) 18 | const custom = arYml.circle 19 | 20 | const merged = merge(standard, custom) 21 | this.modifyPathEnv(merged, standard.machine.environment.PATH) 22 | return yaml.dump(merged, {indent: 2, lineWidth: 120}) 23 | } 24 | 25 | 26 | static modifyPathEnv(circleYml: Object, standardPath: string) { 27 | const {environment} = circleYml.machine 28 | 29 | if (environment.PATH === standardPath) { return } 30 | 31 | const standardPaths = standardPath.split(':') 32 | const paths = environment.PATH.split(':') 33 | 34 | standardPaths 35 | .filter(path => !paths.includes(path)) 36 | .forEach(path => paths.push(path)) 37 | environment.PATH = paths.join(':') 38 | } 39 | 40 | /** 41 | * @private 42 | */ 43 | static standard(arYml: AutoreleaseYml): Object { 44 | return { 45 | general: { 46 | branches: { 47 | ignore: [ 'gh-pages', '/release.*/' ] 48 | } 49 | }, 50 | 51 | machine: { 52 | 53 | environment: { 54 | PATH: './node_modules/.bin:$PATH' 55 | }, 56 | 57 | pre: [ 58 | `git config --global user.name "${arYml.config('git_user_name')}"`, 59 | `git config --global user.email "${arYml.config('git_user_email')}"` 60 | ], 61 | }, 62 | 63 | dependencies: { 64 | post: flat( 65 | arYml.hooks('update_modules', 'pre'), 66 | this.updateModulesCommand(arYml.config('npm_update_depth')), 67 | arYml.hooks('update_modules', 'post') 68 | ) 69 | }, 70 | 71 | deployment: { 72 | create_release_branch: { 73 | branch: ['master'], 74 | commands: flat( 75 | arYml.hooks('release', 'pre'), 76 | this.releaseCommand(arYml), 77 | arYml.hooks('release', 'post'), 78 | 79 | arYml.hooks('gh_pages', 'pre'), 80 | this.ghPagesCommand(arYml.config('create_gh_pages'), arYml.config('gh_pages_dir')), 81 | arYml.hooks('gh_pages', 'post'), 82 | ) 83 | } 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * generate command to update node_modules 90 | * @private 91 | */ 92 | static updateModulesCommand(depth: primitive): string { 93 | if (!depth) { 94 | return 'nca run nca notice update-modules' 95 | } 96 | return `nca run nca update-modules --depth ${depth}` 97 | } 98 | 99 | /** 100 | * generate command to release 101 | * @private 102 | */ 103 | static releaseCommand(arYml: AutoreleaseYml): string { 104 | const options = { 105 | prefix: arYml.config('version_prefix'), 106 | branch: !!arYml.config('create_branch'), 107 | shrinkwrap: !!arYml.config('npm_shrinkwrap') 108 | } 109 | return 'nca release ' + this.optionStr(options) 110 | } 111 | 112 | 113 | /** 114 | * generate command to create gh-pages branch 115 | * @private 116 | */ 117 | static ghPagesCommand(create: primitive, dir: primitive): string { 118 | 119 | if (!create) { 120 | return 'nca run nca notice gh-pages' 121 | } 122 | let command = 'nca run nca gh-pages' 123 | if (dir) { 124 | command += ` --dir ${dir}` 125 | } 126 | return command 127 | } 128 | 129 | /** 130 | * generate command line option string 131 | * @private 132 | */ 133 | static optionStr(options: Object): string { 134 | 135 | return Object.keys(options).map(key => { 136 | 137 | const val = options[key] 138 | 139 | if (typeof val === 'boolean') { return val ? `--${key}` : '' } 140 | 141 | return val != null ? `--${key} ${val}` : '' 142 | }) 143 | .filter(v => v) 144 | .join(' ') 145 | } 146 | 147 | } 148 | 149 | 150 | function flat(...args) { 151 | return args.reduce((arr, v) => { 152 | if (Array.isArray(v)) { 153 | return arr.concat(flat(...v)) 154 | } 155 | else { 156 | arr.push(v) 157 | } 158 | return arr 159 | }, []) 160 | } 161 | -------------------------------------------------------------------------------- /test/spec/lib/autorelease-yml.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import AutoreleaseYml from '../../../src/lib/autorelease-yml' 3 | import assert from 'power-assert' 4 | import fs from 'fs' 5 | import yaml from 'js-yaml' 6 | import {resolve} from 'path' 7 | 8 | 9 | describe('AutoreleaseYml', function() { 10 | 11 | before(function() { 12 | this.dataDir = resolve(__dirname + '/../data') 13 | this.path = resolve(__dirname + '/../data/.autorelease.yml') 14 | this.data = yaml.safeLoad(fs.readFileSync(this.path, 'utf8')) 15 | }) 16 | 17 | 18 | describe('loadFromDir',function() { 19 | 20 | it('can load .autorelease.yml file with a given directory', function() { 21 | const arYml = AutoreleaseYml.loadFromDir(this.dataDir) 22 | assert.deepEqual(arYml.__data, this.data) 23 | }) 24 | }) 25 | 26 | 27 | 28 | describe('checkFormat',function() { 29 | 30 | it('should be ok when no file exists', function() { 31 | const arYml = new AutoreleaseYml('invalid path') 32 | assert.doesNotThrow(x => { arYml.checkFormat() }) 33 | }) 34 | 35 | 36 | it('should be ok with empty data', function() { 37 | const arYml = new AutoreleaseYml(this.path) 38 | arYml.__data = {} 39 | 40 | assert.doesNotThrow(x => { arYml.checkFormat() }) 41 | }) 42 | 43 | 44 | it('should throw error when invalid field exists in root', function() { 45 | const arYml = new AutoreleaseYml(this.path) 46 | arYml.__data.xxxx = {} 47 | 48 | assert.throws(x => { arYml.checkFormat() }, /Unknown field: "xxxx"/) 49 | }) 50 | 51 | 52 | context('about hooks',function() { 53 | 54 | it('should be ok with empty hooks', function() { 55 | const arYml = new AutoreleaseYml(this.path) 56 | arYml.__data.hooks = {} 57 | 58 | assert.doesNotThrow(x => { arYml.checkFormat() }) 59 | }) 60 | 61 | 62 | it('should throw error when invalid field exists in hooks.', function() { 63 | const arYml = new AutoreleaseYml(this.path) 64 | arYml.__data.hooks.ghpages = {} 65 | 66 | assert.throws(x => { arYml.checkFormat() }, /Unknown field: "hooks.ghpages"/) 67 | }) 68 | 69 | it('should be ok when hooks.update_modules contains only "pre" field', function() { 70 | const arYml = new AutoreleaseYml(this.path) 71 | arYml.__data.hooks.update_modules = {pre: ['echo AutoRelease!']} 72 | 73 | assert.doesNotThrow(x => { arYml.checkFormat() }) 74 | }) 75 | 76 | 77 | it('should throw error when hooks.update_modules is empty', function() { 78 | const arYml = new AutoreleaseYml(this.path) 79 | arYml.__data.hooks.update_modules = {} 80 | 81 | assert.throws(x => { arYml.checkFormat() }, /Field not found: "hooks\.update_modules\.pre"/) 82 | }) 83 | 84 | 85 | it('should throw error when invalid field exists in hooks.update_modules', function() { 86 | const arYml = new AutoreleaseYml(this.path) 87 | arYml.__data.hooks.update_modules.before = {} 88 | 89 | assert.throws(x => { arYml.checkFormat() }, /Unknown field: "hooks.update_modules.before"/) 90 | }) 91 | 92 | 93 | it('should be ok when hooks.update_modules is array', function() { 94 | const arYml = new AutoreleaseYml(this.path) 95 | arYml.__data.hooks.update_modules.pre = ['echo "CureApp"', 'rm .gitignore'] 96 | assert.doesNotThrow(x => { arYml.checkFormat() }) 97 | }) 98 | 99 | 100 | it('should be ok when hooks.update_modules is string', function() { 101 | const arYml = new AutoreleaseYml(this.path) 102 | arYml.__data.hooks.update_modules.pre = 'echo "CureApp"' 103 | assert.doesNotThrow(x => { arYml.checkFormat() }) 104 | }) 105 | 106 | it('should throw error when hooks.update_modules is object', function() { 107 | const arYml = new AutoreleaseYml(this.path) 108 | arYml.__data.hooks.update_modules.pre = { cmd1: 'echo "CureApp"' } 109 | assert.throws(x => { arYml.checkFormat() }, /It should be an array or a string/) 110 | }) 111 | 112 | }) 113 | 114 | context('about config',function() { 115 | 116 | it('should be ok with empty config', function() { 117 | 118 | const arYml = new AutoreleaseYml(this.path) 119 | arYml.__data.config = {} 120 | 121 | assert.doesNotThrow(x => { arYml.checkFormat() }) 122 | }) 123 | 124 | it('should throw error when invalid field exists in config', function() { 125 | const arYml = new AutoreleaseYml(this.path) 126 | arYml.__data.config = { isCucumber: true } 127 | 128 | assert.throws(x => { arYml.checkFormat() }, /Unknown field: "config.isCucumber"/) 129 | }) 130 | 131 | it('should throw error when config.git_user_email is an object', function() { 132 | const arYml = new AutoreleaseYml(this.path) 133 | arYml.__data.config = { git_user_email: {domain: 'gmail.com', account: 'shinout310'} } 134 | 135 | assert.throws(x => { arYml.checkFormat() }, /It should not be an object/) 136 | }) 137 | 138 | }) 139 | 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /test/spec/lib/release-executor.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import ReleaseExecutor from '../../../src/lib/release-executor' 3 | import assert from 'power-assert' 4 | 5 | import fs from 'fs' 6 | import {resolve} from 'path' 7 | import {exec} from 'shelljs' 8 | 9 | class ReleaseExecutor4t extends ReleaseExecutor { 10 | write: Function; 11 | exec: Function; 12 | } 13 | 14 | describe('ReleaseExecutor', function() { 15 | 16 | beforeEach(function() { 17 | const log = (v: string) => {} 18 | this.executedCommands = [] 19 | this.executor = new ReleaseExecutor4t(log) 20 | this.executor.write = x => {} 21 | this.executor.exec = x => { 22 | this.executedCommands.push(x) 23 | return { 24 | stdout: 'stdout mock', 25 | stderr: 'stderr mock', 26 | code: 0 27 | } 28 | } 29 | }) 30 | 31 | 32 | describe('release', function() { 33 | 34 | context('when only 1st argument is passed,', function() { 35 | 36 | beforeEach(function() { 37 | this.executor.release('v4.5.2') 38 | }) 39 | 40 | it('should execute 9 commands', function() { 41 | assert(this.executedCommands.length === 9) 42 | }) 43 | 44 | it('should create release branch', function() { 45 | assert(this.executedCommands[0] === 'git checkout -b release-v4.5.2') 46 | }) 47 | 48 | it('should copy .releaseignore to .git/info/exclude', function() { 49 | assert(this.executedCommands[1] === 'cp .releaseignore .git/info/exclude') 50 | }) 51 | 52 | it('should remove .gitignore', function() { 53 | assert(this.executedCommands[2] === 'git rm .gitignore') 54 | }) 55 | 56 | it('should get ignored files by .releaseignore', function() { 57 | const expectedCommand = 'git ls-files --full-name -i --exclude-from .releaseignore' 58 | assert(this.executedCommands[3] === expectedCommand) 59 | 60 | const {stdout, stderr} = exec(expectedCommand, {silent: true}) 61 | 62 | assert(stderr === '') 63 | 64 | const ignored = stdout.trim().split('\n') 65 | assert(ignored.length > 3) 66 | 67 | ignored.forEach(filename => { 68 | const path = resolve(__dirname + '/../../../' + filename) 69 | assert(fs.existsSync(path)) 70 | }) 71 | }) 72 | 73 | it('should remove ignored files', function() { 74 | const prevResult = 'stdout mock' 75 | assert(this.executedCommands[4] === `git rm --cached ${prevResult}`) 76 | }) 77 | 78 | it('should add all untracked changes, commit them, create a tag and push to origin', function() { 79 | assert(this.executedCommands[5] === 'git add -A') 80 | assert(this.executedCommands[6] === 'git commit -m v4.5.2') 81 | assert(this.executedCommands[7] === 'git tag v4.5.2') 82 | assert(this.executedCommands[8] === 'git push --force origin v4.5.2') 83 | }) 84 | 85 | it('should all untracked changes', function() { 86 | assert(this.executedCommands[5] === `git add -A`) 87 | }) 88 | 89 | }) 90 | 91 | context('when 2nd argument:shrinkwrap is passed,', function() { 92 | 93 | beforeEach(function() { 94 | this.executor.release('v4.5.3', true) 95 | }) 96 | 97 | it('should execute 13 commands', function() { 98 | assert(this.executedCommands.length === 13) 99 | }) 100 | 101 | it('should run "npm shrinkwrap"', function() { 102 | assert(this.executedCommands[5] === 'rm -rf node_modules') 103 | assert(this.executedCommands[6] === 'npm install --production') 104 | assert(this.executedCommands[7] === 'npm shrinkwrap') 105 | }) 106 | 107 | it('should re-install modules', function() { 108 | assert(this.executedCommands[12] === 'npm install') 109 | }) 110 | }) 111 | 112 | context('when 3nd argument:branch is passed,', function() { 113 | 114 | beforeEach(function() { 115 | this.executor.release('v4.5.4', null, true) 116 | }) 117 | 118 | it('should execute 12 commands', function() { 119 | assert(this.executedCommands.length === 12) 120 | }) 121 | 122 | it('should add circle.yml, commit it and push it', function() { 123 | assert(this.executedCommands[9] === 'git add -f circle.yml') 124 | assert(this.executedCommands[10] === 'git commit --allow-empty -m "add circle.yml for release"') 125 | assert(this.executedCommands[11] === 'git push --force origin release-v4.5.4') 126 | }) 127 | }) 128 | 129 | context('when 4th argument:remote is passed,', function() { 130 | 131 | beforeEach(function() { 132 | this.executor.release('v4.5.5', null, null, 'github') 133 | }) 134 | 135 | it('should push to the given location', function() { 136 | assert(this.executedCommands[8] === 'git push --force github v4.5.5') 137 | }) 138 | }) 139 | }) 140 | 141 | describe('publishNpm', function() { 142 | 143 | beforeEach(function() { 144 | this.write = (filename, content) => { 145 | assert(filename === '.npmrc') 146 | assert(content === '_auth=xyz\nemail=x@g.com\n') 147 | } 148 | }) 149 | 150 | 151 | it('should return version when npm publish succeeded', function() { 152 | 153 | this.executor.exec = x => { 154 | return { stdout: 'node-circleci-autorelease@0.1.2', code: 0 } 155 | } 156 | assert(this.executor.publishNpm('x@g.com', 'xyz') === '0.1.2') 157 | }) 158 | 159 | it('should return null when npm publish failed', function() { 160 | 161 | this.executor.exec = x => { 162 | return { stderr: '403', code: 1 } 163 | } 164 | assert(this.executor.publishNpm('x@g.com', 'xyz') === null) 165 | }) 166 | 167 | it('should add .npmignore and publish', function() { 168 | 169 | this.executor.publishNpm('x@g.com', 'xyz') 170 | 171 | assert(this.executedCommands[0] === 'cp .releaseignore .npmignore') 172 | assert(this.executedCommands[1] === 'npm publish') 173 | assert(this.executedCommands[2] === 'rm .npmignore') 174 | }) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /src/lib/autorelease-yml.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import fs from 'fs' 4 | import yaml from 'js-yaml' 5 | import {join} from 'path' 6 | import merge from 'deepmerge' 7 | 8 | 9 | /** 10 | * Reader/Writer of .autorelease.yml 11 | */ 12 | export default class AutoreleaseYml { 13 | 14 | __data: Object 15 | loaded: boolean 16 | 17 | static filename = '.autorelease.yml' 18 | 19 | 20 | /** 21 | * default values, used when no file exists 22 | */ 23 | static get defaultValues() { 24 | return { 25 | hooks: { 26 | update_modules: { pre: ['echo "before update-modules"'], post: ['echo "after update-modules"'] }, 27 | release: { pre: ['echo "before release"'], post: ['echo "after release"'] }, 28 | gh_pages: { pre: ['echo "before gh-pages"'], post: ['echo "after gh-pages"'] } 29 | }, 30 | config: this.defaultConfig, 31 | circle: {} 32 | } 33 | } 34 | 35 | /** 36 | * default configs 37 | */ 38 | static get defaultConfig() { 39 | return { 40 | git_user_name: 'CircleCI', 41 | git_user_email: 'circleci@example.com', 42 | 43 | // options for nca update-modules 44 | npm_update_depth: 0, 45 | 46 | // options for nca release 47 | version_prefix: 'v', 48 | create_branch: false, 49 | npm_shrinkwrap: false, 50 | 51 | // options for nca gh-pages 52 | create_gh_pages: false, 53 | gh_pages_dir: null, 54 | 55 | } 56 | } 57 | 58 | /** 59 | * load yml file in the given directory 60 | * @public 61 | */ 62 | static loadFromDir(dirPath: string): AutoreleaseYml { 63 | const path = join(dirPath, this.filename) 64 | return new AutoreleaseYml(path) 65 | } 66 | 67 | 68 | constructor(path: string) { 69 | 70 | this.loaded = false 71 | 72 | try { 73 | this.__data = yaml.safeLoad(fs.readFileSync(path, 'utf8')) 74 | this.loaded = true 75 | } 76 | // if .autorelease.yml is not found, silently prepare a default object 77 | catch (e) { 78 | this.__data = this.constructor.defaultValues 79 | } 80 | } 81 | 82 | 83 | /** 84 | * get hook commands (except bmp hook) 85 | * @public 86 | * @param name hook name 87 | * @param timing one of [update_modules release gh_pages] 88 | */ 89 | hooks(name: string, timing: string): Array { 90 | if (!this.hookNames.includes(name)) throw new Error(`Invalid hook name: "${name}" was given.`) 91 | if (!this.__data.hooks) return [] 92 | const hookObjs = this.__data.hooks[name] 93 | if (!hookObjs) return [] 94 | 95 | const hooks = hookObjs[timing] 96 | const hookArr = Array.isArray(hooks) ? hooks : hooks ? [hooks] : [] 97 | return hookArr.map(this.addOkCommandBeforeHook, this) // Notice that "this" will change 98 | } 99 | 100 | 101 | /** 102 | * get bmp hook commands 103 | * @public 104 | * @param timing one of [update_modules release gh_pages] 105 | */ 106 | bmpHooks(timing: string): Array { 107 | if (!this.__data.hooks) return [] 108 | const hookObjs = this.__data.hooks.bmp 109 | if (!hookObjs) return [] 110 | 111 | const hooks = hookObjs[timing] 112 | return Array.isArray(hooks) ? hooks : hooks ? [hooks] : [] 113 | } 114 | 115 | 116 | /** 117 | * attach 'nca run' before command 118 | */ 119 | addOkCommandBeforeHook(hookCommand: string): string { 120 | // "--" cannot be recognized by node.js 121 | return `nca run ${hookCommand.split(' -- ').join(' --- ')}` 122 | } 123 | 124 | 125 | /** 126 | * @public 127 | */ 128 | config(key: string): primitive { 129 | const val = this.__data.config ? this.__data.config[key] : undefined 130 | return (val != null)? val : this.constructor.defaultConfig[key] 131 | } 132 | 133 | /** 134 | * @public 135 | */ 136 | get circle(): Object { 137 | return Object.assign({}, this.__data.circle) 138 | } 139 | 140 | 141 | /** 142 | * set node version to circle section 143 | * @public 144 | * @param version node version 145 | */ 146 | setNodeVersion(version: string) { 147 | this.__data.circle = merge(this.__data.circle, {machine: {node: {version}}} ) 148 | } 149 | 150 | /** 151 | * get YAML format 152 | */ 153 | toString(): string { 154 | return yaml.dump(this.__data, {indent: 2, lineWidth: 120}) 155 | } 156 | 157 | /** 158 | * save .autorelease.yml to the given directory 159 | */ 160 | saveTo(dir: string) { 161 | const path = join(dir, this.constructor.filename) 162 | fs.writeFileSync(path, this.toString()) 163 | } 164 | 165 | /** 166 | * check format of autorelease.yml 167 | * @public 168 | */ 169 | checkFormat() { 170 | 171 | if (!this.__data) throw new Error('Yaml has not been loaded.') 172 | 173 | Object.keys(this.__data).forEach(name => { 174 | if (! this.rootFieldNames.includes(name)) { 175 | throw new Error(`Unknown field: "${name}"`) 176 | } 177 | }) 178 | 179 | const { hooks, config, circle } = this.__data 180 | 181 | if (hooks) this.checkHooksFormat(hooks) 182 | if (config) this.checkConfigFormat(config) 183 | if (circle) this.checkCircleFormat(circle) 184 | } 185 | 186 | 187 | /** 188 | * @private 189 | */ 190 | get rootFieldNames(): Array { 191 | return ['hooks', 'config', 'circle'] 192 | } 193 | 194 | 195 | 196 | /** 197 | * @private 198 | */ 199 | get hookNames(): Array { 200 | return ['update_modules', 'release', 'gh_pages', 'bmp'] 201 | } 202 | 203 | /** 204 | * @private 205 | */ 206 | get configNames(): Array { 207 | return Object.keys(this.constructor.defaultConfig) 208 | } 209 | 210 | 211 | 212 | /** 213 | * @private 214 | */ 215 | checkHooksFormat(hooks: Object) { 216 | 217 | Object.keys(hooks).forEach(name => { 218 | if (! this.hookNames.includes(name)) { 219 | throw new Error(`Unknown field: "hooks.${name}"`) 220 | } 221 | 222 | if (!hooks[name].pre && !hooks[name].post) { 223 | throw new Error(`Field not found: "hooks.${name}.pre" or "hooks.${name}.post" is required.`) 224 | } 225 | 226 | Object.keys(hooks[name]).forEach(subname => { 227 | if (! ['pre', 'post'].includes(subname)) { 228 | throw new Error(`Unknown field: "hooks.${name}.${subname}"`) 229 | } 230 | this.checkHookCommandFormat(hooks[name][subname], name, subname) 231 | }) 232 | }) 233 | } 234 | 235 | 236 | /** 237 | * @private 238 | */ 239 | checkHookCommandFormat(cmds: any, name: string, subname: string) { 240 | if (typeof cmds === 'string') { 241 | return 242 | } 243 | 244 | if (Array.isArray(cmds)) { 245 | if (typeof cmds === 'string') { 246 | return 247 | } 248 | return 249 | } 250 | 251 | throw new Error(`Invalid type: "hooks.${name}.${subname}". It should be an array or a string.`) 252 | } 253 | 254 | 255 | /** 256 | * @private 257 | */ 258 | checkConfigFormat(config: Object) { 259 | Object.keys(config) 260 | 261 | .filter(name => config[name] != null) 262 | .forEach(name => { 263 | if (!this.configNames.includes(name)) { 264 | throw new Error(`Unknown field: "config.${name}"`) 265 | } 266 | 267 | if (typeof config[name] === 'object') { 268 | throw new Error(`Invalid type: "config.${name}". "It should not be an object."`) 269 | } 270 | 271 | }) 272 | } 273 | 274 | /** 275 | * @private 276 | */ 277 | checkCircleFormat(circle: Object) { 278 | if (!circle) return 279 | 280 | // TODO 281 | return 282 | } 283 | 284 | 285 | } 286 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-circleci-autorelease 2 | **Note:** This library is no longer maintained. 3 | 4 | Autorelease your node packages. 5 | 6 | - [tag](#3-commit-and-push-it) 7 | - [branch](#config-field) 8 | - [gh-pages](#config-about-gh-pages) 9 | - [npm publish](#npm-publish) 10 | 11 | It looks up the latest commit log and extracts release name. 12 | 13 | ```sh 14 | git commit -m 'release 1.2.3' 15 | git push origin master 16 | ``` 17 | 18 | CircleCI creates tag `1.2.3`. 19 | 20 | You can add and remove files for release tag via hooks. 21 | 22 | # installation 23 | 24 | Just run the following command on your Node.js project. 25 | ```sh 26 | npm install --save-dev node-circleci-autorelease 27 | ``` 28 | 29 | 30 | ## install cli for global (optional) 31 | Use [nca-cli](https://github.com/CureApp/nca-cli) for omitting `$(npm bin)/`. 32 | 33 | ```sh 34 | npm install -g nca-cli 35 | ``` 36 | 37 | This tiny module calls local `nca` command without absolute path (e.g. `$(npm bin)/`). 38 | 39 | As `nca-cli` itself doesn't contain `node-circleci-autorelease`, you don't have to consider about version inconsistencies. 40 | 41 | **Notice: the following sample commands call global `nca`.** 42 | 43 | **If you skip installing `nca-cli`, attach `$(npm bin)/` before `nca` command.** 44 | 45 | 46 | # usage 47 | 48 | ## 1. initializing 49 | 50 | ```bash 51 | nca init 52 | ``` 53 | 54 | Two setting files will be generated. 55 | 56 | 1. `.autorelease.yml`: config file. 57 | 2. `.releaseignore`: files/patterns to be ignored in release. The same format as .gitignore. 58 | 59 | You can set current node version with `--node` option. 60 | 61 | 62 | ```bash 63 | nca init --node 64 | ``` 65 | 66 | 67 | ## 2. generate circle.yml 68 | 69 | ```bash 70 | nca generate 71 | ``` 72 | 73 | It creates `circle.yml` to your current working directory for auto-release. 74 | 75 | ## 3. commit and push it 76 | 77 | Push to master branch with a specific commit message. 78 | 79 | ```sh 80 | git commit -m 'release X.Y.Z' 81 | git push origin master 82 | ``` 83 | 84 | Then, CircleCI detects the commit message pattern and creates a tag `X.Y.Z` 85 | (See [version-bumping](#version-bumping) for automated commit.) 86 | 87 | # configuration 88 | 89 | Edit `.autorelease.yml`. It will show below format. 90 | 91 | ```yaml 92 | hooks: 93 | gh_pages: 94 | pre: 95 | - npm run generate-doc 96 | config: 97 | version_prefix: v 98 | create_gh_pages: true 99 | create_gh_pages: doc 100 | circle: 101 | machine: 102 | environment: 103 | node: 104 | version: 6.2.0 105 | ``` 106 | There are three fields in this format. 107 | 108 | - config: config fields 109 | - hooks: hook commands 110 | - circle: totally compatible with circle.yml 111 | 112 | ## config field 113 | 114 | | key | description | default | 115 | | :--------------- | :---------------------------------- | :------------------- | 116 | | git_user_name | user name of the release commit | CircleCI | 117 | | git_user_email | user email of the release commit | circleci@example.com | 118 | | npm_update_depth | --depth option to "npm update" | 0 ( = no run) | 119 | | version_prefix | prefix of tags to be created | v | 120 | | create_branch | create release branch or not | false | 121 | | npm_shrinkwrap | run "npm shrinkwrap" before release | false | 122 | | create_gh_pages | create gh-pages branch or not | false | 123 | | gh_pages_dir | directory to publish on gh-pages | (null) | 124 | 125 | ### npm_update_depth 126 | 127 | node-circleci-autorelease tries to update node_modules via `npm update` everytime after `npm install`. 128 | `npm_update_depth` config is the depth of the update. 129 | By default, 0 is set and `npm update` will never occur. 130 | 131 | ```yaml 132 | config: 133 | npm_update_depth: 3 134 | ``` 135 | 136 | ### version_prefix 137 | 138 | To release `v1.2.3`, you should set 139 | 140 | ```yaml 141 | config: 142 | version_prefix: v 143 | ``` 144 | 145 | at your .autorelease.yml and make a commit with message 146 | 147 | release 1.2.3 148 | 149 | ### npm_shrinkwrap 150 | 151 | node-circleci-autorelease tries to fix all the node_modules versions before release 152 | by the executed ones using `npm shrinkwrap`. To enable this function, 153 | 154 | ```yaml 155 | config: 156 | shrinkwrap: true 157 | ``` 158 | 159 | ### gh-pages configuration 160 | 161 | To release `gh-pages` branch, you should set 162 | 163 | ```yaml 164 | config: 165 | create_gh_pages: true 166 | gh_pages_dir: doc 167 | ``` 168 | 169 | If `gh_pages_dir` is set, only the directory is hosted. 170 | 171 | ### example 172 | 173 | ```yaml 174 | --- 175 | config: 176 | git_user_name: shinout 177 | git_user_email: shinout310@gmail.com 178 | npm_update_depth: 5 179 | version_prefix: v 180 | create_branch: true 181 | npm_shrinkwrap: true 182 | create_gh_pages: true 183 | gh_pages_dir: doc 184 | ``` 185 | 186 | ## hooks field 187 | 188 | You can register commands before/after the following events. 189 | 190 | - update_modules: before/after running `npm update` 191 | - release: before/after releasing process 192 | - gh_pages: before/after creating gh-pages branch 193 | - bmp: before/after bumping (in `nca bmp` command) 194 | 195 | Each section must have "pre" or "post" section containing a command or list of commands. 196 | 197 | ### example 198 | 199 | 1. Convert ES6+ files in src to dist using babel 200 | 201 | ```yaml 202 | --- 203 | hooks: 204 | update_modules: 205 | post: 206 | - babel src -d dist 207 | ``` 208 | 209 | 210 | 2. Add timestamp 211 | 212 | ```yaml 213 | --- 214 | hooks: 215 | release: 216 | pre: date "+%s" > timestamp 217 | ``` 218 | 219 | 3. Add documentation files before release using documentationjs 220 | 221 | ```yaml 222 | --- 223 | hooks: 224 | gh_pages: 225 | pre: documentation build --format html -o doc src/**/* 226 | ``` 227 | 228 | ## circle field 229 | 230 | Write your custom circle.yml setting here. 231 | **Don't write circle.yml directly**, `nca generate` command will generate it automatically. 232 | 233 | ### example 234 | 235 | ```yaml 236 | --- 237 | circle: 238 | general: 239 | branches: 240 | ignore: 241 | - xxx 242 | machine: 243 | environment: 244 | ABC: 123 245 | dependencies: 246 | post: 247 | - npm run xxx 248 | ``` 249 | 250 | # .releaseignore file 251 | 252 | Files/patterns to be ignored in release. 253 | Format is the same as .gitignore. 254 | 255 | ## example 256 | 257 | ```text 258 | # dot files 259 | .* 260 | 261 | # npm https://www.npmjs.com 262 | node_modules 263 | 264 | # documentations 265 | /doc 266 | 267 | # source files 268 | /src 269 | 270 | # test files 271 | /test 272 | 273 | # development tools 274 | /tools 275 | 276 | # CircleCI cetting https://circleci.com 277 | circle.yml 278 | 279 | # debug logs 280 | *.log 281 | ``` 282 | 283 | # version bumping 284 | 285 | `node-circleci-autorelease` can bump versions with version-bumping tools. 286 | Two bumping tools are available. 287 | 288 | - bmp: [kt3k/bmp](https://github.com/kt3k/bmp). 289 | - bmp: [januswel/yangpao](https://github.com/januswel/yangpao). 290 | 291 | ```sh 292 | gem install bmp 293 | ``` 294 | 295 | or 296 | 297 | ```sh 298 | go get github.com/januswel/yangpao 299 | ``` 300 | 301 | ## usage 302 | 303 | ```bash 304 | nca bmp p 305 | nca bmp m 306 | nca bmp j 307 | nca bmp r 308 | ``` 309 | 310 | They update version and commit with a message except running `nca bmp r`. 311 | These commands also update circle.yml automatically. `--skipCircle` or its alias `-s` option skips updating circle.yml. 312 | 313 | ```bash 314 | nca bmp p --skipCircle 315 | ``` 316 | 317 | ### re-release 318 | 319 | `nca bmp r` doesn't bump version. Instead, it makes an empty commit with the following message: 320 | 321 | re-release X.Y.Z 322 | 323 | where X.Y.Z is the current version. This is useful when the last release is failed. 324 | 325 | This feature is disabled by default. 326 | 327 | ### bmp hooks 328 | ```yaml 329 | hooks: 330 | bmp: 331 | pre: 332 | - echo "bmp start" 333 | post: 334 | - echo "bmp end" 335 | ``` 336 | You can set bumping hooks in `.autorelease.yml`. 337 | 338 | # npm publish 339 | 340 | Enable publishing your project by setting two environment variables at CircleCI. 341 | 342 | ```sh 343 | NPM_AUTH # "_auth" of your .npmrc 344 | NPM_EMAIL # "email" of your .npmrc 345 | ``` 346 | 347 | then CircleCI automatically runs `npm publish`. 348 | 349 | # DRY RUN 350 | 351 | ```sh 352 | DRY_RUN=1 nca 353 | ``` 354 | 355 | # JavaScript API 356 | 357 | Run command with args. 358 | 359 | es6+ 360 | 361 | ```js 362 | import {run} from 'node-circleci-autorelease' 363 | nca.run(['bmp', 'p', '-s']) 364 | ``` 365 | 366 | commonjs 367 | 368 | ```js 369 | var nca = require('node-circleci-autorelease') 370 | nca.run(['bmp', 'p', '-s']) 371 | ``` 372 | 373 | Note that 2nd argument should be 374 | 375 | # LICENSE 376 | 377 | MIT 378 | --------------------------------------------------------------------------------