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
52 changes: 52 additions & 0 deletions .github/scripts/bump-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { readFileSync, writeFileSync } from "node:fs";

const MANIFESTS = [
".claude-plugin/plugin.json",
".cursor-plugin/plugin.json",
"openai/plugin.json",
"gemini-extension.json",
];
const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
const USAGE = 'Usage: node .github/scripts/bump-version.mjs <patch|minor|major|X.Y.Z> "<change>" ["<change>" ...]';

function exit(message) {
console.error(`${message}\n${USAGE}`);
process.exit(1);
}

function nextVersion(current, bump) {
const [, major, minor, patch] = current.match(VERSION).map(Number);
if (bump === "patch") return `${major}.${minor}.${patch + 1}`;
if (bump === "minor") return `${major}.${minor + 1}.0`;
if (bump === "major") return `${major + 1}.0.0`;
if (!VERSION.test(bump)) exit(`"${bump}" is not patch, minor, major or an X.Y.Z version.`);
return bump;
}

function isNewer(candidate, current) {
const [a, b] = [candidate, current].map((version) => version.match(VERSION).slice(1).map(Number));
const index = a.findIndex((part, i) => part !== b[i]);
return index !== -1 && a[index] > b[index];
}

const [bump, ...changes] = process.argv.slice(2);
if (!bump) exit("Missing the version to bump to.");
if (changes.length === 0) exit("Describe at least one change for the changelog.");

const current = JSON.parse(readFileSync(MANIFESTS[0], "utf8")).version;
const version = nextVersion(current, bump);
if (!isNewer(version, current)) exit(`${version} must be newer than the current version, ${current}.`);

const changelog = readFileSync("CHANGELOG.md", "utf8");
if (changelog.includes(`\n## ${version}\n`)) exit(`CHANGELOG.md already has a ${version} section.`);
if (!changelog.startsWith("# Changelog\n\n")) exit('CHANGELOG.md must start with a "# Changelog" heading.');

for (const path of MANIFESTS) {
const manifest = readFileSync(path, "utf8");
writeFileSync(path, manifest.replace(/"version": "[^"]+"/, `"version": "${version}"`));
}

const section = `## ${version}\n\n${changes.map((change) => `- ${change}`).join("\n")}\n\n`;
writeFileSync("CHANGELOG.md", changelog.replace("# Changelog\n\n", `# Changelog\n\n${section}`));

console.log(`Bumped ${current} → ${version} in ${MANIFESTS.join(", ")} and added it to CHANGELOG.md.`);
7 changes: 7 additions & 0 deletions .github/scripts/check-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ function checkVersions() {
}
}

