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:
-
Five wrappers (
@zuke/claude,@zuke/codex,@zuke/gcloud,@zuke/gemini,@zuke/gh) started importingSubcommandSettings, a type new in@zuke/core@1.31.0. - Their declared floor was still an older range that predates that symbol.
-
CI was green:
deno checkresolves the workspace's localpackages/coremember regardless of what each wrapper'sdeno.jsonclaims, so the type-check never saw the gap. -
A consumer whose
deno.lockhad 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 exportSubcommandSettings. -
The fix was a follow-up PR raising the floor in the five affected
deno.jsons (anddeno.lock) to^1.31.0, once core1.31.0had 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.0as written would exercise the current core and pass no matter how new a symbol the package uses. Verified against1.25.0exactly, it tests the actual claim. -
minimumDependencyAgeis 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 ownjsr:@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 indeno.jsononly 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:
| Source | What 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 |