Скрипт делает одной командой то, что делалось вручную в 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>
219 lines
14 KiB
Bash
Executable File
219 lines
14 KiB
Bash
Executable File
#!/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 остаются до закрытия."
|