Изменения
Значимые изменения gtcnsl, новые сверху. Формат — Keep a Changelog; версии — по Semantic Versioning.
Keep a Changelog · SemVer
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.