DocsWorking on the code
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
-
Land everything.
developis green, the working tree is clean, and no half-finished work is sitting uncommitted. -
Decide the version. Look at what changed since the last tag. Breaking change in
0.xmeans a MINOR bump. -
Write the changelog.
skelet changelogshows what the release will say, gathered from the fragments inchangelog/unreleased/. Read it through, fix any entry that needs it in its fragment, and add asummaryfragment if the release deserves an opening paragraph. Thenskelet changelog release <version>writeschangelog/<version>.mddated today, lists it in CHANGELOG.md and deletes the fragments. Commit all of it. -
Bump the version constants that actually changed.
sdk.Versiononly if the SDK surface moved,contract.Versiononly if the wire format did. Do not bump them in sympathy with the product version. -
Run the gates.
skelet check,skelet lint,skelet test e2e, andskelet gen --check. All of them, not the fast ones. -
Merge
developintomain. No squash -main’s history should show what went into the release. -
Tag it.
git tag -a v0.1.0 -m "v0.1.0" git push origin main --follow-tagsAnnotated, not lightweight:
git describeand 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.mdas the notes, with the binaries,checksums.txtandinstall.shattached. 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 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.Versionmoved, 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 --tagsCore’s
go.modrequires the SDK atsdk.Version- the localreplacehides that inside this repo, but a plugin fetching core resolves exactly that tag, so it has to exist. -
Publish images, if you are shipping them:
REGISTRY=registry.example.com skelet release v0.1.0 -
Merge
mainback intodevelopso 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:
-
Pack them at the release’s version, as above, and publish each by hand from your machine, logged in as an owner of the
skeletorg: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 -
On npmjs.com, in each package’s settings, add a trusted publisher: GitHub Actions, organization
skeletg, repositoryskelet, workflowrelease.yml(the filename is case-sensitive). -
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, 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.
