Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .github/workflows/version-check.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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);
Expand Down
99 changes: 99 additions & 0 deletions scripts/check-version.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading