diff --git a/AGENTS.md b/AGENTS.md index 8a48a63..0316112 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,3 +52,24 @@ cd web && npm run dev # Vite on :5173, proxies /api to :8080 ``` Requires `web/node_modules` (run `cd web && npm install` first). + +## Releasing / Versioning + +The project follows **SemVer**: `MAJOR.MINOR.PATCH`. + +- **PATCH** (`0.1.5 → 0.1.6`): bug fixes, no API changes. +- **MINOR** (`0.1.x → 0.2.0`): new features, backward-compatible. +- **MAJOR** (`0.x → 1.0`): breaking changes. + +**Bump before commit/push.** Update `VERSION?=` in the `Makefile` before staging any +release-worthy changes. The value is baked into the binary at build time via +`-ldflags -X github.com/darroyo/nasctl/internal/web.Version=$(VERSION)` and surfaced +in the UI sidebar footer and `GET /api/version`. + +The short git SHA is also injected automatically via +`-X github.com/darroyo/nasctl/internal/web.Commit=$(shell git rev-parse --short HEAD)`. + +Override at build time: `make build VERSION=0.2.0-rc1`. + +When reviewing staged changes, always confirm the version bump matches the +nature of the change (patch vs minor vs major). diff --git a/Makefile b/Makefile index 9ca4cd0..7568eb4 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ BINARY=nasctl -VERSION?=0.1.5 +VERSION?=0.1.6 GO?=go -LDFLAGS=-s -w +LDFLAGS=-s -w -X github.com/darroyo/nasctl/internal/web.Version=$(VERSION) -X github.com/darroyo/nasctl/internal/web.Commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) BUILD_FLAGS=CGO_ENABLED=0 .PHONY: build build-all frontend run package clean tidy test diff --git a/cmd/nasctl/main.go b/cmd/nasctl/main.go index ee607a7..e692a14 100644 --- a/cmd/nasctl/main.go +++ b/cmd/nasctl/main.go @@ -17,6 +17,11 @@ import ( "github.com/darroyo/nasctl/internal/web" ) +var ( + version = "dev" + commit = "" +) + func main() { addr := flag.String("addr", envOrDefault("NASCTL_ADDR", ":8080"), "HTTP listen address") dbPath := flag.String("db", envOrDefault("NASCTL_DB", "/var/lib/nasctl/nasctl.db"), "SQLite database path") @@ -79,6 +84,7 @@ func main() { }) eng := engine.New(sambaModule, nfsModule, usersModule) + web.SetVersionInfo(version, commit) srv := web.NewServer(database, eng, web.Options{ AllowedRoots: parseRoots(*allowedRoots), Auth: auth, @@ -86,7 +92,7 @@ func main() { ExportsPath: *exportsPath, }) - log.Printf("nasctl listening on %s (db=%s exec-system=%v)", *addr, *dbPath, *execSystem) + log.Printf("nasctl %s (commit %s) listening on %s (db=%s exec-system=%v)", version, commit, *addr, *dbPath, *execSystem) if err := http.ListenAndServe(*addr, srv.Handler()); err != nil { log.Fatalf("server error: %v", err) } diff --git a/internal/web/handlers_system.go b/internal/web/handlers_system.go index 221dc22..b70a541 100644 --- a/internal/web/handlers_system.go +++ b/internal/web/handlers_system.go @@ -37,6 +37,13 @@ func (s *Server) handleSystemStatus(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, status) } +func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]string{ + "version": Version, + "commit": Commit, + }) +} + func (s *Server) collectDiskUsage() []diskUsage { entries := make(map[string]*diskUsage) diff --git a/internal/web/router.go b/internal/web/router.go index 2ea1c15..5431bd3 100644 --- a/internal/web/router.go +++ b/internal/web/router.go @@ -29,6 +29,7 @@ func NewRouter(s *Server) chi.Router { protected.Post("/apply", s.handleApply) protected.Get("/apply/log", s.handleApplyLog) protected.Get("/system/status", s.handleSystemStatus) + protected.Get("/version", s.handleVersion) protected.Put("/auth/password", s.handleChangePassword) protected.Get("/import/status", s.handleImportStatus) diff --git a/internal/web/version.go b/internal/web/version.go new file mode 100644 index 0000000..1b4e054 --- /dev/null +++ b/internal/web/version.go @@ -0,0 +1,11 @@ +package web + +var ( + Version = "dev" + Commit = "" +) + +func SetVersionInfo(v, c string) { + Version = v + Commit = c +} diff --git a/web/src/api.ts b/web/src/api.ts index d09205c..bb79063 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -65,6 +65,11 @@ export interface SystemStatus { services: ServiceStatus[]; } +export interface VersionInfo { + version: string; + commit: string; +} + export class ApiError extends Error { status: number; constructor(status: number, message: string) { @@ -104,6 +109,7 @@ export const api = { apply: () => request<{ results: { module: string; applied: boolean; error?: string }[] }>("POST", "/apply"), applyLog: () => request<{ entries: ApplyLogEntry[] }>("GET", "/apply/log"), systemStatus: () => request("GET", "/system/status"), + version: () => request("GET", "/version"), // samba listShares: () => request<{ shares: SambaShare[] | null }>("GET", "/samba/shares/"), diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 490d90f..d1d9490 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -1,5 +1,6 @@ +import { useEffect, useState } from "react"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; -import { api } from "../api"; +import { api, VersionInfo } from "../api"; import DirtyBanner from "./DirtyBanner"; const navItems = [ @@ -13,6 +14,11 @@ const navItems = [ export default function Layout({ username, onLogout }: { username: string; onLogout: () => void }) { const navigate = useNavigate(); + const [version, setVersion] = useState(null); + + useEffect(() => { + api.version().then(setVersion).catch(() => {}); + }, []); async function handleLogout() { await api.logout(); @@ -47,6 +53,9 @@ export default function Layout({ username, onLogout }: { username: string; onLog +
+ v{version?.version ?? "dev"}{version?.commit && ` · ${version.commit}`} +