arm-ka-docs/scripts/sync-web.sh
Andrey Epifancev 4475d018cb fix(scripts): sync-web — не исключать игнорируемый web/dist в git add (#110)
git 2.50 на явно названный в pathspec игнорируемый путь (':!web/dist')
печатает «paths are ignored» и возвращает 1 — sync-web останавливался
после проверок, до коммита и PR (issue arm-core #115 создана, PR не было).
web/dist и так игнорируется .gitignore arm-core; исключение симлинка
node_modules оставлено (.gitignore его не ловит).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:47:13 +04:00

222 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Синхронизация встроенного фронта arm-core/web с отдельным репозиторием фронта RSHB/arm-web (ветка test).
# Делает одной командой то, что делалось вручную в arm-core #94 и #100: worktree, issue, перенос набора файлов,
# проверки фронта, коммит, PR. Гайд: dev/contour-transfer-guide.md §3.1.
#
# Точка синхронизации не хранится: это коммит arm-web@<ref>, у которого хэш дерева `src` равен хэшу `web/src`
# в arm-core@<core-ref>. Если такого коммита нет — arm-core/web правили напрямую, мимо arm-web; перенос затёр бы
# эту работу, поэтому скрипт останавливается (код 20).
#
# Набор синхронизации — всё дерево arm-web, кроме служебного (контекст агента, спеки UI, Makefile и скрипты
# демо-стенда): CLAUDE.md, .claude/, openspec/, docs/, scripts/, Makefile, ARCHITECTURE.md, .gitignore, package-lock.json.
#
# Запуск из workspace: docs/scripts/sync-web.sh [--check] [--dry-run] [--ref <ref>] [--core-ref <ref>]
# [--arm-web <dir>] [--arm-core <dir>] [--wt-root <dir>]
# --check только сравнить: 0 — синхронно, 10 — arm-core/web отстаёт, 20 — расхождение (правки напрямую)
# --dry-run worktree, перенос и контроль идентичности без issue, PR, push и проверок npm (посмотреть, что перенесётся)
# --ref ветка arm-web, по умолчанию origin/test; --core-ref — база arm-core, по умолчанию origin/main
# WEB_NODE_MODULES — откуда взять node_modules для проверок (по умолчанию <wt-root>/web-proto/arm-web/node_modules);
# если каталога нет — npm install в worktree
# Скрипт пишет только в новый worktree arm-core на ветке фичи; ничего не мержит, не удаляет и не закрывает —
# закрытие после merge идёт обычным чеклистом «закрывай».
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
# Корень workspace — где рядом лежат arm-core и arm-web. Скрипт запускают и из docs/ (корень = ../..), и из worktree
# .wt/<задача>/docs (корень = ../../../..); внешний кандидат проверяется первым — там основные checkout'ы.
WS="$(cd "$HERE/../.." && pwd)"
for cand in "$HERE/../../../.." "$HERE/../.."; do
if [ -e "$cand/arm-core/.git" ] && [ -e "$cand/arm-web/.git" ]; then WS="$(cd "$cand" && pwd)"; break; fi
done
ARM_WEB="$WS/arm-web"
ARM_CORE="$WS/arm-core"
WT_ROOT="$WS/.wt"
REF=origin/test
CORE_REF=origin/main
MODE=sync
CHECK=0
while [ $# -gt 0 ]; do
case "$1" in
--check) CHECK=1 ;; # имеет приоритет над --dry-run независимо от порядка (пайплайн добавляет $SYNC_WEB_ARGS в конец)
--dry-run) MODE=dry ;;
--ref) REF=$2; shift ;;
--core-ref) CORE_REF=$2; shift ;;
--arm-web) ARM_WEB=$2; shift ;;
--arm-core) ARM_CORE=$2; shift ;;
--wt-root) WT_ROOT=$2; shift ;;
-h|--help) sed -n '2,24p' "$0"; exit 0 ;;
*) echo "sync-web: неизвестный аргумент $1" >&2; exit 2 ;;
esac
shift
done
[ "$CHECK" = 0 ] || MODE=check
# Исключения набора синхронизации: pathspec для git diff и имена для diff -rq
EXCLUDE_NAMES=(CLAUDE.md .claude openspec docs scripts Makefile ARCHITECTURE.md .gitignore package-lock.json)
PATHSPEC=(.)
DIFF_EXCLUDES=(--exclude=node_modules --exclude=dist --exclude=.codegraph --exclude=.superpowers --exclude=.bounded)
for n in "${EXCLUDE_NAMES[@]}"; do PATHSPEC+=(":(exclude,top)$n"); DIFF_EXCLUDES+=("--exclude=$n"); done
die() { echo "sync-web: $1" >&2; exit "${2:-1}"; } # die <сообщение> [код]
[ -d "$ARM_WEB/.git" ] || [ -f "$ARM_WEB/.git" ] || die "arm-web не найден: $ARM_WEB (--arm-web <dir>)" 2
[ -d "$ARM_CORE/.git" ] || [ -f "$ARM_CORE/.git" ] || die "arm-core не найден: $ARM_CORE (--arm-core <dir>)" 2
ARM_WEB=$(cd "$ARM_WEB" && pwd); ARM_CORE=$(cd "$ARM_CORE" && pwd)
git -C "$ARM_WEB" fetch -q origin
git -C "$ARM_CORE" fetch -q origin
git -C "$ARM_WEB" rev-parse -q --verify "$REF^{commit}" >/dev/null || die "в arm-web нет $REF" 2
git -C "$ARM_CORE" rev-parse -q --verify "$CORE_REF^{commit}" >/dev/null || die "в arm-core нет $CORE_REF" 2
# --- 1. Точка синхронизации -------------------------------------------------------------------------------------------
CORE_SRC=$(git -C "$ARM_CORE" rev-parse -q --verify "$CORE_REF:web/src") || die "в arm-core@$CORE_REF нет web/src" 2
HEAD_SHA=$(git -C "$ARM_WEB" rev-parse "$REF")
HEAD7=${HEAD_SHA:0:7}
POINT=""
while IFS= read -r c; do
t=$(git -C "$ARM_WEB" rev-parse -q --verify "$c:src" 2>/dev/null || true)
if [ "$t" = "$CORE_SRC" ]; then POINT=$c; break; fi
done < <(git -C "$ARM_WEB" rev-list "$REF")
if [ -z "$POINT" ]; then
echo "sync-web: расхождение — ни у одного коммита arm-web@$REF дерево src не совпадает с arm-core@$CORE_REF:web/src." >&2
echo "arm-core/web правили напрямую, мимо arm-web; автоперенос затёр бы эти правки. Коммиты arm-core по web/:" >&2
git -C "$ARM_CORE" log --format=' %h %s' -10 "$CORE_REF" -- web/ >&2
echo "Разберитесь вручную: перенесите прямые правки в arm-web (или откатите их), затем повторите." >&2
exit 20
fi
if [ "$POINT" = "$HEAD_SHA" ]; then
echo "sync-web: arm-core@$CORE_REF web/ синхронен с arm-web@$REF ($HEAD7) — переносить нечего"
exit 0
fi
COMMITS=$(git -C "$ARM_WEB" log --format='%h %s' --no-merges "$POINT..$REF")
N_COMMITS=$(printf '%s\n' "$COMMITS" | grep -c . || true)
FILES=$(git -C "$ARM_WEB" diff --name-only "$POINT" "$REF" -- "${PATHSPEC[@]}")
N_FILES=$(printf '%s\n' "$FILES" | grep -c . || true)
echo "sync-web: arm-core@$CORE_REF web/ отстаёт от arm-web@$REF ($HEAD7)."
echo "точка синхронизации: arm-web ${POINT:0:7}; непереносённых коммитов: $N_COMMITS, файлов в наборе: $N_FILES"
printf '%s\n' "$COMMITS" | sed 's/^/ /'
[ "$MODE" != check ] || exit 10
# --- 2. Worktree -----------------------------------------------------------------------------------------------------
TMP_BRANCH="tmp/web-sync-$HEAD7"
WT="$WT_ROOT/web-sync-$HEAD7/arm-core"
[ ! -e "$WT" ] || die "worktree уже существует: $WT — завершите или удалите предыдущую синхронизацию" 2
git -C "$ARM_CORE" rev-parse -q --verify "$TMP_BRANCH" >/dev/null && die "ветка $TMP_BRANCH уже есть в arm-core" 2
mkdir -p "$(dirname "$WT")"
git -C "$ARM_CORE" worktree add -q -b "$TMP_BRANCH" "$WT" "$CORE_REF"
touch "$WT/.bounded"
echo "worktree: $WT ($TMP_BRANCH от $CORE_REF)"
# --- 3. Issue и ветка (не в dry-run) ---------------------------------------------------------------------------------
# tea 0.16+ принимает --login/--repo только после подкоманды: tea issues create --login … (arm-ka-docs#110)
TEA_FLAGS=(--login home --repo RSHB/arm-core)
ISSUE=""
BRANCH="$TMP_BRANCH"
if [ "$MODE" = sync ]; then
TITLE="web: синхронизация встроенного фронта с arm-web@${REF#origin/} ($HEAD7)"
BODY="Встроенный фронт \`arm-core/web\` отстаёт от \`RSHB/arm-web@${REF#origin/}\` ($HEAD7); точка синхронизации — arm-web ${POINT:0:7}.
Непереносённые коммиты arm-web ($N_COMMITS):
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
Bounded-задача, выполнена \`docs/scripts/sync-web.sh\`: перенос набора синхронизации ($N_FILES файлов) в \`web/\`,
проверки vitest / tsc / biome / build, PR в main. Ревью перенесённого кода против бэкенда — за исполнителем."
ISSUE_URL=$(tea issues create "${TEA_FLAGS[@]}" --title "$TITLE" --description "$BODY" | grep -o 'https://[^ ]*/issues/[0-9]*' | tail -1)
ISSUE=${ISSUE_URL##*/}
[ -n "$ISSUE" ] || die "не удалось создать issue через tea"
BRANCH="feat/$ISSUE-web-sync-$HEAD7"
git -C "$WT" branch -m "$TMP_BRANCH" "$BRANCH"
echo "issue: $ISSUE_URL; ветка: $BRANCH"
fi
# --- 4. Перенос набора и контроль идентичности ------------------------------------------------------------------------
git -C "$ARM_WEB" diff --binary "$POINT" "$REF" -- "${PATHSPEC[@]}" | git -C "$WT" apply --directory=web
REF_TREE=$(mktemp -d); trap 'rm -rf "$REF_TREE"' EXIT
git -C "$ARM_WEB" archive "$REF" | tar -x -C "$REF_TREE"
if ! diff -rq "${DIFF_EXCLUDES[@]}" "$REF_TREE" "$WT/web" >/dev/null; then
echo "sync-web: после переноса web/ не совпадает с arm-web@$REF:" >&2
diff -rq "${DIFF_EXCLUDES[@]}" "$REF_TREE" "$WT/web" >&2 || true
exit 1
fi
echo "перенесено: $N_FILES файлов; web/ идентичен arm-web@$REF по набору синхронизации"
if [ "$MODE" = dry ]; then
echo "dry-run: issue, проверки, коммит и PR не выполнялись. Посмотреть: git -C $WT status; убрать:"
echo " git -C $ARM_CORE worktree remove --force $WT && git -C $ARM_CORE branch -D $TMP_BRANCH"
exit 0
fi
# --- 5. Проверки фронта ---------------------------------------------------------------------------------------------
NM=${WEB_NODE_MODULES:-$WT_ROOT/web-proto/arm-web/node_modules}
if [ -d "$NM" ]; then
ln -s "$NM" "$WT/web/node_modules"
echo "node_modules: симлинк на $NM"
else
echo "node_modules: $NM нет — npm install в worktree"
(cd "$WT/web" && npm install --no-audit --no-fund >/dev/null)
fi
run_check() { # имя, команда... — печатает хвост вывода при ошибке и останавливает скрипт
local name=$1; shift
local log; log=$(mktemp)
if (cd "$WT/web" && "$@") >"$log" 2>&1; then rm -f "$log"; return 0; fi
echo "sync-web: проверка «$name» не прошла; worktree оставлен для разбора: $WT" >&2
tail -40 "$log" >&2; rm -f "$log"; exit 1
}
run_check vitest npx vitest run
TESTS=$(cd "$WT/web" && npx vitest run 2>&1 | grep -E '^\s*Tests\s' | sed 's/^ *//' || true)
run_check tsc npx tsc --noEmit
biome_warnings() { (cd "$WT/web" && npx biome check "$1" 2>&1 | grep -oE 'Found [0-9]+ warning' | grep -oE '[0-9]+') || echo 0; }
W_MAIN=$(biome_warnings "$ARM_CORE/web/src")
W_NEW=$(biome_warnings src)
[ "$W_NEW" -le "$W_MAIN" ] || { echo "sync-web: biome — предупреждений стало больше: $W_NEW против $W_MAIN на $CORE_REF; worktree: $WT" >&2; exit 1; }
run_check build npm run build
echo "проверки: $TESTS; tsc чисто; biome $W_NEW предупреждений (на $CORE_REF — $W_MAIN); build ок"
# --- 6. Коммит, push, PR -------------------------------------------------------------------------------------------
# web/dist игнорируется .gitignore arm-core; исключать его pathspec'ом нельзя — git 2.50 на явно названный
# игнорируемый путь отвечает «paths are ignored» и кодом 1 (arm-ka-docs#110). Симлинк node_modules .gitignore не ловит.
git -C "$WT" add -A web -- ':!web/node_modules'
git -C "$WT" -c commit.gpgsign=false commit -q -F - <<EOF
fix(web): синхронизация встроенного фронта с arm-web@${REF#origin/} ($HEAD7) (#$ISSUE)
Перенесён диапазон arm-web ${POINT:0:7}..$HEAD7 ($N_COMMITS коммитов, $N_FILES файлов набора
синхронизации) скриптом docs/scripts/sync-web.sh. После переноса web/ и
arm-web@${REF#origin/} идентичны по набору. Коммиты arm-web:
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
Co-Authored-By: sync-web.sh <noreply@aepif.ru>
EOF
COMMIT=$(git -C "$WT" rev-parse --short=7 HEAD)
git -C "$WT" push -q -u origin "$BRANCH"
PR_BODY="Закрывает #$ISSUE.
Встроенный фронт \`arm-core/web\` отставал от \`RSHB/arm-web@${REF#origin/}\` ($HEAD7). Диапазон arm-web ${POINT:0:7}..$HEAD7
перенесён скриптом \`docs/scripts/sync-web.sh\` без правок; после переноса \`web/\` и arm-web идентичны по набору синхронизации.
Коммиты arm-web ($N_COMMITS):
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
Файлы ($N_FILES):
$(printf '%s\n' "$FILES" | sed 's/^/- web\//')
Проверки в worktree: $TESTS; \`tsc --noEmit\` чисто; \`biome check src\` — $W_NEW предупреждений (на ${CORE_REF#origin/} — $W_MAIN); \`rsbuild build\` собирается.
## Rulings I made
- R-1 — служебные файлы arm-web (CLAUDE.md, .claude/, openspec/, docs/, scripts/, Makefile, ARCHITECTURE.md, .gitignore, lockfile) не переносятся: UI-спеки и демо-стенд живут в arm-web.
- R-2 — bounded-задача без OpenSpec change: перенос готового, проревьюенного в arm-web кода.
Ревью перенесённого кода против бэкенда — за исполнителем, находки с маркерами в комментариях к PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
PR_URL=$(tea pulls create "${TEA_FLAGS[@]}" --head "$BRANCH" --base "${CORE_REF#origin/}" --title "fix(web): синхронизация встроенного фронта с arm-web@${REF#origin/} ($HEAD7) (#$ISSUE)" \
--description "$PR_BODY" | grep -o 'https://[^ ]*/pulls/[0-9]*' | tail -1)
echo
echo "готово: коммит $COMMIT, ветка $BRANCH"
echo "issue: $ISSUE_URL"
echo "PR: ${PR_URL:-создать вручную: tea pulls create ${TEA_FLAGS[*]} --head $BRANCH --base ${CORE_REF#origin/}}"
echo "исполнителю: проверить перенесённый код против бэкенда (коммиты arm-web выше), затем — merge и «закрывай»."
echo "worktree $WT, симлинк web/node_modules и .bounded остаются до закрытия."