Versioning & compatibility

Zuke publishes 57 independent JSR packages from one workspace. They don't all move at the same speed, and knowing which promise each package makes — and how they interlock — is what keeps an upgrade from surprising you at runtime instead of at deno check time.

One tier: every package follows full semver

All 57 packages are 1.x. @zuke/core, the @zuke/cli command, and every tool wrapper make the same promise: a 1.x release never breaks a public symbol, so a minor or patch upgrade is safe to take without reading the diff, and a breaking change bumps the major version. Depend on jsr:@zuke/core@^1 (and jsr:@zuke/deno@^1, …) and let minors resolve.

release-please's bump-minor-pre-major is off, so nothing silently ships a breaking change under a minor bump. Read a package's CHANGELOG.md when its major moves — that is the only release that can require a code change on your side.

What a wrapper's 1.x promise does not cover is the upstream CLI it drives: its flags track a tool that can rename or drop one. When upstream changes, the wrapper keeps the old method working (deprecated) or bumps its major — the promise is about the wrapper's own typed surface, not about upstream's stability.

Every wrapper declares a @zuke/core floor

Each tool wrapper's deno.json pins a minimum core version it needs:

{
  "imports": {
    "@zuke/core": "jsr:@zuke/core@^1.31.0"
  }
}

That floor is hand-maintained — nothing regenerates it — but it is not unverified: the coreFloorCheck target type-checks every package against the core version it declares, and a dedicated Core version floors CI job runs it on every PR.

The floor exists because a wrapper often imports a symbol that only exists from some specific core release onward — a new settings class, a new exported type. If the floor is under-declared, a consumer can pin a wrapper version whose code needs core 1.31 while their lockfile still resolves an older core that doesn't export the symbol. The failure doesn't show up until the wrapper is actually imported at runtime, because the repository's own CI type-checks every package against the workspace-local packages/core, not against the floor each deno.json claims.

The failure mode, concretely

This happened in the Zuke repository, and is what the check now prevents:

  1. Five wrappers (@zuke/claude, @zuke/codex, @zuke/gcloud, @zuke/gemini, @zuke/gh) started importing SubcommandSettings, a type new in @zuke/core@1.31.0.
  2. Their declared floor was still an older range that predates that symbol.
  3. CI was green: deno check resolves the workspace's local packages/core member regardless of what each wrapper's deno.json claims, so the type-check never saw the gap.
  4. A consumer whose deno.lock had already pinned an older, published core — satisfying the wrapper's stale floor — hit a runtime failure: the import resolved to a real core release that doesn't export SubcommandSettings.
  5. The fix was a follow-up PR raising the floor in the five affected deno.jsons (and deno.lock) to ^1.31.0, once core 1.31.0 had actually published — it cannot land in the same change that introduces the new symbol, because the version it needs to name does not exist yet.

How to diagnose it. If an import resolves at type-check time but throws or logs a missing export at runtime: find which core symbol the failing import needs and which version introduced it, then compare that to the floor the wrapper declares and to what your own deno.lock actually resolved for @zuke/core.

# What did the lockfile actually resolve for core?
deno info jsr:@zuke/gh

# Which core version introduced the symbol the import needs?
#   grep it in packages/core/CHANGELOG.md, or search llms-full.txt

If the lockfile-resolved core predates the version the symbol needs, the wrapper's floor is under-declared — file an issue, or bump your own pin to the version that introduced the symbol as a workaround.

Verifying the floors

./zuke coreFloorCheck

It works by writing a throwaway config per package containing that package's own imports and no workspace field, then type-checking the package's mod.ts with deno check --config pointed at it. Without a workspace to resolve against, @zuke/core comes from JSR instead of the local member — precisely the substitution that made the ordinary type-check blind to this class of bug.

Two details are load-bearing:

  • The floor is pinned to the range's exact minimum. A caret range resolves to the newest matching version, so checking ^1.25.0 as written would exercise the current core and pass no matter how new a symbol the package uses. Verified against 1.25.0 exactly, it tests the actual claim.
  • minimumDependencyAge is zeroed in the generated config. Deno otherwise refuses a version published within the last day as a supply-chain precaution — sensible when installing dependencies to run, wrong here, where the case most needing verification is a floor naming the core just released alongside it.

Because it reaches JSR, coreFloorCheck is deliberately not part of ./zuke ci, which stays runnable offline.

Pinning guidance

  • Depend on the caret range, not an exact version: jsr:@zuke/core@^1 (or a wrapper's own jsr:@zuke/<tool>@^1) so patch and non-breaking minor releases resolve automatically.
  • Always commit deno.lock. It pins the exact resolved version of every package (core included) that your build actually runs against — the caret range in deno.json only bounds what's allowed, the lockfile fixes what's used. Regenerate it whenever you deliberately take an upgrade, and review the diff.
// Take patches and non-breaking minors automatically:
import { Build, run, target } from "jsr:@zuke/core@^1";
import { DenoTasks } from "jsr:@zuke/deno@^1";

Zuke's own gate enforces this on itself: every entrypoint that loads zuke.ts runs --frozen, and a lockCheck target fails if a run modified the lock — so a stale lock is caught before CI rather than silently healed on whichever machine happened to run the gate.

Upgrade notes

For what changed in a given release:

SourceWhat it has
The release model how a release is cut, and the project-level milestones
Root CHANGELOG.md project-level milestones, in the repository
Per-package CHANGELOG.md generated per-release notes — release-please output from Conventional Commits, so the authoritative per-package history
JSR — @zuke the live version of every published package