Prepare Release #29
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: Prepare Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Base version number (e.g., 0.10.0)' | |
| required: true | |
| type: string | |
| release_type: | |
| description: 'Release type' | |
| required: true | |
| type: choice | |
| default: 'stable' | |
| options: | |
| - stable | |
| - alpha | |
| - beta | |
| - rc | |
| prerelease_number: | |
| description: 'Pre-release number (numeric only, e.g., "1" → PyPI: X.Y.Zb1, plugin: X.Y.Z-beta.1). Ignored for stable.' | |
| required: false | |
| type: string | |
| default: '1' | |
| ref: | |
| description: 'Branch to release from. Only used for pre-releases. Defaults to repo default branch.' | |
| required: false | |
| type: string | |
| default: '' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| prepare: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # Use ref for pre-releases so we can checkout a feature branch. | |
| # Use RELEASE_TOKEN so tags/releases created here trigger downstream workflows. | |
| ref: ${{ inputs.release_type != 'stable' && inputs.ref || '' }} | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| - name: Validate version format | |
| run: | | |
| if ! echo "${{ inputs.version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "Error: Version must be in format X.Y.Z (e.g., 0.10.0)" | |
| exit 1 | |
| fi | |
| - name: Validate pre-release number | |
| if: inputs.release_type != 'stable' | |
| run: | | |
| NUM="${{ inputs.prerelease_number }}" | |
| if ! echo "$NUM" | grep -qE '^[0-9]+$'; then | |
| echo "Error: Pre-release number must be numeric (e.g., 1, 2, 3), got: '$NUM'" | |
| exit 1 | |
| fi | |
| - name: Check for existing tag | |
| if: inputs.release_type != 'stable' | |
| env: | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| run: | | |
| BASE="${{ inputs.version }}" | |
| TYPE="${{ inputs.release_type }}" | |
| NUM="${{ inputs.prerelease_number }}" | |
| case "$TYPE" in | |
| alpha) PEP440="a" ;; | |
| beta) PEP440="b" ;; | |
| rc) PEP440="rc" ;; | |
| esac | |
| TAG="${BASE}${PEP440}${NUM}" | |
| if git ls-remote --tags origin "refs/tags/$TAG" | grep -q "$TAG"; then | |
| echo "Error: Tag '$TAG' already exists." | |
| echo " → https://github.com/${GITHUB_REPOSITORY}/releases/tag/$TAG" | |
| echo "" | |
| echo "If you genuinely want to re-release this version, delete the existing tag and release first, then run this workflow again." | |
| exit 1 | |
| fi | |
| - name: Compute version strings | |
| id: versions | |
| run: | | |
| BASE="${{ inputs.version }}" | |
| TYPE="${{ inputs.release_type }}" | |
| NUM="${{ inputs.prerelease_number }}" | |
| if [ "$TYPE" = "stable" ]; then | |
| PYPI_VERSION="${BASE}" | |
| PLUGIN_VERSION="${BASE}" | |
| IS_PRERELEASE="false" | |
| else | |
| case "$TYPE" in | |
| alpha) PEP440="a" ;; | |
| beta) PEP440="b" ;; | |
| rc) PEP440="rc" ;; | |
| esac | |
| PYPI_VERSION="${BASE}${PEP440}${NUM}" | |
| PLUGIN_VERSION="${BASE}-${TYPE}.${NUM}" | |
| IS_PRERELEASE="true" | |
| fi | |
| echo "pypi_version=$PYPI_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "plugin_version=$PLUGIN_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" | |
| echo "Computed versions:" | |
| echo " PyPI: $PYPI_VERSION" | |
| echo " Plugin: $PLUGIN_VERSION" | |
| echo " Prerelease: $IS_PRERELEASE" | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # --- Stable-only: create release branch --- | |
| - name: Create release branch | |
| if: steps.versions.outputs.is_prerelease == 'false' | |
| run: | | |
| VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| # Delete remote branch if it exists from a previous attempt | |
| git push origin --delete "release/${VERSION}" 2>/dev/null || true | |
| git checkout -b "release/${VERSION}" | |
| # --- Stable-only: update CHANGELOG --- | |
| - name: Update CHANGELOG.md | |
| if: steps.versions.outputs.is_prerelease == 'false' | |
| env: | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| run: | | |
| VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| DATE=$(date +%Y-%m-%d) | |
| # Create the new Unreleased section template | |
| UNRELEASED_TEMPLATE=$(printf '%s\n\n%s\n\n%s\n\n%s\n\n%s\n\n' \ | |
| '## [Unreleased]' \ | |
| '### Added' \ | |
| '### Changed' \ | |
| '### Fixed' \ | |
| '### Removed') | |
| # Read the current changelog | |
| CHANGELOG=$(cat CHANGELOG.md) | |
| # Check if there's an existing Unreleased section | |
| if grep -q "^## \[Unreleased\]" CHANGELOG.md; then | |
| # Replace [Unreleased] with the new version and date | |
| # First, extract everything after the Unreleased header until the next ## section | |
| # Then insert the new Unreleased section at the top | |
| # Use awk to do the replacement | |
| awk -v version="$VERSION" -v date="$DATE" -v template="$UNRELEASED_TEMPLATE" ' | |
| /^## \[Unreleased\]/ { | |
| print template | |
| print "## [" version "] - " date | |
| next | |
| } | |
| { print } | |
| ' CHANGELOG.md > CHANGELOG.md.tmp | |
| mv CHANGELOG.md.tmp CHANGELOG.md | |
| else | |
| # No Unreleased section exists - insert new version after the header | |
| # Find the first ## line and insert before it | |
| awk -v version="$VERSION" -v date="$DATE" -v template="$UNRELEASED_TEMPLATE" ' | |
| BEGIN { inserted = 0 } | |
| /^## \[/ && !inserted { | |
| print template | |
| print "## [" version "] - " date | |
| print "" | |
| inserted = 1 | |
| } | |
| { print } | |
| ' CHANGELOG.md > CHANGELOG.md.tmp | |
| mv CHANGELOG.md.tmp CHANGELOG.md | |
| fi | |
| # Update the version links at the bottom of the CHANGELOG | |
| # Update the Unreleased link to compare from the new version | |
| sed -i "s|\[Unreleased\]: https://github.com/.*/compare/.*\.\.\.HEAD|[Unreleased]: https://github.com/${GITHUB_REPOSITORY}/compare/${VERSION}...HEAD|" CHANGELOG.md | |
| # Add the new version link if it doesn't exist (insert after Unreleased link) | |
| if ! grep -q "^\[${VERSION}\]:" CHANGELOG.md; then | |
| # Find the previous version from the changelog (first version after Unreleased) | |
| PREV_VERSION=$(grep -oP '^\[[\d.]+\]:' CHANGELOG.md | head -1 | tr -d '[]:' || echo "") | |
| if [ -n "$PREV_VERSION" ]; then | |
| VERSION_LINK="[${VERSION}]: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${VERSION}" | |
| sed -i "/^\[Unreleased\]:/a ${VERSION_LINK}" CHANGELOG.md | |
| fi | |
| fi | |
| # --- Both: update pyproject.toml --- | |
| - name: Update pyproject.toml version | |
| run: | | |
| VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml | |
| # Verify the change | |
| grep "^version = " pyproject.toml | |
| # --- Both: update __init__.py version --- | |
| - name: Update __init__.py version | |
| run: | | |
| VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| sed -i "s/^__version__ = \".*\"/__version__ = \"$VERSION\"/" src/deepwork/__init__.py | |
| # Verify the change | |
| grep "^__version__" src/deepwork/__init__.py | |
| # --- Both: update plugin.json files --- | |
| - name: Update plugin versions | |
| run: | | |
| VERSION="${{ steps.versions.outputs.plugin_version }}" | |
| # Update all Claude Code plugin versions | |
| for PLUGIN_FILE in $(find . -path '*/.claude-plugin/plugin.json'); do | |
| sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" "$PLUGIN_FILE" | |
| echo "Updated $PLUGIN_FILE:" | |
| grep '"version"' "$PLUGIN_FILE" | |
| done | |
| # --- Both: update marketplace.json --- | |
| - name: Update marketplace plugin versions | |
| run: | | |
| VERSION="${{ steps.versions.outputs.plugin_version }}" | |
| # Update the deepwork plugin version in marketplace.json | |
| # Use jq to update only the deepwork plugin entry's version | |
| jq --arg v "$VERSION" '(.plugins[] | select(.name == "deepwork")).version = $v' \ | |
| .claude-plugin/marketplace.json > .claude-plugin/marketplace.json.tmp | |
| mv .claude-plugin/marketplace.json.tmp .claude-plugin/marketplace.json | |
| echo "Updated .claude-plugin/marketplace.json:" | |
| jq '.plugins[] | select(.name == "deepwork") | .version' .claude-plugin/marketplace.json | |
| # --- Pre-release only: pin .mcp.json to pre-release PyPI version --- | |
| - name: Pin MCP server to pre-release version | |
| if: steps.versions.outputs.is_prerelease == 'true' | |
| run: | | |
| PYPI_VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| # Pin uvx to the specific pre-release version so the MCP server | |
| # uses the pre-release package instead of latest stable | |
| jq --arg v "$PYPI_VERSION" \ | |
| '.mcpServers.deepwork.args[0] = "deepwork==" + $v' \ | |
| plugins/claude/.mcp.json > plugins/claude/.mcp.json.tmp | |
| mv plugins/claude/.mcp.json.tmp plugins/claude/.mcp.json | |
| echo "Updated plugins/claude/.mcp.json:" | |
| cat plugins/claude/.mcp.json | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| with: | |
| version: "latest" | |
| enable-cache: true | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Update lock file | |
| run: uv sync | |
| # --- Stable: commit, push branch, create PR --- | |
| - name: Commit and push release branch | |
| if: steps.versions.outputs.is_prerelease == 'false' | |
| run: | | |
| VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| git add CHANGELOG.md pyproject.toml uv.lock .claude-plugin/marketplace.json \ | |
| src/deepwork/__init__.py \ | |
| $(find . -path '*/.claude-plugin/plugin.json') | |
| git commit -m "Release v${VERSION}" | |
| git push -u origin "release/${VERSION}" | |
| - name: Create Pull Request | |
| if: steps.versions.outputs.is_prerelease == 'false' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| gh pr create \ | |
| --title "${VERSION}" \ | |
| --body "## Release v${VERSION} | |
| This PR prepares the release of version ${VERSION}. | |
| ### Changes | |
| - Updated version in pyproject.toml | |
| - Updated plugin versions (Claude Code, Learning Agents) | |
| - Updated marketplace.json | |
| - Updated CHANGELOG.md | |
| - Updated uv.lock | |
| ### After Merge | |
| A GitHub Release will be automatically created when this PR is merged." \ | |
| --label "release" | |
| # --- Pre-release: commit, force-push to pre-release branch, tag, create GH Release --- | |
| - name: Commit and push to pre-release branch | |
| if: steps.versions.outputs.is_prerelease == 'true' | |
| run: | | |
| PYPI_VERSION="${{ steps.versions.outputs.pypi_version }}" | |
| # Commit version bumps on the current (source) branch | |
| git add pyproject.toml uv.lock plugins/claude/.mcp.json \ | |
| .claude-plugin/marketplace.json \ | |
| $(find . -path '*/.claude-plugin/plugin.json') | |
| git commit -m "Pre-release v${PYPI_VERSION}" | |
| # Force-push to the `pre-release` branch. | |
| # This is the branch users subscribe to via: | |
| # claude plugin marketplace add Unsupervisedcom/deepwork#pre-release | |
| git push origin HEAD:refs/heads/pre-release --force | |
| # Tag for the GitHub Release | |
| git tag -a "$PYPI_VERSION" -m "Pre-release $PYPI_VERSION" | |
| git push origin "$PYPI_VERSION" | |
| - name: Create GitHub Pre-release | |
| if: steps.versions.outputs.is_prerelease == 'true' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.versions.outputs.pypi_version }} | |
| name: ${{ steps.versions.outputs.pypi_version }} | |
| body: | | |
| Pre-release `${{ steps.versions.outputs.pypi_version }}` from branch `${{ inputs.ref || github.event.repository.default_branch }}`. | |
| **To use this pre-release**, subscribe to the pre-release plugin channel: | |
| ``` | |
| claude plugin marketplace add Unsupervisedcom/deepwork#pre-release | |
| claude plugin install deepwork@deepwork-plugins | |
| ``` | |
| draft: false | |
| prerelease: true | |
| token: ${{ secrets.RELEASE_TOKEN }} |