feat: bake version into binary and display in UI sidebar
- Inject VERSION via ldflags (-X main=web.Version=) and git short SHA
- Add /api/version endpoint returning {version, commit}
- Display version in Layout sidebar footer (v0.1.6 · 8cfd405)
- Add AGENTS.md section documenting SemVer policy and bump-before-push rule
- Bump VERSION 0.1.5 -> 0.1.6 in Makefile
This commit is contained in:
@@ -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).
|
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).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
BINARY=nasctl
|
BINARY=nasctl
|
||||||
VERSION?=0.1.5
|
VERSION?=0.1.6
|
||||||
GO?=go
|
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
|
BUILD_FLAGS=CGO_ENABLED=0
|
||||||
|
|
||||||
.PHONY: build build-all frontend run package clean tidy test
|
.PHONY: build build-all frontend run package clean tidy test
|
||||||
|
|||||||
+7
-1
@@ -17,6 +17,11 @@ import (
|
|||||||
"github.com/darroyo/nasctl/internal/web"
|
"github.com/darroyo/nasctl/internal/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = ""
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := flag.String("addr", envOrDefault("NASCTL_ADDR", ":8080"), "HTTP listen address")
|
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")
|
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)
|
eng := engine.New(sambaModule, nfsModule, usersModule)
|
||||||
|
web.SetVersionInfo(version, commit)
|
||||||
srv := web.NewServer(database, eng, web.Options{
|
srv := web.NewServer(database, eng, web.Options{
|
||||||
AllowedRoots: parseRoots(*allowedRoots),
|
AllowedRoots: parseRoots(*allowedRoots),
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
@@ -86,7 +92,7 @@ func main() {
|
|||||||
ExportsPath: *exportsPath,
|
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 {
|
if err := http.ListenAndServe(*addr, srv.Handler()); err != nil {
|
||||||
log.Fatalf("server error: %v", err)
|
log.Fatalf("server error: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ func (s *Server) handleSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, status)
|
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 {
|
func (s *Server) collectDiskUsage() []diskUsage {
|
||||||
entries := make(map[string]*diskUsage)
|
entries := make(map[string]*diskUsage)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func NewRouter(s *Server) chi.Router {
|
|||||||
protected.Post("/apply", s.handleApply)
|
protected.Post("/apply", s.handleApply)
|
||||||
protected.Get("/apply/log", s.handleApplyLog)
|
protected.Get("/apply/log", s.handleApplyLog)
|
||||||
protected.Get("/system/status", s.handleSystemStatus)
|
protected.Get("/system/status", s.handleSystemStatus)
|
||||||
|
protected.Get("/version", s.handleVersion)
|
||||||
protected.Put("/auth/password", s.handleChangePassword)
|
protected.Put("/auth/password", s.handleChangePassword)
|
||||||
|
|
||||||
protected.Get("/import/status", s.handleImportStatus)
|
protected.Get("/import/status", s.handleImportStatus)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
Commit = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetVersionInfo(v, c string) {
|
||||||
|
Version = v
|
||||||
|
Commit = c
|
||||||
|
}
|
||||||
@@ -65,6 +65,11 @@ export interface SystemStatus {
|
|||||||
services: ServiceStatus[];
|
services: ServiceStatus[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
version: string;
|
||||||
|
commit: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
status: number;
|
status: number;
|
||||||
constructor(status: number, message: string) {
|
constructor(status: number, message: string) {
|
||||||
@@ -104,6 +109,7 @@ export const api = {
|
|||||||
apply: () => request<{ results: { module: string; applied: boolean; error?: string }[] }>("POST", "/apply"),
|
apply: () => request<{ results: { module: string; applied: boolean; error?: string }[] }>("POST", "/apply"),
|
||||||
applyLog: () => request<{ entries: ApplyLogEntry[] }>("GET", "/apply/log"),
|
applyLog: () => request<{ entries: ApplyLogEntry[] }>("GET", "/apply/log"),
|
||||||
systemStatus: () => request<SystemStatus>("GET", "/system/status"),
|
systemStatus: () => request<SystemStatus>("GET", "/system/status"),
|
||||||
|
version: () => request<VersionInfo>("GET", "/version"),
|
||||||
|
|
||||||
// samba
|
// samba
|
||||||
listShares: () => request<{ shares: SambaShare[] | null }>("GET", "/samba/shares/"),
|
listShares: () => request<{ shares: SambaShare[] | null }>("GET", "/samba/shares/"),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||||
import { api } from "../api";
|
import { api, VersionInfo } from "../api";
|
||||||
import DirtyBanner from "./DirtyBanner";
|
import DirtyBanner from "./DirtyBanner";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -13,6 +14,11 @@ const navItems = [
|
|||||||
|
|
||||||
export default function Layout({ username, onLogout }: { username: string; onLogout: () => void }) {
|
export default function Layout({ username, onLogout }: { username: string; onLogout: () => void }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [version, setVersion] = useState<VersionInfo | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.version().then(setVersion).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await api.logout();
|
await api.logout();
|
||||||
@@ -47,6 +53,9 @@ export default function Layout({ username, onLogout }: { username: string; onLog
|
|||||||
<button className="btn-ghost w-full" onClick={handleLogout}>
|
<button className="btn-ghost w-full" onClick={handleLogout}>
|
||||||
Cerrar sesión
|
Cerrar sesión
|
||||||
</button>
|
</button>
|
||||||
|
<div className="mt-2 border-t border-slate-800 pt-2 text-center font-mono text-xs text-slate-600">
|
||||||
|
v{version?.version ?? "dev"}{version?.commit && ` · ${version.commit}`}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="flex-1">
|
<main className="flex-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user