Skip to content
gtcnsl
EN RU

v1.3.0

2026-07-18 latest ● stable

What changed

Added
  • **Gitea 1.27.x and Runner 2.x are verified and supported, and `--reencrypt` is now gated by a version envelope.** The SECRET_KEY re-encryption crypto (`internal/gitea/secretcrypto`/`secretdb`) was re-verified byte-for-byte against the v1.27.0 Gitea sources — both schemes, all five encrypted sites, no new call sites — and the embedded config-schema baseline moved from 1.26.2 to 1.27.0 (9 sections unchanged, 764 → 772 keys); `scripts/update-gitea-schema`, which `docs/ARCHITECTURE.md` had referenced since ADR 0015 without it ever existing, now actually does. That re-encryption crypto is not a public Gitea contract — an upstream minor could change it without a changelog mention — so `secrets rotate SECRET_KEY --reencrypt` now hard-refuses, before touching anything, against any Gitea minor outside the verified envelope (currently `[1.24, 1.27]`) rather than risk a repeat of the silent-corruption incident v1.2.0 grew from; the explicit, separate override is `--reencrypt-unverified-version-i-understand` (ADR 0028). COMPATIBILITY.md now states the supported upstream range outright (Gitea through 1.27.x, Runner ≥1.0 incl. 2.x); Runner 2.x's breaking change (implicit `DOCKER_USERNAME`/`DOCKER_PASSWORD` env vars dropped) does not affect gtcnsl-generated configs.
  • **`gtcnsl runner register`/`runner reconfigure --token-file <path>`.** New flag, mutually exclusive with `--token`: gtcnsl reads and trims the file itself, and on a detected `gitea-runner` >= 2.1.0 (the release that added upstream `--token-file` support) never places the token in argv — it writes it to a gtcnsl-owned 0600 temp file inside the runner's WorkDir, passes `--token-file` to the `gitea-runner register` subprocess, and removes the temp file regardless of outcome. A token in argv is readable by anyone on the host for the process's lifetime (`/proc/<pid>/cmdline`, `ps`); this closes that window for runners new enough to support it. Runners below 2.1.0 keep the pre-existing `--token` argv behaviour — a documented fallback, not a silent failure. The dind-rootless executor was already env-file-based (ADR 0022) and is unaffected either way (ADR 0030).
  • **`gtcnsl runner register`/`reconfigure` now generate a baseline `config.yaml`, and `gtcnsl runner config generate [--force]` (re)writes it on demand.** Runner 2.x added a batch of new config.yaml keys (`runner.post_task_script`, `action_shallow_clone`, `container.network_create_options.enable_ipv4/6`, and more) that gtcnsl previously gave no way to see or set — `runner register` had only ever written `.runner`, so every installed runner ran on gitea-runner's undocumented built-in defaults. Registration/reconfiguration now write a baseline via the installed binary's own `generate-config` subcommand (atomic, 0640, chowned) — but only if `config.yaml` isn't already there; an existing file (operator-edited or from a previous register) is never touched. `gitea-runner.service`'s ExecStart gains `--config <WorkDir>/config.yaml`, but strictly monotonically: only once that file is confirmed present at unit-render time, so an in-place upgrade of a pre-this-feature install can never point the unit at a file that doesn't exist yet (ADR 0031). Declarative apply of individual config.yaml keys is deliberately deferred; `runner config` is the placeholder parent command for it.
  • **`gtcnsl gitea upgrade [--to <v>] [--yes]` and `gtcnsl runner upgrade [--to <v>] [--yes]`** — the `upgrade` verb README.md and COMPATIBILITY.md already documented, now wired as real commands (ADR 0035). Both are thin wrappers, not new machinery: `gitea upgrade` refuses, before touching anything, when there is nothing installed yet, otherwise delegates the entire flow to the same in-place-upgrade path `gitea install --to <version>` has used since feature 0067 (live paths, foreign-unit preservation, health check); `runner upgrade` wires the CLI straight onto `internal/core.UpgradeRunner` (feature 0014), whose preflight already refused an absent install and which already follows a foreign or legacy-named unit's live binary path (feature 0080, ADR 0034). Flags are deliberately minimal (`--to`/`--yes` only) — `gitea install --to` remains the fuller-flagged path for `--data-dir`/`--rewrite-unit`/`--serving` during an in-place upgrade.
  • **`gtcnsl runner adopt` — bring an existing Gitea Actions Runner under management, never re-registering it.** The runner twin of `gitea adopt` (ADR 0032): registration tokens are typically one-time-use, so unlike `runner reconfigure` this never touches `.runner` beyond tightening its mode to 0640 when it is looser (the file also carries the registration token — never logged, never rewritten, never moved). Dry-run by default; `--yes` applies. Recognizes both the current `gitea-runner.service` naming and the legacy pre-rename `act_runner.service` naming (ADR 0013/0018), reading the binary/work-dir/user straight from the live unit's own directives rather than gtcnsl's defaults — and — thanks to this same release's unified live-unit inspection (ADR 0034, see Fixed below) — a later `runner install --to`/`runner upgrade` recognizes the adopted install and acts on those same live paths. A unit gtcnsl did not write is preserved byte-for-byte by default (ADR 0025); `--rewrite-unit` replaces its content with gtcnsl's template rendered from the live values, keeping its existing filename (no renaming). dind-rootless installs are refused as out of scope (ADR 0022).
  • **Runner state now travels with `gtcnsl backup`/`restore`.** `.runner` registration tokens are typically one-time-use, so losing the file used to mean a fresh token and a lost runner UUID. `backup` now additionally, and optionally (Warning + skip when absent, same as `secrets.ini`/`custom/` today), captures `<WorkDir>/.runner` (secret-class — its token never appears in events/logs), `config.yaml`, the `gitea-runner.service` unit (read through the injected systemd manager, never a hardcoded filesystem path), and dind-rootless's `registration.env` — the last with an explicit warning and manifest note that it alone cannot fully restore a dind-rootless runner (the `gitea-runner-data` Docker volume stays out of scope, ADR 0022). The manifest schema bumps 1 → 2; a schema-1 backup restores exactly as before, and an old gtcnsl still refuses a schema-2 manifest. `restore` stops `gitea-runner.service` before applying its files and starts it again only after Gitea's own health check passes, tolerating "unit not installed"/"not active" as a Warning rather than a Failed (ADR 0033).
