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]+*"] tags: ["v[0-9]+.[0-9]+.[0-9]+*"]
permissions: 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 packages: write
jobs: jobs:
@@ -50,3 +52,15 @@ jobs:
VERSION=${{ github.ref_name }} VERSION=${{ github.ref_name }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max 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) errc := make(chan error, 1)
go func() { 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) { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errc <- err errc <- err
} }
+9 -1
View File
@@ -1718,7 +1718,15 @@ paths:
broken volume reports unhealthy instead of pretending to work. broken volume reports unhealthy instead of pretending to work.
security: [] security: []
responses: 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. } "503": { description: Unhealthy. }
/ics/{token}: /ics/{token}:
+5 -1
View File
@@ -348,7 +348,11 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
}) })
return 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 // 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 invites and reminders, then start it and open the URL to create the first
admin account. admin account.
author: "kristianwind" author: "kristianwind"
version: 1 version: 2
icon: "app" icon: "app"
# The image is distroless — no shell — so keep_entrypoint runs the binary the # 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)$' pattern: '^[0-9]+(ns|us|ms|s|m|h)$'
hint: "must be a duration like 720h, 30m or 45s" 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: install:
# The image has no shell, so the install step borrows one. It only has to make # 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 # the directory layout exist; verdande creates and migrates its own database on