К содержимому
gtcnsl
EN RU

Изменения

Значимые изменения gtcnsl, новые сверху. Формат — Keep a Changelog; версии — по Semantic Versioning.

Keep a Changelog · SemVer
v1.6.0 2026-08-10
Changed
  • Генерация базового `config.yaml` (первый запуск `runner register`/`reconfigure`, команда `gtcnsl runner config generate`) вызывает подкоманду раннера `config generate` на бинарниках ≥ 3.1.0 и откатывается на устаревший `generate-config` на более старых — тот же паттерн version-гейта, что у `--token-file`.
Fixed
  • `runner register --force` поверх работающего демона теперь останавливает сервис до удаления старой регистрации `.runner`. На раннере ≥ 3.0.0 прежний порядок уничтожал старую регистрацию и затем падал на локе `.runner.lock` демона, оставляя хост незарегистрированным; порядок «сначала стоп» (зеркально `reconfigure`) работает и на systemd, и на OpenRC.
Подробнее →
v1.5.0 2026-08-06
Added
  • **openSUSE Leap 16 присоединяется к поддерживаемой матрице — первое zypper-семейство.** `internal/pkgmgr` получил `FamilySUSE`/`DistroOpenSUSE`: `Install` запускает `zypper --non-interactive refresh`, затем `zypper --non-interactive install` (в отличие от RHEL-семейства, где dnf обновляет метаданные неявно, репозитории openSUSE по умолчанию идут с `autorefresh=0`, так что явный refresh обязателен, а не опционален), `IsInstalled` использует ту же `rpm -q`, что и RHEL-семейство (оба — RPM-based), а нераспознанный SUSE-клон, у которого `/etc/os-release` `ID_LIKE` содержит `suse`, теперь резолвится в нужное семейство через тот же fallback, что уже используют клоны Rocky/AlmaLinux. Docker ставится прямо из штатного репозитория Leap (`docker`, сторонний репозиторий не нужен — в отличие от танца с Docker CE репозиторием у RHEL-семейства); Podman — в том же белом списке, что Debian/Ubuntu/Rocky/AlmaLinux; docker-rootless на этом семействе тоже отклоняется, симметрично отказу RHEL-семейства (ADR 0040) — для реализации понадобился бы отдельный SUSE-пакет rootless-extras, вне текущего охвата. Проба CA-бандла `gtcnsl doctor` получила `/etc/ssl/ca-bundle.pem` (SUSE-семейство) третьим кандидатным путём рядом с существующими Debian- и RHEL-путями. См. ADR 0043.
  • **Alpine Linux присоединяется к поддерживаемой матрице — только host-mode.** gtcnsl теперь работает на Alpine через второй, честный бэкенд `service.Manager` (init-агностичную абстракцию, описанную ниже в разделе «Изменено»): `internal/openrc` управляет через `rc-service`/`rc-update` и разворачивает supervise-daemon-скрипты `/etc/init.d`, которые gtcnsl рендерит сам (`internal/gitea`/`internal/runner` получили `RenderOpenRCUnit` рядом с уже существующими systemd-шаблонами). `internal/app` выбирает бэкенд автоматически через `service.Detect()` — без флага, без конфига. `internal/pkgmgr` получил `FamilyAlpine` (`apk update` + `apk add`, `apk info -e` для проверки установки); `internal/osuser` получил BusyBox-ветку `adduser`/`addgroup` для хостов без `useradd`/`usermod`. `gtcnsl doctor` знает про init-системы: на OpenRC-хосте проверка init-системы теперь проходит (PASS), а не жёстко падает с «systemd not running». **MVP-объём, намеренно (ADR 0020/0045): только host-mode Gitea и host-mode раннер.** Executor'ы раннера `docker`, `docker-rootless`, `podman` и `dind-rootless` на Alpine честно отказывают с объяснением (на Alpine нет systemd-logind — session-менеджера, от которого зависят rootless/dind-rootless сетапы этих executor'ов), а не падают непредсказуемо на полпути. См. ADR 0020/0044/0045.
  • **Загрузки теперь восстанавливаются после зависшего соединения вместо бесконечного зависания.** Каждый путь загрузки — установка/апгрейд Gitea, установка/апгрейд gitea-runner и собственный self-update gtcnsl, все они используют единую реализацию `internal/download` — теперь следит за ответом, переставшим отдавать байты, и после 30 секунд тишины прерывает эту попытку и повторяет (до 3 попыток всего) с новым соединением, докачивая через `Range`, если сервер это поддерживает, вместо того чтобы начинать заново. Загрузка, которую в итоге не удалось восстановить, теперь падает с понятной ошибкой «giving up after N attempts» вместо зависания. Запросы списков версий (доступные версии Gitea/раннера, манифест self-update) тоже получили таймаут 15 секунд, так что зависшее соединение больше не может подвесить и экран «loading available versions». См. ADR 0041.
  • **Экран операции TUI теперь показывает, происходит ли вообще что-то.** Живой спиннер крутится рядом с активной стадией всю операцию (а не только до первого события), события Progress рендерят процент плюс объём в человекочитаемом виде, когда размер известен (`download: 46% (19.9 MiB / 42.7 MiB)`), а уведомление о повторе из записи про устойчивость загрузок выше («retrying (attempt 2/3) from byte 307200…») теперь читается как предупреждение, а не теряется в логе.
  • **Esc на выполняющейся операции TUI честно говорит, что делает.** Однократное нажатие Esc теперь показывает `operation still running (holds the gtcnsl lock): c cancel · esc hide` вместо того, чтобы молча выходить, пока операция продолжает работать под капотом — именно эта путаница стояла за прод-инцидентом, где оператор увидел «another gtcnsl operation is already in progress» и не понял, откуда. `c` отменяет: стадия загрузки останавливается немедленно (на отменяемом per-attempt контексте, введённом записью про устойчивость загрузок выше, ADR 0041), другие стадии останавливаются на ближайшей безопасной границе между стадиями, а уже начавшаяся мутирующая стадия (замена бинарника, рестарт юнита) всегда доводится до конца атомарно, а не обрывается на полпути. Второй Esc скрывает экран и возвращает на дашборд, который показывает индикатор «operation in progress…», пока скрытая операция не завершится. Отмена никогда не роняет TUI — экран показывает «Cancelled», а не сырую ошибку. См. ADR 0042.
  • **Выбор версии в TUI теперь восстанавливается после неудачной/просроченной по таймауту загрузки списка версий вместо бесконечного ожидания.** При ошибке теперь предлагается `r` для повтора загрузки в дополнение к уже существующему ручному вводу.
Changed
  • **Внутренний рефакторинг: управление сервисами теперь идёт через init-агностичный `internal/service.Manager` вместо прямого обращения к `systemd.Manager`.** Раньше `internal/core` безусловно вызывал `internal/systemd` для каждой операции с сервисом. Теперь он программируется против `service.Manager` — операций, которые умеет любая init-система (start/stop/restart/enable/deploy unit/reload), — плюс три опциональных интерфейса возможностей (`DropInCapable`, `LingerCapable`, `UnitInspector`) для systemd-специфичных функций (drop-in-оверлеи, session lingering, живая интроспекция юнита), с честным отказом, когда у бэкенда нет нужной возможности. `internal/systemd` не перемещён и не переименован — теперь это просто (единственный, пока) бэкенд, реализующий эту абстракцию. **Поведение не изменилось**: это подготовка к поддержке Alpine/OpenRC, а не сама поддержка Alpine — все существующие тесты проходят без изменений. См. ADR 0044 (уточняет ADR 0020).
Fixed
  • **Зависшее соединение с Gitea/CDN могло подвесить апгрейд бесконечно.** Найдено в прод-инциденте, где HTTP/2-клиент Go завис на загрузке `dl.gitea.com` за Cloudflare: соединение оставалось в установленном состоянии с нулевой пропускной способностью и без ошибки, так что ничего никогда не срабатывало по таймауту. Клиенты загрузки и обнаружения версий в gtcnsl теперь принудительно используют HTTP/1.1, что воспроизводимо избавило от зависания в этом инциденте, и, согласно ADR 0041, применяется только к этим клиентам — административный API Gitea и клиенты health-check не затронуты. (Докачка через Range выше покрывает оставшийся случай «иногда сеть просто икает» поверх этого.)
  • Прерванная загрузка (например, `SIGTERM` посреди передачи) могла оставлять после себя временный файл `gtcnsl-download-*`; теперь загрузка всегда удаляет его при любом неуспешном исходе, включая отменённый контекст.
Подробнее →
v1.4.0 2026-08-04
Added
  • **Стартовый экран TUI теперь — живой дашборд.** SERVICES (статус установки Gitea и gitea-runner — not installed / inactive / active / unknown — плюс версия бинарника) и CONFIG (путь и наличие `app.ini`, число управляемых ключей `secrets.ini`) рендерятся над теми же шестью пунктами ACTIONS, что всегда были в меню. Все данные читаются с хоста вживую (root не нужен) и деградируют честно: ничего не додумывается, «unknown» — когда сама проба падает, и никогда не выводится ложное «в синхроне» для конфига. Статус обновляется по `r`; первый кадр никогда не блокируется на нижележащих вызовах `systemctl`/`exec`.
  • **Golden byte-for-byte снапшоты дашборда TUI и детерминированный рендерер скриншотов защищают от того, чтобы TUI и маркетинговый сайт снова разошлись.** `test/integration/scenarios/tui-golden.sh` захватывает дашборд свежего хоста из настоящего pty (tmux, включая ANSI-коды) и сравнивает его байт-в-байт с закоммиченным фикстур-файлом (`test/integration/golden/dashboard-fresh.ans`); текстовый двойник делает падения читаемыми для человека, а `GOLDEN_UPDATE=1` обновляет оба файла после осознанного изменения. `test/integration/render-tui.sh` превращает любой такой `.ans`-дамп в воспроизводимый PNG через новый `cmd/tui-render` (чистый Go-рендерер, `golang.org/x/image`, с закреплённым и сверенным по sha256 моноширинным шрифтом) — теперь это канонический способ делать скриншоты TUI для сайта. См. ADR 0039.
  • **AlmaLinux 9/10 и Rocky 10 присоединяются к поддерживаемой матрице** вместе с рефакторингом `pkgmgr.Family` (`FamilyDebian` / `FamilyRHEL`), который диспетчеризует каждую команду пакетного менеджера по семейству, а не по отдельному дистрибутиву — добавление дистрибутива в существующее семейство теперь сводится к одной константе `Distro` плюс одной записи в отображении `Family`. Установка Docker/Podman и отказ docker-rootless в `internal/core` теперь диспетчеризуются так же; формулировка отказа docker-rootless обобщена с «on Rocky 9» до «on the RHEL family (Rocky, AlmaLinux)» — он по-прежнему не реализован намеренно (ADR 0040). Существующее поведение Debian/Ubuntu/Rocky не изменилось байт-в-байт.
  • **Ubuntu 22.04 LTS и 26.04 LTS присоединяются к поддерживаемой матрице**, используя тот же диспетчер `pkgmgr.Family`, что ввела работа над AlmaLinux/Rocky выше: менять продуктовый код не пришлось — apt-путь Debian-семейства покрывает оба релиза как есть. Два новых тестовых образа следуют паттерну существующего ubuntu-24.04 (официальные multi-arch базовые образы, самостоятельно собранный systemd по ADR 0023). С этим релизом тестовая контейнерная матрица достигает восьми образов из четырёх семейств дистрибутивов (политика прогонов ADR 0040 «ядро на фичах, полная матрица на release-prep», `docs/RELEASING.md`). Горизонт на заметку: стандартная поддержка 22.04 заканчивается в 2027-04 — тогда он станет кандидатом на удаление.
Changed
  • `GiteaInspection`/`RunnerInspection` (используются внутри install/upgrade/register/reconfigure) получили поле `Active *bool`, сообщающее живое состояние `IsActive` юнита.
Подробнее →
v1.3.2 2026-07-23
Fixed
  • **HTTPS-установки теперь задают `[server] LOCAL_ROOT_URL`, что чинит ssh-пуши на Gitea 1.27+.** `gitea serv` (запускается на каждый SSH-запрос) обращается к внутреннему API Gitea через `LOCAL_ROOT_URL`; без явного значения он по умолчанию выводится как `https://localhost:<порт>/`, а сертификат https-acme или https-manual привязан к реальному домену, не к `localhost` — внутренний TLS-хендшейк падает, и `git push`/`pull` по SSH умирают с «Internal Server Connection Error» (Gitea 1.26 терпела это расхождение; 1.27 ужесточила внутренний TLS-путь). Найдено вживую на проде после апгрейда 1.26.4 → 1.27.0 на https-direct/ACME-хосте. `gitea install --serving https-acme|https-manual` и `gitea enable-https` теперь автоматически записывают `LOCAL_ROOT_URL` равным `ROOT_URL`; behind-proxy-установки не затронуты (там собственный дефолт Gitea `http://localhost:3000` уже корректен). `gtcnsl doctor` получил пробу уровня WARN (никогда не FAIL) для существующих https-установок, которые инструмент не трогал, с готовой командой: `gtcnsl config set server.LOCAL_ROOT_URL <ROOT_URL> --yes`.
  • **`gtcnsl backup` больше не падает на хосте только с раннером.** Хост, где стоит только `gitea-runner` (Gitea не установлена вовсе — реальная, типовая форма выделенной раннер-машины), раньше падал с «core: /usr/local/bin/gitea is required for a backup but is missing». Бинарь Gitea и `app.ini` теперь обязательны только когда живая установка Gitea действительно обнаружена на хосте; если нет — они пропускаются с предупреждением, ровно так же, как раннер-элементы уже пропускались на хосте с Gitea без раннера. Хост, где не обнаружены ни Gitea, ни раннер, по-прежнему падает — с внятным «nothing to back up on this host» — вместо записи пустого бэкапа. `gtcnsl restore` получил симметричный фикс: восстановление runner-only бэкапа больше не останавливает, не запускает и не проверяет `gitea.service` — на раннер-хосте этого юнита обычно вообще нет.
Подробнее →
v1.3.1 2026-07-23
Changed
  • Install-сниппет в README указывает на текущий релиз вместо v1.2.1.
Fixed
  • **Релизный workflow теперь проверяет собственные входные данные.** Релиз, опубликованный с тегом без ведущего `v` (как первая попытка v1.3.0), раньше проходил сборку целиком и умирал на шаге зеркалирования с непонятным `405` — теперь он падает за секунды, до какой-либо публикации, с сообщением, которое прямо описывает восстановление (удалить релиз и тег, пересоздать с `vX.Y.Z`; ре-ран задачи не может исправить неверный тег). Исправлена и синтаксическая ошибка YAML в самом этом гарде, из-за которой файл workflow ненадолго стал невалидным.
Подробнее →
v1.3.0 2026-07-18
Added
  • **Gitea 1.27.x и Runner 2.x сверены и поддерживаются, а `--reencrypt` теперь защищён конвертом версий.** Криптография перешифровки SECRET_KEY (`internal/gitea/secretcrypto`/`secretdb`) заново сверена байт-в-байт с исходниками Gitea v1.27.0 — обе схемы, все пять зашифрованных мест, новых точек вызова не появилось, — а встроенная базовая схема конфигурации поднята с 1.26.2 до 1.27.0 (9 секций без изменений, 764 → 772 ключа); `scripts/update-gitea-schema`, на который `docs/ARCHITECTURE.md` ссылался ещё с ADR 0015, хотя скрипт никогда не существовал, теперь реально существует. Эта криптография перешифровки — не публичный контракт Gitea: апстрим может изменить её в любом миноре, не упомянув в changelog, — поэтому `secrets rotate SECRET_KEY --reencrypt` теперь жёстко отказывает, до любых изменений, на любом миноре Gitea вне сверенного конверта (сейчас `[1.24, 1.27]`), вместо того чтобы рисковать повторением инцидента с тихой порчей данных, из которого вырос v1.2.0; явный, отдельный обход — `--reencrypt-unverified-version-i-understand` (ADR 0028). COMPATIBILITY.md теперь прямо называет поддерживаемый диапазон апстрима (Gitea вплоть до 1.27.x, Runner ≥1.0 включая 2.x); ломающее изменение Runner 2.x (убраны неявные переменные окружения `DOCKER_USERNAME`/`DOCKER_PASSWORD`) не затрагивает конфиги, генерируемые gtcnsl.
  • **`gtcnsl runner register`/`runner reconfigure --token-file <path>`.** Новый флаг, взаимоисключающий с `--token`: gtcnsl сам читает и обрезает пробелы в файле, а при обнаруженном `gitea-runner` >= 2.1.0 (релиз, добавивший поддержку `--token-file` в апстриме) вообще не помещает токен в argv — записывает его во временный файл 0600, принадлежащий gtcnsl, внутри рабочей директории раннера, передаёт `--token-file` в подпроцесс `gitea-runner register` и удаляет временный файл независимо от результата. Токен в argv виден любому на хосте всё время жизни процесса (`/proc/<pid>/cmdline`, `ps`); это закрывает окно для раннеров, которые достаточно новы, чтобы это поддерживать. Раннеры ниже 2.1.0 сохраняют прежнее поведение с `--token` в argv — задокументированный запасной вариант, а не тихий отказ. Executor dind-rootless и так был основан на env-файле (ADR 0022) и не затронут в любом случае (ADR 0030).
  • **`gtcnsl runner register`/`reconfigure` теперь генерируют базовый `config.yaml`, а `gtcnsl runner config generate [--force]` (пере)пишет его по запросу.** Runner 2.x добавил пачку новых ключей config.yaml (`runner.post_task_script`, `action_shallow_clone`, `container.network_create_options.enable_ipv4/6` и другие), которые gtcnsl раньше не давал ни увидеть, ни задать — `runner register` всегда писал только `.runner`, так что каждый установленный раннер работал на недокументированных встроенных дефолтах gitea-runner. Регистрация/реконфигурация теперь пишут базовый файл через собственную подкоманду `generate-config` установленного бинарника (атомарно, 0640, с chown) — но только если `config.yaml` там ещё нет; существующий файл (отредактированный оператором или оставшийся с предыдущей регистрации) никогда не трогается. `ExecStart` юнита `gitea-runner.service` получает `--config <WorkDir>/config.yaml`, но строго монотонно: только после того, как этот файл подтверждённо есть на диске в момент рендера юнита, — так что апгрейд на месте установки, сделанной до этой фичи, никогда не укажет юниту на ещё не существующий файл (ADR 0031). Декларативное применение отдельных ключей config.yaml намеренно отложено; `runner config` — родительская команда-заготовка под это.
  • **`gtcnsl gitea upgrade [--to <v>] [--yes]` и `gtcnsl runner upgrade [--to <v>] [--yes]`** — глагол `upgrade`, который уже упоминали README.md и COMPATIBILITY.md, теперь оформлен как настоящие команды (ADR 0035). Обе — тонкие обёртки, а не новая машинерия: `gitea upgrade` отказывает, до каких-либо изменений, если ставить пока нечего, иначе передаёт весь поток тому же пути обновления на месте, что `gitea install --to <version>` использует с фичи 0067 (живые пути, сохранение чужого юнита, health-check); `runner upgrade` подключает CLI прямо к `internal/core.UpgradeRunner` (фича 0014), чей preflight уже отказывал при отсутствующей установке и уже следовал живому пути бинарника чужого или устаревше-именованного юнита (фича 0080, ADR 0034). Флаги намеренно минимальны (только `--to`/`--yes`) — `gitea install --to` остаётся более полным по флагам путём для `--data-dir`/`--rewrite-unit`/`--serving` в рамках обновления на месте.
  • **`gtcnsl runner adopt` — взять под управление уже существующий Gitea Actions Runner, никогда не перерегистрируя его.** Раннер-близнец `gitea adopt` (ADR 0032): регистрационные токены обычно одноразовые, поэтому в отличие от `runner reconfigure` эта команда не трогает `.runner`, кроме подтягивания его прав до 0640, если они слабее (файл также несёт регистрационный токен — никогда не логируется, не переписывается, не перемещается). По умолчанию dry-run; `--yes` применяет изменения. Распознаёт как текущее имя `gitea-runner.service`, так и устаревшее `act_runner.service` (ADR 0013/0018), считывая бинарник/рабочую директорию/пользователя прямо из директив живого юнита, а не из дефолтов gtcnsl — и — благодаря единой инспекции живого юнита из этого же релиза (ADR 0034, см. Fixed ниже) — последующий `runner install --to`/`runner upgrade` распознаёт усыновлённую установку и работает по тем же живым путям. Юнит, написанный не gtcnsl, по умолчанию сохраняется байт-в-байт (ADR 0025); `--rewrite-unit` заменяет его содержимое шаблоном gtcnsl, отрендеренным из живых значений, сохраняя имя файла (без переименования). Установки dind-rootless отклоняются как вне области действия (ADR 0022).
  • **Состояние раннера теперь путешествует вместе с `gtcnsl backup`/`restore`.** Регистрационные токены `.runner` обычно одноразовые, так что потеря файла раньше означала новый токен и потерянный UUID раннера. `backup` теперь дополнительно и опционально (Warning + пропуск при отсутствии, как и с `secrets.ini`/`custom/` сегодня) захватывает `<WorkDir>/.runner` (секретный класс — его токен никогда не попадает в события/логи), `config.yaml`, юнит `gitea-runner.service` (читается через инжектированный systemd-менеджер, никогда не через захардкоженный путь), и `registration.env` dind-rootless — последний с явным предупреждением и пометкой в манифесте о том, что его одного недостаточно для полного восстановления раннера dind-rootless (Docker-том `gitea-runner-data` остаётся вне области действия, ADR 0022). Схема манифеста поднимается с 1 до 2; бэкап со схемой 1 восстанавливается точно как раньше, а старый gtcnsl по-прежнему отказывает на манифесте со схемой 2. `restore` останавливает `gitea-runner.service` перед применением его файлов и запускает снова только после того, как пройдёт собственный health-check Gitea, воспринимая «юнит не установлен»/«не активен» как Warning, а не Failed (ADR 0033).
Fixed
  • **`secrets rotate SECRET_KEY` — guard ротации, `--reencrypt` и проба слабого ключа в `doctor` теперь учитывают `SECRET_KEY_URI` целиком.** Gitea поддерживает получение `[security] SECRET_KEY` из файла через `SECRET_KEY_URI` с версии 1.19, но gtcnsl везде читал только буквальное значение `SECRET_KEY`: на инстансе с URI-конфигурацией guard и `doctor` молча считали публичный дефолт Gitea «старым ключом» (ложное чтение), а хуже того — обычная ротация записывала новый ключ в `secrets.ini`, ни разу не тронув файл, который Gitea реально читает, — инстанс оставался работать на *старом* ключе, пока собственный учёт gtcnsl утверждал, что ключ уже новый. Это тот же класс тихого расхождения, из которого вырос инцидент v1.2.0, и он делал `--reencrypt` на таком хосте гарантированным откатом по health-check (новый шифротекст, старый работающий ключ). Теперь gtcnsl резолвит `SECRET_KEY`/`SECRET_KEY_URI` точно так же, как это делает сама Gitea при старте (перенесено построчно из `modules/setting/security.go`, fail-closed вместо собственного `log.Fatal` Gitea при одновременной установке обоих или нерезолвящемся URI), а при ротации атомарно переписывает и сам файл за URI — со своим бэкапом с меткой времени и откатом на любом сбое — в дополнение к `secrets.ini` (ADR 0029). Ротация инстанса с буквальным `SECRET_KEY` не затронута.
  • **Неудачный `gitea upgrade` (или `gitea install --to` поверх существующей установки) теперь восстанавливает предыдущий бинарник Gitea, вместо того чтобы оставить хост без него.** Путь обновления на месте снапшотит живой бинарник перед заменой и при любом сбое после замены (деплой юнита, рестарт сервиса, health-check) восстанавливает его и перезапускает сервис — тот же откат на основе снапшота, что уже был у `runner upgrade` (ADR 0036, закрывающий асимметрию, которую ADR 0035 фиксировал как открытый компромисс). Уже существующий файл юнита теперь тоже никогда не удаляется откатом неудачного апгрейда.
  • **`runner install`/`upgrade`/`register`/`reconfigure` теперь следуют собственным фактам живого юнита** — имя юнита (включая устаревшее `act_runner.service` до переименования), путь к бинарнику и рабочая директория читаются из реального systemd-юнита, а не из предположения о каноническом layout gtcnsl (ADR 0034). До этого усыновлённая устаревшая или перенесённая установка была невидима для `runner install --to`, а `--rewrite-unit` создавал второй канонический юнит рядом с существующим. Одновременное присутствие и канонического, и устаревшего юнита теперь — жёсткая, ясно сформулированная ошибка инспекции.
Подробнее →
v1.2.1 2026-07-12
Fixed
  • **События прогресса троттлятся у источника.** Загрузчик рапортовал прогресс на каждом чтении ~32 КБ — сотни событий на файл, большинство с одним и тем же процентом. Теперь репорт только при смене целого процента (не чаще ~5/сек, если сервер не сообщил размер) и всегда на финальном байте.
  • **CLI перестаёт использовать перезапись через `\r`, когда stdout — не терминал.** Самоперезаписывающаяся `\r`-строка прогресса дословно выживает в пайпах, CI-логах и ssh-сессиях без tty. На настоящем терминале ничего не меняется; во всех остальных случаях прогресс печатается обычными строками с шагом в 10 процентных пунктов плюс финальные 100%, вообще без `\r`.
  • **TUI обновляет строку прогресса на месте.** Каждое событие прогресса раньше добавляло новую строку в лог, заполняя экран рядами `download: N%`; теперь событие той же стадии заменяет открытую строку, а любое другое событие её запечатывает.
