diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a13ebe4..91cee37 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.6.0", + "version": "5.7.0", "source": "./", "author": { "name": "Jesse Vincent", diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index c15462a..daed446 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "superpowers", "description": "Core skills library for Claude Code: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.6.0", + "version": "5.7.0", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 41d7936..a5c40bb 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "superpowers", "displayName": "Superpowers", "description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques", - "version": "5.6.0", + "version": "5.7.0", "author": { "name": "Jesse Vincent", "email": "jesse@fsck.com" diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..e1af090 --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,40 @@ +name: Version Consistency Check + +on: + push: + paths: + - '.claude-plugin/plugin.json' + - '.claude-plugin/marketplace.json' + - '.cursor-plugin/plugin.json' + - 'scripts/bump-version.sh' + - 'tests/version-check.sh' + - '.github/workflows/version-check.yml' + tags: + - 'v*.*.*' + pull_request: + paths: + - '.claude-plugin/plugin.json' + - '.claude-plugin/marketplace.json' + - '.cursor-plugin/plugin.json' + - 'scripts/bump-version.sh' + - 'tests/version-check.sh' + - '.github/workflows/version-check.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + version-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check version consistency across plugin manifests + run: bash tests/version-check.sh + + - name: Check version matches latest git tag (on tag push) + if: startsWith(github.ref, 'refs/tags/') + run: bash tests/version-check.sh --check-tag diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 0000000..441ce04 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# bump-version.sh — Update the version string across all plugin manifests. +# +# Usage: +# scripts/bump-version.sh +# scripts/bump-version.sh +# +# When called with one argument the script detects the current version from +# .claude-plugin/plugin.json and replaces it everywhere. +# When called with two arguments the first is the version to replace (useful +# when the files are already inconsistent). +# +# Version files managed by this script: +# .claude-plugin/plugin.json +# .claude-plugin/marketplace.json +# .cursor-plugin/plugin.json + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +VERSION_FILES=( + ".claude-plugin/plugin.json" + ".claude-plugin/marketplace.json" + ".cursor-plugin/plugin.json" +) + +usage() { + echo "Usage: $0 " >&2 + echo " $0 " >&2 + exit 1 +} + +if [[ $# -eq 1 ]]; then + NEW_VERSION="$1" + # Detect the current version from the primary file + PRIMARY="${REPO_ROOT}/${VERSION_FILES[0]}" + OLD_VERSION=$(grep '"version"' "$PRIMARY" | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/' || true) + if [[ -z "$OLD_VERSION" ]]; then + echo "ERROR: Could not detect current version from $PRIMARY" >&2 + exit 1 + fi +elif [[ $# -eq 2 ]]; then + OLD_VERSION="$1" + NEW_VERSION="$2" +else + usage +fi + +# Validate version looks like semver (N.N.N) +if ! echo "$NEW_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "ERROR: Version '$NEW_VERSION' does not look like semver (e.g. 5.7.0)" >&2 + exit 1 +fi + +echo "Bumping version: $OLD_VERSION → $NEW_VERSION" + +ERRORS=0 +for rel_path in "${VERSION_FILES[@]}"; do + abs_path="${REPO_ROOT}/${rel_path}" + if [[ ! -f "$abs_path" ]]; then + echo "WARNING: $rel_path not found, skipping" >&2 + continue + fi + if ! grep -qF "\"version\": \"${OLD_VERSION}\"" "$abs_path"; then + echo "ERROR: version string '${OLD_VERSION}' not found in $rel_path; cannot update" >&2 + ERRORS=$((ERRORS + 1)) + continue + fi + sed -i.bak "s/\"version\": \"${OLD_VERSION}\"/\"version\": \"${NEW_VERSION}\"/" "$abs_path" && rm -f "${abs_path}.bak" + echo " Updated $rel_path" +done + +if [[ $ERRORS -gt 0 ]]; then + echo "FAIL: $ERRORS file(s) could not be updated." >&2 + exit 1 +fi + +echo "Done. Remember to:" +echo " 1. Update RELEASE-NOTES.md with a new ## v${NEW_VERSION} section." +echo " 2. Commit the changes: git commit -am \"chore: bump version to ${NEW_VERSION}\"" +echo " 3. Tag the release: git tag v${NEW_VERSION}" diff --git a/tests/version-check.sh b/tests/version-check.sh new file mode 100755 index 0000000..dad2089 --- /dev/null +++ b/tests/version-check.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# version-check.sh — Validate that all plugin manifests declare the same version. +# +# Optionally validates against the latest git tag when --check-tag is passed. +# +# Usage: +# tests/version-check.sh # consistency check only +# tests/version-check.sh --check-tag # also verify versions match latest git tag +# +# Exit codes: +# 0 All version files are consistent (and match the git tag if --check-tag). +# 1 One or more version files disagree. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +CHECK_TAG=false +if [[ "${1:-}" == "--check-tag" ]]; then + CHECK_TAG=true +fi + +VERSION_FILES=( + ".claude-plugin/plugin.json" + ".claude-plugin/marketplace.json" + ".cursor-plugin/plugin.json" +) + +extract_version() { + grep '"version"' "$1" | head -1 | sed 's/.*"version": *"\([^"]*\)".*/\1/' || true +} + +ERRORS=0 +FIRST_VERSION="" +FIRST_FILE="" + +for rel_path in "${VERSION_FILES[@]}"; do + abs_path="${REPO_ROOT}/${rel_path}" + if [[ ! -f "$abs_path" ]]; then + echo "ERROR: $rel_path not found" >&2 + ERRORS=$((ERRORS + 1)) + continue + fi + ver=$(extract_version "$abs_path") + if [[ -z "$ver" ]]; then + echo "ERROR: Could not read version from $rel_path" >&2 + ERRORS=$((ERRORS + 1)) + continue + fi + if [[ -z "$FIRST_VERSION" ]]; then + FIRST_VERSION="$ver" + FIRST_FILE="$rel_path" + elif [[ "$ver" != "$FIRST_VERSION" ]]; then + echo "ERROR: Version mismatch: $rel_path has '$ver' but $FIRST_FILE has '$FIRST_VERSION'" >&2 + ERRORS=$((ERRORS + 1)) + fi +done + +if [[ $ERRORS -gt 0 ]]; then + echo "" + echo "FAIL: Version files are inconsistent. Run scripts/bump-version.sh to fix." >&2 + exit 1 +fi + +echo "OK: All version files agree on version $FIRST_VERSION" + +if [[ "$CHECK_TAG" == true ]]; then + # Get the tag that points exactly at HEAD (e.g. when triggered by a tag push). + # Falls back gracefully if HEAD is not tagged. + HEAD_TAG=$(git -C "$REPO_ROOT" describe --tags --exact-match HEAD 2>/dev/null || true) + if [[ -z "$HEAD_TAG" ]]; then + echo "WARNING: HEAD is not at a semver tag; skipping tag check." >&2 + elif ! echo "$HEAD_TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "WARNING: HEAD tag '$HEAD_TAG' is not a semver tag; skipping tag check." >&2 + else + TAG_VERSION="${HEAD_TAG#v}" + if [[ "$FIRST_VERSION" != "$TAG_VERSION" ]]; then + echo "ERROR: Plugin version '$FIRST_VERSION' does not match HEAD tag '$HEAD_TAG'." >&2 + echo " Run: scripts/bump-version.sh ${TAG_VERSION}" >&2 + exit 1 + fi + echo "OK: Plugin version $FIRST_VERSION matches HEAD tag $HEAD_TAG" + fi +fi