Fixed
  • **`secrets rotate SECRET_KEY` — the rotation guard, `--reencrypt`, and `doctor`'s weak-key probe now honour `SECRET_KEY_URI` end-to-end.** Gitea has supported sourcing `[security] SECRET_KEY` from a file via `SECRET_KEY_URI` since 1.19, but gtcnsl read only the verbatim `SECRET_KEY` value everywhere: on a URI-configured instance the guard and `doctor` silently treated Gitea's public default as "the old key" (a false read), and — worse — a plain rotation wrote the new key into `secrets.ini` without ever touching the file Gitea actually reads, leaving the instance running the *old* key while gtcnsl's own bookkeeping claimed the new one. That is the same silent-divergence failure class the v1.2.0 incident this project grew from, and it made `--reencrypt` on such a host a guaranteed health-check rollback (new ciphertext, old running key). gtcnsl now resolves `SECRET_KEY`/`SECRET_KEY_URI` exactly the way Gitea does at boot (ported line-for-line from `modules/setting/security.go`, fail-closed instead of Gitea's own `log.Fatal` on both-set or an unresolvable URI) and, on rotation, atomically rewrites the URI-backed file itself — with its own timestamped backup and rollback in every failure branch — in addition to `secrets.ini` (ADR 0029). A verbatim-configured instance's rotation is unaffected.
  • **A failed `gitea upgrade` (or `gitea install --to` over an existing install) now restores the previous Gitea binary instead of leaving the host without one.** The in-place upgrade path snapshots the live binary before swapping it and, on any failure after the swap (unit deploy, service restart, health check), restores it and restarts the service — the same snapshot-based rollback `runner upgrade` already had (ADR 0036, closing the asymmetry ADR 0035 documented as an open trade-off). A pre-existing unit file is also never deleted by a failed upgrade's rollback anymore.
  • **`runner install`/`upgrade`/`register`/`reconfigure` now follow the live unit's own facts** — unit name (including the legacy pre-rename `act_runner.service`), binary path and work dir are read from the actual systemd unit instead of assuming gtcnsl's canonical layout (ADR 0034). Before this, an adopted legacy or relocated install was invisible to `runner install --to`, and `--rewrite-unit` would create a second canonical unit next to the existing one. Both the canonical and legacy unit present at once is now a hard, clearly-worded inspection error.

Download

Pick your architecture. Direct links point at the release assets.

$ curl -fsSL https://dl.gtcnsl.com/v1.3.0/gtcnsl-v1.3.0-linux-amd64 -o /usr/local/bin/gtcnsl && chmod +x /usr/local/bin/gtcnsl
i
Verify
Compare against the checksum manifest before you run anything. checksums.txt