# Releasing

How a version gets cut, and what the branches mean.

## The two branches

**`main` is released code only.** Every commit on it is a release, and every
release is tagged. If you are looking at `main`, you are looking at what someone
could be running in production.

**`develop` is where work integrates.** Feature branches cut from it and merge
back into it. It is not expected to be released, and CI does not gate it - the
suite runs on pull requests into `main`, so a develop→main PR is where a
regression surfaces. Run `skelet check` and `skelet test e2e` locally before merging
anything sizeable, or the release PR is the first time anyone finds out.

Nothing else is long-lived. Feature branches are short and get deleted after they
merge; the repo already carries forty of them, and that is forty too many.

```
feature/x ──┐
            ├──> develop ──────> main + tag vX.Y.Z
feature/y ──┘
```

A fix that cannot wait for the next release branches from `main`, merges to
`main`, gets tagged, and is merged back into `develop` so it is not lost.

## Version numbers, and which is which

Several things carry versions, and they move independently. Conflating them is the
mistake to avoid:

| | Where | What it means |
|---|---|---|
| **Release tag** | `vX.Y.Z` git tag | the whole product |
| **Core module** | `apps/backend/core/vX.Y.Z` git tag | the same number as the release - what a plugin fetches core's GraphQL model packages by |
| **SDK** | `sdk.Version`, tagged `apps/backend/sdk/vX.Y.Z` | the plugin API surface |
| **Provider contract** | `contract.Version` | the remote-provider wire format |
| **Plugins** | each plugin's `Manifest.Version` | that plugin |
| **CLI** | stamped from the release tag when the release workflow builds it | the same number as the release - `skelet version` shows it |
| **Storefront packages** | `@skelet/storefront-*` on npm, stamped from the release tag when the release workflow packs them | the same number as the release - they stay `0.0.0` in the repo |

They all read `0.1.0` today because everything starts there. They will diverge on
the first release that changes one and not the others, and that is correct - the
product reaching v0.4 does not mean the SDK did.

While the product is `0.x`, breaking changes ride the MINOR and everything else
the PATCH. There is no compatibility promise until `1.0.0`.

## Cutting a release

1. **Land everything.** `develop` is green, the working tree is clean, and no
   half-finished work is sitting uncommitted.

2. **Decide the version.** Look at what changed since the last tag. Breaking
   change in `0.x` means a MINOR bump.

