Skip to content

Две линии: где идёт разработка и что видит потребитель ​

Решение #267 (29.08): GitLab — линия разработки, GitHub — зеркало релизов. Оно заменило формулировку от 25.08 («GitHub становится основным местом разработки»), которая пять сессий подряд не исполнялась: ветки жили в GitLab, и решение приведено к практике, а не практика к решению.

Эта страница существует потому, что до неё процедура публикации жила ТОЛЬКО в решениях. Решение, которое исполняется памятью агента, а не механизмом, не исполняется — и пять сессий тому подтверждение.

EN mirror: /docs/publishing.md.

Кто есть кто ​

ЛинияЧто этоЧто там лежит
origin — GitLabЛиния разработкиПолная история: ветки, волны, вся бухгалтерия проекта. Сюда идёт каждый push, и по каждому push собирается CI (быстрая лента на стадии test, полная — на test-full).
github — GitHubЗеркало релизовТолько выпуски, СПЛЮЩЕННЫМИ снимками. Замер #189: 16 коммитов против 405 у origin/main. Общей истории у линий нет — git merge-base пуст, корни разные.
tausik/site — GitLabСайт tausik.techОтдельный репозиторий (VitePress, сборка, nginx, CI сайта). В core и на публичной линии следов сайта НЕТ — замер смены #255 по обоим деревьям дал ноль, и tests/test_site_lives_elsewhere.py держит ноль (решение #368). Ссылка на tausik.tech в README — не след сайта, а указатель на него.

