# Workflow for updating translation PRs on sync events name: Update Translation PR on: pull_request: types: [synchronize] permissions: contents: write pull-requests: write actions: read jobs: check-event-type: runs-on: ubuntu-latest outputs: should_run: ${{ steps.check-paths.outputs.should_run }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check if relevant files changed id: check-paths run: | echo "Checking if relevant files changed..." BASE_SHA="${{ github.event.pull_request.base.sha }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}" # Check if docs.json or en/ files changed CHANGED_FILES=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") RELEVANT_FILES=$(echo "$CHANGED_FILES" | grep -E '^(docs\.json|en/.*\.(md|mdx))$' || true) if [ -n "$RELEVANT_FILES" ]; then echo "should_run=true" >> $GITHUB_OUTPUT echo "✅ Relevant files changed - proceeding with translation update" echo "Changed files:" echo "$RELEVANT_FILES" else echo "should_run=false" >> $GITHUB_OUTPUT echo "ℹ️ No relevant files changed (docs.json or en/**/*.md|mdx)" echo "ℹ️ Skipping translation update" fi update-translation: needs: check-event-type runs-on: ubuntu-latest # dummy job to avoid getting error when no job is executed if: | needs.check-event-type.outputs.should_run == 'true' && github.event.pull_request.draft == false && !startsWith(github.event.pull_request.head.ref, 'docs-sync-pr-') steps: - name: Checkout PR uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Check if PR is English-only id: check-pr-type run: | echo "Checking if this PR contains only English changes..." BASE_SHA="${{ github.event.pull_request.base.sha }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}" # Use PR analyzer to check PR type cd tools/translate python pr_analyzer.py "$BASE_SHA" "$HEAD_SHA" > /tmp/pr_analysis_output.txt 2>&1 if [ $? -eq 0 ]; then # Parse analyzer output source /tmp/pr_analysis_output.txt echo "PR Type: $pr_type" echo "pr_type=$pr_type" >> $GITHUB_OUTPUT if [ "$pr_type" = "english" ]; then echo "✅ English-only PR detected - proceeding with translation update" echo "should_update=true" >> $GITHUB_OUTPUT else echo "ℹ️ Not an English-only PR (type: $pr_type) - skipping translation update" echo "should_update=false" >> $GITHUB_OUTPUT fi else echo "❌ PR analysis failed - likely mixed content, skipping translation update" echo "should_update=false" >> $GITHUB_OUTPUT echo "pr_type=unknown" >> $GITHUB_OUTPUT fi - name: Find associated translation PR if: steps.check-pr-type.outputs.should_update == 'true' id: find-translation-pr env: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER=${{ github.event.pull_request.number }} echo "Looking for translation PR associated with PR #${PR_NUMBER}..." # Search for translation PR by branch name pattern TRANSLATION_PR_DATA=$(gh pr list \ --search "head:docs-sync-pr-${PR_NUMBER}" \ --json number,title,url,state \ --jq '.[0] // empty' 2>/dev/null || echo "") if [ -n "$TRANSLATION_PR_DATA" ] && [ "$TRANSLATION_PR_DATA" != "null" ]; then TRANSLATION_PR_NUMBER=$(echo "$TRANSLATION_PR_DATA" | jq -r '.number') TRANSLATION_PR_STATE=$(echo "$TRANSLATION_PR_DATA" | jq -r '.state') TRANSLATION_PR_URL=$(echo "$TRANSLATION_PR_DATA" | jq -r '.url') if [ "$TRANSLATION_PR_STATE" = "OPEN" ]; then echo "✅ Found active translation PR #${TRANSLATION_PR_NUMBER}" echo "translation_pr_number=$TRANSLATION_PR_NUMBER" >> $GITHUB_OUTPUT echo "translation_pr_url=$TRANSLATION_PR_URL" >> $GITHUB_OUTPUT echo "found_translation_pr=true" >> $GITHUB_OUTPUT else echo "ℹ️ Found translation PR #${TRANSLATION_PR_NUMBER} but it's ${TRANSLATION_PR_STATE} - skipping update" echo "found_translation_pr=false" >> $GITHUB_OUTPUT fi else echo "ℹ️ No translation PR found for PR #${PR_NUMBER} - this might be the first update" echo "found_translation_pr=false" >> $GITHUB_OUTPUT fi - name: Determine update range if: steps.find-translation-pr.outputs.found_translation_pr == 'true' id: update-range env: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER=${{ github.event.pull_request.number }} TRANSLATION_PR_NUMBER="${{ steps.find-translation-pr.outputs.translation_pr_number }}" PR_BASE="${{ github.event.pull_request.base.sha }}" PR_HEAD="${{ github.event.pull_request.head.sha }}" echo "Determining incremental update range..." # Get last processed commit from translation PR comments LAST_PROCESSED=$(gh pr view "$TRANSLATION_PR_NUMBER" \ --json comments \ --jq '.comments | reverse | .[] | .body' 2>/dev/null \ | grep -oP 'Last-Processed-Commit: \K[a-f0-9]+' \ | head -1 || echo "") if [ -n "$LAST_PROCESSED" ]; then echo "✅ Found last processed commit: $LAST_PROCESSED" COMPARE_BASE="$LAST_PROCESSED" else echo "⚠️ No last processed commit found, using PR base" COMPARE_BASE="$PR_BASE" fi COMPARE_HEAD="$PR_HEAD" echo "compare_base=$COMPARE_BASE" >> $GITHUB_OUTPUT echo "compare_head=$COMPARE_HEAD" >> $GITHUB_OUTPUT echo "📊 Incremental update range: $COMPARE_BASE...$COMPARE_HEAD" - name: Install dependencies if: steps.find-translation-pr.outputs.found_translation_pr == 'true' run: | cd tools/translate pip install httpx aiofiles python-dotenv - name: Update translations if: steps.find-translation-pr.outputs.found_translation_pr == 'true' id: update-translations env: DIFY_API_KEY: ${{ secrets.DIFY_API_KEY }} run: | echo "Updating translations for PR #${{ github.event.pull_request.number }}..." PR_NUMBER=${{ github.event.pull_request.number }} SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}" PR_BRANCH="${{ github.event.pull_request.head.ref }}" BASE_SHA="${{ steps.update-range.outputs.compare_base }}" HEAD_SHA="${{ steps.update-range.outputs.compare_head }}" echo "Using incremental comparison: $BASE_SHA...$HEAD_SHA" # Switch to translation branch git fetch origin "$SYNC_BRANCH:$SYNC_BRANCH" || { echo "❌ Could not fetch translation branch $SYNC_BRANCH" echo "update_successful=false" >> $GITHUB_OUTPUT exit 0 } git checkout "$SYNC_BRANCH" # For incremental updates, checkout English files from PR HEAD git fetch origin "$PR_BRANCH:refs/remotes/origin/$PR_BRANCH" git checkout "origin/$PR_BRANCH" -- en/ || echo "No English files to checkout" # Re-run translation analysis and generation cd tools/translate # Create updated sync script cat > update_translations.py <<'EOF' import json import sys import os import asyncio import subprocess from pathlib import Path # Add parent directory to path sys.path.append(os.path.dirname(__file__)) from sync_and_translate import DocsSynchronizer from pr_analyzer import PRAnalyzer async def update_translations(): base_sha = sys.argv[1] head_sha = sys.argv[2] # Analyze changes analyzer = PRAnalyzer(base_sha, head_sha) result = analyzer.categorize_pr() if result['type'] != 'english': print(f"PR type is {result['type']}, not english - skipping") return False # Initialize synchronizer api_key = os.environ.get("DIFY_API_KEY") if not api_key: print("Error: DIFY_API_KEY not set") return False synchronizer = DocsSynchronizer(api_key) # Get English files that need translation file_categories = result['files'] english_files = file_categories['english'] results = { "translated": [], "failed": [], "skipped": [], "updated": True } # Translate English files for file_path in english_files[:10]: # Limit to 10 files for safety print(f"Updating translations for: {file_path}") try: for target_lang in ["cn", "jp"]: target_path = file_path.replace("en/", f"{target_lang}/") success = await synchronizer.translate_file_with_notice( file_path, target_path, target_lang ) if success: results["translated"].append(target_path) else: results["failed"].append(target_path) except Exception as e: print(f"Error processing {file_path}: {e}") results["failed"].append(file_path) # Handle docs.json structure sync if needed (INCREMENTAL MODE) docs_changes = result['docs_json_changes'] if docs_changes['any_docs_json_changes']: print("Updating docs.json structure (incremental mode)...") try: # Get added files added_files = english_files # Get deleted files from git diff deleted_files = [] try: print(f"Detecting deleted files between {base_sha[:8]} and {head_sha[:8]}...") result_git = subprocess.run([ "git", "diff", "--name-status", "--diff-filter=D", base_sha, head_sha ], capture_output=True, text=True, cwd="../../") if result_git.returncode != 0: print(f"Git diff failed: {result_git.stderr}") else: print(f"Git diff output:\n{result_git.stdout}") for line in result_git.stdout.strip().split('\n'): if line and line.startswith('D\t'): file_path = line.split('\t')[1] if file_path.startswith("en/"): deleted_files.append(file_path) print(f" Found deleted file: {file_path}") except Exception as e: print(f"Warning: Could not get deleted files: {e}") import traceback traceback.print_exc() print(f"Added files: {added_files}") print(f"Deleted files: {deleted_files}") # Delete corresponding translation files if deleted_files: print(f"\nDeleting corresponding translation files for {len(deleted_files)} deleted English files...") for en_file in deleted_files: for target_lang in ["cn", "jp"]: target_file = en_file.replace("en/", f"{target_lang}/") target_path = Path(f"../../{target_file}") if target_path.exists(): target_path.unlink() print(f"✓ Deleted {target_file}") results["translated"].append(f"deleted:{target_file}") # Remove empty parent directories parent = target_path.parent repo_root = Path("../../").resolve() while parent.resolve() != repo_root: try: # Only remove if directory is empty if not any(parent.iterdir()): parent.rmdir() print(f"✓ Removed empty directory {parent.relative_to(repo_root)}") parent = parent.parent else: break except (OSError, ValueError): break else: print(f" Skipped {target_file} (doesn't exist)") # Use incremental sync to update docs.json sync_log = synchronizer.sync_docs_json_incremental( added_files=added_files, deleted_files=deleted_files ) print("\n".join(sync_log)) except Exception as e: print(f"Error syncing docs.json structure: {e}") import traceback traceback.print_exc() # Save results with open("/tmp/update_results.json", "w") as f: json.dump(results, f, indent=2) return len(results["failed"]) == 0 if __name__ == "__main__": success = asyncio.run(update_translations()) sys.exit(0 if success else 1) EOF # Run the update python update_translations.py "$BASE_SHA" "$HEAD_SHA" UPDATE_EXIT_CODE=$? echo "update_exit_code=$UPDATE_EXIT_CODE" >> $GITHUB_OUTPUT # Check for changes if [[ -n $(git status --porcelain) ]]; then echo "has_changes=true" >> $GITHUB_OUTPUT echo "✅ Translation updates detected" else echo "has_changes=false" >> $GITHUB_OUTPUT echo "ℹ️ No translation updates needed" fi - name: Commit and push translation updates if: steps.update-translations.outputs.has_changes == 'true' id: commit-updates env: GH_TOKEN: ${{ github.token }} run: | PR_NUMBER=${{ github.event.pull_request.number }} SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}" HEAD_SHA="${{ steps.update-range.outputs.compare_head }}" git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' # Commit only translation changes (not English files) git add cn/ jp/ docs.json git commit -m "🔄 Update translations for commit ${HEAD_SHA:0:8}"$'\n\n'"Auto-generated translation updates for changes in commit ${HEAD_SHA}."$'\n\n'"Last-Processed-Commit: ${HEAD_SHA}"$'\n'"Original-PR: #${PR_NUMBER}"$'\n'"Languages: Chinese (cn), Japanese (jp)"$'\n\n'"🤖 Generated with GitHub Actions" # Push updates to translation branch (no force - preserve history) git push origin "$SYNC_BRANCH" # Add tracking comment to translation PR TRANSLATION_PR_NUMBER="${{ steps.find-translation-pr.outputs.translation_pr_number }}" COMMENT_BODY=""$'\n'"🔄 **Updated for commit \`${HEAD_SHA:0:8}\`**"$'\n\n'"Latest source changes from PR #${PR_NUMBER} have been translated and committed."$'\n\n'"**Source commit:** [\`${HEAD_SHA:0:8}\`](https://github.com/${{ github.repository }}/commit/${HEAD_SHA})"$'\n'"**Original PR:** #${PR_NUMBER}" gh pr comment "$TRANSLATION_PR_NUMBER" --body "$COMMENT_BODY" || echo "Failed to add comment" echo "commit_successful=true" >> $GITHUB_OUTPUT echo "✅ Translation updates committed and pushed" - name: Comment on original PR about update if: steps.update-translations.outputs.has_changes == 'true' && steps.commit-updates.outputs.commit_successful == 'true' uses: actions/github-script@v7 continue-on-error: true with: script: | const fs = require('fs'); const prNumber = ${{ github.event.pull_request.number }}; const translationPrNumber = '${{ steps.find-translation-pr.outputs.translation_pr_number }}'; const translationPrUrl = '${{ steps.find-translation-pr.outputs.translation_pr_url }}'; // Load update results let results = { translated: [], failed: [], skipped: [] }; try { results = JSON.parse(fs.readFileSync('/tmp/update_results.json', 'utf8')); } catch (e) { console.log('Could not load update results'); } let comment = `## 🔄 Translation PR Updated\n\n` comment += `Your English documentation changes have been automatically translated and the translation PR has been updated.\n\n` comment += `### 📝 Original Changes\n\n`; comment += `- **Original PR**: #${prNumber}\n`; comment += `- **Type**: English documentation updates\n`; comment += `### 🔄 Synchronization\n\n`; comment += `- **Automatic Updates**: This PR will be automatically updated if the original PR changes\n`; comment += `- **Independent Review**: This translation PR can be reviewed and merged independently\n`; if (results.translated && results.translated.length > 0) { comment += `### ✅ Updated Translations (${results.translated.length} files):\n`; results.translated.slice(0, 6).forEach(file => { comment += `- \`${file}\`\n`; }); if (results.translated.length > 6) { comment += `- ... and ${results.translated.length - 6} more files\n`; } comment += '\n'; } if (results.failed && results.failed.length > 0) { comment += `### ⚠️ Update Issues (${results.failed.length}):\n`; results.failed.slice(0, 3).forEach(file => { comment += `- \`${file}\`\n`; }); comment += '\n'; } comment += `### 🔄 What's Updated: - **Translation Files**: All corresponding cn and jp files - **Navigation Structure**: Updated docs.json if needed - **Automatic**: This update happened automatically when you updated your PR --- 🤖 _Automatic update from the translation workflow._`; try { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: comment }); } catch (error) { console.log('Could not comment on original PR:', error.message); } - name: Comment on translation PR about update if: steps.update-translations.outputs.has_changes == 'true' && steps.commit-updates.outputs.commit_successful == 'true' uses: actions/github-script@v7 continue-on-error: true with: script: | const fs = require('fs'); const prNumber = ${{ github.event.pull_request.number }}; const translationPrNumber = '${{ steps.find-translation-pr.outputs.translation_pr_number }}'; // Load update results let results = { translated: [], failed: [], skipped: [] }; try { results = JSON.parse(fs.readFileSync('/tmp/update_results.json', 'utf8')); } catch (e) { console.log('Could not load update results'); } const updateComment = `## 🔄 Automatic Translation Update This translation PR has been automatically updated following changes in the original PR #${prNumber}. ### 📝 What Was Updated: - **Source**: Changes from PR #${prNumber} - **Updated Files**: ${results.translated ? results.translated.length : 0} translation files - **Languages**: Chinese (cn) and Japanese (jp) ### ✅ Translation Status: ${results.translated && results.translated.length > 0 ? `**Successfully Updated (${results.translated.length} files):**\n` + results.translated.slice(0, 5).map(f => `- \`${f}\``).join('\n') + (results.translated.length > 5 ? `\n- ... and ${results.translated.length - 5} more` : '') : '- All translations are up to date'} ${results.failed && results.failed.length > 0 ? `\n### ⚠️ Update Issues:\n${results.failed.slice(0, 3).map(f => `- \`${f}\``).join('\n')}` : ''} ### 🔄 Review Process: 1. **Automatic Update**: This PR was updated automatically 2. **Review Needed**: Please review the updated translations 3. **Independent Merge**: This PR can still be merged independently --- 🤖 _This update was triggered automatically by changes to PR #${prNumber}._`; try { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: translationPrNumber, body: updateComment }); } catch (error) { console.log('Could not comment on translation PR:', error.message); } - name: Handle no updates needed if: steps.find-translation-pr.outputs.found_translation_pr == 'true' && steps.update-translations.outputs.has_changes != 'true' uses: actions/github-script@v7 continue-on-error: true with: script: | const prNumber = ${{ github.event.pull_request.number }}; const translationPrNumber = '${{ steps.find-translation-pr.outputs.translation_pr_number }}'; const comment = `## ✅ Translation PR Already Up to Date Your changes to PR #${prNumber} did not require translation updates. The translation PR [#${translationPrNumber}](https://github.com/${{ github.repository }}/pull/${translationPrNumber}) remains current. 🤖 _Automatic check from the translation workflow._`; try { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: comment }); } catch (error) { console.log('Could not comment on original PR:', error.message); }