fix(scripts): sync-statements.sh --in-place не пишет в основной checkout и main, --check всегда печатает итог, правки доков (#120)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-18 19:31:22 +04:00
parent 7900191401
commit fdd25a6000
6 changed files with 61 additions and 15 deletions

View File

@ -38,7 +38,7 @@ test-sync-statements:
sync-web: sync-web:
scripts/sync-web.sh $(SYNC_ARGS) scripts/sync-web.sh $(SYNC_ARGS)
# Копия пакета arm_statements в хост одной командой (worktree, issue, PR); SYNC_ARGS="--host arm-core [--check|--dry-run|--in-place]" # Копия пакета arm_statements в хост одной командой (worktree, issue, PR); SYNC_ARGS="--host arm-core [--check|--dry-run]"
sync-statements: sync-statements:
STATEMENTS_PY=$(PY) scripts/sync-statements.sh $(SYNC_ARGS) STATEMENTS_PY=$(PY) scripts/sync-statements.sh $(SYNC_ARGS)

View File

@ -135,7 +135,7 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу
| `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=<id>`) | | `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=<id>`) |
| `make check` | все проверки выше и `test-snapshot`, `test-sync-web`, `test-sync-statements` | | `make check` | все проверки выше и `test-snapshot`, `test-sync-web`, `test-sync-statements` |
| `make test-sync-statements` | сценарии `sync-statements.sh` на синтетических репозиториях: коды `--check`, `--in-place`, `--dry-run`, отказ при прямой правке | | `make test-sync-statements` | сценарии `sync-statements.sh` на синтетических репозиториях: коды `--check`, `--in-place`, `--dry-run`, отказ при прямой правке |
| `make sync-statements` | копия пакета `arm_statements` в хост одной командой (worktree, issue, PR); `SYNC_ARGS="--host arm-core [--check\|--dry-run\|--in-place]"` | | `make sync-statements` | копия пакета `arm_statements` в хост одной командой (worktree, issue, PR); `SYNC_ARGS="--host arm-core [--check\|--dry-run]"` |
| `make statements-check` | обзор свежести копий по трём хостам рядом (`arm-core`, `tabular-parser`, `scan-gateway`): строка итога на хост; в `check` не входит | | `make statements-check` | обзор свежести копий по трём хостам рядом (`arm-core`, `tabular-parser`, `scan-gateway`): строка итога на хост; в `check` не входит |
Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate <id> --strict`. Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate <id> --strict`.

View File

@ -92,8 +92,8 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour
`.env.test`, `contracts/`). Любая проблема — код возврата 1 и список. `.env.test`, `contracts/`). Любая проблема — код возврата 1 и список.
Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`); проверка Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`); проверка
отставания `web/` и остановка переноса — в `make test-sync-web`; проверка копии `arm_statements` (пропуск, отставание, правка) отставания `web/` и остановка переноса — в `make test-sync-web`, а проверка копии `arm_statements` (пропуск, отставание, правка)
— там же, в `make test-snapshot` (нужен Python ≥ 3.11 с PyYAML, `make venv`). — в `make test-snapshot` (нужен Python ≥ 3.11 с PyYAML, `make venv`).
### 3.1. Синхронизация `arm-core/web` с `arm-web`: `scripts/sync-web.sh` ### 3.1. Синхронизация `arm-core/web` с `arm-web`: `scripts/sync-web.sh`

View File

@ -99,6 +99,7 @@ STATEMENTS_LAG=""
if git -C "$REPO" rev-parse -q --verify "$REF:api/arm_statements" >/dev/null 2>&1; then 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" SYNC_STATEMENTS="$(cd "$(dirname "$0")" && pwd)/sync-statements.sh"
STM_TMP=$(mktemp -d) STM_TMP=$(mktemp -d)
trap 'rm -rf "$STM_TMP"' EXIT
git -C "$REPO" archive "$REF" api | tar -x -C "$STM_TMP" git -C "$REPO" archive "$REF" api | tar -x -C "$STM_TMP"
rc=0 rc=0
# shellcheck disable=SC2086 # shellcheck disable=SC2086

