Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 123 additions & 80 deletions .github/workflows/promote.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# .github/workflows/promote-branch.yaml
# .github/workflows/promote.yaml
---
name: Promote Branch

run-name: Promote ${{ inputs.promotion }}
run-name: "${{ github.event_name == 'workflow_dispatch' && format('Promote {0}', inputs.promotion) || format('Merge PR #{0}', github.event.issue.number) }}"

on:
# Create PR workflow
workflow_dispatch:
inputs:
promotion:
Expand All @@ -15,11 +16,16 @@ on:
- dev -> staging
- staging -> main

# Merge on comment workflow
issue_comment:
types: [created]

jobs:
promote:
create-pr:
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
pull-requests: write

steps:
Expand All @@ -34,112 +40,149 @@ jobs:
elif [[ "$PROMOTION" == "staging -> main" ]]; then
echo "source=staging" >> $GITHUB_OUTPUT
echo "target=main" >> $GITHUB_OUTPUT
else
echo "Error: Invalid promotion path"
exit 1
fi

echo "✓ Promotion: $PROMOTION"

- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
uses: actions/checkout@v5

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Create or find PR
id: pr
- name: Create PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SOURCE="${{ steps.parse.outputs.source }}"
TARGET="${{ steps.parse.outputs.target }}"

# Check if PR already exists
EXISTING_PR=$(gh pr list --base "$TARGET" --head "$SOURCE" --json number --jq '.[0].number')

if [[ -n "$EXISTING_PR" ]]; then
echo "Found existing PR #$EXISTING_PR"
echo "pr_number=$EXISTING_PR" >> $GITHUB_OUTPUT
else
# Create new PR
PR_TITLE="Promote $SOURCE to $TARGET"
PR_BODY="Automated promotion from \`$SOURCE\` to \`$TARGET\`

This PR will be automatically merged via fast-forward."

gh pr create \
--base "$TARGET" \
--head "$SOURCE" \
--title "$PR_TITLE" \
--body "$PR_BODY"

PR_NUMBER=$(gh pr list --base "$TARGET" --head "$SOURCE" --json number --jq '.[0].number')
echo "Created PR #$PR_NUMBER"
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "PR #$EXISTING_PR already exists"
exit 0
fi

- name: Wait for PR checks
# Create new PR
PR_TITLE="Promote $SOURCE to $TARGET"
PR_BODY="Automated promotion from \`$SOURCE\` to \`$TARGET\`

**To merge:** After approval, comment \`/promote\` to trigger fast-forward merge

---
_Created by GitHub Actions_"

gh pr create \
--base "$TARGET" \
--head "$SOURCE" \
--title "$PR_TITLE" \
--body "$PR_BODY"

echo "✓ PR created - awaiting review and approval"

fast-forward-merge:
if: |
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.body == '/promote'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Check if commenter has write access
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SOURCE="${{ steps.parse.outputs.source }}"
TARGET="${{ steps.parse.outputs.target }}"
PR_NUMBER="${{ steps.pr.outputs.pr_number }}"
PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission --jq '.permission')

if [[ "$PERMISSION" != "write" && "$PERMISSION" != "maintain" && "$PERMISSION" != "admin" ]]; then
echo "❌ User does not have write access or higher"
exit 1
fi

echo "✓ User has $PERMISSION access"

- name: Get PR details
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=${{ github.event.issue.number }}
PR_DATA=$(gh pr view "$PR_NUMBER" --repo ${{ github.repository }} --json baseRefName,headRefName,state,reviewDecision)

BASE=$(echo "$PR_DATA" | jq -r '.baseRefName')
HEAD=$(echo "$PR_DATA" | jq -r '.headRefName')
STATE=$(echo "$PR_DATA" | jq -r '.state')
REVIEW=$(echo "$PR_DATA" | jq -r '.reviewDecision')

echo "base=$BASE" >> $GITHUB_OUTPUT
echo "head=$HEAD" >> $GITHUB_OUTPUT
echo "state=$STATE" >> $GITHUB_OUTPUT
echo "review=$REVIEW" >> $GITHUB_OUTPUT

echo "PR: $HEAD → $BASE"
echo "State: $STATE"
echo "Review: $REVIEW"

- name: Validate PR state
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BASE="${{ steps.pr.outputs.base }}"

echo "Waiting for PR checks to complete..."
# Only allow promotions to staging or main
if [[ "$BASE" != "staging" && "$BASE" != "main" ]]; then
gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body "❌ Cannot promote: This workflow only supports promotions to \`staging\` or \`main\` branches"
exit 1
fi

# Wait for checks to complete (max 5 minutes)
SECONDS=0
MAX_WAIT=300
if [[ "${{ steps.pr.outputs.state }}" != "OPEN" ]]; then
gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body "❌ Cannot merge: PR is not open"
exit 1
fi

while [ $SECONDS -lt $MAX_WAIT ]; do
STATUS=$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '.statusCheckRollup | length')

if [ "$STATUS" == "0" ]; then
echo "No status checks required, proceeding..."
break
fi

PENDING=$(gh pr view "$PR_NUMBER" --json statusCheckRollup --jq '[.statusCheckRollup[] | select(.status == "PENDING" or .status == "IN_PROGRESS")] | length')

if [ "$PENDING" == "0" ]; then
echo "All checks completed"
break
fi

echo "Waiting for $PENDING check(s) to complete..."
sleep 10
done
if [[ "${{ steps.pr.outputs.review }}" != "APPROVED" ]]; then
gh pr comment ${{ github.event.issue.number }} --repo ${{ github.repository }} --body "❌ Cannot merge: PR requires approval"
exit 1
fi

echo "✓ PR is approved and ready to merge"

- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- 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 merge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SOURCE="${{ steps.parse.outputs.source }}"
TARGET="${{ steps.parse.outputs.target }}"
SOURCE="${{ steps.pr.outputs.head }}"
TARGET="${{ steps.pr.outputs.base }}"
PR_NUMBER="${{ github.event.issue.number }}"

# Fetch latest
git fetch origin "$SOURCE"
git fetch origin "$TARGET"

# Checkout target and fast-forward merge source
git checkout "$TARGET"
git merge "origin/$SOURCE" --ff-only

# Push - this will automatically close the PR
if ! git merge "origin/$SOURCE" --ff-only; then
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "❌ Fast-forward merge failed. Please rebase $SOURCE on $TARGET"
exit 1
fi

# Push
git push origin "$TARGET"

echo "✓ Successfully promoted $SOURCE to $TARGET (fast-forward)"
echo "✓ PR will be automatically closed by GitHub"

- name: Summary
run: |
echo "## Promotion Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Promotion:** ${{ inputs.promotion }}" >> $GITHUB_STEP_SUMMARY
echo "- **PR:** #${{ steps.pr.outputs.pr_number }}" >> $GITHUB_STEP_SUMMARY
echo "- **Status:** ✓ Fast-forward merge completed" >> $GITHUB_STEP_SUMMARY
# Comment success
gh pr comment "$PR_NUMBER" --repo ${{ github.repository }} --body "✅ Successfully merged $SOURCE → $TARGET via fast-forward"

echo "✓ Fast-forward merge completed"