From f9ad4f31b22cf02ec5ff1ddf6f21e9765c605d28 Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 29 May 2026 14:05:54 +0200 Subject: [PATCH] ci(release): bump plugin manifests on release; fix marketplace docs The Release workflow now runs scripts/bump_version.py to set the version in .claude-plugin/plugin.json and .cursor-plugin/plugin.json to the release tag, regenerate manifest.json, commit, then tag. Claude Code's marketplace keys plugin updates on the version field, so releases that did not bump it left marketplace clients on the cached copy (latest tag v0.2.1 vs manifest 0.2.0). Docs: fix the Claude Code install command to databricks@databricks-agent-skills, correct the example plugin cache path, soften the GPG-signing claim to match reality, and document the release flow plus the cli-compat.json follow-up in CONTRIBUTING. Co-authored-by: Isaac Signed-off-by: simon --- .github/workflows/release.yml | 26 ++++++++++++ CONTRIBUTING.md | 20 +++++++++ README.md | 8 ++-- scripts/bump_version.py | 79 +++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 scripts/bump_version.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0966a26..b9bd7fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,13 @@ jobs: contents: write steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + ref: main + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: '3.11' - name: Validate tag format env: @@ -27,6 +34,25 @@ jobs: exit 1 fi + - name: Bump plugin manifests and regenerate manifest + env: + VERSION: ${{ inputs.version }} + run: python3 scripts/bump_version.py "$VERSION" + + - name: Commit version bump + env: + VERSION: ${{ inputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .claude-plugin/plugin.json .cursor-plugin/plugin.json manifest.json + if git diff --cached --quiet; then + echo "Manifests already at $VERSION; nothing to commit" + else + git commit -s -m "chore(release): bump plugin manifests to $VERSION" + git push origin HEAD:main + fi + - name: Create tag and release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a3a8d2..e994a98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,26 @@ Examples in skills and references must follow secure defaults: - Prefer scoped tokens over broad credentials - Obfuscate sensitive values: use placeholder workspace IDs (`1111111111111111`), URLs (`company-workspace.cloud.databricks.com`), and never include real tokens or passwords +## Releasing + +Releases are cut by the **Release** workflow (`.github/workflows/release.yml`), +triggered manually (`workflow_dispatch`) with a `vX.Y.Z` tag. The workflow: + +1. Runs `scripts/bump_version.py `, which sets the `version` field in + both `.claude-plugin/plugin.json` and `.cursor-plugin/plugin.json` to the + release version and regenerates `manifest.json`. +2. Commits the bump to `main`. +3. Creates the `vX.Y.Z` tag and GitHub release from that commit. + +Bumping the plugin `version` on every release is **required**: Claude Code's +plugin marketplace keys updates on the `version` field, so a release that ships +without bumping it leaves marketplace clients on the cached copy and they never +see the new skills. + +After releasing, open a follow-up PR to update +[`cli-compat.json`](#version-resolution-in-databricks-cli) in the CLI repo so +`databricks aitools install` resolves to the new version. + ## Version resolution in Databricks CLI The Databricks CLI uses [`cli-compat.json`](https://github.com/databricks/cli/blob/main/internal/build/cli-compat.json) diff --git a/README.md b/README.md index d0d5caa..7af7457 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ but end up loaded by the same agents — pick whichever fits your workflow. - **Databricks CLI** writes SKILL.md files directly into each agent's skill directory (`~/.claude/skills/`, `~/.cursor/extensions/<...>`, etc.). - **Plugin marketplaces** (Claude Code, Cursor) cache the plugin under the - agent's plugin directory (e.g. `~/.claude/plugins/cache/databricks-skills/`); + agent's plugin directory (e.g. `~/.claude/plugins/cache/databricks-agent-skills/`); the agent discovers skills from there. **Via the Databricks CLI (canonical; supports experimental skills):** @@ -35,7 +35,7 @@ skill under [`./skills/`](./skills/)): ```text /plugin marketplace add databricks/databricks-agent-skills -/plugin install databricks-skills +/plugin install databricks@databricks-agent-skills ``` **Via the Cursor plugin marketplace:** @@ -149,7 +149,9 @@ Please see [SECURITY](./SECURITY) for vulnerability reporting guidelines. ## Integrity -All future release tags will be GPG-signed and verifiable via `git tag -v `. +Release tags are created by the [Release workflow](./.github/workflows/release.yml) +and map 1:1 to a published version. (GPG signing is planned once a release signing +key is provisioned.) ## Contributing diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 0000000..583518b --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Bump the plugin manifest versions to a release version + regenerate manifest. + +Given a `vX.Y.Z` (or `X.Y.Z`) release version, set the `version` field in both +`.claude-plugin/plugin.json` and `.cursor-plugin/plugin.json`, then regenerate +`manifest.json` so the released commit ships a current manifest. + +Why this exists: Claude Code's plugin marketplace keys updates on the `version` +field in `.claude-plugin/plugin.json`. If a release ships without bumping that +field, marketplace clients keep the cached copy and never see the new skills. +This makes the release tag the single source of truth for the plugin version. + +Run by `.github/workflows/release.yml`. Stdlib-only (no pip), so it runs on the +protected runner that can't reach pypi.org. +""" + +import argparse +import re +from pathlib import Path + +import skills # sibling module in scripts/ + +SEMVER_RE = re.compile(r"^\d+\.\d+\.\d+$") + +# Matches the first `"version": "..."` field. Both manifests carry exactly one +# top-level `version` key, so a targeted in-place replacement keeps the diff to +# a single line and avoids reformatting the JSON (which a load/dump round-trip +# would risk). +VERSION_FIELD_RE = re.compile(r'("version"\s*:\s*")[^"]*(")') + +PLUGIN_MANIFESTS = ( + Path(".claude-plugin") / "plugin.json", + Path(".cursor-plugin") / "plugin.json", +) + + +def normalize_version(raw: str) -> str: + """Strip a leading `v` and validate the result is `X.Y.Z`.""" + version = raw.strip() + if version.startswith("v"): + version = version[1:] + if not SEMVER_RE.match(version): + raise SystemExit(f"ERROR: version must be vX.Y.Z or X.Y.Z, got {raw!r}") + return version + + +def set_version(path: Path, version: str) -> bool: + """Set the `version` field in a plugin manifest. Returns True if changed.""" + text = path.read_text() + new_text, count = VERSION_FIELD_RE.subn(rf"\g<1>{version}\g<2>", text, count=1) + if count != 1: + raise SystemExit( + f'ERROR: expected exactly one "version" field in {path}, found {count}' + ) + if new_text == text: + return False + path.write_text(new_text) + return True + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="Release version, e.g. v0.3.0") + args = parser.parse_args() + + version = normalize_version(args.version) + repo_root = Path(__file__).resolve().parent.parent + + for rel in PLUGIN_MANIFESTS: + changed = set_version(repo_root / rel, version) + print(f"{'set' if changed else 'unchanged'} {rel} -> {version}") + + manifest = skills.generate_manifest(repo_root) + (repo_root / "manifest.json").write_text(skills.serialize_manifest(manifest)) + print("regenerated manifest.json") + + +if __name__ == "__main__": + main()