从一个分支修改版本并发布包,但在另一个分支中保留标签

Bump version and publish packages from one branch but keep tags in another branch

我正在将我的项目由许多相互依赖的包组成,在开发过程中使用 Lerna. We follow something like Gitflow workflow 迁移到 monorepo。主要概念是对 develop 分支和从 develop 创建并合并回的所有其他分支(功能、错误修复等)中的所有源代码进行更改。只要包的新版本准备就绪,我们就可以通过 npm publishyarn publish 发布它,然后将其合并到 master 分支并按以下方式手动标记它:

$ git checkout develop

对源代码进行一些更改,包括版本冲突...

$ git add -A
$ git commit -m "Make some changes and version bump."
$ git checkout master
$ git merge --no-ff develop -m "Version 0.14.1."
$ git tag -a 0.14.1 -m "Version 0.14.1."

现在我想用 Lerna 实现同样的事情来管理所有的包。查看文档,我说 publish command relies on version command that, in turn, uses changed 在幕后命令检测自最新版本以来包中所做的更改:

List local packages that have changed since the last tagged release

考虑在一个包的 develop 分支中进行一些更改(例如,@geoapps/layout

$ lerna changed

说所有的包都改变了(这不是我所期望的):

info cli using local version of lerna
lerna notice cli v3.13.1
lerna info Assuming all packages changed
@geoapps/angle
@geoapps/camera-scene-mode-switcher
...
@geoapps/tracer
@geoapps/vector
lerna success found 39 packages ready to publish

我猜这是因为 Lerna 在 develop 分支中寻找标记的提交进行比较,但在那里找不到任何东西。如果我将源代码更改提交到 master branch

然后 Lerna 在单个 @geoapps/layout 包中正确检测到它们:

$ git checkout master
$ lerna changed
info cli using local version of lerna
lerna notice cli v3.13.1
lerna info Looking for changed packages since 0.14.1
@geoapps/layout
lerna success found 1 package ready to publish

但是在 master 分支中进行更改也不是我想要做的。 include-merged-tags 是我尝试使用的另一个选项,但似乎只有当标记的提交也是 develop 分支历史的一部分时它才有效:

$ git checkout develop
$ git merge --no-ff master -m "Sync with master."

$ lerna changed --include-merged-tags
info cli using local version of lerna
lerna notice cli v3.13.1
lerna info Looking for changed packages since 0.14.1
@geoapps/layout
lerna success found 1 package ready to publish

由于在 master 分支中标记的所有源代码更改都存在于 develop 分支中,我想知道是否可以强制 Lerna 比较在 develop 分支中所做的更改而不是标记从 master 提交,但其父提交 (0.14.1^2) 也属于 develop。可能吗?

环境:

$ node --version
v10.15.0
$ npm --version
6.9.0
$ yarn --version
1.15.2
$ lerna --version
3.13.1

Lerna 的核心开发人员 says that Lerna doesn't suit well to work with Gitflow workflow. To say more, it's prohibited 发布包以检测特定提交(在另一个分支中标记的提交)的更改。最新标记版本应属于进行更改的同一分支。

考虑到它和我们希望继续使用 Gitflow 的想法,我决定修补 Lerna 以实现所需的行为。只需 created git patch 并使用 Lerna 将其放在我的项目的根目录中。

lerna-version-since.patch

diff --git a/commands/version/command.js b/commands/version/command.js
index da9b1c00..3c5e19e2 100644
--- a/commands/version/command.js
+++ b/commands/version/command.js
@@ -104,6 +104,11 @@ exports.builder = (yargs, composed) => {
       requiresArg: true,
       defaultDescription: "alpha",
     },
+    since: {
+      describe: "Look for changes since specified commit instead of last tagged release",
+      type: "string",
+      requiresArg: true,
+    },
     "sign-git-commit": {
       describe: "Pass the `--gpg-sign` flag to `git commit`.",
       type: "boolean",

如果 commands/version/command.js 有变化,我们可能会更新补丁。为了应用补丁,应该 运行 这个命令:

$ git apply -p3 --directory node_modules/@lerna/version lerna-version-since.patch

修补了 Lerna,现在可以在 develop 分支中提交和发布,并在 master 中标记发布。为了使事情更简单,我写了一个名为 lerna-gitflow.js 的脚本,它可以自动生成所有内容。这是 package.json 的脚本部分:

"scripts": {
  "publish:major": "./lerna-gitflow.js publish major",
  "publish:minor": "./lerna-gitflow.js publish minor",
  "publish:patch": "./lerna-gitflow.js publish patch",
  "changes": "./lerna-gitflow.js changes",
  "postinstall": "./lerna-gitflow.js patch"
}

所有这些 publish:*changes 命令应该 运行 来自开发分支(默认为 develop)。

changes 命令仅显示自发布分支中的最新发布标记(默认为 master)以来开发分支(develop)中更改的包。

publish 命令做两件事:

  • 更新 package.json 文件中更改包的版本,在根目录 package.jsonlerna.json 中,并将它们提交到本地的 develop 分支(可以通过 运行宁,例如./lerna-gitflow.js version patch);
  • develop 分支将更改的包发布到 npm 注册表,然后将更改合并到 master 分支而不快进并在那里标记一个新版本(也可以通过 [=71 单独完成) =]宁./lerna-gitflow.js publish --skip-version).

postinstall 脚本尝试在任何 npm installyarn install 调用上修补 Lerna,否则使一切正常工作所需的更改将会丢失。

lerna-gitflow.js

#!/usr/bin/env node
const path = require('path');
const yargs = require('yargs');
const execa = require('execa');
const jsonfile = require('jsonfile');

const noop = () => {};

async function lernaCommand(command, options) {
  const { devBranch } = options;
  const branch = await getCurrentBranch();
  if (branch !== devBranch) {
    return Promise.reject(
      `You should be in "${devBranch}" branch to detect changes but current branch is "${branch}".`
    );
  }
  const latestVersion = await getLatestVersion();

  const bumpVersion = async bump => {
    await lernaVersion(latestVersion, bump);
    const version = await getLernaVersion();
    const packageJsonPath = path.resolve(__dirname, 'package.json');
    const packageJson = await jsonfile.readFile(packageJsonPath);
    packageJson.version = version;
    await jsonfile.writeFile(packageJsonPath, packageJson, { spaces: 2 });
    await exec('git', ['add', '-A']);
    await exec('git', ['commit', '-m', 'Version bump.']);
    return version;
  };

  const reject = e => {
    if (typeof e === 'string') {
      return Promise.reject(e);
    }
    return Promise.reject('Unable to detect any changes in packages, probably nothing has changed.');
  };

  switch (command) {
    case 'publish': {
      const { bump, skipVersion, releaseBranch } = options;
      if (releaseBranch === devBranch) {
        return Promise.reject('Release and development branches can\'t be the same.');
      }
      try {
        const version = skipVersion ? await getLernaVersion() : await bumpVersion(bump);
        await lernaPublish(latestVersion, version);
        await exec('git', ['checkout', releaseBranch]);
        await exec('git', ['merge', '--no-ff', devBranch, '-m', `Version ${version}.`]);
        await exec('git', ['tag', '-a', version, '-m', `Version ${version}.`]);
        await exec('git', ['checkout', devBranch]);
      }
      catch (e) {
        return reject(e);
      }
      break;
    }

    case 'version': {
      const { bump } = options;
      try {
        await bumpVersion(bump);
      }
      catch (e) {
        return reject(e);
      }
      break;
    }

    case 'changed': {
      try {
        await lernaChanged(latestVersion);
      }
      catch (e) {
        return reject(e);
      }
      break;
    }
  }
}

async function lernaPublish(since, version) {
  if (since === version) {
    return Promise.reject(`Unable to publish packages with same version ${version}.`);
  }
  return exec('lerna', ['publish', '--since', since, version, '--no-push', '--no-git-tag-version', '--yes']);
}

async function lernaVersion(since, bump) {
  return exec('lerna', ['version', '--since', since, bump, '--no-push', '--no-git-tag-version', '--yes']);
}

async function lernaChanged(since) {
  return exec('lerna', ['changed', '--since', since]);
}

async function patch() {
  try {
    await exec('git', ['apply', '-p3', '--directory', 'node_modules/@lerna/version', 'lerna-version-since.patch']);
  }
  catch (e) {
    return Promise.reject('Lerna Gitflow patch is not applied (probably, it\'s already applied before).');
  }
}

async function getCurrentBranch() {
  const { stdout } = await exec('git', ['branch']);
  const match = stdout.match(/\* ([\S]+)/);
  if (match === null) {
    return Promise.reject('Unable to detect current git branch.');
  }
  return match[1];
}

async function getLatestTaggedCommit() {
  const { stdout } = await exec('git', ['rev-list', '--tags', '--max-count', 1]);
  if (!stdout) {
    return Promise.reject('Unable to find any tagged commit.');
  }
  return stdout;
}

async function getLatestVersion() {
  const commit = await getLatestTaggedCommit();
  const { stdout } = await exec('git', ['describe', '--tags', commit]);
  return stdout;
}

async function getLernaVersion() {
  const lernaJson = await jsonfile.readFile(path.resolve(__dirname, 'lerna.json'));
  return lernaJson.version;
}

function exec(cmd, args, opts) {
  console.log(`$ ${cmd} ${args.join(' ')}`);
  const promise = execa(cmd, args, opts);
  promise.stdout.pipe(process.stdout);
  promise.stderr.pipe(process.stderr);
  return promise;
}

yargs
  .wrap(null)
  .strict(true)
  .help(true, 'Show help')
  .version(false)
  .fail((msg, error) => {
    console.error(error);
    if (msg) {
      console.error(msg);
    }
  })
  .demandCommand()
  .command(
    'publish <bump>',
    'Bump and commit packages\' in development branch, then publish, merge into and tag in release branch',
    yargs => yargs
      .positional('bump', {
        describe: 'Type of version update',
        type: 'string'
      })
      .option('skip-version', {
        describe: 'Skip version bumping and commiting in development branch',
        type: 'boolean',
        default: false
      }),
    opts => lernaCommand('publish', opts)
  )
  .command(
    'version <bump>',
    'Bump and commit packages\' version in development branch',
    yargs => yargs
      .positional('bump', {
        describe: 'Type of version update',
        type: 'string'
      }),
    opts => lernaCommand('version', opts)
  )
  .command(
    'changes',
    'Detect packages changes since latest release',
    noop,
    opts => lernaCommand('changed', opts)
  )
  .command('patch', 'Patch Lerna to use with Gitflow', noop, () => patch())
  .options({
    'dev-branch': {
      describe: 'Name of git development branch',
      type: 'string',
      demandOption: true,
      default: 'develop'
    },
    'release-branch': {
      describe: 'Name of git release branch',
      type: 'string',
      demandOption: true,
      default: 'master'
    }
  })
  .parse();