Versions and upgrades
Three versions govern a Helix deployment, and confusing them is the usual cause of a puzzling install. This page is the one place they are all explained: what each governs, which of them is actually enforced, how to see what a node is running, and how to move it forward safely.
If you only take one rule away, take this one: float while you develop, pin for anything live.
1. The three versions
Section titled “1. The three versions”| where it lives | what it governs | enforced? | |
|---|---|---|---|
| Core | helix/runtime/VERSION — an image tag | the engine: Spiral, crud, dispatch, the SPA | no — provenance only |
| SDK | helix/custom/package.json, exact | the contract between your extension and the platform | yes — deploy refuses a mismatched minor |
| Client bundle | helix/custom/package.json version | your extension, domain, map and config | no — recorded, never checked |
Only the SDK decides whether a pairing is allowed to run. A 0.1.x bundle runs on any 0.1.x core; a
0.1 bundle on a 0.2 core is refused outright. The core version is provenance — it tells you what a
thing was built and signed off against, which is what you need when something misbehaves, but it never
blocks a deployment on its own.
That is why the core can float while the SDK cannot: picking up a newer core is a fix, picking up a different contract is a rebuild.
2. Floating versus exact
Section titled “2. Floating versus exact”Two tags move:
sdk-<MAJOR.MINOR>(e.g.sdk-0.1) — always the newest core on that SDK line. This is what a client repo develops against.latest— always the newest core, full stop.
Anything else (0.0.5d, 1.2.0) is exact and never moves.
A live node may not run a floating tag. run, spiral, spiralOnly, master, replicant, readOnly
and snapshot refuse to start:
helixctl: 'master' is a live mode but the image is the floating tag 'latest' (currently core 0.0.5d). A live node must state what it runs. Pin it: helixctl pin # writes 0.0.5d to /srv/helix/runtime/VERSION dev, tuning and demo may float.The reason is the one in the message: a node on a moving tag cannot answer “which core is this?”, and that is
precisely the question you need answered when a deployed system is behaving strangely. helixctl pin
resolves the float to the exact tag it currently points at and writes it — one command, and the node can then
account for itself.
dev, tuning and demo are exempt by design. They mount your repo and switching core is part of the
job. That difference — a working copy floats, a release is pinned — is the whole distinction.
3. Reading what a node runs
Section titled “3. Reading what a node runs”Three views of the same fact, from three sides.
From the host — helixctl version:
runtime : /usr/bin/podman (podman)image : registry.gitlab.com/honeywick-consulting/helix/helixcore:0.0.5dpinned tag : 0.0.5d (helix/runtime/VERSION — the tag helixctl pulls)core version : 0.0.5d (true helixCore baked in the image)SDK (image) : 0.1.8 (contract the image provides)SDK (client) : 0.1.6 (deployed bundle built against)core (client): 0.0.5c (deployed bundle built against)client (image): none (client-agnostic core; the client comes from a deployed bundle)Read it as two pairs. pinned tag versus core version says whether you are on a float and what it resolved
to. The two (client) lines say what the deployed bundle was built against — so the example above is a
bundle built on 0.0.5c now running on 0.0.5d, which is fine (same SDK line) and which deploy will have
mentioned in passing when it landed.
From the container — every node prints its identity at boot, taken from the image itself rather than the tag it was pulled under:
[startHelix] family=cluster concurrency=cluster mode=master client=AA core=0.0.5d bundle=AA-1.4.0 sdk=0.1.6 | mongo:durable(ext4)@…From the artifact — the bundle’s client/manifest.json:
{ "company": "AA", "sdkVersion": "0.1.6", "version": "1.4.0", "coreVersion": "0.0.5c" }4. Publishing a client
Section titled “4. Publishing a client”One command builds a release, in one of four forms. All of them resolve your pin to an exact core, build your extension inside a container of that core, and refuse if the two disagree — so the machine you release from needs no node, npm or client toolchain, only the container runtime it already had.
./helix/runtime/helixctl release [version] [--as bundle|set|image|core-set]The version is optional. It defaults to your client’s helix/custom/package.json version — the same
number the About dialog shows, because the build injects it there too. So a release is reproducible from a
checkout: git checkout <tag> && helixctl release produces the same artifact, with nothing to retype and
nothing to keep in step. Give a version explicitly to override it — a release candidate, or rebuilding
identical source against a different --core.
--as | what you hand over | the node takes it with | when |
|---|---|---|---|
bundle | helix-<client>-<ver>.tar | helixctl deploy <tar> | the node already runs the core you want |
set (default) | one tar: core image + bundle + launcher + pin | helixctl install | air-gapped, or a controlled production estate |
image | one image, everything baked in | helixctl pull | you deploy from a registry |
core-set | the core alone | helixctl install, then deploy | bootstrapping or air-gap — see below |
Useful flags: --core <tag> releases against a specific core rather than your current pin; --from <dir>
points at a client repo elsewhere; --image-ref, --no-push and --config-local <file> apply to
--as image.
A bundle
Section titled “A bundle”The extension, domain, map and config, for a node already on the right core.
./helix/runtime/helixctl release 1.4.0 --as bundle./helix/runtime/helixctl deploy helix-<client>-1.4.0.tarA set — nothing needed on the far side
Section titled “A set — nothing needed on the far side”./helix/runtime/helixctl release 1.4.0# -> helix-<client>-release-1.4.0.tar (core image + bundle + helixctl + VERSION)
tar xf helix-<client>-release-1.4.0.tar && cd helix-<client>-release-1.4.0./helixctl install # load the image, pin VERSION, deploy the bundle./helixctl <CLIENT> runAn image — one artifact, nothing to install
Section titled “An image — one artifact, nothing to install”echo registry.example.com/helix > helix/runtime/CLIENT_REGISTRY # once./helix/runtime/helixctl release 1.4.0 --as image # builds and pushes; --no-push to hold backThe image is the exact core with your sealed bundle baked in, labelled with the client, its version and its
SDK. On the node, point helix/runtime/REGISTRY and VERSION at it and helixctl pull — there is nothing
to deploy. helixctl reads the label at launch and mounts no bundle; it refuses if the node also has a
deployed bundle, or if the image is baked for a different client, so there is only ever one answer to what
the node is running.
A core set — the special case
Section titled “A core set — the special case”--as core-set is the odd one out: no client repo, no build, no bundle. It is the core travelling on its
own — the exact image, the launcher from it, and a pin — for the two situations where a client bundle is not
the thing being moved:
- Bootstrapping a machine that has no core yet, ahead of any client.
- An air-gapped estate, where the core arrives separately from the client.
It is normally cut by the core release itself (publishHelixCore.sh <ver> --set) rather than by hand.
install recognises a set with no bundle and runs two steps instead of three:
tar xf helixcore-release-<ver>.tar && cd helixcore-release-<ver>./helixctl install # loads the image and pins VERSION — that is all./helixctl deploy <client-bundle>.tar # the client arrives separately./helixctl <CLIENT> run5. Upgrading
Section titled “5. Upgrading”A single node
Section titled “A single node”./helix/runtime/helixctl pull # brings the core AND the new launcher (the stub self-updates)./helix/runtime/helixctl pin # resolve the float to the exact tag./helix/runtime/helixctl version # confirmTo avoid pulling a moving tag at all, write the exact tag first — echo 0.0.5d > helix/runtime/VERSION —
then pull. You fetch precisely what you asked for and never touch the float.
Whether the client bundle needs rebuilding depends only on the SDK. Same MAJOR.MINOR, and the deployed
bundle keeps serving; a moved minor, and you rebuild and redeploy.
A fleet
Section titled “A fleet”helixctl upgrade [tag] upgrades every tenant on the host, reading a manifest (HELIX_TENANTS, else
./helix-tenants) of client | home-dir | src-dir | restart-cmd rows. Per tenant it pulls, compares the new
image’s SDK to the deployed bundle’s, and then:
- same MAJOR.MINOR — compatible: repin and run the tenant’s restart command.
- moved, with a src-dir — rebuild the bundle (
release --as bundle) and deploy it, then restart. - moved, no src-dir — report and skip, leaving the old bundle serving. An incompatible pair is never silently started.
Pass the tag. helixctl upgrade 0.0.5d repins each tenant’s VERSION to that exact tag; with no argument
it reuses whatever each tenant already holds — and if that is latest, you land straight back on the float
refusal.
Without a helix-tenants file, upgrade exits saying so. That is the signal that you want the single-node
path above.
6. helixctl command reference
Section titled “6. helixctl command reference”Run helixctl help on any node for the same list.
Image and bundle
Section titled “Image and bundle”| command | what it does |
|---|---|
check (alias doctor) | audit host readiness: run / client-dev / core-dev (runs automatically before pull) |
login | log in to the image registry (once per server) |
pull | pull the pinned image (only changed layers download) |
load [tar] | load an image tar on an air-gapped host; refuses to guess between several |
pin [tag] | pin VERSION to an exact core, resolving the current float |
deploy [--config-local <file>] <bundle.tar> | unpack a built bundle and seal it; checks the SDK |
pack [out.tar] | tar the deployed bundle back up for redistribution |
bin [dir] | pull the stdio wrappers (helixSpiral, helixSpiralOnly) out of the image into helix/runtime/bin |
harness [dir] | pull the Spiral test harness (testSpiral, testSpiralRun) out of the image into tests/ |
upgrade [tag] | fleet upgrade every tenant in the host manifest |
version | what this node runs — image, pin, core, SDKs, baked client |
stop / logs / shell | act on the running container |
help | the same reference, on the node |
Releasing
Section titled “Releasing”| command | what it does |
|---|---|
release [ver] [--as bundle|set|image|core-set] | build a release in a client or demo repo; ver defaults to the client’s package.json — see §4 |
install [set.tar] | install a set on a node: load, pin, and deploy if the set carries a bundle |
Running
Section titled “Running”helixctl <CLIENT> <mode>, mirroring startHelix <CLIENT> <mode>:
| mode | what it is |
|---|---|
run (alias solo) | isolated ephemeral node — try or show the product |
demo | run with the demo deployment class |
dev <repo> | mount the client repo and rebuild the extension on save |
tuning <repo> | mount the repo, build nothing — domain tuning |
master | cluster primary, durable |
replicant / readOnly | cluster partners (voting / never elected) |
snapshot | hidden snapshotting member (stub) |
spiral | fed primary — the full stack driven from stdin |
spiralOnly | the raw optimiser as a command-line filter |
For the two stdin-driven modes there are wrappers that take no arguments at all — they work out the client themselves, so a feed is just a pipe:
./helix/runtime/helixctl bin # once: pull them out of the image
./helix/runtime/bin/helixSpiral < feed.json > records.json 2> diagnostics.log./helix/runtime/bin/helixSpiralOnly < feed.json > records.json 2> diagnostics.logThey add no wiring of their own — spiral mode already passes -i without a TTY (which would mangle the
stream), sends its banner to stderr so stdout belongs to Spiral, and execs so the file descriptors pass
straight through. The wrappers only remove what you would otherwise have to type: the client name, and for
spiral the public address, which they default to this host.
Running them from somewhere else
Section titled “Running them from somewhere else”If your own packaging puts the wrapper in its own bin/ — an application tar that ships the Helix runtime as
a subdirectory, say — tell it where the runtime is with HELIX_HOME, the same variable helixctl uses for
its own base directory:
ROOT="$(cd "$(dirname "$0")/.." && pwd)" # a relocatable tar: compute it, never hardcodeexport HELIX_HOME="$ROOT/helixRuntime"export SPIRAL_LICENCE="…"export HELIX_CLIENT=AA # optional — skips client detection"$ROOT/bin/helixSpiralOnly" < feed.json > records.json 2> diagnostics.logA symlink into any directory needs no environment at all: the wrapper resolves its own real path first, so it finds the runtime through the link rather than beside it.
That runtime directory is exactly what helixctl install produces. The parts that matter at run time are
helixctl, an exact VERSION (these are live modes and refuse a float), and client.tar — without which
nothing starts. client/manifest.json is what lets the wrapper work out the client, so it is optional if you
export HELIX_CLIENT; logs/ is created for you.
Mode and deployment class are different axes: run, dev and demo all report mode solo, while the
class (dev / demo / live) says how seriously to treat the instance. Both are printed at boot.
Container log levels follow the same split: dev and tuning log at INFO — they exist for watching
the system, and INFO carries the load/snapshot timings — while every other mode logs at ERROR (spiral
raises itself to WARN). HELIX_LOG_LEVEL overrides in either direction; the ambient LOG_LEVEL is
deliberately ignored, because a stray DEBUG in a launching shell once flooded a production dispatch log to
gigabytes.