Show the build version in the UI and --version
CI passes the release image tag in as DOCKET_VERSION, and previews pass
pr-<number>-<sha>. Builds without it are named by build.rs after the
date and the jj change they came from, with a -dev suffix, so a local
build is never mistaken for a release.
The version sits in the bottom-right corner, in a strip the app grid now
leaves free below the panes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9b8853f..8c53c19 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -78,6 +78,7 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+ build-args: DOCKET_VERSION=${{ steps.meta.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
- env:
@@ -120,6 +121,8 @@ jobs:
# A build of its own from fly.toml, with the `dev` feature the
# release image leaves out. It still waits on the release, so the
- # demo only runs commits that shipped.
+ # demo only runs commits that shipped, and carries its version.
- name: Deploy a demo build
- run: flyctl deploy --app "$FLY_APP" --config fly.toml --remote-only
+ env:
+ NEEDS_BUILD_OUTPUTS_VERSION: ${{ needs.build.outputs.version }}
+ run: flyctl deploy --app "$FLY_APP" --config fly.toml --remote-only --build-arg "DOCKET_VERSION=$NEEDS_BUILD_OUTPUTS_VERSION"
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index 12284e4..c72ec3c 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -31,6 +31,14 @@ jobs:
- uses: superfly/flyctl-actions/setup-flyctl@ed8efb33836e8b2096c7fd3ba1c8afe303ebbff1 # 1.6
+ # Names the build after the PR and the commit it shows.
+ - name: Version
+ id: version
+ env:
+ PR: ${{ github.event.number }}
+ SHA: ${{ github.event.pull_request.head.sha }}
+ run: echo "version=pr-${PR}-${SHA::7}" >> "$GITHUB_OUTPUT"
+
# Creates the app on open, redeploys it on each push, and destroys
# it when the PR closes.
- name: Deploy preview
@@ -39,3 +47,4 @@ jobs:
with:
config: fly.toml
memory: 256
+ build_args: DOCKET_VERSION=${{ steps.version.outputs.version }}
diff --git a/Dockerfile b/Dockerfile
index 67588cf..982fd80 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -22,6 +22,8 @@ COPY . .
# Cargo features to build with. Empty for the release image; the Fly demos
# set `dev` (see fly.toml).
ARG FEATURES=""
+# Shown in the UI and by --version; build.rs names local builds without it.
+ARG DOCKET_VERSION=""
# Copy the binary out of the cache mount so it survives into the runtime
# stage.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
diff --git a/assets/docket.css b/assets/docket.css
index 4e2ef22..88a25a6 100644
--- a/assets/docket.css
+++ b/assets/docket.css
@@ -12,7 +12,7 @@ button { font: inherit; color: inherit; }
/* Three panes on the fill: bare mailboxes, the list on a card, the thread
as a stack of cards. */
-.app { height: 100vh; display: grid; grid-template-columns: 200px 420px minmax(0, 1fr); gap: var(--gl-space-2xs); padding: var(--gl-space-2xs); background: var(--gl-color-surface-fill); }
+.app { height: 100vh; display: grid; grid-template-columns: 200px 420px minmax(0, 1fr); gap: var(--gl-space-2xs); padding: var(--gl-space-2xs) var(--gl-space-2xs) var(--gl-space-s); background: var(--gl-color-surface-fill); }
/* Gloss Input: a field on a rule. */
.field { box-sizing: border-box; width: 100%; border: none; border-bottom: var(--gl-border-w) solid var(--gl-color-border-default); border-radius: 0; background: transparent; padding: 8px 2px; font: var(--gl-step--1) var(--gl-font-text); color: var(--gl-color-text-primary); outline: none; }
@@ -33,6 +33,8 @@ button { font: inherit; color: inherit; }
/* ── mailboxes ── */
.side { padding: var(--gl-space-m) var(--gl-space-s) var(--gl-space-2xs); display: flex; flex-direction: column; gap: var(--gl-space-m); min-height: 0; overflow-y: auto; }
.brand-row { display: flex; justify-content: space-between; align-items: center; }
+/* Centered in the strip .app leaves free at the bottom right. */
+.version { position: fixed; right: var(--gl-space-2xs); bottom: calc((var(--gl-space-s) - 1em) / 2); text-transform: none; letter-spacing: 0; color: var(--gl-color-text-tertiary); }
.search-btn { all: unset; display: flex; cursor: pointer; color: var(--gl-color-text-tertiary); transition: color var(--gl-dur-fast) var(--gl-ease); }
.search-btn:hover { color: var(--gl-color-text-primary); }
.search-btn:focus-visible { outline: 2px solid var(--gl-color-accent); outline-offset: 2px; border-radius: 2px; }
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..654d643
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,34 @@
+use std::process::Command;
+
+// CI passes the release's image tag as DOCKET_VERSION; anything else is a
+// local build, named for the day and the jj change it came from.
+fn main() {
+ println!("cargo:rerun-if-env-changed=DOCKET_VERSION");
+ let version = std::env::var("DOCKET_VERSION")
+ .ok()
+ .filter(|v| !v.is_empty());
+ let version = version.unwrap_or_else(|| {
+ // Every jj operation replaces the op head, so a new change renames
+ // the build.
+ println!("cargo:rerun-if-changed=.jj/repo/op_heads/heads");
+ let date = cmd("date", &["-u", "+%Y-%m-%d"]);
+ let change = cmd(
+ "jj",
+ &["log", "-r", "@", "--no-graph", "-T", "change_id.short()"],
+ );
+ format!("{date}+{change}-dev")
+ });
+ println!("cargo:rustc-env=DOCKET_VERSION={version}");
+}
+
+fn cmd(program: &str, args: &[&str]) -> String {
+ Command::new(program)
+ .args(args)
+ .output()
+ .ok()
+ .filter(|o| o.status.success())
+ .and_then(|o| String::from_utf8(o.stdout).ok())
+ .map(|s| s.trim().to_owned())
+ .filter(|s| !s.is_empty())
+ .unwrap_or_else(|| "unknown".into())
+}
diff --git a/src/bin/docket/config.rs b/src/bin/docket/config.rs
index 4674174..3099c4c 100644
--- a/src/bin/docket/config.rs
+++ b/src/bin/docket/config.rs
@@ -38,7 +38,7 @@ impl Config {
/// Settings come from the KDL file; the CLI only says where to find it.
#[derive(Debug, clap::Parser)]
-#[command(version, about = "Shared household email triage over JMAP")]
+#[command(version = docket::VERSION, about = "Shared household email triage over JMAP")]
pub struct Args {
/// KDL file to read settings from.
#[arg(long, env = "DOCKET_CONFIG", default_value = "docket.kdl")]
diff --git a/src/lib.rs b/src/lib.rs
index 1868bc8..13912ac 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,3 +10,6 @@ pub mod store;
pub mod views;
pub use error::Error;
+
+/// Which build this is: the release's image tag, or a local `-dev` build.
+pub const VERSION: &str = env!("DOCKET_VERSION");
diff --git a/src/views.rs b/src/views.rs
index 877a9fe..ab5db15 100644
--- a/src/views.rs
+++ b/src/views.rs
@@ -133,6 +133,7 @@ pub fn page(p: &Page<'_>) -> Markup {
(list(p))
(reader(p))
}
+ span.type-label.version { (crate::VERSION) }
@if let Some(flash) = &p.flash {
(toast(flash, p.here))
}
diff --git a/tests/api.rs b/tests/api.rs
index 0a90e38..64ec318 100644
--- a/tests/api.rs
+++ b/tests/api.rs
@@ -138,6 +138,12 @@ async fn for_me_lists_messages_grouped_by_state_and_thread() {
// Rows carry sender, age and snippet; values live in the thread.
assert!(body.contains(r#"<span class="from">Northwind Roofing</span>"#));
assert!(!body.contains("FOLDERS"));
+ // The page names the build.
+ let version = format!(
+ r#"<span class="type-label version">{}</span>"#,
+ docket::VERSION
+ );
+ assert!(body.contains(&version));
}
#[tokio::test]