Подробнее →
v1.2.0 2026-07-12
Added
  • **`secrets rotate SECRET_KEY --reencrypt` — безопасная ротация.** Останавливает Gitea, бэкапит базу (sqlite: копия файла рядом с оригиналом, `.reencrypt-bak`; mysql/postgres: страховкой служит единая транзакция перешифровки), записывает новые `secrets.ini`/`app.ini`, перешифровывает каждую затронутую строку — проверяя `decrypt(new) == plaintext` для каждой *до* записи, — затем запускает Gitea и проверяет здоровье. Любой сбой автоматически откатывает всё: базу, `secrets.ini`, `app.ini`, рестарт. Gitea никогда не видит рассинхрона «новый ключ / старый шифротекст». Обе схемы шифрования Gitea реализованы байт-в-байт (сверено с закреплённым исходником v1.26.4), покрыты все пять зашифрованных мест. Поддержанные движки: `sqlite3`, `mysql`, `postgres` (`mssql` отклоняется с понятной ошибкой). Повторный запуск всегда безопасен: каждый запуск мигрирует то, что расшифровывается под текущим ключом, и никогда не трогает строки, которые прочитать нельзя.
  • **`secrets rotate SECRET_KEY --dry-run`** — классифицирует каждую зашифрованную строку (будет перешифрована / уже мигрирована / нечитаема) и выводит счётчики по местам и ID строк, ничего не меняя. Не требует `--yes`.
  • **`--orphan-encrypted-data-i-understand`** — явное осознанное согласие для операторов, которые и так собираются перевыпустить секреты и перезавести 2FA вручную.
  • **`doctor` предупреждает, когда `SECRET_KEY` пуст или равен публичному дефолту Gitea.** Пустой `[security] SECRET_KEY` означает, что Gitea шифрует значением, зашитым в её собственный исходный код, — данные фактически не защищены. Проверка называет безопасный путь исправления (`secrets rotate SECRET_KEY --reencrypt`) и никогда не печатает настоящий ключ оператора.
Changed
  • **Изменение поведения (safety): `secrets rotate SECRET_KEY` отказывается сиротить данные.** Раньше ротация применялась чисто и рапортовала успех, молча осиротив каждую зашифрованную строку — продовый инцидент, из которого вырос этот релиз. Теперь она жёстко останавливается *до записи любого файла*, если существуют данные под текущим ключом, печатает по категориям, сколько будет потеряно, и требует либо `--reencrypt` (мигрировать), либо `--orphan-encrypted-data-i-understand` (принять потерю). Ротация `INTERNAL_TOKEN` / `JWT_SECRET` / `LFS_JWT_SECRET` не изменилась. Строго говоря — изменившийся исход существующего вызова; выпущено в минорном релизе сознательно, потому что прежним исходом была потеря данных.
  • **Бинарь вырос на ~7 МБ** (≈19,6 → ≈27 МБ): gtcnsl теперь несёт те же CGO-free драйверы БД, что использует сам Gitea (`modernc.org/sqlite`, `go-sql-driver/mysql`, `lib/pq`), чтобы читать и перешифровывать базу внутри процесса — plaintext никогда не покидает процесс gtcnsl, клиентские утилиты на хосте не нужны.
Fixed
  • Описание команды `secrets` больше не утверждает, что ротация «(in v0.4)» — она в деле с тех пор, как v0.4 реально вышла.
Подробнее →
v1.1.2 2026-07-08
Fixed
  • **Health-check'и работают на инстансах «только по логину».** При `REQUIRE_SIGNIN_VIEW = true` анонимный `/api/v1/version` отвечает 403, поэтому каждый health-check после рестарта падал и откатывал операцию — вторая половина той же боевой находки, первую починил v1.1.1. Liveness-пинги теперь ходят в неаутентифицированный `/api/healthz` (эндпоинт для балансировщиков), а проверка версии деградирует до «healthz жив», когда version-API скрыт за логином — версия не видна, а не неверна, и бинарь на диске был проверен контрольной суммой до старта. Инстансы настолько старые, что `/api/healthz` у них нет, откатываются к version-эндпоинту, как раньше.
Подробнее →
v1.1.1 2026-07-08
Fixed
  • **Health-check'и против ACME-хостов больше не дают ложный отказ.** Пробер, следующий конфигу, ходит на `127.0.0.1`, а Go не отправляет **SNI** для IP-литералов — встроенный ACME Gitea (autocert) выбирает сертификат строго по имени сервера и отвечал на такое рукопожатие `tls: internal error`, поэтому каждый health-check после рестарта на `https-acme`-установке падал и запускал (корректный, но ненужный) откат — `secrets rotate`, `config apply/set/toggle` и обновления на месте были заблокированы на таких хостах. Теперь пробер предъявляет живой `[server] DOMAIN` как имя сервера TLS, по-прежнему подключаясь к loopback. Найдено вживую на собственном боевом хосте проекта при первой реальной ротации `SECRET_KEY` после adopt.
