From c486049c508a935cb7b9f18e5df72649efc7c7e9 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Fri, 16 Jan 2026 20:39:17 -0800 Subject: [PATCH] refactor: extract build-and-publish workflow into reusable component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Refactor publish.yml to use a reusable workflow for build and publish steps - Add new build-and-publish.yml as a reusable workflow that handles wheel building, PyPI publishing, and PR creation - Add auto-release.yml for automated releases triggered by version changes - Simplify publish.yml to only handle tests, lint, and orchestration Co-Authored-By: Claude Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 17 Claude-Permission-Prompts: 1 Claude-Escapes: 1 Claude-Plan: # Automated Release on CLI Version Bump ## Overview Create a new workflow that automatically publishes to PyPI when a CLI version bump commit passes CI. **Trigger**: Commit message starting with `chore: bump bundled CLI version to` pushed to main, after Test workflow succeeds. **Actions**: 1. Auto-increment SDK patch version (0.1.20 → 0.1.21) 2. Build platform-specific wheels (linux, linux-arm, macos, windows) 3. Publish to PyPI 4. Update version files + changelog 5. Push directly to main (via deploy key) 6. Create git tag + GitHub Release ## Files to Create/Modify ### New: `.github/workflows/auto-release.yml` ```yaml name: Auto Release on CLI Bump on: workflow_run: workflows: ["Test"] types: [completed] branches: [main] jobs: check-trigger: runs-on: ubuntu-latest if: | github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && startsWith(github.event.workflow_run.head_commit.message, 'chore: bump bundled CLI version to') outputs: version: ${{ steps.version.outputs.version }} previous_tag: ${{ steps.previous_tag.outputs.previous_tag }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get current SDK version and calculate next id: version run: | CURRENT=$(python -c "import re; print(re.search(r'__version__ = \"([^\"]+)\"', open('src/claude_agent_sdk/_version.py').read()).group(1))") IFS='.' read -ra PARTS <<< "$CURRENT" NEXT="${PARTS[0]}.${PARTS[1]}.$((PARTS[2] + 1))" echo "version=$NEXT" >> $GITHUB_OUTPUT echo "Current: $CURRENT -> Next: $NEXT" - name: Get previous release tag id: previous_tag run: | PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT build-wheels: needs: check-trigger runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install build dependencies run: pip install build twine wheel shell: bash - name: Build wheel with bundled CLI run: python scripts/build_wheel.py --version "${{ needs.check-trigger.outputs.version }}" --skip-sdist --clean shell: bash - uses: actions/upload-artifact@v4 with: name: wheel-${{ matrix.os }} path: dist/*.whl if-no-files-found: error publish: needs: [check-trigger, build-wheels] runs-on: ubuntu-latest permissions: contents: write env: VERSION: ${{ needs.check-trigger.outputs.version }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 ssh-key: ${{ secrets.DEPLOY_KEY }} - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Update version files run: python scripts/update_version.py "$VERSION" - uses: actions/download-artifact@v4 with: path: dist pattern: wheel-* merge-multiple: true - name: Build sdist and publish to PyPI run: | pip install build twine python -m build --sdist twine upload dist/* env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - name: Commit version changes run: | git config user.email "github-actions[bot]@users.noreply.github.com" git config user.name "github-actions[bot]" git add pyproject.toml src/claude_agent_sdk/_version.py git commit -m "chore: release v$VERSION" - name: Update changelog with Claude continue-on-error: true uses: anthropics/claude-code-action@v1 with: prompt: "/generate-changelog new version: ${{ env.VERSION }}, old version: ${{ needs.check-trigger.outputs.previous_tag }}" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ secrets.GITHUB_TOKEN }} claude_args: | --model claude-opus-4-5 --allowedTools 'Bash(git add:*),Bash(git commit:*),Edit' - name: Push to main run: git push origin main - name: Create tag and GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git tag -a "v$VERSION" -m "Release v$VERSION" git push origin "v$VERSION" # Extract changelog section awk -v ver="$VERSION" '/^## / { if (found) exit; if ($2 == ver) found=1; next } found { print }' CHANGELOG.md > release_notes.md echo -e "\n---\n\n**PyPI:** https://pypi.org/project/claude-agent-sdk/$VERSION/\n\n\`\`\`bash\npip install claude-agent-sdk==$VERSION\n\`\`\`" >> release_notes.md gh release create "v$VERSION" --title "v$VERSION" --notes-file release_notes.md ``` ### Keep Existing: `publish.yml` Keep as-is for manual releases (SDK-only changes, major/minor bumps, emergencies). ### Optional Cleanup: `create-release-tag.yml` Can be removed since auto-release handles tagging. Or keep as fallback for manual publish flow. ## Setup Required ### Deploy Key (Required) 1. Generate SSH key: ```bash ssh-keygen -t ed25519 -C "github-actions-deploy-key" -f deploy_key -N "" ``` 2. Add public key as deploy key: - Repo Settings → Deploy keys → Add deploy key - Title: "GitHub Actions Auto Release" - Paste `deploy_key.pub` contents - Check "Allow write access" 3. Add private key as secret: - Repo Settings → Secrets → New repository secret - Name: `DEPLOY_KEY` - Value: `deploy_key` contents 4. Delete local key files ### Existing Secrets (Already Present) - `PYPI_API_TOKEN` - PyPI publishing - `ANTHROPIC_API_KEY` - Changelog generation ## Flow Diagram ``` Push: "chore: bump bundled CLI version to 2.1.12" ↓ lint.yml + test.yml (parallel) ↓ (workflow_run) auto-release.yml ↓ ┌─ check conditions (CI passed, commit pattern matches) ├─ calculate version: 0.1.20 → 0.1.21 ├─ build 4 platform wheels ├─ publish to PyPI ├─ update _version.py, pyproject.toml ├─ generate changelog ├─ push to main ├─ create tag v0.1.21 └─ create GitHub Release ``` ## Verification After implementation: 1. Create a test branch with the workflow 2. Add `workflow_dispatch` trigger temporarily for testing 3. Run manually and verify each step 4. Remove `workflow_dispatch`, merge to main 5. Test with an actual CLI version bump commit --- .github/workflows/auto-release.yml | 53 +++++++ .github/workflows/build-and-publish.yml | 170 ++++++++++++++++++++++ .github/workflows/publish.yml | 182 +++--------------------- 3 files changed, 244 insertions(+), 161 deletions(-) create mode 100644 .github/workflows/auto-release.yml create mode 100644 .github/workflows/build-and-publish.yml diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 00000000..c48509c3 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,53 @@ +name: Auto Release on CLI Bump + +on: + workflow_run: + workflows: ["Test"] + types: [completed] + branches: [main] + +jobs: + check-trigger: + runs-on: ubuntu-latest + if: | + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + startsWith(github.event.workflow_run.head_commit.message, 'chore: bump bundled CLI version to') + outputs: + version: ${{ steps.version.outputs.version }} + previous_tag: ${{ steps.previous_tag.outputs.previous_tag }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify CLI version file was changed + run: | + if ! git diff --name-only HEAD~1 | grep -q '_cli_version.py'; then + echo "::error::CLI version file not changed in this commit" + exit 1 + fi + + - name: Get current SDK version and calculate next + id: version + run: | + CURRENT=$(python -c "import re; print(re.search(r'__version__ = \"([^\"]+)\"', open('src/claude_agent_sdk/_version.py').read()).group(1))") + IFS='.' read -ra PARTS <<< "$CURRENT" + NEXT="${PARTS[0]}.${PARTS[1]}.$((PARTS[2] + 1))" + echo "version=$NEXT" >> $GITHUB_OUTPUT + echo "Current: $CURRENT -> Next: $NEXT" + + - name: Get previous release tag + id: previous_tag + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT + + release: + needs: check-trigger + uses: ./.github/workflows/build-and-publish.yml + with: + version: ${{ needs.check-trigger.outputs.version }} + previous_tag: ${{ needs.check-trigger.outputs.previous_tag }} + push_directly: true + secrets: inherit diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml new file mode 100644 index 00000000..ce6524f0 --- /dev/null +++ b/.github/workflows/build-and-publish.yml @@ -0,0 +1,170 @@ +name: Build and Publish + +on: + workflow_call: + inputs: + version: + description: 'Version to publish' + required: true + type: string + previous_tag: + description: 'Previous release tag for changelog generation' + required: false + type: string + default: '' + push_directly: + description: 'Push directly to main (true) or create PR (false)' + required: false + type: boolean + default: false + +jobs: + build-wheels: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install build dependencies + run: pip install build twine wheel + shell: bash + + - name: Build wheel with bundled CLI + run: python scripts/build_wheel.py --version "${{ inputs.version }}" --skip-sdist --clean + shell: bash + + - uses: actions/upload-artifact@v4 + with: + name: wheel-${{ matrix.os }} + path: dist/*.whl + if-no-files-found: error + + publish: + needs: build-wheels + runs-on: ubuntu-latest + environment: production + permissions: + contents: write + pull-requests: write + env: + VERSION: ${{ inputs.version }} + steps: + - uses: actions/checkout@v4 + if: inputs.push_directly + with: + fetch-depth: 0 + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - uses: actions/checkout@v4 + if: ${{ !inputs.push_directly }} + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Update version files + run: python scripts/update_version.py "$VERSION" + + - name: Read CLI version from code + id: cli_version + run: | + CLI_VERSION=$(python -c "import re; print(re.search(r'__cli_version__ = \"([^\"]+)\"', open('src/claude_agent_sdk/_cli_version.py').read()).group(1))") + echo "cli_version=$CLI_VERSION" >> $GITHUB_OUTPUT + + - uses: actions/download-artifact@v4 + with: + path: dist + pattern: wheel-* + merge-multiple: true + + - name: Build sdist and publish to PyPI + run: | + pip install build twine + python -m build --sdist + twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + + - name: Configure git + run: | + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + + - name: Commit version changes + run: | + git add pyproject.toml src/claude_agent_sdk/_version.py + git commit -m "chore: release v$VERSION" + + - name: Update changelog with Claude + continue-on-error: true + uses: anthropics/claude-code-action@v1 + with: + prompt: "/generate-changelog new version: ${{ env.VERSION }}, old version: ${{ inputs.previous_tag }}" + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + github_token: ${{ secrets.GITHUB_TOKEN }} + claude_args: | + --model claude-opus-4-5 + --allowedTools 'Bash(git add:*),Bash(git commit:*),Edit' + + # Direct push flow (auto-release) + - name: Push to main + if: inputs.push_directly + run: git push origin main + + - name: Create tag and GitHub Release + if: inputs.push_directly + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git tag -a "v$VERSION" -m "Release v$VERSION" + git push origin "v$VERSION" + + awk -v ver="$VERSION" '/^## / { if (found) exit; if ($2 == ver) found=1; next } found { print }' CHANGELOG.md > release_notes.md + echo -e "\n---\n\n**PyPI:** https://pypi.org/project/claude-agent-sdk/$VERSION/\n\n\`\`\`bash\npip install claude-agent-sdk==$VERSION\n\`\`\`" >> release_notes.md + + gh release create "v$VERSION" --title "v$VERSION" --notes-file release_notes.md + + # PR flow (manual publish) + - name: Create release branch + if: ${{ !inputs.push_directly }} + run: | + BRANCH_NAME="release/v$VERSION" + echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV + git checkout -b "$BRANCH_NAME" + + - name: Push branch and create PR + if: ${{ !inputs.push_directly }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git push origin "$BRANCH_NAME" + + PR_BODY="This PR updates the version to $VERSION after publishing to PyPI. + + ## Changes + - Updated version in \`pyproject.toml\` to $VERSION + - Updated version in \`src/claude_agent_sdk/_version.py\` to $VERSION + - Updated \`CHANGELOG.md\` with release notes + + ## Release Information + - Published to PyPI: https://pypi.org/project/claude-agent-sdk/$VERSION/ + - Bundled CLI version: ${{ steps.cli_version.outputs.cli_version }} + - Install with: \`pip install claude-agent-sdk==$VERSION\` + + 🤖 Generated by GitHub Actions" + + gh pr create \ + --title "chore: release v$VERSION" \ + --body "$PR_BODY" \ + --base main \ + --head "$BRANCH_NAME" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b8b7e936..5be99544 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,7 @@ on: description: 'Package version to publish (e.g., 0.1.4)' required: true type: string + jobs: test: runs-on: ubuntu-latest @@ -56,167 +57,26 @@ jobs: run: | mypy src/ - build-wheels: - needs: [test, lint] - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest] - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build twine wheel - shell: bash - - - name: Build wheel with bundled CLI - run: | - python scripts/build_wheel.py \ - --version "${{ github.event.inputs.version }}" \ - --skip-sdist \ - --clean - shell: bash - - - name: Upload wheel artifact - uses: actions/upload-artifact@v4 - with: - name: wheel-${{ matrix.os }} - path: dist/*.whl - if-no-files-found: error - compression-level: 0 - - publish: - needs: [build-wheels] + get-previous-tag: runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - + outputs: + previous_tag: ${{ steps.previous_tag.outputs.previous_tag }} steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 # Fetch all history including tags for changelog generation - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Set version - id: version - run: | - VERSION="${{ github.event.inputs.version }}" - echo "VERSION=$VERSION" >> $GITHUB_ENV - echo "version=$VERSION" >> $GITHUB_OUTPUT - - - name: Update version - run: | - python scripts/update_version.py "${{ env.VERSION }}" - - - name: Read CLI version from code - id: cli_version - run: | - CLI_VERSION=$(python -c "import re; print(re.search(r'__cli_version__ = \"([^\"]+)\"', open('src/claude_agent_sdk/_cli_version.py').read()).group(1))") - echo "cli_version=$CLI_VERSION" >> $GITHUB_OUTPUT - echo "Bundled CLI version: $CLI_VERSION" - - - name: Download all wheel artifacts - uses: actions/download-artifact@v4 - with: - path: dist - pattern: wheel-* - merge-multiple: true - - - name: Install build dependencies - run: | - python -m pip install --upgrade pip - pip install build twine - - - name: Build source distribution - run: python -m build --sdist - - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - twine upload dist/* - echo "Package published to PyPI" - echo "Install with: pip install claude-agent-sdk==${{ env.VERSION }}" - - - name: Get previous release tag - id: previous_tag - run: | - PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT - echo "Previous release: $PREVIOUS_TAG" - - - name: Create release branch and commit version changes - run: | - # Create a new branch for the version update - BRANCH_NAME="release/v${{ env.VERSION }}" - echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV - - # Configure git - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - - # Create and switch to new branch - git checkout -b "$BRANCH_NAME" - - # Commit version changes - git add pyproject.toml src/claude_agent_sdk/_version.py - git commit -m "chore: release v${{ env.VERSION }}" - - - name: Update changelog with Claude - continue-on-error: true - uses: anthropics/claude-code-action@v1 - with: - prompt: "/generate-changelog new version: ${{ env.VERSION }}, old version: ${{ steps.previous_tag.outputs.previous_tag }}" - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - github_token: ${{ secrets.GITHUB_TOKEN }} - claude_args: | - --model claude-opus-4-5 - --allowedTools 'Bash(git add:*),Bash(git commit:*),Edit' - - - name: Push branch and create PR - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Push the branch with all commits - git push origin "${{ env.BRANCH_NAME }}" - - # Create PR using GitHub CLI - PR_BODY="This PR updates the version to ${{ env.VERSION }} after publishing to PyPI. - - ## Changes - - Updated version in \`pyproject.toml\` to ${{ env.VERSION }} - - Updated version in \`src/claude_agent_sdk/_version.py\` to ${{ env.VERSION }} - - Updated \`CHANGELOG.md\` with release notes - - ## Release Information - - Published to PyPI: https://pypi.org/project/claude-agent-sdk/${{ env.VERSION }}/ - - Bundled CLI version: ${{ steps.cli_version.outputs.cli_version }} - - Install with: \`pip install claude-agent-sdk==${{ env.VERSION }}\` - - 🤖 Generated by GitHub Actions" - - PR_URL=$(gh pr create \ - --title "chore: release v${{ env.VERSION }}" \ - --body "$PR_BODY" \ - --base main \ - --head "${{ env.BRANCH_NAME }}") + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - echo "PR created: $PR_URL" + - name: Get previous release tag + id: previous_tag + run: | + PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT + + release: + needs: [test, lint, get-previous-tag] + uses: ./.github/workflows/build-and-publish.yml + with: + version: ${{ github.event.inputs.version }} + previous_tag: ${{ needs.get-previous-tag.outputs.previous_tag }} + push_directly: false + secrets: inherit