diff --git a/.circleci/config.yml b/.circleci/config.yml index 465f2f3..b36e707 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,9 +26,9 @@ node10linux: &node10linux restore_modules_cache: &restore_modules_cache restore_cache: keys: - - release-cli-{{ checksum "yarn.lock" }} + - release-cli-v2-{{ checksum "yarn.lock" }} # fallback to using the latest cache if no exact match is found - - release-cli- + - release-cli-v2 # jobinstall: &jobinstall # steps: diff --git a/.nycrc.json b/.nycrc.json index eb7c934..a979acd 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -6,5 +6,5 @@ "cache": true, "check-coverage": true, "reporter": ["lcov", "text"], - "exclude": ["test"] + "exclude": ["tests"] } diff --git a/cli.js b/cli.js index 5f79edd..670a87a 100644 --- a/cli.js +++ b/cli.js @@ -6,6 +6,7 @@ const path = require('path'); const proc = require('process'); const parser = require('mri'); const esmLoader = require('esm'); +const prettyConfig = require('@tunnckocore/pretty-config'); const esmRequire = esmLoader(module); @@ -23,7 +24,20 @@ const argv = parser(proc.argv.slice(2), { default: { cwd: proc.cwd(), ci: true, + 'sign-git-tag': false, + 'git-tag-version': false, + }, + alias: { + 'dry-run': ['dryRun', 'dry'], + 'sign-git-tag': ['signGitTag'], + 'git-tag-version': ['gitTagVersion'], }, }); -cli(argv).catch(console.error); +prettyConfig('standard-release', { cwd: argv.cwd }) + .then((cfg) => { + const opts = Object.assign({}, argv, cfg); + + return cli(opts, proc.env); + }) + .catch(console.error); diff --git a/package.json b/package.json index 412cdb7..1d7df3c 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,15 @@ "license": "Apache-2.0", "licenseStart": "2017", "scripts": { + "standard-release": "node cli.js", "docs": "docks --outfile .verb.md && verb", "lint": "eslint '**/*.js' --cache --fix --quiet --format codeframe", - "test-only": "node -r esm test/index.js", - "test": "nyc --require esm node test/index.js", + "test-only": "node -r esm tests/index.js", + "test": "nyc --require esm node tests/index.js", "precommit": "yarn run lint && yarn run test-only", "commit": "yarn dry", "dry": "git add -A && git status --porcelain && gitcommit", - "release": "node cli.js" + "release": "yarn standard-release" }, "engines": { "node": "^8.10.0 || >=10.13.0" @@ -21,16 +22,19 @@ }, "dependencies": { "@tunnckocore/execa": "^2.2.1", + "@tunnckocore/pretty-config": "^0.5.1", "dedent": "^0.7.0", "detect-next-version": "^4.1.0", "esm": "^3.2.0", "git-commits-since": "^2.0.4", "is-ci": "^2.0.0", - "mri": "^1.1.4" + "mri": "^1.1.4", + "rc": "^1.2.8" }, "devDependencies": { "@tunnckocore/config": "^1.0.3", "asia": "^1.0.0-rc.31", + "dedent": "^0.7.0", "docks": "^0.7.0", "fs-extra": "^7.0.1", "simple-git": "^1.107.0" @@ -46,7 +50,7 @@ "main": "index.js", "module": "src/index.js", "typings": "src/index.d.ts", - "version": "0.0.0", + "version": "0.0.0-semantically-released", "repository": "standard-release/cli", "homepage": "https://github.com/standard-release/cli", "author": "Charlike Mike Reagent (https://tunnckocore.com)", diff --git a/src/cli.js b/src/cli.js index 357f849..9c3fc03 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,68 +1,45 @@ -import fs from 'fs'; -import util from 'util'; -import path from 'path'; import proc from 'process'; -import { exec } from '@tunnckocore/execa'; +import path from 'path'; import isCI from 'is-ci'; -import ded from 'dedent'; +import { npm } from './plugins'; import release from './index'; -export default function releaseCli(argv) { - return release(argv) +export default function releaseCli(argv, env) { + return release(argv, env) .then(async (results) => { - // ? temporary - const [result] = results; - if (argv.ci && !isCI) { console.error('Publishing is only allowed on CI services!'); console.error( 'Try passing --no-ci flag to bypass this, if you are sure.', ); proc.exit(1); - } - if (argv.dry) { - console.log(serialize(result)); - console.log(argv); return null; } - if (!result.increment && !result.nextVersion) { - console.log('Skipping `npm publish` stage...'); - return null; + if (argv.verbose) { + console.log('Meta info:', serialize(results)); + console.log('Flags / Options:', argv); } - const token = argv.token || proc.env.NPM_TOKEN; - if (!token) { - throw new Error( - 'Expect --token to be passed or NPM_TOKEN environment variable to be set.', - ); + // should be path to file + if (argv.plugin) { + // with `export default () => {}` or + // named exports `export async function fooPlugin(argv, env, results) {}` + const plugins = await import(path.resolve(argv.cwd, argv.plugin)); + + await Object.keys(plugins) + .filter( + (exportName) => + exportName.endsWith('Plugin') || exportName === 'default', + ) + .map(async (name) => { + const plugin = plugins[name]; + await plugin(argv, env, results); + }); + } else { + await npm(argv, env, results); } - console.log('Meta Info:', serialize(result)); - - const defaultRegistry = 'https://registry.npmjs.org/'; - const registry = - argv.registry || proc.env.NPM_REGISTRY || defaultRegistry; - const content = ded`//registry.npmjs.org/:_authToken=${token} - sign-git-tag=false - git-tag-version=false - allow-same-version=false - `; - - const opts = { - cwd: argv.cwd, - stdio: 'inherit', - }; - - await util.promisify(fs.writeFile)( - path.join(argv.cwd, '.npmrc'), - content, - ); - - await exec(`npm version ${result.nextVersion}`, opts); - await exec(`npm publish --registry ${registry}`, opts); - - console.log('Successfully published.'); return true; }) .catch((err) => { diff --git a/src/plugins.js b/src/plugins.js new file mode 100644 index 0000000..eceb56d --- /dev/null +++ b/src/plugins.js @@ -0,0 +1,113 @@ +import fs from 'fs'; +import util from 'util'; +import path from 'path'; +import { exec } from '@tunnckocore/execa'; +import rc from 'rc'; + +/* eslint-disable import/prefer-default-export */ + +export async function npm(options, env, results) { + const opts = Object.assign({}, options); + + await results.map(async (result) => { + if (!result.increment && !result.nextVersion) { + console.log('Skipping `publish` stage for', result.name); + return; + } + + const pkgFolder = opts.monorepo + ? path.join(opts.cwd, result.path) + : opts.cwd; + + const localPkg = await import(path.join(pkgFolder, 'package.json')); + const cfg = normalizeConfig(options, env, localPkg.publishConfig); + + if (!cfg.token) { + throw new Error( + 'Expect --token, NPM_TOKEN or _authToken in local/global .npmrc', + ); + } + + // Never allow npm using git, we have an app for this. + // Replace the `false` with `opts.signGitTag` and `opts.gitTagVersion` + // if you reconsider that, so the CLI won't need to be used with the APP. + await util.promisify(fs.writeFile)( + path.join(opts.cwd, '.npmrc'), + `${cfg.reg}:_authToken=${ + cfg.token + }\nsign-git-tag=false\ngit-tag-version=false\n`, + ); + + const execOpts = { cwd: pkgFolder, stdio: 'inherit' }; + + if (opts.verbose) { + console.log('Package Info:', result); + console.log('Package Folder', pkgFolder); + } + + console.log(result.name, result.lastVersion, '==>', result.nextVersion); + + if (opts.dryRun) { + return; + } + + await exec(`npm version ${result.nextVersion}`, execOpts); + + const publishCmd = [ + 'npm publish', + result.nextVersion, + '--tag', + cfg.tag, + '--access', + cfg.access, + ]; + + await exec(publishCmd.join(' '), execOpts); + }); + + if (opts.dryRun) { + console.log('Possible publish for', results.length, 'packages.'); + return; + } + console.log('Successfully published', results.length, 'packages.'); +} + +/** + * Normalize and synchronize the config from several places. + * Respect order: 1) options/flags, 2) env vars, 3) pkg.publishConfig, + * 4) local .npmrc, 5) global .npmrc + * + * Returns merged config. + * + * @param {*} options + * @param {*} env + * @param {*} publishConfig + */ +function normalizeConfig(options, env, publishConfig) { + const opts = Object.assign({}, options); + const envs = Object.assign({}, env); + const cfg = Object.assign({}, publishConfig); + + const npmrc = rc('npm', cfg); + + let registry = + opts.registry || envs.NPM_REGISTRY || cfg.registry || npmrc.config_registry; + + // always use the npm registry if not other given + registry = registry.includes('registry.yarnpkg.com') + ? 'registry.npmjs.org' + : registry; + + let regClean = registry.replace(/https?:\/\//, ''); + regClean = regClean.endsWith('/') ? regClean.slice(0, -1) : regClean; + + const rcToken = npmrc[`//${regClean}/:_authToken`]; + + return { + reg: `//${regClean}/`, + registry, + tag: cfg.tag || npmrc.tag, + access: opts.access || cfg.access || npmrc.access, + token: opts.token || envs.NPM_TOKEN || cfg.token || rcToken, + }; +} diff --git a/test/index.js b/test/index.js deleted file mode 100644 index bc6697a..0000000 --- a/test/index.js +++ /dev/null @@ -1,65 +0,0 @@ -import assert from 'assert'; -import path from 'path'; -import test from 'asia'; -import fs from 'fs-extra'; -import dedent from 'dedent'; -import simpleGit from 'simple-git/promise'; - -import release from '../src'; -import { __dirname } from './cjs-globals'; - -test('basic', async () => { - assert.strictEqual(typeof release, 'function'); - - try { - await release({ cwd: 'foo' }); - } catch (err) { - assert.ok(/Cannot find module/.test(err.message)); - } -}); - -test('should detect new commits', async () => { - const fakePkg = path.join(__dirname, 'fakepkg'); - - await fs.remove(fakePkg); - await fs.mkdirp(fakePkg); - await fs.writeFile( - path.join(fakePkg, 'package.json'), - JSON.stringify({ name: '@tunnckocore/kokoko3' }, null, 2), - ); - - const git = simpleGit(fakePkg); - await git.init(); - - const localGitConfig = dedent`[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - [user] - name = Foo Bar Baz - email = foobar@example.com - [commit] - gpgsign = false`; - - await fs.writeFile(path.join(fakePkg, '.git', 'config'), localGitConfig); - - await git.add('./*'); - await git.commit('feat: initial blank release'); - await git.addTag('v1.1.0'); - - await fs.writeFile(path.join(fakePkg, 'foo.txt'), 'bar'); - await git.add('./*'); - await git.commit('major(release): qxu quack'); - - await fs.writeFile(path.join(fakePkg, 'fix2.txt'), '222xasas'); - await git.add('./*'); - await git.commit('fix: fo222222o bar baz'); - - const [result] = await release({ cwd: fakePkg }); - - assert.strictEqual(result.increment, 'major'); - assert.strictEqual(result.lastVersion, '1.1.0'); - assert.strictEqual(result.nextVersion, '2.0.0'); - fs.remove(fakePkg); -}); diff --git a/test/cjs-globals.js b/tests/cjs-globals.js similarity index 100% rename from test/cjs-globals.js rename to tests/cjs-globals.js diff --git a/tests/index.js b/tests/index.js new file mode 100644 index 0000000..fa54b34 --- /dev/null +++ b/tests/index.js @@ -0,0 +1,162 @@ +import path from 'path'; +import assert from 'assert'; +import test from 'asia'; +import fs from 'fs-extra'; +import dedent from 'dedent'; +import simpleGit from 'simple-git/promise'; + +import release from '../src'; +import { __dirname } from './cjs-globals'; + +const FAKE_MONO = path.join(__dirname, 'some-mono-repo'); +const FAKE_PKG = path.join(__dirname, 'kokokokokokokok'); + +fs.removeSync(FAKE_MONO); +fs.removeSync(FAKE_PKG); + +async function gitSetup(dir, initial) { + const git = simpleGit(dir); + await git.init(); + + const localGitConfig = dedent`[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + [user] + name = Foo Bar Baz + email = foobar@example.com + [commit] + gpgsign = false`; + + await fs.outputFile(path.join(dir, '.git', 'config'), localGitConfig); + await fs.outputFile(path.join(dir, 'readme.md'), '# pkg readme'); + + if (initial) { + await git.add('./*'); + await git.commit('chore: initial commit'); + } + + return git; +} + +async function createFile(pkg, filename, content) { + const rand = Math.floor(Math.random()); + const filepath = path.join(pkg, filename || String(rand)); + + await fs.outputJson(filepath, content || { rand }); +} + +test('basic', async () => { + assert.strictEqual(typeof release, 'function'); + + try { + await release({ cwd: 'foo' }); + } catch (err) { + assert.ok(/Cannot find module/.test(err.message)); + } +}); + +test('should detect new commits', async () => { + await fs.remove(FAKE_PKG); + await fs.ensureDir(FAKE_PKG); + + await createFile(FAKE_PKG, 'package.json', { + name: '@tunnckocore/kokoko3', + }); + + const git = await gitSetup(FAKE_PKG); + + await git.add('./*'); + await git.commit('feat: initial blank release'); + await git.addTag('v1.1.0'); + + await createFile(FAKE_PKG); + await git.add('./*'); + await git.commit('major(release): qxu quack'); + + await createFile(FAKE_PKG); + await git.add('./*'); + await git.commit('fix: fo222222o bar baz'); + + const [result] = await release({ cwd: FAKE_PKG }); + + assert.strictEqual(result.increment, 'major'); + assert.strictEqual(result.lastVersion, '1.1.0'); + assert.strictEqual(result.nextVersion, '2.0.0'); + fs.remove(FAKE_PKG); +}); + +/* eslint-disable max-statements */ +test('should work for monorepo setups', async () => { + // the `foo-bar-baz-qux` package + const FAKE_1 = path.join(FAKE_MONO, 'packages', 'foo-bar-baz-qux'); + + // the `@tunnckocore/qq5` package + const FAKE_2 = path.join(FAKE_MONO, '@tunnckocore', 'qq5'); + + // the `@tunnckocore/kokoko3` package + const FAKE_3 = path.join(FAKE_MONO, '@tunnckocore', 'kokoko3'); + + await fs.remove(FAKE_MONO); + await fs.ensureDir(FAKE_MONO); + + const git = await gitSetup(FAKE_MONO, true); + + await createFile(FAKE_MONO, 'package.json', { + private: true, + name: 'some-monorepo-root', + }); + + /** + * add `foo-bar-baz-qux` + */ + let name = 'foo-bar-baz-qux'; + await createFile(FAKE_1, 'package.json', { name }); + + await git.add('./*'); + await git.commit(`chore: add \`${name}\` package`); + await git.addTag(`${name}@1.0.4`); + + /** + * add `@tunnckocore/qq5` + */ + name = '@tunnckocore/qq5'; + await createFile(FAKE_2, 'package.json', { name }); + + await git.add('./*'); + await git.commit(`chore: add \`${name}\` package`); + await git.addTag(`${name}@0.1.0`); + + /** + * add `@tunnckocore/kokoko3` + */ + name = '@tunnckocore/kokoko3'; + await createFile(FAKE_3, 'package.json', { name }); + + await git.add('./*'); + await git.commit(`chore: add \`${name}\` package`); + await git.addTag(`${name}@1.1.0`); + + /** + * Change only inside `@tunnckocore/qq5` and `foo-bar-baz-qux` + * + * TODO: when implemented + */ + + // await createFile(FAKE_1, 'some-new-file'); + // await createFile(fakePkgTwo, 'yeah-new-new'); + + /** + * Get the results + * + * TODO: when implemented + */ + const results = await release({ cwd: FAKE_MONO, monorepo: true }); + console.log(results); + + /** + * Cleanup the whole monorepo + */ + fs.remove(FAKE_MONO); +}); diff --git a/yarn.lock b/yarn.lock index c7b8e08..0bd977d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -108,6 +108,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== +"@std/esm@^0.20.0": + version "0.20.0" + resolved "https://registry.yarnpkg.com/@std/esm/-/esm-0.20.0.tgz#658cb32e3b163f20b7d9f29a78987041068b1182" + integrity sha512-05cQYa1T/6XxKhhnyIKQC8kSBHYkXWueZ0XFLABLlghoymwgyp4XT9ivjDrHb7EbSWbQoHSRk34zhIuPeShJow== + "@szmarczak/http-timer@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" @@ -156,6 +161,16 @@ esm "^3.0.84" semver "^5.6.0" +"@tunnckocore/pretty-config@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@tunnckocore/pretty-config/-/pretty-config-0.5.1.tgz#e8cc45cf28ec5c74245f857ae317955d483a2879" + integrity sha512-6jo7FUloetOMvCVZiIlEWdidDMP3aC84w90f3MmH/CVp9ep96SXOsLsPQOzrE9S+V8Mmdg3rmXv1fOyLD9DT4w== + dependencies: + "@std/esm" "^0.20.0" + js-yaml "^3.10.0" + json-6 "^0.1.128" + pify "^3.0.0" + acorn-jsx@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" @@ -1672,7 +1687,7 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.12.0: +js-yaml@^3.10.0, js-yaml@^3.12.0: version "3.12.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.1.tgz#295c8632a18a23e054cf5c9d3cecafe678167600" integrity sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA== @@ -1685,6 +1700,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-6@^0.1.128: + version "0.1.128" + resolved "https://registry.yarnpkg.com/json-6/-/json-6-0.1.128.tgz#0c52ddf38af9bd2d8ca5fdb402fc0e70776ccbf2" + integrity sha512-drsXxm2Z5DnjgtH80fy6qMfNPQMpaK4blHBcWlHFLiaz/NKfRFSuvhLoJGzZuo64B/t8Nx1stNzCTUzrmKz+dw== + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -2416,7 +2436,7 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= -rc@^1.1.6, rc@^1.2.7: +rc@^1.1.6, rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==