Add deployment info footer
No way to tell which version is running after a deploy.
The footer shows change ID, short SHA, and build date on
every page. In development, config.ru resolves these from
jj at startup; in production, the Dockerfile bakes them
from CI build args.

Assisted-by: Claude Opus 4.6 via Claude Code
change knpqpnktluymosmomtvwmklslrsqyvuy
commit 19c609c34cbf892af3506cc24897435f352bd239
author Alpha Chen <alpha@kejadlen.dev>
date
parent vnywozxl
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dc9b0a2..1e087cf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -37,6 +37,8 @@ jobs:
           password: ${{ secrets.GITHUB_TOKEN }}
       - id: short-sha
         run: echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
+      - id: change-id
+        run: echo "id=$(git cat-file -p HEAD | grep '^change-id ' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT"
       - uses: docker/metadata-action@v5
         id: meta
         with:
@@ -50,6 +52,10 @@ jobs:
           push: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
+          build-args: |
+            COMMIT_SHA=${{ steps.short-sha.outputs.sha }}
+            CHANGE_ID=${{ steps.change-id.outputs.id }}
+            BUILD_DATE=${{ github.event.head_commit.timestamp }}
           cache-from: type=gha
           cache-to: type=gha,mode=max
       - if: github.ref == 'refs/heads/main' && github.event_name == 'push'
diff --git a/BACKLOG.md b/BACKLOG.md
index 5b10f90..b31892f 100644
--- a/BACKLOG.md
+++ b/BACKLOG.md
@@ -25,3 +25,4 @@
 - Fix pages triggering from release
 - Improve the diff page to highlight differences
 - Status endpoint with debug information
+- Ship the gallery in the docker image
diff --git a/Dockerfile b/Dockerfile
index 0e4f958..6a22be3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,6 +11,13 @@ COPY . .
 
 FROM ruby:4.0-slim
 
+ARG COMMIT_SHA
+ARG CHANGE_ID
+ARG BUILD_DATE
+ENV COMMIT_SHA=${COMMIT_SHA}
+ENV CHANGE_ID=${CHANGE_ID}
+ENV BUILD_DATE=${BUILD_DATE}
+
 WORKDIR /app
 
 COPY --from=build /usr/local/bundle /usr/local/bundle
diff --git a/config.ru b/config.ru
index a34f0cc..431becc 100644
--- a/config.ru
+++ b/config.ru
@@ -2,6 +2,12 @@
 
 $LOAD_PATH.unshift(File.expand_path("lib", __dir__))
 
+unless ENV["COMMIT_SHA"]
+  ENV["COMMIT_SHA"] = `jj log -r @ --no-graph -T 'commit_id.short(7)' 2>/dev/null`.strip
+  ENV["CHANGE_ID"] = `jj log -r @ --no-graph -T 'change_id.short(8)' 2>/dev/null`.strip
+  ENV["BUILD_DATE"] = Time.now.strftime("%Y-%m-%d")
+end
+
 require_relative "lib/ketchup/web"
 
 $stderr.puts CONFIG
diff --git a/lib/ketchup/config.rb b/lib/ketchup/config.rb
index b7df85b..93b53e8 100644
--- a/lib/ketchup/config.rb
+++ b/lib/ketchup/config.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-Config = Data.define(:database_url, :sentry, :default_user)
+Config = Data.define(:database_url, :sentry, :default_user, :commit_sha, :change_id, :build_date)
 
 class Config
   SentryConfig = Data.define(:dsn, :env)
@@ -15,6 +15,9 @@ class Config
     parts = ["database=#{database_url}"]
     parts << "sentry=#{sentry.env || "on"}" if sentry
     parts << "default_user=#{default_user.login}" if default_user
+    parts << "commit=#{commit_sha}" if commit_sha
+    parts << "change=#{change_id}" if change_id
+    parts << "built=#{build_date}" if build_date
     "Config(#{parts.join(", ")})"
   end
 
@@ -24,7 +27,10 @@ class Config
     new(
       database_url: env.fetch("DATABASE_URL") { "db/ketchup.db" },
       sentry: sentry_dsn ? SentryConfig.new(dsn: sentry_dsn, env: env["SENTRY_ENV"]) : nil,
-      default_user: default_user ? DefaultUser.parse(default_user) : nil
+      default_user: default_user ? DefaultUser.parse(default_user) : nil,
+      commit_sha: env["COMMIT_SHA"],
+      change_id: env["CHANGE_ID"],
+      build_date: env["BUILD_DATE"]
     )
   end
 end
diff --git a/lib/ketchup/views/layout.rb b/lib/ketchup/views/layout.rb
index 912fc0a..f8d6683 100644
--- a/lib/ketchup/views/layout.rb
+++ b/lib/ketchup/views/layout.rb
@@ -30,8 +30,22 @@ module Views
             span(class: "user") { @current_user[:name] || @current_user[:login] }
           end
           yield
+          render_footer
         end
       end
     end
+
+    private
+
+    def render_footer
+      config = CONFIG
+      parts = []
+      parts << config.change_id if config.change_id
+      parts << config.commit_sha if config.commit_sha
+      parts << config.build_date if config.build_date
+      footer(class: "site-footer") do
+        plain parts.join(" \u00b7 ")
+      end
+    end
   end
 end
diff --git a/public/css/app.css b/public/css/app.css
index 1bbd16c..854f697 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -22,6 +22,9 @@ body {
   line-height: 1.5;
   color: #1a1a1a;
   background: #fff;
+  min-height: 100dvh;
+  display: flex;
+  flex-direction: column;
 }
 
 a {
@@ -52,6 +55,19 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   color: #777;
 }
 
+/* -------------------- */
+/* Site footer */
+/* -------------------- */
+
+.site-footer {
+  margin-block-start: auto;
+  border-block-start: 1px dotted #ccc;
+  padding: var(--space-xs) var(--grid-gutter);
+  font-size: var(--step--2);
+  color: #999;
+  text-align: center;
+}
+
 /* -------------------- */
 /* Page wrapper */
 /* -------------------- */