Skip to content

Release 2.51.0: ecosystem boundaries, znserver cutover, pacman updater, Cloud vault retirement, Vim block edits - #796

Merged
adibhanna merged 17 commits into
mainfrom
v2.51.0
Sep 16, 2026
Merged

adibhanna merged 17 commits into
mainfrom
v2.51.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

Release branch for ZenNotes 2.51.0. Contains, in order: the ecosystem boundary refactor (#789), the server cutover to ZenNotes/znserver (#795), and the later work landed as separate commits on top:

Verified on this branch: typecheck; test:run (app-core 2371, desktop 849); terminal, web-dist lock and web artifact node tests; build (the built index.html has no inline script and the bundle registers sw.js). The packaged-app CDP launch check and the desktop and web smokes run next; the tag follows them.

Ordering: website PR ZenNotes/website#26 (Cloud creation-metadata allowlist) deploys before this reaches users.

Desktop, web, and mobile currently depend on private renderer internals and source checkouts, while server builds depend on the frontend build tree. This change introduces explicit public editor APIs, portable packages, and verified browser artifacts so each host can build against a deliberate dependency boundary.

### Review groups

1. **Contracts and domain:** move portable types into the bridge contract, preserve compatibility exports, and share exact-byte behavior fixtures.
2. **Editor APIs and lifecycle:** expose navigation, editor commands, attachments, notes, folders, databases, tasks, settings, and immutable host snapshots. Coordinate saves, file mutations, workspace transitions, and native relocation/rollback.
3. **Package distribution:** build immutable core/contract/domain archives, preserve singleton React/CodeMirror dependencies, and verify external consumers on Vite 6 and 8.
4. **Server and viewer artifacts:** support Go-only API builds and verified embedded web imports; restore a maintained read-only public viewer with safe Markdown fallback and restricted interactive plots.
5. **Extraction preparation:** provide Go repository, Docker, Nix, and draft-release templates for the later `ZenNotes/znserver` cutover. The current server source and distribution owner remain active until that cutover.

This branch starts from current `main` after v2.50.4, including its verified AUR/Homebrew/Nix metadata. The asset-reference fix is reconciled with the workspace reservation so pending edits and vault switches cannot interrupt a rename/move rewrite.
… CodeQL

The standalone package check installed the packed archives with npm's
offline mode, which only works when the npm cache already holds registry
metadata for every dependency range. npm ci caches tarballs, never that
metadata, so every fresh CI runner failed to resolve lz-string. The check
now prefers the cache but allows the metadata fetch.

On Windows the packers wrote include and exclude globs with backslashes,
and tsc treats those as patterns that match nothing. Generated tsconfig
paths now use forward slashes on every platform. Behind that failure waited
another: Windows checkouts convert the contract fixtures to CRLF, and the
TypeScript, Go server, and TUI consumers hash those bytes against recorded
provenance. The fixture directories now check out with LF everywhere.

CodeQL flagged eight items in the large diff. The wikilink resolvers trim
slashes with a linear scan instead of a backtracking regex, the browser
harness only dispatches CDP events to registered listeners, and store.ts
drops four unused imports and an unreferenced helper.

Validation: the shared-package check fails on an empty npm cache before and
passes after; the Windows glob is reproduced with path.win32; the CRLF hash
mismatch is reproduced arithmetically against the pinned digests; root
typecheck, every workspace suite with the turbo cache bypassed, the isolated
package check, and the Chrome harness pass with these changes.
…ture

The regression test added with the CodeQL cleanup built its own two-note
array without the title and folder fields that NoteRef requires, so the
app-core typecheck failed on every platform once the earlier steps passed.
Vitest ran the test without complaint because it does not typecheck. Reuse
the module-level fixture, which already holds both notes the test needs.
The installed-editor check ran on a GitHub runner for the first time and
stopped at "editor: timed out" with nothing else in the log. The harness
already collected page errors, failed requests, helper output, screenshots
and DOM dumps, but wrote them into a throwaway temporary directory and never
printed them, so the failure was unreadable from CI.

On failure the harness now prints the collected errors, failed requests,
the tail of the server, preview and Chrome output, and a compact page state.
A small collector copies the screenshots and JSON dumps out of the consumer
directory so the Ubuntu job can upload them as an artifact whether the step
passes or fails.

The first note open pulls the editor, store and Markdown chunks on a cold
runner, so it now gets the same sixty seconds the lazy Mermaid and Typst
renders already had. A real rendering failure still fails, with reasons.

Validation: the harness still passes locally against real Chrome; the
collector gathers fifteen files from a previous local run.
…nd match Windows paths in the rollback test

The Ubuntu runner reached the sidebar before the workspace finished
restoring, so the harness's first openNote landed during the transition and
the public navigation guard dropped it by design: no error, no editor, and
sixty seconds later the page was still Home with the note index loaded. The
harness now waits for the shell's workspaceRestored signal, the same
readiness the later host-command section already required, before opening
its first note. The evidence upload and page dump from the previous run are
what made this visible.

The Empty Trash rollback test refuses the comment-tree move by matching a
rename source that ends in .zennotes/comments/trash. Windows joins that path
with backslashes, so the refusal never fired, the transaction succeeded, and
the assertion that it rejects failed on that runner alone. Compare the
normalized path so the refusal fires on every platform.

Validation: the browser harness passes locally against real Chrome and in a
Debian container with Chromium; the vault suite passes locally; macOS and
Linux arm64 already passed the full build on the previous run.
… the delayed cache write before tests remove a vault

The previous run showed the same restoration race at the harness's next
navigation: after loading the host page it opened a note while the
workspace was still restoring, and the public navigation guard dropped the
call. Every scripted navigation now goes through one readiness helper that
waits for the shell's workspaceRestored signal before it opens a note.

On Windows the vault package failed in cleanup rather than in a test:
ListNotes schedules a note-meta cache snapshot one second later, and the
slow runner reached t.TempDir removal while that goroutine was creating
files, so Windows refused to delete the directory. The same race exists on
main; it only surfaced here because the runner happened to line up with the
delay. The vault now counts its delayed writers and exposes Close to drain
them, and the package's tests keep the writer off through TestMain so no
test can race its own cleanup. A test that opts back in drains with Close.

Validation: the browser harness passes locally against real Chrome; go vet
and the full server suite pass in the branch worktree.
The self-hosted Go server now lives in its own repository,
https://github.com/ZenNotes/znserver. Its history was extracted from
apps/server with the full commit trail, its first release (v2.50.4) is
published with checksummed binaries, and the adibhanna/zennotes Docker
image is built and pushed by that repository's publisher (2.50.5 was the
first image from the new channel). This commit removes the copy that
stayed here and points everything that still built the server from
source at the published release instead.

What goes: apps/server, the root Dockerfile and .dockerignore, the
docker-publish workflow, the server Nix package and its vendorHash, the
server-repository templates, the extraction rehearsal script, and the
scripts that built or staged the server (build-go-server,
prepare-server-web-dist, run-go-server-test). CI no longer installs Go,
runs the server job, or checks the Go fixture copies, because those
copies are checked in the server's own CI.

What replaces it: tooling/server-release.json pins the znserver release
(tag plus per-platform SHA-256 from its SHA256SUMS), and
tooling/scripts/server-binary.mjs resolves a server binary in a fixed
order: an explicit ZENNOTES_SERVER_BINARY, a ZENNOTES_SERVER_DIR checkout
built with Go, or the pinned release downloaded into dist/server-binaries
and verified against the pinned digest before it is ever executed. A
download whose checksum does not match is refused rather than run.
dev:server, dev:web-stack, and the app-core browser harness use that
resolver, so browser work needs neither Go nor a server checkout.

The perf harness deliberately does not fall back to the pinned release:
a perf run measures the local web bundle, and the release embeds the
pinned artifact instead. It takes a znserver checkout (syncing the local
web dist in, then building with -tags=embed_web) or an explicit binary,
and fails with the two options spelled out otherwise. sync-web-dist and
the contract-fixture sync take the checkout as their destination for the
same reason; the web-dist lock now lives next to apps/web/dist, the tree
it guards.

Docker Compose runs the published image (adibhanna/zennotes:latest by
default, IMAGE=... to pin) instead of building one; make rebuild pulls,
make nuke removes the pulled image. README, guide, CONTRIBUTING, the
packaging docs, the architecture notes, and the synced-copy comments in
the TypeScript sources point at the new repository. The extraction
rehearsal and cutover documents keep their content with a status banner
recording that the move is done.

Not changed: the server's own release process (bump release.json in
znserver, run its release workflow, then its Docker publisher), the web
artifact pipeline that znserver consumes, and the moving of the Docker
`latest` tag, which happens with the next server release.
The web artifact workflow still ran check:contract-fixtures. That script
now compares the shared fixtures against a ZenNotes/znserver checkout, and
the runner has none, so the step failed before the artifact was built.
The Go copies are checked by the server's own CI against the same bytes,
and the TypeScript consumers keep verifying the fixtures through test:run,
so the step goes rather than gaining a checkout it does not need.
The desktop's `zen` command has been a Node script bundled with the app.
The Go CLI in ZenNotes/tui is the one that keeps moving, so the desktop
now carries the machinery to ship that binary instead: terminal-release.json
pins a release by tag and per-platform checksum, after-pack.js stages the
verified binary through tooling/scripts/terminal-artifact.mjs, build/zen
launches it, and cli-install and terminal-runtime install and repair the
shortcut around it. A release of `null` stages nothing and keeps the Node
fallback, and that is how this ships: the first pin follows the Go CLI
v0.2.0 release, so nothing changes for users in this version.

Notes gain app-owned creation metadata so the desktop, MCP and Go CLI agree
on creation dates instead of trusting filesystem birth times. The desktop
and MCP move, rename and delete the metadata with its note, refuse a
sidecar or metadata directory that links outside the vault, and fail a save
before touching Markdown when the metadata is malformed. Cloud sync carries
the sidecars as vault content, which is why the Laravel allowlist for the
metadata suffix must be deployed before this reaches users.

Settings gets the CLI section, the bridge and IPC carry the install request
and status, help documents the command, CI runs the terminal tooling tests,
and docs/specs/desktop-cli-tui-migration.md records the migration.

Fixes: none by number; groundwork for the desktop-to-Go CLI migration.
On Arch the in-app update ran pacman through pkexec, and pkexec can fall
back to a textual agent on the controlling terminal. An app started from a
window-manager binding has no terminal to show it on, so the install never
finished and the Relaunch button did nothing (#790).

The updater now has an explicit installing phase, requires a graphical
polkit agent instead of the internal fallback, and reads the exit code
honestly: 126 means the user dismissed the authorization dialog, 127 means
no agent could run at all. Each case gets its own message and a recovery
path from Details > About. A manual check, download or install owns its
result once the user starts it, so a background check cannot overwrite it.

Packaging: the launcher is the lowercase build/zennotes, fpm declares the
provides and conflicts, and the AUR package conflicts with the old
ZenNotes name so the two cannot coexist.

Fixes #790.
A Cloud vault deleted on the server stayed "up to date" in the client, and
sync kept reporting success against something that no longer existed
(#791). The client had no notion of a vault that is gone as opposed to a
request that merely failed.

cloud-vault-availability in shared-domain classifies the server's answers.
Only the authenticated vault-level confirmation retires the remote
association; a missing item, revision or upload is a sync error, never
proof of deletion. A retired vault shows as such, auto-sync stops, and the
Cloud settings panel offers unlink and discovers the retirement even while
it stays mounted. The desktop host service and filesystem layer carry the
same states, with tests on every layer.

Fixes #791.
Visual-block edits changed only the first row of the rectangle (#792).
CodeMirror-Vim keeps the block's full rectangle in vim.sel and issues one
selection per row, but CM6 silently keeps only the primary selection unless
the multiple-selections facet is enabled. The Vim extension now enables it,
and the block highlighter paints one inclusive range per logical row so
short rows still show the whole rectangle.

A jsdom suite drives the real Vim adapter through block insert, append and
change, applying ordinary CM6 text transactions since jsdom cannot type into
contenteditable.

Fixes #792.
The handoff document for the ecosystem boundary work, with the review-scope
pointers, the runtime validation summary, the znserver extraction and the
monorepo cutover, plus one sentence in the ecosystem plan pointing at it.
The self-hosted server sends `script-src 'self'` in its Content Security
Policy, and the service-worker registration lived in an inline script in
apps/web/index.html. Browsers refused that script silently, so production
deployments never registered sw.js and the offline shell never installed;
only the Vite dev server, which sends no policy, ever ran it.

The registration moves into the bundle as register-service-worker.ts,
called from the web entry. Behavior is otherwise unchanged: it still skips
file: origins, still waits for load, and still uses the relative sw.js URL
so a prefixed deployment registers for its own scope.
Comment thread apps/desktop/src/main/terminal-runtime.ts Fixed
Comment thread apps/desktop/src/main/terminal-runtime.ts Fixed
CodeQL flagged two check-then-use sequences in the terminal runtime: the
launcher was lstat'ed and then read by path, and the installed binary was
stat'ed for its mode and then read by path for its digest. Between the two
calls another process could swap the file, so the property that was checked
was not the property of the bytes that were used.

Both now go through a single open handle. The launcher opens with
O_NOFOLLOW, so a symlink is refused at open time (ELOOP) instead of being
detected and then followed, and the type check and the marker check read
from that same descriptor. The binary's mode and its hashed bytes come from
one handle too. Behavior for the cases the tests cover is unchanged: no
launcher is fine, a foreign file is refused, a verified install is retained.
The Windows build job failed on two suites that arrived with the later
work. The stale-shortcut repair tests in cli-install.test.ts exercise
AppImage mount paths and macOS bundle links for a feature the app does not
offer on Windows, where the test's POSIX path is rewritten to a drive path
and CLI install refuses outright. The .pacman install tests fake a Linux
platform but drive the updater through POSIX shell doubles for the download
and pkexec, which cannot run on the Windows runner, so every case stopped at
"available" instead of "downloaded".

Both suites now skip on win32 at the describe level, matching the per-test
skips the CLI file already uses. They still run on macOS and Linux, which is
where the behavior exists.
@adibhanna
adibhanna merged commit 8ff2cb8 into main Sep 16, 2026
17 checks passed
@adibhanna
adibhanna deleted the v2.51.0 branch September 16, 2026 20:40
@adibhanna
adibhanna restored the v2.51.0 branch September 16, 2026 20:41
@adibhanna
adibhanna deployed to boundary-artifacts September 16, 2026 20:47 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts September 16, 2026 20:48 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts September 16, 2026 20:51 — with GitHub Actions Active
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.

2 participants