View File

@ -23,7 +23,11 @@
# --source каталог arm-statements, по умолчанию <workspace>/arm-statements; --docs — каталог docs # --source каталог arm-statements, по умолчанию <workspace>/arm-statements; --docs — каталог docs
# STATEMENTS_PY — интерпретатор помощника (Python >= 3.11 с PyYAML); иначе docs/.venv, хост api/.venv, python3 # STATEMENTS_PY — интерпретатор помощника (Python >= 3.11 с PyYAML); иначе docs/.venv, хост api/.venv, python3
# --check важнее --dry-run и --in-place при любом порядке аргументов. # --check важнее --dry-run и --in-place при любом порядке аргументов.
# Итог --check: «statements-check <хост>: <код> — …»; код возврата равен коду в строке; 2 — неверные аргументы или окружение. # Итог --check: «statements-check <хост>: <код> — …»; код возврата равен коду в строке.
# Коды выхода: 0 — готово/синхронно (--in-place: итог проверки 0 или 10, он же код); 1 — сбой раскладки, защит, проверок хоста,
# коммита или push; 2 — неверные аргументы, окружение, нет копии на --host-ref, worktree уже есть; 10 — предупреждение
# (только --check и --in-place); 20 — расхождение или отказ (правка копии, чужая версия, нет зависимости, код вне перечня).
# --in-place требует --host-dir (клон/worktree хоста на ветке не main); основной checkout и ветку main он отвергает.
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
@ -61,14 +65,16 @@ while [ $# -gt 0 ]; do
--docs) [ $# -ge 2 ] || die "--docs требует значение" 2; DOCS=$2; shift ;; --docs) [ $# -ge 2 ] || die "--docs требует значение" 2; DOCS=$2; shift ;;
--core-dir) [ $# -ge 2 ] || die "--core-dir требует значение" 2; CORE_DIR=$2; shift ;; --core-dir) [ $# -ge 2 ] || die "--core-dir требует значение" 2; CORE_DIR=$2; shift ;;
--wt-root) [ $# -ge 2 ] || die "--wt-root требует значение" 2; WT_ROOT=$2; shift ;; --wt-root) [ $# -ge 2 ] || die "--wt-root требует значение" 2; WT_ROOT=$2; shift ;;
-h|--help) sed -n "2,26p" "$0"; exit 0 ;; -h|--help) sed -n "2,30p" "$0"; exit 0 ;;
*) die "неизвестный аргумент $1" 2 ;; *) die "неизвестный аргумент $1" 2 ;;
esac esac
shift shift
done done
if [ "$CHECK" = 1 ]; then MODE=check if [ "$CHECK" = 1 ]; then MODE=check
elif [ "$IN_PLACE" = 1 ] && [ "$DRY" = 1 ]; then die "--in-place несовместим с --dry-run" 2 elif [ "$IN_PLACE" = 1 ] && [ "$DRY" = 1 ]; then die "--in-place несовместим с --dry-run" 2
elif [ "$IN_PLACE" = 1 ]; then MODE=in-place elif [ "$IN_PLACE" = 1 ]; then
MODE=in-place
[ -n "$HOST_DIR" ] || die "--in-place требует --host-dir <worktree или клон хоста на ветке задачи>; --host один не годится: он указывает на основной checkout" 2
elif [ "$DRY" = 1 ]; then MODE=dry elif [ "$DRY" = 1 ]; then MODE=dry
else MODE=sync; fi else MODE=sync; fi
@ -200,14 +206,22 @@ evaluate() {
fi fi
# 5. зависимости пакета в requirements хоста # 5. зависимости пакета в requirements хоста
git -C "$SRC" show "$version:pyproject.toml" > "$TMPD/pyproject.toml" 2>/dev/null || : > "$TMPD/pyproject.toml" git -C "$SRC" show "$version:pyproject.toml" > "$TMPD/pyproject.toml" 2>/dev/null || : > "$TMPD/pyproject.toml"
helper deps "$TMPD/pyproject.toml" "$reqs" if [ ! -f "$reqs" ]; then
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; elif [ -n "$HELPER_OUT" ]; then echo "$HELPER_OUT"; fi bad "нет requirements хоста (api/requirements.txt): зависимости пакета не проверены"
else
helper deps "$TMPD/pyproject.toml" "$reqs"
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; elif [ -n "$HELPER_OUT" ]; then echo "$HELPER_OUT"; fi
fi
fi fi
# 6. коды словаря имён строк (только у хоста со словарём) # 6. коды словаря имён строк (только у хоста со словарём)
if [ -n "$lines" ]; then if [ -n "$lines" ]; then
helper lines "$lines" "$copy/rules/statement_code_orders.yaml" if [ ! -f "$copy/rules/statement_code_orders.yaml" ]; then
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; fi bad "в копии нет rules/statement_code_orders.yaml: коды словаря имён не проверены"
else
helper lines "$lines" "$copy/rules/statement_code_orders.yaml"
if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; fi
fi
fi fi
# 7. перечень кодов в docs # 7. перечень кодов в docs
if [ ! -f "$DOCS/contracts/statement-code-orders.yaml" ]; then if [ ! -f "$DOCS/contracts/statement-code-orders.yaml" ]; then
@ -280,6 +294,11 @@ lay_copy() {
} }
if [ "$MODE" = in-place ]; then if [ "$MODE" = in-place ]; then
HB=$(git -C "$HOST_DIR" symbolic-ref -q --short HEAD 2>/dev/null || true)
if [ "$HOST_DIR" = "$WS/$HOST_NAME" ]; then
die "--in-place не пишет в основной checkout хоста ($HOST_DIR): создайте worktree задачи и укажите его в --host-dir" 2
fi
case "$HB" in main|master) die "--in-place не пишет в ветку $HB хоста: перейдите на ветку задачи (worktree)" 2 ;; esac
COPY="$HOST_DIR/api/arm_statements" COPY="$HOST_DIR/api/arm_statements"
REQS="$HOST_DIR/api/requirements.txt" REQS="$HOST_DIR/api/requirements.txt"
LINES="" LINES=""
@ -291,6 +310,7 @@ if [ "$MODE" = in-place ]; then
evaluate "$COPY" "$REQS" "" "$HOST_NAME" > "$TMPD/ev.out" evaluate "$COPY" "$REQS" "" "$HOST_NAME" > "$TMPD/ev.out"
if [ "$COPY_BAD" = 1 ]; then if [ "$COPY_BAD" = 1 ]; then
cat "$TMPD/ev.out" cat "$TMPD/ev.out"
echo "если копия «чужая» после squash-слияния ветки arm-statements: удалите api/arm_statements и разложите заново"
echo "statements-check $HOST_NAME: 20 — отказ: копия правлена или чужая, ничего не записано" echo "statements-check $HOST_NAME: 20 — отказ: копия правлена или чужая, ничего не записано"
exit 20 exit 20
fi fi
@ -298,8 +318,11 @@ if [ "$MODE" = in-place ]; then
# 2. зависимости и словарь имён по коммиту --ref # 2. зависимости и словарь имён по коммиту --ref
refuse="" refuse=""
git -C "$SRC" show "$REF_SHA:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml" git -C "$SRC" show "$REF_SHA:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml"
helper deps "$TMPD/ref-pyproject.toml" "$REQS" if [ ! -f "$REQS" ]; then refuse="нет requirements хоста (api/requirements.txt)"
if [ "$HELPER_RC" = 20 ]; then refuse="$HELPER_OUT"; fi else
helper deps "$TMPD/ref-pyproject.toml" "$REQS"
if [ "$HELPER_RC" = 20 ]; then refuse="$HELPER_OUT"; fi
fi
if [ -n "$LINES" ]; then if [ -n "$LINES" ]; then
git -C "$SRC" show "$REF_SHA:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \ git -C "$SRC" show "$REF_SHA:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \
|| : > "$TMPD/ref-orders.yaml" || : > "$TMPD/ref-orders.yaml"
@ -367,8 +390,11 @@ OLD7=${OLD:0:7}
# 3. зависимости и словарь имён против TARGET # 3. зависимости и словарь имён против TARGET
refuse="" refuse=""
git -C "$SRC" show "$TARGET:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml" git -C "$SRC" show "$TARGET:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml"
helper deps "$TMPD/ref-pyproject.toml" "$HT/api/requirements.txt" if [ ! -f "$HT/api/requirements.txt" ]; then refuse="нет requirements хоста (api/requirements.txt)"
if [ "$HELPER_RC" = 20 ]; then refuse=$HELPER_OUT; fi else
helper deps "$TMPD/ref-pyproject.toml" "$HT/api/requirements.txt"
if [ "$HELPER_RC" = 20 ]; then refuse=$HELPER_OUT; fi
fi
if [ -n "$HLINES" ]; then if [ -n "$HLINES" ]; then
git -C "$SRC" show "$TARGET:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \ git -C "$SRC" show "$TARGET:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \
|| : > "$TMPD/ref-orders.yaml" || : > "$TMPD/ref-orders.yaml"

