fix(sdlc-rename): cutover сверяет HEAD с новым репозиторием — Gitea не перенаправляет SSH

После переименования в Gitea старый адрес по SSH отвечает «Cannot find
repository» (T6.2 подтвердилось): preflight сверяет main по адресу
aep/sdlc-hub, derive_remote принимает уже сменённый origin; README —
сменить origin сразу после переименования.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-24 14:09:56 +04:00
parent ddc3fd6a10
commit eb86563e2a
4 changed files with 33 additions and 13 deletions

View File

@ -371,10 +371,12 @@ make web
Одноразовое переключение живой установки (хаб назывался `jira-hub`) делает скрипт `scripts/cutover.sh` (обёртка над
`scripts/cutover.py`, stdlib). Выполняет владелец; без `--apply` скрипт только проверяет и печатает шаги.
1. Слить PR переименования; переименовать репозиторий в Gitea `aep/jira-hub` -> `aep/sdlc-hub`.
1. Слить PR переименования; переименовать репозиторий в Gitea `aep/jira-hub` -> `aep/sdlc-hub`. Gitea не
перенаправляет git по SSH со старого имени: сразу сменить адрес `origin` (общий для checkout и всех worktree) —
`git -C ~/projects/jira-bridge/jira-hub remote set-url origin ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git`.
2. Закрыть или не трогать сессии Claude с MCP хаба (после переключения -- `/mcp` reconnect).
3. `git -C ~/projects/jira-bridge/jira-hub pull --ff-only`, сразу `bash ~/projects/jira-bridge/jira-hub/scripts/cutover.sh`
-- сухой прогон, проверить вывод (шаг 1 скрипта: чистый checkout на `main` = `origin/main`, доступен `aep/sdlc-hub`,
-- сухой прогон, проверить вывод (шаг 1 скрипта: чистый checkout на `main` = `main` в `aep/sdlc-hub`, доступен `aep/sdlc-hub`,
нет целевых каталогов, есть `uv`, `make`, `claude`, `launchctl`, `node`, `npm`; неотслеживаемая старая сборка
`src/jira_hub/` не мешает, на шаге 4 она удаляется; адрес нового `origin` берётся из прежнего с заменой имени
репозитория, протокол (ssh/https) сохраняется). Скрипт также предупреждает об открытых сессиях Claude со старым

View File

@ -46,9 +46,9 @@
launchd, двух чужих репозиториев и user scope Claude; руками их легко сделать не в том порядке (I-2 wiki-module).
Скрипт одноразовый, после переключения остаётся в истории как образец; тесты — на сухой прогон и проверки шага 1
(во временном `HOME`), без launchd.
6. **Переименование репозитория в Gitea — руками владельца** в интерфейсе (Settings → Repository name). Gitea оставляет
перенаправление со старого адреса; `tea` и `origin` после этого работают по новому адресу. Скрипт проверяет, что
переименование сделано, и меняет `origin`.
6. **Переименование репозитория в Gitea — руками владельца** в интерфейсе (Settings → Repository name). Перенаправления
по SSH Gitea не даёт (проверено 24.10.2026: «Cannot find repository: aep/jira-hub»), поэтому владелец сразу меняет
`origin` на новый адрес; скрипт сверяет `HEAD` с `main` нового репозитория и принимает уже сменённый `origin`.
7. **Checkout переезжает в `jira-bridge/sdlc-hub`**, workspace `jira-bridge` остаётся: от его пути зависят каталог
памяти Claude, workspace `CLAUDE.md`, путь общей папки реле (`…/utm/rshb/jira-bridge`) и настройки агентов на
виртуалке. Переименование workspace — отдельно, если понадобится.

View File

@ -78,7 +78,9 @@ NEW_REPO = "/aep/sdlc-hub"
def derive_remote(origin: str) -> str:
"""Адрес нового репозитория из адреса origin: тот же протокол и суффикс, имя другое."""
"""Адрес нового репозитория из адреса origin: тот же протокол и суффикс, имя другое; уже новый — как есть."""
if NEW_REPO in origin:
return origin
if OLD_REPO not in origin:
raise ValueError("origin %s не содержит %s" % (origin, OLD_REPO))
return origin.replace(OLD_REPO, NEW_REPO)
@ -125,17 +127,18 @@ def preflight(cfg: Config) -> List[str]:
if br != "main":
problems.append("checkout %s не на ветке main (сейчас: %s)" % (old, br or "?"))
head = _git(old, "rev-parse", "HEAD").stdout.strip()
# ls-remote вместо fetch: проверка остаётся строго чтением (fetch пишет FETCH_HEAD)
lr = _git(old, "ls-remote", "origin", "refs/heads/main")
remote_head = lr.stdout.split()[0] if lr.returncode == 0 and lr.stdout.split() else ""
if not remote_head:
problems.append("не удалось получить origin/main из %s" % old)
elif remote_head != head:
problems.append("HEAD не совпадает с origin/main: сделайте git pull --ff-only в %s" % old)
if not remote:
remote, prob = target_remote(cfg)
if prob:
problems.append(prob)
# сверка с новым адресом: после переименования Gitea не перенаправляет git по SSH, старый origin недоступен;
# ls-remote вместо fetch: проверка остаётся строго чтением (fetch пишет FETCH_HEAD)
if remote:
lr = _git(old, "ls-remote", remote, "refs/heads/main")
remote_head = lr.stdout.split()[0] if lr.returncode == 0 and lr.stdout.split() else ""
if remote_head and remote_head != head:
problems.append("HEAD не совпадает с main в %s: сделайте git pull --ff-only в %s (origin — %s)"
% (remote, old, remote))
if remote:
lr = subprocess.run(["git", "ls-remote", remote], capture_output=True, text=True, env=_git_env(),
stdin=subprocess.DEVNULL)

View File

@ -258,6 +258,7 @@ def test_preflight_tracked_or_other_untracked_is_dirty(env):
@pytest.mark.parametrize("origin,expected", [
("ssh://git@git.aepif.ru:2222/aep/jira-hub.git", "ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git"),
("https://git.aepif.ru/aep/jira-hub.git", "https://git.aepif.ru/aep/sdlc-hub.git"),
("ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git", "ssh://git@git.aepif.ru:2222/aep/sdlc-hub.git"), # origin уже сменён
])
def test_derive_remote(origin, expected):
assert cutover.derive_remote(origin) == expected
@ -367,3 +368,17 @@ def test_step6_rewrites_consumers_with_backups(env):
assert "sdlc-jira" in (proj / ".claude" / "settings.local.json").read_text()
assert any("каталог jira-hub" in ln for ln in left)
assert not (proj / "CLAUDE.md.pre-sdlc").exists()
def test_preflight_checks_head_against_new_remote(env):
"""Gitea не перенаправляет git по SSH после переименования: старый origin недоступен, сверка — с новым адресом."""
git("remote", "set-url", "origin", str(env["tmp"] / "gone.git"), cwd=env["old"])
problems = cutover.preflight(cutover.Config.from_env())
assert not any("origin/main" in s or "HEAD не совпадает" in s for s in problems), problems
other = env["tmp"] / "other"
git("clone", str(env["bare"]), str(other), cwd=env["tmp"])
(other / "b.txt").write_text("y")
git("add", ".", cwd=other)
git("commit", "-m", "ahead", cwd=other)
git("push", "origin", "main", cwd=other)
assert any("HEAD не совпадает" in s for s in cutover.preflight(cutover.Config.from_env()))