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
53 changes: 53 additions & 0 deletions .github/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,66 @@ GitHub Actions workflows for the plugin marketplace.
| `validate-versions.yml` | PR/push when marketplace.json or plugin.json changes | Blocks PRs if versions mismatch |
| `test.yml` | PR to main, push to main/staging | Runs pytest for all plugins |
| `source-branch-check.yml` | PR to main | Enforces staging → main flow |
| `detect-plugin-changes.yml` | Push to main (plugin paths) | Detects changes, dispatches version bump |
| `bump-plugin-versions.yml` | repository_dispatch: bump-versions | Bumps versions, commits, dispatches tagging |
| `create-release-tags.yml` | repository_dispatch: create-tags | Creates and pushes git tags |

## Version Validation

The `validate-versions.yml` workflow uses a matrix strategy to check each plugin in parallel:
- **Local plugins**: Compares marketplace.json version to `{source}/.claude-plugin/plugin.json`
- **External plugins**: Shallow clones the URL and compares versions

## Auto Semantic Versioning

Three workflows chain together via `repository_dispatch` to automatically version plugins:

```
Push to main (plugin changes)
┌─────────────────────────┐
│ detect-plugin-changes │ Detects which plugins changed
│ Analyzes PR labels or │ Determines bump type (major/minor/patch)
│ commit messages │
└───────────┬─────────────┘
│ repository_dispatch: bump-versions
┌─────────────────────────┐
│ bump-plugin-versions │ Calculates new versions
│ Updates plugin.json & │ Creates chore commit
│ marketplace.json │
└───────────┬─────────────┘
│ repository_dispatch: create-tags
┌─────────────────────────┐
│ create-release-tags │ Creates git tags (plugin@vX.Y.Z)
│ Pushes tags to origin │
└─────────────────────────┘
```

### Bump Type Detection

Priority order:
1. **PR labels**: `major`/`breaking` → major, `feat`/`feature`/`minor` → minor, `fix`/`bugfix`/`patch` → patch
2. **Commit message**: `BREAKING CHANGE` → major, `feat:` → minor
3. **Default**: patch

### Prerequisites

Requires `VERSION_PAT` repository secret with:
- Contents: Read and write
- Pull requests: Read
- Workflows: Read and write

### Manual Bumps

If you manually update a `plugin.json` version before merging, the workflow detects the mismatch and syncs `marketplace.json` without auto-incrementing.

### External Plugins

External plugins (like `golang-workflow`) are skipped - they're versioned in their own repositories.

## Adding New Workflows

1. Create `.github/workflows/<name>.yml`
Expand Down
199 changes: 199 additions & 0 deletions .github/workflows/bump-plugin-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Workflow 2 of 3: Auto Semantic Versioning
# Receives plugin list and bump type, calculates new versions, updates files, commits, dispatches to create-release-tags.yml
#
# Triggered by: repository_dispatch event "bump-versions" from detect-plugin-changes.yml
# Payload: { plugins: ["plugin1", "plugin2"], bump_type: "patch|minor|major", trigger_sha: "..." }
#
# Flow: Receive dispatch → bump versions → update marketplace.json → commit → dispatch create-tags event

name: Bump Plugin Versions

on:
repository_dispatch:
types: [bump-versions]

concurrency:
group: bump-versions
cancel-in-progress: false

permissions:
contents: write

jobs:
bump:
name: Bump Versions & Commit
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.VERSION_PAT }}
ref: main # Ensure we're on latest main

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Bump versions
id: versions
run: |
PLUGINS='${{ toJson(github.event.client_payload.plugins) }}'
BUMP_TYPE="${{ github.event.client_payload.bump_type }}"

echo "Plugins to bump: $PLUGINS"
echo "Bump type: $BUMP_TYPE"

# Function to calculate new version
bump_version() {
local current=$1
local bump_type=$2

# Handle missing or null version
if [ -z "$current" ] || [ "$current" = "null" ]; then
echo "0.1.0"
return
fi

# Parse semver (major.minor.patch)
IFS='.' read -r major minor patch <<< "$current"

# Default to 0 if parsing fails
major=${major:-0}
minor=${minor:-0}
patch=${patch:-0}

case "$bump_type" in
major)
echo "$((major + 1)).0.0"
;;
minor)
echo "${major}.$((minor + 1)).0"
;;
patch|*)
echo "${major}.${minor}.$((patch + 1))"
;;
esac
}

