diff --git a/Makefile b/Makefile
index de240d0..31ee1f5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: all build lint fmt fmt-check test check clean
+.PHONY: all build lint fmt fmt-check test check clean docker hooks
BINARY := upaasd
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
@@ -22,12 +22,22 @@ fmt-check:
@test -z "$$(gofmt -l .)" || (echo "Files not formatted:" && gofmt -l . && exit 1)
test:
- go test -v -race -cover ./...
+ go test -v -race -cover -timeout 30s ./...
# Check runs all validation without making changes
# Used by CI and Docker build - fails if anything is wrong
check: fmt-check lint test
@echo "==> All checks passed!"
+docker:
+ docker build .
+
+hooks:
+ @echo "Installing pre-commit hook..."
+ @mkdir -p .git/hooks
+ @printf '#!/bin/sh\nmake check\n' > .git/hooks/pre-commit
+ @chmod +x .git/hooks/pre-commit
+ @echo "Pre-commit hook installed."
+
clean:
rm -rf bin/
diff --git a/README.md b/README.md
index 8e86720..91877d7 100644
--- a/README.md
+++ b/README.md
@@ -110,11 +110,14 @@ chi Router ──► Middleware Stack ──► Handler
### Commands
```bash
-make fmt # Format code
-make lint # Run comprehensive linting
-make test # Run tests with race detection
-make check # Verify everything passes (lint, test, build, format)
-make build # Build binary
+make fmt # Format code
+make fmt-check # Check formatting (read-only, fails if unformatted)
+make lint # Run comprehensive linting
+make test # Run tests with race detection (30s timeout)
+make check # Verify everything passes (fmt-check, lint, test)
+make build # Build binary
+make docker # Build Docker image
+make hooks # Install pre-commit hook (runs make check)
```
### Commit Requirements
diff --git a/internal/handlers/webhook_events.go b/internal/handlers/webhook_events.go
new file mode 100644
index 0000000..d455cda
--- /dev/null
+++ b/internal/handlers/webhook_events.go
@@ -0,0 +1,56 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/go-chi/chi/v5"
+
+ "sneak.berlin/go/upaas/internal/models"
+ "sneak.berlin/go/upaas/templates"
+)
+
+// webhookEventsLimit is the number of webhook events to show in history.
+const webhookEventsLimit = 100
+
+// HandleAppWebhookEvents returns the webhook event history handler.
+func (h *Handlers) HandleAppWebhookEvents() http.HandlerFunc {
+ tmpl := templates.GetParsed()
+
+ return func(writer http.ResponseWriter, request *http.Request) {
+ appID := chi.URLParam(request, "id")
+
+ application, findErr := models.FindApp(request.Context(), h.db, appID)
+ if findErr != nil {
+ h.log.Error("failed to find app", "error", findErr)
+ http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
+
+ return
+ }
+
+ if application == nil {
+ http.NotFound(writer, request)
+
+ return
+ }
+
+ events, eventsErr := application.GetWebhookEvents(
+ request.Context(),
+ webhookEventsLimit,
+ )
+ if eventsErr != nil {
+ h.log.Error("failed to get webhook events",
+ "error", eventsErr,
+ "app", appID,
+ )
+
+ events = []*models.WebhookEvent{}
+ }
+
+ data := h.addGlobals(map[string]any{
+ "App": application,
+ "Events": events,
+ }, request)
+
+ h.renderTemplate(writer, tmpl, "webhook_events.html", data)
+ }
+}
diff --git a/internal/models/webhook_event.go b/internal/models/webhook_event.go
index dd8352e..8c7f14a 100644
--- a/internal/models/webhook_event.go
+++ b/internal/models/webhook_event.go
@@ -52,6 +52,20 @@ func (w *WebhookEvent) Reload(ctx context.Context) error {
return w.scan(row)
}
+// ShortCommit returns a truncated commit SHA for display.
+func (w *WebhookEvent) ShortCommit() string {
+ if !w.CommitSHA.Valid {
+ return ""
+ }
+
+ sha := w.CommitSHA.String
+ if len(sha) > shortCommitLength {
+ return sha[:shortCommitLength]
+ }
+
+ return sha
+}
+
func (w *WebhookEvent) insert(ctx context.Context) error {
query := `
INSERT INTO webhook_events (
diff --git a/internal/server/routes.go b/internal/server/routes.go
index 376b19d..f02636d 100644
--- a/internal/server/routes.go
+++ b/internal/server/routes.go
@@ -70,6 +70,7 @@ func (s *Server) SetupRoutes() {
r.Post("/apps/{id}/deploy", s.handlers.HandleAppDeploy())
r.Post("/apps/{id}/deployments/cancel", s.handlers.HandleCancelDeploy())
r.Get("/apps/{id}/deployments", s.handlers.HandleAppDeployments())
+ r.Get("/apps/{id}/webhooks", s.handlers.HandleAppWebhookEvents())
r.Get("/apps/{id}/deployments/{deploymentID}/logs", s.handlers.HandleDeploymentLogsAPI())
r.Get("/apps/{id}/deployments/{deploymentID}/download", s.handlers.HandleDeploymentLogDownload())
r.Get("/apps/{id}/logs", s.handlers.HandleAppLogs())
diff --git a/templates/app_detail.html b/templates/app_detail.html
index 48234ac..2d62de9 100644
--- a/templates/app_detail.html
+++ b/templates/app_detail.html
@@ -77,7 +77,10 @@
-
Webhook URL
+
Add this URL as a push webhook in your Gitea repository:
{{.WebhookURL}}
diff --git a/templates/templates.go b/templates/templates.go
index 5f733d1..ab46739 100644
--- a/templates/templates.go
+++ b/templates/templates.go
@@ -44,6 +44,7 @@ func initTemplates() {
"app_detail.html",
"app_edit.html",
"deployments.html",
+ "webhook_events.html",
}
pageTemplates = make(map[string]*template.Template)
diff --git a/templates/webhook_events.html b/templates/webhook_events.html
new file mode 100644
index 0000000..23fb295
--- /dev/null
+++ b/templates/webhook_events.html
@@ -0,0 +1,79 @@
+{{template "base" .}}
+
+{{define "title"}}Webhook Events - {{.App.Name}} - µPaaS{{end}}
+
+{{define "content"}}
+{{template "nav" .}}
+
+
+
+
+
+
+ {{if .Events}}
+
+
+
+
+ {{range .Events}}
+
+ |
+
+ |
+ {{.EventType}} |
+ {{.Branch}} |
+
+ {{if and .CommitSHA.Valid .CommitURL.Valid}}
+ {{.ShortCommit}}
+ {{else if .CommitSHA.Valid}}
+ {{.ShortCommit}}
+ {{else}}
+ -
+ {{end}}
+ |
+
+ {{if .Matched}}
+ {{if .Processed}}
+ Matched
+ {{else}}
+ Matched (pending)
+ {{end}}
+ {{else}}
+ No match
+ {{end}}
+ |
+
+ {{end}}
+
+
+
+ {{else}}
+
+
+
+
No webhook events yet
+
Webhook events will appear here once your repository sends push notifications.
+
+
+ {{end}}
+
+{{end}}