arm-ka-docs/scripts/sync-web.sh
Andrey Epifancev e8dbcff83f feat(scripts): sync-web.sh — синхронизация arm-core/web с arm-web и проверка отставания в пайплайне переноса (#94)
Скрипт делает одной командой то, что делалось вручную в arm-core #94 и #100:
точка синхронизации по хэшу дерева src (без хранения состояния), worktree по
абсолютному пути, issue и PR в arm-core через tea, перенос набора (всё кроме
служебного arm-web), контроль diff -rq, проверки vitest/tsc/biome/build до
коммита. Режимы --check (0/10/20), --dry-run, --ref, --core-ref.

prepare-contour-snapshot.sh для arm-core перед снимком вызывает sync-web.sh
--check: при отставании спрашивает «перенести как есть?», «n» запускает
синхронизацию и останавливает перенос (код 3) до merge PR и закрытия issue;
без терминала — флаг --web-as-is; расхождение (прямые правки web/) — стоп.
Тест test_sync_web.sh на синтетических репозиториях, цели make test-sync-web
и sync-web, гайд §3 шаг 0, §3.1, §8.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-10 10:45:35 +04:00

219 lines
14 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=(tea --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 --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 -------------------------------------------------------------------------------------------
git -C "$WT" add -A web -- ':!web/node_modules' ':!web/dist'
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 --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 --head $BRANCH}"
echo "исполнителю: проверить перенесённый код против бэкенда (коммиты arm-web выше), затем — merge и «закрывай»."
echo "worktree $WT, симлинк web/node_modules и .bounded остаются до закрытия."