Report development builds as such, not as the release version

Only a release build (the commit tagged v<VERSION>, unchanged) gives a
version number. Others print 'dev <commit> (3 commits after v0.2.0-rc.1)',
from git describe (the Makefile's DESCRIBE), in jarld version,
jarl --version, jarl status, jarl whoami and the startup log; a build
without git says '(source build)'. The CLI's User-Agent and the footer's
tooltip follow suit, and /status's build gains since and ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-09 15:28:13 +00:00
1 parent 88bb89a9ff
commit fb1ee09250
7 files changed
+123 -23

No files matched your search

+7 -3
View File
@@ -88,9 +88,13 @@ All notable changes to Jarl are documented here. The format follows
linking to the release on Gitea. A build that isn't of a release tag linking to the release on Gitea. A build that isn't of a release tag
shows its commit instead, linked to the commit and marked `-modified` shows its commit instead, linked to the commit and marked `-modified`
with uncommitted changes. Before, every build of a commit after a with uncommitted changes. Before, every build of a commit after a
release still showed the release's version. `jarld version`, release still showed the release's version. `GET /status` and
`jarl --version`, `GET /status` and `/auth/whoami` report the same, the `/auth/whoami` report the build as `build` (docs/development.md).
latter two as `build` (docs/development.md). - `jarld version`, `jarl --version`, `jarl status` and `jarl whoami` give
a version number only for release builds. Other builds say they are
development builds and how far past the last tag they are: `dev
ca0f148b78b6 (3 commits after v0.2.0-rc.1)`. In 0.2.0-rc.1 they still
printed `0.2.0-rc.1 (ca0f148b78b6)`.
- The view in the zone list, and on the zone page, links to that view, - The view in the zone list, and on the zone page, links to that view,
and the views page highlights it. It used to be plain text, and the zone and the views page highlights it. It used to be plain text, and the zone
page linked to the views page as a whole. page linked to the views page as a whole.
+5 -1
View File
@@ -18,7 +18,11 @@ VERSION ?= $(shell cat VERSION 2>/dev/null || echo 0.0.0-dev)
# the release rather than the commit): set when HEAD is tagged v$(VERSION) # the release rather than the commit): set when HEAD is tagged v$(VERSION)
# and no tracked file is changed. # and no tracked file is changed.
RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t) RELEASE_TAG ?= $(shell t=v$(VERSION); git tag --points-at HEAD 2>/dev/null | grep -qx "$$t" && git diff --quiet HEAD 2>/dev/null && echo $$t)
LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION) -X jarl/internal/version.Tag=$(RELEASE_TAG) # DESCRIBE places other builds relative to the last tag ("3 commits after
# v0.2.0-rc.1").
DESCRIBE ?= $(shell git describe --tags --long --match 'v[0-9]*' --abbrev=12 HEAD 2>/dev/null)
LDFLAGS := -s -w -X jarl/internal/version.Version=$(VERSION) -X jarl/internal/version.Tag=$(RELEASE_TAG) \
-X jarl/internal/version.Describe=$(DESCRIBE)
GOFLAGS := -trimpath GOFLAGS := -trimpath
export CGO_ENABLED := 0 export CGO_ENABLED := 0
+12 -6
View File
@@ -232,17 +232,23 @@ runs it as a check before building.
### Which version a build reports ### Which version a build reports
The footer of the web UI, `jarld version` and `jarl --version` say what a The footer of the web UI, `jarld version`, `jarl --version`, `jarl status`
binary was built from, linking to it on Gitea: and `jarl whoami` say what a binary was built from (the footer links to
it on Gitea). Only a release build reports a version number:
* **A release**: `make` run on the commit tagged `v<VERSION>` with no * **A release**: `make` run on the commit tagged `v<VERSION>` with no
tracked file changed. The Makefile then sets `RELEASE_TAG`, and the tracked file changed. The Makefile then sets `RELEASE_TAG`, and the
footer reads "Powered by Jarl v0.2.0", linking to the release. The footer reads "Powered by Jarl v0.2.0", linking to the release. The
release workflow sets `RELEASE_TAG` itself. release workflow sets `RELEASE_TAG` itself.
* **Any other build**: the commit, which Go records from git: "Powered by * **Any other build** is a development build, named by its commit (which
Jarl 1a2b3c4d5e6f", linking to the commit. With uncommitted changes it Go records from git) rather than `VERSION`: "Powered by Jarl
is marked `-modified`. The version commands print `0.2.0 (1a2b3c4d5e6f)`. 1a2b3c4d5e6f", linking to the commit. With uncommitted changes it is
* **No git** (a build from a source tarball): just `VERSION`. marked `-modified`. The other places add where it stands relative to
the last tag, from `git describe` (the Makefile's `DESCRIBE`):
`dev 1a2b3c4d5e6f (3 commits after v0.2.0-rc.1)`, or
`dev 1a2b3c4d5e6f-modified (v0.2.0-rc.1 with uncommitted changes)`.
* **No git** (a build from a source archive): `VERSION`, marked as such:
`0.2.0 (source build)`.
`RELEASE_TAG=v0.2.0 make` forces the first case. `RELEASE_TAG=v0.2.0 make` forces the first case.
+1 -1
View File
@@ -183,7 +183,7 @@ func (c *Client) Do(ctx context.Context, method, path string, query url.Values,
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "jarl-cli/"+version.Version) req.Header.Set("User-Agent", "jarl-cli/"+version.Short())
if c.Token != "" { if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token) req.Header.Set("Authorization", "Bearer "+c.Token)
} }
+65 -9
View File
@@ -4,12 +4,17 @@
// //
// The Makefile sets Version from the VERSION file, and Tag only when the // The Makefile sets Version from the VERSION file, and Tag only when the
// build is of that release: HEAD is the tag v<VERSION> and no tracked file // build is of that release: HEAD is the tag v<VERSION> and no tracked file
// is changed. Other builds are identified by their commit, which Go // is changed. Only such a build reports a version number. Other builds
// records from git by itself. // are development builds, identified by their commit (which Go records
// from git by itself) and by where they stand relative to the last tag
// (Describe, from git describe).
package version package version
import ( import (
"regexp"
"runtime/debug" "runtime/debug"
"strconv"
"strings"
"sync" "sync"
) )
@@ -23,6 +28,11 @@ var Tag = ""
// information when not set at build time. // information when not set at build time.
var Commit = "" var Commit = ""
// Describe is 'git describe --tags --long' of the build's commit
// ("v0.2.0-rc.1-3-gca0f148b78b6"), set by the Makefile; it says how many
// commits a development build is past the last tag.
var Describe = ""
// Repo is Jarl's source repository. // Repo is Jarl's source repository.
const Repo = "https://gitea.adbraun.lol/adam/jarl" const Repo = "https://gitea.adbraun.lol/adam/jarl"
@@ -32,6 +42,11 @@ type Build struct {
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`
Commit string `json:"commit,omitempty"` Commit string `json:"commit,omitempty"`
Modified bool `json:"modified,omitempty"` // built with uncommitted changes Modified bool `json:"modified,omitempty"` // built with uncommitted changes
// Since is the last tag before a development build's commit, and
// Ahead how many commits the build is past it (0: the tag's commit,
// with uncommitted changes).
Since string `json:"since,omitempty"`
Ahead int `json:"ahead,omitempty"`
// Label is the short form to show: the tag of a release, otherwise // Label is the short form to show: the tag of a release, otherwise
// the abbreviated commit (or the version when the commit is unknown). // the abbreviated commit (or the version when the commit is unknown).
Label string `json:"label"` Label string `json:"label"`
@@ -53,9 +68,23 @@ var info = sync.OnceValue(func() Build {
} }
} }
} }
return describe(Version, Tag, commit, modified) b := describe(Version, Tag, commit, modified)
b.Since, b.Ahead = parseDescribe(Describe, b.Commit)
return b
}) })
// parseDescribe splits git describe --long output, if it is of commit.
func parseDescribe(d, commit string) (since string, ahead int) {
m := describeRe.FindStringSubmatch(d)
if m == nil || commit == "" || !strings.HasPrefix(commit, m[3]) {
return "", 0
}
n, _ := strconv.Atoi(m[2])
return m[1], n
}
var describeRe = regexp.MustCompile(`^(v.+)-(\d+)-g([0-9a-f]+)$`)
func describe(version, tag, commit string, modified bool) Build { func describe(version, tag, commit string, modified bool) Build {
b := Build{Version: version, Tag: tag, Commit: commit, Modified: modified} b := Build{Version: version, Tag: tag, Commit: commit, Modified: modified}
switch { switch {
@@ -76,14 +105,41 @@ func describe(version, tag, commit string, modified bool) Build {
// Info describes this build. // Info describes this build.
func Info() Build { return info() } func Info() Build { return info() }
// String is the version for logs and 'version' commands: "0.2.0" for a // String is the version for logs and 'version' commands. Only a release
// release, otherwise the version and the commit ("0.2.0-dev (a1b2c3d4e5f6)"). // build gives a version ("0.2.0"); any other build says it's a
func String() string { // development build: "dev a1b2c3d4e5f6 (3 commits after v0.2.0-rc.1)".
b := Info() func String() string { return Info().String() }
if b.Tag != "" || b.Commit == "" {
func (b Build) String() string {
switch {
case b.Tag != "":
return b.Version return b.Version
case b.Commit == "":
// No git information (built from a source archive).
return b.Version + " (source build)"
} }
return b.Version + " (" + b.Label + ")" s := "dev " + b.Label
switch {
case b.Since == "":
case b.Ahead == 1:
s += " (1 commit after " + b.Since + ")"
case b.Ahead > 1:
s += " (" + strconv.Itoa(b.Ahead) + " commits after " + b.Since + ")"
case b.Modified:
s += " (" + b.Since + " with uncommitted changes)"
default:
s += " (at " + b.Since + ")"
}
return s
}
// Short is the version for a User-Agent: the version of a release,
// otherwise the label ("a1b2c3d4e5f6").
func Short() string {
if b := Info(); b.Tag == "" && b.Commit != "" {
return b.Label
}
return Version
} }
func short(commit string) string { func short(commit string) string {
+27
View File
@@ -20,3 +20,30 @@ func TestDescribe(t *testing.T) {
} }
} }
} }
func TestString(t *testing.T) {
const c = "ca0f148b78b6e1f0a9b8c7d6e5f4a3b2c1d0e9f8"
build := func(tag, commit string, modified bool, desc string) Build {
b := describe("0.2.0-rc.1", tag, commit, modified)
b.Since, b.Ahead = parseDescribe(desc, b.Commit)
return b
}
for _, tc := range []struct {
b Build
want string
}{
{build("v0.2.0-rc.1", c, false, "v0.2.0-rc.1-0-gca0f148b78b6"), "0.2.0-rc.1"},
{build("", c, false, "v0.2.0-rc.1-3-gca0f148b78b6"), "dev ca0f148b78b6 (3 commits after v0.2.0-rc.1)"},
{build("", c, true, "v0.2.0-rc.1-1-gca0f148b78b6"), "dev ca0f148b78b6-modified (1 commit after v0.2.0-rc.1)"},
{build("", c, true, "v0.2.0-rc.1-0-gca0f148b78b6"), "dev ca0f148b78b6-modified (v0.2.0-rc.1 with uncommitted changes)"},
{build("", c, false, "v0.1.0-0-gca0f148b78b6"), "dev ca0f148b78b6 (at v0.1.0)"},
{build("", c, false, ""), "dev ca0f148b78b6"},
// A describe of another commit (stale ldflags) is ignored.
{build("", c, false, "v0.2.0-rc.1-3-gdeadbeef0000"), "dev ca0f148b78b6"},
{build("", "", false, ""), "0.2.0-rc.1 (source build)"},
} {
if got := tc.b.String(); got != tc.want {
t.Errorf("String() = %q, want %q", got, tc.want)
}
}
}
+6 -3
View File
@@ -173,10 +173,13 @@ let modeHolder;
// builds, linked to it in the repository. // builds, linked to it in the repository.
function footer(b) { function footer(b) {
if (!b) return null; if (!b) return null;
const what = b.tag ? "release " + b.tag let title = b.tag ? "Jarl " + b.version + " (release " + b.tag + ")" : "Jarl " + b.version + " (built without git)";
: b.commit ? "commit " + b.commit + (b.modified ? ", with uncommitted changes" : "") : "version " + b.version; if (!b.tag && b.commit) {
title = "Development build of commit " + b.commit + (b.modified ? " with uncommitted changes" : "");
if (b.since) title += b.ahead ? ", " + b.ahead + (b.ahead === 1 ? " commit" : " commits") + " after " + b.since : ", at " + b.since;
}
return h("footer", { class: "site" }, "Powered by ", return h("footer", { class: "site" }, "Powered by ",
h("a", { href: b.url, target: "_blank", rel: "noopener", title: "Jarl " + b.version + ", built from " + what }, "Jarl " + b.label)); h("a", { href: b.url, target: "_blank", rel: "noopener", title }, "Jarl " + (b.tag || b.commit ? b.label : b.version)));
} }
function renderNav() { function renderNav() {