Skip to content

fix(helm): give the migrate Job the session key; add an upgrade test - #6

Merged
christianhuening merged 6 commits into
mainfrom
ci/helm-upgrade-test
Aug 17, 2026
Merged

fix(helm): give the migrate Job the session key; add an upgrade test#6
christianhuening merged 6 commits into
mainfrom
ci/helm-upgrade-test

Conversation

@christianhuening

Copy link
Copy Markdown
Contributor

The bug

The Helm chart has never been installable with default values.

The pre-install/pre-upgrade migrate Job injects KNOT_DATABASE_URL but not KNOT_SESSION_KEY. The binary loads and validates the entire config before dispatching the subcommand (main.rs:41), and validate() rejects an empty session key (lib.rs:203) — so the hook exits 2 with KNOT_SESSION_KEY is required and every helm install/helm upgrade fails before reaching the Deployment.

$ helm install knot ...
Error: INSTALLATION FAILED: failed pre-install: resource Job/knot-migrate
       not ready. status: Failed, message: Job Failed. failed: 1/1

$ kubectl logs <migrate-pod>
config: invalid: KNOT_SESSION_KEY is required (set it in every environment)

Present identically in the published 0.1.0 and 0.2.0 charts. It survived because ct install is disabled in chart CI, so nothing had ever deployed the chart — lint cannot see a hook that fails at runtime.

migrate never reads the session key; it just has to pass validation.

The test that caught it

New helm-upgrade.yaml workflow. It installs the previous published release (chart pulled from ghcr, real image), seeds a document through that release's API, upgrades to the working tree, and asserts:

  1. /api/version changed → the binary actually swapped
  2. the session cookie minted by the old version still authorises → session format + signing key survived
  3. the seeded document still reads back → schema + migrations survived

Triggers on chart/migration/Dockerfile changes, weekly against main, and workflow_dispatch. Split from helm-ci.yaml because it builds an image and runs kind (~10 min) where ct lint takes ~10 s.

Verified locally

Ran end to end against a local kind cluster before pushing:

base /api/version -> 0.2.0
seeded doc 8f9e574d-dffb-4d19-a13b-7d8c3f44b42b
knot-migrate-zdzgw   0/1   Completed        <- the fixed hook
upgraded /api/version -> candidate-localtest
GET /api/docs/8f9e...b42b -> 200
title -> upgrade-canary
PASS: upgrade 0.2.0 -> candidate-localtest, session and data intact

Two things the local run forced

  • Images load via docker save --platform + kind load image-archive, not kind load docker-image. The released image is a multi-arch index and kind imports with --all-platforms, which fails on manifests that were never pulled (content digest ...: not found).
  • The base release installs with migrations.enabled=false and is migrated out-of-band, because the base chart carries the very bug being fixed. Marked for deletion once the resolved base is >= 0.2.1.

Follow-up

The published 0.2.0 chart is broken. This fix needs a 0.2.1 before the chart is usable; until then installs need --set migrations.enabled=false plus a manual /knot-server migrate.

🤖 Generated with Claude Code

The chart has never been installable. The pre-install/pre-upgrade migrate
Job injected KNOT_DATABASE_URL but not KNOT_SESSION_KEY, and the binary
loads and validates the entire config before dispatching the subcommand.
Validation rejects an empty session key, so the hook exited 2 with
"KNOT_SESSION_KEY is required" and every helm install/upgrade failed
before it ever reached the Deployment.

Present identically in the published 0.1.0 and 0.2.0 charts. It survived
because `ct install` is disabled in chart CI, so nothing had ever actually
deployed the chart -- lint alone cannot see a hook that fails at runtime.

The new helm-upgrade workflow is what caught it. It installs the previous
PUBLISHED release (chart pulled from ghcr, not `git archive` of the tag --
Chart.yaml carries 0.0.0 placeholders in-tree and release.yaml injects the
real version at package time), seeds a document through that release's own
API, upgrades to the working tree, and then asserts:

  1. /api/version changed          -> the binary actually swapped
  2. the old cookie still authorises -> session format + signing survived
  3. the seeded document reads back  -> schema + migrations survived

