arm-ka-docs/scripts/prepare-contour-snapshot.sh

299 lines
19 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 <каталог репозитория> <каталог вывода> [ref]
# ref по умолчанию — origin/main (снимок всегда с сервера, не с рабочего дерева).
# Пример: docs/scripts/prepare-contour-snapshot.sh tabular-parser /tmp/contour
# Результат: <каталог вывода>/<имя репозитория>/ — готовое дерево для rsync в клон репозитория контура.
#
# Флаг --web-as-is остался от синхронизации с arm-web (до 21.10.2026) и игнорируется: фронт ведётся в arm-core/web.
# Для каждого сервиса, где в $REF есть api/arm_statements, проверяется свежесть копии пакета (sync-statements.sh --check
# по дереву $REF; Р-42): отставание — предупреждение и абзац в SNAPSHOT.md, прямая правка копии — стоп (код 1), нет
# arm-statements или Python — предупреждение. Нет api/arm_statements — проверка пропускается. $SYNC_STATEMENTS_ARGS —
# дополнительные аргументы sync-statements.sh (для теста: --source … --docs …).
# $SNAPSHOT_SKIP_WEB_BUILD=1 — не проверять сборку web/ (для тестов без сети); по умолчанию проверка включена.
#
# Встроенный фронт arm-core в контуре только собирается (npm install через Nexus + rsbuild build), а зеркало npm банка не
# содержит свежих версий инструментов разработки. Поэтому в снимке web/package.json урезается по РАЗРЕШЁННОМУ списку
# (решение владельца 16.10.2026); всё остальное — eslint, prettier, biome, typescript, vitest, @types/* — разработка
# и проверки идут снаружи. Список — единственное место, которое правят, если сборке понадобится ещё пакет.
WEB_DEV_ALLOW=(@rsbuild/core @rsbuild/plugin-react tailwindcss daisyui) # devDependencies, нужные сборке
WEB_OPTIONAL_KEEP_PREFIX="raisa-" # optionalDependencies: платформенные пакеты
WEB_SCRIPTS_KEEP=(build start preview) # scripts, работающие без инструментов разработки
set -euo pipefail
usage() { echo "usage: $0 <repo-dir> <out-dir> [ref]" >&2; exit 2; }
if [ "${1:-}" = "--web-as-is" ]; then
echo "предупреждение: --web-as-is больше не нужен (фронт ведётся в arm-core/web) — флаг игнорируется" >&2; 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
# --- 0a. Копия пакета arm_statements в api/arm_statements не должна отставать от arm-statements ---------------------------
# Проверяется дерево $REF хоста (git archive api → временный каталог), не рабочее. Нет api/arm_statements в $REF — пакет в
# хост ещё не подключён, проверка молча пропускается. Коды sync-statements.sh --check: 0 — тихо; 10 — предупреждение в
# stderr и абзац в SNAPSHOT.md; 20 — прямая правка/чужая версия копии, снимок не делается; 2 (нет arm-statements или
# интерпретатора) — предупреждение, снимок делается.
git -C "$REPO" fetch -q origin
STATEMENTS_LAG=""
if git -C "$REPO" rev-parse -q --verify "$REF:api/arm_statements" >/dev/null 2>&1; then
SYNC_STATEMENTS="$(cd "$(dirname "$0")" && pwd)/sync-statements.sh"
STM_TMP=$(mktemp -d)
trap 'rm -rf "$STM_TMP"' EXIT
git -C "$REPO" archive "$REF" api | tar -x -C "$STM_TMP"
rc=0
# shellcheck disable=SC2086
stm_out=$("$SYNC_STATEMENTS" --check --host "$NAME" --host-dir "$STM_TMP" ${SYNC_STATEMENTS_ARGS:-} 2>&1) || rc=$?
rm -rf "$STM_TMP"
case "$rc" in
0) ;;
10)
echo "предупреждение: копия arm_statements в $NAME отстаёт от arm-statements или требует внимания:" >&2
echo "$stm_out" >&2
STATEMENTS_LAG=$(printf '%s\n' "$stm_out" | grep '^statements-check' | tail -n 1)
[ -n "$STATEMENTS_LAG" ] || STATEMENTS_LAG="sync-statements.sh --check вернул 10" ;;
20) echo "$stm_out" >&2
echo "ошибка: копия arm_statements в $NAME расходится с arm-statements (прямые правки или чужая версия) — снимок не делается, разберитесь вручную" >&2
exit 1 ;;
2) echo "предупреждение: копия arm_statements в $NAME не проверена (sync-statements.sh --check: нет arm-statements рядом или подходящего Python):" >&2
echo "$stm_out" >&2 ;;
*) echo "$stm_out" >&2
echo "ошибка: sync-statements.sh --check завершился с кодом $rc" >&2; exit 1 ;;
esac
fi
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/ может нести свои .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
# web-check в контуре: только npm install и npm run build (biome/tsc убраны вместе с пакетами)
if line.startswith("web-check:"):
out.append(line)
i += 1
while i < len(lines) and lines[i].startswith("\t"):
if "npx " not in lines[i]:
out.append(lines[i])
i += 1
continue
if line.startswith("# Фронт:"):
line = line.replace("biome, типы, ", "")
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
# --- 2a. arm-core: web/package.json только с зависимостями сборки ---------------------------------------------------------
WEB_REMOVED=""
if [ -f "$DEST/web/package.json" ]; then
WEB_REMOVED=$(python3 -I - "$DEST/web/package.json" "$WEB_OPTIONAL_KEEP_PREFIX" "${WEB_DEV_ALLOW[*]}" "${WEB_SCRIPTS_KEEP[*]}" <<'PY'
import json
import sys
from pathlib import Path
path = Path(sys.argv[1])
optional_prefix = sys.argv[2]
dev_allow = sys.argv[3].split()
scripts_keep = sys.argv[4].split()
pkg = json.loads(path.read_text(encoding="utf-8"))
missing = [d for d in dev_allow if d not in pkg.get("devDependencies", {})]
if missing:
sys.exit(f"web/package.json: в devDependencies нет {', '.join(missing)} из разрешённого списка — скрипт устарел относительно репозитория")
dev = pkg.get("devDependencies", {})
opt = pkg.get("optionalDependencies", {})
scripts = pkg.get("scripts", {})
removed_dev = [n for n in dev if n not in dev_allow]
removed_opt = [n for n in opt if not n.startswith(optional_prefix)]
removed_scripts = [n for n in scripts if n not in scripts_keep]
pkg["devDependencies"] = {n: v for n, v in dev.items() if n in dev_allow}
if "optionalDependencies" in pkg:
pkg["optionalDependencies"] = {n: v for n, v in opt.items() if n.startswith(optional_prefix)}
pkg["scripts"] = {n: v for n, v in scripts.items() if n in scripts_keep}
path.write_text(json.dumps(pkg, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print("devDependencies: " + (", ".join(removed_dev) or "—"))
print("optionalDependencies: " + (", ".join(removed_opt) or "—"))
print("scripts: " + (", ".join(removed_scripts) or "—"))
PY
)
fi
# --- 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_REMOVED" ]; then
{
printf '\nВстроенный фронт `web/`: `package.json` урезан до зависимостей сборки (в контуре фронт только собирается: '
printf '`npm install` через Nexus + `rsbuild build`; зеркало npm банка не содержит свежих инструментов разработки).\n'
printf 'Оставлены `dependencies` целиком, devDependencies `%s`, optionalDependencies `%s*`, scripts `%s`.\n' \
"${WEB_DEV_ALLOW[*]}" "$WEB_OPTIONAL_KEEP_PREFIX" "${WEB_SCRIPTS_KEEP[*]}"
printf 'Убрано:\n\n'
printf '%s\n' "$WEB_REMOVED" | sed 's/^/- /'
printf '\nЦель `make web-check` — `npm install` и `npm run build`. `web/package.json` из контура обратно не переносить: он урезан.\n'
} >> "$DEST/SNAPSHOT.md"
fi
if [ -n "$STATEMENTS_LAG" ]; then
printf '\nКопия пакета arm_statements (`api/arm_statements/`) на момент снимка требует внимания: %s\n' "$STATEMENTS_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
# Сборка урезанного web/ собирается в ВРЕМЕННОЙ копии: снимок остаётся без node_modules/dist/package-lock.json, а упавшая
# проверка не оставляет полуготовых файлов. Нужен npm и доступ к реестру (в контуре — Nexus, снаружи — публичный npm;
# raisa-* опциональны и снаружи пропускаются). Падение сборки почти всегда значит: пакет, нужный сборке, не в WEB_DEV_ALLOW.
if [ -f "$DEST/web/package.json" ] && [ "${SNAPSHOT_SKIP_WEB_BUILD:-0}" != 1 ]; then
echo "проверка сборки урезанного web/ (npm install + npm run build)…" >&2
BUILD_TMP=$(mktemp -d)
cp -R "$DEST/web" "$BUILD_TMP/web"
if ! { (cd "$BUILD_TMP/web" && npm install --no-audit --no-fund && npm run build) >"$BUILD_TMP/build.log" 2>&1; }; then
tail -n 30 "$BUILD_TMP/build.log" >&2
rm -rf "$BUILD_TMP" "$DEST"
echo "ошибка: сборка урезанного web/ не прошла — пакет, нужный сборке, не в разрешённом списке (WEB_DEV_ALLOW в $(basename "$0"))." >&2
echo "снимок не выдан." >&2
exit 1
fi
rm -rf "$BUILD_TMP"
fi
[ "$fail" = 0 ] || exit 1
echo "снимок готов: $DEST"
echo "источник: $NAME@${HASH:0:7} ($DATE); файлов: $(find "$DEST" -type f | wc -l | tr -d ' ')"