# Initialize version map
VERSION_MAP="{}"
BUMPED_PLUGINS=()

# Process each plugin
for plugin in $(echo "$PLUGINS" | jq -r '.[]'); do
PLUGIN_JSON="${plugin}/.claude-plugin/plugin.json"

if [ ! -f "$PLUGIN_JSON" ]; then
echo "::warning::plugin.json not found for $plugin, skipping"
continue
fi

# Get current version
CURRENT_VERSION=$(jq -r '.version // "null"' "$PLUGIN_JSON")
echo "Plugin: $plugin, Current version: $CURRENT_VERSION"

# Check marketplace.json for manual bump detection
MARKETPLACE_VERSION=$(jq -r --arg name "$plugin" '.plugins[] | select(.name == $name) | .version // "null"' .claude-plugin/marketplace.json)

if [ "$CURRENT_VERSION" != "$MARKETPLACE_VERSION" ] && [ "$CURRENT_VERSION" != "null" ] && [ "$MARKETPLACE_VERSION" != "null" ]; then
echo "::notice::Manual bump detected for $plugin (plugin.json: $CURRENT_VERSION, marketplace: $MARKETPLACE_VERSION)"
echo "Syncing marketplace.json to $CURRENT_VERSION without auto-bump"
NEW_VERSION="$CURRENT_VERSION"
else
# Calculate new version
NEW_VERSION=$(bump_version "$CURRENT_VERSION" "$BUMP_TYPE")
echo "Bumping $plugin: $CURRENT_VERSION -> $NEW_VERSION"

# Update plugin.json
jq --arg v "$NEW_VERSION" '.version = $v' "$PLUGIN_JSON" > "${PLUGIN_JSON}.tmp"
mv "${PLUGIN_JSON}.tmp" "$PLUGIN_JSON"
fi

# Add to version map
VERSION_MAP=$(echo "$VERSION_MAP" | jq --arg p "$plugin" --arg v "$NEW_VERSION" '. + {($p): $v}')
BUMPED_PLUGINS+=("$plugin")
done

echo "Version map: $VERSION_MAP"
echo "version_map=$VERSION_MAP" >> $GITHUB_OUTPUT

# Store bumped plugins list for commit message
PLUGINS_LIST=$(IFS=', '; echo "${BUMPED_PLUGINS[*]}")
echo "plugins_list=$PLUGINS_LIST" >> $GITHUB_OUTPUT

- name: Update marketplace.json
run: |
VERSION_MAP='${{ steps.versions.outputs.version_map }}'

echo "Updating marketplace.json with new versions..."

for plugin in $(echo "$VERSION_MAP" | jq -r 'keys[]'); do
NEW_VERSION=$(echo "$VERSION_MAP" | jq -r --arg p "$plugin" '.[$p]')
echo "Setting $plugin to $NEW_VERSION in marketplace.json"

# Update the version for this plugin in marketplace.json
jq --arg name "$plugin" --arg version "$NEW_VERSION" \
'(.plugins[] | select(.name == $name)).version = $version' \
.claude-plugin/marketplace.json > .claude-plugin/marketplace.json.tmp
mv .claude-plugin/marketplace.json.tmp .claude-plugin/marketplace.json
done

echo "marketplace.json updated:"
jq '.plugins[] | {name, version}' .claude-plugin/marketplace.json

- name: Commit changes
id: commit
run: |
# Stage version files
git add */.claude-plugin/plugin.json .claude-plugin/marketplace.json

# Check if there are changes to commit
if git diff --staged --quiet; then
echo "No changes to commit"
echo "committed=false" >> $GITHUB_OUTPUT
exit 0
fi

# Create commit
PLUGINS_LIST="${{ steps.versions.outputs.plugins_list }}"
git commit -m "chore: bump versions for ${PLUGINS_LIST}"

# Push to main
git push origin main

echo "committed=true" >> $GITHUB_OUTPUT
echo "commit_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT

- name: Dispatch tag creation
if: steps.commit.outputs.committed == 'true'
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.VERSION_PAT }}
event-type: create-tags
client-payload: >
{
"version_map": ${{ steps.versions.outputs.version_map }},
"commit_sha": "${{ steps.commit.outputs.commit_sha }}",
"trigger_sha": "${{ github.event.client_payload.trigger_sha }}"
}

