arm-ka-docs/scripts/prepare-contour-snapshot.sh
2026-10-27 17:06:45 +04:00

265 lines
16 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, install и statements-version, заменяет ссылки на внешний 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.
# $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
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/ может нести свои .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, install, statements-version
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
# цели только для разработки вне банка: образ для compose, установка библиотеки arm_statements из соседнего checkout
# и сверка этого checkout с версией в requirements.txt
DROPPED = ("docker-build", "install", "statements-version")
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
# цели DROPPED и комментарий над ними удаляются
if line.split(":", 1)[0] in DROPPED and not line.startswith("\t"):
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:"):
for target in DROPPED:
line = re.sub(rf"\s+{target}(?=\s|$)", "", line)
if line.startswith(('\t@echo "docker-build', '\t@echo "install', '\t@echo "statements-version')):
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
# --- 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 ' ')"