So organisieren Sie Ihr eigenes Node.js-Modul-Repository mit Blackjack und Versionierung

Drei Front-End-Teams entwickeln derzeit drei große Projekte bei ISPsystem : ISPmanager für die Verwaltung von Webservern, VMmanager für die Arbeit mit Virtualisierung und BILLmanager für die Automatisierung des Geschäfts von Hostern. Die Teams arbeiten gleichzeitig in engen Fristen, sodass Sie nicht auf eine Optimierung verzichten können. Um Zeit zu sparen, verwenden wir gemeinsame Lösungen und nehmen gemeinsame Komponenten in separate Projekte auf. Solche Projekte haben ihre eigenen Repositories, die von Mitgliedern aller Teams unterstützt werden. In diesem Artikel geht es um den Aufbau dieser Repositories sowie um die Arbeit mit ihnen.



Wie sind die Repositories gemeinsamer Projekte?


Wir verwenden unseren eigenen Server mit GitLab , um Remote-Repositorys zu speichern. Für uns war es wichtig, das vertraute Arbeitsumfeld beizubehalten und im Verlauf ihrer Entwicklung mit gemeinsamen Modulen arbeiten zu können. Aus diesem Grund haben wir uns geweigert, in privaten Repositories von npmjs.com zu veröffentlichen. Glücklicherweise können Node.js-Module nicht nur mit NPM installiert werden, sondern auch aus anderen Quellen , einschließlich Git-Repositorys.

Wir schreiben in TypeScript, das anschließend zur späteren Verwendung in JavaScript kompiliert wird. Aber in unserer Zeit kompiliert das faule Frontend vielleicht nicht sein JavaScript. Daher benötigen wir unterschiedliche Repositorys für den Quellcode und das kompilierte Projekt.

Nach langen Diskussionen haben wir das folgende Konzept entwickelt. Es sollten zwei separate Repositorys für die Quellen und für die kompilierte Version des Moduls vorhanden sein. Darüber hinaus sollte das zweite Repository ein Spiegel des ersten sein.

Dies bedeutet, dass während der Entwicklung alle Funktionen vor der Veröffentlichung in der Niederlassung mit genau demselben Namen wie die Niederlassung veröffentlicht werden sollten, in der die Entwicklung durchgeführt wird. Somit haben wir die Möglichkeit, die experimentelle Version des Moduls zu verwenden und es von einem bestimmten Zweig aus zu installieren. Das, in dem wir uns entwickeln, ist sehr praktisch, um es in Aktion zu überprüfen.

Außerdem erstellen wir für jede Veröffentlichung ein Etikett, das den Projektstatus speichert. Der Labelname entspricht der in package.json angegebenen Version. Bei der Installation aus einem Git-Repository wird das Etikett nach dem Gitter angezeigt, zum Beispiel:

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

Auf diese Weise können wir die verwendete Version des Moduls reparieren und müssen uns keine Sorgen machen, dass jemand etwas ändert.

Beschriftungen werden auch für instabile Versionen erstellt. Im Quell-Repository, aus dem die Veröffentlichung stammt, wird ihnen jedoch ein abgekürzter Hash des Commits hinzugefügt. Hier ist ein Beispiel für ein solches Etikett:

 1.0.0_e5541dc1 

Mit diesem Ansatz können Sie die Eindeutigkeit von Labels erreichen und sie dem Quell-Repository zuordnen.

Da es sich um stabile und instabile Versionen des Moduls handelt, unterscheiden wir sie folgendermaßen: Wenn die Veröffentlichung vom Master- oder Entwicklungszweig ausgeführt wird, ist die Version stabil, andernfalls nicht.

Wie ist die Arbeit mit gemeinsamen Projekten organisiert?


Alle unsere Vereinbarungen wären nicht sinnvoll, wenn wir sie nicht automatisieren könnten. Automatisieren Sie insbesondere den Veröffentlichungsprozess. Im Folgenden werde ich zeigen, wie die Arbeit mit einem der gängigen Module organisiert ist - einem Dienstprogramm zum Testen benutzerdefinierter Skripte.

Dieses Dienstprogramm bereitet mithilfe der Puppenspielerbibliothek den Chromium-Browser für die Verwendung in Docker-Containern vor und führt Tests mit Mocha aus . Teilnehmer aller Teams können das Dienstprogramm ändern, ohne befürchten zu müssen, dass sie sich gegenseitig beschädigen.

Der folgende Befehl wird in die Datei package.json des Testdienstprogramms geschrieben:

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

Sie führt ein Skript in der Nähe aus:

Vollständiger Veröffentlichungsskriptcode
 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 


Dieser Code führt wiederum über das Modul child_process von Node.js alle erforderlichen Befehle aus.

