From feebb093571d753ff9b5f54839f4a3dda0aeb37c Mon Sep 17 00:00:00 2001 From: "aiste.grigaliunaite@decodo.com" Date: Tue, 20 Oct 2026 16:51:36 +0300 Subject: [PATCH] add CI steps to check package versions --- .github/workflows/release.yml | 31 +++++++++ .github/workflows/version-check.yml | 20 ++++++ CONTRIBUTING.md | 23 +++++++ scripts/check-version.mjs | 99 +++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/version-check.yml create mode 100644 scripts/check-version.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..02f198e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,31 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Read version + id: version + run: echo "tag=v$(jq -r .metadata.version .cursor-plugin/marketplace.json)" >> "$GITHUB_OUTPUT" + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.version.outputs.tag }} + run: | + if git ls-remote --exit-code --tags origin "refs/tags/$TAG" > /dev/null; then + echo "$TAG already exists, nothing to release." + exit 0 + fi + gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --generate-notes diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..ec538b9 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,20 @@ +name: Version check + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/check-version.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39ec40c..1fdbb78 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,8 @@ plugins/decodo/ README.md # setup guide for this plugin install.sh # local install for development scripts/validate-template.mjs # manifest validator (from cursor/plugin-template) +scripts/check-version.mjs # version bump check, run in CI on every PR +.github/workflows/ # version check on PRs, GitHub Release on main ``` `plugin.json` declares every component path explicitly (`skills`, `rules`, `commands`, @@ -146,6 +148,27 @@ commands, so for a definitive answer read a *copy* of the chat store at command string against the repo — the skill's own example commands appear in the store simply because the skill text entered context. +## Versioning and releases + +Any PR that changes files under `plugins/` or `.cursor-plugin/` must bump versions, or the +`Version check` workflow fails: + +- `version` in the `plugin.json` of every plugin whose folder changed +- `metadata.version` in `.cursor-plugin/marketplace.json`, which is the release version + +Bump by hand, MAJOR.MINOR.PATCH only. Docs and tooling outside those folders (`README.md`, +`CONTRIBUTING.md`, `install.sh`, `scripts/`) need no bump. To run the check locally against +`main`: + +```bash +node scripts/check-version.mjs origin/main +``` + +When a merge to `main` carries a `metadata.version` that has no tag yet, the `Release` workflow +tags that commit `vX.Y.Z` and creates a GitHub Release with notes generated from the merged PRs. +If the tag already exists it does nothing, so a PR that bumps to a version someone else already +released produces no release. + ## Submitting to the marketplace Submit the repository URL at [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish); diff --git a/scripts/check-version.mjs b/scripts/check-version.mjs new file mode 100644 index 0000000..a36e563 --- /dev/null +++ b/scripts/check-version.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node + +// Fails when shipped files change without a version bump. Compares the working +// tree against a base commit (default HEAD^1: on a pull_request checkout HEAD is +// the merge commit, so its first parent is the tip of the base branch). +// +// node scripts/check-version.mjs [base-ref] + +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const marketplacePath = ".cursor-plugin/marketplace.json"; +const versionPattern = /^(\d+)\.(\d+)\.(\d+)$/; + +const base = process.argv[2] ?? "HEAD^1"; +const errors = []; + +function git(...args) { + return execFileSync("git", args, { encoding: "utf8" }); +} + +function readJsonAt(ref, filePath) { + try { + return JSON.parse(git("show", `${ref}:${filePath}`)); + } catch { + return null; + } +} + +function readJson(filePath) { + return JSON.parse(readFileSync(filePath, "utf8")); +} + +function parseVersion(version, context) { + const match = versionPattern.exec(version ?? ""); + if (!match) { + errors.push(`${context}: "${version}" is not a MAJOR.MINOR.PATCH version`); + return null; + } + return match.slice(1).map(Number); +} + +function isGreater(a, b) { + for (let i = 0; i < 3; i++) { + if (a[i] !== b[i]) return a[i] > b[i]; + } + return false; +} + +function requireBump(context, baseVersion, headVersion) { + const parsedHead = parseVersion(headVersion, context); + const parsedBase = parseVersion(baseVersion, `${context} on ${base}`); + if (!parsedHead || !parsedBase) return; + if (!isGreater(parsedHead, parsedBase)) { + errors.push(`${context}: version ${headVersion} must be greater than ${baseVersion} on ${base}`); + } else { + console.log(`${context}: ${baseVersion} -> ${headVersion}`); + } +} + +const changed = git("diff", "--name-only", base, "--") + .split("\n") + .filter(Boolean); +const isUnder = (dir) => changed.some((file) => file.startsWith(`${dir}/`)); + +const marketplace = readJson(marketplacePath); + +for (const plugin of marketplace.plugins ?? []) { + const dir = path.posix.normalize(plugin.source); + if (!isUnder(dir)) continue; + + const manifestPath = `${dir}/.cursor-plugin/plugin.json`; + const baseManifest = readJsonAt(base, manifestPath); + const headManifest = readJson(manifestPath); + if (!baseManifest) { + parseVersion(headManifest.version, manifestPath); + console.log(`${manifestPath}: new plugin at ${headManifest.version}`); + continue; + } + requireBump(manifestPath, baseManifest.version, headManifest.version); +} + +if (isUnder("plugins") || isUnder(".cursor-plugin")) { + const baseMarketplace = readJsonAt(base, marketplacePath); + requireBump( + `${marketplacePath} metadata.version`, + baseMarketplace?.metadata?.version, + marketplace.metadata?.version, + ); +} else { + console.log("No changes under plugins/ or .cursor-plugin/, no version bump needed."); +} + +if (errors.length > 0) { + for (const error of errors) console.error(`error: ${error}`); + process.exit(1); +}