Verified end to end against a local kind cluster: 0.2.0 -> working tree,
with the pre-upgrade hook Job completing successfully once the session key
is wired in.

Two things the local run forced:

- Images are loaded via `docker save --platform` + `kind load image-archive`
  rather than `kind load docker-image`. The released image is a multi-arch
  index and kind imports with --all-platforms, which fails on manifests that
  were never pulled ("content digest ...: not found").

- The base release is installed with migrations.enabled=false and migrated
  out-of-band, because the base chart carries the very bug being fixed. That
  workaround is marked for deletion once the resolved base is >= 0.2.1.
Promotes the Unreleased section to [0.2.1] and teaches release.yaml to lift
that section out of CHANGELOG.md into the GitHub Release body. Until now the
release page carried only the artifact blurb plus GitHub's generated commit
list, so the actual "what changed and what do I have to do about it" lived
only in the repo.

Written into a single file because action-gh-release honours `body` OR
`body_path`, not both. A tag with no matching changelog section still
releases, but emits a warning and links to CHANGELOG.md.

0.2.1 is a chart-only release: no crates/, web/ or e2e/ files changed, so the
image is behaviourally identical to 0.2.0.
helm/kind-action v1.14.0 ships kind v0.31.0, and pointing it at
kindest/node:v1.35.5 fails the moment anything touches the node:

  ERROR: unknown containerd config version: 4 (supported versions: 2 and 3)

The CLI and the node image are a matched pair. Letting the action pick its
own default keeps them in step; the exact Kubernetes version is irrelevant
to what this test asserts.
Restoring a snapshot corrupted the document for every connected client.

ReplaceWithMarkdown clears the "default" fragment and applies the restored
content in ONE transaction, but it persisted and fanned out the caller's
`update_bytes`, which encodes only the insertion. Peers received the insert
without the delete, so they kept what they already had and appended the
restored text:

  expected: "First version of the doc."
  actual:   "Completely different content.First version of the doc"

The persisted update was missing the deletion too, so this was a data
integrity bug rather than a display glitch -- replaying the update log
reproduced the merged content.

Fixed by capturing what the transaction actually produced via
observe_update_v1, the same pattern PatchTaskChecked already used 40 lines
below, and persisting/broadcasting that.

The existing replace_with_markdown_swaps_content test could not catch this:
it asserts on the room's OWN document, which was always correct. The new
replace_broadcast_replaces_peer_content asserts on the frame a peer receives,
applied to a peer holding the pre-restore state. Reverting only the two
broadcast/persist lines turns it red with the exact production symptom while
the old test stays green.

This is the real cause of the flaky e2e history.spec failure. Earlier I
attributed that to React 19, then react-router, then yrs 0.27, and finally to
a prosemirror-view minor bump -- all wrong. main has been red since v0.2.0
was tagged; the one green run on PR #5 was luck, since whether the client
re-syncs full state or applies the incremental update is timing dependent.
history.spec picked snapButtons.last() and called it "the OLDEST snapshot
which should be V1". With KNOT_SNAPSHOT_EVERY_N=1 the writer snapshots after
every persisted batch, so typing V1 leaves a trail of PREFIX snapshots
("F", "First version of the", ...). The list is ORDER BY snapshot_seq DESC,
so the oldest entry is the complete V1 only when every keystroke happened to
land in a single batch -- true on a fast machine, false on a loaded runner.

That is the second half of this spec's flakiness. With the CRDT broadcast bug
fixed the restore stopped merging, and the failure changed shape:

  before: "Completely different content.First version of the doc"   (merged)
  after:  "First version of the"                                    (prefix)

The preview assertion could not catch it either: it only required "First
version", which any prefix satisfies, so the spec failed later at the restore
assertion and looked like a restore bug.

Now polls the NEWEST snapshot until its preview contains all of V1, pins that
one by seq, re-selects it after V2, and asserts the full string in both places.
@christianhuening
christianhuening merged commit f86f0e4 into main Aug 17, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant