prepare-contour-snapshot.sh урезает web/package.json arm-core по разрешённому списку (devDependencies: @rsbuild/core, @rsbuild/plugin-react, tailwindcss, daisyui; optional: raisa-*; scripts: build, start, preview), сокращает web-check до npm install + npm run build, пишет убранное в SNAPSHOT.md и сам собирает урезанный web/ во временной копии: не собралось — снимка нет. Гайд: раздел 3.2 и правило «web/package.json из контура обратно не переносить». Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
303 lines
19 KiB
Bash
Executable File
303 lines
19 KiB
Bash
Executable File
#!/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 …).
|
||
# $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 [--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
|
||
# 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 "$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
|
||
|
||
# Сборка урезанного 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 ' ')"
|