Add version tracking, an in-app changelog, and a one-command update script
- internal/version: single source of truth, injected at build time via
-ldflags from the VERSION file (Dockerfile does this automatically).
- CHANGELOG.md, embedded into the binary (a root-level package, since
//go:embed can't reach outside its own directory) and served at
/changelog. Footer links to it with the running version.
- gitfed-server -version flag; well-known doc's "software" field now
reflects the real version instead of a hardcoded "0.1.0".
- deploy/update.sh: bumps VERSION (patch/minor/major/explicit), refuses to
run without a matching CHANGELOG.md entry, builds+imports the image
tagged with that version (not just :latest, so rollout history means
something), points deployment.yaml at it, applies, and commits both the
release and the deploy as separate commits.
- deployment.yaml now pins gitfed:0.3.0 explicitly instead of :latest.
12 files changed
+174 −12
A
CHANGELOG.md
+21 −0
A
VERSION
+1 −0
A
changelog.go
+10 −0
M
cmd/gitfed-server/main.go
+9 −3
A
cmd/gitfed-web/handlers_changelog.go
+18 −0
M
cmd/gitfed-web/render.go
+12 −4
M
cmd/gitfed-web/routes.go
+1 −0
M
deploy/docker/Dockerfile
+5 −3
M
deploy/k8s/README.md
+11 −0
M
deploy/k8s/deployment.yaml
+2 −2
A
deploy/update.sh
+71 −0
A
internal/version/version.go
+13 −0
CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d588b81
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,21 @@
+# Changelog
+
+## 0.3.0
+
+- File browser (`browse files` on any repo): breadcrumbed directory listing, markdown rendered inline, anything else shown as plain text, binary files detected and not dumped.
+- Rendered README/LICENSE pages got real styling — tables, blockquotes, task-list checkboxes, code blocks — instead of bare unstyled HTML.
+- Fixed: repos pushed under a branch name that didn't match the bare repo's default `HEAD` (e.g. `main` vs `master`) silently looked empty everywhere — browsing, README rendering, tags. Every freshly pushed repo hit this. Bare repos now default `HEAD` to `main` at creation, and reads fall back to a repo's sole branch when `HEAD` doesn't resolve, so already-affected repos self-heal with no migration.
+- Fixed: errors crossing the admin RPC socket lost their identity (a fresh `errors.New` never equals a sentinel like "not found," even with identical text), which 500'd the repo settings page for any repo with no collaborators yet.
+- Logo: the "Branch Blocks" mark (a git fork drawn as three square-cornered rectangles) in the header and as a light/dark-aware favicon.
+- Version tracking and this changelog.
+
+## 0.2.0
+
+- Real username/password login for the web UI, with server-side sessions (not just an admin tool behind `kubectl port-forward` anymore).
+- Self-service: manage your own SSH keys, create and configure your own repos (public/private, topics, collaborators) without needing an instance admin.
+- Admin section (user management, trust store, audit log) gated by an admin role on your account instead of being the only thing the web UI could do.
+- Merged the public repo browser into the same app as the admin UI — one login, scoped by what each route actually needs.
+
+## 0.1.0
+
+- Initial implementation: SSH server with a local CA, certificate-based cross-instance identity, ACL model with public/private repos, federation trust store with rate limiting, audit log, admin TUI, admin web UI, public read-only repo browser, and Kubernetes deployment manifests.
VERSION
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..0d91a54
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.3.0
changelog.go
diff --git a/changelog.go b/changelog.go
new file mode 100644
index 0000000..d2103c3
--- /dev/null
+++ b/changelog.go
@@ -0,0 +1,10 @@
+// Package gitfed holds nothing but the embedded changelog — Go's //go:embed
+// can't reach outside the directory containing the directive, so this lives
+// at the module root next to CHANGELOG.md itself rather than duplicating
+// the file under cmd/gitfed-web.
+package gitfed
+
+import _ "embed"
+
+//go:embed CHANGELOG.md
+var Changelog string
cmd/gitfed-server/main.go
diff --git a/cmd/gitfed-server/main.go b/cmd/gitfed-server/main.go
index 2d41e58..19df0ac 100644
--- a/cmd/gitfed-server/main.go
+++ b/cmd/gitfed-server/main.go
@@ -17,16 +17,21 @@ import (
"gitfed/internal/federation"
"gitfed/internal/ssh"
"gitfed/internal/store"
+ "gitfed/internal/version"
)
-const version = "0.1.0"
-
func main() {
configPath := flag.String("config", "gitfed.json", "path to instance config file")
initDomain := flag.String("init", "", "initialize a new instance for this domain and exit")
dataDir := flag.String("data-dir", "data", "data directory to use with -init")
+ showVersion := flag.Bool("version", false, "print the version and exit")
flag.Parse()
+ if *showVersion {
+ fmt.Println("gitfed-server " + version.Version)
+ return
+ }
+
if *initDomain != "" {
if err := initInstance(*configPath, *initDomain, *dataDir); err != nil {
log.Fatalf("init: %v", err)
@@ -74,6 +79,7 @@ func initInstance(configPath, domain, dataDir string) error {
}
func run(configPath string) error {
+ log.Printf("gitfed-server %s starting", version.Version)
cfg, err := config.Load(configPath)
if err != nil {
return fmt.Errorf("load config (did you run -init first?): %w", err)
@@ -114,7 +120,7 @@ func run(configPath string) error {
if cfg.ListenHTTP != "" {
go func() {
- handler := federation.Handler(cfg.Domain, cfg.Contact, version, localCA)
+ handler := federation.Handler(cfg.Domain, cfg.Contact, version.Version, localCA)
mux := http.NewServeMux()
mux.Handle("/.well-known/gitfed.json", handler)
log.Printf("gitfed well-known endpoint listening on %s", cfg.ListenHTTP)
cmd/gitfed-web/handlers_changelog.go
diff --git a/cmd/gitfed-web/handlers_changelog.go b/cmd/gitfed-web/handlers_changelog.go
new file mode 100644
index 0000000..81bb92d
--- /dev/null
+++ b/cmd/gitfed-web/handlers_changelog.go
@@ -0,0 +1,18 @@
+package main
+
+import (
+ "html/template"
+ "net/http"
+
+ "gitfed"
+)
+
+func (s *server) handleChangelog(w http.ResponseWriter, r *http.Request) {
+ rendered, err := renderMarkdown(gitfed.Changelog)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ body := `<div class="markdown-body">` + string(rendered) + `</div>`
+ s.render(w, r, "Changelog", "", template.HTML(body))
+}
cmd/gitfed-web/render.go
diff --git a/cmd/gitfed-web/render.go b/cmd/gitfed-web/render.go
index 9755e56..ec5a7fb 100644
--- a/cmd/gitfed-web/render.go
+++ b/cmd/gitfed-web/render.go
@@ -8,6 +8,8 @@ import (
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
+
+ "gitfed/internal/version"
)
var markdown = goldmark.New(goldmark.WithExtensions(extension.GFM))
@@ -94,6 +96,9 @@ const shellSrc = `<!doctype html>
.markdown-body th, .markdown-body td { border: 1px solid #2a2f3a; padding: 0.4rem 0.8rem; text-align: left; }
.markdown-body th { background: #171a21; }
.markdown-body input[type="checkbox"] { width: auto; margin: 0 0.4em 0 0; }
+ footer { max-width: 960px; margin: 2rem auto 1.5rem; padding: 0 1.5rem; }
+ footer a { color: #6b7078; font-size: 0.78rem; text-decoration: none; }
+ footer a:hover { color: #9aa1ac; }
</style>
</head>
<body>
@@ -117,6 +122,9 @@ const shellSrc = `<!doctype html>
<main>
{{.Body}}
</main>
+<footer>
+ <a href="/changelog">gitfed {{.Version}}</a>
+</footer>
</body>
</html>`
@@ -126,10 +134,10 @@ func (s *server) render(w http.ResponseWriter, r *http.Request, title, active st
sess, loggedIn := s.currentSession(r)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = shellTpl.Execute(w, struct {
- Title, Domain, Active, Username string
- LoggedIn, IsAdmin bool
- Body, BrandMark template.HTML
- }{title, s.domain, active, sess.Username, loggedIn, sess.IsAdmin, body, template.HTML(brandMark)})
+ Title, Domain, Active, Username, Version string
+ LoggedIn, IsAdmin bool
+ Body, BrandMark template.HTML
+ }{title, s.domain, active, sess.Username, version.Version, loggedIn, sess.IsAdmin, body, template.HTML(brandMark)})
}
// flash renders the ?msg=&err= query params (set by handlers that redirect
cmd/gitfed-web/routes.go
diff --git a/cmd/gitfed-web/routes.go b/cmd/gitfed-web/routes.go
index 46da898..bb01302 100644
--- a/cmd/gitfed-web/routes.go
+++ b/cmd/gitfed-web/routes.go
@@ -11,6 +11,7 @@ func (s *server) routes(mux *http.ServeMux) {
mux.HandleFunc("GET /login", s.handleLoginForm)
mux.HandleFunc("POST /login", s.handleLogin)
mux.HandleFunc("POST /logout", s.handleLogout)
+ mux.HandleFunc("GET /changelog", s.handleChangelog)
// Self-service — any logged-in user, scoped to their own stuff via
// CheckAccess inside the handlers.
deploy/docker/Dockerfile
diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile
index 0d9d0c4..9332989 100644
--- a/deploy/docker/Dockerfile
+++ b/deploy/docker/Dockerfile
@@ -11,9 +11,11 @@ WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
-RUN CGO_ENABLED=0 go build -o /out/gitfed-server ./cmd/gitfed-server && \
- CGO_ENABLED=0 go build -o /out/gitfed-web ./cmd/gitfed-web && \
- CGO_ENABLED=0 go build -o /out/gitfed-tui ./cmd/gitfed-tui
+RUN VERSION=$(cat VERSION) && \
+ LDFLAGS="-X gitfed/internal/version.Version=$VERSION" && \
+ CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o /out/gitfed-server ./cmd/gitfed-server && \
+ CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o /out/gitfed-web ./cmd/gitfed-web && \
+ CGO_ENABLED=0 go build -ldflags "$LDFLAGS" -o /out/gitfed-tui ./cmd/gitfed-tui
FROM debian:bookworm-slim
RUN apt-get update && \
deploy/k8s/README.md
diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md
index d66a00b..86fd9ab 100644
--- a/deploy/k8s/README.md
+++ b/deploy/k8s/README.md
@@ -51,6 +51,17 @@ pull this from a registry — it only ever uses what you've imported.
`kubectl rollout restart deployment/gitfed -n gitfed` — importing a new
image under the same tag does not restart pods that are already running.
+**For every update after the first**, use `deploy/update.sh` instead of the
+steps above — it does the same build/import/rollout, but tags the image
+with the actual version (`gitfed:0.3.1`, not just `:latest`) so
+`kubectl rollout history` means something, bumps `VERSION`, and commits.
+Add the new version's `## X.Y.Z` section to `CHANGELOG.md` first, then:
+
+```sh
+deploy/update.sh # patch bump
+deploy/update.sh minor # or minor/major/an explicit X.Y.Z
+```
+
## 2. DNS
Add one more A/AAAA record to the same zone ess-helm's subdomains live in:
deploy/k8s/deployment.yaml
diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml
index e4ee6ca..f5a38c1 100644
--- a/deploy/k8s/deployment.yaml
+++ b/deploy/k8s/deployment.yaml
@@ -44,7 +44,7 @@ spec:
fsGroup: 1000
containers:
- name: server
- image: gitfed:latest
+ image: gitfed:0.3.0
imagePullPolicy: Never
command: ["/usr/local/bin/gitfed-server", "-config", "/etc/gitfed/gitfed.json"]
ports:
@@ -75,7 +75,7 @@ spec:
periodSeconds: 20
- name: web
- image: gitfed:latest
+ image: gitfed:0.3.0
imagePullPolicy: Never
command:
- sh
deploy/update.sh
diff --git a/deploy/update.sh b/deploy/update.sh
new file mode 100755
index 0000000..080bb97
--- /dev/null
+++ b/deploy/update.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# One-command release: bump VERSION, build+import the image on the VPS
+# tagged with that version (not just :latest, so `kubectl rollout history`
+# and `get pods -o yaml` actually show what's running), point
+# deploy/k8s/deployment.yaml at it, apply, and wait for the rollout.
+#
+# Usage:
+# deploy/update.sh # bump patch (0.3.0 -> 0.3.1)
+# deploy/update.sh minor # 0.3.0 -> 0.4.0
+# deploy/update.sh major # 0.3.0 -> 1.0.0
+# deploy/update.sh 0.5.0 # explicit version
+#
+# Add the new version's entry to CHANGELOG.md yourself before running this
+# — the script won't guess what changed. It commits the VERSION bump (and
+# CHANGELOG.md, if you've staged it) as "Release vX.Y.Z".
+
+set -euo pipefail
+cd "$(dirname "${BASH_SOURCE[0]}")/.."
+
+VPS_HOST="${GITFED_VPS_HOST:-ubuntu@51.77.215.92}"
+VPS_SRC_DIR="${GITFED_VPS_SRC_DIR:-~/gitfed-src}"
+NAMESPACE="gitfed"
+
+current="$(cat VERSION)"
+
+bump_patch() { IFS=. read -r maj min pat <<<"$1"; echo "$maj.$min.$((pat + 1))"; }
+bump_minor() { IFS=. read -r maj min _ <<<"$1"; echo "$maj.$((min + 1)).0"; }
+bump_major() { IFS=. read -r maj _ _ <<<"$1"; echo "$((maj + 1)).0.0"; }
+
+case "${1:-patch}" in
+ patch) new_version="$(bump_patch "$current")" ;;
+ minor) new_version="$(bump_minor "$current")" ;;
+ major) new_version="$(bump_major "$current")" ;;
+ [0-9]*.[0-9]*.[0-9]*) new_version="$1" ;;
+ *) echo "usage: $0 [patch|minor|major|X.Y.Z]" >&2; exit 1 ;;
+esac
+
+if ! grep -q "^## $new_version\$" CHANGELOG.md; then
+ echo "error: CHANGELOG.md has no '## $new_version' entry yet — add one first." >&2
+ exit 1
+fi
+
+echo "==> $current -> $new_version"
+echo "$new_version" > VERSION
+
+git add VERSION CHANGELOG.md
+git commit -m "Release v$new_version"
+
+echo "==> syncing source to $VPS_HOST:$VPS_SRC_DIR"
+rsync -az --delete \
+ --exclude='.git' --exclude='bin' --exclude='demo' --exclude='.claude' --exclude='*.db' \
+ ./ "$VPS_HOST:$VPS_SRC_DIR/"
+
+echo "==> building gitfed:$new_version on the VPS"
+ssh "$VPS_HOST" "cd $VPS_SRC_DIR && docker build -f deploy/docker/Dockerfile -t gitfed:$new_version -t gitfed:latest ."
+
+echo "==> importing into k3s containerd"
+ssh "$VPS_HOST" "docker save gitfed:$new_version | sudo k3s ctr images import -"
+
+echo "==> pointing deployment.yaml at gitfed:$new_version"
+sed -i.bak "s|image: gitfed:.*|image: gitfed:$new_version|" deploy/k8s/deployment.yaml
+rm -f deploy/k8s/deployment.yaml.bak
+rsync -az deploy/k8s/deployment.yaml "$VPS_HOST:$VPS_SRC_DIR/deploy/k8s/deployment.yaml"
+
+echo "==> applying and rolling out"
+ssh "$VPS_HOST" "kubectl -n $NAMESPACE apply -f $VPS_SRC_DIR/deploy/k8s/deployment.yaml && kubectl -n $NAMESPACE rollout status deployment/gitfed --timeout=90s"
+
+git add deploy/k8s/deployment.yaml
+git commit -m "Deploy v$new_version"
+
+echo "==> done: v$new_version is live"
internal/version/version.go
diff --git a/internal/version/version.go b/internal/version/version.go
new file mode 100644
index 0000000..722d0ed
--- /dev/null
+++ b/internal/version/version.go
@@ -0,0 +1,13 @@
+// Package version holds gitfed's version string, the single source of
+// truth other packages (the well-known federation doc, the web UI footer)
+// display it from.
+package version
+
+// Version is overridden at build time via
+//
+// go build -ldflags "-X gitfed/internal/version.Version=X.Y.Z"
+//
+// See deploy/docker/Dockerfile (reads VERSION at the repo root) and
+// deploy/update.sh (bumps VERSION). Binaries built without that flag (e.g.
+// `go build ./...` during development) report "dev".
+var Version = "dev"