Create Release #315
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: Create Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version_type: | |
| description: "Type of version bump" | |
| required: true | |
| type: choice | |
| options: | |
| - major | |
| - minor | |
| - patch | |
| default: patch | |
| deploy_target: | |
| description: "Initial deployment target" | |
| required: true | |
| type: choice | |
| options: | |
| - all | |
| - staging | |
| - prod | |
| - none | |
| default: all | |
| permissions: | |
| contents: read | |
| jobs: | |
| create-branch: | |
| permissions: | |
| contents: write | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| new_version: ${{ steps.new-version.outputs.new_version }} | |
| branch_name: ${{ steps.new-version.outputs.branch_name }} | |
| steps: | |
| - name: Checkout main branch | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| ref: main | |
| fetch-depth: 0 | |
| # ACTIONS_TOKEN required to push to protected main branch | |
| token: ${{ secrets.ACTIONS_TOKEN || github.token }} | |
| - name: Set up Git | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Setup Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7 | |
| with: | |
| python-version: "3.13" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 | |
| with: | |
| version: "0.12.8" # keep in step with the ghcr.io/astral-sh/uv tag in both Dockerfiles | |
| - name: Get current version | |
| id: current-version | |
| run: | | |
| CURRENT_VERSION=$(awk -F'"' '/^version = / {print $2}' pyproject.toml) | |
| if [ -z "$CURRENT_VERSION" ]; then | |
| echo "Could not find version in pyproject.toml" | |
| exit 1 | |
| fi | |
| echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| echo "Current version: $CURRENT_VERSION" | |
| - name: Calculate new version | |
| id: new-version | |
| run: | | |
| CURRENT_VERSION="${{ steps.current-version.outputs.current_version }}" | |
| VERSION_TYPE="${{ inputs.version_type }}" | |
| # Split version into major, minor, and patch | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| # Increment appropriate version component | |
| if [ "$VERSION_TYPE" = "major" ]; then | |
| MAJOR=$((MAJOR + 1)) | |
| MINOR=0 | |
| PATCH=0 | |
| elif [ "$VERSION_TYPE" = "minor" ]; then | |
| MINOR=$((MINOR + 1)) | |
| PATCH=0 | |
| else # patch | |
| PATCH=$((PATCH + 1)) | |
| fi | |
| NEW_VERSION="$MAJOR.$MINOR.$PATCH" | |
| BRANCH_NAME="release/$NEW_VERSION" | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT | |
| echo "New version: $NEW_VERSION" | |
| echo "Branch name: $BRANCH_NAME" | |
| - name: Check if branch already exists | |
| id: check-branch | |
| run: | | |
| BRANCH_NAME="${{ steps.new-version.outputs.branch_name }}" | |
| # Check if branch exists locally or remotely | |
| if git show-ref --verify --quiet refs/heads/$BRANCH_NAME || git show-ref --verify --quiet refs/remotes/origin/$BRANCH_NAME; then | |
| echo "Branch $BRANCH_NAME already exists" | |
| echo "branch_exists=true" >> $GITHUB_OUTPUT | |
| exit 1 | |
| else | |
| echo "Branch $BRANCH_NAME does not exist, proceeding..." | |
| echo "branch_exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Update main branch version | |
| run: | | |
| CURRENT_VERSION="${{ steps.current-version.outputs.current_version }}" | |
| NEW_VERSION="${{ steps.new-version.outputs.new_version }}" | |
| echo "Updating main branch version from $CURRENT_VERSION to $NEW_VERSION" | |
| # Update version in pyproject.toml | |
| sed -i "s/^version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/" pyproject.toml | |
| # Sync uv.lock file with the new version | |
| echo "Syncing uv.lock file..." | |
| uv sync --all-extras | |
| # Commit and push the version update to main | |
| git add pyproject.toml uv.lock | |
| git commit -m "Release v$NEW_VERSION" | |
| git push origin main | |
| echo "✅ Main branch updated to version $NEW_VERSION with synced lock file" | |
| - name: Create release branch | |
| run: | | |
| BRANCH_NAME="${{ steps.new-version.outputs.branch_name }}" | |
| echo "Creating release branch: $BRANCH_NAME from updated main" | |
| # Create and checkout the release branch from current HEAD (updated main) | |
| git checkout -b "$BRANCH_NAME" | |
| # Push the release branch | |
| git push origin "$BRANCH_NAME" | |
| echo "✅ Release branch $BRANCH_NAME created and pushed" | |
| create-tag: | |
| needs: create-branch | |
| permissions: | |
| contents: write | |
| uses: ./.github/workflows/tag-release.yml | |
| with: | |
| branch_ref: ${{ needs.create-branch.outputs.branch_name }} | |
| secrets: inherit | |
| # ============================================================================ | |
| # DEPLOYMENTS | |
| # Each deployment is dispatched as an independent workflow run for better | |
| # monitoring. workflow_dispatch is exempt from GitHub's "GITHUB_TOKEN events | |
| # don't trigger workflows" rule, so this works with the default token too; | |
| # ACTIONS_TOKEN is preferred when available. | |
| # ============================================================================ | |
| deploy-staging: | |
| needs: [create-tag] | |
| if: inputs.deploy_target == 'staging' || inputs.deploy_target == 'all' | |
| permissions: | |
| actions: write | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| run_id: ${{ steps.dispatch.outputs.run_id }} | |
| steps: | |
| - name: Dispatch staging deployment | |
| id: dispatch | |
| env: | |
| GH_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token }} | |
| run: | | |
| echo "🚀 Dispatching staging deployment from tag ${{ needs.create-tag.outputs.tag_name }}" | |
| # Trigger the workflow | |
| gh workflow run staging.yml \ | |
| --repo ${{ github.repository }} \ | |
| --ref ${{ needs.create-tag.outputs.tag_name }} | |
| # Wait briefly for the run to be created | |
| sleep 5 | |
| # Get the run ID of the triggered workflow (dispatched at the tag, | |
| # so filtering by the tag ref avoids grabbing an unrelated run) | |
| RUN_ID=$(gh run list \ | |
| --repo ${{ github.repository }} \ | |
| --workflow=staging.yml \ | |
| --branch ${{ needs.create-tag.outputs.tag_name }} \ | |
| --limit 1 \ | |
| --json databaseId \ | |
| --jq '.[0].databaseId') | |
| echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT | |
| echo "✅ Staging deployment dispatched: https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" | |
| deploy-prod: | |
| needs: [create-tag, deploy-staging] | |
| if: | | |
| !cancelled() && | |
| needs.create-tag.result == 'success' && | |
| (inputs.deploy_target == 'prod' || inputs.deploy_target == 'all') && | |
| (needs.deploy-staging.result == 'success' || needs.deploy-staging.result == 'skipped') | |
| permissions: | |
| actions: write | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| run_id: ${{ steps.dispatch.outputs.run_id }} | |
| steps: | |
| - name: Wait before prod deployment | |
| if: inputs.deploy_target == 'all' | |
| run: | | |
| echo "⏳ Waiting 30 seconds before dispatching prod deployment..." | |
| echo " (prod.yml has concurrency group - will queue if staging still running)" | |
| sleep 30 | |
| - name: Dispatch prod deployment | |
| id: dispatch | |
| env: | |
| GH_TOKEN: ${{ secrets.ACTIONS_TOKEN || github.token }} | |
| run: | | |
| echo "🚀 Dispatching prod deployment from tag ${{ needs.create-tag.outputs.tag_name }}" | |
| # Trigger the workflow | |
| gh workflow run prod.yml \ | |
| --repo ${{ github.repository }} \ | |
| --ref ${{ needs.create-tag.outputs.tag_name }} | |
| # Wait briefly for the run to be created | |
| sleep 5 | |
| # Get the run ID of the triggered workflow (dispatched at the tag, | |
| # so filtering by the tag ref avoids grabbing an unrelated run) | |
| RUN_ID=$(gh run list \ | |
| --repo ${{ github.repository }} \ | |
| --workflow=prod.yml \ | |
| --branch ${{ needs.create-tag.outputs.tag_name }} \ | |
| --limit 1 \ | |
| --json databaseId \ | |
| --jq '.[0].databaseId') | |
| echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT | |
| echo "✅ Prod deployment dispatched: https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" | |
| create-summary: | |
| needs: [create-branch, create-tag, deploy-staging, deploy-prod] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Create summary | |
| env: | |
| STAGING_RUN_ID: ${{ needs.deploy-staging.outputs.run_id }} | |
| PROD_RUN_ID: ${{ needs.deploy-prod.outputs.run_id }} | |
| run: | | |
| echo "## Release Created" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Version | ${{ needs.create-tag.outputs.version }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Tag | \`${{ needs.create-tag.outputs.tag_name }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Branch | \`${{ needs.create-branch.outputs.branch_name }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Type | ${{ inputs.version_type }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Deploy Target | ${{ inputs.deploy_target }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Release URL:** ${{ needs.create-tag.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Show deployment links | |
| if [ "${{ inputs.deploy_target }}" != "none" ]; then | |
| echo "### Deployments" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Deployments run as **separate workflow runs** for independent monitoring." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ -n "$STAGING_RUN_ID" ]; then | |
| echo "- **Staging**: [View Run](https://github.com/${{ github.repository }}/actions/runs/$STAGING_RUN_ID)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ -n "$PROD_RUN_ID" ]; then | |
| echo "- **Production**: [View Run](https://github.com/${{ github.repository }}/actions/runs/$PROD_RUN_ID)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Monitor all deployments: \`gh run list --workflow=staging.yml\` / \`gh run list --workflow=prod.yml\`" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ inputs.deploy_target }}" = "staging" ]; then | |
| echo "### Next Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Staging deployment has been triggered from tag ${{ needs.create-tag.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Test the release in staging environment" >> $GITHUB_STEP_SUMMARY | |
| echo "3. When ready for production: \`gh workflow run prod.yml --ref ${{ needs.create-tag.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ inputs.deploy_target }}" = "prod" ]; then | |
| echo "### Next Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Production deployment has been triggered from tag ${{ needs.create-tag.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "2. The git tag is available for future deployments" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ inputs.deploy_target }}" = "all" ]; then | |
| echo "### Next Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Staging deployment triggered from tag ${{ needs.create-tag.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "2. Production deployment will follow (queued via concurrency group)" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### Next Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "1. Release tag ${{ needs.create-tag.outputs.tag_name }} created (no deployment)" >> $GITHUB_STEP_SUMMARY | |
| echo "2. To deploy to staging: \`gh workflow run staging.yml --ref ${{ needs.create-tag.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "3. To deploy to production: \`gh workflow run prod.yml --ref ${{ needs.create-tag.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY | |
| fi |