Hedronite · Curriculum · DevOps · Frontend · Mon 2026-11-09

A release history page that never shows a soaking release as good A static export in Bun and TypeScript

Five real deploys in, five pages out: only a release whose soak window closed with its budget intact gets the good marker, and a ledger that disagrees with its report stops the build.

Lesson Class: Curriculum · F-DevOps · rung A
Author: Evan Eugene Pincham
Tested with: Bun 1.3 (lab image oven/bun:1.3-debian)
Lab: lab/ · sh check.sh (8 checks) · no network, no browser
Source: Kubernetes Deployments and Pod Lifecycle · The Site Reliability Workbook ch.16 · Armstrong thesis ch.6 · DOP-C02 v1.6 Task 1.4 · Bun File I/O · Lustre element · MDN aria-live · WAI-ARIA APG Alert

Lab download. Starter is the tree you edit. Worked answer is a separate file; open it after the starter passes. Bun 1.3 or newer. package.json has no dependencies to install. All lab requirements.

Good Means Confirmed
The good marker is written in the positive: a release earns it only when its soak window closes with the restart budget intact.
Derive, Then Compare
The page walks the ledger and tracks the last confirmed release itself, then checks the report field by field.
Disagreement Stops the Build
A report that calls a soaking release last_good, or a rollback aimed at a release that was itself rolled back, is a build error, not a page.
A release that has crashed once in three seconds has not earned a tick yet; the page waits for the window to close.

A deploy tool can be careful about when it calls a release good. A dashboard built on top of it can still undo that care with one lazy line: "not rolled back, so show a green tick". This lesson builds a release history page where the green marker means one thing only, and where the page refuses to be built if its inputs contradict each other.

The page is a Bun static export written in TypeScript. There is no server and no client-side JavaScript.

What is real in this lesson: the TypeScript, the build, the page HTML and the check output quoted below, all produced by running the lab. What is simulated: the deployer that produced the inputs. It is a decision kernel that reads a fixture plan of timestamped events and decides what to do. It starts no live processes, reads no clock and talks to no cluster. Time moves only when an event's ts field says so. Nothing in this lesson deploys anything.

1. Concept

The inputs. The page is driven by the text output of a small deployer that puts a new release on probation. Here is that deployer in plain words:

It writes three text files per deploy:

  1. The plan: JSONL events, each with ts (milliseconds) and event. Release events carry version.
  2. The apply ledger: JSONL lines naming what the deployer decided (restarting, confirmed, rolled_back, restarted_post_soak).
  3. The report: seven key: value lines (soak_ms, max_restarts, current, last_good, candidate, candidate.restarts, deploy).

Why readiness at one instant is a weak test. Kubernetes has a field for exactly this gap. .spec.minReadySeconds "specifies the minimum number of seconds for which a newly created Pod should be ready without any of its containers crashing, for it to be considered available. This defaults to 0 (the Pod will be considered available as soon as it is ready)." The default is no probation. A separate knob, the progress deadline, surfaces a stalled rollout as a Progressing condition with reason ProgressDeadlineExceeded. Kubernetes takes no action on a stalled Deployment other than to report that condition. So someone or something above it still has to decide what the stall means and display it.

The same applies to any page that says "good". A release that has been serving for three seconds and has already crashed once is not good yet. Showing it as good is a false claim, even if the release later turns out fine. The SRE Workbook's chapter on canarying makes the same point about evaluation. A canary needs enough size and duration to be representative, and ending it after a handful of requests gives no useful signal.

Three statuses, one marker. Every release on the page is in exactly one state:

StatusMeaningGood marker
confirmedThe baseline, or a candidate whose window closed with the budget intactyes
soakingServing, but still on probationno
rolled back (budget)Spent its restart budget inside the window; traffic went back to the last confirmed releaseno

All rendering goes through a single function whose only input is the derived status:

export const isGood = (r: Release): boolean => r.status === "confirmed";

There is no "not rolled back, therefore good" shortcut. The rule is written in the positive, so a status added later is not good until someone deliberately makes it so.

Derive, then cross-check. The page does not copy statuses out of the report. It walks the ledger in order, tracking the last confirmed release itself, and then compares the report to what it derived. The report is only a summary, and a summary can be stale or wrong. When the two disagree, the build stops. The page never silently picks a side. The disagreements the page checks for are:

Failing at build time is the right place for this. A wrong history page is worst when someone is looking at it during an incident. A build that exits non-zero gives a pipeline that stops on failure a reason not to publish.

Why there is no Bun backend. The page reads three files per deploy at build time. Nothing on it changes after the build. A server would serve the same bytes on every request, so any static host does the job. Checking consistency per request would also move the failure to the worst moment, when the page is open. The tool is Bun, but only as a build script and a TypeScript runner. The file work uses Bun.file(...).text() and Bun.write. For mkdir and readdir, the Bun file I/O documentation points to its node:fs implementation, which is what build.ts uses.

A hand-written parser, on purpose. The deployer is a Rust program. This lesson does not generate a TypeScript client from a Rust schema. Its output is plain JSONL and key: value text, and the parser is about thirty-five lines. The cost of writing it by hand is that the parser and the deployer can drift apart. The guard is that the parser is strict:

A new deployer event therefore fails the build instead of being skipped.

A display label is not state. Kubernetes documents that a Pod that is failing to start repeatedly may show CrashLoopBackOff in the Status field of some kubectl commands. That is a display field, not the Pod phase. The same split applies here. The row text and the CSS class are a display of a status that was derived and checked, and they should never become the source of truth.

2. Worked example

All output below is from the lab. Fixtures are real output of the deployer's reference build, plus two hand-edited forgeries.

A soaking release

The plan, the ledger and the report of the soaking deploy:

=== plan soaking
{"ts":0,"event":"serving","version":"v1"}
{"ts":1000,"event":"promoted","version":"v2","previous":"v1"}
{"ts":3000,"event":"exited","version":"v2"}
=== apply soaking
{"attempt":1,"event":"restarting","max":2,"ts":3000,"version":"v2"}
=== report soaking
soak_ms: 30000
max_restarts: 2
current: v2
last_good: v1
candidate: v2
candidate.restarts: 1
deploy: soaking

history.ts builds the release list from the plan (every serving and promoted line, in order). The baseline v1 starts as confirmed. The candidate v2 starts as soaking. Then it walks the ledger:

The report check then agrees on each field:

The page the reference build produces:

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>release history: soaking</title>
<link rel="stylesheet" href="history.css"></head>
<body><main data-current="v2" data-last-good="v1">
<h1>Release history</h1>
<p class="banner warn" role="status" aria-live="polite" data-deploy="soaking">v2 is serving but still on probation; the last confirmed release is v1.</p>
<table><thead><tr><th>Release</th><th>Status</th></tr></thead><tbody>
<tr class="good confirmed" data-version="v1" data-status="confirmed" data-good="true"><td>v1</td><td>confirmed (baseline)</td></tr>
<tr class="soaking" data-version="v2" data-status="soaking" data-good="false"><td>v2 (serving now)</td><td>soaking: on probation, 1 of 2 restarts used; not yet good</td></tr>
</tbody></table>
<p data-field="policy">Soak window 30000 ms, restart budget 2. Last confirmed: v1.</p>
</main></body></html>

Notice three things:

A rollback, shown as such

The storm deploy exits three times inside the window. In the deployer's own output this fixture is generated under the name flap. Its page:

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>release history: storm</title>
<link rel="stylesheet" href="history.css"></head>
<body><main data-current="v1" data-last-good="v1">
<h1>Release history</h1>
<p class="banner error" role="alert" aria-live="assertive" data-deploy="rolled_back">v2 was rolled back for spending its restart budget; v1 is serving.</p>
<table><thead><tr><th>Release</th><th>Status</th></tr></thead><tbody>
<tr class="good confirmed" data-version="v1" data-status="confirmed" data-good="true"><td>v1 (serving now)</td><td>confirmed (baseline)</td></tr>
<tr class="rolled_back" data-version="v2" data-status="rolled_back" data-good="false"><td>v2</td><td>rolled back (budget): 3 exits inside the soak window, budget 2 restarts; traffic went back to v1</td></tr>
</tbody></table>
<p data-field="policy">Soak window 30000 ms, restart budget 2. Last confirmed: v1.</p>
</main></body></html>

v1 is marked "(serving now)" because the report's current is v1, the release traffic went back to.

A chain, where the last confirmed release matters

In the chain deploy, v2 is rolled back and v3 is promoted. The plan's promoted line for v3 says previous=v2, which is stale: v2 was just rolled back.

=== apply chain
{"attempt":1,"event":"restarting","max":2,"ts":2000,"version":"v2"}
{"attempt":2,"event":"restarting","max":2,"ts":3000,"version":"v2"}
{"event":"rolled_back","exits_in_window":3,"from":"v2","max":2,"reason":"restart_budget_exceeded","to":"v1","ts":4000,"version":"v2","window_ms":30000}
{"attempt":1,"event":"restarting","max":2,"ts":52000,"version":"v3"}
{"attempt":2,"event":"restarting","max":2,"ts":53000,"version":"v3"}
{"event":"rolled_back","exits_in_window":3,"from":"v3","max":2,"reason":"restart_budget_exceeded","to":"v1","ts":54000,"version":"v3","window_ms":30000}

The walk, with lastGood starting at v1:

  1. v2 restarts twice. The attempts are 1 and 2 of 2.
  2. v2 is rolled back to v1. That equals lastGood, and v2 has spent exactly the budget. lastGood stays v1, because a rolled-back release never becomes the target.
  3. v3 does the same, and its rollback also targets v1.
  4. The report (candidate: v3, deploy: rolled_back, current: v1, last_good: v1) agrees with all of it.

The page never reads the plan's previous field. It uses the plan only to learn which releases exist and in what order.

Two forgeries that must fail the build

Each of these two deploys was derived from a real one by hand-editing one thing:

The reference build stops on both:

build error: forged_good: report: last_good v2, ledger says v1
build error: forged_target: ledger: rolled back v3 to v2, last confirmed is v1
build failed: 2 deploy(s) whose ledger and report disagree

Both forgeries are internally plausible. The report is well formed, and the ledger is well formed JSONL. They are only wrong against each other, and against the rule that the rollback target is the last confirmed release. That is why the page has to re-derive instead of trusting either file.

3. Lab

Layout. The lab has two trees and a checker:

The starter's bugs, as found by the checker:

The eight checks:

CheckWhat it requires
static_buildEvery honest deploy builds to its own HTML page
confirmed_is_goodA survivor's releases carry the good marker, with an ok banner
soaking_never_goodA soaking release is never marked good, and its banner is warn
rolled_back_names_budgetA rolled-back release is not good and its banner is error
post_soak_not_countedA release that restarts after the window is still confirmed
chain_target_last_goodIn the chain, v1 is good and v2 and v3 are rolled back
forged_ledger_fails_buildThe two forgeries make the build exit non-zero
disagreement_is_build_errorSeven specific disagreements each throw an error

What to do:

  1. Run the checker on starter/ and read the three failures.
  2. Fix the starting status so a promoted release is soaking until the ledger says confirmed.
  3. Rewrite isGood in terms of confirmed only.
  4. Make the ledger walk track the last confirmed release, and add the report cross-checks until the forgeries fail.

To run it yourself, copy check.sh beside the files of one tree and run sh check.sh. It needs Bun and no network.

The gate runs the checker in a clean container with Bun 1.3 and no network. The invocation looks like this, with <workspace> standing for a copy of the lab and LAB_TREE selecting the tree:

docker run --rm --pull=never --network=none -e LAB_TREE=reference -v <workspace>:/lab -w /lab oven/bun:1.3-debian sh check.sh

Real gate output, reference (the docker line with its temporary path is trimmed). The gate collects the checker's stderr after its stdout, so the build errors from check 7 appear at the end of the block:

chain -> dist/chain.html v1=confirmed(good) v2=rolled_back v3=rolled_back
slowburn -> dist/slowburn.html v1=confirmed(good) v2=confirmed(good)
soaking -> dist/soaking.html v1=confirmed(good) v2=soaking
storm -> dist/storm.html v1=confirmed(good) v2=rolled_back
survivor -> dist/survivor.html v1=confirmed(good) v2=confirmed(good)
built 5 pages
RESULT static_build PASS
survivor: v1=confirmed/true v2=confirmed/true banner=ok
RESULT confirmed_is_good PASS
soaking: v2=soaking/false banner=warn
RESULT soaking_never_good PASS
storm: v2=rolled_back/false banner=error
RESULT rolled_back_names_budget PASS
slowburn: v2=confirmed/true
RESULT post_soak_not_counted PASS
chain: v1=true v2=rolled_back/false v3=rolled_back/false
RESULT chain_target_last_good PASS
RESULT forged_ledger_fails_build PASS
report says confirmed while ledger soaking: threw: report: deploy confirmed, ledger says v2 is soaking
report serving v2 after rollback: threw: report: current v2, ledger says v1
report candidate.restarts 1, ledger 2: threw: report: candidate.restarts 1, ledger counts 2
rollback reason not budget: threw: ledger: rollback reason probe_failed is not restart_budget_exceeded
ledger line without version: threw: ledger line 1: restarting has no version
rollback to a release never confirmed: threw: ledger: rolled back v2 to v2, last confirmed is v1
report calls the soaking release last_good: threw: report: last_good v2, ledger says v1
RESULT disagreement_is_build_error PASS
$ bun build.ts fixtures dist
build error: forged_good: report: last_good v2, ledger says v1
build error: forged_target: ledger: rolled back v3 to v2, last confirmed is v1
build failed: 2 deploy(s) whose ledger and report disagree

Real gate output, starter:

chain -> dist/chain.html v1=confirmed(good) v2=rolled_back v3=rolled_back
slowburn -> dist/slowburn.html v1=confirmed(good) v2=confirmed(good)
soaking -> dist/soaking.html v1=confirmed(good) v2=confirmed(good)
storm -> dist/storm.html v1=confirmed(good) v2=rolled_back
survivor -> dist/survivor.html v1=confirmed(good) v2=confirmed(good)
built 5 pages
RESULT static_build PASS
survivor: v1=confirmed/true v2=confirmed/true banner=ok
RESULT confirmed_is_good PASS
soaking: v2=confirmed/true banner=ok
RESULT soaking_never_good FAIL
storm: v2=rolled_back/false banner=error
RESULT rolled_back_names_budget PASS
slowburn: v2=confirmed/true
RESULT post_soak_not_counted PASS
chain: v1=true v2=rolled_back/false v3=rolled_back/false
RESULT chain_target_last_good PASS
forged_good -> dist-rejected/forged_good.html v1=confirmed(good) v2=confirmed(good)
forged_target -> dist-rejected/forged_target.html v1=confirmed(good) v2=rolled_back v3=rolled_back
forged: build exited 0
RESULT forged_ledger_fails_build FAIL
report says confirmed while ledger soaking: did NOT throw
RESULT disagreement_is_build_error FAIL
$ bun build.ts fixtures dist

reference: PASS
starter: FAIL
VERDICT: GATE PASS

The starter passes five checks and fails three. The starter's soaking page shows v2=confirmed(good) while the soaking release has used one restart, which is exactly the false claim this lesson exists to prevent.

Read the reference for limits. From reading history.ts, the reference leaves these gaps. They are not covered by the checker, and the exercises below were not run:

  1. exits_in_window and window_ms on a rollback line are displayed or ignored but never checked. exits_in_window should equal the restarts counted plus one, and window_ms should equal the report's soak_ms.
  2. The page never recomputes the window from ts. A confirmed line stamped before promotion plus the window would pass. The page trusts the deployer's timing and checks only the order and counts of its decisions.
  3. If the candidate is the baseline itself (nothing was ever promoted), the report.deploy comparison is skipped.
  4. statusText branches with if. Writing it as a switch over Status with a never default would make the compiler flag any new status that is not handled.

