|
| 1 | +name: Reverse Sync on Push |
| 2 | + |
| 3 | +env: |
| 4 | + TARGET_REPO: alaudadevops/tektoncd-operator |
| 5 | + # Ignoring the files or folders in this prefix (uses comma to split) |
| 6 | + IGNORE_PATHS: .github/,README.md |
| 7 | + # will check these files change to create a new patch |
| 8 | + SYNCED_PATHS: "docs/ theme/ .yarn/ doom.config.yml yarn.lock tsconfig.json package.json sites.yaml" |
| 9 | + |
| 10 | +on: |
| 11 | + push: |
| 12 | + branches: |
| 13 | + - main |
| 14 | + - release-* |
| 15 | + |
| 16 | +# Limit token capabilities to what the job really needs |
| 17 | +permissions: |
| 18 | + contents: read # checkout / git diff |
| 19 | + pull-requests: write # create PR in target repo |
| 20 | + |
| 21 | +# (Optional) Prevent multiple syncs of the same ref running in parallel |
| 22 | +concurrency: |
| 23 | + group: reverse-sync-${{ github.ref }} |
| 24 | + cancel-in-progress: true |
| 25 | +jobs: |
| 26 | + reverse-sync: |
| 27 | + runs-on: ubuntu-latest |
| 28 | + |
| 29 | + steps: |
| 30 | + - name: Checkout devops-pipelines-docs repository |
| 31 | + uses: actions/checkout@v4 |
| 32 | + with: |
| 33 | + token: ${{ secrets.GH_TOKEN }} |
| 34 | + fetch-depth: 0 |
| 35 | + |
| 36 | + - name: Check if commit is from merged PR |
| 37 | + id: check_pr_commit |
| 38 | + run: | |
| 39 | + # Get the latest commit |
| 40 | + commit_sha="${{ github.sha }}" |
| 41 | + echo "commit_sha=$commit_sha" >> $GITHUB_OUTPUT |
| 42 | +
|
| 43 | + # Get commit message |
| 44 | + commit_message=$(git log -1 --pretty=format:"%s" $commit_sha) |
| 45 | + echo "commit_message=$commit_message" >> $GITHUB_OUTPUT |
| 46 | +
|
| 47 | + # Get commit author |
| 48 | + commit_author=$(git log -1 --pretty=format:"%an" $commit_sha) |
| 49 | + commit_author_email=$(git log -1 --pretty=format:"%ae" $commit_sha) |
| 50 | + echo "commit_author=$commit_author" >> $GITHUB_OUTPUT |
| 51 | + echo "commit_author_email=$commit_author_email" >> $GITHUB_OUTPUT |
| 52 | +
|
| 53 | + echo "=> Commit: $commit_sha" |
| 54 | + echo "=> Message: $commit_message" |
| 55 | + echo "=> Author: $commit_author <$commit_author_email>" |
| 56 | +
|
| 57 | + # Check if this is a merge commit from GitHub (squash merge creates a single commit) |
| 58 | + # Look for PR number in commit message (GitHub automatically adds this) |
| 59 | + if [[ "$commit_message" =~ \(#([0-9]+)\)$ ]]; then |
| 60 | + pr_number="${BASH_REMATCH[1]}" |
| 61 | + echo "pr_number=$pr_number" >> $GITHUB_OUTPUT |
| 62 | + echo "is_pr_commit=true" >> $GITHUB_OUTPUT |
| 63 | + echo "✅ Detected commit from PR #$pr_number" |
| 64 | + else |
| 65 | + echo "is_pr_commit=false" >> $GITHUB_OUTPUT |
| 66 | + echo "ℹ️ Not a PR commit - skipping reverse sync" |
| 67 | + fi |
| 68 | +
|
| 69 | + # Skip if the commit is from our sync bot |
| 70 | + if [[ "$commit_author_email" == "[email protected]" ]] && [[ "$commit_message" == *"[reverse-sync]"* ]]; then |
| 71 | + echo "skip_sync=true" >> $GITHUB_OUTPUT |
| 72 | + echo "🤖 Commit is from sync bot - skipping reverse sync" |
| 73 | + elif [[ "$commit_message" == *"Sync documentation"* ]] || [[ "$commit_message" == *"sync-docs"* ]]; then |
| 74 | + echo "skip_sync=true" >> $GITHUB_OUTPUT |
| 75 | + echo "🤖 Commit appears to be from sync process - skipping reverse sync" |
| 76 | + else |
| 77 | + echo "skip_sync=false" >> $GITHUB_OUTPUT |
| 78 | + echo "👥 Commit is from external contributor - proceeding with reverse sync" |
| 79 | + fi |
| 80 | +
|
| 81 | + - name: Get PR information |
| 82 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' |
| 83 | + id: get_pr_info |
| 84 | + run: | |
| 85 | + pr_number="${{ steps.check_pr_commit.outputs.pr_number }}" |
| 86 | +
|
| 87 | + # Get PR information using GitHub API |
| 88 | + pr_info=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ |
| 89 | + "https://api.github.com/repos/${{ github.repository }}/pulls/$pr_number") |
| 90 | +
|
| 91 | + pr_title=$(echo "$pr_info" | jq -r '.title') |
| 92 | + pr_author=$(echo "$pr_info" | jq -r '.user.login') |
| 93 | + pr_url=$(echo "$pr_info" | jq -r '.html_url') |
| 94 | + pr_base_ref=$(echo "$pr_info" | jq -r '.base.ref') |
| 95 | +
|
| 96 | + echo "pr_title=$pr_title" >> $GITHUB_OUTPUT |
| 97 | + echo "pr_author=$pr_author" >> $GITHUB_OUTPUT |
| 98 | + echo "pr_url=$pr_url" >> $GITHUB_OUTPUT |
| 99 | + echo "pr_base_ref=$pr_base_ref" >> $GITHUB_OUTPUT |
| 100 | +
|
| 101 | + echo "=> PR #$pr_number: $pr_title" |
| 102 | + echo "=> Author: $pr_author" |
| 103 | + echo "=> URL: $pr_url" |
| 104 | +
|
| 105 | + - name: Get commit changes |
| 106 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' |
| 107 | + id: get_changes |
| 108 | + run: | |
| 109 | + commit_sha="${{ steps.check_pr_commit.outputs.commit_sha }}" |
| 110 | +
|
| 111 | + # Get the parent commit (previous commit before this one) |
| 112 | + parent_commit=$(git rev-parse ${commit_sha}^) |
| 113 | + echo "parent_commit=$parent_commit" >> $GITHUB_OUTPUT |
| 114 | +
|
| 115 | + # Get list of changed files in the commit, excluding ignored paths |
| 116 | + ignore_pattern=$(echo "$IGNORE_PATHS" | sed 's/,/|/g' | sed 's|/$||g') |
| 117 | + echo "🙈 Ignored paths: $ignore_pattern" |
| 118 | +
|
| 119 | + git diff --name-only $parent_commit $commit_sha | grep -v -E "^($ignore_pattern)" > changed_files.txt || true |
| 120 | +
|
| 121 | + echo "📋 Changed files in commit:" |
| 122 | + cat changed_files.txt |
| 123 | +
|
| 124 | + # Check if any relevant files were changed |
| 125 | + if [ -s changed_files.txt ]; then |
| 126 | + echo "has_doc_changes=true" >> $GITHUB_OUTPUT |
| 127 | + echo "✅ Documentation changes detected" |
| 128 | + else |
| 129 | + echo "has_doc_changes=false" >> $GITHUB_OUTPUT |
| 130 | + echo "ℹ️ No documentation changes detected" |
| 131 | + fi |
| 132 | +
|
| 133 | + - name: Checkout target repository |
| 134 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' && steps.get_changes.outputs.has_doc_changes == 'true' |
| 135 | + uses: actions/checkout@v4 |
| 136 | + with: |
| 137 | + repository: ${{env.TARGET_REPO}} |
| 138 | + token: ${{ secrets.GH_TOKEN }} |
| 139 | + path: target-docs |
| 140 | + fetch-depth: 0 |
| 141 | + ref: ${{ steps.get_pr_info.outputs.pr_base_ref }} |
| 142 | + |
| 143 | + - name: Create reverse sync branch |
| 144 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' && steps.get_changes.outputs.has_doc_changes == 'true' |
| 145 | + id: create_branch |
| 146 | + run: | |
| 147 | + cd target-docs |
| 148 | +
|
| 149 | + # Create a unique branch name |
| 150 | + pr_number="${{ steps.check_pr_commit.outputs.pr_number }}" |
| 151 | + branch_name="reverse-sync/pr-$pr_number-$(date +%s)" |
| 152 | + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT |
| 153 | +
|
| 154 | + git checkout -b "$branch_name" |
| 155 | +
|
| 156 | + # Configure git |
| 157 | + git config user.name "Documentation Sync Bot" |
| 158 | + git config user.email "[email protected]" |
| 159 | +
|
| 160 | + echo "📝 Created branch: $branch_name" |
| 161 | +
|
| 162 | + - name: Apply changes from devops-pipelines-docs |
| 163 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' && steps.get_changes.outputs.has_doc_changes == 'true' |
| 164 | + run: | |
| 165 | + commit_sha="${{ steps.check_pr_commit.outputs.commit_sha }}" |
| 166 | + parent_commit="${{ steps.get_changes.outputs.parent_commit }}" |
| 167 | +
|
| 168 | + # Create a patch with only the synced paths |
| 169 | + echo "📑 Will only sync these paths: $SYNCED_PATHS" |
| 170 | + git format-patch $parent_commit..$commit_sha --stdout -- $SYNCED_PATHS > changes.patch |
| 171 | +
|
| 172 | + cd target-docs |
| 173 | +
|
| 174 | + # Apply the patch |
| 175 | + if [ -s ../changes.patch ]; then |
| 176 | + echo "📦 Applying changes from devops-pipelines-docs..." |
| 177 | + git apply ../changes.patch || { |
| 178 | + echo "⚠️ Patch application failed, trying manual copy..." |
| 179 | +
|
| 180 | + # Fallback: manual copy of changed files |
| 181 | + while IFS= read -r file; do |
| 182 | + if [ -f "../$file" ]; then |
| 183 | + mkdir -p "$(dirname "$file")" |
| 184 | + cp "../$file" "$file" |
| 185 | + echo "✅ Copied: $file" |
| 186 | + fi |
| 187 | + done < ../changed_files.txt |
| 188 | + } |
| 189 | + else |
| 190 | + echo "⚠️ No patch generated, using manual copy..." |
| 191 | +
|
| 192 | + # Manual copy approach |
| 193 | + while IFS= read -r file; do |
| 194 | + if [ -f "../$file" ]; then |
| 195 | + mkdir -p "$(dirname "$file")" |
| 196 | + cp "../$file" "$file" |
| 197 | + echo "✅ Copied: $file" |
| 198 | + fi |
| 199 | + done < ../changed_files.txt |
| 200 | + fi |
| 201 | +
|
| 202 | + - name: Commit changes |
| 203 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' && steps.get_changes.outputs.has_doc_changes == 'true' |
| 204 | + id: commit_changes |
| 205 | + run: | |
| 206 | + cd target-docs |
| 207 | +
|
| 208 | + git add . |
| 209 | +
|
| 210 | + if git diff --staged --quiet; then |
| 211 | + echo "has_changes=false" >> $GITHUB_OUTPUT |
| 212 | + echo "ℹ️ No changes to commit" |
| 213 | + else |
| 214 | + echo "has_changes=true" >> $GITHUB_OUTPUT |
| 215 | +
|
| 216 | + pr_number="${{ steps.check_pr_commit.outputs.pr_number }}" |
| 217 | + pr_title="${{ steps.get_pr_info.outputs.pr_title }}" |
| 218 | + pr_author="${{ steps.get_pr_info.outputs.pr_author }}" |
| 219 | + pr_url="${{ steps.get_pr_info.outputs.pr_url }}" |
| 220 | + commit_sha="${{ steps.check_pr_commit.outputs.commit_sha }}" |
| 221 | +
|
| 222 | + # Create commit message with reverse sync marker |
| 223 | + cat > commit_message.txt << EOF |
| 224 | + [reverse-sync] Sync documentation changes from devops-pipelines-docs PR #$pr_number |
| 225 | +
|
| 226 | + This commit incorporates changes from external contributors to the devops-pipelines-docs repository. |
| 227 | +
|
| 228 | + 📋 Original PR Details: |
| 229 | + - Title: $pr_title |
| 230 | + - Author: $pr_author |
| 231 | + - URL: $pr_url |
| 232 | + - Commit: $commit_sha |
| 233 | +
|
| 234 | + 🔄 This is a reverse sync commit - it should not trigger forward sync. |
| 235 | + EOF |
| 236 | +
|
| 237 | + git commit -F commit_message.txt |
| 238 | + rm commit_message.txt |
| 239 | +
|
| 240 | + echo "✅ Changes committed successfully" |
| 241 | + fi |
| 242 | +
|
| 243 | + - name: Push branch and create PR |
| 244 | + if: steps.check_pr_commit.outputs.is_pr_commit == 'true' && steps.check_pr_commit.outputs.skip_sync == 'false' && steps.get_changes.outputs.has_doc_changes == 'true' && steps.commit_changes.outputs.has_changes == 'true' |
| 245 | + run: | |
| 246 | + cd target-docs |
| 247 | + branch_name="${{ steps.create_branch.outputs.branch_name }}" |
| 248 | + pr_base_ref="${{ steps.get_pr_info.outputs.pr_base_ref }}" |
| 249 | + pr_number="${{ steps.check_pr_commit.outputs.pr_number }}" |
| 250 | + pr_title="${{ steps.get_pr_info.outputs.pr_title }}" |
| 251 | + pr_author="${{ steps.get_pr_info.outputs.pr_author }}" |
| 252 | + pr_url="${{ steps.get_pr_info.outputs.pr_url }}" |
| 253 | +
|
| 254 | + # Push the branch |
| 255 | + git push origin "$branch_name" |
| 256 | +
|
| 257 | + # Create PR body with proper JSON escaping |
| 258 | + pr_body=$(cat << 'EOF' |
| 259 | + ### 🔄 Reverse Sync from devops-pipelines-docs |
| 260 | +
|
| 261 | + This PR incorporates documentation changes from external contributors to the devops-pipelines-docs repository. |
| 262 | +
|
| 263 | + #### Original PR Details |
| 264 | + - **Repository**: danielfbm/devops-pipelines-docs |
| 265 | + - **PR**: #%PR_NUMBER% - %PR_TITLE% |
| 266 | + - **Author**: @%PR_AUTHOR% |
| 267 | + - **URL**: %PR_URL% |
| 268 | +
|
| 269 | + #### Changes |
| 270 | + This PR includes changes to documentation files that were contributed by external contributors to the public devops-pipelines-docs repository. |
| 271 | +
|
| 272 | + #### Important Notes |
| 273 | + - ⚠️ This PR contains the `[reverse-sync]` marker to prevent infinite sync loops |
| 274 | + - ✅ Once merged, this will NOT trigger a forward sync back to devops-pipelines-docs |
| 275 | + - 🔍 Please review the changes to ensure they align with internal documentation standards |
| 276 | +
|
| 277 | + --- |
| 278 | + *This PR was automatically created by the reverse sync workflow.* |
| 279 | + EOF) |
| 280 | +
|
| 281 | + # Replace placeholders in the PR body |
| 282 | + pr_body=$(echo "$pr_body" | sed "s/%PR_NUMBER%/$pr_number/g") |
| 283 | + pr_body=$(echo "$pr_body" | sed "s/%PR_TITLE%/$pr_title/g") |
| 284 | + pr_body=$(echo "$pr_body" | sed "s/%PR_AUTHOR%/$pr_author/g") |
| 285 | + pr_body=$(echo "$pr_body" | sed "s|%PR_URL%|$pr_url|g") |
| 286 | +
|
| 287 | + # Create JSON payload with proper escaping |
| 288 | + json_payload=$(jq -n \ |
| 289 | + --arg title "[reverse-sync] Documentation changes from devops-pipelines-docs PR #$pr_number" \ |
| 290 | + --arg head "$branch_name" \ |
| 291 | + --arg base "$pr_base_ref" \ |
| 292 | + --arg body "$pr_body" \ |
| 293 | + '{title: $title, head: $head, base: $base, body: $body}') |
| 294 | +
|
| 295 | + # Create the PR using GitHub API |
| 296 | + curl -X POST \ |
| 297 | + -H "Authorization: token ${{ secrets.GH_TOKEN }}" \ |
| 298 | + -H "Accept: application/vnd.github.v3+json" \ |
| 299 | + -H "Content-Type: application/json" \ |
| 300 | + https://api.github.com/repos/${{env.TARGET_REPO}}/pulls \ |
| 301 | + -d "$json_payload" |
| 302 | +
|
| 303 | + - name: Create workflow summary |
| 304 | + run: | |
| 305 | + if [ "${{ steps.check_pr_commit.outputs.is_pr_commit }}" == "false" ]; then |
| 306 | + echo "## ℹ️ Not a PR Commit" >> $GITHUB_STEP_SUMMARY |
| 307 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 308 | + echo "This commit was not created from a merged PR, so no reverse sync was performed." >> $GITHUB_STEP_SUMMARY |
| 309 | + elif [ "${{ steps.check_pr_commit.outputs.skip_sync }}" == "true" ]; then |
| 310 | + echo "## 🤖 Sync Bot Commit - Reverse Sync Skipped" >> $GITHUB_STEP_SUMMARY |
| 311 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 312 | + echo "This commit was created by the sync bot, so reverse sync was skipped to prevent loops." >> $GITHUB_STEP_SUMMARY |
| 313 | + elif [ "${{ steps.get_changes.outputs.has_doc_changes }}" == "false" ]; then |
| 314 | + echo "## ℹ️ No Documentation Changes" >> $GITHUB_STEP_SUMMARY |
| 315 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 316 | + echo "This commit didn't contain any documentation changes, so no reverse sync was needed." >> $GITHUB_STEP_SUMMARY |
| 317 | + elif [ "${{ steps.commit_changes.outputs.has_changes }}" == "false" ]; then |
| 318 | + echo "## ℹ️ No Changes to Sync" >> $GITHUB_STEP_SUMMARY |
| 319 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 320 | + echo "The documentation changes were already present in target-docs." >> $GITHUB_STEP_SUMMARY |
| 321 | + else |
| 322 | + echo "## 🎉 Reverse Sync Completed" >> $GITHUB_STEP_SUMMARY |
| 323 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 324 | + echo "Successfully created a PR in target-docs with the documentation changes." >> $GITHUB_STEP_SUMMARY |
| 325 | + echo "" >> $GITHUB_STEP_SUMMARY |
| 326 | + echo "### Details:" >> $GITHUB_STEP_SUMMARY |
| 327 | + echo "- **Source PR**: #${{ steps.check_pr_commit.outputs.pr_number }} by @${{ steps.get_pr_info.outputs.pr_author }}" >> $GITHUB_STEP_SUMMARY |
| 328 | + echo "- **Branch**: ${{ steps.create_branch.outputs.branch_name }}" >> $GITHUB_STEP_SUMMARY |
| 329 | + echo "- **Base ref**: ${{ steps.get_pr_info.outputs.pr_base_ref }}" >> $GITHUB_STEP_SUMMARY |
| 330 | + echo "- **Target repository**: ${{env.TARGET_REPO}}" >> $GITHUB_STEP_SUMMARY |
| 331 | + fi |
0 commit comments