Files
jellytau/specs/build-provenance.html
T

466 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE HTML>
<html lang="en" class="navy" dir="ltr">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Build Provenance - JellyTau Documentation</title>
<!-- Custom HTML head -->
<meta name="description" content="Requirements, traceability, and architecture for the JellyTau Jellyfin client.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
<link rel="icon" href="../favicon.svg">
<link rel="shortcut icon" href="../favicon.png">
<link rel="stylesheet" href="../css/variables.css">
<link rel="stylesheet" href="../css/general.css">
<link rel="stylesheet" href="../css/chrome.css">
<link rel="stylesheet" href="../css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="../FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="../fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="../highlight.css">
<link rel="stylesheet" href="../tomorrow-night.css">
<link rel="stylesheet" href="../ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body class="sidebar-visible no-js">
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "../";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "navy";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('navy')
html.classList.add(theme);
var body = document.querySelector('body');
body.classList.remove('no-js')
body.classList.add('js');
</script>
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var body = document.querySelector('body');
var sidebar = null;
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
sidebar_toggle.checked = sidebar === 'visible';
body.classList.remove('sidebar-visible');
body.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><a href="../index.html">Introduction</a></li><li class="chapter-item expanded affix "><li class="part-title">Requirements & Traceability</li><li class="chapter-item expanded "><a href="../requirements.html"><strong aria-hidden="true">1.</strong> Requirements Specification</a></li><li class="chapter-item expanded "><a href="../traceability.html"><strong aria-hidden="true">2.</strong> Traceability Matrix</a></li><li class="chapter-item expanded "><a href="../traceability-ci.html"><strong aria-hidden="true">3.</strong> Traceability CI</a></li><li class="chapter-item expanded "><a href="../traces-quick-ref.html"><strong aria-hidden="true">4.</strong> Traces Quick Reference</a></li><li class="chapter-item expanded affix "><li class="part-title">Architecture</li><li class="chapter-item expanded "><a href="../architecture/index.html"><strong aria-hidden="true">5.</strong> Overview</a><a class="toggle"><div></div></a></li><li><ol class="section"><li class="chapter-item "><a href="../architecture/01-rust-backend.html"><strong aria-hidden="true">5.1.</strong> Rust Backend</a></li><li class="chapter-item "><a href="../architecture/02-svelte-frontend.html"><strong aria-hidden="true">5.2.</strong> Svelte Frontend</a></li><li class="chapter-item "><a href="../architecture/03-data-flow.html"><strong aria-hidden="true">5.3.</strong> Data Flow</a></li><li class="chapter-item "><a href="../architecture/04-type-sync-and-threading.html"><strong aria-hidden="true">5.4.</strong> Type Sync & Threading</a></li><li class="chapter-item "><a href="../architecture/05-platform-backends.html"><strong aria-hidden="true">5.5.</strong> Platform Backends</a></li><li class="chapter-item "><a href="../architecture/06-downloads-and-offline.html"><strong aria-hidden="true">5.6.</strong> Downloads & Offline</a></li><li class="chapter-item "><a href="../architecture/07-connectivity.html"><strong aria-hidden="true">5.7.</strong> Connectivity</a></li><li class="chapter-item "><a href="../architecture/08-database-design.html"><strong aria-hidden="true">5.8.</strong> Database Design</a></li><li class="chapter-item "><a href="../architecture/09-security.html"><strong aria-hidden="true">5.9.</strong> Security</a></li></ol></li><li class="chapter-item expanded "><li class="part-title">UX</li><li class="chapter-item expanded "><a href="../ux-flows.html"><strong aria-hidden="true">6.</strong> UX Flows</a></li><li class="chapter-item expanded affix "><li class="part-title">Specs — Pending Work</li><li class="chapter-item expanded "><a href="../specs/index.html"><strong aria-hidden="true">7.</strong> Specs Index</a></li><li class="chapter-item expanded "><a href="../specs/SPEC-TEMPLATE.html"><strong aria-hidden="true">8.</strong> Spec Template</a></li><li class="chapter-item expanded "><a href="../specs/SPEC-REVIEW-CHECKLIST.html"><strong aria-hidden="true">9.</strong> Spec Review Checklist</a></li><li class="chapter-item expanded "><a href="../specs/playback-backend-unification.html"><strong aria-hidden="true">10.</strong> Playback Backend Unification</a></li><li class="chapter-item expanded "><a href="../specs/linux-native-video-spike.html"><strong aria-hidden="true">11.</strong> Linux Native Video Spike</a></li><li class="chapter-item expanded "><a href="../specs/backend-owned-stream-selection.html"><strong aria-hidden="true">12.</strong> Backend-Owned Stream Selection</a></li><li class="chapter-item expanded "><a href="../specs/player-facade-enforcement.html"><strong aria-hidden="true">13.</strong> Player Facade Enforcement</a></li><li class="chapter-item expanded "><a href="../specs/windows-native-audio-backend.html"><strong aria-hidden="true">14.</strong> Windows Native Audio Backend</a></li><li class="chapter-item expanded "><a href="../specs/libmpv2-migration.html"><strong aria-hidden="true">15.</strong> libmpv2 Migration</a></li><li class="chapter-item expanded "><a href="../specs/read-through-media-cache.html"><strong aria-hidden="true">16.</strong> Read-Through Media Cache</a></li><li class="chapter-item expanded "><a href="../specs/scoped-search.html"><strong aria-hidden="true">17.</strong> Scoped Search</a></li><li class="chapter-item expanded "><a href="../specs/scoped-search-boundary.html"><strong aria-hidden="true">18.</strong> Scoped Search Boundary</a></li><li class="chapter-item expanded "><a href="../specs/scoped-search-boundary-implementation.html"><strong aria-hidden="true">19.</strong> Scoped Search Boundary — Implementation</a></li><li class="chapter-item expanded "><a href="../specs/frontend-domain-model.html"><strong aria-hidden="true">20.</strong> Frontend Domain Model</a></li><li class="chapter-item expanded "><a href="../specs/desktop-native-video.html"><strong aria-hidden="true">21.</strong> Desktop Native Video</a></li><li class="chapter-item expanded "><a href="../specs/build-provenance.html" class="active"><strong aria-hidden="true">22.</strong> Build Provenance</a></li><li class="chapter-item expanded affix "><li class="part-title">Build & Release</li><li class="chapter-item expanded "><a href="../build/build-release.html"><strong aria-hidden="true">23.</strong> Build & Release</a></li><li class="chapter-item expanded "><a href="../release-checklist.html"><strong aria-hidden="true">24.</strong> Release Checklist</a></li><li class="chapter-item expanded "><a href="../native-player-verification.html"><strong aria-hidden="true">25.</strong> Native Player Verification</a></li><li class="chapter-item expanded "><a href="../build/build-desktop-packages.html"><strong aria-hidden="true">26.</strong> Desktop Packaging</a></li><li class="chapter-item expanded "><a href="../build/build-windows.html"><strong aria-hidden="true">27.</strong> Windows Build</a></li><li class="chapter-item expanded "><a href="../defect-windows.html"><strong aria-hidden="true">28.</strong> Defect Windows</a></li><li class="chapter-item expanded "><a href="../build/docker.html"><strong aria-hidden="true">29.</strong> Docker</a></li><li class="chapter-item expanded "><a href="../build/build-builder-image.html"><strong aria-hidden="true">30.</strong> Builder Image</a></li><li class="chapter-item expanded affix "><li class="spacer"></li><li class="chapter-item expanded affix "><a href="../api-redirect.html">Rust API Reference (rustdoc)</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
<div class="sidebar-resize-indicator"></div>
</div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</label>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">JellyTau Documentation</h1>
<div class="right-buttons">
<a href="../print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
<a href="https://gitea.tourolle.paris/dtourolle/jellytau" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa fa-github"></i>
</a>
<a href="https://gitea.tourolle.paris/dtourolle/jellytau/_edit/master/docs/../docs/specs/build-provenance.md" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="spec-build-provenance-git-describe--build-profile"><a class="header" href="#spec-build-provenance-git-describe--build-profile">Spec: Build provenance (git describe + build profile)</a></h1>
<p><strong>Status:</strong> Proposed — not started. <code>src-tauri/build.rs</code> still contains only
<code>tauri_build::build()</code>, and nothing reports a version over IPC. Note that
<code>scripts/set-version.sh</code> has since landed, which changes the "three hand-bumped
files" premise below: versions are now stamped from one place.
<strong>Requirements:</strong> ⚠️ the suggested id <strong>DR-093 has since been allocated</strong> to the
traceability coverage gate — allocate a fresh id (DR-215 or later) on
implementation. Build provenance surfaced in-app and in logs; no UR — this is a
diagnostic capability, not a user feature
<strong>UX spec:</strong> n/a — adds an About block to Settings; no new flow
<strong>Supersedes / revises:</strong></p>
<h2 id="summary"><a class="header" href="#summary">Summary</a></h2>
<p>Make every build say exactly what it is. Today a running JellyTau reports no
version at all — not in the UI, not in the logs — and the only version string in
the tree is the hand-maintained <code>0.2.0</code> duplicated across three files.</p>
<p>This adds a <code>build.rs</code>-generated provenance string (<code>git describe</code> + short SHA +
dirty flag + debug/release profile), exposes it over IPC, and renders it in a new
Settings About block. It also removes one of the three hand-bumped version
files.</p>
<h2 id="motivation"><a class="header" href="#motivation">Motivation</a></h2>
<p>The concrete problem: when a user reports "the equalizer does nothing on my
device" — which is a live risk for v0.2.0, whose Android audio settings are not
yet device-verified — there is currently no way to tell which build they are
running. Tag? Master? A local debug build from three weeks ago? The bug report
cannot distinguish them.</p>
<p>Two smaller irritations this also fixes:</p>
<ul>
<li><strong>Debug builds masquerade as releases.</strong> <code>0.2.0</code> is <code>0.2.0</code> whether it came
from a tagged release or <code>bun run tauri dev</code>.</li>
<li><strong>Three files carry the version.</strong> <code>package.json</code>, <code>src-tauri/Cargo.toml</code> and
<code>src-tauri/tauri.conf.json</code> must be bumped in lockstep; the release checklist
exists partly to stop them drifting.</li>
</ul>
<h3 id="what-this-deliberately-does-not-do"><a class="header" href="#what-this-deliberately-does-not-do">What this deliberately does <em>not</em> do</a></h3>
<p><strong>The canonical version stays hand-bumped in <code>Cargo.toml</code>.</strong> Cargo requires a
literal semver string at manifest-parse time and cannot derive it from git. The
same is true of <code>tauri.conf.json</code>. Attempting to source the <em>release</em> version
from a tag trades a scripted, reviewable bump for a fragile build-time
dependency that breaks in exactly the environment we care most about (CI, in
Docker, from a shallow clone).</p>
<p>So: <strong>the release version is authored; the build provenance is derived.</strong> They
answer different questions — "what release is this?" versus "what commit is this
binary actually built from?" — and only the second benefits from git.</p>
<h2 id="layer-assignment"><a class="header" href="#layer-assignment">Layer assignment</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Logic / responsibility</th><th>Layer</th><th>Why it belongs there</th></tr></thead><tbody>
<tr><td>Capturing git describe / SHA / dirty state at compile time</td><td>Rust (<code>build.rs</code>)</td><td>Only the Rust build has a compile step that can shell out to git and bake the result into the binary. A frontend equivalent would report the <em>dev server's</em> state, not the shipped binary's.</td></tr>
<tr><td>Degrading to a sentinel when git is unavailable</td><td>Rust (<code>build.rs</code>)</td><td>Build-environment concern. Must never fail the build — CI runs in Docker from a shallow clone.</td></tr>
<tr><td>Release version (<code>0.2.0</code>)</td><td>Rust (<code>Cargo.toml</code>, authored)</td><td>Domain fact about the product, not derivable from the environment.</td></tr>
<tr><td>Deciding <em>what a build is</em> (release / dev / dirty)</td><td>Rust</td><td>Domain classification. The frontend must not infer "this is a dev build" from a string shape — it renders what it is told.</td></tr>
<tr><td>Rendering the About block, copy-to-clipboard</td><td>Frontend</td><td>Pure presentation.</td></tr>
</tbody></table>
</div>
<p>Borderline row: the release/dev/dirty classification could be done in the
frontend by pattern-matching the describe string. It goes to Rust because that is
a <em>rule about what constitutes a release build</em>, and it would have to change if
the tagging scheme changed — the litmus test in the template puts that in Rust.
Send a typed enum, not a string for the frontend to parse.</p>
<h2 id="design"><a class="header" href="#design">Design</a></h2>
<h3 id="buildrs"><a class="header" href="#buildrs"><code>build.rs</code></a></h3>
<pre><pre class="playground"><code class="language-rust">fn main() {
emit_build_provenance();
tauri_build::build()
}
fn emit_build_provenance() {
let describe = std::process::Command::new("git")
.args(["describe", "--tags", "--always", "--dirty"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
println!("cargo:rustc-env=JELLYTAU_GIT_DESCRIBE={describe}");
// Rebuild when HEAD moves or a ref is written, so the string does not go
// stale across commits. Guarded: these paths do not exist in a git-less
// source tarball, and emitting rerun-if-changed for a missing path would
// force a rebuild every time.
for p in [".git/HEAD", ".git/refs"] {
if std::path::Path::new("../").join(p).exists() {
println!("cargo:rerun-if-changed=../{p}");
}
}
}</code></pre></pre>
<p>🔴 <strong><code>build.rs</code> must never fail the build.</strong> Every git call is
<code>.ok()</code>-swallowed; a missing git binary, a shallow clone, or a source tarball all
yield <code>"unknown"</code>. A build that breaks because git is absent would be a worse bug
than the one this fixes.</p>
<p>Note the <code>../</code> prefixes: <code>build.rs</code> runs with CWD at <code>src-tauri/</code>, so the repo's
<code>.git</code> is one level up.</p>
<h3 id="the-provenance-type"><a class="header" href="#the-provenance-type">The provenance type</a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// TRACES: DR-093
#[derive(specta::Type, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildInfo {
/// Authored release version (Cargo.toml).
pub version: String,
/// `git describe --tags --always --dirty`, or "unknown".
pub git_describe: String,
/// What kind of build this is — classified in Rust, not inferred by the UI.
pub kind: BuildKind,
}
/// TRACES: DR-093
#[derive(specta::Type, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum BuildKind {
/// Built from a clean, exactly-tagged commit in release mode.
Release,
/// Release-mode build that is not on a clean tag (e.g. master, or dirty).
Untagged,
/// debug_assertions build.
Development,
/// Git state unavailable at build time.
Unknown,
}
<span class="boring">}</span></code></pre></pre>
<p>Classification:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let kind = if cfg!(debug_assertions) {
BuildKind::Development
} else if describe == "unknown" {
BuildKind::Unknown
} else if describe.contains('-') { // "v0.2.0-3-gcb79a37" or "...-dirty"
BuildKind::Untagged
} else {
BuildKind::Release
};
<span class="boring">}</span></code></pre></pre>
<h3 id="command"><a class="header" href="#command">Command</a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>/// TRACES: DR-093
#[tauri::command]
#[specta::specta]
pub fn get_build_info() -&gt; BuildInfo { … }
<span class="boring">}</span></code></pre></pre>
<p>No parameters, so the camelCase param rule does not apply; the struct fields do
need <code>#[serde(rename_all = "camelCase")]</code> (above). Regenerate <code>bindings.ts</code>.</p>
<p>Also log the provenance once at startup, next to the existing init logging —
that is what makes a user-submitted log file self-identifying, which is most of
the value.</p>
<h3 id="settings--about"><a class="header" href="#settings--about">Settings About</a></h3>
<p>A new block at the bottom of <code>src/routes/settings/+page.svelte</code>, rendering
version, describe string, and a badge for non-release builds. One
copy-to-clipboard button that yields a paste-ready block for bug reports:</p>
<pre><code>JellyTau 0.2.0 (v0.2.0-3-gcb79a37-dirty, development)
linux x86_64
</code></pre>
<p>Platform/arch come from the existing Tauri APIs; do not shell out.</p>
<h3 id="removing-one-version-file"><a class="header" href="#removing-one-version-file">Removing one version file</a></h3>
<p><code>tauri.conf.json</code>'s <code>"version"</code> field can be omitted, in which case Tauri falls
back to the Cargo version. That takes the bump from three files to two.</p>
<p><strong>Verify before adopting</strong>: confirm the Android <code>versionName</code>/<code>versionCode</code> and
the NSIS installer version still resolve correctly with the field absent —
Android packaging in particular reads the Tauri config. If either regresses,
keep the field and drop this part; it is a convenience, not the point of the
spec.</p>
<h2 id="out-of-scope"><a class="header" href="#out-of-scope">Out of scope</a></h2>
<ul>
<li>Deriving the <em>release</em> version from git tags (see Motivation).</li>
<li>A build-time timestamp. It defeats reproducible builds and adds little over
the commit SHA.</li>
<li>CI provenance/attestation, SBOM, signing.</li>
<li>Displaying the Jellyfin server version (separate concern, already available
from <code>/System/Info</code>).</li>
</ul>
<h2 id="acceptance-criteria"><a class="header" href="#acceptance-criteria">Acceptance criteria</a></h2>
<ul>
<li><input disabled="" type="checkbox"/>
<code>cargo build</code> succeeds with git absent, from a shallow clone, and from a source tarball with no <code>.git</code> — yielding <code>"unknown"</code> in each case, never a build failure.</li>
<li><input disabled="" type="checkbox"/>
A tagged clean release build reports <code>BuildKind::Release</code>; <code>bun run tauri dev</code> reports <code>Development</code>; a dirty tree reports <code>Untagged</code> (release mode) with <code>-dirty</code> in the describe string.</li>
<li><input disabled="" type="checkbox"/>
The describe string changes after a new commit without a manual <code>cargo clean</code> (rerun-if-changed works).</li>
<li><input disabled="" type="checkbox"/>
Provenance is logged once at startup.</li>
<li><input disabled="" type="checkbox"/>
Settings About renders version + describe + build-kind badge, with working copy-to-clipboard.</li>
<li><input disabled="" type="checkbox"/>
🔴 CI checkouts that build a shippable artifact set <code>fetch-depth: 0</code>, or their artifacts are knowingly stamped <code>unknown</code>. Currently only <code>publish-docs.yml</code> sets it; <code>build-release.yml</code> has five checkouts and <code>build-and-test.yml</code> two, all of which would report <code>unknown</code> as-is.</li>
<li><input disabled="" type="checkbox"/>
<strong>No toolchain installed in CI</strong> — git is already present in the builder image; nothing new is added.</li>
<li><input disabled="" type="checkbox"/>
<code>bun run check</code>, <code>bun run test</code>, <code>bun run check:boundary</code> pass.</li>
<li><input disabled="" type="checkbox"/>
<code>cargo fmt</code> clean, <code>cargo clippy</code> clean, <code>bun run test:rust</code> passes.</li>
<li><input disabled="" type="checkbox"/>
<code>bindings.ts</code> regenerated.</li>
<li><input disabled="" type="checkbox"/>
DR-093 allocated in <code>requirements.md</code>; new code carries <code>// TRACES:</code>.</li>
</ul>
<h2 id="testing"><a class="header" href="#testing">Testing</a></h2>
<p><strong>Rust</strong>: the classification is pure and must be extracted from the command as
<code>classify_build(describe: &amp;str, debug: bool) -&gt; BuildKind</code> so it can be tested
directly. Cover: <code>"v0.2.0"</code><code>Release</code>; <code>"v0.2.0-3-gcb79a37"</code><code>Untagged</code>;
<code>"v0.2.0-dirty"</code><code>Untagged</code>; <code>"unknown"</code><code>Unknown</code>; <code>debug = true</code> → always
<code>Development</code> regardless of describe.</p>
<p><code>build.rs</code> itself is not unit-testable. Verify its failure path manually by
building with <code>PATH</code> stripped of git, and from a <code>git archive</code> tarball — both
must succeed with <code>"unknown"</code>.</p>
<p><strong>Frontend</strong>: assert the About block renders each <code>BuildKind</code> correctly, and that
it renders the backend-supplied kind rather than re-deriving it from the string
(a test that passes a <code>Release</code> kind with a <code>-dirty</code> describe and asserts the
badge follows the <em>kind</em> would catch that regression).</p>
<h2 id="traces"><a class="header" href="#traces">TRACES</a></h2>
<ul>
<li><code>build.rs</code> provenance emission → <code>// TRACES: | DR-093</code></li>
<li><code>BuildInfo</code> / <code>BuildKind</code> / <code>classify_build</code><code>// TRACES: | DR-093</code></li>
<li><code>get_build_info</code> command → <code>// TRACES: | DR-093</code></li>
<li>Settings About block → <code>// TRACES: | DR-093</code></li>
<li><code>classify_build</code> tests → <code>UT-BUILD-1</code></li>
<li>Allocate <strong>DR-093</strong> in <code>requirements.md</code> ("Build provenance: git describe and
build profile surfaced in-app and in logs"). Next free DR at time of writing
is DR-093.</li>
</ul>
<h2 id="notes-for-the-implementer"><a class="header" href="#notes-for-the-implementer">Notes for the implementer</a></h2>
<ul>
<li>Do the <code>build.rs</code> + command + logging first; the About UI is the smaller half
and the logging alone delivers most of the diagnostic value.</li>
<li>The <code>fetch-depth: 0</code> change is the easiest part to forget and the one that
makes CI artifacts useless if missed — it is why that acceptance box is
flagged. Weigh it per workflow: test-only jobs do not need it.</li>
<li>Do not add a build timestamp "while you are in there" — see Out of scope.</li>
<li>A parallel Claude session may be active — <code>git diff</code> before "repairing"
unexpected changes.</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<a rel="prev" href="../specs/desktop-native-video.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../build/build-release.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
<a rel="prev" href="../specs/desktop-native-video.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
<a rel="next prefetch" href="../build/build-release.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="../elasticlunr.min.js"></script>
<script src="../mark.min.js"></script>
<script src="../searcher.js"></script>
<script src="../clipboard.min.js"></script>
<script src="../highlight.js"></script>
<script src="../book.js"></script>
<!-- Custom JS scripts -->
</div>
</body>
</html>