feat(scripts): sync-statements.sh — worktree хоста, issue, make check и PR; --dry-run; предупреждение «сначала ядро» (#120)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-18 19:18:14 +04:00
parent 97777cbd7a
commit 17c0fd20f9
2 changed files with 223 additions and 3 deletions

View File

@ -13,7 +13,9 @@
# 20 — расхождение (прямая правка копии, чужая версия, нет копии, нет зависимости, код вне перечня)
# --in-place разложить копию в рабочее дерево хоста (без git-операций); 1 — защиты (.gitignore хоста, пути для контура),
# 20 — отказ (правка копии, чужая версия, нет зависимости, код вне перечня)
# --dry-run worktree хоста без issue и PR (режим пока не реализован); вместе с --in-place — код 2
# (без режима) worktree хоста от --host-ref, issue, make check хоста, коммит и PR; 20 — отказ до worktree, 2 — копии нет
# на --host-ref или worktree уже есть, 0 — копия уже на --ref
# --dry-run то же до раскладки включительно, без issue, проверок, коммита и PR (worktree остаётся); с --in-place — код 2
# --host arm-core | tabular-parser | scan-gateway; по умолчанию — по origin хоста (arm-core, arm-tabular-parser,
# arm-scan-gateway), иначе по имени каталога
# --host-dir каталог хоста (корень репозитория)
@ -59,7 +61,7 @@ while [ $# -gt 0 ]; do
--docs) [ $# -ge 2 ] || die "--docs требует значение" 2; DOCS=$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 ;;
-h|--help) sed -n "2,24p" "$0"; exit 0 ;;
-h|--help) sed -n "2,26p" "$0"; exit 0 ;;
*) die "неизвестный аргумент $1" 2 ;;
esac
shift
@ -329,4 +331,179 @@ if [ "$MODE" = in-place ]; then
exit "$EVAL_CODE"
fi
die "режим не реализован" 1
# --- обычный режим и --dry-run: worktree хоста, issue, проверки, PR ------------------------------------------------
TARGET=$REF_SHA
T7=${TARGET:0:7}
case "$HOST_NAME" in
arm-core) GITEA_REPO=RSHB/arm-core ;;
tabular-parser) GITEA_REPO=RSHB/arm-tabular-parser ;;
scan-gateway) GITEA_REPO=RSHB/arm-scan-gateway ;;
esac
# 1. хост
if ! git -C "$HOST_DIR" fetch -q origin 2>/dev/null; then
echo "sync-statements: fetch хоста не удался, работаю с локальным $HOST_REF" >&2
fi
git -C "$HOST_DIR" rev-parse -q --verify "$HOST_REF^{commit}" >/dev/null || die "в хосте нет $HOST_REF" 2
# 2. состояние хоста на --host-ref
git -C "$HOST_DIR" rev-parse -q --verify "$HOST_REF:api/arm_statements" >/dev/null 2>&1 \
|| die "копии нет на $HOST_REF: подключение хоста — вручную, --in-place в worktree задачи подключения (docs/dev/checks.md)" 2
HT="$TMPD/host-tree"; mkdir -p "$HT"
git -C "$HOST_DIR" archive "$HOST_REF" api | tar -x -C "$HT"
HLINES=""
if [ "$HOST_NAME" = arm-core ] && [ -f "$HT/api/rules/statement_lines.yaml" ]; then HLINES="$HT/api/rules/statement_lines.yaml"; fi
evaluate "$HT/api/arm_statements" "$HT/api/requirements.txt" "$HLINES" "$HOST_NAME"
if [ "$EVAL_CODE" = 20 ]; then
echo "statements-check $HOST_NAME: 20 — отказ: копия на $HOST_REF правлена, чужая или несовместима, worktree не создан"
exit 20
fi
HOST_WARNS=$EVAL_WARNS
OLD=$(tr -d ' \t\r\n' < "$HT/api/arm_statements/VERSION")
OLD7=${OLD:0:7}
# 3. зависимости и словарь имён против TARGET
refuse=""
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 [ "$HELPER_RC" = 20 ]; then refuse=$HELPER_OUT; fi
if [ -n "$HLINES" ]; then
git -C "$SRC" show "$TARGET:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \
|| : > "$TMPD/ref-orders.yaml"
helper lines "$HLINES" "$TMPD/ref-orders.yaml"
if [ "$HELPER_RC" = 20 ]; then refuse="${refuse:+$refuse
}$HELPER_OUT"; fi
fi
if [ -n "$refuse" ]; then
echo "$refuse"
echo "statements-check $HOST_NAME: 20 — отказ: $REF несовместим с хостом, worktree не создан"
exit 20
fi
# 4. уже на TARGET
if [ "$OLD" = "$TARGET" ]; then
echo "копия уже на $T7, раскладывать нечего"
[ -z "$HOST_WARNS" ] || echo "предупреждение: $HOST_WARNS"
exit 0
fi
# 5. «сначала ядро»
CORE_WARN=""
if [ "$HOST_NAME" != arm-core ]; then
CORE_VER=""
if [ -d "$CORE_DIR" ]; then
git -C "$CORE_DIR" fetch -q origin 2>/dev/null || true
CORE_VER=$(git -C "$CORE_DIR" show origin/main:api/arm_statements/VERSION 2>/dev/null | tr -d ' \t\r\n' || true)
fi
if [ -z "$CORE_VER" ] || { [ "$CORE_VER" != "$TARGET" ] && ! git -C "$SRC" merge-base --is-ancestor "$TARGET" "$CORE_VER" 2>/dev/null; }; then
CORE_WARN="ядро (arm-core@origin/main) ещё не на $T7: по порядку выкладки сначала ядро"
echo "предупреждение: $CORE_WARN"
fi
fi
# 6. worktree хоста
TMP_BRANCH="tmp/statements-sync-$T7"
WT="$WT_ROOT/statements-sync-$T7/$HOST_NAME"
[ ! -e "$WT" ] || die "worktree уже существует: $WT — завершите или удалите предыдущую синхронизацию" 2
if git -C "$HOST_DIR" rev-parse -q --verify "$TMP_BRANCH" >/dev/null; then
die "ветка $TMP_BRANCH уже существует в $HOST_NAME — завершите или удалите предыдущую синхронизацию" 2
fi
mkdir -p "$(dirname "$WT")"
git -C "$HOST_DIR" worktree add -q -b "$TMP_BRANCH" "$WT" "$HOST_REF"
touch "$WT/.bounded"
echo "worktree: $WT ($TMP_BRANCH от $HOST_REF)"
# 7. issue и ветка (не в dry-run)
# tea 0.16+ принимает --login/--repo только после подкоманды (arm-ka-docs#110)
TEA_FLAGS=(--login home --repo "$GITEA_REPO")
COMMITS=$(git -C "$SRC" log --format='%h %s' --no-merges "$OLD..$TARGET" 2>/dev/null || true)
N_COMMITS=$(printf '%s\n' "$COMMITS" | grep -c . || true)
ISSUE=""; ISSUE_URL=""
BRANCH=$TMP_BRANCH
if [ "$MODE" = sync ]; then
TITLE="arm_statements: копия пакета arm-statements@$T7"
BODY="Копия пакета \`api/arm_statements\` в \`$HOST_NAME\` была на \`$OLD7\`, станет \`$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`).
Коммиты arm-statements \`$OLD7..$T7\` ($N_COMMITS):
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
Bounded-задача, выполнена \`docs/scripts/sync-statements.sh\`: копия раскладывается из \`git archive\`, не правится."
ISSUE_URL=$(tea issues create "${TEA_FLAGS[@]}" --title "$TITLE" --description "$BODY" | grep -o 'https://[^ ]*/issues/[0-9]*' | tail -1)
ISSUE=${ISSUE_URL##*/}
[ -n "$ISSUE" ] || die "не удалось создать issue через tea (worktree оставлен: $WT)"
BRANCH="feat/$ISSUE-statements-sync-$T7"
git -C "$WT" branch -m "$TMP_BRANCH" "$BRANCH"
echo "issue: $ISSUE_URL; ветка: $BRANCH"
fi
# 8. раскладка и проверка результата
lay_copy "$WT" "$TARGET" || die "раскладка выполнена, но копию нельзя коммитить (см. выше); worktree оставлен: $WT" 1
evaluate "$WT/api/arm_statements" "$WT/api/requirements.txt" "$([ -n "$HLINES" ] && echo "$WT/api/rules/statement_lines.yaml")" "$HOST_NAME"
if [ "$EVAL_CODE" = 20 ]; then
echo "statements-check $HOST_NAME: 20 — после раскладки расхождение; worktree оставлен: $WT" >&2
exit 1
fi
SC_LINE=$(summary)
echo "перенесено: api/arm_statements = arm-statements@$T7"
echo "$SC_LINE"
# 9. dry-run
if [ "$MODE" = dry ]; then
echo "dry-run: issue, проверки, коммит и PR не выполнялись. Посмотреть: git -C $WT status; убрать:"
echo " git -C $HOST_DIR worktree remove --force $WT && git -C $HOST_DIR branch -D $TMP_BRANCH"
exit 0
fi
# 10. проверки хоста
run_make() { # run_make <имя> <цель>: хвост вывода при ошибке, worktree остаётся
local log; log=$(mktemp)
if make -C "$WT" "$2" >"$log" 2>&1; then rm -f "$log"; return 0; fi
echo "sync-statements: проверка «$1» не прошла; worktree оставлен для разбора: $WT" >&2
tail -40 "$log" >&2; rm -f "$log"; exit 1
}
run_make "make venv" venv
run_make "make check" check
echo "проверки хоста: make check зелёный"
# 11. коммит, push, PR
git -C "$WT" add -A api/arm_statements
git -C "$WT" -c commit.gpgsign=false commit -q -F - <<EOF
chore(arm_statements): копия arm-statements@$T7 (#$ISSUE)
Копия api/arm_statements: $OLD7..$T7 ($N_COMMITS коммитов), скрипт docs/scripts/sync-statements.sh.
Коммиты arm-statements:
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
$SC_LINE
Co-Authored-By: sync-statements.sh <noreply@aepif.ru>
EOF
COMMIT=$(git -C "$WT" rev-parse --short=7 HEAD)
git -C "$WT" push -q -u origin "$BRANCH"
PR_BODY="Закрывает #$ISSUE.
Копия \`api/arm_statements\` в \`$HOST_NAME\` обновлена скриптом \`docs/scripts/sync-statements.sh\`: \`$OLD7\` -> \`$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`).
Коммиты arm-statements ($N_COMMITS):
$(printf '%s\n' "$COMMITS" | sed 's/^/- /')
Проверки: \`make check\` в worktree зелёный; $SC_LINE
${CORE_WARN:+
Предупреждение: $CORE_WARN
}
## Rulings I made
- R-1 — копия не правится: перенос проревьюенного в arm-statements кода, содержимое равно \`git archive\` коммита \`$T7\`.
- R-2 — bounded-задача без OpenSpec change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
PR_URL=$(tea pulls create "${TEA_FLAGS[@]}" --head "$BRANCH" --base "${HOST_REF#origin/}" \
--title "chore(arm_statements): копия arm-statements@$T7 (#$ISSUE)" --description "$PR_BODY" \
| grep -o 'https://[^ ]*/pulls/[0-9]*' | tail -1)
# 12. итог
echo
echo "готово: коммит $COMMIT, ветка $BRANCH"
echo "issue: $ISSUE_URL"
echo "PR: ${PR_URL:-создать вручную: tea pulls create ${TEA_FLAGS[*]} --head $BRANCH --base ${HOST_REF#origin/}}"
[ -z "$CORE_WARN" ] || echo "предупреждение: $CORE_WARN"
echo "исполнителю: ревью диапазона не нужно — код проревьюен в arm-statements; merge и «закрывай» по команде человека."
echo "worktree $WT и .bounded остаются до закрытия."

View File

@ -296,5 +296,48 @@ sync 1 "in-place: повтор после защиты путей остаётс
check "$(has '/Users/' && echo 0 || echo 1)" "in-place: повтор — снова /Users/"
git -C "$SRC" checkout -q main
# --- sync-statements.sh --dry-run (обычный режим без issue и PR) --------------------------------------------------
S27=${S2:0:7}
git -C "$SRC" checkout -q main
rm_wt() { # rm_wt <хост-каталог> <имя хоста>: убрать worktree и ветку синхронизации
git -C "$1" worktree remove --force "$WORK/wt/statements-sync-$S27/$2" 2>/dev/null || true
git -C "$1" branch -D "tmp/statements-sync-$S27" >/dev/null 2>&1 || true
}
WTC="$WORK/wt/statements-sync-$S27/arm-core"
sync 0 "dry-run: arm-core" --dry-run --host-dir "$WORK/arm-core"
check "$([ -d "$WTC" ] && git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && [ -f "$WTC/.bounded" ] && echo 0 || echo 1)" "dry-run: worktree, ветка и .bounded"
check "$([ "$(tr -d '\n' < "$WTC/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "dry-run: VERSION = S2"
rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc"
check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$WTC/api/arm_statements" >/dev/null && echo 0 || echo 1)" "dry-run: копия равна git archive S2"
check "$(has 'dry-run: issue, проверки, коммит и PR не выполнялись' && echo 0 || echo 1)" "dry-run: сообщение и команды удаления"
check "$([ -z "$(git -C "$WORK/arm-core" status --porcelain)" ] && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && echo 0 || echo 1)" "dry-run: хост не тронут"
sync 2 "dry-run: повтор" --dry-run --host-dir "$WORK/arm-core"
check "$(has 'уже существует' && echo 0 || echo 1)" "dry-run: повтор — уже существует"
rm_wt "$WORK/arm-core" arm-core
check "$([ ! -d "$WTC" ] && ! git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && echo 0 || echo 1)" "dry-run: worktree и ветка удалены"
sync 0 "dry-run: сначала ядро" --dry-run --host-dir "$WORK/tabular-parser"
check "$(has 'сначала ядро' && echo 0 || echo 1)" "dry-run: предупреждение «сначала ядро»"
rm_wt "$WORK/tabular-parser" tabular-parser
# arm-core на S2 закоммичена и запушена -> раскладывать нечего
"$SYNC" "${COMMON[@]}" --in-place --host-dir "$WORK/arm-core" >/dev/null
commit_push "$WORK/arm-core" "copy S2" main
sync 0 "dry-run: уже синхронно" --dry-run --host-dir "$WORK/arm-core"
check "$(has 'раскладывать нечего' && [ ! -e "$WTC" ] && echo 0 || echo 1)" "dry-run: уже синхронно — worktree не создан"
# прямая правка на origin/main хоста -> отказ 20
echo "# прямая правка" >> "$WORK/tabular-parser/api/arm_statements/steps.py"
commit_push "$WORK/tabular-parser" "прямая правка" main
sync 20 "dry-run: прямая правка на origin/main" --dry-run --host-dir "$WORK/tabular-parser"
check "$(has steps.py && [ ! -e "$WORK/wt/statements-sync-$S27/tabular-parser" ] && echo 0 || echo 1)" "dry-run: отказ — steps.py, worktree не создан"
# хост без копии
sync 2 "dry-run: хост без копии" --dry-run --host-dir "$FR"
check "$(has -- '--in-place' && echo 0 || echo 1)" "dry-run: хост без копии — подсказка --in-place"
# tea: флаги только после подкоманды
check "$(grep -nE '(^|[^[:alnum:]_-])tea[[:space:]]+-' "$SYNC" >/dev/null && echo 1 || echo 0)" "tea: флаги после подкоманды"
[ "$fail" = 0 ] || exit 1
echo "ok: sync-statements.sh — все сценарии пройдены"