v1.5.0
2026-08-06
latest
● stable
What changed
Added
- **openSUSE Leap 16 joins the supported matrix — the first zypper-based family.** `internal/pkgmgr` gained `FamilySUSE`/`DistroOpenSUSE`: `Install` runs `zypper --non-interactive refresh` then `zypper --non-interactive install` (unlike RHEL-family's implicit dnf refresh, openSUSE's default repos ship `autorefresh=0`, so the explicit refresh step is required, not optional), `IsInstalled` shares RHEL-family's `rpm -q` (both are RPM-based), and an unrecognised SUSE derivative whose `/etc/os-release` `ID_LIKE` contains `suse` now resolves to the right family via the same fallback Rocky/AlmaLinux clones already use. Docker installs straight from Leap's default repo (`docker`, no third-party repo needed, unlike RHEL-family's Docker CE repo dance); Podman is in the same whitelist as Debian/Ubuntu/Rocky/AlmaLinux; docker-rootless is refused on this family too, symmetric with the RHEL-family refusal (ADR 0040) — an implementation would need SUSE's own rootless-extras package, out of scope here. `gtcnsl doctor`'s CA-bundle probe gained `/etc/ssl/ca-bundle.pem` (SUSE family) as a third candidate path alongside the existing Debian and RHEL ones. See ADR 0043.
- **Alpine Linux joins the supported matrix — host-mode only.** gtcnsl now runs on Alpine via a second, honest `service.Manager` backend (the init-agnostic abstraction described under Changed, below): `internal/openrc` drives `rc-service`/`rc-update` and deploys supervise-daemon-based `/etc/init.d` scripts gtcnsl renders itself (`internal/gitea`/`internal/runner` gained `RenderOpenRCUnit` alongside their existing systemd templates). `internal/app` picks the backend automatically via `service.Detect()` — no flag, no config. `internal/pkgmgr` gained `FamilyAlpine` (`apk update` + `apk add`, `apk info -e` for install checks); `internal/osuser` gained a BusyBox `adduser`/`addgroup` branch for hosts with no `useradd`/`usermod`. `gtcnsl doctor` is init-aware: an OpenRC host now PASSes the init-system check instead of hard-failing as "systemd not running". **MVP scope, by design (ADR 0020/0045): host-mode Gitea and host-mode runner only.** The `docker`, `docker-rootless`, `podman`, and `dind-rootless` runner executors all refuse cleanly on Alpine with an explanation (no systemd-logind session manager for the rootless/dind-rootless setups those executors need) rather than failing unpredictably partway through. See ADR 0020/0044/0045.
- **Downloads now recover from a stalled connection instead of hanging forever.** Every download path — Gitea install/upgrade, gitea-runner install/upgrade, and gtcnsl's own self-update, all of which share `internal/download`'s single implementation — now watches for a response that stops producing bytes and, after 30s of silence, aborts that attempt and retries (up to 3 attempts total) with a fresh connection, resuming via `Range` when the server supports it instead of starting over. A download that ultimately can't be recovered now fails with a clear "giving up after N attempts" error instead of hanging. Version listing requests (available Gitea/runner versions, the self-update manifest) now also carry a 15s timeout, so a stalled connection can no longer hang the "loading available versions" screen either. See ADR 0041.
- **The TUI's operation screen now shows whether anything is actually happening.** A live spinner runs next to the active stage for the whole operation (not just before the first event), Progress events render a percentage plus human-readable volumes when the size is known (`download: 46% (19.9 MiB / 42.7 MiB)`), and the download-resilience retry notice above ("retrying (attempt 2/3) from byte 307200…") now reads as a warning instead of blending into the log.
- **Esc on a running TUI operation is honest about what it does.** Pressing Esc once now shows `operation still running (holds the gtcnsl lock): c cancel · esc hide` instead of silently leaving while the operation keeps running underneath — the exact confusion behind a production incident where the operator saw "another gtcnsl operation is already in progress" with no idea why. `c` cancels: the download stage stops immediately (built on the per-attempt cancellable context the download-resilience entry above introduced, ADR 0041), other stages stop at the next safe boundary between stages, and an already-started mutating stage (binary swap, unit restart) always finishes atomically rather than being torn mid-way. A second Esc hides the screen and returns to the dashboard, which shows an "operation in progress…" indicator until the hidden operation finishes. Cancelling never crashes the TUI — the screen reads "Cancelled", not a raw error. See ADR 0042.
- **The TUI's version picker recovers from a failed/timed-out version list instead of spinning forever.** On an error it now offers `r` to retry the load in addition to the existing manual-entry fallback.
Changed
- **Internal refactor: service management now goes through an init-agnostic `internal/service.Manager` instead of `systemd.Manager` directly.** `internal/core` used to call `internal/systemd` for every service operation, unconditionally. It now programs against `service.Manager` — the operations any init system can do (start/stop/ restart/enable/deploy unit/reload) — plus three optional capability interfaces (`DropInCapable`, `LingerCapable`, `UnitInspector`) for the systemd-only features (drop-in overlays, session lingering, live unit introspection), gated by an honest refusal when a backend lacks one. `internal/systemd` is unmoved and unrenamed — it is simply, now, the (only, still) backend implementing this abstraction. **No behaviour changes**: this is preparation for Alpine/OpenRC support, not the Alpine support itself — every existing test passes unmodified. See ADR 0044 (refines ADR 0020).
Fixed
- **A stalled Gitea/CDN connection could hang an upgrade indefinitely.** Traced to a production incident where Go's HTTP/2 client wedged against a Cloudflare-fronted `dl.gitea.com` download: the connection stayed established with zero throughput and no error, so nothing ever timed out. gtcnsl's download and version-discovery clients now force HTTP/1.1, which reproducibly avoided the wedge in the incident and, per ADR 0041, is dedicated to just these clients — the Gitea admin API and health-check clients are unaffected. (The Range-resume above covers the remaining "sometimes networks just blip" case on top of this.)
- An interrupted download (e.g. `SIGTERM` mid-transfer) could leave a `gtcnsl-download-*` temp file behind; download now always removes it on any non-success outcome, including a cancelled context.
Download
Pick your architecture. Direct links point at the release assets.
$ curl -fsSL https://dl.gtcnsl.com/v1.5.0/gtcnsl-v1.5.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