Из этого следует одно практическое правило, которое стоит помнить дословно: git push github v1-9-wave — не синхронизация, а перенос 431 коммита приватного архива в публичную линию. Публикация есть акт над ДЕРЕВОМ, а не над коммитом (решение #260).

Порядок выкладки (решение #368) ​

GitLab хранит всю историю; в GitHub уезжает только тег финальной версии — сплющенный снимок ОТФИЛЬТРОВАННОГО дерева, поставленный fast-forward-ребёнком поверх подтверждённой публичной головы. «GitLab идентичен GitHub» означает: содержимое снимка равно отфильтрованному дереву тега байт в байт, и это равенство проверяет машина, а не память.

  1. Релиз собирается и закрывается на линии разработки (GitLab); полный прогон зелёный по процедуре CI (.gitlab-ci.yml: клон, bootstrap --no-detect --ide all, pytest -m '').

  2. Снимок строится и проверяется кодом — scripts/publication_snapshot.py, команда tausik publish:

    bash
    # что войдёт, что останется, классы утечки на снимке; коммит не пишется
    tausik publish snapshot --from v1.9.0 --parent github/main --dry-run
    # снимок поверх публичной головы; печатает три проверки и команды выкладки
    tausik publish snapshot --from v1.9.0 --parent github/main
    # «идентичен»: дерево снимка == отфильтрованное дерево источника
    tausik publish verify --snapshot <sha> --from v1.9.0

    Команда работает над ОБЪЕКТАМИ: рабочее дерево, индекс, ветки и remote не трогаются. Отказ, если класс утечки на снимке не ноль, если --parent не коммит или если снимок не равен фильтру. Чего команда знать не может — так это вершину REMOTE: устаревший локальный github/main ловит fast-forward-only push на шаге 3, и ответ на тот отказ — fetch и снимок заново на новой вершине. Force не предусмотрен флагом — и запрещён файрволом.

  3. Push и тег — акты владельца, руками, после чтения того, что напечатала команда: git push github <sha>:refs/heads/main (только fast-forward), затем git push github <sha>:refs/tags/v1.9.0 — push по refspec, а не второй git tag -a: имя v1.9.0 уже стоит на релизном коммите линии разработки (из него шаг 2 и собирал снимок), и git откажется создать его второй раз. Одно имя, два объекта — релизный коммит на GitLab и снимок на GitHub — и одно отфильтрованное дерево, равенство которого доказал publish verify. tests/test_publish_cli.py выполняет напечатанные акты против голого remote и держит: main и тег на remote встают на снимок, а локальное имя остаётся на истории. Тег на GitHub — ЛЕГКОВЕСНЫЙ (ref без объекта тега и без сообщения; аннотированные теги до 1.8.0 ставились на orphan-линии руками); заметки к выпуску — это CHANGELOG и GitHub Release, а published_tags.json в любом случае записывает коммит. В том же заходе, на линии разработки, обновляется tausik/published_tags.json (см. «Теги» ниже).

  4. Заявленная редакция SENAR опубликована: tausik publish senar-check спрашивает у GitHub Kibertum/SENAR тег редакции, которую заявляет TAUSIK. Код 1 — тега нет, код 2 — спросить не удалось; в обоих случаях выпуск TAUSIK не тегируется. Печатает обе версии и ссылку на релизы SENAR.

  5. Заметки к выпуску: тело GitHub Release ведётся на английском, полный текст живёт в docs/en/whats-new-X.Y.md и docs/ru/whats-new-X.Y.md, и тело ссылается на обе страницы. tausik publish notes --version X.Y.Z --body-file <файл> отказывает телу без любой из них; запускать до создания релиза. Правило действует с 1.10; опубликованные теги ради него не перевыпускаются.

  6. Авторство внешних вкладов сохраняется: чужие коммиты входят мержем или squash-ом с Co-Authored-By автора — иначе авторство стирается.

Что публикуется: фильтр, а не всё дерево ​

До #368 наружу уходило ВСЁ отслеживаемое дерево, включая tausik/ — папку собственной бухгалтерии проекта: замер смены #251 дал на github/main 2438 файлов tausik/ из 3576, то есть 70 % того, что клонировал потребитель, — и именно в этой бухгалтерии жили объявленные ниже классы утечки.

Теперь исключения — ОДНА объявленная константа publication_snapshot.EXCLUDED_FROM_PUBLIC_SNAPSHOT, удерживаемая tests/test_publication_snapshot.py:

Остаётся на линии разработкиПочему
tausik/tasks/, tausik/stories/, tausik/epics/, tausik/decisions/, tausik/memory/, tausik/graph-snapshots/проекция состояния для переноса между машинами по ветке; потребителю фреймворка не нужна
docs/ru/research/release-111-economy-plan-2026-10-01.mdвнутренний план выпуска со ссылками на трекер разработки; публичные результаты остаются в других документах
.gitlab-ci.ymlпайплайн линии разработки
scripts/ci_lane_dev.py, tests/test_ci_lane_dev.pyчтение того пайплайна: инструмент для хоста, с которым у публичного репозитория нет отношений. cli_push_ok импортирует модуль НЕОБЯЗАТЕЛЬНО, поэтому опубликованное дерево работает без него и молчит, а не жалуется

AGENTS.md и CLAUDE.md остаются публичными, но snapshot очищает их порождённые блоки DYNAMIC. Стабильные правила агента публикуются; текущая сессия разработки, имена задач и путь к журналу хоста — нет.

Храповики tausik/*.json (gates, policy, published_tags, spec_coverage) едут: их читают гейты и тесты. Замер на дереве 1.9: 1308 файлов уходит, 3090 остаётся.

Классы утечки проверяются машиной по-прежнему — на ВСЁМ дереве (tests/test_publication_lines.py) и на СНИМКЕ (tausik publish snapshot отказывает при ненулевом классе). Замер смены #256:

КлассБыло (#180)Всё деревоСнимокСтатус
Локальный путь с именем пользователя12 файлов00вычищено, удерживается храповиком
Имена чужих проектов44 файла00вычищено, удерживается храповиком
Внутренний хост gitlab.yumash.ru17 файлов4 файла0остаток объявлен на дереве, на снимке ноль
Путь среды разработки вида D:\Workне измерялся22 файла0остаток объявлен на дереве, на снимке ноль

Остаток на всём дереве живёт в бухгалтерии, которая наружу не едет; его число зафиксировано, рост краснеет. На снимке оба класса — ноль, и это не объявление, а отказ команды.

Файл, который ОПИСЫВАЕТ утечку — эта страница, задача о ней, решение, сам тест, — утечкой не является. Иначе проверка запретила бы писать о проблеме, что дороже самой проблемы.

Теги: публичный тег — обещание, а не закладка ​

README велит подключать репозиторий сабмодулем, а сабмодуль пинится SHA или тегом. Значит перемещение опубликованного тега переставляет ЧУЖОЙ пин на другое дерево, и молча. Тем же доводом здесь уже запрещён force-push, и сам git отказывается перетирать тег словами «would clobber existing tag». Поэтому:

опубликованный тег не двигается никогда, не удаляется и не перевыпускается.

Что измерено (смена #233, git ls-remote обоих линий) ​

публичная (github)приватная (origin)локально
тегов91722

Только у origin: v1.1.0 v1.2.0 v1.3.0 v1.3.2 v1.3.7 v1.4.0 v1.4.1v1.4.2 v1.5.2. Только у github: v1.5.3.

Общих имён восемь, и все восемь указывают на РАЗНЫЕ объекты — совпадающих нет ни одного:

тегgithuborigin
v1.5.58084cc9fc61bbb10
v1.5.6f380a2e0698f2d00
v1.5.77c311e817eb40153
v1.5.849dcf47de4f961b0
v1.6.0c50c6de9af283bdf
v1.6.1e036321dcdde825a
v1.7.0bf13a09e2ec833a5
v1.8.0623fb4ee3866702f

Причина измерена, а не предположена: публичные теги ставились на orphan-снимке, у которого нет общего предка с настоящей историей. Один и тот же номер релиза называет два разных дерева — и до 1.9 имя тега НЕ является общим адресом между линиями.

Что с этим делается ​

Прошлое не чинится: тринадцать недостающих тегов задним числом НЕ выставляются, восемь разошедшихся НЕ приводятся друг к другу. Любое из этих действий переставило бы чужой пин.

Расхождение объявлено этой таблицей и удерживается механизмом: tausik/published_tags.json хранит девять пар «имя → объект», снятых с публичной линии, а tests/test_published_tags_are_promises.py сверяет с ними живой ls-remote. Изменение, исчезновение И появление считаются движением. Недоступная сеть даёт ПРОПУСК с объяснением, а не зелёное: проверка, которая проходит, не дозвонившись, утверждает то, чего не измеряла.

Шаг выкладки, добавленный с 1.9 ​

С 1.9 два объекта за одним именем — это МОДЕЛЬ, а не случайность (решение #368): имя на линии разработки стоит на релизном коммите с его историей, имя на GitHub — на снимке отфильтрованного дерева этого коммита, и деревья равны под фильтром по publish verify. Что в восьми строках выше было необъяснённым расхождением, с v1.9.0 — заявленное отношение. tausik/published_tags.json обновляется на линии разработки в том же заходе, что и push, коммитом сразу после него: снимок не может нести собственный sha, поэтому копия этого файла внутри снимка по построению отстаёт на один релиз — у клона потребителя нет remote с именем github, и проверка обещания там ПРОПУСКАЕТ с объяснением, а не утверждает сравнение, которого не делала.

См. также ​

  • security-checklist.md — что проверяется перед выкладкой наружу
  • sessions.md — где записываются решения вроде #267