CD phase 4 — plugin version skew #14

Closed
opened 2026-09-15 08:59:23 +01:00 by cruelacid · 1 comment
Owner

Plugin sends its version on the socket URL and an X-Nectenda-Plugin header (CORS allow-lists updated); shard records versions, exposes nectenda_plugin_connections{version}, enforces MIN_PLUGIN_VERSION with close code 4007; plugin shows update-required; CI compat step runs the previous public plugin release against the new server; compatibility rule in docs/releasing.md. Checkpoint: an old plugin gets 4007 and the notice; compat step green.

Plugin sends its version on the socket URL and an X-Nectenda-Plugin header (CORS allow-lists updated); shard records versions, exposes nectenda_plugin_connections{version}, enforces MIN_PLUGIN_VERSION with close code 4007; plugin shows update-required; CI compat step runs the previous public plugin release against the new server; compatibility rule in docs/releasing.md. Checkpoint: an old plugin gets 4007 and the notice; compat step green.
Author
Owner

Done and deployed. Commit e1fd5fa, which travelled through the phase 3 gate and reached both hosts about 30 seconds after promotion.

The wire. The plugin names its build on everything it sends: X-Nectenda-Plugin on every request to a Nectenda service, and &v= on the WebSocket handshake, where a header cannot be set. The version is substituted into the bundle from manifest.json at build time, so the number is written down once and the bundle carries a literal. Attachment downloads are deliberately excluded: those are presigned bucket URLs with no headers at all, and any extra header turns them into a preflight the bucket refuses.

Both CORS allow-lists name the header, and both now have a test that fails when it is removed. A header missing from that list fails the preflight, the request never leaves the browser, and nothing appears in any log on either side. The plan noted this had bitten twice; it is now the kind of thing that cannot bite silently again.

The floor. The shard refuses anything below MIN_PLUGIN_VERSION after the upgrade with close code 4007, and counts live connections per version as nectenda_plugin_connections{version}. Confirmed live on eu1: the metric is exported, and the floor in effect is 0.

It stays 0. A self-hosted server has no business refusing its owner's client, and there is no public release to raise it to yet. When there is, the metric is what makes raising it a decision about a number rather than a guess about whose sync stops.

The plugin's answer to 4007 is update the plugin to keep syncing, said once per episode, with a status bar label of its own and slow retries — an update should resume sync without Obsidian being restarted. Editing continues locally throughout.

Compatibility is checked, not asserted. The gate downloads the previous public release and runs the sign-in and two-vault suites with it against the new server. Every other suite drives the plugin built from the same commit, so a change that breaks yesterday's plugin passes all of them. With no public release yet the step prints that there is nothing to fetch and passes visibly, so a green tick never quietly means the check did not run. I exercised the override it depends on against a real external plugin directory rather than only writing it.

On the checkpoint. "An old plugin gets 4007 and the notice" is proved against a real server through the test harness — real sockets, real upgrade, real close code — and by four mutation checks: the server ignoring the floor, the plugin dropping the 4007 branch, the plugin not naming its version, and the header removed from either allow-list. I did not raise the floor on production to demonstrate it there, because the only clients that exist are your own vaults and cutting them off would buy nothing before a public release.

One thing the tooling caught. My first version put a reference to a private document in a comment in packages/shared, which is mirrored publicly. build-mirror.mjs refused to stage it. Worth knowing that check earns its place.

922 unit tests green, full multi-vault suite 57 passed.

That closes the continuous-deployment plan: phases 1 to 4 all done.

Done and deployed. Commit e1fd5fa, which travelled through the phase 3 gate and reached both hosts about 30 seconds after promotion. **The wire.** The plugin names its build on everything it sends: `X-Nectenda-Plugin` on every request to a Nectenda service, and `&v=` on the WebSocket handshake, where a header cannot be set. The version is substituted into the bundle from `manifest.json` at build time, so the number is written down once and the bundle carries a literal. Attachment downloads are deliberately excluded: those are presigned bucket URLs with no headers at all, and any extra header turns them into a preflight the bucket refuses. **Both CORS allow-lists** name the header, and both now have a test that fails when it is removed. A header missing from that list fails the preflight, the request never leaves the browser, and nothing appears in any log on either side. The plan noted this had bitten twice; it is now the kind of thing that cannot bite silently again. **The floor.** The shard refuses anything below `MIN_PLUGIN_VERSION` after the upgrade with close code 4007, and counts live connections per version as `nectenda_plugin_connections{version}`. Confirmed live on eu1: the metric is exported, and the floor in effect is `0`. It stays `0`. A self-hosted server has no business refusing its owner's client, and there is no public release to raise it to yet. When there is, the metric is what makes raising it a decision about a number rather than a guess about whose sync stops. **The plugin's answer** to 4007 is *update the plugin to keep syncing*, said once per episode, with a status bar label of its own and slow retries — an update should resume sync without Obsidian being restarted. Editing continues locally throughout. **Compatibility is checked, not asserted.** The gate downloads the previous public release and runs the sign-in and two-vault suites with it against the new server. Every other suite drives the plugin built from the same commit, so a change that breaks yesterday's plugin passes all of them. With no public release yet the step prints that there is nothing to fetch and passes visibly, so a green tick never quietly means the check did not run. I exercised the override it depends on against a real external plugin directory rather than only writing it. **On the checkpoint.** "An old plugin gets 4007 and the notice" is proved against a real server through the test harness — real sockets, real upgrade, real close code — and by four mutation checks: the server ignoring the floor, the plugin dropping the 4007 branch, the plugin not naming its version, and the header removed from either allow-list. I did not raise the floor on production to demonstrate it there, because the only clients that exist are your own vaults and cutting them off would buy nothing before a public release. **One thing the tooling caught.** My first version put a reference to a private document in a comment in `packages/shared`, which is mirrored publicly. `build-mirror.mjs` refused to stage it. Worth knowing that check earns its place. 922 unit tests green, full multi-vault suite 57 passed. That closes the continuous-deployment plan: phases 1 to 4 all done.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Nectenda/nectenda#14
No description provided.