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
17 changes: 16 additions & 1 deletion .github/workflows/release-skills.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ jobs:
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip pyyaml
- name: Stamp plugin version from release tag
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: python scripts/set_plugin_version.py --version "${GITHUB_REF_NAME#v}"
- name: Validate plugin manifest
shell: bash
run: |
Expand All @@ -36,7 +40,18 @@ jobs:
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip pyyaml
- run: python scripts/build_skill_archives.py --out dist
- name: Stamp plugin version from release tag
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: python scripts/set_plugin_version.py --version "${GITHUB_REF_NAME#v}"
- name: Build skill archives
shell: bash
run: |
if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then
python scripts/build_skill_archives.py --version "${GITHUB_REF_NAME#v}" --out dist
else
python scripts/build_skill_archives.py --out dist
fi
- uses: actions/upload-artifact@v4
with:
name: skill-archives
Expand Down
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,21 @@ python3 scripts/validate_plugin.py
python3 scripts/validate_skills.py
```

If you want Codex to load this repo as a local plugin, the simplest path is to expose this repo at `~/plugins/kedify-mcp` and use a local marketplace entry that points to `./plugins/kedify-mcp`.
If you want Codex to load this repo as a local plugin, the simplest path is to expose this repo under your local `plugins/` directory and use a local marketplace entry that points to `./plugins/kedify-mcp`.

Example:

```bash
mkdir -p ~/plugins
ln -sfn /home/jkarasek/go/src/github.com/kedify/kedify-mcp ~/plugins/kedify-mcp
mkdir -p "$HOME/plugins"
ln -sfn "$PWD" "$HOME/plugins/kedify-mcp"
```

After the marketplace entry exists, use the normal local update flow:

1. Refresh the plugin cachebuster:

```bash
python3 /home/jkarasek/.codex/skills/.system/plugin-creator/scripts/update_plugin_cachebuster.py \
/home/jkarasek/go/src/github.com/kedify/kedify-mcp
python3 scripts/set_plugin_version.py --version 0.1.0
```

2. Reinstall the plugin from the configured local marketplace:
Expand All @@ -95,4 +94,10 @@ Release artifacts are optimized for ChatGPT skill upload.
- one zip artifact per skill
- no separate whole-plugin zip release artifact

See [RELEASING.md](/home/jkarasek/go/src/github.com/kedify/kedify-mcp/RELEASING.md:1) for the exact artifact layout, versioning model, and GitHub Actions release flow.
See [`RELEASING.md`](RELEASING.md) for the exact artifact layout, versioning model, and GitHub Actions release flow.

The preferred local release command is:

```bash
python scripts/cut_release.py --version 0.1.0 --push
```
45 changes: 38 additions & 7 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,47 @@ Reasons:
Versioning convention:

- Git tag: `v0.1.0`
- `.codex-plugin/plugin.json` version: `0.1.0`
- release-stamped `.codex-plugin/plugin.json` version: `0.1.0`
- release asset: `kedify-mcp-autoscaling-debug-v0.1.0.zip`

## Release steps

Preferred local release command:

```bash
python scripts/cut_release.py --version 0.1.0 --push
```

What it does:

- requires a clean git worktree
- stamps `.codex-plugin/plugin.json` to `0.1.0`
- validates plugin and skills
- builds and verifies the skill archives locally
- commits the version bump with `release: v0.1.0`
- creates annotated tag `v0.1.0`
- pushes the current branch and tag when `--push` is passed

Manual step-by-step equivalent:

1. Update skill or plugin-packaging source files in this repo.
2. If needed, deploy compatible MCP backend changes from `dashboard-api-service` first.
3. Bump `.codex-plugin/plugin.json` `version`.
4. Merge to `main`.
5. Tag the release:
3. Merge to `main`.
4. Run:

```bash
git tag v0.1.0
python scripts/cut_release.py --version 0.1.0
```

5. If you did not use `--push`, push the branch and tag:

```bash
git push origin HEAD
git push origin v0.1.0
```

6. GitHub Actions will:
- stamp `.codex-plugin/plugin.json` to the tag version inside the CI workspace
- validate plugin manifests
- validate each skill
- build one zip per skill into `dist/`
Expand All @@ -102,12 +126,19 @@ Run the same checks locally before tagging:

```bash
python -m pip install --upgrade pip pyyaml
python scripts/validate_plugin.py
python scripts/set_plugin_version.py --version 0.1.0
python scripts/validate_plugin.py --expected-version 0.1.0
python scripts/validate_skills.py
python scripts/build_skill_archives.py --out dist
python scripts/build_skill_archives.py --version 0.1.0 --out dist
python scripts/verify_archives.py dist
```

You can also preview the release cutter without changing git state:

```bash
python scripts/cut_release.py --version 0.1.0 --dry-run
```

## ChatGPT upload

For manual ChatGPT skill upload, use the per-skill zip from the GitHub Release.
Expand Down
122 changes: 122 additions & 0 deletions scripts/cut_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import subprocess
import sys

from common import SEMVER_RE


def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Cut a kedify-mcp release by stamping plugin.json, validating, "
"committing the version bump, creating a tag, and optionally pushing."
)
)
parser.add_argument("--version", required=True, help="Release version without the leading v")
parser.add_argument(
"--push",
action="store_true",
help="Push the current branch and release tag after creating them locally",
)
parser.add_argument(
"--remote",
default="origin",
help="Git remote to push to when --push is used (default: origin)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the actions that would run without changing git state",
)
args = parser.parse_args()

version = args.version.strip()
if SEMVER_RE.fullmatch(version) is None:
raise SystemExit(f"Version `{version}` is not valid semver")
tag_name = f"v{version}"

ensure_clean_worktree()
ensure_tag_missing(tag_name)

if args.dry_run:
print(f"Would set plugin.json version to {version}")
print(f"Would validate plugin and skills for {tag_name}")
print(f"Would commit .codex-plugin/plugin.json with message: release: {tag_name}")
print(f"Would create annotated tag: {tag_name}")
if args.push:
branch = git_output("rev-parse", "--abbrev-ref", "HEAD")
print(f"Would push branch `{branch}` and tag `{tag_name}` to `{args.remote}`")
return

run_python_script("scripts/set_plugin_version.py", "--version", version)
run_python_script("scripts/validate_plugin.py", "--expected-version", version)
run_python_script("scripts/validate_skills.py")
run_python_script("scripts/build_skill_archives.py", "--version", version, "--out", "dist")
run_python_script("scripts/verify_archives.py", "dist")

git("add", ".codex-plugin/plugin.json")
diff = subprocess.run(["git", "diff", "--cached", "--quiet"], check=False)
if diff.returncode == 0:
print("No plugin.json changes to commit; tagging current HEAD")
else:
git("commit", "-m", f"release: {tag_name}")
Comment on lines +61 to +65
git("tag", "-a", tag_name, "-m", tag_name)

print(f"Created release commit and tag {tag_name}")

if args.push:
branch = git_output("rev-parse", "--abbrev-ref", "HEAD")
git("push", args.remote, branch)
git("push", args.remote, tag_name)
print(f"Pushed branch `{branch}` and tag `{tag_name}` to `{args.remote}`")
else:
print(f"Next step: git push origin HEAD && git push origin {tag_name}")


def ensure_clean_worktree() -> None:
status = git_output("status", "--short")
if status:
raise SystemExit(
"Refusing to cut a release from a dirty worktree.\n"
"Commit or stash your changes first, then rerun the release command."
)


def ensure_tag_missing(tag_name: str) -> None:
result = subprocess.run(
["git", "rev-parse", "-q", "--verify", f"refs/tags/{tag_name}"],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
raise SystemExit(f"Git tag `{tag_name}` already exists")


def run_python_script(script: str, *script_args: str) -> None:
cmd = [sys.executable, script, *script_args]
print("+", " ".join(cmd))
subprocess.run(cmd, check=True)


def git(*git_args: str) -> None:
cmd = ["git", *git_args]
print("+", " ".join(cmd))
subprocess.run(cmd, check=True)


def git_output(*git_args: str) -> str:
result = subprocess.run(
["git", *git_args],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()


if __name__ == "__main__":
main()
38 changes: 38 additions & 0 deletions scripts/set_plugin_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json

from common import PLUGIN_MANIFEST_PATH, SEMVER_RE, load_json


def main() -> None:
parser = argparse.ArgumentParser(
description="Set .codex-plugin/plugin.json version to a specific release value."
)
parser.add_argument("--version", required=True, help="Semver version to write")
args = parser.parse_args()

version = args.version.strip()
if SEMVER_RE.fullmatch(version) is None:
raise SystemExit(f"Version `{version}` is not valid semver")

payload = load_json(PLUGIN_MANIFEST_PATH)
if not isinstance(payload, dict):
raise SystemExit(f"{PLUGIN_MANIFEST_PATH} must contain a JSON object")

old_version = payload.get("version")
payload["version"] = version
with PLUGIN_MANIFEST_PATH.open("w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2)
handle.write("\n")

if old_version == version:
print(f"Plugin version already set to {version}")
else:
print(f"Updated plugin version from {old_version!r} to {version!r}")


if __name__ == "__main__":
main()
Loading