关于npm:突出版本并从一个分支发布软件包,但将标签保留在另一个分支

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

我正在将包含许多彼此依赖的程序包的项目迁移到Lerna的monorepo中。在开发过程中,我们遵循类似Gitflow工作流程的方法。主要概念是在develop分支和从develop创建并合并回develop的所有其他分支(功能,错误修复等)中进行所有源代码更改。只要准备好新版本的软件包,我们就会通过npm publishyarn publish发布它,然后将其合并到master分支,并通过以下方式在其中手动标记:

1
$ git checkout develop

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

1
2
3
4
5
$ 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命令依赖于version命令,而version命令又在幕后使用经过更改的命令来检测自最新版本以来软件包中所做的更改:

List local packages that have changed since the last tagged release

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

>
</p>
<div class=

1
$ lerna changed

说所有软件包都已更改(这不是我期望的):

1
2
3
4
5
6
7
8
9
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分支

>
</p>
<p>
然后Lerna在单个<wyn>@geoapps/layout</wyn>包中正确检测到它们:
</p>
<div class=

1
2
$ git checkout master
$ lerna changed
1
2
3
4
5
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分支历史的一部分时才起作用:

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

>
</p>
<div class=

1
$ lerna changed --include-merged-tags

1
2
3
4
5
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。可能吗?

环境:

1
2
3
4
5
6
7
8
$ node --version
v10.15.0
$ npm --version
6.9.0
$ yarn --version
1.15.2
$ lerna --version
3.13.1


Lerna的核心开发人员说Lerna不适合与Gitflow工作流程一起使用。进一步说,禁止发布从特定提交(在另一个分支中标记为提交)检测其更改的软件包。最新的标记发行版应属于进行更改的同一分支。

考虑到这一点以及我们希望与Gitflow保持联系的愿望,我决定修补Lerna以获得所需的行为。刚创建了git patch并使用Lerna将其放在我项目的根目录中。

lerna-version-since.patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中的某些更改,则我们可能会更新补丁。为了应用补丁程序,应运行以下命令:

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

修补了Lerna之后,现在可以在develop分支中发布和发布,并在master中标记发布。为了简化操作,我编写了一个名为lerna-gitflow.js的脚本,该脚本可以自动完成所有操作。这是package.json的脚本部分:

1
2
3
4
5
6
7
"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)运行。

自发布分支(默认为master)以来,changes命令仅显示自开发分支(develop)以来已更改的软件包。

publish命令执行两件事:

  • 更新更改后的软件包的package.json文件中,根package.jsonlerna.json中的版本,并将它们提交到本地的develop分支中(可以通过运行例如./lerna-gitflow.js version patch单独完成);
  • 将更改的软件包从develop分支发布到npm注册表,然后将更改合并到master分支而无需快速转发,并在那里标记新版本(也可以通过运行./lerna-gitflow.js publish --skip-version单独完成)。

postinstall脚本尝试在任何npm installyarn install调用上修补Lerna,否则使所有工作正常进行的必要更改将丢失。

lerna-gitflow.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/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();