arm-ka-docs/scripts/prepare-contour-snapshot.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

217 lines
13 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
# Снимок сервисного репозитория АРМ КА для переноса в контур банка (dev/contour-transfer-guide.md).
#
# Что делает: берёт дерево коммита (`git archive`, без истории), вырезает то, чего в контуре нет или что ему не нужно
# (AI-инструментарий, планы, Dockerfile для локальной сборки), переводит `make venv` с uv на `python -m venv` + pip через
# Nexus (как `setup/setup.sh` шаблона), убирает цель docker-build, заменяет ссылки на внешний Gitea, пишет SNAPSHOT.md с
# провенансом и проверяет результат. Репозиторий-источник не меняется.
#
# Запуск из workspace: docs/scripts/prepare-contour-snapshot.sh [--web-as-is] <каталог репозитория> <каталог вывода> [ref]
# ref по умолчанию — origin/main (снимок всегда с сервера, не с рабочего дерева).
# Пример: docs/scripts/prepare-contour-snapshot.sh tabular-parser /tmp/contour
# Результат: <каталог вывода>/<имя репозитория>/ — готовое дерево для rsync в клон репозитория контура.
#
# Для arm-core перед снимком проверяется, не отстаёт ли встроенный фронт web/ от RSHB/arm-web (sync-web.sh --check).
# Если отстаёт — вопрос «перенести как есть?»: «y» — снимок с отставанием (оно записывается в SNAPSHOT.md); «n» —
# запускается синхронизация (sync-web.sh: worktree, issue, PR) и перенос останавливается (код 3) до merge PR и закрытия
# issue. Без терминала вопрос не задаётся: нужен флаг --web-as-is, иначе код 3. Расхождение (прямые правки web/ в arm-core) —
# всегда стоп. arm-web ищется рядом с репозиторием (../arm-web) или в $ARM_WEB; если его нет — предупреждение, снимок делается.
# $SYNC_WEB_ARGS — дополнительные аргументы sync-web.sh (для теста: --dry-run --wt-root …).
set -euo pipefail
usage() { echo "usage: $0 [--web-as-is] <repo-dir> <out-dir> [ref]" >&2; exit 2; }
WEB_AS_IS=0
if [ "${1:-}" = "--web-as-is" ]; then WEB_AS_IS=1; shift; fi
[ $# -ge 2 ] || usage
REPO=$(cd "$1" && pwd)
mkdir -p "$2"
OUT=$(cd "$2" && pwd)
REF=${3:-origin/main}
NAME=$(basename "$REPO")
DEST="$OUT/$NAME"
case "$NAME" in
arm-core|tabular-parser|scan-gateway) ;;
*) echo "ошибка: $NAME — не сервисный репозиторий (ожидаются arm-core, tabular-parser, scan-gateway); docs и fixtures в контур не переносятся" >&2; exit 2 ;;
esac
if [ -e "$DEST" ]; then
echo "ошибка: $DEST уже существует — удалите или укажите другой каталог вывода" >&2
exit 2
fi
# --- 0. arm-core: встроенный фронт web/ не должен отставать от arm-web -----------------------------------------------
WEB_LAG=""
if [ "$NAME" = arm-core ]; then
SYNC_WEB="$(cd "$(dirname "$0")" && pwd)/sync-web.sh"
ARM_WEB_DIR=${ARM_WEB:-$(dirname "$REPO")/arm-web}
# shellcheck disable=SC2206
SYNC_ARGS=(--arm-core "$REPO" --arm-web "$ARM_WEB_DIR" --core-ref "$REF" ${SYNC_WEB_ARGS:-})
if [ ! -e "$ARM_WEB_DIR/.git" ]; then
echo "предупреждение: arm-web не найден ($ARM_WEB_DIR) — отставание web/ от arm-web не проверено" >&2
else
rc=0; "$SYNC_WEB" --check "${SYNC_ARGS[@]}" || rc=$?
case "$rc" in
0) ;;
10)
if [ "$WEB_AS_IS" = 1 ]; then
answer=y
elif [ -t 0 ] || [ -p /dev/stdin ] || [ -f /dev/stdin ]; then
read -r -p "web/ в arm-core отстаёт от arm-web. Перенести в контур как есть? [y/N] " answer || answer=n
else
answer=n
echo "нет терминала и нет --web-as-is: перенос как есть не подтверждён" >&2
fi
case "$answer" in
y|Y|д|Д) WEB_LAG="отстаёт от arm-web (перенесён как есть по решению оператора, см. sync-web.sh --check)" ;;
*)
echo "запускается синхронизация arm-core/web с arm-web (sync-web.sh)…" >&2
"$SYNC_WEB" "${SYNC_ARGS[@]}" || true
echo "перенос в контур остановлен до полного завершения синхронизации: merge PR и закрытие issue (чеклист «закрывай»), затем повторите снимок." >&2
exit 3 ;;
esac ;;
20) echo "ошибка: web/ в arm-core расходится с arm-web (прямые правки) — снимок не делается, разберитесь вручную" >&2; exit 1 ;;
*) echo "ошибка: sync-web.sh --check завершился с кодом $rc" >&2; exit 1 ;;
esac
fi
fi
git -C "$REPO" fetch -q origin
HASH=$(git -C "$REPO" rev-parse "$REF")
DATE=$(git -C "$REPO" show -s --format=%cs "$REF")
SUBJECT=$(git -C "$REPO" show -s --format=%s "$REF")
mkdir -p "$DEST"
git -C "$REPO" archive "$REF" | tar -x -C "$DEST"
# --- 1. Вырезать то, чего в контуре нет ------------------------------------------------------------------------
# .claude/, CLAUDE.md, openspec/config.yaml — инструкции и контекст AI-агента (config.yaml: блоки context/rules с путями
# ../docs и ../fixtures; openspec CLI в контуре нет, specs/ и design.md читаются без него); docs/superpowers/ — планы и
# ledger (в них локальные пути); Dockerfile/.dockerignore — локальная сборка образа, платформа собирает образ своим CI.
# openspec/specs/ и openspec/changes/archive/ остаются: это спецификация проекта и решения (design.md).
# Артефакты агента вырезаются на любой глубине: фронт перенесён в arm-core/web/ из arm-web со своими .claude/, CLAUDE.md,
# openspec/config.yaml, docs/superpowers/ и .codegraph/. Dockerfile/.dockerignore — только в корне (образ сервиса).
rm -rf "$DEST/Dockerfile" "$DEST/.dockerignore"
find "$DEST" \( \( -type d \( -name .claude -o -name .codegraph -o -path '*/docs/superpowers' \) \) \
-o \( -type f \( -name CLAUDE.md -o -path '*/openspec/config.yaml' \) \) \) -prune -exec rm -rf {} +
find "$DEST" -type d -name docs -empty -delete
# --- 2. Makefile: venv через python -m venv + pip с Nexus, без uv; без docker-build --------------------------------
python3 -I - "$DEST/Makefile" <<'PY'
import re
import sys
from pathlib import Path
path = Path(sys.argv[1])
lines = path.read_text(encoding="utf-8").splitlines()
out: list[str] = []
i = 0
venv_patched = False
while i < len(lines):
line = lines[i]
# рецепт venv: все строки с uv заменяются одним рецептом на pip
if line.startswith("venv:"):
comment_start = len(out)
while comment_start > 0 and out[comment_start - 1].startswith("#"):
comment_start -= 1
del out[comment_start:]
out.append("# Контур банка: Python 3.11 платформы, pip через Nexus (setup/pip.conf), как в setup/setup.sh; dev-зависимости")
out.append("# тянут requirements.txt. PYTHON переопределяется: make venv PYTHON=/usr/bin/python3.11")
out.append("PYTHON ?= python3.11")
out.append("venv:")
out.append("\tcd api && $(PYTHON) -m venv .venv")
out.append("\tcp setup/pip.conf api/.venv/pip.conf")
out.append("\tcd api && .venv/bin/pip install -r requirements-dev.txt")
i += 1
while i < len(lines) and lines[i].startswith("\t"):
i += 1
venv_patched = True
continue
# цель docker-build и комментарий над ней удаляются
if line.startswith("docker-build:"):
comment_start = len(out)
while comment_start > 0 and out[comment_start - 1].startswith("#"):
comment_start -= 1
del out[comment_start:]
i += 1
while i < len(lines) and lines[i].startswith("\t"):
i += 1
if i < len(lines) and lines[i] == "" and out and out[-1] == "":
i += 1
continue
if line.startswith("check-full:"):
line = re.sub(r"\s+docker-build\b", "", line)
if line.startswith("\t$(MAKE) docker-build"):
i += 1
continue
if line.startswith(".PHONY:"):
line = re.sub(r"\s+docker-build\b", "", line)
if line.startswith('\t@echo "docker-build'):
i += 1
continue
line = line.replace(" + docker-build", "")
out.append(line)
i += 1
if not venv_patched:
sys.exit("Makefile: цель venv не найдена — скрипт устарел относительно репозитория")
path.write_text("\n".join(out) + "\n", encoding="utf-8")
PY
# --- 3. Ссылки на внешний Gitea → имя репозитория и номер ---------------------------------------------------------
# В openspec-архивах и README встречаются ссылки вида https://git.aepif.ru/RSHB/<repo>/issues/5 и pulls/7.
while IFS= read -r file; do
[ -n "$file" ] || continue
python3 -I - "$file" <<'PY'
import re
import sys
from pathlib import Path
path = Path(sys.argv[1])
text = path.read_text(encoding="utf-8")
text = re.sub(r"https?://git\.aepif\.ru/(RSHB/[\w.-]+)/(issues|pulls)/(\d+)", r"\1#\3", text)
text = re.sub(r"https?://git\.aepif\.ru/?", "внешний Gitea проекта", text)
path.write_text(text, encoding="utf-8")
PY
done < <(grep -rIl "git.aepif.ru" "$DEST" 2>/dev/null || true)
# --- 4. Провенанс ---------------------------------------------------------------------------------------------------
cat > "$DEST/SNAPSHOT.md" <<EOF
# Снимок $NAME для контура
Источник: внешний репозиторий проекта \`RSHB/$NAME\`, коммит \`$HASH\` ($DATE, «${SUBJECT}»).
Снимок без истории; подготовлен \`prepare-contour-snapshot.sh\` ($(date +%Y-%m-%d)).
Чего здесь нет по сравнению с источником: \`.claude/\`, \`.codegraph/\`, \`CLAUDE.md\`, \`openspec/config.yaml\`, \`docs/superpowers/\`
на любой глубине, в т.ч. в \`web/\` (инструментарий, контекст агента и планы разработки снаружи), \`Dockerfile\`/\`.dockerignore\`
в корне (образ собирает платформа). Соседних репозиториев \`docs\` и
\`fixtures\` в контуре нет: тесты идентичности контракта и тесты с маркером \`fixtures\` пропускаются — это ожидаемо.
Копия контракта — \`api/contracts/\`, спецификация — \`openspec/specs/\`, решения — \`openspec/changes/archive/*/design.md\`.
Разработка ведётся снаружи; правки в контуре возвращаются в источник вручную (см. dev/contour-transfer-guide.md в docs).
EOF
if [ -n "$WEB_LAG" ]; then
printf '\nВстроенный фронт \`web/\` на момент снимка %s.\n' "$WEB_LAG" >> "$DEST/SNAPSHOT.md"
fi
# --- 5. Проверки ----------------------------------------------------------------------------------------------------
fail=0
for p in Dockerfile .dockerignore; do
if [ -e "$DEST/$p" ]; then echo "ошибка: в снимке остался $p" >&2; fail=1; fi
done
left=$(cd "$DEST" && find . \( -name .claude -o -name .codegraph -o -name CLAUDE.md -o -path '*/openspec/config.yaml' \
-o -path '*/docs/superpowers' \) -print)
if [ -n "$left" ]; then echo "ошибка: в снимке остались артефакты агента:" >&2; echo "$left" >&2; fail=1; fi
if grep -rIn "/Users/\|/home/[a-z]" "$DEST" >/dev/null 2>&1; then
echo "ошибка: в снимке есть локальные пути:" >&2; grep -rIn "/Users/\|/home/[a-z]" "$DEST" >&2; fail=1
fi
if grep -rIn "git.aepif.ru\|uv venv\|uv pip" "$DEST" >/dev/null 2>&1; then
echo "ошибка: остались ссылки на внешний Gitea или uv:" >&2; grep -rIn "git.aepif.ru\|uv venv\|uv pip" "$DEST" >&2; fail=1
fi
for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/requirements.txt api/.env.test api/contracts; do
if [ ! -e "$DEST/$p" ]; then echo "ошибка: платформенный файл $p отсутствует" >&2; fail=1; fi
done
[ "$fail" = 0 ] || exit 1
echo "снимок готов: $DEST"
echo "источник: $NAME@${HASH:0:7} ($DATE); файлов: $(find "$DEST" -type f | wc -l | tr -d ' ')"