3. **Write the changelog.** `skelet changelog` shows what the release will say,
   gathered from the fragments in `changelog/unreleased/`. Read it through, fix any
   entry that needs it in its fragment, and add a `summary` fragment if the release
   deserves an opening paragraph. Then `skelet changelog release <version>` writes
   `changelog/<version>.md` dated today, lists it in [CHANGELOG.md](https://skelet.dev/changelog/)
   and deletes the fragments. Commit all of it.

4. **Bump the version constants that actually changed.** `sdk.Version` only if
   the SDK surface moved, `contract.Version` only if the wire format did. Do not
   bump them in sympathy with the product version.

5. **Run the gates.** `skelet check`, `skelet lint`, `skelet test e2e`, and
   `skelet gen --check`. All of them, not the fast ones.

6. **Merge `develop` into `main`.** No squash - `main`'s history should show what
   went into the release.

7. **Tag it.**

   ```
   git tag -a v0.1.0 -m "v0.1.0"
   git push origin main --follow-tags
   ```

   Annotated, not lightweight: `git describe` and the SDK gate both want a real
   tag object.

   The push runs `.github/workflows/release.yml`, which tests the CLI, builds it
   for macOS and Linux, and publishes the GitHub release - `changelog/X.Y.Z.md`
   as the notes, with the binaries, `checksums.txt` and `install.sh` attached.
   The README's one-line install takes the newest release, so check the run goes
   green: until it does, new installs get the previous one. A release you made by
   hand first just gets the files added. After it, the same workflow publishes the
   four storefront packages to npm at the tag's version - see
   [the storefront packages on npm](#the-storefront-packages-on-npm).

   The Go modules take tags of their own, prefixed with their directory, because
   that's how Go finds a module that isn't at the repo root. Core's always matches
   the release; the SDK's only when `sdk.Version` moved, and then to that number:

   ```
   git tag -a apps/backend/core/v0.1.0 -m "core v0.1.0"
   git tag -a apps/backend/sdk/v0.1.0 -m "sdk v0.1.0"    # only if sdk.Version changed
   git push origin --tags
   ```

   Core's `go.mod` requires the SDK at `sdk.Version` - the local `replace` hides
   that inside this repo, but a plugin fetching core resolves exactly that tag, so
   it has to exist.

8. **Publish images**, if you are shipping them:

   ```
   REGISTRY=registry.example.com skelet release v0.1.0
   ```

9. **Merge `main` back into `develop`** so the changelog and any release-time
   fixes are on both.

## The storefront packages on npm

`@skelet/storefront-core`, `-next`, `-astro` and `-react` publish to npm from
`release.yml`, at the release's version, so a shop in a repo of its own can
install what a Skelet checkout reads from `packages/`. They're MIT, each with its
own `LICENSE`, unlike the GPL rest of the repo. Inside the workspace
nothing changes: the templates import the TypeScript source, and only a packed
tarball points at the compiled `dist/`.

`packages/publish.mjs` does the work. `build` compiles a package with `tsc`, one
JS and one `.d.ts` per source file so each `"use client"` and `"use server"`
stays on its own module, and adds the `.js` extensions ESM needs. `pack
<version>` stamps that version on all four, packs them, then checks each tarball
the way a project would meet it: no `workspace:` or `catalog:` ranges left, every
import resolvable, types beside every module, and core and react installed into
an empty directory with npm and imported module by module. CI runs the same pack
on every pull request into `main`, so a release finds out nothing new.

To see what a release would publish:

```
skelet pnpm storefront-core exec node ../publish.mjs pack 0.4.0
```

The tarballs land in `packages/.packs/`.

**Once, before the first release that publishes them**, because npm only lets a
workflow publish a package that already exists:

1. Pack them at the release's version, as above, and publish each by hand from
   your machine, logged in as an owner of the `skelet` org:

   ```
   for name in storefront-core storefront-react storefront-next storefront-astro; do
     npm publish packages/.packs/skelet-$name-0.4.0.tgz --access public --otp <code>
   done
   ```

2. On npmjs.com, in each package's settings, add a trusted publisher: GitHub
   Actions, organization `skeletg`, repository `skelet`, workflow `release.yml`
   (the filename is case-sensitive).
3. In the same settings, require two-factor authentication and disallow tokens,
   so that workflow is the only way a version gets published.

From then on a release tag publishes all four. A rerun skips versions already on
npm, and a prerelease tag like `v0.5.0-rc.1` goes out under npm's `next` tag so
`latest` keeps pointing at the last real release.

## What the first tag switches on

The SDK's compatibility gate (`sdk.TestAPICompatibility`) resolves its baseline
with `git describe --tags --match "apps/backend/sdk/v*"` and **skips while no such
tag exists**. The product tags don't count, so it stays inert until the SDK is
tagged for the first time.

The first `apps/backend/sdk/v0.1.0` activates it. From that point every change to the SDK's exported
surface is diffed against the last release with `apidiff`, and a change that
breaks compatibility fails CI unless `sdk.Version` carries the bump the rules
demand. That is the point - but it means the first SDK tag is also the moment the
SDK stops being freely reshapeable, so get the surface where you want it *before*
tagging rather than after.

## Before the first release

One thing is release-blocking and it is not about code:

- **Copyright and contribution terms.** The code is GPL-3.0 with the
  [plugin exception](https://github.com/skeletg/skelet/blob/main/PLUGIN-EXCEPTION.txt), and copyright is asserted to Skelet
  Group. If that entity is to hold the rights - which is what keeps commercial
  licensing possible later - the contribution terms need to say so before the
  first outside contribution, not after.

---

From the Skelet documentation: https://skelet.dev/docs/releasing/
