Upgrade the platform
Meridian has two independently versioned things to upgrade: the platform
(the four core libraries baked into the meridian-api and
meridian-config-plane images) and plugins (published separately and
resolved by the running host at boot). This recipe covers both, and — this is
the part that bites if done out of order — the sequencing between them.
Before starting, make sure you already have a registry set up (Set up a plugin registry) and know how your instance declares its plugins (Declare and install plugins).
Before you start
- Images never bundle business plugins.
Dockerfileinstalls only the workspace and the core plugins (external-plugins/, copied into the image); every plugin underplugins:in the instance manifest is resolved against the registry (or apathoverride) at boot, inensureInstancePlugins(apps/api/src/registry.ts). A plugin that is listed but cannot be resolved — no registry configured, version not found, download failed — fails the boot loudly, the same as an invalid manifest. Practically: publish a plugin version before you deploy or restart anything that will require it. Doing it the other way round turns a routine restart into an outage. - Version resolution is real semver, via the npm
semverpackage: the registry picks the highest published version satisfying the manifest's range (maxSatisfying,libs/plugin-cli/src/registry/registry.ts) and the host checks a plugin'ssdk:range against the running SDK version (satisfies,apps/api/src/plugins.ts). An incompatiblesdk:range is a soft failure: the plugin is skipped and logged, the rest of the host still boots. An unresolvable registry entry is a hard failure: the boot aborts. Keep the two apart when you plan a rollout. - A run's workflow spec and behavior code are journaled when the run starts; changing plugin code or workflow YAML never affects a run already in flight, only new runs. See Why durable execution.
1. Publish new plugin versions first
Do this regardless of whether the platform itself is also changing — it's the step that must land before anything else touches the affected environment.
From the plugin's own repo, build the deliverable
dist/(esbuild bundles every behavior/adapter/source module the manifest references, inlining its runtime dependencies; onlynode:*and@meridian/*stay external):meridian-plugin build .Publish it to the target registry (local directory or
gs://bucket):meridian-plugin publish . --registry gs://meridian-plugins # or: export MERIDIAN_REGISTRY=gs://meridian-plugins && meridian-plugin publish .A published version is immutable —
publishPluginrefuses to overwrite an existing@publisher/name/<version>(libs/plugin-cli/src/registry/registry.ts). If you need to ship a fix, bump the version inplugin.yamland publish again; there is no in-place replace.Confirm it landed before moving on:
meridian-plugin versions @posos/demo --registry gs://meridian-plugins
See Build and bundle and Publish to the registry for the authoring side of this in full, and Bundling and distribution for why the artifact is self-contained.
2. Point instances at the new plugin version
This is safe to do any time after step 1 — the registry now has what any instance will ask for.
- If the instance manifest's range already covers the new version (e.g.
"@posos/demo": "^0.1.0"and you published0.1.2), no manifest change is needed: the next endpoint/proxy restart resolves the new highest match automatically. - If you need the new version specifically, tighten or bump the range in the
manifest and restart endpoint + proxy so
ensureInstancePluginsre-resolves it. See Declare and install plugins and the instance manifest reference for theplugins:block. - A stale local cache never blocks an upgrade to a newer version — only the
requested version is looked up, downloaded once into
MERIDIAN_PLUGIN_CACHE, and reused afterwards.
3. Upgrade the platform libraries (lockstep)
Do this when @meridian/shared, engine-core, llm-client, or plugin-cli
change — they version together as a fixed group
(.changeset/config.json, fixed), so plugin authors only ever have to check
one number.
Any PR touching one of the four libraries includes a changeset:
pnpm changesetOn merge to
main, the release workflow (changesets/action,.github/workflows/release.yml) opens or updates a "Version PR". Merging that PR runs:pnpm run version-packages # changeset version && lockfile refreshwhich bumps all four
package.jsonversions identically. Note thatPLUGIN_CONTRACT_VERSION(libs/shared/src/version.ts) — the constant the host compares every plugin'ssdk:range against — is decoupled from the package version and only changes on a real break of the plugin-facing surface. Publishing these packages to an external npm registry is deliberately disabled for now (release.yml): they're consumed in-workspace, so "upgrading the platform" means building new images from the mergedmain, not installing a new SDK release.Build the images from that commit.
endpointandproxyare the samemeridian-apiimage, started with different commands (pnpm --filter @meridian/api run restatevsrun server— seeDockerfile);uiis the separatemeridian-config-planeimage (Dockerfile.ui). For a compose deployment:pnpm run build:plugins # external plugins mounted via PLUGINS_PATH, not baked in docker compose --profile stack up -d --build
For the full changeset mechanics (drafting a changeset, reviewing the Version PR), see Release and version.
4. Roll out in order
Once the images (if any) are built and the plugin versions you need are published, restart in this order — it's the order that keeps in-flight runs safe and keeps every component talking to a component it can still reach:
- Endpoint — restart it first. It re-registers with Restate on boot; any run already in progress resumes exactly where the journal left it, which is the durability guarantee Restate gives you (no drain step needed). See Why durable execution.
- Proxy — restart next.
- UI — restart last; it is stateless and only serves the console.
- Restate itself upgrades on its own schedule and procedure, independent
of the four services above — keep the
restate-datavolume across the upgrade so the journal survives.
Test the exact platform + plugin version combination in pre-production first; the same advice applies to plugin swaps and to a platform bump alike.
5. Verify
GET /healthzandGET /readyzon the proxy —/readyzreturns 503 with a{ checks }detail if the Restate ingress or the run index isn't reachable yet after the restart.- Check the endpoint's plugin-load logs (
component: "plugins") for the plugin you just published — it should show as loaded, with no SDK incompatibility warning for the new platform version. - Start one new event and confirm the run picks up the new plugin version/behavior, while runs already in flight before the restart keep behaving as journaled.
Roll back
- Plugin roll back is just re-pointing the manifest's range at the older
version (or leaving a
pathoverride in place) and restarting endpoint + proxy — published versions are immutable, so the old version is still sitting in the registry untouched. - Platform roll back is redeploying the previous image tag. If you also
need to roll back the SDK version a plugin was checked against, remember
the check is a soft failure: a plugin whose
sdk:range no longer matches is dropped with a warning, not a hard boot failure — so a partial rollback degrades gracefully rather than blocking restart.
Related
- Set up a plugin registry and Declare and install plugins — the registry and manifest sides of plugin resolution.
- Build and bundle and Publish to the registry — the plugin-author side of steps 1–2.
- Release and version — the changesets workflow behind step 3.
- Back up and restore — snapshot
restate-databefore a platform upgrade you're unsure about. - Why durable execution — why restarting the endpoint never loses an in-flight run.
- Bundling and distribution and Autonomy and packaging — why plugin artifacts are self-contained and versioned independently of the platform.