Como organizar seu próprio repositório de módulo Node.js. com blackjack e versionamento

Atualmente, três equipes de front-end estão desenvolvendo três grandes projetos no ISPsystem : ISPmanager para gerenciar servidores da Web, VMmanager para trabalhar com virtualização e BILLmanager para automatizar os negócios de hosters. As equipes trabalham ao mesmo tempo, no modo de prazos apertados, para que você não possa prescindir da otimização. Para economizar tempo, usamos soluções comuns e levamos componentes comuns a projetos separados. Esses projetos têm seus próprios repositórios, que são suportados por membros de todas as equipes. Este artigo será sobre a construção desses repositórios, bem como o trabalho com eles.



Como estão os repositórios de projetos comuns


Usamos nosso próprio servidor com o GitLab para armazenar repositórios remotos. Era importante para nós manter o ambiente de trabalho familiar e poder trabalhar com módulos comuns no processo de seu desenvolvimento. Portanto, nos recusamos a publicar nos repositórios particulares do npmjs.com . Felizmente, os módulos Node.js podem ser instalados não apenas com o NPM, mas também de outras fontes , incluindo repositórios git.

Escrevemos em TypeScript, que é posteriormente compilado em JavaScript para uso posterior. Mas em nossos dias, talvez o front-end preguiçoso não compile seu JavaScript. Portanto, precisamos de repositórios diferentes para o código-fonte e o projeto compilado.

Depois de passar pelos espinhos de longas discussões, desenvolvemos o seguinte conceito. Deve haver dois repositórios separados para as fontes e para a versão compilada do módulo. Além disso, o segundo repositório deve ser um espelho do primeiro.

Isso significa que, durante o desenvolvimento, qualquer recurso deve ser publicado antes do lançamento na ramificação com o mesmo nome exato da ramificação em que o desenvolvimento está em andamento. Assim, temos a oportunidade de usar a versão experimental do módulo, instalando-o a partir de um ramo específico. Aquele em que estamos desenvolvendo é muito conveniente para verificá-lo em ação.

Além disso, para cada publicação, criamos um rótulo que salva o status do projeto. O nome do rótulo corresponde à versão especificada em package.json. Ao instalar a partir de um repositório git, o rótulo é indicado após a rede, por exemplo:

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

Assim, podemos consertar a versão usada do módulo e não nos preocupar que alguém mude alguma coisa.

Os rótulos também são criados para versões instáveis; no entanto, um hash abreviado do commit é adicionado a eles no repositório de origem, a partir do qual a publicação foi feita. Aqui está um exemplo desse rótulo:

 1.0.0_e5541dc1 

Essa abordagem permite alcançar a exclusividade dos rótulos, além de associá-los ao repositório de origem.

Como estamos falando de versões estáveis ​​e instáveis ​​do módulo, veja como as distinguimos: se a publicação for realizada a partir do ramo mestre ou de desenvolvimento, a versão será estável, caso contrário não.

Como é organizado o trabalho com projetos comuns?


Todos os nossos acordos não fariam sentido se não pudéssemos automatizá-los. Em particular, automatize o processo de publicação. Abaixo, mostrarei como o trabalho é organizado com um dos módulos comuns - um utilitário para testar scripts personalizados.

Esse utilitário, usando a biblioteca de marionetistas , prepara o navegador Chromium para uso em contêineres de docker e executa testes usando o Mocha . Os participantes de todas as equipes podem modificar o utilitário sem medo de quebrar algo um do outro.

O comando a seguir está escrito no arquivo package.json do utilitário de teste:

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

Ela executa um script próximo:

Código completo do script de publicação
 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 


Por sua vez, esse código através do módulo child_process do Node.js executa todos os comandos necessários.

Aqui estão as principais etapas de seu trabalho:


1. Verifique se há alterações não autorizadas

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

Aqui, verificamos o resultado do comando git diff . Não é bom que a publicação contenha alterações que não estão na fonte. Além disso, isso interromperá a conexão de versões instáveis ​​com confirmações.

2. Montagem de utilidade

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

A constante de construção obtém o resultado da construção. Se tudo der certo, o parâmetro status será 0. Caso contrário, nada será publicado.

3. Implementando o Repositório da Versão Compilada

Todo o processo de publicação nada mais é do que enviar alterações para um repositório específico. Portanto, o script cria um diretório temporário em nosso projeto no qual inicializa o repositório git e o associa ao repositório de montagem remota.

 /*       */ 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)); 

Este é um processo padrão usando o git init e o git remote .

4. Geração de nome de etiqueta

Primeiro, descobrimos o nome da ramificação da qual estamos publicando usando o comando git symbolic-ref . E defina o nome da ramificação na qual as alterações serão carregadas (não há ramificação de desenvolvimento no repositório de montagem).

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

Usando o comando git rev-parse , obtemos um hash abreviado do último commit no ramo em que estamos. Pode ser necessário gerar o nome do rótulo da versão instável.

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

Bem, na verdade, invente o nome do rótulo.

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

5. Verificando a ausência da mesma tag exata no repositório remoto

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

Se um rótulo semelhante foi criado anteriormente, o resultado do comando git ls-remote não estará vazio. A mesma versão deve ser publicada apenas uma vez.

6. Criando a ramificação apropriada no repositório de montagem

Como eu disse anteriormente, o repositório de versões compiladas do utilitário é um espelho do repositório com códigos-fonte. Portanto, se a publicação não for do ramo mestre ou de desenvolvimento, devemos criar o ramo correspondente no repositório do assembly. Bem, ou pelo menos certifique-se de sua existência

 /*       */ 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)); } 

Se o ramo estava ausente antes, inicializamos com um commit vazio usando o sinalizador --allow-empty .

7. Preparação de arquivo

Primeiro, você precisa excluir tudo o que poderia estar no repositório implantado. Afinal, se usarmos uma ramificação pré-existente, ela conterá a versão anterior do utilitário.

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

Em seguida, transferimos os arquivos atualizados necessários para publicação e os adicionamos ao índice do repositório.

 /*    */ 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)); 

Após essa manipulação, o git reconhecerá bem as alterações feitas pelas linhas dos arquivos. Dessa forma, obtemos um histórico de alterações consistente, mesmo no repositório de versões compiladas.

8. Confirmando e enviando alterações

Como uma mensagem de confirmação no repositório de montagem, usamos o nome do rótulo para versões estáveis. E para instável - uma mensagem de confirmação do repositório de origem. Dessa maneira, apoiando nossa ideia de um repositório de espelhos.

 /*       */ 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. Excluindo um Diretório Temporário

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

Revisão de atualizações em projetos comuns


Um dos processos mais importantes após fazer alterações em projetos comuns se torna uma revisão. Apesar da tecnologia desenvolvida permitir criar versões completamente isoladas de módulos, ninguém deseja ter dezenas de versões diferentes do mesmo utilitário. Portanto, cada um dos projetos comuns deve seguir um único caminho de desenvolvimento. Isso deve ser acordado entre as equipes.

A revisão das atualizações em projetos comuns é realizada por membros de todas as equipes, na medida do possível. Esse é um processo complexo, pois cada equipe vive em seu próprio sprint e possui uma carga de trabalho diferente. Às vezes, a transição para uma nova versão pode demorar.

Aqui você só pode recomendar não negligenciar e não atrasar esse processo.

Source: https://habr.com/ru/post/pt418135/


All Articles