#!/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@, у которого хэш дерева `src` равен хэшу `web/src` # в arm-core@. Если такого коммита нет — 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 ] [--core-ref ] # [--arm-web ] [--arm-core ] [--wt-root ] # --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 для проверок (по умолчанию /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 )" 2 [ -d "$ARM_CORE/.git" ] || [ -f "$ARM_CORE/.git" ] || die "arm-core не найден: $ARM_CORE (--arm-core )" 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 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 остаются до закрытия."