Honmoon ships one artifact: the honmoon binary, built from the Rust workspace with the
dashboard embedded, published as a GitHub Release.
Releases are cut by release-please from the
Conventional Commits on main. Two workflows split the work:
release-please.yml owns the version, the
CHANGELOG, the tag, and the Release; release.yml builds
the binaries, uploads them onto that Release, and publishes it. Nobody bumps a version or
pushes a tag by hand.
The Release is created as a draft and published only once its binaries are on it, so a release is never visible without them. Everything that follows from that is under When a release fails.
- Binaries — published to GitHub Releases
for
x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu, andaarch64-apple-darwin. - npm packages (
@honmoon/cli,@honmoon/policy,@honmoon/api) — not published. Their public API is still moving with the gateway; publishing now would freeze it under semver before it has settled. - Rust crates (
honmoon-core,honmoon-proxy,honmoon-mgmt,honmoon-cli) — not published to crates.io. They are workspace-internal: onlyhonmoon-cliis a product surface, and it ships as a binary.
-
Merge your work to
mainwith Conventional Commits. Only four types open a release at all:feat:bumps the minor, andfix:,perf:andrevert:bump the patch. A breaking change (feat!:or aBREAKING CHANGE:footer) bumps the minor too while the major is still0, whatever type carries it.docs:,chore:,style:,refactor:,test:,build:andci:are release-please's hidden types: they render nothing in the CHANGELOG, and it drops a release whose entry would be empty, so a stretch of only those commits produces no release PR. They ride along in the next release a visible commit opens, adding no bump of their own.The type is not the only gate. A commit whose files all sit under
.claude,.please,datasets,docs,scriptsorwikiis dropped regardless of its type: none of those directories is an input to the binary, the packages or the README, so afeat:confined to them ships nothing and bumps nothing (#279). One file outside them is enough to make the whole commit count, so a change that also updates the wiki is unaffected. The list isexclude-pathsinrelease-please-config.json; why those directories and not.githubis in the header ofrelease-please.yml. -
Review the release PR. Every push to
mainrefreshes an open PR titledchore(release): vX.Y.Z, carrying the new version across every manifest and the CHANGELOG entry it will publish. It only ever proposes — nothing is tagged or published while it sits there, and the version it names keeps climbing to the largest bump among the commits since the last release. Edit the CHANGELOG in the PR if the generated entry needs trimming — what it says at merge time is what the Release notes say. -
Merge the release PR. That is the whole release. release-please tags
vX.Y.Z, creates a draft Release with the CHANGELOG entry as its notes, and then callsrelease.yml, which builds the three targets, uploads the tarballs plusSHA256SUMSonto that draft, and publishes it as its last step. -
Watch the run.
gh run watchor the Actions tab. Nothing is visible to users until the run reaches that last step: while it is building, the tag exists and the Release does not appear on the releases page or at/releases/latest. If the run fails, see When a release fails.
To cut a version release-please would not pick on its own — a specific number, or any release
at all out of hidden-type commits — put Release-As: 0.3.0 in a commit body on main. The
footer is a note, and a commit carrying a note renders even under a hidden section, so it
clears the empty-CHANGELOG gate that would otherwise suppress the release.
That footer is only read from a commit that survives the path filter in step 1, so the commit
carrying it must touch at least one file outside exclude-paths. Two shapes look right and do
nothing: Release-As: on a commit confined to docs/ or scripts/, and Release-As: on a
git commit --allow-empty. The empty commit is dropped by the same rule as the confined one —
"every file is under an excluded path" is vacuously true of a commit with no files, and
release-please reads the footer only after that filter has run. Put the footer on the commit
that carries the change, or on one that also touches a counted path. A version carrying a pre-release identifier (v0.2.0-rc.1 — anything with a
-) is marked a GitHub pre-release and never becomes "latest".
scripts/bump-version.ts predates release-please and is no longer part of this path; the
version sites are the extra-files list in
release-please-config.json.
| Job | What it does |
|---|---|
release-please |
Maintains the release PR. On the push that merges it, pushes refs/tags/vX.Y.Z, creates the Release as a draft, then calls release.yml with that tag. |
verify-version |
Checks out the tag, then compares [workspace.package].version against the tag, every package.json, the Claude plugin manifest, and the README install snippets. Fails naming the file that disagrees. |
mark-prerelease |
Flags a tag carrying a pre-release identifier as a pre-release, while it is still a draft. Runs first and gates build. |
build (×3) |
Builds apps/dashboard with Bun first, then cargo build --release --locked -p honmoon-cli --target <target> on a native runner, from the tagged commit. Smoke-tests honmoon --version and packages each tarball with its .sha256. |
release |
Skipped on a dry run. Folds the per-target checksums into one SHA256SUMS, re-verifies it, uploads onto the draft, and then publishes it. Publishing is the last step of the last job, so nothing else can run against a Release users can already see. |
release-please runs as the org release GitHub App rather than as GITHUB_TOKEN, whose events
never reach other workflows — that is what gives the release PR its CI.
verify-version is the backstop for extra-files: a missed rewrite is only a warning on
release-please's side, so those version sites are re-checked before anything ships. The one
extra-file it does not cover is Cargo.lock, which build's cargo build --locked fails on
instead. build depends on mark-prerelease, so a failed gh release edit cannot let the
run reach the step that publishes the draft — an unflagged pre-release would be published
straight to "latest". Its tag test is on the step, not the job, so a stable or dry run passes
through a job that succeeds while doing nothing — a job skipped by its own condition would
skip build with it. mark-prerelease itself depends on no job; while the Release was
published at tag time that kept a failing verify-version from stranding a pre-release at
/releases/latest, and the draft has since subsumed it, so what the missing edge buys now is
only that it runs alongside verify-version rather than after it. It and release are the
only jobs with contents: write.
The dashboard build order is not cosmetic. crates/honmoon-mgmt/build.rs writes a placeholder
index.html when apps/dashboard/dist is missing so a bare cargo build still links, which
means a cargo-first release would succeed and ship a binary whose dashboard reads "Dashboard
not built". The explicit grep in the build job is what turns that into a failure.
Every failure before release.yml's final step leaves the Release a draft. A draft is
listed for anyone with push access and invisible to everyone else: it is absent from the
public releases page, /releases/latest does not resolve to it, and its assets are not
publicly downloadable. So a half-finished release is not something users can find — it is
something you clean up.
Re-run release.yml with the tag once the cause is fixed (see
Dry-running the pipeline). It rebuilds from the tagged commit,
re-uploads with --clobber, and publishes the draft. Nothing has to be reset first, and the
run is safe to repeat: the publish step edits the Release only while it is still a draft.
What this does cost is a window of a different shape, and it is worth knowing it is there.
The release PR merged before any of this ran, so main already carries the bumped manifests,
the README install snippets, and the CHANGELOG entry for a version that has no published
Release. The tree claims 0.2.0; the releases page does not have it. That lasts until the
re-run succeeds, and it is the deliberate trade for never showing a stable Release with no
binaries on it — see issue #230. The README's install snippets are the visible edge of it:
they point at a download that does not exist yet.
One failure sits earlier than that and looks different. force-tag-creation makes
release-please push refs/tags/vX.Y.Z before it creates the Release, so if the Release
creation itself fails there is a tag and no Release at all, draft or otherwise — and
release.yml was never called. Re-run release-please.yml rather than release.yml: the tag
push is skipped when the ref already exists, and the Release is created from there.
The invariant behind all of this — the draft, the tag option that a draft makes mandatory, and
the publish being the last step — is asserted by scripts/check-release-draft.ts. Its test
runs under bun test, and the script reads the two files directly if you want to check them
before cutting anything:
bun scripts/check-release-draft.tsEach of the three settings silently undoes the fix on its own, and none of them fails until a release is actually being cut.
Run release.yml manually with an empty tag — from the Actions tab, or:
gh workflow run release.yml --ref mainThat builds all three targets and uploads the tarballs as workflow artifacts, but touches no Release and skips the tag comparison. Use it after changing the workflow, or to confirm the binaries build before cutting anything.
Passing a tag instead builds that tag's commit, uploads onto that tag's existing
Release, and publishes it if it is still a draft — which is how you redo a release that failed
after the tag was cut. The checkout is pinned to the tag rather than to the ref the run was
launched from, so a manual run cannot put binaries built from some other branch onto a
Release. Run against a tag whose Release is already published, it re-uploads the assets and
leaves the Release's own settings alone, so re-dispatching an old tag cannot disturb which
release is "latest".
Every Release carries a SHA256SUMS covering all three tarballs.
VERSION=0.2.0
TARGET=x86_64-unknown-linux-gnu
BASE="https://github.com/pleaseai/honmoon/releases/download/v${VERSION}"
curl -fsSLO "${BASE}/honmoon-${VERSION}-${TARGET}.tar.gz"
curl -fsSLO "${BASE}/SHA256SUMS"
# Linux
sha256sum --ignore-missing -c SHA256SUMS
# macOS
shasum -a 256 --ignore-missing -c SHA256SUMS--ignore-missing restricts the check to the tarball actually downloaded; without it the two
files you did not fetch are reported as failures.