Skip to content

feat: fledgling jsr — claim packages on JSR + link repo for OIDC publishing#7

Open
theoephraim wants to merge 5 commits into
mainfrom
feat/jsr-support
Open

feat: fledgling jsr — claim packages on JSR + link repo for OIDC publishing#7
theoephraim wants to merge 5 commits into
mainfrom
feat/jsr-support

Conversation

@theoephraim

@theoephraim theoephraim commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds a new fledgling jsr [packages…] subcommand — the same first-publish story fledgling tells for npm, on JSR. JSR has no create-on-first-publish, so every package must exist on jsr.io before CI (or anyone) can publish a version. For each target package this:

  1. Scaffolds a minimal jsr.json (name, version, source exports entry) from package.json where missing — an existing jsr.json/deno.json is authoritative and never rewritten
  2. Claims the package via the JSR management API (POST /scopes/{scope}/packages)
  3. Links the GitHub repo (PATCH githubRepository) — JSR's entire trusted-publishing setup: any workflow in the linked repo can then publish token-lessly via OIDC (npx jsr publish with permissions: id-token: write, no JSR_TOKEN secret in CI)
  4. Syncs score metadata — reconciles each package's description (from package.json) and runtime compatibility (from config) to jsr.io. JSR scores packages partly on these, and they live only on jsr.io (the jsr.json manifest has no description field), so they can't ride along at publish time — the local tool that holds the token is the right place to reconcile them. Drift-aware: only changed fields are pushed.

Idempotent like the npm flow: claimed packages are skipped, the repo link is re-asserted, and metadata drift is reconciled on every run.

Design notes

  • Separate command, not a registry abstraction. JSR needs none of the npm machinery (no npm CLI, no 2FA/OTP/session warming, no provider/workflow/environment trust config — auth is a bearer token, trust is one repo link, GitHub-only). Forcing it through processTarget's npm-shaped settings would make everything conditional, so src/jsr.ts (API client + helpers, the JSR analogue of npm.ts) and src/jsr-cmd.ts (clack flow mirroring sync.ts) stand alone.
  • Name mapping precedence: manifest name → --scope / fledgling.jsr.scope config → the npm name's own scope.
  • Metadata is config-driven, not inferred. Description comes automatically from package.json (collapsed to one line, clamped to JSR's 250-char limit). runtimeCompat is not guessed from source — it's set explicitly via fledgling.jsr.runtimeCompat (root default) or a package's own package.json (per-package override). Each metadata field is a separate PATCH because JSR's updatePackage body is a oneOf. Opt out entirely with --skip-metadata.
  • Operational hardening ported from a live 30-package run (see credit) and re-verified against JSR's docs/OpenAPI:
    • management API 429s bulk claims after only a few calls — every request retries with Retry-After-aware exponential backoff, plus a 1s per-package cooldown
    • hard cap of 20 new packages per scope per rolling week — on weeklyPackageLimitExceeded the run stops cleanly, lists what's left, and says to re-run after the reset (re-running picks up where it left off)
    • GET /user/member/{scope} preflight separates "token can't manage this scope" from per-package failures
    • a publish-only token 403s on create/link (missingPermission) — surfaced with a specific "needs a FULL-access token" message
    • sends a User-Agent identifying fledgling — JSR asks management-API clients to, and reserves the right to block those that don't
  • UX matches the existing commands: dry-run by default, --yes to apply, interactive confirm in a TTY, --dry-run for CI. No token / invalid token degrades to a dry run with guidance.

Verified against JSR's docs + OpenAPI

Confirmed against api.jsr.io/.well-known/openapi, the jsr.json schema, and the scoring/quotas docs:

  • jsr.json manifest fields are name, version, license, exports, publishno description (so metadata must go through the API)
  • PackageScore includes hasDescription, atLeastOneRuntimeCompatible, multipleRuntimesCompatible, hasProvenance, README/docs factors
  • UpdatePackageRequest is a oneOf — one field per PATCH
  • weekly limit is exactly 20/scope/7-day rolling window; OIDC is GitHub-only; management tokens are package/publish (publish only) vs all (management)

Testing

  • Typecheck + build clean; --help, completions, and config types wired
  • Live dry runs against api.jsr.io: unscoped-name error path, --scope mapping, plan output, no-token/invalid-token degradation, already-claimed path, metadata drift detection (reads live description + runtimeCompat, diffs, plans only the changes), 250-char truncation warning, and --skip-metadata opt-out
  • Not yet exercised: a real apply (needs a full-access JSR_TOKEN). The claim/link/metadata/quota logic is ported faithfully from the reference script that ran against 30 live packages, and the endpoints/fields are re-verified against the OpenAPI spec. Note the management API is undocumented-as-stable — worth flagging in release notes.

Credit

Huge thanks to @Saeris for the groundwork this is built on: the proposal and reference implementation (proposals/fledgling-jsr-support.md + scripts/jsr-claim.mjs + scripts/jsr-meta.mjs), including the live findings on rate limits, the weekly quota, token permission requirements, and the score-metadata approach.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • fledgling 1.1.1 → 1.2.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@Saeris

Saeris commented Jul 6, 2026

Copy link
Copy Markdown

I forgot I had Claude draft a proposal for a PR 😅

Something unique about JSR compared to NPM is that it scores your package based on the presence of certain metadata, too, which is why there is a jsr-meta script as well. Maybe worth considering here for setup completeness? Or perhaps that is better suited for package publishing in Bumpy instead.

…to jsr.io

JSR scores packages partly on having a description and marked runtime
compatibility, and both live only on jsr.io — the jsr.json manifest has no
description field, so they can't be set at publish time. Reconcile them in
`fledgling jsr` (which already holds the full-access token): description from
package.json, runtimeCompat from config (no inference). Drift-aware and
idempotent — only changed fields are PATCHed, each as its own request since
JSR's updatePackage body is a oneOf. Opt out with --skip-metadata.
cli.ts previously held the npm-shaped arg schema, the default/add command
logic (runPlain/createRun), and every gunshi command object inline — the odd
one out, since sync/init/jsr already kept their behavior in their own files.

Extract the shared flag schema + gunshi plumbing (Ctx, selectorsOf) into
args.ts, move the default/add command (which share createRun) into add.ts,
and have sync.ts/init.ts/jsr-cmd.ts each export their own full command object
(name, description, args, run) next to the logic it calls. cli.ts is now
just imports + gunshi wiring.
Consistent naming across commands: add.command.ts, sync.command.ts,
init.command.ts, jsr.command.ts, all under src/commands/. Only import
paths changed (adjusted for the extra directory level) — no behavior
change.
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