- name: Summary
run: |
echo "## Bump Plugin Versions Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Bump Type:** ${{ github.event.client_payload.bump_type }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Version Changes" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
echo '${{ steps.versions.outputs.version_map }}' | jq . >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.commit.outputs.committed }}" = "true" ]; then
echo "Committed changes and dispatched \`create-tags\` event." >> $GITHUB_STEP_SUMMARY
else
echo "No changes to commit." >> $GITHUB_STEP_SUMMARY
fi
118 changes: 118 additions & 0 deletions .github/workflows/create-release-tags.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Workflow 3 of 3: Auto Semantic Versioning
# Receives version map, creates git tags for each bumped plugin
#
# Triggered by: repository_dispatch event "create-tags" from bump-plugin-versions.yml
# Payload: { version_map: {"plugin1": "1.0.2", "plugin2": "1.0.1"}, commit_sha: "..." }
#
# Flow: Receive dispatch → create git tags → push tags

name: Create Release Tags

on:
repository_dispatch:
types: [create-tags]

concurrency:
group: create-tags
cancel-in-progress: false

permissions:
contents: write

jobs:
tag:
name: Create & Push Tags
runs-on: ubuntu-latest
timeout-minutes: 5

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.VERSION_PAT }}
fetch-depth: 0 # Need full history for proper tagging
ref: main # Ensure we're on latest main with version bump commit

Comment on lines +32 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Tag the bump commit, not current main

This workflow checks out and pulls main, but never checks out the commit_sha passed in the dispatch payload. If another commit lands on main between the bump commit and this workflow running, the tags will be created on the newer HEAD instead of the bump commit that produced the versions. That can tag the wrong code. Consider checking out the specific commit_sha before tagging.

Useful? React with 👍 / 👎.

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Pull latest changes
run: |
# Ensure we have the version bump commit
git pull origin main

- name: Create tags
id: tags
run: |
VERSION_MAP='${{ toJson(github.event.client_payload.version_map) }}'

echo "Version map: $VERSION_MAP"

CREATED_TAGS=()
SKIPPED_TAGS=()

for plugin in $(echo "$VERSION_MAP" | jq -r 'keys[]'); do
VERSION=$(echo "$VERSION_MAP" | jq -r --arg p "$plugin" '.[$p]')
TAG="${plugin}@v${VERSION}"

echo "Processing tag: $TAG"

# Check if tag already exists
if git tag -l "$TAG" | grep -q .; then
echo "::warning::Tag $TAG already exists, skipping"
SKIPPED_TAGS+=("$TAG")
continue
fi

# Create annotated tag
git tag -a "$TAG" -m "Release ${plugin} v${VERSION}

Auto-generated by GitHub Actions workflow.
Trigger commit: ${{ github.event.client_payload.trigger_sha }}"

echo "Created tag: $TAG"
CREATED_TAGS+=("$TAG")
done

# Output results
echo "created_tags=${CREATED_TAGS[*]}" >> $GITHUB_OUTPUT
echo "skipped_tags=${SKIPPED_TAGS[*]}" >> $GITHUB_OUTPUT
echo "created_count=${#CREATED_TAGS[@]}" >> $GITHUB_OUTPUT

- name: Push tags
if: steps.tags.outputs.created_count != '0'
run: |
echo "Pushing tags to origin..."
git push origin --tags

- name: Summary
run: |
echo "## Create Release Tags Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

echo "### Created Tags" >> $GITHUB_STEP_SUMMARY
CREATED="${{ steps.tags.outputs.created_tags }}"
if [ -n "$CREATED" ]; then
for tag in $CREATED; do
echo "- \`$tag\`" >> $GITHUB_STEP_SUMMARY
done
else
echo "_No tags created_" >> $GITHUB_STEP_SUMMARY
fi

echo "" >> $GITHUB_STEP_SUMMARY
echo "### Skipped Tags (already exist)" >> $GITHUB_STEP_SUMMARY
SKIPPED="${{ steps.tags.outputs.skipped_tags }}"
if [ -n "$SKIPPED" ]; then
for tag in $SKIPPED; do
echo "- \`$tag\`" >> $GITHUB_STEP_SUMMARY
done
else
echo "_None skipped_" >> $GITHUB_STEP_SUMMARY
fi

echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "**Trigger commit:** \`${{ github.event.client_payload.trigger_sha }}\`" >> $GITHUB_STEP_SUMMARY
Loading