Create Release #224
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@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@v6 | |
| with: | |
| python-version: "3.13" | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 | |
| - 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 | |
| # Check if ACTIONS_TOKEN is available for dispatch-based deployments | |
| # Dispatch gives independent workflow runs for better monitoring | |
| # Reusable workflows are fallback when PAT not available | |
| check-deploy-method: | |
| needs: create-tag | |
| runs-on: ubuntu-latest | |
| outputs: | |
| use_dispatch: ${{ steps.check.outputs.use_dispatch }} | |
| steps: | |
| - name: Check for ACTIONS_TOKEN | |
| id: check | |
| run: | | |
| if [ "${{ secrets.ACTIONS_TOKEN != '' }}" = "true" ]; then | |
| echo "use_dispatch=true" >> $GITHUB_OUTPUT | |
| echo "✅ ACTIONS_TOKEN available - will dispatch independent workflow runs" | |
| else | |
| echo "use_dispatch=false" >> $GITHUB_OUTPUT | |
| echo "ℹ️ ACTIONS_TOKEN not set - using reusable workflows (bundled in this run)" | |
| fi | |
| # ============================================================================ | |
| # DISPATCH-BASED DEPLOYMENTS (when ACTIONS_TOKEN is available) | |
| # Each deployment runs as an independent workflow for better monitoring | |
| # ============================================================================ | |
| deploy-staging-dispatch: | |
| needs: [create-branch, create-tag, check-deploy-method] | |
| if: | | |
| needs.check-deploy-method.outputs.use_dispatch == 'true' && | |
| (inputs.deploy_target == 'staging' || inputs.deploy_target == 'all') | |
| 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 }} | |
| 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 | |
| RUN_ID=$(gh run list \ | |
| --repo ${{ github.repository }} \ | |
| --workflow=staging.yml \ | |
| --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-dispatch: | |
| needs: [create-branch, create-tag, check-deploy-method, deploy-staging-dispatch] | |
| if: | | |
| always() && | |
| needs.check-deploy-method.outputs.use_dispatch == 'true' && | |
| (inputs.deploy_target == 'prod' || inputs.deploy_target == 'all') && | |
| (needs.deploy-staging-dispatch.result == 'success' || needs.deploy-staging-dispatch.result == 'skipped') | |
| 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 }} | |
| 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 | |
| RUN_ID=$(gh run list \ | |
| --repo ${{ github.repository }} \ | |
| --workflow=prod.yml \ | |
| --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" | |
| # ============================================================================ | |
| # REUSABLE WORKFLOW DEPLOYMENTS (fallback when ACTIONS_TOKEN not available) | |
| # Deployments run as part of this workflow - less visibility but no PAT needed | |
| # ============================================================================ | |
| deploy-staging-reusable: | |
| needs: [create-branch, create-tag, check-deploy-method] | |
| if: | | |
| needs.check-deploy-method.outputs.use_dispatch == 'false' && | |
| (inputs.deploy_target == 'staging' || inputs.deploy_target == 'all') | |
| permissions: | |
| contents: read | |
| deployments: write | |
| id-token: write | |
| security-events: write | |
| uses: ./.github/workflows/staging.yml | |
| secrets: inherit | |
| deploy-prod-reusable: | |
| needs: [create-branch, create-tag, check-deploy-method, deploy-staging-reusable] | |
| if: | | |
| always() && | |
| needs.check-deploy-method.outputs.use_dispatch == 'false' && | |
| (inputs.deploy_target == 'prod' || inputs.deploy_target == 'all') && | |
| (needs.deploy-staging-reusable.result == 'success' || needs.deploy-staging-reusable.result == 'skipped') | |
| permissions: | |
| contents: read | |
| deployments: write | |
| id-token: write | |
| security-events: write | |
| uses: ./.github/workflows/prod.yml | |
| secrets: inherit | |
| create-summary: | |
| needs: | |
| [ | |
| create-branch, | |
| create-tag, | |
| check-deploy-method, | |
| deploy-staging-dispatch, | |
| deploy-prod-dispatch, | |
| deploy-staging-reusable, | |
| deploy-prod-reusable, | |
| ] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| steps: | |
| - name: Create summary | |
| env: | |
| USE_DISPATCH: ${{ needs.check-deploy-method.outputs.use_dispatch }} | |
| STAGING_RUN_ID: ${{ needs.deploy-staging-dispatch.outputs.run_id }} | |
| PROD_RUN_ID: ${{ needs.deploy-prod-dispatch.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 method and links | |
| if [ "$USE_DISPATCH" = "true" ]; then | |
| echo "### Deployments (Independent Workflows)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Deployments are running 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 | |
| else | |
| echo "### Deployments (Reusable Workflows)" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Deployments are running as **part of this workflow** (ACTIONS_TOKEN not configured)." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "To enable independent deployment tracking, set the \`ACTIONS_TOKEN\` secret." >> $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 |