diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 9054eee..c5549ad 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -275,7 +275,10 @@ func runServe() { logger := setupLogger(cfg.Log.Level, cfg.Log.Format) // Create and start server - srv, err := server.New(cfg, logger) + srv, err := server.New(cfg, logger, server.BuildInfo{ + Version: Version, + Commit: Commit, + }) if err != nil { logger.Error("failed to create server", "error", err) os.Exit(1) diff --git a/internal/server/browse_test.go b/internal/server/browse_test.go index 3cc37c8..f4f2f9a 100644 --- a/internal/server/browse_test.go +++ b/internal/server/browse_test.go @@ -430,6 +430,10 @@ func TestHandleBrowseSourcePage(t *testing.T) { } } + if !strings.Contains(body, "proxy test-version (test-commit)") { + t.Error("browse source footer should contain proxy build information, not the package version") + } + // Check that the escapeHTML function is present for XSS protection if !strings.Contains(body, "function escapeHTML(str)") { t.Error("browse source page missing escapeHTML function for XSS protection") diff --git a/internal/server/layout.go b/internal/server/layout.go index ef39858..2da9469 100644 --- a/internal/server/layout.go +++ b/internal/server/layout.go @@ -2,17 +2,24 @@ package server import "net/http" -// Layout carries per-request fields consumed by the shared base template -// (canonical URL, og:url). It is embedded in every page data struct so that -// templates can reference {{.UIBaseURL}} and {{.CanonicalPath}} alongside the -// page's own fields. +// BuildInfo identifies the running proxy binary. +type BuildInfo struct { + Version string + Commit string +} + +// Layout carries shared fields consumed by the base template. It is embedded +// in every page data struct so templates can access canonical URL and build +// information alongside the page's own fields. type Layout struct { + BuildInfo BuildInfo UIBaseURL string CanonicalPath string } func (s *Server) layoutFor(r *http.Request) Layout { return Layout{ + BuildInfo: s.buildInfo, UIBaseURL: s.cfg.UIBaseURL, CanonicalPath: r.URL.Path, } diff --git a/internal/server/server.go b/internal/server/server.go index 71af1af..bb964e8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -92,6 +92,7 @@ type Server struct { db *database.DB storage storage.Storage logger *slog.Logger + buildInfo BuildInfo http *http.Server templates *Templates cancel context.CancelFunc @@ -100,7 +101,7 @@ type Server struct { } // New creates a new Server with the given configuration. -func New(cfg *config.Config, logger *slog.Logger) (*Server, error) { +func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, error) { var activityLog *accesslog.Logger if cfg.AccessLog.Path != "" { var err error @@ -169,6 +170,7 @@ func New(cfg *config.Config, logger *slog.Logger) (*Server, error) { db: db, storage: store, logger: logger, + buildInfo: buildInfo, templates: &Templates{}, healthCache: hc, accessLog: activityLog, diff --git a/internal/server/server_test.go b/internal/server/server_test.go index a870106..03b253f 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -104,6 +104,7 @@ func newTestServer(t *testing.T) *testServer { db: db, storage: store, logger: logger, + buildInfo: BuildInfo{Version: "test-version", Commit: "test-commit"}, templates: &Templates{}, healthCache: hc, } @@ -313,6 +314,9 @@ func TestDashboard(t *testing.T) { if !strings.Contains(body, "Cached Artifacts") { t.Error("dashboard should contain stats") } + if !strings.Contains(body, "proxy test-version (test-commit)") { + t.Error("dashboard footer should contain build information") + } if !strings.Contains(body, "Popular Packages") { t.Error("dashboard should contain popular packages section") } @@ -598,6 +602,9 @@ func TestVersionShowWithHitCount(t *testing.T) { if !strings.Contains(body, "42 cache hits") { t.Error("expected page to show hit count") } + if !strings.Contains(body, "proxy test-version (test-commit)") { + t.Error("version show footer should contain proxy build information, not the package version") + } } func TestSearchWithNullValues(t *testing.T) { @@ -1327,10 +1334,14 @@ func TestNewServer_StorageConnectivityCheck(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - srv, err := New(cfg, logger) + buildInfo := BuildInfo{Version: "test-version", Commit: "test-commit"} + srv, err := New(cfg, logger, buildInfo) if err != nil { t.Fatalf("New() failed: %v", err) } + if srv.buildInfo != buildInfo { + t.Errorf("build info = %#v, want %#v", srv.buildInfo, buildInfo) + } // On Windows, OpenBucket normalises to file:///C:/path; on Unix the // absolute path already starts with /, so file:// + /path == file:///path. @@ -1354,7 +1365,7 @@ func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) { } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - if _, err := New(cfg, logger); err == nil { + if _, err := New(cfg, logger, BuildInfo{}); err == nil { t.Fatal("New() succeeded with invalid access log path") } else if !strings.Contains(err.Error(), "initializing access log") { t.Fatalf("New() error = %v, want access log initialization error", err) diff --git a/internal/server/templates/layout/footer.html b/internal/server/templates/layout/footer.html index 5aa970d..33daddf 100644 --- a/internal/server/templates/layout/footer.html +++ b/internal/server/templates/layout/footer.html @@ -12,6 +12,11 @@

Aboutgithub.com/git-pkgs/proxy

+ {{if .BuildInfo.Version}} +

+ proxy {{.BuildInfo.Version}}{{if .BuildInfo.Commit}} ({{.BuildInfo.Commit}}){{end}} +

+ {{end}}

Resources

diff --git a/internal/server/templates_test.go b/internal/server/templates_test.go index e9a4967..158278b 100644 --- a/internal/server/templates_test.go +++ b/internal/server/templates_test.go @@ -186,6 +186,64 @@ func TestRenderEmitsCanonicalAndOG(t *testing.T) { } } +func TestFooterUsesBuildInfoWhenPageDefinesVersion(t *testing.T) { + templates := &Templates{} + buildInfo := BuildInfo{Version: "proxy-build-1.2.3", Commit: "abc123def"} + wantFooter := "proxy proxy-build-1.2.3 (abc123def)" + + tests := []struct { + name string + page string + data any + shadow string + }{ + { + name: "version show page", + page: "version_show", + data: VersionShowData{ + Layout: Layout{BuildInfo: buildInfo}, + Package: &database.Package{ + PURL: "pkg:npm/lodash", + Ecosystem: "npm", + Name: "lodash", + }, + Version: &database.Version{ + PURL: "pkg:npm/lodash@9.9.9", + PackagePURL: "pkg:npm/lodash", + }, + }, + shadow: "9.9.9", + }, + { + name: "browse source page", + page: "browse_source", + data: BrowseSourceData{ + Layout: Layout{BuildInfo: buildInfo}, + Ecosystem: "npm", + PackageName: "lodash", + Version: "9.9.9", + }, + shadow: "9.9.9", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := httptest.NewRecorder() + if err := templates.Render(w, tt.page, tt.data); err != nil { + t.Fatalf("Render(%q) failed: %v", tt.page, err) + } + body := w.Body.String() + if !strings.Contains(body, wantFooter) { + t.Errorf("footer missing build info %q", wantFooter) + } + if strings.Contains(body, "proxy "+tt.shadow) { + t.Errorf("footer used page Version %q instead of BuildInfo", tt.shadow) + } + }) + } +} + func TestRenderOmitsCanonicalWhenUIBaseURLUnset(t *testing.T) { templates := &Templates{}