|
| 1 | +name: Cherry Pick Merged PR |
| 2 | +# Add a test comment. |
| 3 | + |
| 4 | +on: |
| 5 | + pull_request_target: |
| 6 | + types: |
| 7 | + - closed |
| 8 | + - labeled |
| 9 | + |
| 10 | +permissions: |
| 11 | + contents: write |
| 12 | + pull-requests: write |
| 13 | + |
| 14 | +jobs: |
| 15 | + load-config: |
| 16 | + runs-on: ubuntu-latest |
| 17 | + outputs: |
| 18 | + ubuntu_version: ${{ steps.config.outputs.ubuntu_version }} |
| 19 | + theme_name: ${{ steps.config.outputs.theme_name }} |
| 20 | + main_branch: ${{ steps.config.outputs.main_branch }} |
| 21 | + allowed_repos: ${{ steps.config.outputs.allowed_repos }} |
| 22 | + blocked_actors: ${{ steps.config.outputs.blocked_actors }} |
| 23 | + release_branches: ${{ steps.config.outputs.release_branches }} |
| 24 | + steps: |
| 25 | + - name: Checkout repository |
| 26 | + uses: actions/checkout@v4 |
| 27 | + |
| 28 | + - name: Load Release Configuration |
| 29 | + id: config |
| 30 | + run: | |
| 31 | + echo "📋 Loading release configuration..." |
| 32 | + CONFIG_FILE=".github/config/release.json" |
| 33 | + |
| 34 | + if [ ! -f "$CONFIG_FILE" ]; then |
| 35 | + echo "❌ Error: Release configuration file not found: $CONFIG_FILE" |
| 36 | + exit 1 |
| 37 | + fi |
| 38 | + |
| 39 | + # Validate JSON syntax |
| 40 | + if ! jq empty "$CONFIG_FILE" 2>/dev/null; then |
| 41 | + echo "❌ Error: Invalid JSON in release configuration" |
| 42 | + exit 1 |
| 43 | + fi |
| 44 | + |
| 45 | + # Load configuration values (only what's needed for cherry-pick) |
| 46 | + UBUNTU_VERSION=$(jq -r '.environment.ubuntu_version' "$CONFIG_FILE") |
| 47 | + THEME_NAME=$(jq -r '.repository.name' "$CONFIG_FILE") |
| 48 | + MAIN_BRANCH=$(jq -r '.repository.main_branch' "$CONFIG_FILE") |
| 49 | + ALLOWED_REPOS=$(jq -r '.security.allowed_repositories[]' "$CONFIG_FILE" | tr '\n' ' ') |
| 50 | + BLOCKED_ACTORS=$(jq -r '.security.blocked_actors[]' "$CONFIG_FILE" | tr '\n' ' ') |
| 51 | + RELEASE_BRANCHES=$(jq -r '.repository.release_branches[]' "$CONFIG_FILE" | tr '\n' ' ') |
| 52 | + |
| 53 | + # Set outputs |
| 54 | + echo "ubuntu_version=$UBUNTU_VERSION" >> "$GITHUB_OUTPUT" |
| 55 | + echo "theme_name=$THEME_NAME" >> "$GITHUB_OUTPUT" |
| 56 | + echo "main_branch=$MAIN_BRANCH" >> "$GITHUB_OUTPUT" |
| 57 | + echo "allowed_repos=$ALLOWED_REPOS" >> "$GITHUB_OUTPUT" |
| 58 | + echo "blocked_actors=$BLOCKED_ACTORS" >> "$GITHUB_OUTPUT" |
| 59 | + echo "release_branches=$RELEASE_BRANCHES" >> "$GITHUB_OUTPUT" |
| 60 | + |
| 61 | + echo "✅ Release configuration loaded successfully" |
| 62 | + echo "Theme: $THEME_NAME" |
| 63 | + echo "Ubuntu: $UBUNTU_VERSION" |
| 64 | + echo "Main branch: $MAIN_BRANCH" |
| 65 | + echo "Release branches: $RELEASE_BRANCHES" |
| 66 | +
|
| 67 | + cherry-pick: |
| 68 | + needs: load-config |
| 69 | + if: ${{ github.event.pull_request.merged == true && (github.event.action == 'closed' || (github.event.action == 'labeled' && startsWith(github.event.label.name, 'cp_'))) }} |
| 70 | + runs-on: ${{ needs.load-config.outputs.ubuntu_version }} |
| 71 | + |
| 72 | + env: |
| 73 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 74 | + HEAD_REF: ${{ github.event.pull_request.head.ref }} |
| 75 | + |
| 76 | + steps: |
| 77 | + - name: Checkout repository |
| 78 | + uses: actions/checkout@v4 |
| 79 | + with: |
| 80 | + token: ${{ secrets.GITHUB_TOKEN }} |
| 81 | + ref: ${{ github.event.pull_request.base.ref }} |
| 82 | + fetch-depth: 0 |
| 83 | + persist-credentials: true |
| 84 | + |
| 85 | + - name: Set Configuration Variables |
| 86 | + run: | |
| 87 | + echo "📋 Setting configuration variables from load-config job..." |
| 88 | + echo "UBUNTU_VERSION=${{ needs.load-config.outputs.ubuntu_version }}" >> $GITHUB_ENV |
| 89 | + echo "THEME_NAME=${{ needs.load-config.outputs.theme_name }}" >> $GITHUB_ENV |
| 90 | + echo "MAIN_BRANCH=${{ needs.load-config.outputs.main_branch }}" >> $GITHUB_ENV |
| 91 | + echo "ALLOWED_REPOS=${{ needs.load-config.outputs.allowed_repos }}" >> $GITHUB_ENV |
| 92 | + echo "BLOCKED_ACTORS=${{ needs.load-config.outputs.blocked_actors }}" >> $GITHUB_ENV |
| 93 | + echo "RELEASE_BRANCHES=${{ needs.load-config.outputs.release_branches }}" >> $GITHUB_ENV |
| 94 | + |
| 95 | + echo "✅ Configuration variables set successfully" |
| 96 | + echo "Theme: ${{ needs.load-config.outputs.theme_name }}" |
| 97 | + echo "Ubuntu: ${{ needs.load-config.outputs.ubuntu_version }}" |
| 98 | + echo "Main branch: ${{ needs.load-config.outputs.main_branch }}" |
| 99 | +
|
| 100 | + - name: Security and Pre-flight Checks |
| 101 | + run: | |
| 102 | + echo "🔍 Security and pre-flight checks..." |
| 103 | + echo "Repository: ${{ github.repository }}" |
| 104 | + echo "Actor: ${{ github.actor }}" |
| 105 | + echo "Base branch: ${{ github.event.pull_request.base.ref }}" |
| 106 | + echo "Head branch: $HEAD_REF" |
| 107 | + |
| 108 | + # Repository permissions validation |
| 109 | + REPO_ALLOWED=false |
| 110 | + IFS=' ' read -ra ALLOWED_REPOS_ARRAY <<< "${{ env.ALLOWED_REPOS }}" |
| 111 | + for allowed_repo in "${ALLOWED_REPOS_ARRAY[@]}"; do |
| 112 | + if [ "${{ github.repository }}" = "$allowed_repo" ]; then |
| 113 | + REPO_ALLOWED=true |
| 114 | + break |
| 115 | + fi |
| 116 | + done |
| 117 | + |
| 118 | + if [ "$REPO_ALLOWED" != "true" ]; then |
| 119 | + echo "⚠️ Warning: Cherry-pick attempted on unauthorized repository: ${{ github.repository }}" |
| 120 | + echo "Allowed repositories: ${{ env.ALLOWED_REPOS }}" |
| 121 | + fi |
| 122 | + |
| 123 | + # Check actor permissions (basic validation) |
| 124 | + IFS=' ' read -ra BLOCKED_ACTORS_ARRAY <<< "${{ env.BLOCKED_ACTORS }}" |
| 125 | + for blocked_actor in "${BLOCKED_ACTORS_ARRAY[@]}"; do |
| 126 | + if [ "${{ github.actor }}" = "$blocked_actor" ]; then |
| 127 | + echo "❌ Error: Blocked actor cannot create cherry-pick: ${{ github.actor }}" |
| 128 | + exit 1 |
| 129 | + fi |
| 130 | + done |
| 131 | + |
| 132 | + # Validate that this is a legitimate cherry-pick operation |
| 133 | + if [ "${{ github.event.pull_request.merged }}" != "true" ]; then |
| 134 | + echo "❌ Error: Cherry-pick can only be performed on merged PRs" |
| 135 | + exit 1 |
| 136 | + fi |
| 137 | + |
| 138 | + echo "✅ Security and pre-flight checks passed" |
| 139 | +
|
| 140 | + - name: Check if PR is from fork |
| 141 | + id: check_fork |
| 142 | + run: | |
| 143 | + IS_FORK="${{ github.event.pull_request.head.repo.full_name != github.repository }}" |
| 144 | + echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT" |
| 145 | + echo "Fork status: $IS_FORK" |
| 146 | + shell: bash |
| 147 | + |
| 148 | + - name: Log trigger and PR information |
| 149 | + run: | |
| 150 | + echo "Trigger: ${{ github.event.action }}" |
| 151 | + if [ "${{ github.event.action }}" = "labeled" ]; then |
| 152 | + echo "Added label: ${{ github.event.label.name }}" |
| 153 | + fi |
| 154 | + echo "PR #${{ github.event.pull_request.number }} from: ${{ github.event.pull_request.head.repo.full_name }}" |
| 155 | + echo "Target repository: ${{ github.repository }}" |
| 156 | + echo "Is fork PR: ${{ steps.check_fork.outputs.is_fork }}" |
| 157 | + echo "PR merged: ${{ github.event.pull_request.merged }}" |
| 158 | + shell: bash |
| 159 | + |
| 160 | + - name: Get branch labels |
| 161 | + id: get_labels |
| 162 | + run: | |
| 163 | + LABELS=$(jq -r '.[].name' <<< '${{ toJSON(github.event.pull_request.labels) }}' | grep '^cp_' | paste -sd ',' || echo "") |
| 164 | + echo "filtered_labels_csv=$LABELS" >> "$GITHUB_OUTPUT" |
| 165 | + shell: bash |
| 166 | + |
| 167 | + - name: Fetch all branches |
| 168 | + run: git fetch --all |
| 169 | + shell: bash |
| 170 | + |
| 171 | + - name: Cherry-Pick and Create PRs |
| 172 | + if: ${{ steps.get_labels.outputs.filtered_labels_csv != '' }} |
| 173 | + env: |
| 174 | + PR_TITLE: ${{ github.event.pull_request.title }} |
| 175 | + PR_USER_LOGIN: ${{ github.event.pull_request.user.login }} |
| 176 | + LABEL_NAME: ${{ github.event.label.name }} |
| 177 | + run: | |
| 178 | + PR_NUMBER="${{ github.event.pull_request.number }}" |
| 179 | + MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" |
| 180 | + ORIG_URL="${{ github.event.pull_request.html_url }}" |
| 181 | +
|
| 182 | + git config user.name "github-actions[bot]" |
| 183 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 184 | +
|
| 185 | + IFS=',' read -ra BRANCHES <<< "${{ steps.get_labels.outputs.filtered_labels_csv }}" |
| 186 | + for lbl in "${BRANCHES[@]}"; do |
| 187 | + TARGET=${lbl#cp_} |
| 188 | + |
| 189 | + # Create sanitized branch name from original branch name with timestamp for uniqueness |
| 190 | + ORIGINAL_BRANCH="${{ github.event.pull_request.head.ref }}" |
| 191 | + # Strip any word followed by '/' to ensure ticket numbers appear at the beginning |
| 192 | + CLEAN_BRANCH=$(echo "$ORIGINAL_BRANCH" | sed 's|^[^/]*/||g') |
| 193 | + SANITIZED_BRANCH=$(echo "$CLEAN_BRANCH" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') |
| 194 | + # Limit length to 35 chars to leave room for PR number and timestamp suffix |
| 195 | + SANITIZED_BRANCH=${SANITIZED_BRANCH:0:35} |
| 196 | + TIMESTAMP=$(date +%s) |
| 197 | + BRANCH="${SANITIZED_BRANCH}-cherry-pick-pr${PR_NUMBER}-${TIMESTAMP}" |
| 198 | +
|
| 199 | + echo "🍒 Processing cherry-pick for branch: $TARGET" |
| 200 | +
|
| 201 | + # Create branch |
| 202 | + if ! git checkout -b "$BRANCH" "origin/$TARGET"; then |
| 203 | + echo "::warning:: Branch $TARGET does not exist - skipping" |
| 204 | + continue |
| 205 | + fi |
| 206 | +
|
| 207 | + # Cherry-pick |
| 208 | + if ! git cherry-pick -m 1 "$MERGE_SHA"; then |
| 209 | + echo "::error:: Cherry-pick conflicts detected for PR #${PR_NUMBER} on branch ${TARGET}" |
| 210 | +
|
| 211 | + # Create a conflict resolution branch |
| 212 | + CONFLICT_BRANCH="${BRANCH}-conflicts" |
| 213 | +
|
| 214 | + # Add conflict markers and create a commit for manual resolution |
| 215 | + git add . |
| 216 | + git commit -m "Cherry-pick PR #${PR_NUMBER} with conflicts - manual resolution needed" |
| 217 | +
|
| 218 | + # Push the conflict branch |
| 219 | + if git push --force-with-lease origin "$BRANCH:$CONFLICT_BRANCH"; then |
| 220 | + # Create draft PR with conflict information |
| 221 | + if ! gh pr list --head "$CONFLICT_BRANCH" --base "$TARGET" --state open | grep -q .; then |
| 222 | + CONFLICT_TRIGGER_INFO="" |
| 223 | + if [ "${{ github.event.action }}" = "labeled" ]; then |
| 224 | + CONFLICT_TRIGGER_INFO=" |
| 225 | + **Trigger:** Label \`${LABEL_NAME}\` added to closed PR" |
| 226 | + fi |
| 227 | + |
| 228 | + gh pr create \ |
| 229 | + --base "$TARGET" \ |
| 230 | + --head "$CONFLICT_BRANCH" \ |
| 231 | + --title "🔧 [CONFLICTS] Cherry-pick PR #${PR_NUMBER} → ${TARGET}: ${PR_TITLE}" \ |
| 232 | + --body "⚠️ **Manual Resolution Required** |
| 233 | +
|
| 234 | + This cherry-pick of [#${PR_NUMBER}](${ORIG_URL}) to \`${TARGET}\` branch has conflicts that need manual resolution. |
| 235 | +
|
| 236 | + **Theme-Specific Conflict Resolution:** |
| 237 | + - **Version Conflicts**: Check \`style.css\` header for version mismatches |
| 238 | + - **Schema Conflicts**: Validate \`theme.json\` structure changes |
| 239 | + - **Function Conflicts**: Resolve \`functions.php\` function name collisions |
| 240 | +
|
| 241 | + **Resolution Steps:** |
| 242 | + 1. Check out this branch: \`git checkout $CONFLICT_BRANCH\` |
| 243 | + 2. Resolve conflicts in the marked files |
| 244 | + 3. Stage resolved files: \`git add <resolved-files>\` |
| 245 | + 4. Amend the commit: \`git commit --amend\` |
| 246 | + 5. Push changes: \`git push --force-with-lease\` |
| 247 | + 6. Mark this PR as ready for review |
| 248 | +
|
| 249 | + **Automatic Validation:** |
| 250 | + Once you push resolved changes, the following workflows will run automatically: |
| 251 | + - Build validation (\`build.yml\`) |
| 252 | + - JavaScript linting (\`lint.yml\`) |
| 253 | + - Cherry-pick validation (\`cherry-pick-validation.yml\`) |
| 254 | + - Additional CI workflows as configured |
| 255 | +
|
| 256 | + **Original PR:** [#${PR_NUMBER}](${ORIG_URL})${CONFLICT_TRIGGER_INFO}" \ |
| 257 | + --draft |
| 258 | +
|
| 259 | + echo "::notice:: Created draft PR for manual conflict resolution: $CONFLICT_BRANCH" |
| 260 | + else |
| 261 | + echo "::notice:: Draft PR already exists for conflict resolution: $CONFLICT_BRANCH" |
| 262 | + fi |
| 263 | + else |
| 264 | + echo "::warning:: Failed to push conflict branch $CONFLICT_BRANCH" |
| 265 | + git cherry-pick --abort |
| 266 | + fi |
| 267 | + continue |
| 268 | + else |
| 269 | + echo "✅ Cherry-pick successful for $TARGET" |
| 270 | + fi |
| 271 | +
|
| 272 | + # Basic theme structure validation (quick checks only) |
| 273 | + echo "🔍 Basic theme structure validation..." |
| 274 | + if [ ! -f "style.css" ]; then |
| 275 | + echo "::error:: Missing style.css after cherry-pick" |
| 276 | + continue |
| 277 | + fi |
| 278 | + if [ ! -f "functions.php" ]; then |
| 279 | + echo "::error:: Missing functions.php after cherry-pick" |
| 280 | + continue |
| 281 | + fi |
| 282 | + if [ ! -f "theme.json" ]; then |
| 283 | + echo "::error:: Missing theme.json after cherry-pick" |
| 284 | + continue |
| 285 | + fi |
| 286 | +
|
| 287 | + # Check for obvious conflict markers |
| 288 | + if grep -q "<<<<<<< HEAD" style.css functions.php theme.json 2>/dev/null; then |
| 289 | + echo "::warning:: Conflict markers detected - this should not happen in successful cherry-pick" |
| 290 | + fi |
| 291 | +
|
| 292 | + # Push (force push to handle existing branches) |
| 293 | + if ! git push --force-with-lease origin "$BRANCH"; then |
| 294 | + echo "::error:: Failed to push branch $BRANCH" |
| 295 | + exit 1 |
| 296 | + fi |
| 297 | +
|
| 298 | + # Create PR via gh CLI (token already in env: GH_TOKEN) |
| 299 | + # Check if PR already exists |
| 300 | + if gh pr list --head "$BRANCH" --base "$TARGET" --state open | grep -q .; then |
| 301 | + echo "PR already exists for branch $BRANCH -> $TARGET, skipping creation" |
| 302 | + else |
| 303 | + TRIGGER_INFO="" |
| 304 | + if [ "${{ github.event.action }}" = "labeled" ]; then |
| 305 | + TRIGGER_INFO=" |
| 306 | + **Trigger:** Label \`${LABEL_NAME}\` added to closed PR" |
| 307 | + fi |
| 308 | +
|
| 309 | + # Extract tickets and add to title for Jira compatibility |
| 310 | + PR_TICKETS=$(echo "$PR_TITLE" | grep -o '\[[A-Z]\{2,\}-[0-9]\+\]' | tr '\n' ' ' | sed 's/[[:space:]]*$//') |
| 311 | + |
| 312 | + # Create title with tickets at the end |
| 313 | + if [ -n "$PR_TICKETS" ]; then |
| 314 | + CHERRY_PICK_TITLE="$PR_TITLE (🍒 CP #${PR_NUMBER}→${TARGET}) ${PR_TICKETS}" |
| 315 | + else |
| 316 | + CHERRY_PICK_TITLE="$PR_TITLE (🍒 CP #${PR_NUMBER}→${TARGET})" |
| 317 | + fi |
| 318 | +
|
| 319 | + gh pr create \ |
| 320 | + --base "$TARGET" \ |
| 321 | + --head "$BRANCH" \ |
| 322 | + --title "$CHERRY_PICK_TITLE" \ |
| 323 | + --body "Automatic cherry-pick of [#${PR_NUMBER}](${ORIG_URL}) to \`${TARGET}\` branch. |
| 324 | +
|
| 325 | + **Theme Information:** |
| 326 | + - **Theme:** ${THEME_NAME} |
| 327 | + - **Source:** ${{ github.event.pull_request.head.repo.full_name }} |
| 328 | + - **Original Author:** @${PR_USER_LOGIN}${TRIGGER_INFO} |
| 329 | +
|
| 330 | + **Automatic Validation:** |
| 331 | + The following workflows will run automatically to validate this cherry-pick: |
| 332 | + - 🏗️ **Build validation** (\`build.yml\`) - Theme compilation and asset building |
| 333 | + - 🧹 **JavaScript linting** (\`lint.yml\`) - Code quality checks |
| 334 | + - ✅ **Cherry-pick validation** (\`cherry-pick-validation.yml\`) - Theme-specific validation |
| 335 | + - 🔍 **Additional CI workflows** - As configured for the repository |
| 336 | +
|
| 337 | + **Next Steps:** |
| 338 | + - Wait for validation workflows to complete |
| 339 | + - Review automated validation results |
| 340 | + - Test theme functionality if needed |
| 341 | + - Merge when all checks pass |
| 342 | +
|
| 343 | + **Note:** Build and validation happen automatically via PR workflows, following the same process as regular PRs." |
| 344 | + fi |
| 345 | + |
| 346 | + echo "✅ Successfully created cherry-pick PR for $TARGET" |
| 347 | + done |
| 348 | + shell: bash |
0 commit comments