github-actionsci-cdautomationdevopssemantic-versioningbranch-managementrelease-management

Automate Branch Promotions with GitHub Actions

Cristobal MitchellFounder of ReleaseRay··Updated February 28, 2026·5 min read
Share:

Stop manually creating PRs for branch promotions. Use these free GitHub Actions templates to automatically create staging and production PRs with semantic version prediction when CI passes.

If you're searching for github actions branch promotion or automate release github actions, you're in the right place. Copy-paste-ready workflows. No more manual release overhead.


The Problem: Why Manual Branch Promotions Don't Scale

Every week, someone on your team manually creates a PR from develop to staging. Someone manually writes release notes. Someone manually calculates the next version number and checks for breaking changes. Someone hopes nothing was forgotten. Usually it's the same someone. Often it's 11pm on a Thursday.

This is toil. And toil doesn't scale.

I've surveyed 500+ engineering teams. The numbers: 15–30 minutes per release for PR creation, 2–4 releases per week on average, 1–2 hours per week per team on release overhead. That's 52–104 hours per year that could be spent shipping features. You do the math.

Why Manual Version Bumping Fails

Human error is inevitable. A breaking change gets a PATCH bump because someone forgot to check. "Who was supposed to create the staging PR?" echoes in Slack. One release uses chore(release):, another uses fix(release):, and semantic-release gets confused. By the time someone creates the PR, develop has drifted and now there are merge conflicts. Automated GitHub Actions branch promotion removes these failure points.


Architecture: How Automated Branch Promotion Works

Here's the flow from feature merge to production release:

┌────────────────────────────────────────────────────────────┐
│ 1. Feature branch → PR → merge to develop                  │
└────────────────────────────────────────────────────────────┘
                              ↓
┌────────────────────────────────────────────────────────────┐
│ 2. CI runs (tests, lint, build) on develop                 │
└────────────────────────────────────────────────────────────┘
                              ↓
┌────────────────────────────────────────────────────────────┐
│ 3. CI passes ✅ → Auto-Promote workflow triggers           │
└────────────────────────────────────────────────────────────┘
                              ↓
┌────────────────────────────────────────────────────────────┐
│ 4. Workflow analyzes commits (feat, fix, BREAKING)        │
│    → Determines semantic type for squash merge             │
│    → Creates PR: develop → staging                         │
└────────────────────────────────────────────────────────────┘
                              ↓
┌────────────────────────────────────────────────────────────┐
│ 5. Team reviews automated PR, merges to staging            │
└────────────────────────────────────────────────────────────┘
                              ↓
┌────────────────────────────────────────────────────────────┐
│ 6. Production workflow: Predicts next version (v1.2.3)     │
│    → Creates PR: staging → main with release notes         │
└────────────────────────────────────────────────────────────┘

Key insight: The workflow analyzes conventional commits to preserve semantic meaning through squash merges. A promotion with feat: commits becomes feat(release):, so semantic-release correctly does a MINOR bump.


Get release management tips in your inbox

Practical guides on release notes, changelogs, and shipping better software. No spam, unsubscribe anytime.

TL;DR

Free GitHub Actions templates. They create staging PRs when develop CI passes, production PRs with semantic version prediction, detect breaking changes automatically, follow security best practices (SHA-pinned actions, minimal permissions), and include pre-merge checklists. Copy, paste, customize. Get the templates →


Complete Workflow: Staging Promotion (Copy-Paste Ready)

Here's the full auto-promote-staging.yml workflow. Copy it to .github/workflows/auto-promote-staging.yml and customize branch names and CI workflow name if needed.

# .github/workflows/auto-promote-staging.yml
# Learn more: https://releaseray.com/blog/automated-branch-promotions

name: Auto-Promote to Staging

on:
  push:
    branches: [develop]
  workflow_run:
    workflows: ["CI"]
    branches: [develop]
    types: [completed]

permissions:
  contents: write
  pull-requests: write
  issues: read

concurrency:
  group: promote-staging
  cancel-in-progress: false

jobs:
  create-promotion-pr:
    name: Create Staging Promotion PR
    runs-on: ubuntu-latest
    if: |
      github.event_name == 'push' ||
      (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: develop
          persist-credentials: true

      - name: Configure Git
        run: |
          git config user.name "Release Bot"
          git config user.email "bot@yourdomain.com"

      - name: Check if promotion is needed
        id: check_diff
        run: |
          git fetch origin staging:staging
          COMMITS_AHEAD=$(git rev-list --count staging..develop)
          echo "commits_ahead=$COMMITS_AHEAD" >> $GITHUB_OUTPUT
          if [ "$COMMITS_AHEAD" -eq "0" ]; then
            echo "needs_promotion=false" >> $GITHUB_OUTPUT
          else
            echo "needs_promotion=true" >> $GITHUB_OUTPUT
          fi

      - name: Analyze commits and determine promotion type
        if: steps.check_diff.outputs.needs_promotion == 'true'
        id: commits
        run: |
          FEAT_COUNT=$(git log staging..develop --pretty=format:"%s" --no-merges | grep -c "^feat" || true)
          FIX_COUNT=$(git log staging..develop --pretty=format:"%s" --no-merges | grep -c "^fix" || true)
          BREAKING_COUNT=$(git log staging..develop --pretty=format:"%s %b" --no-merges | grep -ci "BREAKING CHANGE\|^feat.*!\|^fix.*!" || true)
          echo "feat_count=$FEAT_COUNT" >> $GITHUB_OUTPUT
          echo "fix_count=$FIX_COUNT" >> $GITHUB_OUTPUT
          echo "breaking_count=$BREAKING_COUNT" >> $GITHUB_OUTPUT
          if [ "$BREAKING_COUNT" -gt "0" ]; then
            echo "commit_type=feat!" >> $GITHUB_OUTPUT
            echo "commit_desc=promote develop to staging with breaking changes" >> $GITHUB_OUTPUT
          elif [ "$FEAT_COUNT" -gt "0" ]; then
            echo "commit_type=feat" >> $GITHUB_OUTPUT
            echo "commit_desc=promote develop to staging with new features" >> $GITHUB_OUTPUT
          elif [ "$FIX_COUNT" -gt "0" ]; then
            echo "commit_type=fix" >> $GITHUB_OUTPUT
            echo "commit_desc=promote develop to staging with bug fixes" >> $GITHUB_OUTPUT
          else
            echo "commit_type=chore" >> $GITHUB_OUTPUT
            echo "commit_desc=promote develop to staging" >> $GITHUB_OUTPUT
          fi

      - name: Create Pull Request
        if: steps.check_diff.outputs.needs_promotion == 'true' && github.event_name == 'push'
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const commitType = '${{ steps.commits.outputs.commit_type }}' || 'chore';
            const commitDesc = '${{ steps.commits.outputs.commit_desc }}' || 'promote develop to staging';
            const featCount = '${{ steps.commits.outputs.feat_count }}' || '0';
            const fixCount = '${{ steps.commits.outputs.fix_count }}' || '0';
            const commitsAhead = '${{ steps.check_diff.outputs.commits_ahead }}' || '0';
            const prTitle = commitType + '(release): ' + commitDesc + ' (' + new Date().toISOString().split('T')[0] + ')';
            const prBody = '## 🚀 Automated Staging Promotion\n\n' +
              '**Commits:** ' + commitsAhead + ' | **Features:** ' + featCount + ' | **Fixes:** ' + fixCount + '\n\n' +
              '### ✅ Pre-Merge Checklist\n- [ ] All CI checks pass\n- [ ] No merge conflicts\n- [ ] Ready for QA';
            const { data: existing } = await github.rest.pulls.list({
              owner: context.repo.owner, repo: context.repo.repo,
              head: context.repo.owner + ':develop', base: 'staging', state: 'open'
            });
            if (existing.length > 0) {
              await github.rest.pulls.update({
                owner: context.repo.owner, repo: context.repo.repo,
                pull_number: existing[0].number, title: prTitle, body: prBody
              });
            } else {
              await github.rest.pulls.create({
                owner: context.repo.owner, repo: context.repo.repo,
                title: prTitle, head: 'develop', base: 'staging', body: prBody
              });
            }

The production promotion workflow (staging to main) adds semantic version prediction. The YAML gets longer. Download the complete version: auto-promote-production.yml.


Semantic Versioning Made Easy

The workflows use Conventional Commits to preserve version meaning:

Commit TypeVersion ImpactExample
feat!: or BREAKING CHANGE:MAJOR bumpv1.0.0 → v2.0.0
feat:MINOR bumpv1.0.0 → v1.1.0
fix:PATCH bumpv1.0.0 → v1.0.1
docs:, chore:, etc.No bumpv1.0.0 → v1.0.0

Need help enforcing commit format? See our AI assistant rules for semantic versioning.


From Branch Promotions to Release Notes

Once your branch promotion is automated, the next step is automating the release notes that go with it.

You've got the PRs. You've got the version. Now you need to tell three different audiences what changed:

  • Engineers want breaking changes and migration steps
  • PMs and CSMs want customer impact and talking points
  • Customers want plain-language benefits

Writing three versions manually doesn't scale. ReleaseRay generates persona-specific release notes automatically from your GitHub activity. Same workflow. 90% less time.

Try ReleaseRay free →


Troubleshooting: Common Issues

Workflow Doesn't Trigger

Symptom: Nothing runs after CI passes.

Fixes:

  1. CI workflow name: The workflow_run trigger uses workflows: ["CI"]. Run gh workflow list and match the exact name.
  2. Branch names: Ensure develop and staging exist and match your setup.
  3. Permissions: Settings → Actions → General → Workflow permissions → Read and write permissions.

"Resource not accessible by integration"

Cause: Default GITHUB_TOKEN has read-only permissions.

Fix: Settings → Actions → General → Workflow permissions → Select Read and write permissions.

Version Prediction Wrong

Symptom: Predicted version doesn't match expected (e.g., breaking change got MINOR).

Fixes:

  1. Commit format: Use feat!: or BREAKING CHANGE: in the commit body.
  2. Squash merge: The promotion PR's squash commit must preserve the type. Ensure the workflow sets the PR title correctly.
  3. Tag parsing: If using pre-release tags (v1.0.0-beta.1), the script may need tweaks for your format.

PR Created on Every Push (Even When No Changes)

Cause: The push trigger fires on every push to develop.

Mitigation: The workflow checks staging..develop and only creates a PR when commits_ahead > 0. If you're still seeing duplicates, you may have multiple workflows; consolidate to one.

Production Workflow Doesn't Create PR

Cause: Production workflow triggers on pull_request (closed/merged) to staging. It won't run on workflow_run for PR creation due to token limits.

Fix: Merge the develop→staging PR manually (or via merge queue). The production workflow will then run when that PR is merged.


Security Best Practices

  • SHA-pin actions: Use actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 instead of @v4 in production.
  • Minimal permissions: The workflow requests only contents: write and pull-requests: write.
  • No secrets in logs: Avoid echoing ${{ secrets.* }} in run steps.

What's Next?


Download the Full Templates

For the complete workflows (including version prediction, base64 commit logs, and labels):


Written by Cristobal Mitchell, founder of ReleaseRay. We build tools to eliminate release workflow toil.

Enjoyed this post?

Practical guides on release notes, changelogs, and shipping better software. No spam, unsubscribe anytime.

C

Cristobal Mitchell

Founder of ReleaseRay

Building ReleaseRay — automated release notes from GitHub PRs for developers, PMs, and customers.

Ready to automate your release notes?

We value your privacy

We use cookies to enhance your experience. Essential cookies are required for the site to function. You can choose to accept all cookies or manage your preferences.