Skip to content

docs: add CLAUDE.md as token-efficient navigation map #65

docs: add CLAUDE.md as token-efficient navigation map

docs: add CLAUDE.md as token-efficient navigation map #65

Workflow file for this run

name: Release
on:
pull_request:
types: [closed]
branches: [main]
permissions:
contents: write
jobs:
# ===========================================================================
# Step 1: Determine version and commit bump
# ===========================================================================
version:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
commit_sha: ${{ steps.bump.outputs.commit_sha }}
steps:
- uses: actions/checkout@v4
with:
ssh-key: ${{ secrets.VERSION_BUMP_KEY }}
fetch-depth: 0
- name: Determine version
id: version
run: |
PLUGIN_VERSION=$(jq -r '.version' .claude-plugin/plugin.json)
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")
echo "plugin=$PLUGIN_VERSION latest_tag=$LATEST_TAG"
# If plugin.json version is ahead of the latest tag, use it (manual bump)
if [ "$PLUGIN_VERSION" != "$LATEST_TAG" ]; then
P_MAJOR=$(echo "$PLUGIN_VERSION" | cut -d. -f1)
P_MINOR=$(echo "$PLUGIN_VERSION" | cut -d. -f2)
P_PATCH=$(echo "$PLUGIN_VERSION" | cut -d. -f3)
TAG_MAJOR=$(echo "$LATEST_TAG" | cut -d. -f1)
TAG_MINOR=$(echo "$LATEST_TAG" | cut -d. -f2)
TAG_PATCH=$(echo "$LATEST_TAG" | cut -d. -f3)
if [ "$P_MAJOR" -gt "$TAG_MAJOR" ] 2>/dev/null || \
([ "$P_MAJOR" -eq "$TAG_MAJOR" ] && [ "$P_MINOR" -gt "$TAG_MINOR" ]) 2>/dev/null || \
([ "$P_MAJOR" -eq "$TAG_MAJOR" ] && [ "$P_MINOR" -eq "$TAG_MINOR" ] && [ "$P_PATCH" -gt "$TAG_PATCH" ]) 2>/dev/null; then
echo "Manual version bump detected: $LATEST_TAG → $PLUGIN_VERSION"
echo "version=$PLUGIN_VERSION" >> "$GITHUB_OUTPUT"
echo "bumped=manual" >> "$GITHUB_OUTPUT"
else
echo "plugin.json version $PLUGIN_VERSION is not ahead of tag $LATEST_TAG — auto-bumping patch"
echo "bumped=auto" >> "$GITHUB_OUTPUT"
fi
else
echo "No manual bump — auto-bumping patch"
echo "bumped=auto" >> "$GITHUB_OUTPUT"
fi
# Auto-bump patch if no manual bump was set
if ! grep -q "version=" "$GITHUB_OUTPUT" 2>/dev/null; then
MAJOR=$(echo "$LATEST_TAG" | cut -d. -f1)
MINOR=$(echo "$LATEST_TAG" | cut -d. -f2)
PATCH=$(echo "$LATEST_TAG" | cut -d. -f3)
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
echo "Auto-bumping: $LATEST_TAG → $NEW_VERSION"
echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
fi
- name: Sync version files
run: |
VERSION="${{ steps.version.outputs.version }}"
jq --arg v "$VERSION" '.version = $v' .claude-plugin/plugin.json > tmp.json && mv tmp.json .claude-plugin/plugin.json
- name: Commit version bump
id: bump
run: |
VERSION="${{ steps.version.outputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .claude-plugin/plugin.json
if git diff --cached --quiet; then
echo "Version files already at $VERSION — no commit needed"
else
git commit -m "bump to v${VERSION}"
git push
fi
echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# ===========================================================================
# Step 2: Create draft release (tag + release atomically)
# ===========================================================================
create-release:
needs: version
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.create_release.outputs.release_created }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Generate release notes
id: notes
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
VERSION="${{ needs.version.outputs.version }}"
NOTES=$(awk "/^## ${VERSION}$/,/^## /{if(/^## ${VERSION}$/)next; if(/^## /)exit; print}" CHANGELOG.md 2>/dev/null)
if [ -z "$NOTES" ]; then
NOTES=$(awk "/^## v${VERSION}$/,/^## /{if(/^## v${VERSION}$/)next; if(/^## /)exit; print}" CHANGELOG.md 2>/dev/null)
fi
if [ -z "$NOTES" ]; then
NOTES="Merged: ${PR_TITLE}"
fi
{
echo "notes<<EOF"
echo "$NOTES"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Create draft release
id: create_release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
run: |
TAG="v${{ needs.version.outputs.version }}"
COMMIT="${{ needs.version.outputs.commit_sha }}"
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then
echo "Tag $TAG already exists — skipping release"
echo "release_created=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Tag the version-bumped commit, not the stale checkout
git tag "$TAG" "$COMMIT"
git push origin "$TAG"
# Create release; clean up tag on failure
if ! gh release create "$TAG" --title "$TAG" --draft --notes "$RELEASE_NOTES"; then
echo "::warning::Release creation failed — cleaning up orphaned tag"
git push origin --delete "$TAG" || true
echo "release_created=false" >> "$GITHUB_OUTPUT"
exit 1
fi
echo "release_created=true" >> "$GITHUB_OUTPUT"
# ===========================================================================
# Step 3: Build binaries (parallel matrix)
# ===========================================================================
binaries:
needs: [version, create-release]
if: needs.create-release.outputs.release_created == 'true'
runs-on: ubuntu-latest
defaults:
run:
working-directory: src
strategy:
matrix:
os: [linux, darwin, windows]
arch: [amd64, arm64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: src/go.mod
cache-dependency-path: src/go.sum
- name: Build binary
run: make build-for GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} VERSION=v${{ needs.version.outputs.version }}
- name: Upload release asset
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
EXT=""
if [ "${{ matrix.os }}" = "windows" ]; then EXT=".exe"; fi
ASSET="bin/devkit-${{ matrix.os }}-${{ matrix.arch }}${EXT}"
echo "Uploading ${ASSET}"
gh release upload "v${{ needs.version.outputs.version }}" \
"$ASSET" \
--clobber
working-directory: src
# ===========================================================================
# Step 4: Generate checksums and publish release
# ===========================================================================
publish:
needs: [version, create-release, binaries]
if: needs.create-release.outputs.release_created == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download all release assets and generate checksums
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="v${{ needs.version.outputs.version }}"
mkdir -p /tmp/release-assets
gh release download "$TAG" --dir /tmp/release-assets
(cd /tmp/release-assets && sha256sum devkit-* > checksums.txt && cat checksums.txt)
gh release upload "$TAG" /tmp/release-assets/checksums.txt --clobber
- name: Publish release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release edit "v${{ needs.version.outputs.version }}" --draft=false
# ===========================================================================
# Cleanup on failure
# ===========================================================================
cleanup-on-failure:
needs: [version, create-release, binaries, publish]
if: >-
always() &&
needs.create-release.outputs.release_created == 'true' &&
(needs.binaries.result == 'failure' || needs.binaries.result == 'cancelled' ||
needs.publish.result == 'failure' || needs.publish.result == 'cancelled')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Delete draft release on failure
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="v${{ needs.version.outputs.version }}"
echo "::warning::Cleaning up draft release $TAG due to workflow failure"
gh release delete "$TAG" --yes || echo "::warning::Failed to delete release $TAG — manual cleanup required"
git push origin --delete "$TAG" || echo "::warning::Failed to delete tag $TAG — manual cleanup required"