Подробнее →
v1.1.0 2026-07-08
Added
  • **`gitea install` / `runner install` обнаруживают существующую установку.** Оба потока сначала инспектируют хост (версия бинаря, unit-файл, живой app.ini) и поверх существующей установки — даже ручной, с кастомными путями — переключаются на семантику обновления на месте: пути берутся из **живой** установки, а не из дефолтов; явные `--data-dir`/`--log-dir`, противоречащие живой установке, отклоняются («обновление никогда не переносит данные»); unit-файл, написанный не gtcnsl, **сохраняется байт-в-байт** при обновлении (только замена бинаря + рестарт) — заменить его позволяет новый флаг `--rewrite-unit`. Экран установки в TUI показывает обнаружение баннером обновления с предзаполненными живыми путями и отклоняет противоречащие правки на месте.
  • **`gtcnsl gitea adopt` — взять ручную установку под управление, не перенося данные.** По умолчанию dry-run; `--yes` применяет. Извлекает управляемые секреты из `app.ini` в `secrets.ini` с **теми же значениями** (без рестарта — проба секретов у doctor становится зелёной, открываются `secrets check`/`rotate`), ужимает world-readable `app.ini` до 0640, а `--emit-template` пишет свободный от секретов `app.ini.tmpl` для декларативного цикла. `--unit` дополнительно нормализует рукописный `gitea.service` к канону «управляемая база + drop-in оператора» (ADR 0025), причём эквивалентность доказывается эффективными свойствами самого systemd — любое неожиданное расхождение откатывается автоматически.
  • **Шаблон юнита gitea выдаёт `CAP_NET_BIND_SERVICE`.** Свежие установки могут указывать `server.HTTP_PORT` ниже 1024 (например, прямой HTTPS на :443 со встроенным ACME Gitea) без ручной правки юнита: `AmbientCapabilities` выдаёт capability непривилегированному сервису, а `CapabilityBoundingSet` отбрасывает все остальные. На дефолтном :3000 безвредно.
  • **Профили сервинга у `gitea install`.** `--serving behind-proxy` (по умолчанию — обычный HTTP на :3000, в точности прежнее поведение), `--serving https-acme` (Gitea сама терминирует TLS встроенным Let's Encrypt на :443, :80 отвечает на ACME-челлендж и редиректит; требуются `--domain`, `--acme-email` и **явное согласие `--acme-tos`**) и `--serving https-manual` (ваши `--cert-file`/`--key-file`, `--https-port` по умолчанию 443). Профильные префлайты выполняются до того, как что-либо скачано: целевые порты биндятся, сертификаты существуют и читаемы сервисным пользователем, домен для ACME указывает на этот хост (best-effort-предупреждение). Мастер установки в TUI получил тот же выбор отдельным шагом, с согласием на условия CA в виде явного чекбокса.
  • **`gtcnsl gitea enable-https` — переключить живую HTTP-установку на HTTPS, с откатом.** Едет на декларативной машинерии конфига: секция `[server]` переписывается атомарно с бэкапом, Gitea перезапускается и проверяется health-check'ом, а **любая ошибка автоматически откатывает к рабочему HTTP-конфигу**. Привилегированный порт на рукописном юните без `CAP_NET_BIND_SERVICE` — жёсткий стоп с печатью точных директив; `--grant-caps` вместо этого кладёт маленький drop-in от gtcnsl (файл юнита оператора никогда не редактируется, ADR 0025), который откат тоже отзывает. Повторный запуск на уже-https установке — no-op.
  • **Doctor проверяет когерентность https/порта/capability.** `PROTOCOL = https` на порту ниже 1024, чей юнит *эффективно* не выдаёт `CAP_NET_BIND_SERVICE` (drop-in учитываются, systemd спрашивается напрямую), репортится как блокер с готовым рецептом — именно такая ручная сборка иначе умирает на bind при следующем рестарте, месяцы спустя.
  • Пробер здоровья теперь выводит цель проверки из живого конфига `[server]` (схема, порт, увеличенное окно первой выдачи при включённом ACME) вместо предположения `http://127.0.0.1:3000`; https проверяется по loopback без верификации сертификата — он привязан к домену; это только liveness.
Подробнее →
v1.0.0 2026-07-01
Added
  • **`gtcnsl backup` / `backup list` / `restore`.** Снимок бинаря Gitea, `app.ini`, `secrets.ini` и дерева `custom/` в каталог с манифестом под `/var/lib/gtcnsl/backups/`, просмотр списка и восстановление одного из них безопасным потоком снимок → остановка → замена → перезапуск → health-проверка, который откатывается сам, если Gitea не поднимается. `backup --keep N` подчищает старые снимки.
  • **Исполнитель раннера `dind-rootless`.** Запуск Gitea Actions Runner в rootless-контейнере Docker-in-Docker под управлением systemd, без бинаря раннера на хосте (ADR 0022). Выбирается через `runner install --executor=dind-rootless`.
  • **Оппортунистическое обновление подписывающего ключа Gitea.** Когда встроенный релизный ключ близок к истечению, gtcnsl подтягивает свежую копию — с тем же fingerprint — с keys.openpgp.org, иначе откатывается на встроенный ключ, так что верификация загрузок продолжает работать при ежегодном продлении ключа без нового релиза gtcnsl. В обычном случае сети не требуется (ADR 0024).
  • **`gtcnsl doctor` теперь сообщает о включении в автозагрузку** юнитов `gitea` и `gitea-runner`, так что отсутствующий `systemctl enable` всплывает раньше, чем это сделает перезагрузка.
  • **Политика совместимости.** [`COMPATIBILITY.md`](COMPATIBILITY.md) фиксирует публичную поверхность, обещание по SemVer и политику «сначала deprecate, потом удаление».
Fixed
  • **Обновление Gitea на месте теперь действительно перезапускается на новый бинарь.** `gtcnsl gitea install --to <новее>` поверх работающей установки (документированный путь обновления) заменял бинарь, но вызывал обычный `systemctl start` — а это no-op на уже работающем юните: старая Gitea продолжала отвечать, и установка падала на health-проверке, сообщая старую версию. Теперь юнит перезапускается, так что обновление заменяет работающий инстанс. Обнаружено новым контейнерным сценарием приёмки обновления (фейковый менеджер systemd всегда «успешно» стартовал, скрывая проблему от юнит-тестов).
  • **Юниты включаются в автозагрузку.** `gitea` и `gitea-runner` теперь `systemctl enable`-ятся, так что поднимаются после перезагрузки, а не остаются лежать.
  • **Листинг версий использует зеркало `dl.gitea.com`**, а не выведенный из строя эндпоинт API `gitea.com`, который начал возвращать 500 и ломал выбор версии в TUI/CLI.
  • **Обновлён встроенный подписывающий ключ Gitea (Teabot)**, у которого истёк signing-подключ (2026-06-23), из-за чего перестала работать GPG-верификация скачанных бинарей Gitea. Продлён до 2027-06-26; тот же закреплённый fingerprint.
Подробнее →
v0.6.3 2026-05-30
Fixed
  • Релизный пайплайн теперь берёт каждый бинарник из реального пути сборки и отказывается публиковать отсутствующий или пустой файл — первопричина пустых загрузок в v0.6.0–v0.6.2.
Подробнее →
v0.6.2 2026-05-30
Fixed
  • gtcnsl --version теперь работает и печатает ту же строку, что и gtcnsl version.
  • self-update больше не откатывается всегда — его проверка версии после замены падала из-за отсутствующего флага --version.
  • Релизные бинарники сообщают версию с ведущей v — починены проверки self-update «уже последняя» и сверка тега.
Подробнее →
v0.6.1 2026-05-30
Fixed
  • Шаг зеркалирования больше не зависит от возможности shell, отсутствующей в релизном образе, — бинарники реально доезжают до dl.gtcnsl.com (v0.6.0 опубликовался в Gitea, но не был зазеркалирован).
Подробнее →
v0.6.0 2026-05-30
Added
  • gtcnsl self-update заменяет работающий бинарник на последний (или конкретный --to vX.Y.Z) релиз с dl.gtcnsl.com: загрузка → проверка SHA-256 → атомарная замена → проверка запуска → откат при любой ошибке. Флаги: --dry-run, --rollback, --yes, --dl-host.
  • Обнаружение и загрузка читают манифест versions.json с download-хоста и не вызывают Git API, поэтому self-update переживает уход хоста в приватность.
  • Экран Self-update в TUI (Check / Apply / Rollback) с тем же потоком, что и в CLI.
  • Релизный воркфлоу зеркалирует каждый опубликованный бинарник + checksums.txt на download-хост (источник истины — релиз в Gitea).
Changed
  • Релизные бинарники теперь называются gtcnsl-vX.Y.Z-linux-<arch> (с ведущей v) — по контракту download-хоста.
Подробнее →
v0.5.0 2026-05-24
Added
  • Интерактивный TUI: запустите gtcnsl без аргументов, чтобы управлять Gitea, Runner, Config, Secrets и Doctor из меню — все операции v0.1–v0.4, с цветными логами, прокруткой и спиннером.
  • gtcnsl config get / set / toggle — чтение или изменение одного ключа app.ini с тем же бэкап → атомарная запись → рестарт → откат по health-check, что и в декларативном потоке. Секретные и составные ключи отклоняются с подсказкой.
  • gtcnsl doctor — предполётные проверки хоста (systemd, исходящий HTTPS, диск, root) плюс проверки установленных Gitea и раннера; --fix --yes автоматически чинит частые проблемы (нет ca-certificates, нет секретов).
  • Каталог схемы конфига наполняет экраны конфигурации значениями по умолчанию, типами и описаниями ключей; gtcnsl config sync-schema подтягивает схему более новой Gitea.
Fixed
  • Запись конфига и секретов теперь сохраняет владельца файла после атомарного переименования — Gitea по-прежнему читает свой конфиг после apply (раньше файл мог остаться во владении root).
  • doctor --fix теперь корректно перепроверяет исходящий HTTPS в том же запуске после установки ca-certificates.
Подробнее →
v0.4.1 2026-05-23
Added
  • Каждый релиз зеркалируется в S3-совместимое хранилище рядом с релизом Gitea, с префиксом /latest/ и манифестом versions.json для будущего self-update.
  • Pre-release-теги (-rc / -beta / -alpha) получают версионную загрузку, но не попадают в /latest/ и versions.json — кандидат не становится дефолтной целью установки.
Подробнее →
v0.4.0 2026-05-23
Added
  • gtcnsl config apply --template — рендер и применение декларативного app.ini с подстановкой ${VAR}, diff, бэкапом, рестартом и автооткатом при провале health-check.
  • Управляемые секреты: gtcnsl secrets generate / check / rotate для четырёх ключевых секретов Gitea в /etc/gitea/secrets.ini; rotate переприменяет конфиг и откатывает оба файла при ошибке.
  • Встроенный каталог схемы конфига Gitea (значения по умолчанию, типы, описания), с gtcnsl config sync-schema для подтяжки более новой версии.
  • gtcnsl runner install --executor=podman (Tier 3, экспериментально).
Подробнее →
v0.3.1 2026-05-23
Added
  • Health-пробы исходящего трафика контейнеров и свободного места при настройке раннера, с готовыми командами-фиксами при провале (некритичные предупреждения).
  • gtcnsl runner reconfigure --admin-token подчищает осиротевшую регистрацию раннера на стороне Gitea после повторной регистрации.
  • docs/VPS-CHECKLIST.md — операторский чек-лист настройки gtcnsl-раннера на VPS.
Подробнее →
v0.3.0 2026-05-23
Added
  • gtcnsl gitea install / upgrade — установка последней Gitea (проверка GPG + SHA-256) с укреплённым systemd-юнитом и health-check после старта; атомарные апгрейды версий с откатом при ошибке.
  • gtcnsl runner install / register / reconfigure / upgrade для act_runner, с исполнителями host, docker и docker-rootless.
  • Поддержка пакетных менеджеров по дистрибутивам (apt / dnf) с детектом уже установленного Docker.
  • Один статический бинарник для amd64, arm64 и armv7, распространяемый через собственные релизы проекта в Gitea.
Подробнее →