The pre-commit hooks kept their own list of formatters, so nixpkgs-fmt in
a hook and nixfmt in the check disagreed the moment flake-checks moved.
`nix fmt` is now built from the same treefmt module the formatting check
is, and the hook and the Makefile both call it.
gateFlake judges the formatting check right after the lock moves, but the
fmt area runs last, so the new toolchain's formatting had not been applied
yet and the flake area dropped itself every run.
flake-checks moved its treefmt Nix formatter to nixfmt, so every .nix file
failed the formatting check. The devShell and the pre-commit hook shipped
nixpkgs-fmt, which reformatted them straight back.
flake-checks moved its nix formatter from nixpkgs-fmt to nixfmt, so every
.nix file failed the formatting check. gateFlake only ran `nix eval`, so
that reached the final gate, which drops the newest commit first and had
to pop all eleven areas above it to get there. Three hours, nothing
shipped.
prettier has no TOML parser, so `prek run --all-files` failed on
.mdformat.toml with "No parser could be inferred". The Makefile's own
prettier glob never included toml.
golang.org/x/net deprecated http2.Server, staticcheck failed on
hscontrol/noise.go, and the whole batch of 50 direct requirements was
dropped for it. The bisect tries every atom together first, so one atom
per module costs nothing until something actually breaks.
nixpkgs decides which prettier and gofumpt format the tree. A lock bump to
prettier 3.9.6 reformatted CHANGELOG.md, which no area had touched, and the
formatting check failed on a file the bot never edited.
go env GOVERSION reports the toolchain the go command switched to, so a
go.mod that had outrun nixpkgs read back as a nixpkgs that had caught
up, and checkToolchain compared 1.27.1 against itself.
tailscale.com@main raised the go directive to 1.27.1 while nixpkgs was
still on 1.27.0. go build downloads the newer toolchain and looks fine;
the nix builders set GOTOOLCHAIN=local and fail, so the whole bump was
being thrown away one area at a time.
The oapi-codegen pin stays the single source of truth: the generate area
reads it back out of the Makefile, so the clients are regenerated with
whatever this lands on.
Debian is resolved from the numeric tags, which exist only for released
versions: forky-slim is published today and is testing. Distroless
follows that same release rather than its own repository names, since
gcr answers for base-debian99 as readily as for base-debian13.
Keeps the interlocked pins current and reports what it could not move.
Areas apply, gate and commit one at a time, so a dependency that breaks
the build costs one commit rather than the whole pull request.
Self-hosted-runner shape (repositories, platform, gitAuthor) that a
repo-level config cannot set, and nothing has run it since the workflow
was removed in 6a311f4ab.
Both stages compile a tailscale tree cloned from an unpinned branch. The
golang images set GOTOOLCHAIN=local, so the moment upstream raises its go
directive the build fails outright instead of fetching a toolchain.
Neither file was listed in any paths-filter, so a lockfile-only change
skipped build, check-generated, check-tests and the whole integration
matrix. Also drops integration_test/, which has never existed.
#3472 and #3487 merged without a changelog entry, and the #3409 and
peer map entries landed under 0.30.0 although they ship in 0.29.4.
Sets the release date.
Sub-builders called after validateServerConfig (prefixV4, prefixV6,
allocation strategy, dns, oidc client secret/path, isSafeServerURL)
each returned the first error. So an operator that fixed one
issue saw the next one only on the next startup.
Lift one *configValidator across the entire LoadServerConfig flow.
validateServerConfigInto(v) populates it; each sub-builder's error
is wrapped in a structured *ConfigError (with the original sentinel
kept on the Cause field, so errors.Is against errOidcMutuallyExclusive,
errServerURLSame/Suffix, ErrNoPrefixConfigured, and
ErrInvalidAllocationStrategy keeps working). v.Err() is checked once,
right before the &Config{} construction.
TestReadConfig/base-domain-in-server-url-err matched the old sentinel
wording; flipped to the new structured Reason. Added
TestLoadServerConfig_CollectsAcrossSubBuilders to lock the wiring:
four sub-builder failures from a single config produce four
*ConfigErrors in the report.
Updates #3227
deprecator.Log() called log.Fatal directly, so a single deprecated
key killed the process before any other validation rule could
report. Operators fixing one issue at a time.
Replace Log() with Apply(*configValidator). Warns still go to
log.Warn; fatals are pushed onto the validator as *ConfigError so
they merge with the rest of the config-load report. The free-form
strings collected via set.Set are gone; deprecator now stores
typed deprecation{OldKey, NewKey} records and Apply renders them
into the structured ConfigError shape.
Move the early-return PKCE check into a new validatePKCEConfig
modular validator for the same reason.
Updates #3227
A *ListenerBindError that wraps syscall.EADDRINUSE now ends with a
"sudo ss -tlnp 'sport = :PORT'" pointer, and one wrapping
syscall.EACCES with a CAP_NET_BIND_SERVICE / setcap pointer. The
underlying chain is preserved via fmt.Errorf("%w"), so errors.Is /
errors.As continue to walk to the typed bind error and the syscall
errno.
Drop the "headscale ran into an error and had to shut down" wrap,
which only restated the symptom.
Export types.PortFromAddr so the classifier can render the port
number in the hint.
Updates #3227
The HTTP-01 challenge listener was launched in an orphan goroutine that
called log.Fatal on bind failure, which bypassed the signal-handler
shutdown path and made bind errors look identical to the main HTTP
listener.
Bind eagerly via net.ListenConfig in getTLSSettings, return the listener
+ http.Server in a tlsBundle, register the Serve loop with the existing
errgroup, and call Shutdown alongside the other servers. Bind failures
now surface as ListenerBindError(Listener:"ACME HTTP-01 challenge")
through the normal error chain.
Updates #3227
Replace the three "binding to TCP address" wraps with typed
ListenerBindError values that carry the listener role, the YAML key
that drove the address, and the resolved address. The wrapped error
chain still walks to syscall.EADDRINUSE / EACCES so existing callers
that match those sentinels keep working; the difference is that the
operator now sees which listener failed instead of an unattributed
"binding to TCP address" line.
Updates #3227
ListenerBindError wraps a TCP listener bind failure with the listener
name and the YAML key that drove its address. Preserves the underlying
*net.OpError via Unwrap so errors.Is(err, syscall.EADDRINUSE) keeps
working through any number of fmt.Errorf("%w") wraps. Lets the
top-level CLI classifier match listener failures with errors.As
instead of string matching.
Updates #3227
derpConfig, databaseConfig, and dnsToTailcfgDNS used log.Fatal to
reject invalid combinations the moment they were observed. Lift those
checks into modular validators (validateDERPConfig,
validateDatabaseConfig, validateMagicDNSConfig) called from
validateServerConfig so each violation lands in the same configValidator
collector and renders as a structured ConfigError next to all other
config feedback. The sub-builder functions trust validation has run
and no longer crash the process.
Updates #3227
Convert the errorText accumulator inside validateServerConfig to the
typed configValidator pattern. Each rule violation now renders as a
multi-line block naming the YAML key, the value the operator wrote,
and a hint pointing at the resolution. The dns.extra_records mutex
that previously crashed via log.Fatal is folded into the same
collector so an operator sees every problem in one go instead of
fixing them one startup attempt at a time.
Test wantErr assertions on validation output switch to substring
match because the rendered errors are now multi-line.
Updates #3227
Reject configurations where two configured TCP listeners would bind
the same kernel socket. Covers every pair of listen_addr,
grpc_listen_addr, metrics_listen_addr, and tls_letsencrypt_listen
(when HTTP-01 ACME is in use). Each violation renders as a structured
ConfigError naming both YAML keys, the values the operator wrote, and
a hint pointing at the canonical setup.
Closes the symptom in #3227: the misconfiguration is now rejected at
config-load time with a self-explaining error, instead of failing at
runtime with a misleading "address already in use" log because the
ACME challenge listener and the main HTTP listener competed for the
same port inside the same process.
Fixes#3227
portFromAddr resolves numeric and named ports (":http", ":https")
without touching /etc/services or the resolver. listenersOverlap
follows kernel rules: same port plus a wildcard host on either side,
or same port plus identical specific host, both count as collision;
different specific hosts on the same port do not. Set an explicit
viper default for tls_letsencrypt_listen so a minimal config still
resolves to ":http".
Updates #3227
Foundation for restructuring config validation feedback. Typed error
with errors.Is/As/Join hooks, structured fields rendered as a
multi-line operator-facing block (current, conflicts with, allowed,
minimum, maximum, why, hint, see), and a configValidator that joins
violations via errors.Join. ConfigErrors walks the tree to collect
every *ConfigError, and Cause keeps existing sentinel identities
reachable through errors.Is.
Updates #3227
The filter lived in ACL.UnmarshalJSON, so grants, ssh and nodeAttrs still
hit RejectUnknownMembers. Strip the members in the HuJSON AST instead, at
the single decode entrypoint. Grant "app" payloads are left untouched.
Fixes#3479