View File

@ -207,6 +207,15 @@ sync 20 "check: код словаря вне перечня" --check --host-dir
check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "check: код вне перечня — 1120 и fsbu4" check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "check: код вне перечня — 1120 и fsbu4"
reset_hosts reset_hosts
rm "$AC/arm_statements/rules/statement_code_orders.yaml"
sync 20 "check: нет statement_code_orders.yaml в копии" --check --host-dir "$WORK/arm-core"
check "$(has 'statements-check arm-core: 20' && has 'rules/statement_code_orders.yaml' && echo 0 || echo 1)" "check: нет перечня в копии — находка и итоговая строка"
reset_hosts
rm "$AC/requirements.txt"
sync 20 "check: нет requirements.txt" --check --host-dir "$WORK/arm-core"
check "$(has 'statements-check arm-core: 20' && has 'requirements' && echo 0 || echo 1)" "check: нет requirements — находка и итоговая строка"
reset_hosts
echo "# новый код" >> "$WORK/docs/contracts/statement-code-orders.yaml" echo "# новый код" >> "$WORK/docs/contracts/statement-code-orders.yaml"
sync 10 "check: перечень в docs новее" --check --host-dir "$WORK/arm-core" sync 10 "check: перечень в docs новее" --check --host-dir "$WORK/arm-core"
check "$(has contracts/statement-code-orders.yaml && has 'statements-check arm-core: 10' && echo 0 || echo 1)" "check: перечень в docs — в выводе файл и итог 10" check "$(has contracts/statement-code-orders.yaml && has 'statements-check arm-core: 10' && echo 0 || echo 1)" "check: перечень в docs — в выводе файл и итог 10"
@ -247,6 +256,16 @@ git -C "$FR" checkout -q -b main
put "$FR/api/requirements.txt" $'PyYAML==6.0.3' put "$FR/api/requirements.txt" $'PyYAML==6.0.3'
commit_push "$FR" "fresh" main commit_push "$FR" "fresh" main
# хосты — на ветку задачи: --in-place отвергает main/master
for h in arm-core tabular-parser scan-gateway; do git -C "$WORK/$h" checkout -q -b work; done
git -C "$FR" checkout -q -b work
sync 2 "in-place: --host без --host-dir" --in-place --host tabular-parser
check "$(has -- '--host-dir' && echo 0 || echo 1)" "in-place: --host без --host-dir — сообщение"
git -C "$WORK/scan-gateway" checkout -q main
sync 2 "in-place: ветка main хоста" --in-place --host-dir "$WORK/scan-gateway"
check "$(has main && [ "$(tr -d '\n' < "$WORK/scan-gateway/api/arm_statements/VERSION")" = "$S1" ] && echo 0 || echo 1)" "in-place: main — копия не тронута"
git -C "$WORK/scan-gateway" checkout -q work
sync 0 "in-place: первичная раскладка" --in-place --host-dir "$FR" --ref origin/main sync 0 "in-place: первичная раскладка" --in-place --host-dir "$FR" --ref origin/main
check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: VERSION = полный хэш S2" check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: VERSION = полный хэш S2"
rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc" rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc"