Skip to content

Create Release

Create Release #290

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
- 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