Skip to content

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.

where it liveswhat it governsenforced?
Corehelix/runtime/VERSION — an image tagthe engine: Spiral, crud, dispatch, the SPAno — provenance only
SDKhelix/custom/package.json, exactthe contract between your extension and the platformyesdeploy refuses a mismatched minor
Client bundlehelix/custom/package.json versionyour extension, domain, map and configno — 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.

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.

Three views of the same fact, from three sides.

From the hosthelixctl version:

runtime : /usr/bin/podman (podman)
image : registry.gitlab.com/honeywick-consulting/helix/helixcore:0.0.5d
pinned 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" }

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.

Terminal window
./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.

--aswhat you hand overthe node takes it withwhen
bundlehelix-<client>-<ver>.tarhelixctl deploy <tar>the node already runs the core you want
set (default)one tar: core image + bundle + launcher + pinhelixctl installair-gapped, or a controlled production estate
imageone image, everything baked inhelixctl pullyou deploy from a registry
core-setthe core alonehelixctl install, then deploybootstrapping 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.

The extension, domain, map and config, for a node already on the right core.

Terminal window
./helix/runtime/helixctl release 1.4.0 --as bundle
./helix/runtime/helixctl deploy helix-<client>-1.4.0.tar
Terminal window
./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> run

An image — one artifact, nothing to install

Section titled “An image — one artifact, nothing to install”
Terminal window
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 back

The 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.

--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:

Terminal window
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> run
Terminal window
./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 # confirm

To 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.

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.

Run helixctl help on any node for the same list.

commandwhat it does
check (alias doctor)audit host readiness: run / client-dev / core-dev (runs automatically before pull)
loginlog in to the image registry (once per server)
pullpull 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
versionwhat this node runs — image, pin, core, SDKs, baked client
stop / logs / shellact on the running container
helpthe same reference, on the node
commandwhat 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

helixctl <CLIENT> <mode>, mirroring startHelix <CLIENT> <mode>:

modewhat it is
run (alias solo)isolated ephemeral node — try or show the product
demorun 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
mastercluster primary, durable
replicant / readOnlycluster partners (voting / never elected)
snapshothidden snapshotting member (stub)
spiralfed primary — the full stack driven from stdin
spiralOnlythe 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:

Terminal window
./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.log

They 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.

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:

Terminal window
ROOT="$(cd "$(dirname "$0")/.." && pwd)" # a relocatable tar: compute it, never hardcode
export HELIX_HOME="$ROOT/helixRuntime"
export SPIRAL_LICENCE=""
export HELIX_CLIENT=AA # optional — skips client detection
"$ROOT/bin/helixSpiralOnly" < feed.json > records.json 2> diagnostics.log

A 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.