name: Execute Documentation Sync on: workflow_run: workflows: ["Analyze Documentation Changes"] types: - completed branches: [main, revamp] workflow_dispatch: inputs: pr_number: description: 'PR number to process' required: true type: string permissions: contents: write pull-requests: write actions: read jobs: execute-sync: runs-on: ubuntu-latest if: | (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || github.event_name == 'workflow_dispatch' steps: - name: Check workflow source id: check-source run: | echo "Checking workflow source..." echo "Event name: ${{ github.event_name }}" if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then # Manual trigger - bypass source checks for revamp branch if [[ "${{ github.ref }}" == "refs/heads/revamp" ]]; then echo "Manual workflow_dispatch on revamp branch - proceeding" echo "is_fork=false" >> $GITHUB_OUTPUT echo "should_process=true" >> $GITHUB_OUTPUT else echo "Manual workflow_dispatch not on revamp branch - skipping" echo "should_process=false" >> $GITHUB_OUTPUT fi else # workflow_run event - use existing logic echo "Event: ${{ github.event.workflow_run.event }}" echo "Repository: ${{ github.event.workflow_run.repository.full_name }}" echo "Head Repository: ${{ github.event.workflow_run.head_repository.full_name }}" echo "Head Branch: ${{ github.event.workflow_run.head_branch }}" # Security check: Only process PRs from the same repository or trusted forks if [[ "${{ github.event.workflow_run.event }}" != "pull_request" ]]; then echo "Not a pull request event, skipping" echo "should_process=false" >> $GITHUB_OUTPUT exit 0 fi # Check if this is from a fork IS_FORK="false" if [[ "${{ github.event.workflow_run.repository.full_name }}" != "${{ github.event.workflow_run.head_repository.full_name }}" ]]; then IS_FORK="true" fi echo "is_fork=$IS_FORK" >> $GITHUB_OUTPUT echo "should_process=true" >> $GITHUB_OUTPUT fi - name: Download analysis artifacts if: steps.check-source.outputs.should_process == 'true' uses: actions/github-script@v7 id: download-artifacts with: script: | const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: ${{ github.event.workflow_run.id }} }); const matchArtifact = artifacts.data.artifacts.find(artifact => { return artifact.name.startsWith('docs-sync-analysis-'); }); if (!matchArtifact) { console.log('No analysis artifacts found'); return false; } const download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, artifact_id: matchArtifact.id, archive_format: 'zip' }); const fs = require('fs'); fs.writeFileSync('/tmp/artifacts.zip', Buffer.from(download.data)); // Extract PR number from artifact name const prNumber = matchArtifact.name.split('-').pop(); core.setOutput('pr_number', prNumber); core.setOutput('artifact_found', 'true'); return true; - name: Extract and validate artifacts if: steps.download-artifacts.outputs.artifact_found == 'true' id: extract-artifacts run: | echo "Extracting artifacts..." # Create secure temporary directory WORK_DIR=$(mktemp -d /tmp/sync-XXXXXX) echo "work_dir=$WORK_DIR" >> $GITHUB_OUTPUT # Extract to temporary directory cd "$WORK_DIR" unzip /tmp/artifacts.zip # Validate extracted files REQUIRED_FILES="analysis.json sync_plan.json changed_files.txt" for file in $REQUIRED_FILES; do if [ ! -f "$file" ]; then echo "Error: Required file $file not found" exit 1 fi done # Validate JSON structure python3 -c " import json import sys try: with open('analysis.json') as f: analysis = json.load(f) with open('sync_plan.json') as f: sync_plan = json.load(f) # Validate required fields assert 'pr_number' in analysis assert 'files_to_sync' in sync_plan assert 'target_languages' in sync_plan print('Artifacts validated successfully') except Exception as e: print(f'Validation error: {e}') sys.exit(1) " # Extract PR number and other metadata PR_NUMBER=$(python3 -c "import json; print(json.load(open('analysis.json'))['pr_number'])") echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT # Check if sync is required SYNC_REQUIRED=$(python3 -c "import json; print(str(json.load(open('sync_plan.json'))['sync_required']).lower())") echo "sync_required=$SYNC_REQUIRED" >> $GITHUB_OUTPUT - name: Checkout base repository if: steps.extract-artifacts.outputs.sync_required == 'true' uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Set up Python if: steps.extract-artifacts.outputs.sync_required == 'true' uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies if: steps.extract-artifacts.outputs.sync_required == 'true' run: | cd tools/translate pip install httpx aiofiles python-dotenv - name: Check for manual approval requirement if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-source.outputs.is_fork == 'true' id: check-approval uses: actions/github-script@v7 with: script: | const prNumber = ${{ steps.extract-artifacts.outputs.pr_number }}; // Get PR details const pr = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); const author = pr.data.user.login; const authorAssociation = pr.data.author_association; // Check if author is trusted const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR']; const trustedContributors = process.env.TRUSTED_CONTRIBUTORS?.split(',') || []; const isTrusted = trustedAssociations.includes(authorAssociation) || trustedContributors.includes(author); if (!isTrusted) { // Check for approval from maintainer const reviews = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber }); const hasApproval = reviews.data.some(review => review.state === 'APPROVED' && trustedAssociations.includes(review.author_association) ); if (!hasApproval) { console.log('PR requires manual approval from a maintainer'); core.setOutput('needs_approval', 'true'); // Comment on PR await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: '⏸️ **Documentation sync is pending approval**\n\n' + 'This PR requires approval from a maintainer before automatic synchronization can proceed.\n\n' + 'Once approved, the documentation will be automatically translated and synchronized.' }); return; } } core.setOutput('needs_approval', 'false'); - name: Execute safe synchronization if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-approval.outputs.needs_approval != 'true' id: sync env: DIFY_API_KEY: ${{ secrets.DIFY_API_KEY }} run: | echo "Executing documentation synchronization..." WORK_DIR="${{ steps.extract-artifacts.outputs.work_dir }}" PR_NUMBER="${{ steps.extract-artifacts.outputs.pr_number }}" # Create a new branch for the sync results SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}" git checkout -b "$SYNC_BRANCH" # Run synchronization with security constraints cd tools/translate # Create a secure sync script cat > secure_sync.py <<'EOF' import json import sys import os import asyncio from pathlib import Path # Add parent directory to path sys.path.append(os.path.dirname(__file__)) from sync_and_translate import DocsSynchronizer async def secure_sync(): work_dir = sys.argv[1] # Load sync plan with open(f"{work_dir}/sync_plan.json") as f: sync_plan = json.load(f) # Security: Only sync files from the approved list files_to_sync = sync_plan.get("files_to_sync", []) # Validate file paths again for file_info in files_to_sync: file_path = file_info["path"] # Security checks if ".." in file_path or file_path.startswith("/"): print(f"Security error: Invalid path {file_path}") return False if not file_path.startswith("en/"): print(f"Security error: File outside en/ directory: {file_path}") 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) # Perform limited sync results = { "translated": [], "failed": [], "skipped": [] } for file_info in files_to_sync[:10]: # Limit to 10 files file_path = file_info["path"] print(f"Processing: {file_path}") try: # Only translate if file exists and is safe if os.path.exists(f"../../{file_path}"): for target_lang in ["zh-hans", "ja-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) else: results["skipped"].append(file_path) except Exception as e: print(f"Error processing {file_path}: {e}") results["failed"].append(file_path) # Handle docs.json structure sync if needed if sync_plan.get("structure_changes", {}).get("structure_changed"): print("Syncing docs.json structure...") try: sync_log = synchronizer.sync_docs_json_structure() print("\n".join(sync_log)) except Exception as e: print(f"Error syncing structure: {e}") # Save results with open("/tmp/sync_results.json", "w") as f: json.dump(results, f, indent=2) return len(results["failed"]) == 0 if __name__ == "__main__": success = asyncio.run(secure_sync()) sys.exit(0 if success else 1) EOF # Run the secure sync python secure_sync.py "$WORK_DIR" SYNC_EXIT_CODE=$? echo "sync_exit_code=$SYNC_EXIT_CODE" >> $GITHUB_OUTPUT # Check for changes if [[ -n $(git status --porcelain) ]]; then echo "has_changes=true" >> $GITHUB_OUTPUT else echo "has_changes=false" >> $GITHUB_OUTPUT fi - name: Commit and create translation PR if: steps.sync.outputs.has_changes == 'true' id: create-translation-pr run: | PR_NUMBER="${{ steps.extract-artifacts.outputs.pr_number }}" SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}" git config user.name 'github-actions[bot]' git config user.email 'github-actions[bot]@users.noreply.github.com' # Commit translation changes git add . git commit -m "🌐 Auto-translate documentation for PR #${PR_NUMBER} Auto-generated translations for documentation changes in PR #${PR_NUMBER}. Original PR: #${PR_NUMBER} Languages: Chinese (zh-hans), Japanese (ja-jp) πŸ€– Generated with GitHub Actions" # Push the translation branch to main repo git push origin "$SYNC_BRANCH" --force # Get original PR details for translation PR ORIGINAL_PR_TITLE=$(gh pr view ${PR_NUMBER} --json title --jq '.title' 2>/dev/null || echo "Documentation changes") # Create translation PR body cat > /tmp/translation_pr_body.md </dev/null || echo "") if [ -n "$TRANSLATION_PR_URL" ]; then # Extract PR number from URL TRANSLATION_PR_NUMBER=$(echo "$TRANSLATION_PR_URL" | grep -o '[0-9]\+$') echo "translation_pr_number=$TRANSLATION_PR_NUMBER" >> $GITHUB_OUTPUT echo "translation_pr_url=$TRANSLATION_PR_URL" >> $GITHUB_OUTPUT echo "branch_name=$SYNC_BRANCH" >> $GITHUB_OUTPUT echo "creation_successful=true" >> $GITHUB_OUTPUT echo "βœ… Translation PR created successfully: #${TRANSLATION_PR_NUMBER}" else echo "❌ Failed to create translation PR" echo "creation_successful=false" >> $GITHUB_OUTPUT fi - name: Comment on original PR with translation PR link if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-approval.outputs.needs_approval != 'true' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const prNumber = ${{ steps.extract-artifacts.outputs.pr_number }}; const hasChanges = '${{ steps.sync.outputs.has_changes }}' === 'true'; const translationPrNumber = '${{ steps.create-translation-pr.outputs.translation_pr_number }}'; const translationPrUrl = '${{ steps.create-translation-pr.outputs.translation_pr_url }}'; const creationSuccessful = '${{ steps.create-translation-pr.outputs.creation_successful }}' === 'true'; let comment = '## πŸ€– Automatic Translation Status\n\n'; if (hasChanges && creationSuccessful && translationPrNumber) { // Load sync results if available let results = { translated: [], failed: [], skipped: [] }; try { results = JSON.parse(fs.readFileSync('/tmp/sync_results.json', 'utf8')); } catch (e) { console.log('Could not load sync results'); results = { translated: [], failed: [], skipped: [] }; } comment += `πŸŽ‰ **Translation PR Created Successfully!**\n\n`; comment += `Your English documentation changes have been automatically translated and a separate PR has been created.\n\n`; comment += `### πŸ”— Translation PR: [#${translationPrNumber}](${translationPrUrl})\n\n`; if (results.translated && results.translated.length > 0) { comment += `### βœ… Successfully Translated (${results.translated.length} files):\n`; results.translated.slice(0, 8).forEach(file => { comment += `- \`${file}\`\n`; }); if (results.translated.length > 8) { comment += `- ... and ${results.translated.length - 8} more files\n`; } comment += '\n'; } if (results.failed && results.failed.length > 0) { comment += `### ⚠️ Translation Issues (${results.failed.length}):\n`; results.failed.slice(0, 5).forEach(file => { comment += `- \`${file}\`\n`; }); if (results.failed.length > 5) { comment += `- ... and ${results.failed.length - 5} more\n`; } comment += '\n'; } comment += '### πŸ”„ What Happens Next:\n'; comment += `1. **Review**: The translation PR [#${translationPrNumber}](${translationPrUrl}) is ready for review\n`; comment += '2. **Independent**: Both PRs can be reviewed and merged independently\n'; comment += '3. **Automatic**: Future updates to this PR will automatically update the translation PR\n\n'; comment += '### πŸ“‹ Languages Included:\n'; comment += '- πŸ‡¨πŸ‡³ **Chinese (zh-hans)**: Simplified Chinese translations\n'; comment += '- πŸ‡―πŸ‡΅ **Japanese (ja-jp)**: Japanese translations\n'; comment += '- πŸ“‹ **Navigation**: Updated docs.json structure for both languages\n\n'; comment += '---\n'; comment += '_πŸ€– This is an automated translation workflow. The translation PR was created automatically and is ready for review._'; } else if (hasChanges && !creationSuccessful) { comment += '⚠️ **Translation PR Creation Failed**\n\n'; comment += 'The automatic translation process completed, but there was an issue creating the translation PR.\n\n'; comment += '**What you can do:**\n'; comment += '1. Check the workflow logs for detailed error information\n'; comment += '2. Contact a maintainer if the issue persists\n'; comment += '3. The translations may have been generated but need manual PR creation\n\n'; comment += '_πŸ€– This is an automated notification from the translation workflow._'; } else { comment += 'βœ… **No Translation Changes Needed**\n\n'; comment += 'Your changes did not require new translations, or all translations are already up to date.\n\n'; comment += '_πŸ€– This is an automated check from the translation workflow._'; } await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body: comment }); - name: Comment on translation PR with original PR link if: steps.create-translation-pr.outputs.creation_successful == 'true' && steps.create-translation-pr.outputs.translation_pr_number uses: actions/github-script@v7 continue-on-error: true with: script: | const prNumber = ${{ steps.extract-artifacts.outputs.pr_number }}; const translationPrNumber = ${{ steps.create-translation-pr.outputs.translation_pr_number }}; const backLinkComment = [ '## πŸ”— Linked to Original PR', '', `This translation PR was automatically created for the English documentation changes in **PR #${prNumber}**.`, '', '### πŸ“ Original Changes', `- **Original PR**: #${prNumber}`, '- **Type**: English documentation updates', '- **Auto-translation**: This PR contains the corresponding translations', '', '### πŸ”„ Synchronization', '- **Automatic Updates**: This PR will be automatically updated if the original PR changes', '- **Independent Review**: This translation PR can be reviewed and merged independently', '- **Quality Check**: Please review translations for accuracy and cultural appropriateness', '', '---', `πŸ€– _This PR is part of the automated translation workflow. Any updates to PR #${prNumber} will automatically update this translation PR._` ].join('\\n'); try { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: translationPrNumber, body: backLinkComment }); console.log(`Successfully linked translation PR #${translationPrNumber} to original PR #${prNumber}`); } catch (error) { console.log(`Could not comment on translation PR #${translationPrNumber}:`, error.message); } handle-failure: runs-on: ubuntu-latest if: github.event.workflow_run.conclusion == 'failure' steps: - name: Report analysis failure uses: actions/github-script@v7 with: script: | // Try to extract PR number from workflow run const workflowRun = context.payload.workflow_run; console.log('Analysis workflow failed'); console.log('Attempting to notify PR if possible...'); // This is a best-effort attempt to notify // In practice, you might want to store PR number differently