Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/docker-package-extraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
'@alfalab/scripts-artifacts': minor
'arui-scripts': patch
---

Сборка артефактов поставки вынесена в отдельный пакет `@alfalab/scripts-artifacts`.

Пакет устроен как набор чистых функций и настраивается одним файлом `arui-scripts-artifacts.ts` в
корне проекта (путь можно задать через `--c`/`--config`). Конфиг может быть на TypeScript, ESM или
CommonJS и описывает всё: базовый образ, параметры nginx, кастомные шаблоны и свои команды сборки —
так что кастомные сборочные скрипты в проектах больше не нужны.

Поддерживаются оба типа артефактов: docker-образ (`docker-build`, `docker-build:compiled`) и
tar-архив (`archive-build`). Хост-пайплайн (очистка `buildPath` → сборка приложения → удаление
dev-зависимостей) у них общий.

`arui-scripts` теперь использует этот пакет вместо собственной копии шаблонов и утилит. Поведение
команд и все ключи оверрайдов (`Dockerfile`, `DockerfileCompiled`, `nginx`, `nginxConf`, `start.sh`)
сохранены без изменений — отрендеренные Dockerfile, nginx-конфиги и start.sh совпадают побайтово.

Попутно починен `archive-build`: он падал с `TypeError: Cannot read properties of undefined
(reading 'c')`, потому что в `tar@7` нет default-экспорта, а код использовал `import tar from 'tar'`.

Настройки сборки артефактов в конфиге arui-scripts (`dockerRegistry`, `baseDockerImage`,
`nginxRootPath`, `nginx`, `runFromNonRootUser`, `removeDevDependenciesDuringDockerBuild`,
`archiveName`, `additionalBuildPath`) и оверрайды `Dockerfile`, `DockerfileCompiled`, `nginx`,
`nginxConf`, `start.sh` объявлены устаревшими: они продолжают работать, но команды сборки печатают
предупреждение со ссылкой на замену, а в следующей мажорной версии будут удалены.

Настройки в конфиге @alfalab/scripts-artifacts сгруппированы по доменам: `docker`, `nginx`, `archive`, `build`,
`packageManager`, `localFiles`. Так же разложен и код пакета — по папке на домен.

Единственное отличие в поведении: `archive-build` теперь подхватывает локальный `start.sh` из корня
проекта так же, как уже подхватывал `nginx.conf` (раньше игнорировал). Отключается опцией
`localFiles.allowStartScript: false`.
2 changes: 2 additions & 0 deletions packages/arui-scripts-artifacts/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
.turbo
8 changes: 8 additions & 0 deletions packages/arui-scripts-artifacts/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
root: true,
extends: [require.resolve('arui-presets-lint/eslint')],
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.eslint.json'],
},
};
2 changes: 2 additions & 0 deletions packages/arui-scripts-artifacts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
build
3 changes: 3 additions & 0 deletions packages/arui-scripts-artifacts/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
src
build/tsconfig.tsbuildinfo
__tests__
2 changes: 2 additions & 0 deletions packages/arui-scripts-artifacts/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.turbo
build
1 change: 1 addition & 0 deletions packages/arui-scripts-artifacts/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @alfalab/scripts-artifacts
298 changes: 298 additions & 0 deletions packages/arui-scripts-artifacts/README.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/arui-scripts-artifacts/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// фикстуры лежат рядом с тестами и сами тестами не являются
testMatch: ['**/__tests__/**/*.test.ts'],
testPathIgnorePatterns: ['/node_modules/', '/build/'],
};
50 changes: 50 additions & 0 deletions packages/arui-scripts-artifacts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@alfalab/scripts-artifacts",
"version": "1.0.0",
"description": "Максимально кастомизируемые скрипты и шаблоны для сборки артефактов поставки (docker-образ, tar-архив) приложений, основанных на arui-scripts",
"main": "./build/index.js",
"typings": "./build/index.d.ts",
"bin": {
"arui-scripts-artifacts": "./build/bin/index.js"
},
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/core-ds/arui-scripts.git"
},
"bugs": {
"url": "https://github.com/core-ds/arui-scripts/issues"
},
"homepage": "https://github.com/core-ds/arui-scripts/tree/master/packages/arui-scripts-artifacts#readme",
"scripts": {
"build": "tsc --project tsconfig.json",
"test": "jest",
"lint:scripts": "arui-presets-lint scripts",
"format": "arui-presets-lint format",
"format:check": "arui-presets-lint format:check",
"lint": "yarn lint:scripts && yarn format:check",
"lint:fix": "yarn lint:scripts --fix && yarn format",
"audit": "yarn npm audit --severity high --environment production"
},
"dependencies": {
"commander": "^12",
"fs-extra": "6.0.1",
"jiti": "^2.4.2",
"semver": "^7.5.4",
"shelljs": "0.8.5",
"tar": "7.5.11"
},
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.0",
"@types/semver": "^7.5.0",
"@types/shelljs": "^0.8.12",
"jest": "^29.7.0",
"ts-jest": "29.1.0",
"typescript": "6.0.2"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}
103 changes: 103 additions & 0 deletions packages/arui-scripts-artifacts/src/archive/build-archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import path from 'path';

import fs from 'fs-extra';
import { create as createTar } from 'tar';

import { resolveArtifactsConfig } from '../config';
import { type ArtifactsOptions, type ResolvedArtifactsConfig } from '../config/types';
import { NGINX_CONFIG_FILENAME } from '../nginx/constants';
import { type BeforeBuildHook, runHostPipeline } from '../pipeline/host-pipeline';
import { renderTemplates } from '../pipeline/render';
import { START_SCRIPT_FILENAME } from '../start-script/constants';

export type BuildArchiveOptions = ArtifactsOptions & {
/** Хук, вызываемый после очистки `buildPath`, но до сборки приложения. */
beforeBuild?: BeforeBuildHook;
};

const NODE_MODULES_DIR_NAME = 'node_modules';
const PACKAGE_JSON_FILENAME = 'package.json';

/**
* Собирает tar-архив с production-сборкой: nginx-конфиг, start.sh, `buildPath`, `node_modules`,
* `package.json` и дополнительные директории из `archive.additionalPaths`.
*
* Хост-пайплайн (очистка, сборка, удаление dev-зависимостей) — тот же, что у docker-образа, поэтому
* архив и образ собираются из одного и того же состояния проекта.
*/
export async function buildArchive(options: BuildArchiveOptions = {}): Promise<void> {
const { beforeBuild, templates, overrides, ...rest } = options;

const config: ResolvedArtifactsConfig = resolveArtifactsConfig({
...rest,
artifact: 'archive',
});

const { cwd, buildPath, archive, localFiles } = config;
const pathToTempDir = path.join(cwd, archive.tempDirName);

try {
console.log(`Build archive ${archive.name}`);

Check warning on line 40 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.time('Total time');

Check warning on line 41 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.time('Setting up time');

Check warning on line 42 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

const rendered = renderTemplates({ config, templates, overrides });

await fs.emptyDir(pathToTempDir);

const nginxConf = localFiles.nginxConf
? await fs.readFile(localFiles.nginxConf, 'utf8')
: rendered.nginxConf;

const startScript =
localFiles.startScript && localFiles.allowStartScript
? await fs.readFile(localFiles.startScript, 'utf8')
: rendered.startScript;

await Promise.all([
fs.writeFile(path.join(pathToTempDir, NGINX_CONFIG_FILENAME), nginxConf, 'utf8'),
fs.writeFile(path.join(pathToTempDir, START_SCRIPT_FILENAME), startScript, {
encoding: 'utf8',
mode: 0o555,
}),
]);

console.timeEnd('Setting up time');

Check warning on line 65 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

await runHostPipeline(config, beforeBuild);

console.time('Archive build time');

Check warning on line 69 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

await Promise.all([
fs.copy(path.resolve(cwd, buildPath), path.join(pathToTempDir, buildPath)),
fs.copy(
path.join(cwd, NODE_MODULES_DIR_NAME),
path.join(pathToTempDir, NODE_MODULES_DIR_NAME),
),
fs.copy(
path.join(cwd, PACKAGE_JSON_FILENAME),
path.join(pathToTempDir, PACKAGE_JSON_FILENAME),
),
...archive.additionalPaths.map((additionalPath) =>
fs.copy(path.join(cwd, additionalPath), path.join(pathToTempDir, additionalPath)),
),
]);

await createTar({ file: archive.name, cwd: pathToTempDir }, fs.readdirSync(pathToTempDir));

console.timeEnd('Archive build time');

Check warning on line 88 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.time('Cleanup time');

Check warning on line 89 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement

await fs.remove(pathToTempDir);

console.timeEnd('Cleanup time');

Check warning on line 93 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
console.timeEnd('Total time');

Check warning on line 94 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
} catch (err) {
await fs.remove(pathToTempDir);
console.error('Error during archive-build.');

Check warning on line 97 in packages/arui-scripts-artifacts/src/archive/build-archive.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
if (config.debug) {
console.error(err);
}
throw err;
}
}
8 changes: 8 additions & 0 deletions packages/arui-scripts-artifacts/src/archive/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Имя tar-архива по умолчанию. */
export const DEFAULT_ARCHIVE_NAME = 'build.tar';

