diff --git a/RELEASING.md b/RELEASING.md index 060f17c92..5c8721738 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -44,7 +44,7 @@ both `vX.Y.Z` and `latest`. The image is the static binary in a image; see `Dockerfile.goreleaser`. The binary version is embedded at build time via `-ldflags -X -main.version=...`; `tailcat --version` prints it. Builds made with +main.version=...`; `tailcat version` prints it. Builds made with `go install github.com/tailscale/tailcat/cmd/tailcat@vX.Y.Z` instead report the module version from the Go build info. diff --git a/cmd/tailcat/e2e_test.go b/cmd/tailcat/e2e_test.go index 31209c546..4b7d16bc1 100644 --- a/cmd/tailcat/e2e_test.go +++ b/cmd/tailcat/e2e_test.go @@ -95,6 +95,22 @@ func buildTailcatBinary(dir string) error { return nil } +// TestVersionFlag verifies that "tailcat --version" works even though +// --version is not a registered flag; main special-cases it on parse +// failure. The advertised interface is the version subcommand, but +// nixpkgs' versionCheckHook runs "tailcat --version" and depends on +// its exit status and output. +func TestVersionFlag(t *testing.T) { + bin := buildTailcat(t) + out, err := exec.Command(bin, "--version").CombinedOutput() + if err != nil { + t.Fatalf("tailcat --version: %v\n%s", err, out) + } + if v := strings.TrimSpace(string(out)); v == "" || strings.Contains(v, "\n") { + t.Errorf("output = %q; want a single non-empty version line", out) + } +} + // testNoopCommand returns a child command that exits successfully, // for tests that only care that a wrapper ran it. Windows has no // "true" binary. diff --git a/cmd/tailcat/tailcat.go b/cmd/tailcat/tailcat.go index 1377939bc..e8e66e6ac 100644 --- a/cmd/tailcat/tailcat.go +++ b/cmd/tailcat/tailcat.go @@ -54,7 +54,6 @@ var ( flagKey *string flagAllow *string flagVerbose *bool - flagVersion *bool flagFullAddress *bool flagJSON *bool flagDERPMapURL *string @@ -89,7 +88,6 @@ func newRootCommand() *ff.Command { flagServe = rootFS.StringLong("serve", "", "comma-separated list of port numbers, port ranges, or service names to serve; the same list the serve subcommand takes as arguments. Service names are: 'all' (serve all ports), 'exit-node' (run an exit node for all addresses), 'no-auth-ssh' (auth-free SSH server). If empty, it accepts a single connection on any port, writes it to stdout, and exits.") flagKey = rootFS.StringLong("key", "", "'new' for an ephemeral key. If empty, the default saved key is used if it exists ('default' in server mode, 'client-default' in client modes; see genkey), else an ephemeral key. Otherwise the path to a *.private.json or a name like 'foo' to read it from $CONFIG/tailcat/keys/foo.private.json") flagVerbose = rootFS.BoolLong("verbose", "be verbose") - flagVersion = rootFS.BoolLong("version", "print the tailcat version and exit") flagJSON = rootFS.BoolLong("json", "in server mode, write {\"listenAddr\": ...} JSON to stdout") flagDERPMapURL = rootFS.StringLong("derpmap-url", tailcat.DefaultDERPMapURL, "URL of the JSON DERP map used to resolve or auto-select a DERP region") @@ -202,7 +200,7 @@ func newRootCommand() *ff.Command { { Name: "version", Usage: "tailcat version", - ShortHelp: "print the tailcat version (like --version)", + ShortHelp: "print the tailcat version", Exec: func(ctx context.Context, args []string) error { fmt.Println(versionString()) return nil @@ -497,13 +495,16 @@ func usagef(format string, args ...any) error { func main() { root := newRootCommand() err := root.Parse(os.Args[1:]) + if err != nil && slices.Contains(os.Args[1:], "--version") { + // The advertised way to get the version is the version + // subcommand, but nixpkgs' versionCheckHook runs "tailcat + // --version", so keep that working as an unadvertised alias. + // It's not a registered flag, so it only shows up here as a + // parse failure. + fmt.Println(versionString()) + return + } if err == nil { - // The --version flag short-circuits everything else, - // including subcommand dispatch. - if *flagVersion { - fmt.Println(versionString()) - return - } if *flagVerbose { tailcat.Verbose = true }