function checkChangelog() {
const version = readJson(MANIFESTS[0]).version;
const sections = readFileSync("CHANGELOG.md", "utf8").match(/^## .+$/gm) ?? [];
if (!sections.includes(`## ${version}`)) fail(`CHANGELOG.md: no "## ${version}" section for the current version`);
}

function serverUrls(path, key) {
const servers = readJson(path).mcpServers;
return Object.fromEntries(Object.entries(servers).map(([name, server]) => [name, server[key]]));
Expand Down Expand Up @@ -177,6 +183,7 @@ const cliFlag = process.argv.indexOf("--cli");
if (cliFlag === -1) {
checkNames();
checkVersions();
checkChangelog();
checkMcpServers();
checkSkills();
checkFiles();
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Release

on:
push:
branches: [main]
paths:
- CHANGELOG.md
workflow_dispatch:

permissions:
contents: write

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.1
- uses: actions/setup-node@v7.1.0
with:
node-version: 24
- run: node .github/scripts/check-plugin.mjs

- id: version
run: |
version=$(jq -r .version .claude-plugin/plugin.json)
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "tag=v$version" >> "$GITHUB_OUTPUT"

- name: Build the OpenAI plugin ZIP
env:
DEMO_RECORDING_URL: ${{ secrets.OPENAI_DEMO_RECORDING_URL }}
VERSION: ${{ steps.version.outputs.version }}
run: |
stage="$RUNNER_TEMP/polar"
mkdir -p "$stage/.codex-plugin"
if [ -n "$DEMO_RECORDING_URL" ]; then
jq --arg url "$DEMO_RECORDING_URL" \
'.extensions["com.openai"].review.demo_recording_url = $url' \
openai/plugin.json > "$stage/.codex-plugin/plugin.json"
else
echo "::warning::The OPENAI_DEMO_RECORDING_URL secret isn't set, so OpenAI's portal will reject this ZIP."
cp openai/plugin.json "$stage/.codex-plugin/plugin.json"
fi
cp openai/mcp.json "$stage/.mcp.json"
cp -R skills README.md LICENSE "$stage/"
for asset in $(jq -r '.interface.logo, .interface.composerIcon' openai/plugin.json | sort -u); do
mkdir -p "$stage/$(dirname "$asset")"
cp "$asset" "$stage/$asset"
done
(cd "$stage" && zip -qrX "$RUNNER_TEMP/polar-openai-$VERSION.zip" .)
unzip -l "$RUNNER_TEMP/polar-openai-$VERSION.zip"

- uses: actions/upload-artifact@v7
with:
name: polar-openai-${{ steps.version.outputs.version }}
path: ${{ runner.temp }}/polar-openai-${{ steps.version.outputs.version }}.zip
retention-days: 30

- name: Create the tag and GitHub release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists."
exit 0
fi
awk -v heading="## $VERSION" '$0 == heading { found = 1; next } /^## / { found = 0 } found' \
CHANGELOG.md > "$RUNNER_TEMP/notes.md"
gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md"
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## 0.1.0

- First release: the Polar MCP servers (`polar` and `polar-sandbox`) and the `setup-polar`, `polar-integration`, `polar-testing` and `polar-migration` skills, for Claude Code, Codex, Cursor, Gemini CLI and Antigravity.
51 changes: 51 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Releasing a new version

Every release gets a version number, a changelog entry, a `vX.Y.Z` tag and a GitHub release. The tag, the release and the OpenAI ZIP are created automatically when the release PR is merged.

## 1. Bump the version and add a changelog entry

On a new branch from `main`, run:

```bash
node .github/scripts/bump-version.mjs patch 'Change 1', 'Change 2'
```

Use `patch`, `minor` or `major`, or an exact version such as `0.2.0`. Each quoted argument becomes one bullet. Use single quotes so backticks stay literal. The script sets the version in all four manifests (`.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `openai/plugin.json` and `gemini-extension.json`) and adds a section for it at the top of `CHANGELOG.md`. That section becomes the GitHub release notes, so write it for people who use the plugin. You can still edit it before committing.

## 2. Open the pull request

The [Validate plugin](https://github.com/polarsource/agent-toolkit/actions/workflows/validate.yml) workflow checks that the four versions match and that `CHANGELOG.md` has a section for the new version. It also installs and validates the plugin in Claude Code, Codex, Cursor, Gemini CLI and Antigravity, and checks every `polar` CLI command the docs mention against the published CLI. Merge once it passes.

## 3. Check the release run

Merging runs the **Release** workflow. In the repository's Actions tab, check that it:

1. passed the plugin checks,
2. created the `vX.Y.Z` tag and GitHub release with your changelog notes,
3. attached the `polar-openai-X.Y.Z` ZIP to the run as an artifact.

If it fails, fix the cause on `main`, then start it again from **Actions → Release → Run workflow**. It skips the release if the tag already exists and always rebuilds the ZIP.

## 4. Update each listing

| Where | What to do |
| --- | --- |
| Anthropic's plugin directory | Nothing. It rescans `main`, and a reviewer approves the new version. Check its status at [claude.ai/directory/manage](https://claude.ai/directory/manage). |
| OpenAI's plugin directory | Only if skills, test cases, listing metadata or the MCP URL changed: download the `polar-openai-X.Y.Z` artifact from the release run, upload it at [platform.openai.com/plugins](https://platform.openai.com/plugins) and submit it for review. Changes to the MCP server's tools need no upload. OpenAI rescans the server daily and holds new tools for review. |
| Cursor Marketplace | Cursor reviews every update. Check the plugin's status in Cursor's publisher dashboard, and resubmit at [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish) if the new version doesn't show up for review. |
| Gemini CLI extension gallery | Nothing. It indexes new tags daily, as long as the repository has the `gemini-cli-extension` topic. |
| GitHub installs | Nothing. Users of Claude Code, Codex, Cursor, Gemini CLI and Antigravity get the new version when they update the plugin. |

## Recording a new demo video

OpenAI's review needs a demo video of the test cases in `openai/plugin.json`. When the test cases or the tools they use change, record a new one and store its link as a repository secret:

```bash
gh secret set OPENAI_DEMO_RECORDING_URL --repo polarsource/agent-toolkit
```

Then rebuild the ZIP from **Actions → Release → Run workflow**. The link must open without signing in, and the video shouldn't show the review account's login code.

## Changing the MCP server URLs

Each agent reads the servers from a different file, so update all four: `.mcp.json`, `gemini-extension.json`, `mcp_config.json` and `openai/mcp.json`. The plugin checks fail if they disagree.
Loading