chore: harden release machinery (5 independent fixes) - #287
Merged
Conversation
Fix 1 — Restore RELEASING.md sections deleted by acb8a86 Commit acb8a86 (PR #239) accidentally removed the "What happens after tagging" numbered breakdown and the "Post-release" checklist, replacing them with a self-referential "See @RELEASING.md for the full runbook." line. Both sections are restored verbatim (with step 4 updated to mention the bounded crates.io poll). The atomic_write_file note from PR #239 is kept unchanged. Fix 2 — Stop publish-crates from swallowing genuine failures Both cargo publish steps previously exited 0 regardless of the actual error. Now output is captured; the job continues only when the error matches an already-published signal (idempotent re-run), and exits 1 on all other failures. The bare sleep 30 index wait is replaced with a bounded poll of the crates.io API (max 20 × 15 s = 5 min, then fail). Fix 3 — Fix bump-version.mjs CHANGELOG link-table corruption The third regex matched the [0.1.0] releases/tag line and silently repointed it to the new version while adding a duplicate entry. The fix: merge the old second + third replacements into one that captures the repo base URL from the existing [Unreleased] line and inserts a new [version] compare link directly after it, leaving all prior entries untouched. Dry-run diff verified against a CHANGELOG copy. Fix 4 — Remove the broken workflow_dispatch -f version= path (Closes #127) The prepare job pushed to protected main via the Actions bot, which branch protection rejected (GH006). It left an orphaned tag and published nothing while still being invocable. The prepare job and version input are deleted. Tag-push remains the single release path; plain workflow_dispatch (no inputs) remains the dry-run path. All downstream if: conditions and ref: checkouts simplified to use github.ref directly. RELEASING.md and CLAUDE.md updated to remove the broken path docs. Fix 5 — Gitignore generated examples/ output 78 compiler-output files (foo.mds → foo.md, @message → foo.json) were untracked. Added minimal pattern set with negation guards to protect the 120 currently-tracked files (README.md, package.json, mds.json, vars.json, vars-minimal.json, tsconfig.json). Verified: no tracked file became ignored; file count unchanged at 120. Co-Authored-By: Claude <noreply@anthropic.com>
Fix 1 (follow-up to gitignore) — Add footgun comment and CI guard
The examples/**/*.md + **/*.json catch-alls silently ignore any
hand-authored .md/.json added under examples/ unless an explicit '!'
exception exists. Added a warning comment in .gitignore explaining
the invariant and pointing at the CI guard. Added an 'examples-guard'
job in ci.yml that iterates the set of ignored files under examples/
(bounded at MAX=500, linear pass) and fails if any has no sibling .mds
source — meaning the file is hand-authored content being silently
dropped. No existing jobs were an appropriate home; the guard is
checkout-only and fast.
Demonstration:
PASS: 0 ignored files on current tree
FAIL: examples/edge-cases/GUIDE.md (no GUIDE.mds sibling) → guard
exits 1 naming the offender; dummy then removed
Fix 2 (follow-up to bump-version.mjs) — Correct premature CHANGELOG
link-reference table. Commit 3aef465 introduced two premature entries:
[Unreleased]: .../compare/v0.4.0...HEAD (wrong; v0.4.0 not yet tagged)
[0.4.0]: .../compare/v0.3.0...v0.4.0 (wrong; pre-release)
Corrected to the proper pre-bump state:
[Unreleased]: .../compare/v0.3.0...HEAD
[0.4.0] line removed
The 0.3.0→0.4.0 dry-run with the branch's own bump-version.mjs script
against the corrected CHANGELOG confirms the fix produces:
8a9,10 <- ## [0.4.0] — 2026-08-09 inserted
1011c1013,1014 <- [Unreleased] updated to v0.4.0...HEAD
<- [0.4.0] compare link inserted
[0.1.0] releases/tag line untouched
Co-Authored-By: Claude <noreply@anthropic.com>
PR #240 (de8857d) added a `miette::miette!` call that exceeded the line-length limit and was not run through `cargo fmt` before merging. PRs #239 and #240 each passed CI on their own branches, but the combination left `main` (c8b4062) red on the `cargo fmt --check` gate. This commit applies the corrective reformat so that PR #287 can pass CI independently of the merge order for PR #286, which incidentally carries the same fix on its branch.
…e job The old `examples-guard` job iterated `git ls-files --others --ignored` to find files under examples/ that were gitignored but lacked a sibling .mds source. This set is always empty in a fresh CI checkout (generated files are never committed), so the job passed unconditionally and never caught anything. Replace it with `examples-gitignore-coverage`: build the mds CLI, run `mds build` over every example directory expected to compile cleanly (excluding `stress-test/errors/` which contains intentionally-failing fixtures by design), then assert `git status --porcelain --untracked-files=all -- examples/` is empty. If the compiler ever emits an output extension not covered by the .gitignore catch-alls, the untracked file appears and the job fails with a concrete list. Local proof: PASSING — mds build wrote 73 outputs across 14 directories/files; git status was empty (all covered by .gitignore). FAILING — temporarily commenting out `examples/**/*.md` caused 61 generated .md files to appear as `?? examples/...` in git status. Also update the FOOTGUN WARNING comment in .gitignore to accurately describe what CI does and does not enforce: the build-then-check job catches new compiler output extensions; it cannot catch a hand-authored file being silently swallowed by the catch-alls (that remains local discipline).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Five independent release-machinery and repo-hygiene fixes, plus two rounds of coordinator follow-up.
Hard constraints respected:
crates/mds-core/**,crates/mds-cli/**, and the[Unreleased]body ofCHANGELOG.mdare untouched.Fix 1 — Restore accidentally deleted sections from RELEASING.md
Commit
acb8a86(ancestor on main) deleted "What happens after tagging" and "Post-release" fromRELEASING.md. Both sections are restored verbatim, with step 4 updated to reference the bounded crates.io poll added in Fix 2.Fix 2 — Stop release.yml from swallowing genuine publish failures
The
publish-cratesstep usedcargo publish || true, which turned every failure (network error, bad token, crates.io down) into a silent no-op. Replaced with an explicit idempotency check:cargo publishsucceeds → proceed normally.exit 1(actual error, stop the workflow).Also replaced
sleep 30with a bounded crates.io sparse-index poll (max 20 × 15 s = 5 min) so the subsequentmds-clipublish doesn't race a propagation delay or hang indefinitely.Fix 3 — Fix bump-version.mjs CHANGELOG link-table corruption
The third
.replace()inbump-version.mjsused a regex that matched the[0.1.0]releases/tag line and overwrote it with a compare link, corrupting the table. Removed the buggy third replace; combined old second + third into one that captures the repo base URL from the existing[Unreleased]line and correctly inserts both the updated[Unreleased]pointer and the new version's compare link in one operation.Dry-run verified with 0.3.0 → 0.4.0 transition against the branch's own CHANGELOG copy:
[0.1.0]tag line: untouched ✓[0.4.0]compare link: inserted exactly once ✓[Unreleased]pointer: updated to v0.4.0...HEAD ✓Fix 4 — Remove the broken workflow_dispatch -f version= path
The
workflow_dispatch -f version=X.Y.Zpath was broken (#127): thepreparejob pushed a release commit directly to protectedmain, which branch protection rejects (GH006). It left orphaned tags and published nothing. Rather than patch around protected-branch restrictions, removedprepareentirely and simplified the workflow to two entry points:refs/tags/vX.Y.Z) → full release (version-gate → build-napi → stage-and-verify-napi → publish-crates → publish-npm → github-release).stage-and-verify-napi, publishing nothing.All job
needs:/if:/ref:expressions updated for consistency.CLAUDE.mdandRELEASING.mdupdated to remove the broken path references.Fix 5 — Gitignore the 78 generated examples/ output files
mds buildwrites compiled output next to each.mdssource:.mdfor Markdown templates,.jsonfor@messagetemplates. These 78 generated files were untracked noise. Added catch-all patterns with negation exceptions protecting all hand-authored files (7 README.md files, 10+ config JSON files).Follow-up: CHANGELOG link table
Commit
3aef465(ancestor) had introduced a premature[Unreleased]pointer atv0.4.0...HEADand a[0.4.0]compare link. Both were wrong (the current release is v0.3.0). Corrected:[Unreleased]now points atv0.3.0...HEAD; the premature[0.4.0]line is removed. (Only the link-reference table was touched; the[Unreleased]body is untouched per hard constraint.)Follow-up: Replace examples-guard with build-then-check CI job
The original
examples-guardjob iteratedgit ls-files --others --ignoredto find gitignored files lacking a sibling.mdssource. In CI (fresh checkout, no generated files committed), this set is always empty — the guard unconditionally passed and never caught anything.Replaced with
examples-gitignore-coverage: build the mds CLI, runmds buildover every example directory expected to compile cleanly (excludingstress-test/errors/which contains intentionally-failing fixtures by design), then assertgit status --porcelain --untracked-files=all -- examples/is empty. If the compiler ever starts emitting an output extension not covered by the.gitignorecatch-alls, the new untracked file appears and the job fails with a concrete file list.Proved both ways locally:
examples/**/*.mdfrom.gitignore→ 61 generated.mdfiles appeared as??in git status ✓Also updated the FOOTGUN WARNING comment in
.gitignoreto accurately describe what CI does and does not enforce.Breaking Changes
None. All changes are to release tooling, CI, and documentation.
Reviewer Focus Areas
release.yml: publish-crates idempotency guard and crates.io index poll logicscripts/bump-version.mjs: single combined.replace()covering both the[Unreleased]update and new version link insertion.github/workflows/ci.yml:examples-gitignore-coveragejob — confirm the directory list covers the full clean-compiling surface and the assertion is soundRELEASING.md: restored sections and removal of the brokenworkflow_dispatch -f version=pathCloses #127