The v3.0.1 tag that failed on purpose
Tagging v3.0.1 failed the first time, because the release gate is supposed to catch exactly this. The real fix exposed a genuine circular dependency: a Homebrew formula that lives inside the same repository whose tag it describes.

devforgekit doctor --release-check runs before every tag, and it's not decorative: it checks that VERSION, package.json, cli/package.json, and Formula/devforgekit.rb all agree on the same version number before anything gets pushed. The first attempt to tag v3.0.1 hit that gate and stopped cold: the release workflow ran, found the Homebrew formula still referencing 3.0.0, and refused to create a release. No GitHub Release, no draft, nothing public. That's exactly the rollback-safe state the gate exists to guarantee.
A real ordering bug, not a validation bug
The instinct when a gate blocks a release is to check whether the gate is being too strict. It wasn't. The gate correctly requires the formula to already reference the version being tagged, but the formula's url and sha256 fields can only correctly describe a tag once that tag exists. Fixing the formula before tagging is impossible in principle; fixing it after tagging is what the gate is designed to prevent. That's a genuine ordering problem in the release pipeline, not a bug in the check itself.
The fix was to work forward, not around the gate: delete the premature tag (following the rollback procedure RELEASE.md already documents for exactly this window, since a tag that's been pushed but never had a GitHub Release created from it hasn't touched anything public), update the formula, verify it for real, then re-tag.
Verifying a Homebrew formula against a tag that doesn't exist yet
"Update the formula" sounds simple until you try to compute a real checksum. shasum -a 256 needs an actual file to hash, and the only way to get DevForgeKit's real source archive is to ask GitHub for the tag's tarball, which doesn't exist until the tag is pushed. Pushing the tag on its own, without the workflow succeeding, is harmless: a git tag is a normal, low-risk ref, and the release workflow failing at the gate creates nothing public. So that's what happened: push the tag alone, download the real archive GitHub generated from it, hash it for real, and use that exact value. Not a commit-SHA archive substituted in as an approximation, either: GitHub embeds the ref name into every path inside a generated tarball, so a commit-SHA archive and a tag-name archive of the identical commit are not byte-identical, and would produce a different, wrong checksum.
git tag -a v3.0.1 -m "DevForgeKit v3.0.1"
git push origin v3.0.1
curl -fsSL -o v3.0.1.tar.gz \
https://github.com/NouradinAbdurahman/DevForgeKit/archive/refs/tags/v3.0.1.tar.gz
shasum -a 256 v3.0.1.tar.gzThe checksum that was correct, then wasn't
The formula fix landed as its own commit, on top of the tag, which is exactly the self-reference the gate exposed in the first place. Formula/devforgekit.rb lives inside the DevForgeKit repository, so it's part of whatever tree the v3.0.1 tag's tarball contains. The Formula PR merged, scripts/release.sh finalize re-ran, and it correctly deleted the premature tag and re-created v3.0.1 at the new commit: the one containing the Formula fix. Which meant the checksum computed against the old tag's archive no longer matched the new tag's archive: same version number, genuinely different bytes, genuinely different sha256.
Caught the same way the first checksum was verified: not assumed, downloaded and hashed for real, against the actual, now-permanent tag. A second, one-line Formula correction shipped, verified again with a real brew install --build-from-source against a fresh tap clone (untap, re-tap from GitHub, install; not the same local checkout that pushed the fix). This is a one-time cost, not a recurring one: v3.0.1's tag is now a real, published release and won't be re-pointed again, so every future Formula update lands after the tag it describes, not entangled with it.
Two more, found the same way: verify, don't assume
The published GitHub Release's body came back essentially empty. release.yml's changelog-extraction step worked exactly as designed; the CHANGELOG.md section it extracted from was just genuinely blank. scripts/release.sh promote correctly renamed the empty "## [Unreleased]" heading straight to "## [3.0.1]" with no content, since promoting a release candidate to stable made no code changes. Technically accurate, practically useless as a release description. Fixed forward: real content added explaining the promotion, then the already-published release's body updated directly, without touching the immutable tag.
And npm flagged something on the very first publish attempt: bin.devforgekit in package.json was "./devforgekit", a leading ./ that this npm version treats as an invalid script name and silently strips at publish time. Checked whether that had already shipped broken: the live v3.0.1-rc1 package's registry entry showed the corrected form already, meaning npm's auto-correction had quietly done the right thing during that earlier publish. Still fixed the source directly rather than keep depending on an npm client silently patching a real bug on every future publish.
What shipped, for real
Every fix here went through the same discipline: compute the real value, verify it against the real artifact, never assume a number carries over from a similar-looking prior state. v3.0.1 is published, marked Latest, not a draft, not a prerelease: devforgekit@3.0.1 on npm with the latest dist-tag correctly pointing at it, the Homebrew tap verified against a genuinely fresh clone, 1,350 tests passing. The formula/tag circular dependency this exposed is real and worth solving properly (create the tag, create the release, update the formula against the released tarball, publish the formula), but that's a v3.1 pipeline redesign, not a patch bolted onto a release already in flight.