From c134a8201bdab8aa00ddabadcf9f94253a5fb635 Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Thu, 21 May 2026 21:24:05 -0400 Subject: [PATCH 1/8] feat: auto-bump version on staging and prod deploys - Patch version bumped in deploy-staging.yaml before calling the Render hook, ensuring staging always runs the version shown on the dev branch - Minor version bumped in deploy-prod.yaml after syncing dev to main, with patch reset to 0 and the bump synced back to dev afterward to prevent branch divergence on subsequent deploys --- .github/workflows/deploy-prod.yaml | 87 +++++++++++++++++++++++++-- .github/workflows/deploy-staging.yaml | 54 +++++++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 1c48ce7..729edc4 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -40,10 +40,89 @@ jobs: - name: Push updated main branch run: git push origin main + # Bump minor version on main + bump-minor-version: + runs-on: ubuntu-latest + needs: sync-dev-to-main + if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} + permissions: + contents: write + steps: + - name: Checkout main + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: main + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Bump minor version + run: | + python3 - <<'EOF' + import re + + with open("pyproject.toml") as f: + content = f.read() + + match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) + major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) + new_version = f"{major}.{minor + 1}.0" + + new_content = re.sub( + r'^version = "\d+\.\d+\.\d+"', + f'version = "{new_version}"', + content, + count=1, + flags=re.MULTILINE, + ) + + with open("pyproject.toml", "w") as f: + f.write(new_content) + + print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") + EOF + + - name: Commit and push + run: | + git add pyproject.toml + git diff --cached --quiet && exit 0 + git commit -m "chore: bump minor version [skip ci]" + git push origin main + + # Sync minor version bump back to dev so dev and main stay on the same commit + sync-minor-to-dev: + runs-on: ubuntu-latest + needs: bump-minor-version + if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} + permissions: + contents: write + steps: + - name: Checkout dev + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: dev + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fast-forward dev to main + run: | + git fetch origin main + git merge --ff-only origin/main + git push origin dev + # Build and push Docker images push-to-dockerhub: runs-on: ubuntu-latest - needs: sync-dev-to-main + needs: bump-minor-version if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} env: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} @@ -84,7 +163,7 @@ jobs: # Push to GitHub Container Registry push-to-ghcr: runs-on: ubuntu-latest - needs: sync-dev-to-main + needs: bump-minor-version if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} permissions: contents: read @@ -119,7 +198,7 @@ jobs: # Deploy to Staging deploy-to-staging: runs-on: ubuntu-latest - needs: [sync-dev-to-main, push-to-dockerhub, push-to-ghcr] + needs: [bump-minor-version, push-to-dockerhub, push-to-ghcr] if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} env: RENDER_DEPLOY_HOOK_URL_DEV: ${{ secrets.RENDER_DEPLOY_HOOK_URL_DEV }} @@ -143,7 +222,7 @@ jobs: # Deploy to Production deploy-to-prod: runs-on: ubuntu-latest - needs: [sync-dev-to-main, push-to-dockerhub, push-to-ghcr, deploy-to-staging] + needs: [bump-minor-version, push-to-dockerhub, push-to-ghcr, deploy-to-staging, sync-minor-to-dev] if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} env: RENDER_DEPLOY_HOOK_URL_PROD: ${{ secrets.RENDER_DEPLOY_HOOK_URL_PROD }} diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 10813b8..868319e 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -6,11 +6,17 @@ on: types: - completed +concurrency: + group: deploy-staging-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: false + jobs: # Deploy to staging environment deploy-staging: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' }} + permissions: + contents: write env: RENDER_DEPLOY_HOOK_URL_DEV: ${{ secrets.RENDER_DEPLOY_HOOK_URL_DEV }} steps: @@ -21,6 +27,54 @@ jobs: exit 1 fi + - name: Checkout dev + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: dev + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Pull latest dev + run: git pull --rebase origin dev + + - name: Bump patch version + run: | + python3 - <<'EOF' + import re + + with open("pyproject.toml") as f: + content = f.read() + + match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) + major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) + new_version = f"{major}.{minor}.{patch + 1}" + + new_content = re.sub( + r'^version = "\d+\.\d+\.\d+"', + f'version = "{new_version}"', + content, + count=1, + flags=re.MULTILINE, + ) + + with open("pyproject.toml", "w") as f: + f.write(new_content) + + print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") + EOF + + - name: Commit and push + run: | + git add pyproject.toml + git diff --cached --quiet && exit 0 + git commit -m "chore: bump patch version [skip ci]" + git push origin dev + - name: Deploy to Staging Environment run: | echo "🚀 Deploying to Staging..." From a7e6fc1a23b8e4e9295159aed7ddc65c4c647239 Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 12:05:29 -0400 Subject: [PATCH 2/8] fix: harden auto-version-bump deploy workflows - staging: guard against feedback loop by skipping bot bump commits, checkout exact validated SHA instead of rebasing on latest dev, verify dev HEAD matches before bumping, push via HEAD:dev from detached HEAD - prod: add error handler with recovery message to sync-minor-to-dev fast-forward step, add missing permissions/contents:write to sync-dev-to-main, remove no-op git merge --abort after --ff-only failure --- .github/workflows/deploy-prod.yaml | 9 +++++++-- .github/workflows/deploy-staging.yaml | 20 ++++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 729edc4..7971918 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -8,6 +8,8 @@ jobs: sync-dev-to-main: runs-on: ubuntu-latest if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} + permissions: + contents: write steps: - name: Checkout repository uses: actions/checkout@v4 @@ -33,7 +35,6 @@ jobs: git merge --ff-only origin/dev || { echo "❌ Fast-forward merge failed. Manual conflict resolution required." echo "Please ensure dev branch is ahead of main with no conflicts." - git merge --abort exit 1 } @@ -116,7 +117,11 @@ jobs: - name: Fast-forward dev to main run: | git fetch origin main - git merge --ff-only origin/main + git merge --ff-only origin/main || { + echo "❌ Fast-forward merge of main into dev failed. dev has diverged from main." + echo "Please manually fast-forward dev to main before retrying the production deployment." + exit 1 + } git push origin dev # Build and push Docker images diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 868319e..fde0f8b 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -14,7 +14,7 @@ jobs: # Deploy to staging environment deploy-staging: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' }} + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' && !startsWith(github.event.workflow_run.head_commit.message, 'chore: bump') }} permissions: contents: write env: @@ -27,11 +27,11 @@ jobs: exit 1 fi - - name: Checkout dev + - name: Checkout validated commit uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - ref: dev + ref: ${{ github.event.workflow_run.head_sha }} fetch-depth: 0 - name: Configure Git @@ -39,8 +39,16 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Pull latest dev - run: git pull --rebase origin dev + - name: Verify dev HEAD matches validated SHA + run: | + git fetch origin dev + DEV_HEAD=$(git rev-parse origin/dev) + VALIDATED_SHA="${{ github.event.workflow_run.head_sha }}" + if [ "$DEV_HEAD" != "$VALIDATED_SHA" ]; then + echo "❌ dev has advanced to $DEV_HEAD beyond the validated SHA $VALIDATED_SHA." + echo "Skipping deployment to avoid deploying unvalidated commits." + exit 1 + fi - name: Bump patch version run: | @@ -73,7 +81,7 @@ jobs: git add pyproject.toml git diff --cached --quiet && exit 0 git commit -m "chore: bump patch version [skip ci]" - git push origin dev + git push origin HEAD:dev - name: Deploy to Staging Environment run: | From 18e2d2ae1c7582c39874fdb8ae5ff6fc9accd23b Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 12:09:11 -0400 Subject: [PATCH 3/8] fix: quote if condition in deploy-staging to escape YAML tag character --- .github/workflows/deploy-staging.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index fde0f8b..6904573 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -14,7 +14,7 @@ jobs: # Deploy to staging environment deploy-staging: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' && !startsWith(github.event.workflow_run.head_commit.message, 'chore: bump') }} + if: "${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'dev' && !startsWith(github.event.workflow_run.head_commit.message, 'chore: bump') }}" permissions: contents: write env: From 2de12053d49b965b3c98d2111ee21f1eed2d94cc Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 12:45:40 -0400 Subject: [PATCH 4/8] fix: deploy before version bump, add regex guard to bump scripts --- .github/workflows/deploy-prod.yaml | 2 ++ .github/workflows/deploy-staging.yaml | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 7971918..20df704 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -70,6 +70,8 @@ jobs: content = f.read() match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) + if not match: + raise SystemExit('version = "x.y.z" not found in pyproject.toml') major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) new_version = f"{major}.{minor + 1}.0" diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 6904573..4360e99 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -59,6 +59,8 @@ jobs: content = f.read() match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) + if not match: + raise SystemExit('version = "x.y.z" not found in pyproject.toml') major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) new_version = f"{major}.{minor}.{patch + 1}" @@ -76,13 +78,6 @@ jobs: print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") EOF - - name: Commit and push - run: | - git add pyproject.toml - git diff --cached --quiet && exit 0 - git commit -m "chore: bump patch version [skip ci]" - git push origin HEAD:dev - - name: Deploy to Staging Environment run: | echo "🚀 Deploying to Staging..." @@ -91,3 +86,10 @@ jobs: exit 1 } echo "✅ Staging deployment completed successfully!" + + - name: Commit and push + run: | + git add pyproject.toml + git diff --cached --quiet && exit 0 + git commit -m "chore: bump patch version [skip ci]" + git push origin HEAD:dev From 5fedb4d7996501d56b43dae4cccca4b08eccc4d0 Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 12:49:33 -0400 Subject: [PATCH 5/8] fix: bump and push version before deploying to staging --- .github/workflows/deploy-staging.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 4360e99..5843311 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -78,6 +78,13 @@ jobs: print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") EOF + - name: Commit and push + run: | + git add pyproject.toml + git diff --cached --quiet && exit 0 + git commit -m "chore: bump patch version [skip ci]" + git push origin HEAD:dev + - name: Deploy to Staging Environment run: | echo "🚀 Deploying to Staging..." @@ -86,10 +93,3 @@ jobs: exit 1 } echo "✅ Staging deployment completed successfully!" - - - name: Commit and push - run: | - git add pyproject.toml - git diff --cached --quiet && exit 0 - git commit -m "chore: bump patch version [skip ci]" - git push origin HEAD:dev From a7a5ae946d7cdcaaaea886519191ec233aff73b4 Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 18:30:50 -0400 Subject: [PATCH 6/8] fix: extract shared bump_version script, exit 0 on stale SHA skip --- .github/workflows/deploy-prod.yaml | 27 +-------------------- .github/workflows/deploy-staging.yaml | 32 +++--------------------- scripts/bump_version.py | 35 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 55 deletions(-) create mode 100644 scripts/bump_version.py diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 20df704..21b69b1 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -62,32 +62,7 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" - name: Bump minor version - run: | - python3 - <<'EOF' - import re - - with open("pyproject.toml") as f: - content = f.read() - - match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) - if not match: - raise SystemExit('version = "x.y.z" not found in pyproject.toml') - major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) - new_version = f"{major}.{minor + 1}.0" - - new_content = re.sub( - r'^version = "\d+\.\d+\.\d+"', - f'version = "{new_version}"', - content, - count=1, - flags=re.MULTILINE, - ) - - with open("pyproject.toml", "w") as f: - f.write(new_content) - - print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") - EOF + run: python3 scripts/bump_version.py minor - name: Commit and push run: | diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 5843311..02f4876 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -45,38 +45,12 @@ jobs: DEV_HEAD=$(git rev-parse origin/dev) VALIDATED_SHA="${{ github.event.workflow_run.head_sha }}" if [ "$DEV_HEAD" != "$VALIDATED_SHA" ]; then - echo "❌ dev has advanced to $DEV_HEAD beyond the validated SHA $VALIDATED_SHA." - echo "Skipping deployment to avoid deploying unvalidated commits." - exit 1 + echo "⚠️ dev has advanced to $DEV_HEAD beyond the validated SHA $VALIDATED_SHA. Skipping." + exit 0 fi - name: Bump patch version - run: | - python3 - <<'EOF' - import re - - with open("pyproject.toml") as f: - content = f.read() - - match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) - if not match: - raise SystemExit('version = "x.y.z" not found in pyproject.toml') - major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) - new_version = f"{major}.{minor}.{patch + 1}" - - new_content = re.sub( - r'^version = "\d+\.\d+\.\d+"', - f'version = "{new_version}"', - content, - count=1, - flags=re.MULTILINE, - ) - - with open("pyproject.toml", "w") as f: - f.write(new_content) - - print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") - EOF + run: python3 scripts/bump_version.py patch - name: Commit and push run: | diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 0000000..c07c677 --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +import re +import sys + +if len(sys.argv) != 2 or sys.argv[1] not in ("patch", "minor"): + raise SystemExit("Usage: bump_version.py [patch|minor]") + +bump_type = sys.argv[1] + +with open("pyproject.toml") as f: + content = f.read() + +match = re.search(r'^version = "(\d+)\.(\d+)\.(\d+)"', content, re.MULTILINE) +if not match: + raise SystemExit('version = "x.y.z" not found in pyproject.toml') + +major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) + +if bump_type == "patch": + new_version = f"{major}.{minor}.{patch + 1}" +else: + new_version = f"{major}.{minor + 1}.0" + +new_content = re.sub( + r'^version = "\d+\.\d+\.\d+"', + f'version = "{new_version}"', + content, + count=1, + flags=re.MULTILINE, +) + +with open("pyproject.toml", "w") as f: + f.write(new_content) + +print(f"Bumped: {major}.{minor}.{patch} -> {new_version}") From b5f7a2590a650427cd38e40ed98a87df2cfbd41d Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 20:13:25 -0400 Subject: [PATCH 7/8] fix: update uv.lock alongside pyproject.toml in version bump commits --- .github/workflows/deploy-prod.yaml | 7 +++++-- .github/workflows/deploy-staging.yaml | 7 +++++-- scripts/bump_version.py | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 21b69b1..82f4f23 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -62,11 +62,14 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" - name: Bump minor version - run: python3 scripts/bump_version.py minor + run: | + pip install uv + python3 scripts/bump_version.py minor + uv lock - name: Commit and push run: | - git add pyproject.toml + git add pyproject.toml uv.lock git diff --cached --quiet && exit 0 git commit -m "chore: bump minor version [skip ci]" git push origin main diff --git a/.github/workflows/deploy-staging.yaml b/.github/workflows/deploy-staging.yaml index 02f4876..555489d 100644 --- a/.github/workflows/deploy-staging.yaml +++ b/.github/workflows/deploy-staging.yaml @@ -50,11 +50,14 @@ jobs: fi - name: Bump patch version - run: python3 scripts/bump_version.py patch + run: | + pip install uv + python3 scripts/bump_version.py patch + uv lock - name: Commit and push run: | - git add pyproject.toml + git add pyproject.toml uv.lock git diff --cached --quiet && exit 0 git commit -m "chore: bump patch version [skip ci]" git push origin HEAD:dev diff --git a/scripts/bump_version.py b/scripts/bump_version.py index c07c677..6e1d1ff 100644 --- a/scripts/bump_version.py +++ b/scripts/bump_version.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +"""Bump the project version in pyproject.toml. Usage: bump_version.py [patch|minor].""" + import re import sys From 696a839c010290169c93601c7f40e455b907c666 Mon Sep 17 00:00:00 2001 From: aditeyabaral Date: Sun, 24 May 2026 20:25:58 -0400 Subject: [PATCH 8/8] fix: gate staging deploy on sync-minor-to-dev to ensure dev is up to date --- .github/workflows/deploy-prod.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 82f4f23..324cc47 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -183,7 +183,7 @@ jobs: # Deploy to Staging deploy-to-staging: runs-on: ubuntu-latest - needs: [bump-minor-version, push-to-dockerhub, push-to-ghcr] + needs: [bump-minor-version, push-to-dockerhub, push-to-ghcr, sync-minor-to-dev] if: ${{ contains(fromJson(vars.PROD_DEPLOYMENT_ALLOWED_USERS), github.actor) }} env: RENDER_DEPLOY_HOOK_URL_DEV: ${{ secrets.RENDER_DEPLOY_HOOK_URL_DEV }}