|
| 1 | +name: Sync Release Pull Request |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: |
| 6 | + - develop |
| 7 | + |
| 8 | +jobs: |
| 9 | + sync-release-pr: |
| 10 | + name: Create or update develop → main PR |
| 11 | + runs-on: ubuntu-latest |
| 12 | + permissions: |
| 13 | + contents: write |
| 14 | + pull-requests: write |
| 15 | + steps: |
| 16 | + - uses: actions/checkout@v4 |
| 17 | + with: |
| 18 | + fetch-depth: 0 |
| 19 | + |
| 20 | + - name: Open or update release PR |
| 21 | + env: |
| 22 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 23 | + PR_TITLE: "Release: develop → main" |
| 24 | + PR_TEMPLATE: .github/PULL_REQUEST_TEMPLATE.md |
| 25 | + run: | |
| 26 | + set -euo pipefail |
| 27 | +
|
| 28 | + if [ ! -f "$PR_TEMPLATE" ]; then |
| 29 | + echo "Pull request template not found at $PR_TEMPLATE" |
| 30 | + exit 1 |
| 31 | + fi |
| 32 | +
|
| 33 | + git fetch origin main --prune |
| 34 | +
|
| 35 | + BODY_FILE="$(mktemp)" |
| 36 | + export BODY_FILE |
| 37 | +
|
| 38 | + python3 - <<'PY' |
| 39 | + import os |
| 40 | + import re |
| 41 | + import subprocess |
| 42 | +
|
| 43 | + template_path = os.environ["PR_TEMPLATE"] |
| 44 | + body_file_path = os.environ["BODY_FILE"] |
| 45 | +
|
| 46 | + # Get commits that are in develop but not in main |
| 47 | + log = subprocess.check_output( |
| 48 | + ["git", "log", "--pretty=format:%s (%h)", "origin/main..HEAD", "--no-merges"], |
| 49 | + text=True, |
| 50 | + ).strip().splitlines() |
| 51 | +
|
| 52 | + # Filter out empty lines |
| 53 | + log = [line for line in log if line.strip()] |
| 54 | +
|
| 55 | + max_items = 60 |
| 56 | + items = log[:max_items] |
| 57 | + extra = max(len(log) - len(items), 0) |
| 58 | +
|
| 59 | + if items: |
| 60 | + bullets = [f"- {line}" for line in items] |
| 61 | + if extra: |
| 62 | + bullets.append(f"- …and {extra} more commits") |
| 63 | + else: |
| 64 | + bullets = ["- No new changes since last release"] |
| 65 | +
|
| 66 | + with open(template_path, "r", encoding="utf-8") as f: |
| 67 | + content = f.read() |
| 68 | + lines = content.splitlines() |
| 69 | +
|
| 70 | + # Find the Changes section and replace the placeholder |
| 71 | + if "## Changes" in lines: |
| 72 | + header_index = lines.index("## Changes") |
| 73 | + |
| 74 | + # Find the next section header |
| 75 | + end_index = len(lines) |
| 76 | + for i in range(header_index + 1, len(lines)): |
| 77 | + if lines[i].startswith("## "): |
| 78 | + end_index = i |
| 79 | + break |
| 80 | +
|
| 81 | + # Extract section content |
| 82 | + section = lines[header_index + 1 : end_index] |
| 83 | + |
| 84 | + # Find and replace the placeholder bullet point |
| 85 | + replaced = False |
| 86 | + for idx, line in enumerate(section): |
| 87 | + # Match empty bullet point (with optional whitespace) |
| 88 | + if re.fullmatch(r"-\s*", line.strip()) or line.strip() == "-": |
| 89 | + section = section[:idx] + bullets + section[idx + 1 :] |
| 90 | + replaced = True |
| 91 | + break |
| 92 | +
|
| 93 | + if not replaced: |
| 94 | + # If no placeholder found, insert bullets after empty lines/comments |
| 95 | + insert_idx = 0 |
| 96 | + for idx, line in enumerate(section): |
| 97 | + if line.strip() and not line.strip().startswith("<!--"): |
| 98 | + insert_idx = idx |
| 99 | + break |
| 100 | + insert_idx = idx + 1 |
| 101 | + section = section[:insert_idx] + bullets + section[insert_idx:] |
| 102 | +
|
| 103 | + final_lines = lines[: header_index + 1] + section + lines[end_index:] |
| 104 | + else: |
| 105 | + # If no Changes section exists, append one |
| 106 | + final_lines = lines + ["", "## Changes", ""] + bullets |
| 107 | +
|
| 108 | + with open(body_file_path, "w", encoding="utf-8") as f: |
| 109 | + f.write("\n".join(final_lines).rstrip() + "\n") |
| 110 | + |
| 111 | + # Debug: print the generated body |
| 112 | + print("Generated PR body:") |
| 113 | + print("-" * 40) |
| 114 | + with open(body_file_path, "r") as f: |
| 115 | + print(f.read()) |
| 116 | + print("-" * 40) |
| 117 | + PY |
| 118 | +
|
| 119 | + PR_NUMBER="$(gh pr list --base main --head develop --state open --json number --jq '.[0].number // empty')" |
| 120 | +
|
| 121 | + if [ -n "$PR_NUMBER" ]; then |
| 122 | + echo "Updating existing PR #$PR_NUMBER" |
| 123 | + gh pr edit "$PR_NUMBER" \ |
| 124 | + --title "$PR_TITLE" \ |
| 125 | + --body-file "$BODY_FILE" |
| 126 | + else |
| 127 | + echo "Creating new release PR" |
| 128 | + gh pr create \ |
| 129 | + --base main \ |
| 130 | + --head develop \ |
| 131 | + --title "$PR_TITLE" \ |
| 132 | + --body-file "$BODY_FILE" |
| 133 | + fi |
0 commit comments