Skip to content

feat(plugin): add stability system (beta/stable) with promote command#264

Merged
MarioCadenas merged 18 commits intomainfrom
mario/plugin-stability-tiers
Apr 30, 2026
Merged

feat(plugin): add stability system (beta/stable) with promote command#264
MarioCadenas merged 18 commits intomainfrom
mario/plugin-stability-tiers

Conversation

@MarioCadenas
Copy link
Copy Markdown
Collaborator

@MarioCadenas MarioCadenas commented Apr 10, 2026

Adds a stability field to plugin manifests with import-path enforcement and full CLI tooling. Two tiers (beta ──→ stable); promotion is one-way. Importing from @databricks/appkit/beta is explicit consent to breaking-change risk; @databricks/appkit follows semver strictly.

Highlights

  • Schemasstability enum on plugin-manifest.schema.json and template-plugins.schema.json; template manifest bumped to 1.1.
  • Subpath exports@databricks/appkit/beta and @databricks/appkit-ui/{js,react}/beta wired through package.json and tsdown.config.ts.
  • Manifest as single source of truthtools/generate-plugin-entries.ts reads each plugin's manifest.json and writes committed stable-exports.generated.ts / beta-exports.generated.ts barrels. src/index.ts and src/beta.ts delegate via export *. CI fails if any of the four files (the two barrels, the synced template/appkit.plugins.json, and the existing plugin-manifest.generated.ts) drift from the manifests.
  • plugin sync / list / create — propagate stability, strip requiredByTemplate for non-stable plugins, surface a STABILITY column, prompt for tier on create.
  • plugin promote (new) — flips manifest, rewrites import paths in user .ts/.tsx, regenerates barrels (monorepo only), runs sync. Hardened: schema-regex name validation, isWithinDirectory bounds, symlink skip, --allow-installed for node_modules, --dry-run, --skip-imports, --skip-sync.
  • Init templatetemplate/server/server.ts branches on $p.Stability and emits a single consolidated from '@databricks/appkit/beta' import for beta plugins. Pure text/template, no sprig.
  • Docs — new Plugin Stability Tiers page.

Behavior contract

  • Absent stability"stable". Unknown values rejected up front.
  • Demotion rejected with a clear error.
  • Only stable plugins can be requiredByTemplate.
  • Failed post-promote regen or sync exits non-zero — no silent drift.

Companion CLI

databricks/cli#5090 exposes pluginVar.Stability to the init template and labels beta plugins in the picker. End-to-end verified against real local tarballs.

Test plan

  • pnpm build && pnpm -r typecheck && pnpm check:fix && pnpm test && pnpm docs:build — all clean
  • 38 promote integration tests cover invalid names, path traversal, unknown tiers, demotion, dry-run, import rewriting, symlink skipping, and node_modules protection.
  • CI freshness checks fail the PR if the auto-generated barrels or the synced template/appkit.plugins.json drift from the source manifests.

Demo

Demo shows analytics and serving as beta (just as an example) to test the functionality

demo-beta-system.mp4
image