/** Имя временной директории, в которой собирается содержимое tar-архива. */
export const DEFAULT_ARCHIVE_TEMP_DIR_NAME = '.archive-build';

/** Директории проекта, которые кладутся в архив рядом со сборкой. */
export const DEFAULT_ARCHIVE_ADDITIONAL_PATHS = ['config'];
6 changes: 6 additions & 0 deletions packages/arui-scripts-artifacts/src/archive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { buildArchive, type BuildArchiveOptions } from './build-archive';
export {
DEFAULT_ARCHIVE_ADDITIONAL_PATHS,
DEFAULT_ARCHIVE_NAME,
DEFAULT_ARCHIVE_TEMP_DIR_NAME,
} from './constants';
66 changes: 66 additions & 0 deletions packages/arui-scripts-artifacts/src/bin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#! /usr/bin/env node
import path from 'path';

import fs from 'fs-extra';

import { resolveCommandOptions } from '../cli/config-file';
import { createCli, extractConfigPath } from '../cli/create-cli';
import { resolveConfigFile } from '../cli/load-config-file';
import { type LocalFilesOptions } from '../config/types';
import { DOCKERFILE_FILENAME } from '../docker/constants';
import { BASE_NGINX_CONFIG_FILENAME, NGINX_CONFIG_FILENAME } from '../nginx/constants';
import { buildArtifact } from '../pipeline/build-artifact';
import { START_SCRIPT_FILENAME } from '../start-script/constants';

// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const { version } = require('../../package.json');

/**
* Локальные файлы проекта, замещающие сгенерированные шаблоны, если они лежат в корне проекта.
* Явно заданные в конфиге `localFiles` имеют приоритет над автодетектом.
*/
function detectLocalFiles(cwd: string): LocalFilesOptions {
const resolveIfExists = (fileName: string) => {
const filePath = path.join(cwd, fileName);

return fs.existsSync(filePath) ? filePath : null;
};

return {
dockerfile: resolveIfExists(DOCKERFILE_FILENAME),
startScript: resolveIfExists(START_SCRIPT_FILENAME),
nginxConf: resolveIfExists(NGINX_CONFIG_FILENAME),
nginxBaseConf: resolveIfExists(BASE_NGINX_CONFIG_FILENAME),
};
}

(async () => {
const argv = process.argv.slice(2);
const cwd = process.cwd();

const configFile = await resolveConfigFile(cwd, extractConfigPath(argv));

const program = createCli({
configFile,
version,
run: async ({ command, args }) => {
const options = resolveCommandOptions(command, configFile);

if (!options) {
throw new Error(`Unknown command: ${command}`);
}

await buildArtifact({
...options,
localFiles: { ...detectLocalFiles(cwd), ...options.localFiles },
// аргументы командной строки (name=... version=... registry=...) — высший приоритет
argv: args,
});
},
});

await program.parseAsync(argv, { from: 'user' });
})().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});
Loading
Loading