Bump Harbor versions and add direct .ru routing
This commit is contained in:
177
scripts/harbor-version.mjs
Normal file
177
scripts/harbor-version.mjs
Normal file
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { parseVersion, versionCompatibility } from '../src/shared/versions.js';
|
||||
|
||||
const COMPONENTS = ['macClient', 'gatewayClient', 'gatewayBackend'];
|
||||
const VERSION_FILE = 'src/shared/versions.js';
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const aliases = {
|
||||
mac: 'macClient',
|
||||
'mac-client': 'macClient',
|
||||
client: 'gatewayClient',
|
||||
'gateway-client': 'gatewayClient',
|
||||
backend: 'gatewayBackend',
|
||||
'gateway-backend': 'gatewayBackend',
|
||||
};
|
||||
|
||||
export function versionsFromSource(source) {
|
||||
return Object.fromEntries(COMPONENTS.map((component) => {
|
||||
const match = new RegExp(`${component}:\\s*'(\\d+\\.\\d+\\.\\d+)'`).exec(source);
|
||||
if (!match) throw new Error(`Не найдена версия ${component}`);
|
||||
return [component, match[1]];
|
||||
}));
|
||||
}
|
||||
|
||||
export function affectedComponents(files) {
|
||||
const affected = new Set();
|
||||
const add = (...components) => components.forEach((component) => affected.add(component));
|
||||
for (const file of files) {
|
||||
if (file === VERSION_FILE) continue;
|
||||
if (/^(package-lock\.json|src\/shared\/)/.test(file)) add(...COMPONENTS);
|
||||
else if (/^(src\/web\/|public\/|index\.html$|vite\.config\.js$)/.test(file)) {
|
||||
add('macClient', 'gatewayClient');
|
||||
} else if (/^src\/server\//.test(file)) add('macClient', 'gatewayBackend');
|
||||
else if (/^(Dockerfile\.client|docker-compose\.client\.yml|entrypoint\.client\.sh|scripts\/(install-macos-client|harbor-network-monitor)\.sh)$/.test(file)) {
|
||||
add('macClient');
|
||||
} else if (/^(Dockerfile|Dockerfile\.runtime-base|docker-compose\.gateway\.yml|entrypoint\.sh|scripts\/(deploy-gateway|build-runtime-base|build-on-107-deploy-111)\.sh)$/.test(file)) {
|
||||
add('gatewayBackend');
|
||||
}
|
||||
}
|
||||
return COMPONENTS.filter((component) => affected.has(component));
|
||||
}
|
||||
|
||||
function formatVersion({ major, minor, hotfix }) {
|
||||
return `${major}.${minor}.${hotfix}`;
|
||||
}
|
||||
|
||||
export function bumpVersions(versions, level, requested = []) {
|
||||
const current = Object.fromEntries(COMPONENTS.map((component) => {
|
||||
const parsed = parseVersion(versions[component]);
|
||||
if (!parsed) throw new Error(`Некорректная версия ${component}: ${versions[component]}`);
|
||||
return [component, parsed];
|
||||
}));
|
||||
if (level === 'major') {
|
||||
const major = Math.max(...COMPONENTS.map((component) => current[component].major)) + 1;
|
||||
return Object.fromEntries(COMPONENTS.map((component) => [component, `${major}.0.0`]));
|
||||
}
|
||||
|
||||
const targets = new Set(requested.map((target) => aliases[target] || target));
|
||||
if (!targets.size) throw new Error(`${level} требует хотя бы один компонент`);
|
||||
for (const target of targets) {
|
||||
if (!COMPONENTS.includes(target)) throw new Error(`Неизвестный компонент: ${target}`);
|
||||
}
|
||||
if (level === 'minor' && (targets.has('gatewayClient') || targets.has('gatewayBackend'))) {
|
||||
targets.add('gatewayClient');
|
||||
targets.add('gatewayBackend');
|
||||
}
|
||||
if (!['minor', 'hotfix'].includes(level)) throw new Error(`Неизвестный уровень: ${level}`);
|
||||
|
||||
const next = { ...versions };
|
||||
if (level === 'minor' && targets.has('gatewayClient')) {
|
||||
const minor = Math.max(current.gatewayClient.minor, current.gatewayBackend.minor) + 1;
|
||||
next.gatewayClient = `${current.gatewayClient.major}.${minor}.0`;
|
||||
next.gatewayBackend = `${current.gatewayBackend.major}.${minor}.0`;
|
||||
targets.delete('gatewayClient');
|
||||
targets.delete('gatewayBackend');
|
||||
}
|
||||
for (const target of targets) {
|
||||
const value = current[target];
|
||||
next[target] = level === 'minor'
|
||||
? `${value.major}.${value.minor + 1}.0`
|
||||
: formatVersion({ ...value, hotfix: value.hotfix + 1 });
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function git(args) {
|
||||
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
||||
}
|
||||
|
||||
function changedFiles(base) {
|
||||
const tracked = git(['diff', '--name-only', base, '--']).split('\n');
|
||||
const untracked = git(['ls-files', '--others', '--exclude-standard']).split('\n');
|
||||
return [...new Set([...tracked, ...untracked].filter(Boolean))];
|
||||
}
|
||||
|
||||
function baselineVersions(base) {
|
||||
try {
|
||||
return versionsFromSource(git(['show', `${base}:${VERSION_FILE}`]));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function compareVersions(before, after) {
|
||||
const left = parseVersion(before);
|
||||
const right = parseVersion(after);
|
||||
if (!left || !right) return -1;
|
||||
for (const key of ['major', 'minor', 'hotfix']) {
|
||||
if (right[key] !== left[key]) return right[key] > left[key] ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function validateCompatibility(versions) {
|
||||
const compatibility = versionCompatibility(versions);
|
||||
if (!compatibility.major) throw new Error('У всех компонентов должен совпадать major');
|
||||
if (!compatibility.gateway) throw new Error('Gateway client и backend должны совпадать по major.minor');
|
||||
}
|
||||
|
||||
function writeVersions(versions) {
|
||||
const file = path.join(root, VERSION_FILE);
|
||||
let source = fs.readFileSync(file, 'utf8');
|
||||
for (const component of COMPONENTS) {
|
||||
source = source.replace(
|
||||
new RegExp(`(${component}:\\s*')\\d+\\.\\d+\\.\\d+(')`),
|
||||
`$1${versions[component]}$2`,
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(file, source);
|
||||
}
|
||||
|
||||
function printVersions(versions) {
|
||||
for (const component of COMPONENTS) console.log(`${component}: ${versions[component]}`);
|
||||
}
|
||||
|
||||
function main([command = 'check', ...args]) {
|
||||
const source = fs.readFileSync(path.join(root, VERSION_FILE), 'utf8');
|
||||
const current = versionsFromSource(source);
|
||||
validateCompatibility(current);
|
||||
|
||||
if (command === 'bump') {
|
||||
const next = bumpVersions(current, args[0], args.slice(1));
|
||||
validateCompatibility(next);
|
||||
writeVersions(next);
|
||||
printVersions(next);
|
||||
return;
|
||||
}
|
||||
|
||||
const base = args[0] || 'HEAD';
|
||||
const affected = affectedComponents(changedFiles(base));
|
||||
if (command === 'affected') {
|
||||
console.log(affected.length ? affected.join('\n') : 'Нет изменений, требующих bump.');
|
||||
return;
|
||||
}
|
||||
if (command !== 'check') throw new Error(`Неизвестная команда: ${command}`);
|
||||
|
||||
const baseline = baselineVersions(base);
|
||||
if (!baseline) {
|
||||
console.log('Version contract создаётся впервые; baseline для bump отсутствует.');
|
||||
return;
|
||||
}
|
||||
const missing = affected.filter((component) => compareVersions(baseline[component], current[component]) <= 0);
|
||||
if (missing.length) throw new Error(`Не повышена версия: ${missing.join(', ')}`);
|
||||
console.log(affected.length ? `Version check: ${affected.join(', ')}` : 'Version check: bump не требуется.');
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
try {
|
||||
main(process.argv.slice(2));
|
||||
} catch (error) {
|
||||
console.error(`[harbor-version] ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user