diff --git a/Dockerfile b/Dockerfile index 029e1ca..9391b9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,10 +7,10 @@ COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /gitea-mobile ./cmd/server # Stage 2: Runtime +# Templates and static assets are embedded in the binary via go:embed, +# so no COPY steps are needed for them. FROM gcr.io/distroless/static:nonroot COPY --from=builder /gitea-mobile /gitea-mobile -COPY static/ /static/ -COPY internal/templates/ /templates/ EXPOSE 8080 USER nonroot:nonroot ENTRYPOINT ["/gitea-mobile"] diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 1134c72..3cf0312 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -12,6 +12,8 @@ import ( "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config" giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea" "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/middleware" + "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/templates" + "gitea.leeworks.dev/0xwheatyz/gitea-mobile/static" ) // Handler holds shared dependencies for all HTTP handlers. @@ -64,8 +66,8 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) { } mux.HandleFunc("/settings", settingsHandler.ServeHTTP) - // Static files. - mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + // Static files — served from embedded filesystem. + mux.Handle("/static/", http.StripPrefix("/static/", http.FileServerFS(static.FS))) } // isHTMX returns true if the request is an HTMX partial request. @@ -235,7 +237,7 @@ func (h *Handler) ErrorInternal(w http.ResponseWriter, r *http.Request) { // renderError renders the error template with the given data and status code. func (h *Handler) renderError(w http.ResponseWriter, r *http.Request, data errorData) { - tmpl, err := template.ParseFiles("internal/templates/error.html") + tmpl, err := template.ParseFS(templates.FS, "error.html") if err != nil { slog.Error("failed to parse error template", "error", err) http.Error(w, fmt.Sprintf("%d %s", data.Code, data.Title), data.Code) @@ -298,7 +300,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { } } - tmpl, err := template.ParseFiles("internal/templates/dashboard.html") + tmpl, err := template.ParseFS(templates.FS, "dashboard.html") if err != nil { slog.Error("failed to parse dashboard template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -390,7 +392,7 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { // For HTMX infinite-scroll requests (page > 1), return only the card fragment. if isHTMX(r) && page > 1 { - tmpl, err := template.ParseFiles("internal/templates/issues.html") + tmpl, err := template.ParseFS(templates.FS, "issues.html") if err != nil { slog.Error("failed to parse issues template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -407,7 +409,7 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) { return } - tmpl, err := template.ParseFiles("internal/templates/issues.html") + tmpl, err := template.ParseFS(templates.FS, "issues.html") if err != nil { slog.Error("failed to parse issues template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -502,7 +504,7 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) { // For HTMX infinite-scroll requests (page > 1), return only the card fragment. if isHTMX(r) && page > 1 { - tmpl, err := template.ParseFiles("internal/templates/pulls.html") + tmpl, err := template.ParseFS(templates.FS, "pulls.html") if err != nil { slog.Error("failed to parse pulls template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -519,7 +521,7 @@ func (h *Handler) ListPulls(w http.ResponseWriter, r *http.Request) { return } - tmpl, err := template.ParseFiles("internal/templates/pulls.html") + tmpl, err := template.ParseFS(templates.FS, "pulls.html") if err != nil { slog.Error("failed to parse pulls template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -587,7 +589,7 @@ func (h *Handler) IssueDetail(w http.ResponseWriter, r *http.Request) { } // Build the content HTML using the template. - tmpl, err := template.ParseFiles("internal/templates/issue_detail.html") + tmpl, err := template.ParseFS(templates.FS, "issue_detail.html") if err != nil { slog.Error("failed to parse issue_detail template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -660,7 +662,7 @@ func (h *Handler) PullDetail(w http.ResponseWriter, r *http.Request) { } // Build the content HTML using the template. - tmpl, err := template.ParseFiles("internal/templates/pull_detail.html") + tmpl, err := template.ParseFS(templates.FS, "pull_detail.html") if err != nil { slog.Error("failed to parse pull_detail template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) @@ -701,7 +703,7 @@ func (h *Handler) NewIssue(w http.ResponseWriter, r *http.Request) { return } - tmpl, err := template.ParseFiles("internal/templates/create_issue.html") + tmpl, err := template.ParseFS(templates.FS, "create_issue.html") if err != nil { slog.Error("failed to parse create_issue template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) diff --git a/internal/handlers/integration_test.go b/internal/handlers/integration_test.go index b5dcd3e..ef356b8 100644 --- a/internal/handlers/integration_test.go +++ b/internal/handlers/integration_test.go @@ -7,9 +7,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "os" - "path/filepath" - "runtime" "strings" "testing" @@ -18,26 +15,8 @@ import ( "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/middleware" ) -// TestMain changes the working directory to the project root so that -// template files can be found by handlers that use relative paths. -func TestMain(m *testing.M) { - // Walk up from this source file to find the project root (where go.mod lives). - _, filename, _, _ := runtime.Caller(0) - dir := filepath.Dir(filename) - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - break - } - parent := filepath.Dir(dir) - if parent == dir { - // Reached filesystem root without finding go.mod; run tests from cwd. - break - } - dir = parent - } - _ = os.Chdir(dir) - os.Exit(m.Run()) -} +// Note: TestMain is no longer needed because templates and static assets +// are embedded at compile time via go:embed. Tests work from any directory. // mockGiteaAPI starts an httptest server that simulates the Gitea API // endpoints needed by the handlers. Returns the server and a cleanup func. diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index 04b608d..d1cf32a 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -8,10 +8,9 @@ import ( "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/auth" "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/middleware" + "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/templates" ) -const settingsTemplatePath = "internal/templates/settings.html" - // SettingsHandler handles GET and POST requests for the settings page. type SettingsHandler struct { SessionSecret string @@ -101,7 +100,7 @@ func (h *SettingsHandler) renderWithMessage(w http.ResponseWriter, r *http.Reque } func (h *SettingsHandler) renderSettings(w http.ResponseWriter, data settingsData) { - tmpl, err := template.ParseFiles(settingsTemplatePath) + tmpl, err := template.ParseFS(templates.FS, "settings.html") if err != nil { slog.Error("failed to parse settings template", "error", err) http.Error(w, "template error", http.StatusInternalServerError) diff --git a/internal/templates/embed.go b/internal/templates/embed.go new file mode 100644 index 0000000..566672d --- /dev/null +++ b/internal/templates/embed.go @@ -0,0 +1,9 @@ +// Package templates provides embedded HTML template files. +package templates + +import "embed" + +// FS contains all HTML template files embedded at compile time. +// +//go:embed *.html +var FS embed.FS diff --git a/static/embed.go b/static/embed.go new file mode 100644 index 0000000..301b067 --- /dev/null +++ b/static/embed.go @@ -0,0 +1,10 @@ +// Package static provides embedded static asset files (CSS, JS, icons, etc.). +package static + +import "embed" + +// FS contains all static asset files embedded at compile time. +// Files are served via http.FileServerFS in the handlers package. +// +//go:embed *.css *.js *.json *.png +var FS embed.FS