Actualmente, tres equipos de front-end están desarrollando tres proyectos principales en ISPsystem: ISPmanager para administrar servidores web, VMmanager para trabajar con virtualización y BILLmanager para automatizar el negocio de los proveedores de alojamiento. Los equipos trabajan al mismo tiempo, en el modo de plazos ajustados, por lo que no puede prescindir de la optimización. Para ahorrar tiempo, utilizamos soluciones comunes y tomamos componentes comunes en proyectos separados. Dichos proyectos tienen sus propios repositorios, que cuentan con el apoyo de miembros de todos los equipos. Este artículo tratará sobre la construcción de estos repositorios, así como el trabajo con ellos.

¿Cómo son los repositorios de proyectos comunes?
Utilizamos nuestro propio servidor con
GitLab para almacenar repositorios remotos. Para nosotros era importante mantener el ambiente de trabajo familiar y poder trabajar con módulos comunes en el proceso de su desarrollo. Por lo tanto, nos negamos a publicar en repositorios privados
npmjs.com . Afortunadamente, los módulos Node.js pueden instalarse no solo con NPM, sino también desde
otras fuentes , incluidos los repositorios git.
Escribimos en TypeScript, que posteriormente se compila en JavaScript para su uso posterior. Pero en nuestro tiempo, tal vez el front-end perezoso no compila su JavaScript. Por lo tanto, necesitamos diferentes repositorios para el código fuente y el proyecto compilado.
Después de pasar por las espinas de largas discusiones, desarrollamos el siguiente concepto. Debe haber dos repositorios separados para las fuentes y para la versión compilada del módulo. Además, el segundo repositorio debe ser un espejo del primero.
Esto significa que durante el desarrollo, cualquier característica debe publicarse antes del lanzamiento en la rama con el mismo nombre que la rama en la que se está desarrollando. Por lo tanto, tenemos la oportunidad de usar la versión experimental del módulo, instalándolo desde una rama específica. El mismo en el que estamos desarrollando es muy conveniente para comprobarlo en acción.
Además, para cada publicación, creamos una etiqueta que guarda el estado del proyecto. El nombre de la etiqueta corresponde a la versión especificada en package.json. Al instalar desde un repositorio de git, la etiqueta se indica después de la red, por ejemplo:
npm install git+ssh://[url ]
Por lo tanto, podemos arreglar la versión utilizada del módulo y no preocuparnos de que alguien cambie algo.
Las etiquetas también se crean para versiones inestables, sin embargo, se les agrega un hash abreviado del commit en el repositorio de origen, desde el que se realizó la publicación. Aquí hay un ejemplo de tal etiqueta:
1.0.0_e5541dc1
Este enfoque le permite lograr la unicidad de las etiquetas, así como asociarlas con el repositorio de origen.
Como estamos hablando de versiones estables e inestables del módulo, así es como las distinguimos: si la publicación se realiza desde la rama maestra o de desarrollo, la versión es estable, de lo contrario no.
¿Cómo se organiza el trabajo con proyectos comunes?
Todos nuestros acuerdos no tendrían sentido si no pudiéramos automatizarlos. En particular, automatice el proceso de publicación. A continuación, mostraré cómo se organiza el trabajo con uno de los módulos comunes: una utilidad para probar scripts personalizados.
Esta utilidad, que utiliza la biblioteca de
titiriteros , prepara el navegador Chromium para su uso en contenedores acoplables y ejecuta pruebas con
Mocha . Los participantes de todos los equipos pueden modificar la utilidad sin temor a romper algo entre sí.
El siguiente comando está escrito en el archivo package.json de la utilidad de prueba:
"publish:git": "ts-node ./scripts/publish.ts"
Ella ejecuta un script cercano:
Código completo del script de publicación 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
A su vez, este código a través del módulo Node.js
child_process ejecuta todos los comandos necesarios.
Estas son las principales etapas de su trabajo:
1. Verifique los cambios no autorizados const isDiff = !!spawnSync('git', ['diff'], getSpawnOptions(rootDir, 'pipe')).stdout.toString().trim();
Aquí verificamos el resultado del
comando git diff . No es bueno si la publicación contiene cambios que no están en la fuente. Además, esto interrumpirá la conexión de versiones inestables con commits.
2. Asamblea de servicios públicos const build = spawnSync('npm', ['run', 'build'], getSpawnOptions(rootDir))
La constante de compilación obtiene el resultado de la compilación. Si todo salió bien, el parámetro de estado será 0. De lo contrario, no se publicará nada.
3. Despliegue del repositorio de versiones compiladasTodo el proceso de publicación no es más que enviar cambios a un repositorio específico. Por lo tanto, el script crea un directorio temporal en nuestro proyecto en el que inicializa el repositorio git y lo asocia con el repositorio de ensamblado remoto.
const tempDir = join(rootDir, 'temp'); if (existsSync(tempDir)) { spawnSync('rm', ['-rf', 'temp'], getSpawnOptions(rootDir)); } mkdirSync(tempDir); 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 es un proceso estándar que usa
git init y
git remote .
4. Generación de nombre de etiquetaPrimero, descubrimos el nombre de la rama desde la que estamos publicando usando el comando
git symbolic-ref . Y establezca el nombre de la rama en la que se cargarán los cambios (no hay una rama de desarrollo en el repositorio de ensamblados).
const branch = spawnSync( 'git', ['symbolic-ref', '--short', 'HEAD'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim(); const buildBranch = branch === 'develop' ? 'master' : branch;
Usando el comando
git rev-parse , obtenemos un hash abreviado del último commit en la rama en la que estamos. Puede ser necesario generar el nombre de la etiqueta de la versión inestable.
<source lang="typescript"> const shortSHA = spawnSync( 'git', ['rev-parse', '--short', 'HEAD'], getSpawnOptions(rootDir, 'pipe') ).stdout.toString().trim();
Bueno, en realidad inventa el nombre de la etiqueta.
const tag = buildBranch === 'master' ? version : `${version}_${shortSHA}`;
5. Comprobar la ausencia de la misma etiqueta exacta en el repositorio remoto /* */ const isTagExists = !!spawnSync( 'git', ['ls-remote', 'origin', `refs/tags/${tag}`], getSpawnOptions(tempDir, 'pipe') ).stdout.toString().trim();
Si se creó una etiqueta similar anteriormente, el resultado del
comando git ls-remote no estará vacío. La misma versión debe publicarse solo una vez.
6. Crear la rama apropiada en el repositorio de ensambladosComo dije anteriormente, el repositorio de versiones compiladas de la utilidad es un espejo del repositorio con códigos fuente. Por lo tanto, si la publicación no es de la rama maestra o de desarrollo, debemos crear la rama correspondiente en el repositorio de ensamblados. Bueno, o al menos asegúrate de que exista
/* */ 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 rama estuvo ausente antes, inicializamos con un commit vacío usando la bandera
--allow-empty .
7. Preparación de archivosPrimero debe eliminar todo lo que pueda estar en el repositorio desplegado. Después de todo, si usamos una rama preexistente, contiene la versión anterior de la utilidad.
spawnSync( 'rm', ['-rf', 'lib', 'package.json', 'package-lock.json', 'README.md'], getSpawnOptions(tempDir) );
A continuación, transferimos los archivos actualizados necesarios para su publicación y los agregamos al índice del repositorio.
/* */ 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));
Después de tal manipulación, git reconocerá bien los cambios realizados por las líneas de los archivos. De esta forma obtenemos un historial de cambios consistente incluso en el repositorio de versiones compiladas.
8. Comprometer y enviar cambiosComo mensaje de confirmación en el repositorio de ensamblados, utilizamos el nombre de la etiqueta para versiones estables. Y para inestable: un mensaje de confirmación del repositorio de origen. De esta manera, respaldamos nuestra idea de un almacenamiento espejo.
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. Eliminar un directorio temporal spawnSync('rm', ['-rf', 'temp'], getSpawnOptions(rootDir))
Revisión de actualizaciones en proyectos comunes
Uno de los procesos más importantes después de realizar cambios en proyectos comunes se convierte en una revisión. A pesar de que la tecnología desarrollada le permite crear versiones completamente aisladas de módulos, nadie quiere tener docenas de versiones diferentes de la misma utilidad. Por lo tanto, cada uno de los proyectos comunes debe seguir un único camino de desarrollo. Esto debe ser acordado entre los equipos.
La revisión de actualizaciones en proyectos comunes es llevada a cabo por miembros de todos los equipos en la medida de lo posible. Este es un proceso complejo, ya que cada equipo vive en su propio sprint y tiene una carga de trabajo diferente. A veces, la transición a una nueva versión puede retrasarse.
Aquí solo puede recomendar no descuidar y no retrasar este proceso.