Skip to main content

max / makenotwork

sando: run docs check first in code_smoke Run the staged binary's DB-free MNW_CHECK_DOCS mode before creating the throwaway smoke DB, so a broken internal docs link fails the pipeline in well under a second instead of after a full migrate+seed+boot, and never reaches prod as a live 404. Collisions are reported but do not fail. Add GateFailure::CodeSmokeDocs and a 60s ceiling that backstops a staged binary predating the flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 15:38 UTC
Commit: 575b271365059e46559bc1b0dff2ea73eb158e60
Parent: 971b05a
2 files changed, +109 insertions, -0 deletions
@@ -1259,6 +1259,16 @@ async fn code_smoke(ctx: &GateCtx) -> Result<GateOutcome> {
1259 1259 .await);
1260 1260 };
1261 1261
1262 + // Docs integrity, first and cheapest: run the staged binary's DB-free
1263 + // `MNW_CHECK_DOCS` mode before creating the throwaway DB. A broken internal
1264 + // docs link (a `[..](x.md)` resolving to a slug no page serves) fails here
1265 + // in well under a second instead of after a full migrate+seed+boot, and a
1266 + // rotted link never reaches prod as a live 404. Collisions are reported by
1267 + // the check but do not fail it; only broken links do.
1268 + if let Some(outcome) = code_smoke_docs_check(ctx, &bin, &mut log_buf).await {
1269 + return Ok(finish(outcome.with_log_ref(log_ref), log_buf).await);
1270 + }
1271 +
1262 1272 let dbname = code_smoke_db_name(&ctx.version);
1263 1273 let maintenance_url = pg_url_with_dbname(scratch_url, "postgres");
1264 1274 let throwaway_url = pg_url_with_dbname(scratch_url, &dbname);
@@ -1289,6 +1299,70 @@ async fn code_smoke(ctx: &GateCtx) -> Result<GateOutcome> {
1289 1299 Ok(finish(outcome.with_log_ref(log_ref), log_buf).await)
1290 1300 }
1291 1301
1302 + /// Run the staged binary's DB-free docs integrity check (`MNW_CHECK_DOCS=1`).
1303 + ///
1304 + /// Returns `Some(failed)` if the check reports broken links, cannot be spawned,
1305 + /// or overruns its ceiling; `None` when the docs are clean. Output is appended
1306 + /// to `log_buf` either way. The 60s ceiling backstops the case where the staged
1307 + /// binary predates the flag and would fall through to a normal (DB-needing)
1308 + /// boot and hang.
1309 + async fn code_smoke_docs_check(
1310 + ctx: &GateCtx,
1311 + bin: &str,
1312 + log_buf: &mut Vec<u8>,
1313 + ) -> Option<GateOutcome> {
1314 + let server_dir = ctx.worktree.join("server");
1315 + log_buf.extend_from_slice(b"---- docs check (MNW_CHECK_DOCS) ----\n");
1316 + let mut cmd = tokio::process::Command::new(bin);
1317 + cmd.env("MNW_CHECK_DOCS", "1")
1318 + .current_dir(&server_dir)
1319 + .stdout(std::process::Stdio::piped())
1320 + .stderr(std::process::Stdio::piped())
1321 + .kill_on_drop(true);
1322 + let out = match tokio::time::timeout(std::time::Duration::from_secs(60), cmd.output()).await {
1323 + Ok(Ok(out)) => out,
1324 + Ok(Err(e)) => {
1325 + log_buf.extend_from_slice(format!("docs check spawn failed: {e}\n").as_bytes());
1326 + return Some(GateOutcome::failed(GateFailure::SpawnFailed {
1327 + message: e.to_string(),
1328 + }));
1329 + }
1330 + Err(_elapsed) => {
1331 + let reason =
1332 + "docs check timed out after 60s (staged binary may predate MNW_CHECK_DOCS)"
1333 + .to_string();
1334 + log_buf.extend_from_slice(reason.as_bytes());
1335 + log_buf.push(b'\n');
1336 + return Some(GateOutcome::failed(GateFailure::CodeSmokeSetup { reason }));
1337 + }
1338 + };
1339 + log_buf.extend_from_slice(&out.stdout);
1340 + log_buf.extend_from_slice(&out.stderr);
1341 + if out.status.success() {
1342 + return None;
1343 + }
1344 + Some(GateOutcome::failed(GateFailure::CodeSmokeDocs {
1345 + broken: parse_check_docs_broken_count(&out.stdout),
1346 + }))
1347 + }
1348 +
1349 + /// Best-effort parse of the broken-link count from the `MNW_CHECK_DOCS` sentinel
1350 + /// line (`MNW_CHECK_DOCS: N broken link(s)`). Returns 0 if absent — the failure
1351 + /// still stands, only the summary count is unknown.
1352 + fn parse_check_docs_broken_count(stdout: &[u8]) -> u32 {
1353 + let text = String::from_utf8_lossy(stdout);
1354 + for line in text.lines() {
1355 + if let Some(rest) = line.strip_prefix("MNW_CHECK_DOCS:") {
1356 + for tok in rest.split_whitespace() {
1357 + if let Ok(n) = tok.parse::<u32>() {
1358 + return n;
1359 + }
1360 + }
1361 + }
1362 + }
1363 + 0
1364 + }
1365 +
1292 1366 /// The createdb-to-dropdb interior of `code_smoke`: migrate+seed, then boot and
1293 1367 /// probe. Returns the outcome without a `log_ref` (the caller attaches it after
1294 1368 /// teardown). Never returns `Err` — spawn/child failures map to typed outcomes.
@@ -1956,6 +2030,30 @@ mod tests {
1956 2030 }
1957 2031
1958 2032 #[test]
2033 + fn parse_check_docs_broken_count_reads_the_sentinel() {
2034 + // The failing sentinel carries the count.
2035 + assert_eq!(
2036 + parse_check_docs_broken_count(b"some log\nMNW_CHECK_DOCS: 3 broken link(s)\n"),
2037 + 3
2038 + );
2039 + // Count with a preceding log line still parses (first bare int wins).
2040 + assert_eq!(
2041 + parse_check_docs_broken_count(
2042 + b" broken link: a -> b\nMNW_CHECK_DOCS: 1 broken link(s)\n"
2043 + ),
2044 + 1
2045 + );
2046 + // The parser only runs on failure; the "ok" sentinel is never fed to it,
2047 + // and its "(2" token is not a bare int, so it yields 0 harmlessly.
2048 + assert_eq!(
2049 + parse_check_docs_broken_count(b"MNW_CHECK_DOCS: ok (2 collision(s) reported)\n"),
2050 + 0
2051 + );
2052 + // Absent sentinel -> 0; the failure still stands, only the count is lost.
2053 + assert_eq!(parse_check_docs_broken_count(b"unrelated output"), 0);
2054 + }
2055 +
2056 + #[test]
1959 2057 fn name_target_points_a_test_failure_at_its_crate() {
1960 2058 // With one target the crate was implicit; with fifteen the operator
1961 2059 // needs the summary to say which one broke.
@@ -232,6 +232,13 @@ pub enum GateFailure {
232 232 /// a broken migration, a seed error, or a config-load failure. The persisted
233 233 /// gate log carries the process output; `exit_code` is the OS status.
234 234 CodeSmokeSeed { exit_code: Option<i32> },
235 + /// `code_smoke`: the DB-free `MNW_CHECK_DOCS` run found broken internal docs
236 + /// links (an internal `[..](x.md)` resolving to a slug no page serves — a
237 + /// live 404 waiting in the public docs). Runs first, before the throwaway
238 + /// DB is even created, so a rotted link fails cheaply. `broken` is the
239 + /// reported count (0 if the sentinel could not be parsed); the gate log
240 + /// carries the offending source→target lines.
241 + CodeSmokeDocs { broken: u32 },
235 242 /// `code_smoke`: the booted server answered `GET /health` but never emitted
236 243 /// its `listening` startup log. The gate asserts both signals — the bind
237 244 /// readiness log and the health probe — so a missing log line fails even
@@ -305,6 +312,10 @@ impl GateFailure {
305 312 format!("migrate+seed run failed: exit {c}")
306 313 }
307 314 GateFailure::CodeSmokeSeed { exit_code: None } => "migrate+seed run failed".into(),
315 + GateFailure::CodeSmokeDocs { broken: 0 } => "broken internal docs link(s)".into(),
316 + GateFailure::CodeSmokeDocs { broken } => {
317 + format!("{broken} broken internal docs link(s)")
318 + }
308 319 GateFailure::CodeSmokeNoListeningLog => {
309 320 "served /health but never logged 'listening'".into()
310 321 }