@MarioCadenas MarioCadenas force-pushed the mario/plugin-stability-tiers branch from 54bde93 to 591d59b Compare April 27, 2026 09:22
@MarioCadenas MarioCadenas changed the title feat: add three-tier plugin stability system (experimental/preview/st… feat(plugin): add stability system (beta/stable) with promote command Apr 27, 2026
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 27, 2026
Mirrors the AppKit PR which dropped `experimental` and renamed
`preview` to `beta` (databricks/appkit#264). Forward-compat fallback
for unknown tiers is preserved.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 28, 2026
The AppKit template now branches on `{{$p.Stability}}` to route beta
plugins through the `/beta` subpath export. Make pluginVar carry the
field so the template can read it.

See databricks/appkit#264 commit d826a532.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 28, 2026
…tern

The AppKit init template was tightened to emit a single combined beta
import line via a string-accumulator pre-pass (databricks/appkit#264
commit 488797fc):

    {{- $betaImports := "" -}}
    {{- range $name, $p := .plugins -}}
      {{- if eq $p.Stability "beta" -}}
        {{- if eq $betaImports "" -}}
          {{- $betaImports = $name -}}
        {{- else -}}
          {{- $betaImports = printf "%s, %s" $betaImports $name -}}
        {{- end -}}
      {{- end -}}
    {{- end -}}

That pattern depends on three text/template features:
- Variable reassignment (`$x = ...`) inside `range`, against an
  outer-scope variable.
- The `printf` builtin.
- Pointer-field access (`$p.Stability`) on map values.

If a future refactor of executeTemplate (or a Go template engine
swap) breaks any of these, users would silently get malformed
server.ts files at `databricks apps init` time. This regression test
exercises the exact pattern with hermetic input — no AppKit checkout
required — and asserts the expected import shapes for all-stable,
single beta, multi beta, all-beta, and unknown-tier (alpha) cases.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas force-pushed the mario/plugin-stability-tiers branch from 0973606 to 9d7cd7d Compare April 28, 2026 15:03
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 28, 2026
Round-trip the new optional `stability` field on AppKit plugin manifests
(experimental / preview / stable, schema 1.1, see databricks/appkit#264)
and surface non-stable tiers as a colored parenthetical suffix in the
`databricks apps init` plugin picker. Stability is stored as a plain
string so unknown future tiers round-trip and render in gray instead of
breaking the loader.
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 28, 2026
Mirrors the AppKit PR which dropped `experimental` and renamed
`preview` to `beta` (databricks/appkit#264). Forward-compat fallback
for unknown tiers is preserved.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 28, 2026
The AppKit template now branches on `{{$p.Stability}}` to route beta
plugins through the `/beta` subpath export. Make pluginVar carry the
field so the template can read it.

See databricks/appkit#264 commit d826a532.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 28, 2026
…tern

The AppKit init template was tightened to emit a single combined beta
import line via a string-accumulator pre-pass (databricks/appkit#264
commit 488797fc):

    {{- $betaImports := "" -}}
    {{- range $name, $p := .plugins -}}
      {{- if eq $p.Stability "beta" -}}
        {{- if eq $betaImports "" -}}
          {{- $betaImports = $name -}}
        {{- else -}}
          {{- $betaImports = printf "%s, %s" $betaImports $name -}}
        {{- end -}}
      {{- end -}}
    {{- end -}}

That pattern depends on three text/template features:
- Variable reassignment (`$x = ...`) inside `range`, against an
  outer-scope variable.
- The `printf` builtin.
- Pointer-field access (`$p.Stability`) on map values.

If a future refactor of executeTemplate (or a Go template engine
swap) breaks any of these, users would silently get malformed
server.ts files at `databricks apps init` time. This regression test
exercises the exact pattern with hermetic input — no AppKit checkout
required — and asserts the expected import shapes for all-stable,
single beta, multi beta, all-beta, and unknown-tier (alpha) cases.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas force-pushed the mario/plugin-stability-tiers branch from 9d7cd7d to 04b791a Compare April 28, 2026 19:14
@MarioCadenas MarioCadenas marked this pull request as ready for review April 28, 2026 19:14
atilafassina
atilafassina previously approved these changes Apr 29, 2026
Comment thread packages/shared/src/cli/commands/plugin/sync/sync.ts Outdated
Comment thread packages/shared/src/cli/commands/plugin/manifest-types.ts Outdated
Comment thread packages/shared/src/cli/commands/plugin/promote/promote.ts
@atilafassina atilafassina self-requested a review April 29, 2026 12:18
@atilafassina atilafassina dismissed their stale review April 29, 2026 12:19

need to double-check the import rewrites. It may not be plugin-scoped and rewriting more imports than it should from /beta to /

MarioCadenas added a commit that referenced this pull request Apr 29, 2026
Address review feedback on PR #264.

## Comment: rewriteImportsInFile rewrites every beta import, not just the
## promoted one (#264 (comment)...)

Pre-fix, the rewriter did:

    content.split("@databricks/appkit/beta").join("@databricks/appkit")

which mangled multi-specifier imports — promoting only `betaA` would
move `betaB` along with it onto the stable subpath, where `betaB`
doesn't exist:

    // before
    import { betaA, betaB } from "@databricks/appkit/beta";
    // promote betaA --to stable, pre-fix (BROKEN)
    import { betaA, betaB } from "@databricks/appkit";
                  ^^^^^ now resolves to undefined at runtime

Now: take `pluginName` as an argument, match the import statement via
regex, parse the specifier list, find the targeted plugin (by binding
name — handles `name`, `name as alias`, `type name`, and the
kebab-to-camelCase manifest convention like `vector-search` ->
`vectorSearch`), and either:

  - Single-specifier import: rewrite the source.
  - Multi-specifier import: keep remaining specifiers on the original
    source AND emit a new import line for the promoted specifier from
    the new source.

Imports that don't reference the targeted plugin are left untouched.

Eight new unit tests cover: single-specifier rewrite, dry-run preserved,
no-op when plugin not in file, multi-specifier split (the reviewer's
case), preserves `import type`, matches aliased specifiers on the
binding (not the alias), multi-line specifier lists, and multiple
packages in the same file rewritten independently. The existing
runPromote integration test was rewritten to lock in the contract that
unrelated beta imports stay put.

## Comment: redundant `as "beta"` cast in sync.ts; would lie if a
## third tier is added later

The cast was unnecessary — narrowing `manifest.stability !== "stable"`
on top of a truthy check already produces the non-stable variant
(currently `"beta"`). Removed the cast and added a comment explaining
the narrowing so a future `"alpha"` tier flows through type-correctly
without revisiting this code.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Copy link
Copy Markdown
Member

@pkosiec pkosiec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I'm still not a fan of introducing the full changes right now - why:

  • AppKit is 0.x, which doesn't mean it gained the full stability
  • with the whole promotion system the plugin development will be slower: think about all the discussions about "what does it mean that a plugin is stable"; even right now it's hard to say what "stable" means. We're looking for user feedback and we might change plugins.
  • I don't think we can confidently say that all plugins are stable - based on user feedback we might change some significantly; we have even internal discussions about
  • Usually "beta" means the API is already established and it won't change radically. So the "beta" level is already too high for e.g. Agent plugin, if we'll really consider it experimental.

I'm not saying it's not useful, it is, but I'd postpone the full mechanism as much as possible.


Instead, what do you think about something very lightweight: label some really early plugins as "experimental". No "stable" terminology at all. Later on we can add more stability levels.

Why?

  1. It matches 0.x reality - we're not promising any plugin is "stable," you're just flagging which ones are especially likely to change.
  2. It's the minimum viable distinction - one label, not a promotion system.
  3. It reduces process overhead - no debates about what "stable" or "graduated" means. A plugin is either experimental or it's not.
  4. Precedent: TypeScript itself uses --experimental* flags, Node.js marks APIs as "Experimental" with no formal "stable" label at the other end (just "not experimental").

WDYT?

Comment thread docs/docs/plugins/stability.md Outdated
MarioCadenas added a commit to databricks/cli that referenced this pull request Apr 30, 2026
Mirrors the AppKit-side rename (databricks/appkit#264). Real synced
manifests will now carry `"stability": "ga"` instead of `"stable"`;
empty/unset still maps to GA. Also renames internal test fixtures
and identifiers from "stable" to "ga"/"GA" so the codebase stays
consistent with the new wire value.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Add a `stability` field to plugin manifests with full CLI tooling.
Plugins declare their tier in manifest.json; the import path
(`@databricks/appkit/<tier>` for non-stable, `@databricks/appkit` for
stable) enforces the contract at the package boundary so consumers
cannot accidentally take a dependency on an unstable API.

This commit lands the initial three-tier implementation
(experimental/preview/stable). A follow-up commit on this branch
collapses to two tiers (beta/stable); the per-commit history shows
the evolution.

- Schema: stability enum on plugin-manifest and template-plugins schemas
- Entrypoints: per-tier subpath exports in appkit and appkit-ui
- CLI sync: propagate stability, strip requiredByTemplate for non-stable,
  orphan resource detection, template version bumped to 1.1
- CLI list: always-visible STABILITY column
- CLI scaffold: stability prompt during plugin create
- CLI promote: new command to promote plugins between tiers
- Docs: stability tiers documentation page

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
…lures

Address review findings on the three-tier plugin stability system:

Security:
- Validate plugin name against npm-package charset (rejects "..", path
  separators, NUL, backslash; allows @scope/name)
- Bound the node_modules manifest lookup with isWithinDirectory so a typo'd
  or malicious name cannot escape dist/plugins
- Refuse to mutate manifests under node_modules unless --allow-installed
  is passed (reinstall would silently revert otherwise)
- Replace console.error(err) in the action handler with a fixed message
  and a DEBUG-gated full-error dump to avoid leaking absolute paths /
  stack frames into telemetry

Correctness:
- Validate the manifest's stability field before any mutation; reject
  unknown values (e.g. "Stable") instead of silently treating them as
  stable and writing back
- Make a failed post-promote 'plugin sync' exit non-zero and skip the
  success message
- Guard sync.ts orphan detection against null / non-object plugin
  entries in a stale appkit.plugins.json

Robustness:
- Skip symlinked directories during the project walk (prevents both
  cycles and out-of-tree rewrites)
- Expand the directory-skip list (.git, .turbo, .next, .nuxt, .cache,
  .svelte-kit, .vite, .parcel-cache, coverage, build, out)
- Align findTsFiles depth cap with sync.ts MAX_SCAN_DEPTH
- Add boundary check inside the recursive TS walk

Tests:
- Replace placeholder tests with real runPromote integration tests
  covering invalid names, path-traversal attempts, unknown tiers,
  invalid manifest stability, demotion, no-op, dry-run,
  import rewriting, symlink boundaries, and the node_modules guard

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Re-running 'pnpm docs:build' picks up three derived artifacts that the
original feature commit missed:

- docs/docs/api/appkit/Interface.PluginManifest.md (typedoc): adds the
  new optional 'stability' field on PluginManifest
- docs/static/schemas/plugin-manifest.schema.json: adds the
  experimental/preview/stable enum (default "stable")
- docs/static/schemas/template-plugins.schema.json: adds 'stability'
  and bumps schema version from const "1.0" to enum ["1.0","1.1"]

Generated by 'pnpm docs:build'; no source changes.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Removes the "experimental" tier entirely and renames "preview" to "beta".
The stability system now has two tiers instead of three:

  beta ──→ stable

Beta plugins follow the previous Preview contract: APIs may change
between minor releases, but the plugin is on a path to stable.

Changes:
- Schema: plugin-manifest and template-plugins enums updated to
  ["beta", "stable"]; manifest schema version stays at 1.1.
- Subpath exports: drop @databricks/appkit/experimental and
  @databricks/appkit-ui/{js,react}/experimental; rename /preview to
  /beta. Source entry files renamed via git mv (experimental.ts files
  removed).
- Promote command: TIER_ORDER, IMPORT_PATH_MAP, isStability(), help
  text, and tests updated. Legacy tier values are now rejected both as
  --to targets and as manifest stability values; tests assert this.
- Create command: stability prompt offers Stable | Beta only.
- list/sync/CreateAnswers types narrowed to "beta" | "stable".
- Docs: stability.md rewritten for the two-tier system; auto-generated
  Interface.PluginManifest.md and docs/static/schemas/* match the new
  enum after pnpm docs:build.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The init template hard-coded a single `from '@databricks/appkit'` import
for every plugin, which would resolve to `undefined` for beta plugins
(they live behind the `/beta` subpath export).

Splits the import emission by plugin stability:
- Stable plugins stay on the existing line.
- Each beta plugin gets its own `from '@databricks/appkit/beta'`
  import line.

Backwards-compatible with the current Databricks CLI: until pluginVar
exposes Stability, `$p.Stability` resolves to the empty string, every
plugin compares unequal to "beta", and rendering matches today's
output. Once databricks/cli#5090 lands the field, beta plugins start
routing to the right import automatically.

The plugins: [...] array body is unchanged — both stable and beta
plugins are constructed identically (`plugin()`) once they're in
scope, so the runtime call list doesn't depend on tier.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The previous template emitted a separate import line per beta plugin:

    import { betaA } from '@databricks/appkit/beta';
    import { betaB } from '@databricks/appkit/beta';

Switch to a single combined import:

    import { betaA, betaB } from '@databricks/appkit/beta';

Implementation: a string-accumulator pre-pass over .plugins builds a
comma-separated list of beta names using printf, then a single guarded
import line is emitted if the list is non-empty. Pure text/template,
no sprig functions required, so it works against the Databricks CLI's
existing template engine.

Behavior unchanged when no beta plugins are selected (no extra import
line, identical to all-stable rendering today).

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Closes a real gap in the stability system: the manifest's `stability`
field, the synced appkit.plugins.json, and the runtime entry exports
(src/index.ts, src/beta.ts) were three independent sources of truth that
could drift apart. Promoting a plugin via manifest edit alone produced
a broken state where the CLI generated `import { x } from
"@databricks/appkit/beta"` for a plugin that was still actually exported
from `@databricks/appkit`, leaving `x` undefined at runtime.

Make the manifest the single source of truth.

- New generator at tools/generate-plugin-entries.ts reads every
  packages/appkit/src/plugins/<name>/manifest.json, groups by
  `stability`, and writes two committed barrels:
    packages/appkit/src/plugins/stable-exports.generated.ts
    packages/appkit/src/plugins/beta-exports.generated.ts
  Hidden plugins (manifest.hidden = true) are skipped, preserving the
  current vector-search behaviour.

- src/index.ts and src/beta.ts now delegate to the generated barrels
  via `export *`, so adding/moving a plugin only requires editing one
  file (its manifest).

- The hand-curated kitchen-sink barrel at src/plugins/index.ts is
  removed; the generated barrels replace it and knip flagged it as
  unused.

- Wired into the build:
  - packages/appkit/package.json#scripts.build:package now runs the
    generator before tsdown.
  - Root `generate:types` includes plugin-entries so existing CI
    "Check generated types are up to date" catches stale barrels.
    The CI step's git diff list is extended accordingly.
  - Added a focused `generate:plugin-entries` root script for
    convenience.

- `appkit plugin promote` detects monorepo context (presence of
  tools/generate-plugin-entries.ts) and re-runs the generator after
  updating the manifest, before kicking off `plugin sync`. Outside
  the monorepo (third-party plugin projects) it's a no-op.

- docs/docs/plugins/stability.md gains a "For First-Party Plugin
  Authors" section explaining the single-source-of-truth model and
  how to flip a plugin between tiers manually.

Verified end-to-end: marking genie as `stability: "beta"` →
`pnpm run generate:plugin-entries` → genie moves from
stable-exports.generated.ts to beta-exports.generated.ts →
`pnpm sync:template` → genie carries `stability: "beta"` and no
`requiredByTemplate` in appkit.plugins.json. All three layers
consistent without manual edits.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Three review-blocker fixes that together restore the PR's headline
"manifest is the single source of truth, all layers stay aligned" claim:

1. Post-promote sync writes to the right file in the monorepo.
   Previously `runPromote` shelled out to `npx appkit plugin sync --write`
   with no flags, which defaults `outputPath` to `<cwd>/appkit.plugins.json`
   and never touches `template/appkit.plugins.json` (the file shipped with
   the AppKit init template). In the monorepo path the manifest changed
   and the runtime barrels regenerated, but the synced template manifest
   stayed stale. Detect monorepo context the same way the generator step
   does (presence of `tools/generate-plugin-entries.ts`) and prefer
   `pnpm run sync:template` when available, which has the right flags
   wired (`--plugins-dir packages/appkit/src/plugins --output
   template/appkit.plugins.json --require-plugins server`). Outside the
   monorepo the original `npx appkit plugin sync --write` is still used.

2. CI freshness gate now covers `template/appkit.plugins.json`.
   The existing "Check generated types are up to date" step diffs the
   four schema/registry/barrel files but not the synced template
   manifest. A contributor editing a plugin's `stability` by hand and
   forgetting `pnpm sync:template` could land a drift between the
   manifest and what the Go init template branches on at scaffold time.
   Add a follow-up step that runs `pnpm run sync:template` and fails
   the build on a non-empty diff, mirroring the existing pattern.

3. Generator validates `manifest.name` and folder name against the
   schema regex AND a JS-identifier rule.
   `tools/generate-plugin-entries.ts` interpolates `name` directly into
   `export { ${name} } from "./${folder}";`. The schema accepts
   `^[a-z][a-z0-9-]*$` (kebab-case) and even shows `"my-custom-plugin"`
   as an example. Today this is masked because `vector-search` (the
   only kebab-named plugin) is `hidden: true`, but the instant any
   hyphenated plugin graduates the generator emits a TypeScript syntax
   error. The same gap is also a defense-in-depth code-injection vector
   (CWE-94) — a malicious manifest with `"name": "x }; await
   import('./evil'); //"` would generate parseable, side-effect-importing
   TS that runs at module load on every consumer.

   Add a `validateIdentifier` pass that runs both the schema regex
   (`^[a-z][a-z0-9-]*$`, mirroring `appkit plugin validate`) and a
   JS-identifier rule (`^[a-z][a-zA-Z0-9_]*$`, restricted to lowercase
   first letter to match the existing built-ins) on both `manifest.name`
   and the plugin folder name. Throws with a clear message pointing at
   the offending manifest and explaining how to fix it.

Verified manually:
- Adding a `bad-name` plugin folder with `"name": "bad-name"` makes
  the generator throw with the expected error message before tsdown
  runs.
- Removing the bad plugin and re-running produces the canonical
  6-stable / 0-beta barrels.
- `pnpm build && pnpm -r typecheck && pnpm test && pnpm docs:build`
  all clean. 88 test files, 1680 tests passing.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Rebase onto main pulled in the new `jobs` plugin (#280) and a duplicate
`export * from "./plugins/stable-exports.generated";` from the conflict
resolution in `packages/appkit/src/index.ts`. Two follow-on changes:

- Re-run `tools/generate-plugin-entries.ts` so `jobs` is included in
  `stable-exports.generated.ts`. The codegen commit on this branch
  predates the jobs plugin landing.
- Dedupe the duplicate `export *` line in `index.ts` that crept in
  during the merge resolution of the codegen commit.

No behavior change beyond making `jobs` reachable through
`@databricks/appkit` (matching what main already does via the
hand-curated barrel that this PR replaces).

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The new "Check synced template manifest is up to date" step in the lint
job ran `pnpm run sync:template`, which goes through
`packages/shared/bin/appkit.js` -> `packages/shared/dist/cli/index.js`.
The lint job only does `pnpm install --frozen-lockfile` (no build), so
dist doesn't exist and Node throws ERR_MODULE_NOT_FOUND.

Initially tried bypassing the bin and running the CLI from source via
`tsx packages/shared/src/cli/index.ts`, but that made knip discover the
source file as a new entry point (since the workflow yaml now references
it from outside the ignored `packages/shared` workspace) and flag
`commander` and `dotenv/config` as unlisted root-level dependencies.

Cleaner fix: add a step to build just the shared package (~1s) before
the freshness check, then use the existing `pnpm run sync:template`
script unchanged. Mirrors what users would do locally.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Previous attempt used `pnpm --filter shared build:package` to produce
the dist needed by `pnpm sync:template`. That script also re-runs
`tools/generate-schema-types.ts` which writes the raw (unformatted)
version of `plugin-manifest.generated.ts` — and the next CI step
("Run Biome Check") then fails because the file no longer matches
what biome would format it to.

Invoke tsdown directly via `pnpm --filter shared exec tsdown` so we get
the dist without regenerating any schema-derived sources. Reproduced
locally: tsdown builds, sync:template succeeds, biome check stays clean.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Closes the docs side of the "manifest is the single source of truth"
contract. Previously, flipping a plugin's `stability` to `"beta"` updated
the runtime export barrel, the synced template manifest, and the API
typedoc, but the plugin's hand-written docs page in
`docs/docs/plugins/<name>.md` looked identical to a stable plugin's.

A new `tools/generate-plugin-doc-banners.ts` reads each
`packages/appkit/src/plugins/<name>/manifest.json` and, when stability
is non-stable, injects a Docusaurus admonition immediately after the
H1:

  :::warning Beta plugin
  This plugin is currently **beta**. APIs may change between minor
  releases. Import from `@databricks/appkit/beta`. See
  [Plugin Stability Tiers](./stability.md).
  :::

The block is delimited by marker comments and is fully idempotent:
re-runs strip any existing banner first, then re-inject only when
stability != "stable". Stable / absent stability removes the banner.

Wiring:
- New generator + a `generate:plugin-doc-banners` script in root
  `package.json`. Folded into `pnpm run generate:types` so the existing
  CI freshness gate covers it.
- CI's "Check generated types are up to date" step now also diffs the
  eight built-in plugin doc pages (analytics, files, genie, jobs,
  lakebase, server, model-serving, vector-search). A drift between
  manifest stability and the docs page banner fails the PR.
- `appkit plugin promote` (monorepo branch) now runs
  `pnpm run generate:types` instead of `generate-plugin-entries.ts`
  alone, so promote keeps both the runtime barrels AND the docs banner
  in sync after the manifest write.
- Doc filename mapping handles the one mismatch (`serving` ->
  `model-serving.md`) via a small `DOC_FILE_OVERRIDES` table.
- Stability docs (`docs/docs/plugins/stability.md`) updated to mention
  the new docs-side generator alongside the runtime barrels.

Verified locally:
- `pnpm run generate:types` against canonical state (all stable) writes
  the barrels and skips every doc page (no banner to add or remove).
  Working tree stays clean.
- Temporarily marking serving as beta -> generator injects the banner
  into model-serving.md, with the manifest -> doc-file override picking
  the right file. Reverting -> banner stripped, file byte-identical to
  the committed version.
- All quality gates pass: build, sync:template, check:fix, typecheck,
  91 test files, docs:build.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
The doc-banners generator was only wired into `pnpm run generate:types`,
which means a maintainer flipping a plugin's `stability` field and
running `pnpm build` (the most common build flow) got the runtime
barrels and the synced template manifest updated but NOT the plugin's
docs page banner. Discovered by trying it: marking the jobs plugin as
beta and running `pnpm build` left `docs/docs/plugins/jobs.md`
unchanged — no banner appeared until `pnpm run generate:types` was
invoked manually.

Append the generator to the root `pnpm build` script so the docs banner
follows the manifest on the same flow that handles everything else.
The generator is idempotent and fast (sub-100ms for ~7 plugins), so
running it on every build has negligible cost.

Verified end-to-end:
- Mark jobs as beta -> `pnpm build` -> jobs.md gains the
  `:::warning Beta plugin` admonition immediately after the H1.
- Revert manifest -> `pnpm build` -> banner stripped, jobs.md
  byte-identical to the committed version.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Address review feedback on PR #264.

## Comment: rewriteImportsInFile rewrites every beta import, not just the
## promoted one (#264 (comment)...)

Pre-fix, the rewriter did:

    content.split("@databricks/appkit/beta").join("@databricks/appkit")

which mangled multi-specifier imports — promoting only `betaA` would
move `betaB` along with it onto the stable subpath, where `betaB`
doesn't exist:

    // before
    import { betaA, betaB } from "@databricks/appkit/beta";
    // promote betaA --to stable, pre-fix (BROKEN)
    import { betaA, betaB } from "@databricks/appkit";
                  ^^^^^ now resolves to undefined at runtime

Now: take `pluginName` as an argument, match the import statement via
regex, parse the specifier list, find the targeted plugin (by binding
name — handles `name`, `name as alias`, `type name`, and the
kebab-to-camelCase manifest convention like `vector-search` ->
`vectorSearch`), and either:

  - Single-specifier import: rewrite the source.
  - Multi-specifier import: keep remaining specifiers on the original
    source AND emit a new import line for the promoted specifier from
    the new source.

Imports that don't reference the targeted plugin are left untouched.

Eight new unit tests cover: single-specifier rewrite, dry-run preserved,
no-op when plugin not in file, multi-specifier split (the reviewer's
case), preserves `import type`, matches aliased specifiers on the
binding (not the alias), multi-line specifier lists, and multiple
packages in the same file rewritten independently. The existing
runPromote integration test was rewritten to lock in the contract that
unrelated beta imports stay put.

## Comment: redundant `as "beta"` cast in sync.ts; would lie if a
## third tier is added later

The cast was unnecessary — narrowing `manifest.stability !== "stable"`
on top of a truthy check already produces the non-stable variant
(currently `"beta"`). Removed the cast and added a comment explaining
the narrowing so a future `"alpha"` tier flows through type-correctly
without revisiting this code.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Hard rename of the production-ready stability tier from "stable" to
"ga" (general availability). Schema enum becomes ["beta", "ga"];
"stable" is no longer a valid manifest value.

Why "ga": "stable" was overloaded — every git branch, plugin release,
and SDK version is "stable" in some sense. "ga" matches the broader
Databricks product vocabulary for the production-ready / semver-strict
tier and reads unambiguously alongside "beta".

Touched layers (every place "stable" had any meaning is renamed):

- Schemas: `enum: ["beta", "stable"]` -> `["beta", "ga"]` in both
  `plugin-manifest.schema.json` and `template-plugins.schema.json`,
  with descriptions updated. Default value changes from "stable" to
  "ga" so absent stability still maps to the production-ready tier.
- TypeScript types: `Stability = "beta" | "stable"` -> `"beta" | "ga"`
  across `manifest-types.ts`, `plugin-manifest.generated.ts`,
  `promote.ts`, `sync.ts`, `list.ts`, `create.ts`, and the two
  generators in `tools/`.
- Promote command: TIER_ORDER, IMPORT_PATH_MAP, isStability, error
  messages, validation, --to flag help, and the post-promote
  generator/sync block.
- Sync command: the strip-requiredByTemplate condition now compares
  against "ga" instead of "stable".
- Create command: interactive prompt offers "GA" / "Beta" (label),
  with values "ga" / "beta". Existing default-omit logic preserved.
- Generated barrel: renamed `stable-exports.generated.ts` ->
  `ga-exports.generated.ts` via `git mv`. `src/index.ts` and the
  `tools/generate-plugin-entries.ts` writer updated. CI freshness
  diff list and root `package.json` scripts updated too.
- Doc banners generator: only beta plugins get a banner; the GA
  branch strips any existing banner. No banner is emitted for GA
  (matches the design — absence of beta = GA, no extra label needed).
- Tests: bulk-renamed `to: "stable"` test args to `to: "ga"`,
  updated `isStability`/TIER_ORDER tests for the new tier name, and
  extended the legacy-rejection cases so manifests or --to flags
  carrying "stable" (now legacy), "experimental", or "preview" are
  all rejected up front.
- Stability docs page: tier name updated everywhere; promotion path
  diagram now reads `beta ──→ ga`; current-plugins-by-tier section
  says "GA" instead of "Stable".

Backwards compat: the schema rejects the legacy "stable" value, so a
stale manifest must migrate to "ga" (or omit the field). Built-in
plugins never wrote the field explicitly, so no first-party manifest
needs editing. Third-party plugins that explicitly committed
`"stability": "stable"` will fail validation until they update — this
is the "hard rename" the user explicitly chose over a deprecation
alias.

CLI side (databricks/cli#5090) is intentionally NOT touched here. The
CLI hardcodes a "stable" check in `Plugin.StabilityLabel()` to suppress
the picker suffix for the default tier. After this rename, that check
still suppresses "stable" (now legacy / never written by AppKit) but
absent-stability plugins (the new GA default) already render with no
suffix because the field is omitted from the JSON. So the CLI keeps
working without changes; an explicit "ga" written into a manifest
would render as "(ga)" in the picker, which is acceptable for now.
A follow-up CLI PR can update the suppression check.

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas force-pushed the mario/plugin-stability-tiers branch 2 times, most recently from 2108039 to 5e94a10 Compare April 30, 2026 13:29
…anner paths

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
fjakobs pushed a commit to databricks/cli that referenced this pull request Apr 30, 2026
## Why

AppKit
([databricks/appkit#264](databricks/appkit#264))
added a `stability` field on plugins (`stable` or `beta`) and bumped
`appkit.plugins.json` to schema `1.1`. `databricks apps init` was
stability-blind: every plugin rendered identically in the picker, and
the AppKit init template needs `Stability` on each selected plugin to
route beta plugins through the `@databricks/appkit/beta` subpath
([commit
d826a532](databricks/appkit@d826a532)).

## Changes

- `libs/apps/manifest`: add `Plugin.Stability`
(`json:"stability,omitempty"`)
  and a `StabilityLabel()` passthrough. Plain string, not an enum, so
  unknown future tiers round-trip.
- `libs/apps/prompt`: new `RenderStabilityTier` adds a colored `(beta)`
  suffix to plugin labels (yellow `#FFAB00`); unknown tiers fall back to
  gray. Stable/unset gets no suffix.
- `cmd/apps/init.go`: `pluginVar` now carries `Stability`, populated
from
  the manifest, so the AppKit template can branch on `{{$p.Stability}}`.

No flag-gating of beta plugins. AppKit's `sync` step already strips
`requiredByTemplate` from non-stable plugins, so they show up as
selectable rather than mandatory.

## Test plan

- [x] `go test ./cmd/apps/... ./libs/apps/manifest/...
./libs/apps/prompt/...`
- [x] `golangci-lint` clean on changed packages.
- [ ] Visual: `apps init` against a local template with mixed
  stable/beta/unknown plugins. Yellow `(beta)`, gray for unknown,
  nothing for stable.

`huh` applies its own foreground to the focused row, so the inline tier
color is partially overridden on whichever row is currently highlighted.
Easy to swap for a non-colored `[beta]` token if that's annoying in
practice.


## Demo 


https://github.com/user-attachments/assets/85905f4f-40c4-465f-940b-6889c74a3c77

---------

Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
@MarioCadenas MarioCadenas merged commit 1a77ce9 into main Apr 30, 2026
8 checks passed
@MarioCadenas MarioCadenas deleted the mario/plugin-stability-tiers branch April 30, 2026 16:14
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.

4 participants