Mantle uses Changesets for versioning and automated releases. The CI pipeline is fully automated once the Version PR is merged — no manual tagging or publishing required.
Before performing a manual release (bootstrap or re-tag), confirm you have:
- Push access to
mainand tags ongithub.com/dvflw/mantle ghCLI installed and authenticated (gh auth status)- GHCR write access — your GitHub account must have write access to
ghcr.io/dvflw(granted via org membership)
The automated CI path (Version PR → merge) requires no local tooling beyond git.
1. changeset file added during dev
↓
2. push to main → release-please.yml creates/updates "Version PR"
↓
3. merge Version PR
↓
4. release-tags.yml detects version bumps, pushes package-scoped tags
↓
5a. release-engine.yml → goreleaser: binaries + Docker + GitHub Release + Trivy
5b. release-helm.yml → helm push OCI + GitHub Release
Note on workflow naming:
release-please.ymlis named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename tochangeset-version.ymlis tracked in dvflw/mantle#123.
Every PR that changes user-visible behavior needs a changeset. Run this from the repo root:
bun run changesetThe interactive CLI asks:
- Which packages changed? Select from
@mantle/engine,@mantle/helm-chart,@mantle/site - Bump type per package:
major/minor/patch - Summary: One-line description of the change (appears in CHANGELOG)
This creates a file like .changeset/fuzzy-lions-dance.md. Commit it alongside your code.
@mantle/site: changesets version the docs site but do not trigger a separate release workflow. Thesite/v*tag is pushed automatically but there is no equivalentrelease-site.yml— site deploys happen via the Astro/hosting pipeline.
When to use each bump type:
patch— bug fixes, internal refactors, docs updatesminor— new features, new CLI commands, new config fieldsmajor— breaking changes (workflow YAML format, CLI flags, API shape)
If a PR touches only CI, tests, or tooling with no user impact, skip the changeset.
After any push to main that contains pending changesets, the release-please.yml workflow automatically creates (or updates) a PR titled "ci: version packages".
This PR:
- Bumps
versionin each affectedpackage.json(taking the highest bump across all pending changesets) - Generates / appends to
packages/*/CHANGELOG.md - Deletes the consumed changeset files
Review the PR to confirm the version bumps and changelog entries are correct. If more changesets land on main before you merge, CI updates the PR automatically.
Loop guard: the workflow has an
ifcondition that skips runs where the triggering commit message starts with"ci: version packages". This prevents the workflow from retriggering on its own Version PR merge commit. If you ever rename the commit message in the workflow, update the guard condition inrelease-please.ymlto match.
When you're ready to cut a release, merge the Version PR. That's the release trigger.
After the Version PR merges, release-tags.yml fires and:
- Detects which
package.jsonfiles changed version - If the engine version bumped, syncs
Chart.yaml appVersionand commits it back tomain - Pushes package-scoped tags:
engine/v<version>,helm-chart/v<version>,site/v<version>
Those tags trigger the package-specific release workflows:
| Tag pattern | Workflow | What it produces |
|---|---|---|
engine/v* |
release-engine.yml |
goreleaser builds Linux/macOS amd64/arm64 binaries + checksums + LICENSE, pushes multi-arch ghcr.io/dvflw/mantle:<version> Docker image (+ floating tags on stable), creates GitHub Release, runs Trivy CVE scan |
helm-chart/v* |
release-helm.yml |
Packages and pushes oci://ghcr.io/dvflw/helm-charts/mantle:<version>, creates GitHub Release |
How the engine tag translates to a clean version: the workflow strips the engine/ prefix, sets GORELEASER_CURRENT_TAG=v<version>, and creates a local git tag v<version> alias so goreleaser can validate the tag against the current commit. Only the namespaced tag (engine/v<version>) is permanent in the remote.
Floating tags (major.minor, major, latest) are only pushed for stable releases. For pre-release versions (e.g. 0.5.0-rc.1) the versioned image is pushed but floating tags are skipped.
Platform-specific image tags: per-arch tags like ghcr.io/dvflw/mantle:<version>-amd64 and ghcr.io/dvflw/mantle:<version>-arm64 exist in GHCR as internal references required by the multi-arch manifest, but are not intended for direct consumption. Always pull the multi-arch manifest tag (ghcr.io/dvflw/mantle:<version>), which selects the correct platform automatically. Update any CI or deploy configs that reference the old suffixed tags directly.
Trivy scan policy: the trivy job runs after release with exit-code: 1 on CRITICAL or HIGH CVEs. A failure marks the workflow run as failed but does not retract the already-published release. If a CVE scan fails post-release, open a patch release issue immediately.
Partial-failure recovery: if release-tags.yml fails after pushing some but not all tags, push the missing tags manually:
git tag helm-chart/v<version>
git push origin helm-chart/v<version>The tag-triggered workflows are safe to retrigger — goreleaser will fail cleanly if the GitHub Release already exists.
This applies when package.json is already at the target version but .changeset/ contains no pending changeset files — for example, the first-ever release of a newly bootstrapped repo, or after manually editing package.json outside the changeset flow.
In this state the release-please.yml workflow has nothing to process and will not create a Version PR. Push the tags directly:
git tag engine/v<version>
git push origin engine/v<version>
# If also releasing the Helm chart at the same version:
git tag helm-chart/v<version>
git push origin helm-chart/v<version>After the initial tag, use the full changeset flow for all subsequent releases.
After the workflows complete, substitute your version for <version>:
# Check GitHub Releases
gh release list
# Verify Docker image
docker pull ghcr.io/dvflw/mantle:<version>
docker run --rm ghcr.io/dvflw/mantle:<version> mantle version
# Verify Helm chart
helm show chart oci://ghcr.io/dvflw/helm-charts/mantle --version <version>To cut a pre-release (e.g. 0.5.0-rc.1), no changeset is needed — bump the version manually and tag directly:
# 1. Set the pre-release version in package.json
# Edit packages/engine/package.json: "version": "0.5.0-rc.1"
git add packages/engine/package.json
git commit -m "chore: bump engine to 0.5.0-rc.1"
git push origin main
# 2. Tag and push
git tag engine/v0.5.0-rc.1
git push origin engine/v0.5.0-rc.1goreleaser's prerelease: auto publishes the GitHub Release as a pre-release when the version contains a pre-release identifier. The versioned Docker image (0.5.0-rc.1) is pushed; floating tags (latest, major, major.minor) are not.
Releases are immutable — GitHub Releases and pushed OCI images cannot be unpublished retroactively for users who have already pulled them. Rolling back means directing users to the last known-good version and cutting a patch.
1. Communicate immediately with the last known-good version and the broken version to avoid.
2. Optionally hide the broken release from the GitHub Releases UI:
# Mark as pre-release so it no longer shows as the "latest" release
gh release edit engine/v<broken-version> --prerelease
# Or move to draft (still accessible by direct URL/tag, just hidden from listing)
gh release edit engine/v<broken-version> --draftThese do not retract published Docker images or Helm charts from GHCR. Downstream users who have already pulled the image are not affected.
3. Cut a patch release as the authoritative fix via the normal changeset flow.