Qyra

Upgrade safety

Read the release-safety artifact, check an upgrade span, and pick the right deployment strategy

🛠 This page is for engineering teams self-hosting their own Qyra instance. If you're on Qyra Cloud, upgrades are handled for you automatically.

Release-safety artifacts are in Beta. They are published for every release and are safe to read today, but we're still refining how risk is classified and what the artifact carries, so fields and verdicts may change. Build automation against it and keep that automation easy to change, and tell us if the artifact doesn't answer a question you need it to.

Every Qyra release publishes a machine-readable answer to the question: "is upgrading from version X to version Y safe to roll, and are there required stops on the way?"

Two documents carry that answer:

DocumentScopeWhere to get it
release-safety.jsonOne release: the step from previousVersion to versionAttached to every GitHub release as an asset, and committed at the repository root of the release tag (so it ships inside the Docker image)
release-safety-index.jsonEvery release back to the index floor (~12 months)Committed at the repository root: raw.githubusercontent.com/quanvio/qyra/main/release-safety-index.json

Both are public — no account or license required — and both are designed to be consumed by your own automation (CI checks, GitOps hooks) as well as by a human before an upgrade.

The one rule that matters

Only an explicit rollingUpdateSafe: true means a rolling update is advised. unknown means NOT safe.

unknown is what the pipeline publishes when it could not prove safety — a degraded check, an unclassifiable change, an old release nobody vouched for. Treat every unknown exactly like false: use the Recreate strategy. Never write automation that treats "not false" as safe.

The check supplies this verdict. The Helm chart does not fetch or calculate it. Set upgrade.mode: RollingUpdate for true, and upgrade.mode: Recreate for false or unknown. With migrationJob.enabled, chart 2.16.284 and later stops application workloads before its pre-upgrade migration hook when the mode is Recreate. Chart 2.16.283 and earlier needs the manual fallback in the upgrade runbook.

Everything else on this page is detail on top of that rule.

This page is the decision layer: whether an upgrade is safe. Once you have your verdict, the upgrade runbook is the execution layer: the sequence for Kubernetes, docker compose, and automation, plus the migrate command reference, recovery, and rollback.

How to read the signal

  1. Verdicts compose by AND across a span. Upgrading across several releases (say 1.111.01.115.0) is rolling-safe only if every release in the span is rollingUpdateSafe: true. One false or unknown anywhere in the span means the whole upgrade needs Recreate. The cumulative index exists so you can evaluate a whole span in one fetch.
  2. Required stops are hard requirements. If any release in your span lists a version in requiredStops, you must upgrade to that version first, let it run its migrations, and then continue. Skipping a stop runs later migrations against a schema that's missing the prerequisites they assume — which is how databases end up in states that need manual repair.
  3. minPreviousVersion floors direct jumps. Each release states the oldest version you may upgrade from directly. If you're on something older, upgrade to an intermediate version first.
  4. Versions older than the index floor route through the floor. The index reaches back to 0.1893.0. If you're running something older, first upgrade to 0.1893.0 (the floor entry is marked syntheticRequiredStop: true for exactly this reason), then evaluate the rest of your span normally. In practice: upgrading from 0.1050.0 to 1.121.0 means 0.1050.00.1893.0 → whatever the span check from there tells you.
  5. Backfilled entries are conservative. Entries marked backfilled: true were generated retroactively — nobody vouched for those releases at the time they shipped. Expect unknown verdicts there, and plan a Recreate upgrade for any span that crosses them.

Checking an upgrade span

The answer depends on exactly three inputs: the version you're running, the version you're targeting, and every release in between. The Qyra CLI (version 1.126.0 or later) answers it in one command:

qyra upgrade-check --from 1.111.0 --to 1.115.0
Release safety check: 1.111.0 -> 1.115.0
Direction: upgrade
Verdict: UNSAFE
Unsafe rolling-update safety: 1.112.0
Unknown rolling-update safety: 1.113.0, 1.113.1, 1.114.0, 1.115.0
Minimum previous version: 1.111.0
Covered releases: 1.112.0, 1.113.0, 1.113.1, 1.114.0, 1.115.0

The command fetches the public index and applies the span rules from this page — AND-composition, required stops, minimum versions — locally. It needs no Qyra login, token, or instance access, so it runs anywhere, including CI against an air-gapped production instance. Here the verdict is UNSAFE because the span crosses one release known not to be rolling-safe and several unproven ones — so deploy with Recreate: stop the old version, then start the new one.

