feat: make it possible to see which version is actually running
Release / Build and publish image (push) Failing after 9m56s

"I have updated and restarted, but I cannot tell whether it worked" is
not a question the panel could answer. The version was on the startup
line and in the settings page, and nowhere you can reach from outside.

/healthz carries it now. That is the one endpoint answering without a
session, which makes it the only way to ask the question from a terminal
or from the panel itself. The "listening" line repeats it too, because
that is the line the Rune's done_regex matches and therefore the one
sitting at the bottom of the console after a deploy.

The update notice could also never have worked. It reads GitHub's
releases/latest, and this repository had only tags — a tag is not a
release, and that endpoint answers 404 for a repository without any. The
release workflow creates one now, after the image is pushed so a release
never announces a version that failed to build.

VERDANDE_UPDATE_CHECK is a Rune variable, so it can be switched on
without editing the container. Still off by default: it is an outbound
request, and that is a choice to make rather than a default to inherit.

Rune version 1 -> 2, so the panel offers the update.

Note that the check still cannot see anything while the repository is
private — releases/latest is 404 anonymously, exactly as the GHCR package
was. Making the repo public, or teaching the check to carry a token, is a
separate decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-17 14:35:36 +02:00
committed by Kristian Wind
co-authored by Claude Opus 5
parent 78ec9b08a1
commit 0dbea0ea4b
5 changed files with 43 additions and 5 deletions
+15 -1
View File
@@ -6,7 +6,9 @@ on:
tags: ["v[0-9]+.[0-9]+.[0-9]+*"]
permissions:
contents: read
# write rather than read: the job creates the GitHub Release the update notice
# reads. packages: write is for pushing the image.
contents: write
packages: write
jobs:
@@ -50,3 +52,15 @@ jobs:
VERSION=${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
# A tag is not a release. The in-app update notice reads GitHub's
# releases/latest, which answers 404 for a repository that only has tags —
# so without this step the check can never find anything, however correctly
# it is switched on.
#
# Created after the image, so a release never announces a version that
# failed to build.
- uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
make_latest: true
+5 -1
View File
@@ -77,7 +77,11 @@ func run() error {
errc := make(chan error, 1)
go func() {
log.Info("listening", "addr", cfg.Addr)
// The version is repeated here rather than only on the "starting" line
// above: this is the line the Rune's done_regex matches, so it is the one
// sitting at the bottom of the panel console after a deploy — which is
// exactly when somebody wants to know what actually started.
log.Info("listening", "addr", cfg.Addr, "version", version)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errc <- err
}
+9 -1
View File
@@ -1718,7 +1718,15 @@ paths:
broken volume reports unhealthy instead of pretending to work.
security: []
responses:
"200": { description: Healthy. }
"200":
description: Healthy. Carries the running version — the one thing you can ask without a session.
content:
application/json:
schema:
type: object
properties:
status: { type: string, const: ok }
version: { type: string, example: v0.3.1 }
"503": { description: Unhealthy. }
/ics/{token}:
+5 -1
View File
@@ -348,7 +348,11 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
// The version travels with the health check because this is the one endpoint
// that answers without a session. "Which version is actually running?" is
// otherwise a question you cannot ask from outside — and after a deploy it is
// the first question there is.
writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "version": Version})
}
// serveWeb serves the PWA, falling back to index.html so that client-side routes
+9 -1
View File
@@ -11,7 +11,7 @@ gameskill:
invites and reminders, then start it and open the URL to create the first
admin account.
author: "kristianwind"
version: 1
version: 2
icon: "app"
# The image is distroless — no shell — so keep_entrypoint runs the binary the
@@ -70,6 +70,14 @@ gameskill:
pattern: '^[0-9]+(ns|us|ms|s|m|h)$'
hint: "must be a duration like 720h, 30m or 45s"
# Off by default because it is an outbound request nobody asked for. It sends
# no identifier and reads one public URL — GitHub's releases API — but that is
# a choice to make rather than a default to inherit.
- key: VERDANDE_UPDATE_CHECK
name: "Look for new releases (asks GitHub once a day; sends nothing about you)"
type: bool
default: false
install:
# The image has no shell, so the install step borrows one. It only has to make
# the directory layout exist; verdande creates and migrates its own database on