| id | 184 |
|---|---|
| title | Automate the cross-tool benchmark on merge to main and publish numbers to the assets branch |
| status | ✅ |
| model | opus |
| depends-on | |
| summary | Fold docs/research/benchmarks/run.sh into a pinned, integrity-verified `mdsmith-release bench` subcommand; run it on every merge to main and publish the refreshed JSON + fragments to the orphan `assets` branch (the demo.gif pattern) so the website serves current cross-tool numbers without a maintainer hand-refresh. |
The mdsmith-vs-Rust comparison is a hand-refreshed
artifact. See the
benchmark research doc.
run.sh installs the tools, runs hyperfine, promotes
JSON, and regenerates fragments by hand.
CI (bench-fragments) only checks that the committed
fragments match the committed JSON. It never re-measures.
So the table goes stale after every engine change. It is
stale now. It predates both plan 175's single-core work
and the Run parallelization.
Fix this the way demo.gif is built. On merge to main,
measure. Push the result to the orphan assets branch.
The website pulls from there.
demo.yml→record-demo.yml: onpush: main(skip the bot to avoid loops) record the artifact, then apublishjob force-updates the orphanassetsbranch with only that artifact, committing only if it changed.website/layouts/partials/hero.htmlpullsraw.githubusercontent.com/<repo>/assets/assets/demo.gif.docs/development/release-tooling.md: workflow runtime logic goes throughgo run ./cmd/mdsmith-release <sub>, not inline scripts. So the harness becomes a subcommand.run.shalready version-pins hyperfine/mado/panache via release tarballs but without checksums;rumdl(uv tool install rumdl) andmarkdownlint-cli2(npm i) are unpinned entirely.
- Trigger: on merge to
main, publish to the existing orphanassetsbranch underassets/benchmarks/(reuse, don't proliferate branches). Record-only — never gates a merge or a release. - Harness lives in
mdsmith-release bench(Go owns install + integrity + hyperfine + promote + fragment regen);run.shbecomes a thin wrapper that calls it so local hand-refresh and CI run identical logic. - Pin version + integrity: every tool fetch is a
pinned version verified by SHA-256;
markdownlint-cli2via a committed lockfile +npm ci. - Lands on PR #330 (this branch).
- Create this plan.
-
internal/releasebenchmark core +mdsmith-release benchsubcommand: pinned-tool manifest{tool,version,url,sha256}(benchTools()ininternal/release/bench.go), fail-loud SHA-256 verifier (verifyChecksum), corpus build, hyperfine orchestration, JSON promote intodocs/research/benchmarks/data/(promoteBenchJSON), fragment regen via the existinggen_fragments.py. Unit-tested the pure parts inbench_test.go(manifest invariants, verifier ok/mismatch, promote copy/missing-source, tar extraction). - Pinned
rumdl0.1.93 andmarkdownlint-cli20.22.1. Recorded real SHA-256s for the four binary tools (hyperfine/mado/panache/rumdl); rumdl moved from the unpinneduv tool installto its pinned GitHub release tarball so it shares the verify path. The fifth tool,markdownlint-cli2, is pinned by the committeddocs/research/benchmarks/npm/package.json + package-lock.json and installs vianpm ci(npm's lockfile integrity is its SHA). - Rewrote
run.shas a thin wrapper overgo run ./cmd/mdsmith-release bench. Thebench-fragmentsdrift gate is untouched. -
benchmark.yml:push: main, skipgithub-actions[bot];recordruns the subcommand + normalizes fragments;publishupdates theassetsbranch underassets/benchmarks/(JSON + fragments), commit only on change. Modelled ondemo.yml. Both workflows are now subtree-scoped so they coexist on the shared branch (see Deviations). - Build-time fetch (chosen over the runtime model):
mdsmith-release pull-site-assetspulls the published fragments + demo GIF into the tree duringpages.yml; a scopedmdsmith fixrebakes the comparison page's<?include?>body. The demo GIF now ships as a first-party static asset (was a runtimeraw.githubusercontenthotlink).bench-fragmentsstays coherent: it is a separate workflow that never calls the pull and still validates the committed snapshot vs committed JSON. - Gates pass:
go build ./...,go test ./...,golangci-lint,mdsmith check .. End-to-end (benchmark.yml, the assets-branch push, and the site build-time fetch) is only exercisable on merge to main and cannot be validated on the PR.
- Website mechanism: user chose build-time fetch over
the runtime
demo.gif-style fetch, and asked thatdemo.gifmove to build-time too. Implemented aspull-site-assetsinpages.yml;hero.htmlnow serves a localimg/demo.gif(.gitignored). demo.ymlpublish made subtree-safe (dropped thegit rm -rf . && git clean -fdxwhole-tree wipe). Both it andbenchmark.ymlnow touch only their own subtree on the shared orphanassetsbranch, so neither erases the other's output.rumdlis pinned via its GitHub release tarball + a SHA-256 (cross-checked against the publisher.sha256) rather thanuv, so all four binary tools share one fetch+verify path.
-
mdsmith-release benchruns the identical commands/flags/corpora as the oldrun.shand reusesgen_fragments.py, so it reproduces the fourdata/*.jsonfiles and the fragments byte-for-byte on the same inputs. (The timing values are non-deterministic; the format/pipeline is identical. Full e2e only on merge to main.) - A tampered download (wrong SHA-256) fails the run
loudly; covered by
TestVerifyChecksum/TestExtractTarGzBinaryinbench_test.go. -
run.shdelegates to the subcommand and thebench-fragmentsdrift gate is unchanged (stays green: committed JSON/fragments untouched). -
benchmark.ymlwrites onlyassets/benchmarks/, commits only on change, and pushes to theassetsbranch — nevermain. (Exercisable only on merge.) - Website serves numbers from the
assetsbranch via build-time fetch (pull-site-assets+ scopedmdsmith fixinpages.yml). (Exercisable only on a site deploy.) -
go test ./...,golangci-lint,mdsmith check .all pass.