Using it as a CI gate

The exit code carries the verdict: exit 0 only when the whole span is proven safe to roll. Any other outcome — an unknown or false release in the span, a required stop, a version the index doesn't cover, a fetch failure — exits non-zero. That fail-closed contract means you can use the bare command as a pipeline gate:

qyra upgrade-check --from "$CURRENT_VERSION" --to "$TARGET_VERSION"

For automation that needs the detail, --json prints a stable machine-readable object (fromVersion, toVersion, direction, safe, verdict, requiredStops, minPreviousVersion, coveredVersions, missingRanges):

qyra upgrade-check --from 1.111.0 --to 1.115.0 --json | jq .safe

What the verdict means for your deployment

Verdict for the spanKubernetes / Helmdocker compose
Every release rollingUpdateSafe: trueSet upgrade.mode: RollingUpdatedocker compose up -d with the new tag
Anything false or unknownSet upgrade.mode: Recreate. With migrationJob.enabled, use the runbook's automatic path or fallbackdocker compose down, update the tag, docker compose up -d

Recreate means a short window of downtime. With migrationJob.enabled and chart 2.16.284 or later, the chart stops database-capable application workloads before it migrates. Chart 2.16.283 and earlier needs the manual fallback. Without a migration Job, Recreate still applies the Deployment strategies during Helm's normal apply phase. A rolling update is only appropriate when the verdict certifies that behaviour as safe.

Worked example: reading a release artifact

This is the real artifact attached to release 1.121.0:

{
  "schemaVersion": "2",
  "version": "1.121.0",
  "previousVersion": "1.120.1",
  "releaseDate": "2026-08-11T11:08:39.052Z",
  "migrations": {
    "present": false,
    "count": 0,
    "coreCount": 0,
    "eeCount": 0,
    "files": []
  },
  "compatibility": {
    "rollingUpdateSafe": true,
    "recommendedStrategy": "RollingUpdate"
  },
  "api": {
    "rest": {
      "checked": true,
      "breaking": false,
      "changes": [],
      "breakingCount": 0,
      "advisories": [],
      "advisoryCount": 0
    },
    "mcp": {
      "checked": true,
      "breaking": false,
      "changes": [],
      "breakingCount": 0,
      "advisories": [],
      "advisoryCount": 0
    }
  },
  "config": {
    "checked": true,
    "breaking": false,
    "changes": []
  },
  "upgrade": {
    "minPreviousVersion": "1.111.0",
    "requiredStops": []
  },
  "declaredBreaks": []
}

Reading it top to bottom:

  • No migrations ship in this release (migrations.present: false), so there is no schema change to coordinate.
  • rollingUpdateSafe: true — this is the explicit green verdict. Upgrading from 1.120.1 (the previousVersion) to 1.121.0 can be a rolling update.
  • All three change surfaces were checked and came back clean: no breaking REST API changes, no breaking MCP tool changes (not even non-breaking advisories on either), and no environment variable removals, renames, or default changes.
  • You may jump here directly from 1.111.0 or newer (upgrade.minPreviousVersion), with no required stops on the way — but remember the span rule: the jump is only rolling-safe if every release in between is, which you check against the index, not this single artifact.
  • No engineer declared a breaking change (declaredBreaks is empty).

Field reference: release-safety.json

Fields that can't prove safety report the string "unknown" instead of a boolean — and per the rule above, you consume "unknown" as unsafe.

Top level

FieldTypeMeaning
schemaVersionstringAlways "2" for this format
versionstringThe release this artifact describes
previousVersionstring | nullThe release immediately before it — the step this artifact covers
releaseDateISO date-timeWhen the release was published

migrations

FieldTypeMeaning
presentboolean | "unknown"Whether this release ships database migrations
count, coreCount, eeCountintegerHow many, split between core and Enterprise migrations
files[].namestringMigration file name
files[].editioncore | eeWhich edition ships it
files[].tablesstring[]Tables the migration touches
files[].heavinessobjectThree verdicts — locksTable, rewritesTable, scansTable — each boolean or "unknown", flagging migrations that may lock, rewrite, or scan whole tables (and therefore take time or block writes on large databases)

compatibility

FieldTypeMeaning
rollingUpdateSafeboolean | "unknown"The verdict. Only true means a rolling update is advised
recommendedStrategyRollingUpdate | RecreateA convenience rendering of the verdict. Gate your automation on rollingUpdateSafe, not on this field

api and config

