如何使用二十一点和版本控制来组织自己的Node.js模块存储库

三个前端团队目前正在ISPsystem上开发三个主要项目:用于管理Web服务器的ISPmanager,用于虚拟化的VMmanager和用于自动化托管者业务的BILLmanager。 团队在紧迫的期限内同时工作,因此您不能没有优化。 为了节省时间,我们使用通用的解决方案,并将通用的组件放入单独的项目中。 此类项目有自己的存储库,并得到所有团队成员的支持。 本文将讨论这些存储库的构建以及与之合作的工作。



常见项目的存储库如何


我们将自己的服务器与GitLab一起使用来存储远程存储库。 对于我们而言,保持熟悉的工作环境并在开发过程中能够使用通用模块非常重要。 因此,我们拒绝在npmjs.com私有存储库中发布。 幸运的是,Node.js模块不仅可以与NPM一起安装,还可以从其他来源 (包括git存储库)安装。

我们用TypeScript编写,然后将其编译为JavaScript以供以后使用。 但是在我们这个时代,也许懒惰的前端不编译其JavaScript。 因此,我们需要用于源代码和已编译项目的不同存储库。

经过长时间讨论的棘手,我们提出了以下概念。 源和模块的编译版本应该有两个单独的存储库。 此外,第二个存储库应该是第一个存储库的镜像。

这意味着在开发期间,任何功能都应在发布之前在分支中以与进行开发的分支完全相同的名称发布。 因此,我们有机会使用该模块的实验版本,并从特定分支安装它。 我们正在开发的产品非常便于对其进行检查。

另外,对于每个出版物,我们都会创建一个标签来保存项目状态。 标签名称与package.json中指定的版本相对应。 从git仓库安装时,标签显示在网格后面,例如:

npm install git+ssh://[url ]#1.0.0 

因此,我们可以修复模块的使用版本,而不必担心有人会更改某些内容。

也为不稳定版本创建标签,但是,在源发布库中将提交的缩写哈希添加到它们中。 这是此类标签的示例:

 1.0.0_e5541dc1 

这种方法使您可以实现标签的唯一性,并将它们与源存储库相关联。

因为我们在谈论模块的稳定版本和不稳定版本,所以我们将它们区分开来:如果发布是从master或develop分支执行的,则版本是稳定的,否则不是稳定的。

普通项目如何组织?


如果我们不能使它们自动化,那么我们所有的协议都是没有意义的。 特别是,自动化发布过程。 下面,我将展示如何使用一个通用模块(一个用于测试自定义脚本的实用程序)来组织工作。

该实用程序使用puppeteer库,为Chromium浏览器准备了用于docker容器的代码,并使用Mocha运行测试。 所有团队的参与者都可以修改实用程序,而不必担心会互相破坏。

将以下命令写入测试实用程序的package.json文件中:

 "publish:git": "ts-node ./scripts/publish.ts" 

她运行附近的脚本:

