Docs

Working on the code

DocsWorking on the code

Releasing

6 sections6 min readdocs/releasing.md

On this page6 sections
  1. 1The two branches
  2. 2Version numbers, and which is which
  3. 3Cutting a release
  4. 4The storefront packages on npm
  5. 5What the first tag switches on
  6. 6Before the first release

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:

WhereWhat it means
Release tagvX.Y.Z git tagthe whole product
Core moduleapps/backend/core/vX.Y.Z git tagthe same number as the release - what a plugin fetches core’s GraphQL model packages by
SDKsdk.Version, tagged apps/backend/sdk/vX.Y.Zthe plugin API surface
Provider contractcontract.Versionthe remote-provider wire format
Pluginseach plugin’s Manifest.Versionthat plugin
CLIstamped from the release tag when the release workflow builds itthe 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 themthe 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 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 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, 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.