feat: stamp the build version into every service

pkg/version.Version (default "dev") is set at link time via -ldflags from each
service Dockerfile's VERSION build-arg, which the deploy passes as the git tag
(git describe --tags). It surfaces as the OpenTelemetry service.version resource
attribute (so Grafana/Tempo are version-aware), alongside the SPA's existing
About version. Adds the VERSION build-arg to the backend/gateway/validator/bot
compose builds and a serviceResource test covering service.name + service.version.
This commit is contained in:
Ilia Denisov
2026-06-22 07:28:27 +02:00
parent 520a9092fe
commit 8d45ae6e3b
7 changed files with 62 additions and 7 deletions
+3 -1
View File
@@ -33,7 +33,9 @@ COPY backend ./backend
# Reduce the workspace to what the backend needs: backend + pkg. loadtest and the # Reduce the workspace to what the backend needs: backend + pkg. loadtest and the
# gateway replace it requires are not in this context, so drop both. # gateway replace it requires are not in this context, so drop both.
RUN go work edit -dropuse=./gateway -dropuse=./platform/telegram -dropuse=./loadtest -dropreplace=scrabble/gateway@v0.0.0 RUN go work edit -dropuse=./gateway -dropuse=./platform/telegram -dropuse=./loadtest -dropreplace=scrabble/gateway@v0.0.0
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/backend ./backend/cmd/backend # VERSION (the deploy passes the git tag) is stamped into the binary via the linker.
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-X scrabble/pkg/version.Version=${VERSION}" -o /out/backend ./backend/cmd/backend
# --- runtime ----------------------------------------------------------------- # --- runtime -----------------------------------------------------------------
FROM gcr.io/distroless/static-debian12:nonroot FROM gcr.io/distroless/static-debian12:nonroot
+8
View File
@@ -71,6 +71,8 @@ services:
# Seed dictionary for a FRESH volume; the per-contour value comes from the # Seed dictionary for a FRESH volume; the per-contour value comes from the
# deploy env (Gitea TEST_/PROD_DICT_VERSION). See the volume note below. # deploy env (Gitea TEST_/PROD_DICT_VERSION). See the volume note below.
DICT_VERSION: ${DICT_VERSION:-v1.2.1} DICT_VERSION: ${DICT_VERSION:-v1.2.1}
# Build version stamped into the binary (git tag; see pkg/version).
VERSION: ${APP_VERSION:-dev}
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
depends_on: depends_on:
@@ -132,6 +134,8 @@ services:
VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-} VITE_TELEGRAM_GAME_CHANNEL_NAME: ${VITE_TELEGRAM_GAME_CHANNEL_NAME:-}
VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-} VITE_GATEWAY_URL: ${VITE_GATEWAY_URL:-}
VITE_APP_VERSION: ${APP_VERSION:-dev} VITE_APP_VERSION: ${APP_VERSION:-dev}
# Go binary version (the SPA's VITE_APP_VERSION is the same git tag).
VERSION: ${APP_VERSION:-dev}
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
depends_on: [backend] depends_on: [backend]
@@ -218,6 +222,8 @@ services:
context: .. context: ..
dockerfile: platform/telegram/Dockerfile dockerfile: platform/telegram/Dockerfile
target: validator target: validator
args:
VERSION: ${APP_VERSION:-dev}
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
environment: environment:
@@ -268,6 +274,8 @@ services:
context: .. context: ..
dockerfile: platform/telegram/Dockerfile dockerfile: platform/telegram/Dockerfile
target: bot target: bot
args:
VERSION: ${APP_VERSION:-dev}
restart: unless-stopped restart: unless-stopped
logging: *default-logging logging: *default-logging
depends_on: [vpn] depends_on: [vpn]
+3 -1
View File
@@ -70,7 +70,9 @@ RUN rm gateway/internal/webui/dist/landing.html
# Reduce the workspace to what the gateway needs: gateway + pkg (loadtest is not in # Reduce the workspace to what the gateway needs: gateway + pkg (loadtest is not in
# this context; its scrabble/gateway replace targets ./gateway, which is present here). # this context; its scrabble/gateway replace targets ./gateway, which is present here).
RUN go work edit -dropuse=./backend -dropuse=./platform/telegram -dropuse=./loadtest RUN go work edit -dropuse=./backend -dropuse=./platform/telegram -dropuse=./loadtest
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/gateway ./gateway/cmd/gateway # VERSION (the deploy passes the git tag) is stamped into the binary via the linker.
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-X scrabble/pkg/version.Version=${VERSION}" -o /out/gateway ./gateway/cmd/gateway
# --- runtime ----------------------------------------------------------------- # --- runtime -----------------------------------------------------------------
FROM gcr.io/distroless/static-debian12:nonroot AS gateway FROM gcr.io/distroless/static-debian12:nonroot AS gateway
+13 -3
View File
@@ -29,6 +29,8 @@ import (
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"scrabble/pkg/version"
) )
// Exporter selectors supported per signal. // Exporter selectors supported per signal.
@@ -95,9 +97,7 @@ func New(ctx context.Context, cfg Config) (*Runtime, error) {
return nil, err return nil, err
} }
res, err := resource.New(ctx, resource.WithAttributes( res, err := serviceResource(ctx, cfg)
attribute.String("service.name", cfg.ServiceName),
))
if err != nil { if err != nil {
return nil, fmt.Errorf("telemetry: build resource: %w", err) return nil, fmt.Errorf("telemetry: build resource: %w", err)
} }
@@ -122,6 +122,16 @@ func New(ctx context.Context, cfg Config) (*Runtime, error) {
return &Runtime{tracerProvider: tracerProvider, meterProvider: meterProvider}, nil return &Runtime{tracerProvider: tracerProvider, meterProvider: meterProvider}, nil
} }
// serviceResource builds the OpenTelemetry resource describing this service: its
// service.name and the service.version stamped into the binary at build time
// (pkg/version, set from the git tag by the deploy).
func serviceResource(ctx context.Context, cfg Config) (*resource.Resource, error) {
return resource.New(ctx, resource.WithAttributes(
attribute.String("service.name", cfg.ServiceName),
attribute.String("service.version", version.Version),
))
}
// TracerProvider returns the runtime tracer provider, or the global one when r is // TracerProvider returns the runtime tracer provider, or the global one when r is
// not initialised. // not initialised.
func (r *Runtime) TracerProvider() trace.TracerProvider { func (r *Runtime) TracerProvider() trace.TracerProvider {
+21
View File
@@ -4,6 +4,8 @@ import (
"context" "context"
"testing" "testing"
"time" "time"
"scrabble/pkg/version"
) )
// TestConfigValidate covers the supported and rejected exporter selections. // TestConfigValidate covers the supported and rejected exporter selections.
@@ -82,3 +84,22 @@ func TestNilRuntime(t *testing.T) {
t.Errorf("nil runtime Shutdown: %v", err) t.Errorf("nil runtime Shutdown: %v", err)
} }
} }
// TestServiceResource checks the resource carries service.name and the embedded
// service.version (pkg/version, stamped at build time).
func TestServiceResource(t *testing.T) {
res, err := serviceResource(context.Background(), DefaultConfig("svc"))
if err != nil {
t.Fatalf("serviceResource: %v", err)
}
attrs := map[string]string{}
for _, kv := range res.Attributes() {
attrs[string(kv.Key)] = kv.Value.AsString()
}
if attrs["service.name"] != "svc" {
t.Errorf("service.name = %q, want svc", attrs["service.name"])
}
if attrs["service.version"] != version.Version {
t.Errorf("service.version = %q, want %q", attrs["service.version"], version.Version)
}
}
+10
View File
@@ -0,0 +1,10 @@
// Package version exposes the build version stamped into every Scrabble service
// binary. The default is "dev"; release builds override it through the linker
// (`go build -ldflags "-X scrabble/pkg/version.Version=<value>"`), wired from the
// VERSION build-arg in each service Dockerfile, which the deploy sets to the git
// tag (`git describe --tags`). It surfaces as the OpenTelemetry service.version
// resource attribute (see pkg/telemetry) and the SPA About screen.
package version
// Version is the build version, "dev" unless overridden at link time.
var Version = "dev"
+4 -2
View File
@@ -19,8 +19,10 @@ COPY platform/telegram ./platform/telegram
# Reduce the workspace to what the platform needs: only pkg + platform/telegram. # Reduce the workspace to what the platform needs: only pkg + platform/telegram.
RUN go work edit -dropuse=./backend -dropuse=./gateway -dropuse=./loadtest -dropreplace=scrabble/gateway@v0.0.0 -dropreplace=scrabble-solver RUN go work edit -dropuse=./backend -dropuse=./gateway -dropuse=./loadtest -dropreplace=scrabble/gateway@v0.0.0 -dropreplace=scrabble-solver
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/validator ./platform/telegram/cmd/validator # VERSION (the deploy passes the git tag) is stamped into both binaries via the linker.
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -o /out/bot ./platform/telegram/cmd/bot ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-X scrabble/pkg/version.Version=${VERSION}" -o /out/validator ./platform/telegram/cmd/validator
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags "-X scrabble/pkg/version.Version=${VERSION}" -o /out/bot ./platform/telegram/cmd/bot
# --- validator (home) -------------------------------------------------------- # --- validator (home) --------------------------------------------------------
FROM gcr.io/distroless/static-debian12:nonroot AS validator FROM gcr.io/distroless/static-debian12:nonroot AS validator