完整发布脚本代码
 import { spawnSync } from 'child_process'; import { mkdirSync, existsSync } from 'fs'; import { join } from 'path'; import chalk from 'chalk'; /** *     */ /** *      * @param cwd -    * @param stdio -  / */ const getSpawnOptions = (cwd = process.cwd(), stdio = 'inherit') => ({ cwd, shell: true, stdio, }); /*    */ const rootDir = join(__dirname, '../'); /*     */ const isDiff = !!spawnSync('git', ['diff'], getSpawnOptions(rootDir, 'pipe')).stdout.toString().trim(); if (isDiff) { console.log(chalk.red('There are uncommitted changes')); } else { /*   */ const build = spawnSync('npm', ['run', 'build'], getSpawnOptions(rootDir)); /*     */ if (build.status === 0) { /*       */ const tempDir = join(rootDir, 'temp'); if (existsSync(tempDir)) { spawnSync('rm', ['-rf', 'temp'], getSpawnOptions(rootDir)); } mkdirSync(tempDir); /*    package.json */ const { name, version, repository } = require(join(rootDir, 'package.json')); const originUrl = repository.url.replace(`${name}-source`, name); spawnSync('git', ['init'], getSpawnOptions(tempDir)); spawnSync('git', ['remote', 'add', 'origin', originUrl], getSpawnOptions(tempDir)); /*        */ const branch = spawnSync( 'git', ['symbolic-ref', '--short', 'HEAD'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); /*      */ const buildBranch = branch === 'develop' ? 'master' : branch; /*       ,       */ const shortSHA = spawnSync( 'git', ['rev-parse', '--short', 'HEAD'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); /*  */ const tag = buildBranch === 'master' ? version : `${version}_${shortSHA}`; /*        */ const isTagExists = !!spawnSync( 'git', ['ls-remote', 'origin', `refs/tags/${tag}`], getSpawnOptions(tempDir, 'pipe') ).stdout.toString().trim(); if (isTagExists) { console.log(chalk.red(`Tag ${tag} already exists`)); } else { /*       */ const isBranchExits = !!spawnSync( 'git', ['ls-remote', '--exit-code', 'origin', buildBranch], getSpawnOptions(tempDir, 'pipe') ).stdout.toString().trim(); if (isBranchExits) { /*     */ spawnSync('git', ['fetch', 'origin', buildBranch], getSpawnOptions(tempDir)); spawnSync('git', ['checkout', buildBranch], getSpawnOptions(tempDir)); } else { /*    master */ spawnSync('git', ['fetch', 'origin', 'master'], getSpawnOptions(tempDir)); spawnSync('git', ['checkout', 'master'], getSpawnOptions(tempDir)); /*    */ spawnSync('git', ['checkout', '-b', buildBranch], getSpawnOptions(tempDir)); /*    */ spawnSync('git', ['commit', '--allow-empty', '-m', '"Initial commit"'], getSpawnOptions(tempDir)); } /*     */ spawnSync( 'rm', ['-rf', 'lib', 'package.json', 'package-lock.json', 'README.md'], getSpawnOptions(tempDir) ); /*    */ spawnSync('cp', ['-r', 'lib', 'temp/lib'], getSpawnOptions(rootDir)); spawnSync('cp', ['package.json', 'temp/package.json'], getSpawnOptions(rootDir)); spawnSync('cp', ['package-lock.json', 'temp/package-lock.json'], getSpawnOptions(rootDir)); spawnSync('cp', ['README.md', 'temp/README.md'], getSpawnOptions(rootDir)); /*    */ spawnSync('git', ['add', '--all'], getSpawnOptions(tempDir)); /*       */ const lastCommitMessage = spawnSync( 'git', ['log', '--oneline', '-1'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); /*      */ const message = buildBranch === 'master' ? version : lastCommitMessage; /*      */ spawnSync('git', ['commit', '-m', `"${message}"`], getSpawnOptions(tempDir)); /*      */ spawnSync('git', ['tag', tag], getSpawnOptions(tempDir)); /*      */ spawnSync('git', ['push', 'origin', buildBranch], getSpawnOptions(tempDir)); spawnSync('git', ['push', '--tags'], getSpawnOptions(tempDir)); console.log(chalk.green('Published successfully!')); } /*    */ spawnSync('rm', ['-rf', 'temp'], getSpawnOptions(rootDir)); } else { console.log(chalk.red(`Build was exited exited with code ${build.status}`)); } } console.log(''); // space 


反过来,此代码通过Node.js child_process模块执行所有必要的命令。

以下是他工作的主要阶段:


1.检查未经授权的更改

 const isDiff = !!spawnSync('git', ['diff'], getSpawnOptions(rootDir, 'pipe')).stdout.toString().trim(); 

在这里,我们检查git diff命令的结果。 如果发布中包含不在源中的更改,那就不好了。 另外,这将破坏不稳定版本与提交的连接。

2.实用程序组装

 const build = spawnSync('npm', ['run', 'build'], getSpawnOptions(rootDir)); 

构建常量获取构建结果。 如果一切顺利,则status参数将为0。否则,将不会发布任何内容。

3.部署编译的版本库

整个发布过程无非就是将更改提交到特定的存储库。 因此,脚本在我们的项目中创建一个临时目录,在其中初始化git存储库并将其与远程程序集存储库关联。

 /*       */ const tempDir = join(rootDir, 'temp'); if (existsSync(tempDir)) { spawnSync('rm', ['-rf', 'temp'], getSpawnOptions(rootDir)); } mkdirSync(tempDir); /*    package.json */ const { name, version, repository } = require(join(rootDir, 'package.json')); const originUrl = repository.url.replace(`${name}-source`, name); spawnSync('git', ['init'], getSpawnOptions(tempDir)); spawnSync('git', ['remote', 'add', 'origin', originUrl], getSpawnOptions(tempDir)); 

这是使用git initgit remote的标准过程。

4.标签名称生成

首先,我们使用git symbolic-ref命令找出要发布的分支的名称。 并设置更改将被上传到的分支的名称(程序集存储库中没有develop分支)。

 /*        */ const branch = spawnSync( 'git', ['symbolic-ref', '--short', 'HEAD'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); /*      */ const buildBranch = branch === 'develop' ? 'master' : branch; 

使用git rev-parse命令,我们获得了所在分支中最后一次提交的缩写哈希。 可能需要生成不稳定版本的标签名称。

 <source lang="typescript">/*       ,       */ const shortSHA = spawnSync( 'git', ['rev-parse', '--short', 'HEAD'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); 

好吧,实际上是组成标签的名称。

 /*  */ const tag = buildBranch === 'master' ? version : `${version}_${shortSHA}`; 

5.检查远程存储库中是否缺少完全相同的标记

 /*        */ const isTagExists = !!spawnSync( 'git', ['ls-remote', 'origin', `refs/tags/${tag}`], getSpawnOptions(tempDir, 'pipe') ).stdout.toString().trim(); 

如果之前创建了类似的标签,则git ls-remote命令的结果将不会为空。 同一版本只能发布一次。

6.在程序集存储库中创建适当的分支

正如我之前所说的,该实用程序的编译版本库是源代码库的镜像。 因此,如果发布不是来自master或develop分支,则必须在程序集存储库中创建相应的分支。 好吧,或者至少确保它的存在

 /*       */ const isBranchExits = !!spawnSync( 'git', ['ls-remote', '--exit-code', 'origin', buildBranch], getSpawnOptions(tempDir, 'pipe') ).stdout.toString().trim(); if (isBranchExits) { /*     */ spawnSync('git', ['fetch', 'origin', buildBranch], getSpawnOptions(tempDir)); spawnSync('git', ['checkout', buildBranch], getSpawnOptions(tempDir)); } else { /*    master */ spawnSync('git', ['fetch', 'origin', 'master'], getSpawnOptions(tempDir)); spawnSync('git', ['checkout', 'master'], getSpawnOptions(tempDir)); /*    */ spawnSync('git', ['checkout', '-b', buildBranch], getSpawnOptions(tempDir)); /*    */ spawnSync('git', ['commit', '--allow-empty', '-m', '"Initial commit"'], getSpawnOptions(tempDir)); } 

如果以前没有该分支,则使用--allow-empty标志使用空提交进行初始化。

7.文件准备

首先,您需要删除已部署存储库中的所有内容。 毕竟,如果我们使用预先存在的分支,则它包含实用程序的先前版本。

 /*     */ spawnSync( 'rm', ['-rf', 'lib', 'package.json', 'package-lock.json', 'README.md'], getSpawnOptions(tempDir) ); 

接下来,我们传输发布所需的更新文件,并将它们添加到存储库索引中。

 /*    */ spawnSync('cp', ['-r', 'lib', 'temp/lib'], getSpawnOptions(rootDir)); spawnSync('cp', ['package.json', 'temp/package.json'], getSpawnOptions(rootDir)); spawnSync('cp', ['package-lock.json', 'temp/package-lock.json'], getSpawnOptions(rootDir)); spawnSync('cp', ['README.md', 'temp/README.md'], getSpawnOptions(rootDir)); /*    */ spawnSync('git', ['add', '--all'], getSpawnOptions(tempDir)); 

经过这样的操作后,git将很好地识别文件行所做的更改。 这样,即使在已编译的版本存储库中,我们也可以获得一致的更改历史记录。

8.提交和提交更改

作为程序集存储库中的提交消息,我们将标签名称用于稳定版本。 而且对于不稳定-来自源存储库的提交消息。 这样,就支持了我们关于镜像存储库的想法。

 /*       */ const lastCommitMessage = spawnSync( 'git', ['log', '--oneline', '-1'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); /*      */ const message = buildBranch === 'master' ? version : lastCommitMessage; /*      */ spawnSync('git', ['commit', '-m', `"${message}"`], getSpawnOptions(tempDir)); /*      */ spawnSync('git', ['tag', tag], getSpawnOptions(tempDir)); /*      */ spawnSync('git', ['push', 'origin', buildBranch], getSpawnOptions(tempDir)); spawnSync('git', ['push', '--tags'], getSpawnOptions(tempDir)); 

9.删除临时目录

 spawnSync('rm', ['-rf', 'temp'], getSpawnOptions(rootDir)); 

审查常见项目中的更新


对常见项目进行更改后,最重要的过程之一就是进行审查。 尽管已开发的技术允许您创建完全隔离的模块版本,但没有人愿意拥有同一实用程序的许多不同版本。 因此,每个通用项目都必须遵循一条单独的开发路径。 这应该在团队之间达成共识。

通用项目的更新由所有团队的成员尽可能地进行审查。 这是一个复杂的过程,因为每个团队都依靠自己的冲刺而生存,并且具有不同的工作量。 有时过渡到新版本的时间可能会延迟。

在这里,您只能建议不要忽略并且不要拖延此过程。

Source: https://habr.com/ru/post/zh-CN418135/


All Articles