Две линии: где идёт разработка и что видит потребитель
Решение #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» означает: содержимое снимка равно отфильтрованному дереву тега байт в байт, и это равенство проверяет машина, а не память.
Релиз собирается и закрывается на линии разработки (GitLab); полный прогон зелёный по процедуре CI (
.gitlab-ci.yml: клон,bootstrap --no-detect --ide all,pytest -m '').Снимок строится и проверяется кодом —
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 не предусмотрен флагом — и запрещён файрволом.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(см. «Теги» ниже).Заявленная редакция SENAR опубликована:
tausik publish senar-checkспрашивает у GitHub Kibertum/SENAR тег редакции, которую заявляет TAUSIK. Код 1 — тега нет, код 2 — спросить не удалось; в обоих случаях выпуск TAUSIK не тегируется. Печатает обе версии и ссылку на релизы SENAR.Заметки к выпуску: тело 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; опубликованные теги ради него не перевыпускаются.Авторство внешних вкладов сохраняется: чужие коммиты входят мержем или 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 файлов | 0 | 0 | вычищено, удерживается храповиком |
| Имена чужих проектов | 44 файла | 0 | 0 | вычищено, удерживается храповиком |
Внутренний хост gitlab.yumash.ru | 17 файлов | 4 файла | 0 | остаток объявлен на дереве, на снимке ноль |
Путь среды разработки вида D:\Work | не измерялся | 22 файла | 0 | остаток объявлен на дереве, на снимке ноль |
Остаток на всём дереве живёт в бухгалтерии, которая наружу не едет; его число зафиксировано, рост краснеет. На снимке оба класса — ноль, и это не объявление, а отказ команды.
Файл, который ОПИСЫВАЕТ утечку — эта страница, задача о ней, решение, сам тест, — утечкой не является. Иначе проверка запретила бы писать о проблеме, что дороже самой проблемы.
Теги: публичный тег — обещание, а не закладка
README велит подключать репозиторий сабмодулем, а сабмодуль пинится SHA или тегом. Значит перемещение опубликованного тега переставляет ЧУЖОЙ пин на другое дерево, и молча. Тем же доводом здесь уже запрещён force-push, и сам git отказывается перетирать тег словами «would clobber existing tag». Поэтому:
опубликованный тег не двигается никогда, не удаляется и не перевыпускается.
Что измерено (смена #233, git ls-remote обоих линий)
| публичная (github) | приватная (origin) | локально | |
|---|---|---|---|
| тегов | 9 | 17 | 22 |
Только у 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.
Общих имён восемь, и все восемь указывают на РАЗНЫЕ объекты — совпадающих нет ни одного:
| тег | github | origin |
|---|---|---|
v1.5.5 | 8084cc9f | c61bbb10 |
v1.5.6 | f380a2e0 | 698f2d00 |
v1.5.7 | 7c311e81 | 7eb40153 |
v1.5.8 | 49dcf47d | e4f961b0 |
v1.6.0 | c50c6de9 | af283bdf |
v1.6.1 | e036321d | cdde825a |
v1.7.0 | bf13a09e | 2ec833a5 |
v1.8.0 | 623fb4ee | 3866702f |
Причина измерена, а не предположена: публичные теги ставились на 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