release.sh: переставлять split-ветки принудительно
git subtree split -b отказывается двигать существующую ветку, если новый коммит ей не потомок — после reset в ядре ветка молча оставалась старой, и продукт тянул устаревшую половину. Ветки производные, поэтому branch -f. Поймано doctor'ом: сверка поддерева с ядром показала расхождение серверной половины при совпадающей браузерной. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LeBkZLiQrvdECf5nfgiMz
This commit is contained in:
+34
-2
@@ -15,6 +15,7 @@
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -47,6 +48,24 @@ const SERVER_CORE = path.join(BACK, 'core');
|
|||||||
// «Demo Panel» в нём норма, а не недоделка. Узнаётся по каталогу core/ рядом.
|
// «Demo Panel» в нём норма, а не недоделка. Узнаётся по каталогу core/ рядом.
|
||||||
const IS_TEMPLATE = fs.existsSync(path.join(PANEL, 'core', 'browser'));
|
const IS_TEMPLATE = fs.existsSync(path.join(PANEL, 'core', 'browser'));
|
||||||
|
|
||||||
|
// Ядро попадает в панель одним из двух способов, и проверки у них разные:
|
||||||
|
//
|
||||||
|
// subtree — панель лежит в git-репозитории продукта, ядро подтянуто
|
||||||
|
// `git subtree add` и потому отслеживается git. Обновляется
|
||||||
|
// `git subtree pull`; update.sh здесь не нужен и вреден — он
|
||||||
|
// сносит каталог целиком и затёр бы поддерево.
|
||||||
|
//
|
||||||
|
// copy — ядро раскладывает update.sh из соседнего ma7engine. Так устроен
|
||||||
|
// сам шаблон, и так живёт панель вне git.
|
||||||
|
function coreIsTracked(dir) {
|
||||||
|
try {
|
||||||
|
const out = execFileSync('git', ['ls-files', '--', dir],
|
||||||
|
{ cwd: PANEL, stdio: ['ignore', 'pipe', 'ignore'] });
|
||||||
|
return out.toString().trim() !== '';
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
const MODE = !IS_TEMPLATE && coreIsTracked(path.join(FRONT, 'src/core')) ? 'subtree' : 'copy';
|
||||||
|
|
||||||
// ─── накопитель результатов ─────────────────────────────────────────────────
|
// ─── накопитель результатов ─────────────────────────────────────────────────
|
||||||
const errors = [], warns = [], skips = [], oks = [];
|
const errors = [], warns = [], skips = [], oks = [];
|
||||||
|
|
||||||
@@ -71,13 +90,25 @@ if (!fs.existsSync(FRONT)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateSh = path.join(FRONT, 'scripts/update.sh');
|
const updateSh = path.join(FRONT, 'scripts/update.sh');
|
||||||
if (fs.existsSync(updateSh)) ok('update.sh на месте');
|
if (MODE === 'subtree') {
|
||||||
|
if (fs.existsSync(updateSh)) {
|
||||||
|
err('в subtree-панели остался frontend/scripts/update.sh',
|
||||||
|
'он сносит каталог ядра целиком (rm -rf) и затрёт подтянутое поддерево — ' +
|
||||||
|
'обновление здесь делается git subtree pull');
|
||||||
|
} else ok('ядро приходит через git subtree (update.sh не нужен)');
|
||||||
|
} else if (fs.existsSync(updateSh)) ok('update.sh на месте');
|
||||||
else err('нет frontend/scripts/update.sh', 'без него ядро в панели не обновляется');
|
else err('нет frontend/scripts/update.sh', 'без него ядро в панели не обновляется');
|
||||||
|
|
||||||
const frontPkgText = need(path.join(FRONT, 'package.json'), 'фронт');
|
const frontPkgText = need(path.join(FRONT, 'package.json'), 'фронт');
|
||||||
const frontPkg = frontPkgText ? JSON.parse(frontPkgText) : null;
|
const frontPkg = frontPkgText ? JSON.parse(frontPkgText) : null;
|
||||||
if (frontPkg) {
|
if (frontPkg) {
|
||||||
const s = frontPkg.scripts ?? {};
|
const s = frontPkg.scripts ?? {};
|
||||||
|
if (MODE === 'subtree') {
|
||||||
|
const stale = ['update', 'predev', 'prebuild'].filter(h => /update/.test(s[h] ?? ''));
|
||||||
|
if (stale.length) err(`frontend/package.json: остались хуки копирования (${stale.join(', ')})`,
|
||||||
|
'ядро приходит поддеревом — эти скрипты его затрут');
|
||||||
|
else ok('в package.json нет хуков копирования ядра');
|
||||||
|
} else {
|
||||||
if (/update\.sh/.test(s.update ?? '')) ok('скрипт update ведёт на scripts/update.sh');
|
if (/update\.sh/.test(s.update ?? '')) ok('скрипт update ведёт на scripts/update.sh');
|
||||||
else err('frontend/package.json: скрипт "update" не зовёт scripts/update.sh');
|
else err('frontend/package.json: скрипт "update" не зовёт scripts/update.sh');
|
||||||
|
|
||||||
@@ -86,6 +117,7 @@ if (frontPkg) {
|
|||||||
else err(`frontend/package.json: нет "${hook}": "npm run update"`,
|
else err(`frontend/package.json: нет "${hook}": "npm run update"`,
|
||||||
'без него dev и build идут на устаревшей копии ядра');
|
'без него dev и build идут на устаревшей копии ядра');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tsconfig.json шаблона содержит //-комментарий — JSON.parse на нём падает,
|
// tsconfig.json шаблона содержит //-комментарий — JSON.parse на нём падает,
|
||||||
@@ -330,7 +362,7 @@ if (!IS_TEMPLATE) {
|
|||||||
|
|
||||||
// ─── вывод ──────────────────────────────────────────────────────────────────
|
// ─── вывод ──────────────────────────────────────────────────────────────────
|
||||||
const rel = path.relative(process.cwd(), PANEL) || '.';
|
const rel = path.relative(process.cwd(), PANEL) || '.';
|
||||||
console.log(`doctor: ${rel}${IS_TEMPLATE ? ' (шаблон ma7engine)' : ''}\n`);
|
console.log(`doctor: ${rel}${IS_TEMPLATE ? ' (шаблон ma7engine)' : ` (ядро: ${MODE})`}\n`);
|
||||||
|
|
||||||
for (const m of oks) console.log(` ok ${m}`);
|
for (const m of oks) console.log(` ok ${m}`);
|
||||||
for (const m of skips) console.log(` — ${m}`);
|
for (const m of skips) console.log(` — ${m}`);
|
||||||
|
|||||||
+30
-12
@@ -76,7 +76,15 @@ function edit(relPath, pairs) {
|
|||||||
fs.writeFileSync(file, text);
|
fs.writeFileSync(file, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
edit('frontend/package.json', [['"ma7engine-example-frontend"', `"${slug}-frontend"`]]);
|
edit('frontend/package.json', [
|
||||||
|
['"ma7engine-example-frontend"', `"${slug}-frontend"`],
|
||||||
|
// В панели ядро приезжает через git subtree, а не копированием: хуки
|
||||||
|
// predev/prebuild и сам update.sh здесь не нужны и вредны — update.sh
|
||||||
|
// сносит каталог целиком (rm -rf) и затёр бы подтянутое поддерево.
|
||||||
|
[` "predev": "npm run update",\n`, ''],
|
||||||
|
[` "update": "sh scripts/update.sh",\n`, ''],
|
||||||
|
[` "prebuild": "npm run update",\n`, ''],
|
||||||
|
]);
|
||||||
// Заголовок вкладки браузера — не то же, что branding.name в App.tsx: его видно
|
// Заголовок вкладки браузера — не то же, что branding.name в App.tsx: его видно
|
||||||
// до того, как React смонтируется, и в закладках.
|
// до того, как React смонтируется, и в закладках.
|
||||||
edit('frontend/index.html', [['<title>Demo Panel</title>', `<title>${display}</title>`]]);
|
edit('frontend/index.html', [['<title>Demo Panel</title>', `<title>${display}</title>`]]);
|
||||||
@@ -193,6 +201,13 @@ for (const f of ['Dockerfile', '.dockerignore']) {
|
|||||||
fs.copyFileSync(path.join(ROOT, f), path.join(target, f));
|
fs.copyFileSync(path.join(ROOT, f), path.join(target, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Доктор берётся из scripts/, а не тот, что мог приехать внутри frontend/:
|
||||||
|
// в шаблоне frontend/scripts/doctor.mjs — вендоренная копия, и она бывает
|
||||||
|
// старше исходника. Панель должна получить актуальные проверки.
|
||||||
|
fs.mkdirSync(path.join(target, 'frontend/scripts'), { recursive: true });
|
||||||
|
fs.copyFileSync(path.join(ROOT, 'scripts/doctor.mjs'),
|
||||||
|
path.join(target, 'frontend/scripts/doctor.mjs'));
|
||||||
|
|
||||||
fs.mkdirSync(path.join(target, 'scripts'), { recursive: true });
|
fs.mkdirSync(path.join(target, 'scripts'), { recursive: true });
|
||||||
const dev = fs.readFileSync(path.join(ROOT, 'scripts/demo.sh'), 'utf8')
|
const dev = fs.readFileSync(path.join(ROOT, 'scripts/demo.sh'), 'utf8')
|
||||||
.replace('# Поднимает демо-панель целиком: бэкенд в фоне, Vite на переднем плане.',
|
.replace('# Поднимает демо-панель целиком: бэкенд в фоне, Vite на переднем плане.',
|
||||||
@@ -208,19 +223,15 @@ const dev = fs.readFileSync(path.join(ROOT, 'scripts/demo.sh'), 'utf8')
|
|||||||
fs.writeFileSync(path.join(target, 'scripts/dev.sh'), dev, { mode: 0o755 });
|
fs.writeFileSync(path.join(target, 'scripts/dev.sh'), dev, { mode: 0o755 });
|
||||||
|
|
||||||
// ─── ядро ───────────────────────────────────────────────────────────────────
|
// ─── ядро ───────────────────────────────────────────────────────────────────
|
||||||
// Копия ядра кладётся сразу: в панели она закоммиченный исходник, и панель
|
// Ядро сюда не копируется: его приносит `git subtree add` — командами ниже.
|
||||||
// должна собираться с первого коммита, а не после того, как кто-то вспомнит
|
// Копия и поддерево не уживаются, поэтому update.sh в панель не едет вовсе.
|
||||||
// про `npm run update`. Путь к ядру известен — это мы сами.
|
fs.rmSync(path.join(target, 'frontend/scripts/update.sh'), { force: true });
|
||||||
try {
|
|
||||||
execFileSync('sh', [path.join(target, 'frontend/scripts/update.sh')],
|
|
||||||
{ env: { ...process.env, MA7ENGINE: ROOT }, stdio: 'pipe' });
|
|
||||||
console.log('new-panel: ядро скопировано в frontend/src/core и backend/core');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('new-panel: не удалось разложить ядро — запусти `npm run update` во фронте\n' +
|
|
||||||
String(e.stderr ?? e.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── что дальше ─────────────────────────────────────────────────────────────
|
// ─── что дальше ─────────────────────────────────────────────────────────────
|
||||||
|
// Панель — надстройка: она обычно лежит подкаталогом продуктового репозитория,
|
||||||
|
// и prefix для subtree считается от его корня. Угадать корень отсюда нельзя,
|
||||||
|
// поэтому в подсказке путь даётся относительно каталога панели.
|
||||||
|
const prefix = `${path.basename(target)}/`;
|
||||||
console.log(`
|
console.log(`
|
||||||
new-panel: панель ${display} создана в ${target}
|
new-panel: панель ${display} создана в ${target}
|
||||||
|
|
||||||
@@ -232,5 +243,12 @@ new-panel: панель ${display} создана в ${target}
|
|||||||
Пароль и секрет обязательны — без них ядро не даст процессу подняться:
|
Пароль и секрет обязательны — без них ядро не даст процессу подняться:
|
||||||
ADMIN_PASS=... ADMIN_JWT_SECRET=...
|
ADMIN_PASS=... ADMIN_JWT_SECRET=...
|
||||||
|
|
||||||
|
Ядро приносит git subtree — из каталога панели в её репозитории:
|
||||||
|
git subtree add --prefix=${prefix}frontend/src/core ${ROOT} dist/browser --squash
|
||||||
|
git subtree add --prefix=${prefix}backend/core ${ROOT} dist/server --squash
|
||||||
|
|
||||||
|
Дальше обновляется тем же git subtree pull. Копировать ядро руками не нужно —
|
||||||
|
и не стоит: поддерево умеет merge и покажет конфликт, а копия молча затрёт.
|
||||||
|
|
||||||
Проверить, что панель собрана по стандарту:
|
Проверить, что панель собрана по стандарту:
|
||||||
cd ${target}/frontend && npm run doctor`);
|
cd ${target}/frontend && npm run doctor`);
|
||||||
|
|||||||
+6
-1
@@ -31,9 +31,14 @@ if [ -n "$(git status --porcelain)" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# `git subtree split -b <ветка>` умеет только создать ветку и отказывается
|
||||||
|
# двигать существующую, если новый коммит ей не потомок, — а после любого
|
||||||
|
# reset/rebase в ядре это ровно так. Поэтому split считает коммит, а ветка
|
||||||
|
# переставляется принудительно: ветки производные, своей истории у них нет.
|
||||||
split() { # split <каталог> <ветка>
|
split() { # split <каталог> <ветка>
|
||||||
printf 'release: %s → %s\n' "$1" "$2"
|
printf 'release: %s → %s\n' "$1" "$2"
|
||||||
git subtree split --prefix="$1" -b "$2" -q
|
rev=$(git subtree split --prefix="$1" -q)
|
||||||
|
git branch -f "$2" "$rev"
|
||||||
printf ' %s\n' "$(git log -1 --format='%h %s' "$2")"
|
printf ' %s\n' "$(git log -1 --format='%h %s' "$2")"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user