Hier sind die Hauptphasen seiner Arbeit:


1. Überprüfen Sie, ob nicht autorisierte Änderungen vorliegen

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

Hier überprüfen wir das Ergebnis des Befehls git diff . Es ist nicht gut, wenn die Veröffentlichung Änderungen enthält, die nicht in der Quelle enthalten sind. Außerdem wird dadurch die Verbindung instabiler Versionen mit Commits unterbrochen.

2. Utility-Baugruppe

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

Die Build-Konstante erhält das Ergebnis des Builds. Wenn alles gut gegangen ist, ist der Statusparameter 0. Andernfalls wird nichts veröffentlicht.

3. Bereitstellen des kompilierten Versionsrepositorys

Der gesamte Veröffentlichungsprozess besteht lediglich aus dem Übermitteln von Änderungen an ein bestimmtes Repository. Daher erstellt das Skript in unserem Projekt ein temporäres Verzeichnis, in dem es das Git-Repository initialisiert und dem Remote-Assembly-Repository zuordnet.

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

Dies ist ein Standardprozess mit git init und git remote .

4. Generierung von Markennamen

Zuerst ermitteln wir den Namen des Zweigs, aus dem wir veröffentlichen, mit dem Befehl git symbolic-ref . Legen Sie den Namen des Zweigs fest, in den die Änderungen hochgeladen werden sollen (im Assembly-Repository befindet sich kein Entwicklungszweig).

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

Mit dem Befehl git rev-parse erhalten wir einen abgekürzten Hash des letzten Commits in dem Zweig, in dem wir uns befinden. Möglicherweise muss der Labelname der instabilen Version generiert werden.

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

Nun, machen Sie tatsächlich den Namen des Etiketts.

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

5. Überprüfen Sie, ob im Remote-Repository genau dasselbe Tag fehlt

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

Wenn zuvor ein ähnliches Label erstellt wurde, ist das Ergebnis des Befehls git ls-remote nicht leer. Die gleiche Version sollte nur einmal veröffentlicht werden.

6. Erstellen Sie den entsprechenden Zweig im Assembly-Repository

Wie ich bereits sagte, ist das Repository kompilierter Versionen des Dienstprogramms ein Spiegel des Repositorys mit Quellcodes. Wenn die Veröffentlichung nicht vom Master- oder Entwicklungszweig stammt, müssen wir daher den entsprechenden Zweig im Assembly-Repository erstellen. Nun, oder zumindest sicherstellen, dass es existiert

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

Wenn der Zweig zuvor nicht vorhanden war, initialisieren wir ihn mit einem leeren Commit mit dem Flag --allow-empty .

7. Dateivorbereitung

Zuerst müssen Sie alles löschen, was sich im bereitgestellten Repository befinden könnte. Wenn wir einen bereits vorhandenen Zweig verwenden, enthält dieser schließlich die vorherige Version des Dienstprogramms.

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

Als Nächstes übertragen wir die für die Veröffentlichung erforderlichen aktualisierten Dateien und fügen sie dem Repository-Index hinzu.

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

Nach einer solchen Manipulation erkennt git die Änderungen, die durch die Zeilen der Dateien vorgenommen wurden. Auf diese Weise erhalten wir auch im kompilierten Versionsrepository einen konsistenten Änderungsverlauf.

8. Änderungen übernehmen und einreichen

Als Commit-Nachricht im Assembly-Repository verwenden wir den Labelnamen für stabile Versionen. Und für instabil - eine Commit-Nachricht aus dem Quell-Repository. Auf diese Weise unterstützen wir unsere Idee eines Spiegel-Repositorys.

 /*       */ 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. Löschen eines temporären Verzeichnisses

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

Überprüfung von Updates in gängigen Projekten


Einer der wichtigsten Prozesse nach Änderungen an gemeinsamen Projekten wird eine Überprüfung. Trotz der Tatsache, dass Sie mit der entwickelten Technologie vollständig isolierte Versionen von Modulen erstellen können, möchte niemand Dutzende verschiedener Versionen desselben Dienstprogramms haben. Daher muss jedes der gemeinsamen Projekte einem einzigen Entwicklungspfad folgen. Dies sollte zwischen den Teams vereinbart werden.

Die Überprüfung der Aktualisierungen in gemeinsamen Projekten wird nach Möglichkeit von Mitgliedern aller Teams durchgeführt. Dies ist ein komplexer Prozess, da jedes Team von seinem eigenen Sprint lebt und eine andere Arbeitsbelastung hat. Manchmal kann sich der Übergang zu einer neuen Version verzögern.

Hier können Sie nur empfehlen, diesen Vorgang nicht zu vernachlässigen und nicht zu verzögern.

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


All Articles