FieldTypeMeaning
api.rest, api.mcpobjectOne entry per API surface — REST endpoints and MCP tools — with the fields below
…​.checkedbooleanWhether the diff for that surface actually ran
…​.breakingboolean | "unknown"Whether the surface has consumer-breaking changes
…​.changes[]string[]The breaking changes, one human-readable line each (e.g. GET /api/v1/… — response property removed). The list is capped with an explicit overflow line; breakingCount always carries the full total
…​.breakingCountintegerUncapped total number of breaking changes
…​.advisories[]string[]Non-breaking advisory notes (for example, a response enum gaining a new value) — worth a scan if you consume the API strictly, but they never affect the breaking verdict. Capped like changes
…​.advisoryCountintegerUncapped total number of advisory notes
config.checkedbooleanWhether the environment variable surface was diffed
config.breakingboolean | "unknown"Whether any config change is breaking
config.changes[]objectEach change is removed (with previousDefault), renamed (with previousName and defaultValue), or defaultChanged (with previousDefault and defaultValue) — scan this list for any environment variable you set explicitly

upgrade and declaredBreaks

FieldTypeMeaning
upgrade.minPreviousVersionstring | nullOldest version you may upgrade from directly
upgrade.requiredStopsstring[]Versions you must pass through on the way to this release
declaredBreaks[]objectBreaking changes declared by engineers in the source (file, line, reason, and whether the break makes this release a requiredStop)

Field reference: release-safety-index.json

The index is a derived summary — one entry per release, oldest first. Per-release artifacts remain the source of truth.

FieldTypeMeaning
backfillFloorVersionstringThe oldest release the index covers (0.1893.0)
entries[].version, previousVersion, releaseDateSame meaning as the per-release artifact
entries[].rollingUpdateSafeboolean | "unknown"The verdict for that single release step
entries[].requiredStops, minPreviousVersionSame meaning as the per-release artifact
entries[].backfilledbooleantrue when the entry was generated retroactively — expect conservative (unknown) verdicts
entries[].syntheticRequiredStopbooleantrue only on the floor entry: versions older than the floor must upgrade to it first

Before you upgrade: checklist

  • Read the release notes for every release in your span.
  • Run the span check — note the verdict, required stops, and minPreviousVersion.
  • If the span verdict is anything other than true, plan a maintenance window and set upgrade.mode: Recreate. With migrationJob.enabled, follow the upgrade runbook.
  • If migrations are present, check their heaviness flags in the per-release artifacts — table rewrites and scans on large tables take time.
  • Scan config.changes for environment variables you set explicitly.
  • Confirm your database backup (and point-in-time recovery, if configured) is current.
  • Follow the upgrade runbook for the sequence itself, and read its rollback section before you need it.
  • After upgrading, update the Qyra CLI to match.

Appendix: checking a span without the CLI

In environments where you can't run the CLI (no Node.js, or a fully air-gapped pipeline that mirrors the index), you can evaluate a span directly from the index with curl and jq. Check every release between your current version (exclusive) and your target (inclusive):

FROM=1.111.0   # the version you are running
TO=1.115.0     # the version you want

curl -fsSL https://raw.githubusercontent.com/quanvio/qyra/main/release-safety-index.json |
jq --arg from "$FROM" --arg to "$TO" '
  .entries
  | (map(.version == $from) | index(true)) as $i
  | (map(.version == $to) | index(true)) as $j
  | if $i == null or $j == null then
      error("version not found in index — treat the span as unsafe")
    else
      .[$i+1:$j+1]
      | {
          releasesInSpan: length,
          rollingUpdateSafe: (map(.rollingUpdateSafe == true) | all),
          requiredStops: (map(.requiredStops[]) | unique),
          minPreviousVersionForTarget: (last.minPreviousVersion)
        }
    end'

Against the live index, that span answers:

{
  "releasesInSpan": 5,
  "rollingUpdateSafe": false,
  "requiredStops": [],
  "minPreviousVersionForTarget": "1.111.0"
}

Read it in order:

  1. requiredStops is empty — no mandatory intermediate version, so a direct jump is allowed…
  2. …provided your current version is at or above minPreviousVersionForTarget. Here 1.111.01.111.0, so the jump is permitted.
  3. rollingUpdateSafe is false — the span crosses at least one release that is not proven rolling-safe, so deploy with Recreate.

If your current version isn't in the index at all, the command errors on purpose. That's the fail-safe default: a version the index can't see is a span it can't vouch for — the same rule upgrade-check applies.