Add engine binary distribution — cross-compile, release, auto-bootstrap #42
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} | |
| - 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" 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" |