Closing one of these gaps and adding a forged fixture for it is a good extension.

A note on the banner. The banner uses role="alert" with aria-live="assertive" for a rollback, role="status" with polite for soaking, and off for confirmed. MDN says not to use assertive "unless the interruption is imperative", and the WAI-ARIA alert pattern describes an alert as "a brief, important message" that attracts attention "without interrupting the user's task". This page is static, so nothing changes after load. These attributes express how urgent each message is, but this lesson does not test how any particular screen reader handles an alert that is present at load. It makes no claim there.

Check yourself

These questions use only the fixtures and output above, so you can answer each one by reading the lab.

  1. In the slowburn deploy, v2 exits three times in total, and the budget is two restarts. Why is it still confirmed? Hint: the window ends at 31000 (promotion at 1000 plus 30000), the tick at 31500 is the first event past it and confirms v2, and the third exit at 32000 comes after that, so the ledger records it as restarted_post_soak and the page shows "1 restart after soak (not counted)".
  2. The starter's soaking page printed v2=confirmed(good). Which single line in the starter caused that, and which check caught it? Hint: the status set to confirmed at promotion; soaking_never_good failed.
  3. The forged_target ledger is valid JSONL, and every line has ts, event and version. What makes the build refuse it? Hint: the page tracks the last confirmed release itself (v1), and the second rollback claims to: v2, a release that was itself rolled back.
  4. Why would a check that greps the built HTML for the word "good" be wrong on this page? Hint: the soaking row says "not yet good" and carries data-good="false"; the marker is the good class token or data-good="true".

4. Other paths

Gleam with Lustre (hypothetical sketch, not compiled or run). Lustre can be used as an HTML templating library without its runtime. Its documentation describes passing elements to functions like to_string_tree or to_document_string. It also describes text as turning "a Gleam string into a text node" and none as "for rendering nothing", useful for conditional rendering. Gleam's custom types make the three statuses explicit, and case expressions must cover every variant:

import gleam/int
import lustre/attribute
import lustre/element.{type Element}
import lustre/element/html

pub type Status {
  Confirmed(restarts: Int)
  Soaking(restarts: Int)
  RolledBack(to: String, exits: Int)
}

pub type Release {
  Release(version: String, status: Status)
}

// Good means confirmed. A new variant will not compile until it is placed here.
pub fn is_good(status: Status) -> Bool {
  case status {
    Confirmed(_) -> True
    Soaking(_) | RolledBack(_, _) -> False
  }
}

fn status_text(status: Status, max: Int) -> String {
  let m = int.to_string(max)
  case status {
    Confirmed(n) ->
      "confirmed: survived the soak window with " <> int.to_string(n) <> " of " <> m <> " restarts used"
    Soaking(n) ->
      "soaking: on probation, " <> int.to_string(n) <> " of " <> m <> " restarts used; not yet good"
    RolledBack(to, exits) ->
      "rolled back (budget): " <> int.to_string(exits) <> " exits inside the soak window, budget " <> m <> " restarts; traffic went back to " <> to
  }
}

fn row(r: Release, max: Int) -> Element(msg) {
  let class = case is_good(r.status) {
    True -> "good"
    False -> ""
  }
  html.tr([attribute.class(class)], [
    html.td([], [element.text(r.version)]),
    html.td([], [element.text(status_text(r.status, max))]),
  ])
}

Rendering would end in element.to_document_string. The compiler's exhaustiveness check is a real help: every status must be handled in is_good and status_text. It does not stop the starter's bug, which is choosing Confirmed at promotion time. A type system makes forgotten cases visible, but it cannot make the logic correct. The ledger walk and the report cross-check are still code you have to write, and the same forgeries would still have to fail the build.

A live page. If the page had to update while a deploy soaks, you would need either a server that re-reads the files or client JavaScript that polls them. This lesson deliberately builds neither. The static page is correct as of the build, and a soaking release on it stays marked soaking until a rebuild shows otherwise.

5. Citations