Comment organiser votre propre référentiel de modules Node.js avec blackjack et versioning

Trois équipes front-end développent actuellement trois projets majeurs chez ISPsystem : ISPmanager pour la gestion des serveurs Web, VMmanager pour travailler avec la virtualisation et BILLmanager pour automatiser les activités des hébergeurs. Les équipes travaillent en même temps, dans un délai serré, donc l'optimisation ne peut être évitée. Pour gagner du temps, nous utilisons des solutions communes et intégrons des composants communs dans des projets distincts. Ces projets ont leurs propres référentiels, qui sont pris en charge par les membres de toutes les équipes. Cet article portera sur la construction de ces référentiels, ainsi que sur le travail avec eux.



Comment sont les référentiels de projets communs


Nous utilisons notre propre serveur avec GitLab pour stocker des référentiels distants. Il était important pour nous de maintenir l'environnement de travail familier et de pouvoir travailler avec des modules communs dans le processus de leur développement. Par conséquent, nous avons refusé de publier dans les référentiels privés npmjs.com . Heureusement, les modules Node.js peuvent être installés non seulement avec NPM, mais aussi à partir d' autres sources , y compris les référentiels git.

Nous écrivons en TypeScript, qui est ensuite compilé en JavaScript pour une utilisation ultérieure. Mais à notre époque, le frontal paresseux ne compile peut-être pas son JavaScript. Par conséquent, nous avons besoin de différents référentiels pour le code source et le projet compilé.

Après avoir traversé les épines de longues discussions, nous avons développé le concept suivant. Il devrait y avoir deux référentiels distincts pour les sources et pour la version compilée du module. De plus, le deuxième référentiel doit être un miroir du premier.

Cela signifie que pendant le développement, toute fonctionnalité doit être publiée avant la sortie dans la branche avec le même nom exact que la branche dans laquelle le développement est en cours. Ainsi, nous avons la possibilité d'utiliser la version expérimentale du module, en l'installant à partir d'une branche spécifique. Celui dans lequel nous développons est très pratique pour le vérifier en action.

De plus, pour chaque publication, nous créons une étiquette qui enregistre le statut du projet. Le nom de l'étiquette correspond à la version spécifiée dans package.json. Lors de l'installation à partir d'un référentiel git, l'étiquette est indiquée après le treillis, par exemple:

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

Ainsi, nous pouvons corriger la version utilisée du module et ne pas craindre que quelqu'un change quelque chose.

Des étiquettes sont également créées pour les versions instables, cependant, un hachage abrégé de la validation leur est ajouté dans le référentiel source, à partir duquel la publication a été effectuée. Voici un exemple d'une telle étiquette:

 1.0.0_e5541dc1 

Cette approche vous permet de réaliser l'unicité des étiquettes, ainsi que de les associer au référentiel source.

Puisque nous parlons de versions stables et instables du module, voici comment nous les distinguons: si la publication est effectuée à partir de la branche master ou develop, la version est stable, sinon non.

Comment s'organise le travail avec les projets communs?


Tous nos accords n'auraient aucun sens si nous ne pouvions pas les automatiser. En particulier, automatisez le processus de publication. Ci-dessous, je montrerai comment le travail est organisé avec l'un des modules communs - un utilitaire pour tester des scripts personnalisés.

Cet utilitaire, à l'aide de la bibliothèque de marionnettistes , prépare le navigateur Chromium pour une utilisation dans les conteneurs Docker et exécute des tests à l' aide de Mocha . Les participants de toutes les équipes peuvent modifier l'utilitaire sans craindre de casser quelque chose les uns des autres.

La commande suivante est écrite dans le fichier package.json de l'utilitaire de test:

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

Elle exécute un script à proximité:

Code de script de publication complet
 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 


À son tour, ce code via le module Node.js child_process exécute toutes les commandes nécessaires.

Voici les principales étapes de son travail:


1. Vérifiez les modifications non autorisées

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

Ici, nous vérifions le résultat de la commande git diff . Ce n'est pas bon si la publication contient des modifications qui ne sont pas dans la source. De plus, cela rompra la connexion des versions instables avec les validations.

2. Ensemble utilitaire

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

La constante de génération obtient le résultat de la génération. Si tout s'est bien passé, le paramètre d'état sera 0. Sinon, rien ne sera publié.

3. Déploiement du référentiel de versions compilé

L'ensemble du processus de publication n'est rien de plus que la soumission de modifications à un référentiel spécifique. Par conséquent, le script crée un répertoire temporaire dans notre projet dans lequel il initialise le référentiel git et l'associe au référentiel d'assembly distant.

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

Il s'agit d'un processus standard utilisant git init et git remote .

4. Génération de nom d'étiquette

Tout d'abord, nous découvrons le nom de la branche à partir de laquelle nous publions à l'aide de la commande git symbolic-ref . Et définissez le nom de la branche dans laquelle les modifications seront téléchargées (il n'y a pas de branche de développement dans le référentiel d'assembly).

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

En utilisant la commande git rev-parse , nous obtenons un hachage abrégé du dernier commit dans la branche dans laquelle nous nous trouvons. Il peut être nécessaire de générer le nom d'étiquette de la version instable.

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

Eh bien, inventez le nom du label.

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

5. Vérification de l'absence de la même balise exacte dans le référentiel distant

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

Si une étiquette similaire a été créée précédemment, le résultat de la commande git ls-remote ne sera pas vide. La même version ne doit être publiée qu'une seule fois.

6. Création de la branche appropriée dans le référentiel d'assembly

Comme je l'ai dit plus tôt, le référentiel des versions compilées de l'utilitaire est un miroir du référentiel avec les codes sources. Par conséquent, si la publication ne provient pas de la branche master ou develop, nous devons créer la branche correspondante dans le référentiel d'assembly. Eh bien, ou du moins assurez-vous que son existence

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

Si la branche était absente auparavant, nous initialisons avec un commit vide en utilisant l' indicateur --allow-empty .

7. Préparation du dossier

Vous devez d'abord supprimer tout ce qui pourrait se trouver dans le référentiel déployé. Après tout, si nous utilisons une branche préexistante, elle contient la version précédente de l'utilitaire.

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

Ensuite, nous transférons les fichiers mis à jour nécessaires à la publication et les ajoutons à l'index du référentiel.

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

Après une telle manipulation, git reconnaîtra bien les modifications apportées par les lignes des fichiers. De cette façon, nous obtenons un historique des modifications cohérent même dans le référentiel de versions compilé.

8. Validation et soumission des modifications

En tant que message de validation dans le référentiel d'assembly, nous utilisons le nom d'étiquette pour les versions stables. Et pour unstable - un message de validation du référentiel source. De cette façon, soutenir notre idée d'un référentiel miroir.

 /*       */ 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. Suppression d'un répertoire temporaire

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

Examen des mises à jour dans les projets communs


L'un des processus les plus importants après avoir apporté des modifications à des projets communs devient un examen. Malgré le fait que la technologie développée vous permet de créer des versions de modules complètement isolées, personne ne veut avoir des dizaines de versions différentes du même utilitaire. Par conséquent, chacun des projets communs doit suivre un chemin de développement unique. Cela devrait être convenu entre les équipes.

L'examen des mises à jour dans les projets communs est effectué par les membres de toutes les équipes chaque fois que possible. C'est un processus complexe, car chaque équipe vit sur son propre sprint et a une charge de travail différente. Parfois, la transition vers une nouvelle version peut être retardée.

Ici, vous ne pouvez que recommander de ne pas négliger et de ne pas retarder ce processus.

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


All Articles