Hecate is pre-1.0. Releases should be boring, repeatable, and explicit about what is alpha-grade versus production-shaped.
- Use semantic-version tags:
v0.x.yuntil the public API and storage schema reach a v1 stability bar. - Use patch releases for bug fixes, docs corrections, and small UI polish.
- Use minor releases for API additions, storage changes, provider/runtime behavior changes, or operator workflow changes.
- For pre-release tags use the dotted-suffix semver form:
v0.1.0-alpha.1,v0.1.0-alpha.2,v0.1.0-rc.1. Goreleaser and tauri-action handle them the same as stable tags; consumers can opt out via semver tooling that recognizes pre-release tags. - Keep shipping
v0.1.0-alpha.Nwhile the alpha-to-beta roadmap is open. The first beta tag is a quality gate (v0.1.0-beta.1), not the next release by default. - Do not publish a release from a dirty worktree.
Every release stays marked as a GitHub pre-release until v1.0.0
stable. The semver suffix (-alpha.N, -beta.N, -rc.N) already
signals pre-stability to humans; the GitHub pre-release flag makes
that signal machine-readable for package managers, mirrors, and any
downstream automation that respects it.
Practical consequences:
https://github.com/hecatehq/hecate/releases/latest/will resolve to nothing while every release is a pre-release — GitHub's "latest" semantics explicitly skip pre-releases.- In-app auto-update therefore cannot rely on
/releases/latest/download/latest.json. The desktop app instead readshttps://hecate.sh/releases/alpha/latest.json, which CI publishes after every successful release via thepublish-updater-websitejob in_tauri-shared.yml. Seedesktop-updater-signing.mdfor the pipeline and troubleshooting. - Once
v1.0.0ships, that release lands without the pre-release flag. The decision to drop the flag will be deliberate — the release before it (v1.0.0-rc.Nor equivalent) is the last gate. The dedicatedhecate.sh/releases/alpha/latest.jsonchannel can then be retired in favor of GitHub's native endpoint, or kept as the canonical multi-channel surface; that's a v1 decision.
Historical context for the channel switch. v0.1.0-alpha.27 was
the last release built with the GitHub-native updater endpoint
(/releases/latest/download/latest.json) baked into the bundle.
v0.1.0-alpha.28 introduced the new
hecate.sh/releases/alpha/latest.json endpoint and was briefly used
to validate the bridge path. Because Hecate had no real installed
alpha cohort to migrate, alpha.28 was flipped back to pre-release
after validation and old alpha.21–27 installs are expected to
reinstall manually from the current alpha. Alpha.29+ ship as
pre-releases by default; auto-update routing no longer depends on
GitHub's latest semantics once a bundle contains the hecate.sh
channel endpoint.
Every v* tag fires .github/workflows/release.yml, which expands into
five Actions jobs:
goreleaser(~5–10 min) — multi-arch Go binary tarballs forlinux+darwin × amd64+arm64; each tarball includeshecateandhecate-acp. It also publishes multi-arch Docker images onghcr.io/hecatehq/hecate, source tarball, checksums, GitHub Release entry.tauri / build(matrix, ~10–15 min, runs after goreleaser) — three legs building native desktop bundles and uploading them to the same Release entry:.dmg(macOS arm64),.deb+.AppImage(Linux x86_64),.msi(Windows x86_64).tauri / publish updater manifest— stitches signed updater payloads intolatest.jsonand uploads the GitHub Release copy.tauri / publish updater manifest to website— commits the same manifest towebsite/public/releases/alpha/, dispatches the website deploy, and blocks untilhttps://hecate.sh/releases/alpha/latest.jsonserves the new version.update-release-docs— reads the actual uploaded Release assets and refreshes the README Desktop app table plus pinned Docker/tarball examples. This runs only after the Tauri matrix succeeds, so it never links to bundles that were not published.
Acceptance after the run:
- All release jobs green.
- Release entry marked Pre-release for
-alpha.Ntags. - Goreleaser-side artifacts attached: tarballs for each goos/goarch + checksums.
Each tarball contains both
hecateandhecate-acp. - Tauri-side bundles attached: 1
.dmg, 1.deb, 1.AppImage, 1.msi. If any is missing, the matrix leg silently skipped upload — open the run to see what failed. - README Desktop app table and pinned install examples point at the release
tag. The workflow commits this docs-only refresh to
masterwith[skip ci]. https://hecate.sh/releases/alpha/latest.jsonserves the same version as the release tag. This is the updater endpoint bundled into alpha.28+ desktop apps; the GitHub Releaselatest.jsonasset is a backup/source artifact.docker pull ghcr.io/hecatehq/hecate:X.Y.Zsucceeds (novprefix — goreleaser uses bare semver as the docker tag). The image contains both/usr/local/bin/hecateand/usr/local/bin/hecate-acp; the entrypoint is/usr/local/bin/hecate.docker run --rm -p 8765:8765 ghcr.io/hecatehq/hecate:X.Y.Zthencurl :8765/healthzreturnsversion: "X.Y.Z".
Run the full local gate before cutting a public alpha tag:
just verifyThe target runs the non-destructive launch checks in order:
- docs/env drift check
- Go unit tests
go vet- Go race tests
- ACP bridge smoke test
- Docker smoke test
- UI unit tests
- UI e2e tests
- production binary build with embedded UI
If a check is intentionally skipped, call it out in the release notes with the reason and the risk. Docker smoke and UI e2e are allowed to be slow; they are not optional for a public alpha build.
The gate does not exercise the Tauri matrix. PR validation
(tauri-build.yml) covers that on every PR touching the desktop pipeline;
post-tag, the release matrix is the next opportunity to catch regressions.
The canonical entry point is the just release recipe, which runs
just verify first and then delegates to scripts/release.ts:
just release vX.Y.ZIt performs, in order: the full project verification gate, clean-worktree check, tag-uniqueness check, goreleaser-on-PATH check, goreleaser snapshot dry-run, interactive confirmation prompt, Tauri version stamp commit (Cargo.toml, package.json, tauri.conf.json), annotated tag, push.
Pass --skip-snapshot to skip the dry-run when you've already validated
locally:
just release vX.Y.Z --skip-snapshotThe script's annotated tag message is just the version string. For
substantive release notes, tag manually instead so the message becomes
the canonical release notes (what git show vX.Y.Z and the GitHub
Releases page surface):
bun scripts/stamp-version.ts # stamps Tauri version files
git add tauri/src-tauri/Cargo.toml tauri/src-tauri/Cargo.lock \
tauri/src-tauri/tauri.conf.json tauri/package.json
git commit -m "chore(tauri): stamp version X.Y.Z"
git push origin master
git tag -a vX.Y.Z -F /tmp/release-notes.txt # message from a file
git push origin vX.Y.ZReproduces what CI's goreleaser job does, locally, without publishing:
goreleaser release --snapshot --cleanBuilds Go binaries for linux+darwin × amd64+arm64 and per-arch Docker
images into ./dist, skips publishing to GHCR, skips the GitHub release.
~2–3 minutes; surfaces almost every config issue you'd otherwise hit on
the real tag push. Does not exercise the Tauri matrix — that's
GitHub-Actions-only.
bun scripts/release.ts runs this for you unless you pass
--skip-snapshot.
Inspect the auto-generated changelog. The first tag in the repo lists
every commit since the dawn of git history; subsequent tags list only commits
since the previous tag. If the changelog is unusable, tune
.goreleaser.yaml's changelog.filters or use --release-notes <file> to
override before tagging.
Pre-flight checks before the snapshot run (the script enforces these):
git statusis clean. Goreleaser refuses to release from a dirty worktree.dist/is gitignored at repo root. The snapshot writes binaries and tarballs into./dist; if the directory is tracked, those artifacts can leak into a follow-up commit and break the next release on--clean. Theui/dist/entry in.gitignoredoes not cover repo-rootdist/.goreleaseritself is on PATH (go install github.com/goreleaser/goreleaser/v2@latest).
scripts/release.ts automatically stamps Tauri version files
(Cargo.toml, package.json, tauri.conf.json). After the Release bundles
are uploaded, .github/workflows/release.yml automatically refreshes release
docs via scripts/update-release-links.ts.
The post-release updater covers:
README.md— Desktop app download table and pinned Docker image example.docs/deployment.md— image-pinning example, tarball URLs, and the "Available tarballs forvX.Y.Z" list.docs/desktop-app.md— current-state release heading.
If new docs add copy-pasted release tags, extend scripts/update-release-links.ts
in the same change so future releases do not drift.
If the CI run fails after pushing the tag:
git push --delete origin vX.Y.Z
git tag -d vX.Y.Z
# fix root cause, retag, retryTag deletion on GitHub also clears the dangling Release entry (if one was created before the failure step). Goreleaser's release pipeline is mostly idempotent — a clean retag at a fixed commit produces the same artifacts.
For Tauri-side failures, the .dmg / .deb / .AppImage / .msi may be
partially uploaded. tauri-action uploads with --clobber, so a retag
re-uploads cleanly without manual cleanup.
The published image is built by goreleaser in CI using Dockerfile.release.
For local validation:
docker compose build hecate
just test-docker-smokedocker compose uses the development Dockerfile, not Dockerfile.release.
Any new ENV var or runtime default needs to land in both files; otherwise
local dev and the published image diverge silently.
For published images, pin by tag in deployment examples and release notes.
Avoid recommending latest for anything beyond quick experiments.
Each release note should include:
- Highlights — the most important operator-visible changes.
- Breaking or risky changes — config, storage, API, auth, provider, or UI behavior changes that can surprise an operator.
- Migration notes — storage/schema considerations and any manual steps.
- Verification — the exact gate that passed, normally
just verify. - Known limitations — link to
known-limitations.mdand call out any release-specific caveats.
The public alpha is credible for early technical users, but not a production SLA. Keep these expectations visible:
- APIs and persisted schemas can still change before v1.
- The gateway/provider path is more mature than the task runtime.
- The sandbox is a per-call subprocess with env sanitisation, output cap, wall-clock timeout, and an auto-detected
bwrap/sandbox-execwrapper where available. It is not hardened OS isolation or container-level isolation. - Multi-node deployments are not the primary tested path yet.
- Provider lifecycle covers preset and OpenAI-compatible custom-endpoint adds, plus persisted settings edits; broader provider workflows are still evolving.