|
| 1 | +# Releasing Mantle |
| 2 | + |
| 3 | +Mantle uses [Changesets](https://github.com/changesets/changesets) for versioning and automated releases. |
| 4 | +The CI pipeline is fully automated once the Version PR is merged — no manual tagging or publishing required. |
| 5 | + |
| 6 | +## Prerequisites |
| 7 | + |
| 8 | +Before performing a manual release (bootstrap or re-tag), confirm you have: |
| 9 | + |
| 10 | +- **Push access** to `main` and tags on `github.com/dvflw/mantle` |
| 11 | +- **`gh` CLI** installed and authenticated (`gh auth status`) |
| 12 | +- **GHCR write access** — your GitHub account must have write access to `ghcr.io/dvflw` (granted via org membership) |
| 13 | + |
| 14 | +The automated CI path (Version PR → merge) requires no local tooling beyond git. |
| 15 | + |
| 16 | +## How It Works |
| 17 | + |
| 18 | +``` |
| 19 | +1. changeset file added during dev |
| 20 | + ↓ |
| 21 | +2. push to main → release-please.yml creates/updates "Version PR" |
| 22 | + ↓ |
| 23 | +3. merge Version PR |
| 24 | + ↓ |
| 25 | +4. release-tags.yml detects version bumps, pushes package-scoped tags |
| 26 | + ↓ |
| 27 | +5a. release-engine.yml → goreleaser: binaries + Docker + GitHub Release + Trivy |
| 28 | +5b. release-helm.yml → helm push OCI + GitHub Release |
| 29 | +``` |
| 30 | + |
| 31 | +> **Note on workflow naming:** `release-please.yml` is named after a tool we evaluated but didn't adopt — it actually runs the Changesets CLI. A rename to `changeset-version.yml` is tracked in [the relevant issue]. |
| 32 | +
|
| 33 | +## Step 1: Add a Changeset (During Development) |
| 34 | + |
| 35 | +Every PR that changes user-visible behavior needs a changeset. Run this from the repo root: |
| 36 | + |
| 37 | +```bash |
| 38 | +bun run changeset |
| 39 | +``` |
| 40 | + |
| 41 | +The interactive CLI asks: |
| 42 | +1. **Which packages changed?** Select from `@mantle/engine`, `@mantle/helm-chart`, `@mantle/site` |
| 43 | +2. **Bump type per package:** `major` / `minor` / `patch` |
| 44 | +3. **Summary:** One-line description of the change (appears in CHANGELOG) |
| 45 | + |
| 46 | +This creates a file like `.changeset/fuzzy-lions-dance.md`. Commit it alongside your code. |
| 47 | + |
| 48 | +> **`@mantle/site`:** changesets version the docs site but do not trigger a separate release workflow. The `site/v*` tag is pushed automatically but there is no equivalent `release-site.yml` — site deploys happen via the Astro/hosting pipeline. |
| 49 | +
|
| 50 | +**When to use each bump type:** |
| 51 | +- `patch` — bug fixes, internal refactors, docs updates |
| 52 | +- `minor` — new features, new CLI commands, new config fields |
| 53 | +- `major` — breaking changes (workflow YAML format, CLI flags, API shape) |
| 54 | + |
| 55 | +If a PR touches only CI, tests, or tooling with no user impact, skip the changeset. |
| 56 | + |
| 57 | +## Step 2: The Version PR |
| 58 | + |
| 59 | +After any push to `main` that contains pending changesets, the `release-please.yml` workflow automatically creates (or updates) a PR titled **"ci: version packages"**. |
| 60 | + |
| 61 | +This PR: |
| 62 | +- Bumps `version` in each affected `package.json` (taking the highest bump across all pending changesets) |
| 63 | +- Generates / appends to `packages/*/CHANGELOG.md` |
| 64 | +- Deletes the consumed changeset files |
| 65 | + |
| 66 | +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. |
| 67 | + |
| 68 | +> **Loop guard:** the workflow has an `if` condition 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 in `release-please.yml` to match. |
| 69 | +
|
| 70 | +## Step 3: Merge the Version PR |
| 71 | + |
| 72 | +When you're ready to cut a release, **merge the Version PR**. That's the release trigger. |
| 73 | + |
| 74 | +## Step 4: Tags and Artifacts (Automated) |
| 75 | + |
| 76 | +After the Version PR merges, `release-tags.yml` fires and: |
| 77 | + |
| 78 | +1. Detects which `package.json` files changed version |
| 79 | +2. If the engine version bumped, syncs `Chart.yaml appVersion` and commits it back to `main` |
| 80 | +3. Pushes package-scoped tags: `engine/v<version>`, `helm-chart/v<version>`, `site/v<version>` |
| 81 | + |
| 82 | +Those tags trigger the package-specific release workflows: |
| 83 | + |
| 84 | +| Tag pattern | Workflow | What it produces | |
| 85 | +|---|---|---| |
| 86 | +| `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 | |
| 87 | +| `helm-chart/v*` | `release-helm.yml` | Packages and pushes `oci://ghcr.io/dvflw/helm-charts/mantle:<version>`, creates GitHub Release | |
| 88 | + |
| 89 | +**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. |
| 90 | + |
| 91 | +**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. |
| 92 | + |
| 93 | +**Platform-specific image tags:** the pipeline no longer publishes per-arch tags like `ghcr.io/dvflw/mantle:<version>-amd64`. Only the multi-arch manifest tag (`ghcr.io/dvflw/mantle:<version>`) is pushed. Update any CI or deploy configs that reference the old suffixed tags. |
| 94 | + |
| 95 | +**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. |
| 96 | + |
| 97 | +**Partial-failure recovery:** if `release-tags.yml` fails after pushing some but not all tags, push the missing tags manually: |
| 98 | + |
| 99 | +```bash |
| 100 | +git tag helm-chart/v<version> |
| 101 | +git push origin helm-chart/v<version> |
| 102 | +``` |
| 103 | + |
| 104 | +The tag-triggered workflows are safe to retrigger — goreleaser will fail cleanly if the GitHub Release already exists. |
| 105 | + |
| 106 | +## First Release of a New Version (No Pending Changesets) |
| 107 | + |
| 108 | +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. |
| 109 | + |
| 110 | +In this state the `release-please.yml` workflow has nothing to process and will not create a Version PR. Push the tags directly: |
| 111 | + |
| 112 | +```bash |
| 113 | +git tag engine/v<version> |
| 114 | +git push origin engine/v<version> |
| 115 | + |
| 116 | +# If also releasing the Helm chart at the same version: |
| 117 | +git tag helm-chart/v<version> |
| 118 | +git push origin helm-chart/v<version> |
| 119 | +``` |
| 120 | + |
| 121 | +After the initial tag, use the full changeset flow for all subsequent releases. |
| 122 | + |
| 123 | +## Verifying a Release |
| 124 | + |
| 125 | +After the workflows complete, substitute your version for `<version>`: |
| 126 | + |
| 127 | +```bash |
| 128 | +# Check GitHub Releases |
| 129 | +gh release list |
| 130 | + |
| 131 | +# Verify Docker image |
| 132 | +docker pull ghcr.io/dvflw/mantle:<version> |
| 133 | +docker run --rm ghcr.io/dvflw/mantle:<version> mantle version |
| 134 | + |
| 135 | +# Verify Helm chart |
| 136 | +helm show chart oci://ghcr.io/dvflw/helm-charts/mantle --version <version> |
| 137 | +``` |
| 138 | + |
| 139 | +## Pre-releases |
| 140 | + |
| 141 | +To cut a pre-release (e.g. `0.5.0-rc.1`), no changeset is needed — bump the version manually and tag directly: |
| 142 | + |
| 143 | +```bash |
| 144 | +# 1. Set the pre-release version in package.json |
| 145 | +# Edit packages/engine/package.json: "version": "0.5.0-rc.1" |
| 146 | +git add packages/engine/package.json |
| 147 | +git commit -m "chore: bump engine to 0.5.0-rc.1" |
| 148 | +git push origin main |
| 149 | + |
| 150 | +# 2. Tag and push |
| 151 | +git tag engine/v0.5.0-rc.1 |
| 152 | +git push origin engine/v0.5.0-rc.1 |
| 153 | +``` |
| 154 | + |
| 155 | +goreleaser'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. |
| 156 | + |
| 157 | +## Rollback |
| 158 | + |
| 159 | +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. |
| 160 | + |
| 161 | +**1. Communicate immediately** with the last known-good version and the broken version to avoid. |
| 162 | + |
| 163 | +**2. Optionally hide the broken release from the GitHub Releases UI:** |
| 164 | + |
| 165 | +```bash |
| 166 | +# Mark as pre-release so it no longer shows as the "latest" release |
| 167 | +gh release edit engine/v<broken-version> --prerelease |
| 168 | + |
| 169 | +# Or move to draft (still accessible by direct URL/tag, just hidden from listing) |
| 170 | +gh release edit engine/v<broken-version> --draft |
| 171 | +``` |
| 172 | + |
| 173 | +> These do not retract published Docker images or Helm charts from GHCR. Downstream users who have already pulled the image are not affected. |
| 174 | +
|
| 175 | +**3. Cut a patch release** as the authoritative fix via the normal changeset flow. |
0 commit comments