mirror of
https://github.com/langgenius/dify-docs.git
synced 2026-03-27 13:28:32 +07:00
fix: prevent race condition properly while maintaining security
Instead of removing 'synchronize' trigger from analyze workflow (which is needed for security checks), we now make the execute workflow skip when: - It's an incremental update (synchronize event) - AND a translation branch already exists This ensures: - Security checks always run (analyze workflow on all PR events) - No race condition (execute skips, update handles incremental changes) - Initial PRs still work (execute runs when no translation branch exists) The logic: - PR opened/reopened: analyze → execute → create new translation PR - PR synchronized: analyze (security) + update → update existing translation PR - Execute workflow sees incremental=true + branch_exists=true and skips
This commit is contained in:
2
.github/workflows/sync_docs_analyze.yml
vendored
2
.github/workflows/sync_docs_analyze.yml
vendored
@@ -2,7 +2,7 @@ name: Analyze Documentation Changes
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- 'docs.json'
|
||||
- 'en/**/*.md'
|
||||
|
||||
29
.github/workflows/sync_docs_execute.yml
vendored
29
.github/workflows/sync_docs_execute.yml
vendored
@@ -244,8 +244,29 @@ jobs:
|
||||
cd tools/translate
|
||||
pip install httpx aiofiles python-dotenv
|
||||
|
||||
- name: Check if update workflow should handle this
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true'
|
||||
id: check-update-workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
IS_INCREMENTAL="${{ steps.extract-artifacts.outputs.is_incremental }}"
|
||||
BRANCH_EXISTS="${{ steps.check-branch.outputs.branch_exists }}"
|
||||
PR_NUMBER="${{ steps.extract-artifacts.outputs.pr_number }}"
|
||||
|
||||
# If this is an incremental update AND translation branch exists,
|
||||
# skip execute workflow and let update workflow handle it
|
||||
if [[ "$IS_INCREMENTAL" == "true" && "$BRANCH_EXISTS" == "true" ]]; then
|
||||
echo "⏭️ Incremental update detected with existing translation branch"
|
||||
echo " Update workflow will handle this - skipping execute workflow"
|
||||
echo "should_skip=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "should_skip=false" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check for manual approval requirement
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-source.outputs.is_fork == 'true'
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-source.outputs.is_fork == 'true' && steps.check-update-workflow.outputs.should_skip != 'true'
|
||||
id: check-approval
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
@@ -303,7 +324,7 @@ jobs:
|
||||
core.setOutput('needs_approval', 'false');
|
||||
|
||||
- name: Run translation and commit
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-approval.outputs.needs_approval != 'true'
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-approval.outputs.needs_approval != 'true' && steps.check-update-workflow.outputs.should_skip != 'true'
|
||||
id: translate
|
||||
env:
|
||||
DIFY_API_KEY: ${{ secrets.DIFY_API_KEY }}
|
||||
@@ -369,7 +390,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Comment on original PR with translation PR link
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-approval.outputs.needs_approval != 'true'
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true' && steps.check-approval.outputs.needs_approval != 'true' && steps.check-update-workflow.outputs.should_skip != 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
@@ -464,7 +485,7 @@ jobs:
|
||||
});
|
||||
|
||||
- name: Comment on translation PR with original PR link
|
||||
if: steps.translate.outputs.creation_successful == 'true' && steps.translate.outputs.translation_pr_number && steps.check-branch.outputs.branch_exists == 'false'
|
||||
if: steps.translate.outputs.creation_successful == 'true' && steps.translate.outputs.translation_pr_number && steps.check-branch.outputs.branch_exists == 'false' && steps.check-update-workflow.outputs.should_skip != 'true'
|
||||
uses: actions/github-script@v7
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
941
.github/workflows/sync_docs_execute.yml.backup
vendored
Normal file
941
.github/workflows/sync_docs_execute.yml.backup
vendored
Normal file
@@ -0,0 +1,941 @@
|
||||
# Workflow for executing documentation translations
|
||||
name: Execute Documentation Sync
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Analyze Documentation Changes"]
|
||||
types:
|
||||
- completed
|
||||
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_dispatch' || github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Check workflow source
|
||||
id: check-source
|
||||
run: |
|
||||
echo "Checking workflow source..."
|
||||
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
|
||||
|
||||
- name: Download analysis artifacts
|
||||
if: steps.check-source.outputs.should_process == 'true' || github.event_name == 'workflow_dispatch'
|
||||
uses: actions/github-script@v7
|
||||
id: download-artifacts
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Determine which workflow run to get artifacts from
|
||||
let runId;
|
||||
let prNumber;
|
||||
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
// Manual trigger - use the pr_number input
|
||||
prNumber = '${{ github.event.inputs.pr_number }}';
|
||||
console.log(`Manual trigger for PR #${prNumber}`);
|
||||
|
||||
// Find the most recent analyze workflow run for this specific PR
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'sync_docs_analyze.yml',
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Find run that matches our specific PR number
|
||||
let matchingRun = null;
|
||||
for (const run of runs.data.workflow_runs) {
|
||||
if (run.conclusion === 'success' && run.event === 'pull_request' && run.pull_requests.length > 0) {
|
||||
const pullRequest = run.pull_requests[0];
|
||||
if (pullRequest.number.toString() === prNumber) {
|
||||
matchingRun = run;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingRun) {
|
||||
console.log(`No successful analyze workflow run found for PR #${prNumber}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
runId = matchingRun.id;
|
||||
console.log(`Found analyze workflow run: ${runId} for PR #${prNumber}`);
|
||||
} else {
|
||||
// Triggered by workflow_run
|
||||
runId = context.payload.workflow_run.id;
|
||||
console.log(`Workflow run trigger, run ID: ${runId}`);
|
||||
}
|
||||
|
||||
// List artifacts from the analyze workflow run
|
||||
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: runId
|
||||
});
|
||||
|
||||
console.log(`Found ${artifacts.data.artifacts.length} artifacts`);
|
||||
artifacts.data.artifacts.forEach(a => console.log(` - ${a.name}`));
|
||||
|
||||
const matchArtifact = artifacts.data.artifacts.find(artifact => {
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
return artifact.name === `docs-sync-analysis-${prNumber}`;
|
||||
} else {
|
||||
return artifact.name.startsWith('docs-sync-analysis-');
|
||||
}
|
||||
});
|
||||
|
||||
if (!matchArtifact) {
|
||||
console.log('No matching analysis artifact found');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`Downloading artifact: ${matchArtifact.name}`);
|
||||
|
||||
const download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip'
|
||||
});
|
||||
|
||||
fs.writeFileSync('/tmp/artifacts.zip', Buffer.from(download.data));
|
||||
console.log('Artifact downloaded successfully');
|
||||
|
||||
// Extract PR number from artifact name
|
||||
if (!prNumber) {
|
||||
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
|
||||
|
||||
# Extract head SHA to checkout the PR branch (needed for new files)
|
||||
HEAD_SHA=$(python3 -c "import json; print(json.load(open('analysis.json'))['head_sha'])")
|
||||
echo "head_sha=$HEAD_SHA" >> $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 PR branch
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.extract-artifacts.outputs.head_sha }} # Checkout PR's head commit to access new files
|
||||
|
||||
- name: Check if translation branch exists
|
||||
if: steps.extract-artifacts.outputs.sync_required == 'true'
|
||||
id: check-branch
|
||||
run: |
|
||||
PR_NUMBER="${{ steps.extract-artifacts.outputs.pr_number }}"
|
||||
SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}"
|
||||
|
||||
# Check if translation branch exists on remote (after repo checkout)
|
||||
if git ls-remote --exit-code --heads origin "$SYNC_BRANCH" >/dev/null 2>&1; then
|
||||
echo "✅ Translation branch exists: $SYNC_BRANCH"
|
||||
echo "branch_exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "🆕 Translation branch does not exist yet"
|
||||
echo "branch_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- 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 }}"
|
||||
BRANCH_EXISTS="${{ steps.check-branch.outputs.branch_exists }}"
|
||||
HEAD_SHA="${{ steps.extract-artifacts.outputs.head_sha }}"
|
||||
|
||||
# Prepare translation branch
|
||||
SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}"
|
||||
|
||||
if [ "$BRANCH_EXISTS" = "true" ]; then
|
||||
echo "✅ Fetching existing translation branch for incremental update"
|
||||
git fetch origin "$SYNC_BRANCH:$SYNC_BRANCH"
|
||||
git checkout "$SYNC_BRANCH"
|
||||
|
||||
# For incremental updates, we're already on the right base
|
||||
# Just checkout English files from PR to translate
|
||||
git checkout "$HEAD_SHA" -- en/ 2>/dev/null || echo "No English files to update"
|
||||
|
||||
else
|
||||
echo "🆕 Creating new translation branch"
|
||||
git checkout -b "$SYNC_BRANCH"
|
||||
|
||||
# Reset branch to main to avoid including English file changes from PR
|
||||
# Use --soft to keep working directory with PR files (needed for translation)
|
||||
git reset --soft origin/main
|
||||
# Unstage everything
|
||||
git reset
|
||||
fi
|
||||
|
||||
# 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
|
||||
import subprocess
|
||||
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 translation config
|
||||
config_path = Path("../../tools/translate/config.json")
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
translation_config = json.load(f)
|
||||
|
||||
# Get language settings from config
|
||||
SOURCE_LANGUAGE = translation_config.get("source_language", "en")
|
||||
TARGET_LANGUAGES = translation_config.get("target_languages", ["cn", "jp"])
|
||||
source_dir = translation_config["languages"][SOURCE_LANGUAGE]["directory"]
|
||||
|
||||
# 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
|
||||
|
||||
# Allow source language files and docs.json
|
||||
if not (file_path.startswith(f"{source_dir}/") or file_path == "docs.json"):
|
||||
print(f"Security error: File outside {source_dir}/ 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": []
|
||||
}
|
||||
|
||||
# Get metadata for diff detection
|
||||
metadata = sync_plan.get("metadata", {})
|
||||
base_sha = metadata.get("base_sha", "")
|
||||
head_sha = metadata.get("head_sha", "")
|
||||
|
||||
# Detect modified vs added files
|
||||
added_files = []
|
||||
modified_files = []
|
||||
|
||||
if base_sha and head_sha:
|
||||
try:
|
||||
# Get list of added files (A) and modified files (M)
|
||||
result = subprocess.run([
|
||||
"git", "diff", "--name-status", "--diff-filter=AM",
|
||||
base_sha, head_sha
|
||||
], capture_output=True, text=True, cwd="../../")
|
||||
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line and '\t' in line:
|
||||
status, file_path = line.split('\t', 1)
|
||||
if status == 'A':
|
||||
added_files.append(file_path)
|
||||
elif status == 'M':
|
||||
modified_files.append(file_path)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not detect file status: {e}")
|
||||
# Fallback: treat all as added
|
||||
added_files = [f["path"] for f in files_to_sync if f["path"].startswith(f"{source_dir}/")]
|
||||
|
||||
# If we couldn't detect, treat all as added (safe fallback)
|
||||
if not added_files and not modified_files:
|
||||
added_files = [f["path"] for f in files_to_sync if f["path"].startswith(f"{source_dir}/")]
|
||||
|
||||
print(f"Detected {len(added_files)} added files, {len(modified_files)} modified files")
|
||||
|
||||
for file_info in files_to_sync[:10]: # Limit to 10 files
|
||||
file_path = file_info["path"]
|
||||
print(f"Processing: {file_path}")
|
||||
|
||||
# Skip docs.json - it's handled separately in structure sync
|
||||
if file_path == "docs.json":
|
||||
results["skipped"].append(f"{file_path} (structure file - handled separately)")
|
||||
continue
|
||||
|
||||
# Skip versioned directories (frozen/archived docs)
|
||||
if file_path.startswith("versions/"):
|
||||
results["skipped"].append(f"{file_path} (versioned - not auto-translated)")
|
||||
continue
|
||||
|
||||
try:
|
||||
# Only translate if file exists and is safe
|
||||
if os.path.exists(f"../../{file_path}"):
|
||||
# Determine if this is a modified file
|
||||
is_modified = file_path in modified_files
|
||||
|
||||
# Get diff for modified files
|
||||
diff_original = None
|
||||
if is_modified and base_sha and head_sha:
|
||||
try:
|
||||
diff_result = subprocess.run([
|
||||
"git", "diff", base_sha, head_sha, "--", file_path
|
||||
], capture_output=True, text=True, cwd="../../")
|
||||
if diff_result.returncode == 0:
|
||||
diff_original = diff_result.stdout
|
||||
print(f" Retrieved diff for {file_path} ({len(diff_original)} chars)")
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not get diff for {file_path}: {e}")
|
||||
|
||||
for target_lang in TARGET_LANGUAGES:
|
||||
target_dir = translation_config["languages"][target_lang]["directory"]
|
||||
target_path = file_path.replace(f"{source_dir}/", f"{target_dir}/")
|
||||
|
||||
# Load existing translation for modified files
|
||||
the_doc_exist = None
|
||||
if is_modified:
|
||||
target_full_path = Path(f"../../{target_path}")
|
||||
if target_full_path.exists():
|
||||
try:
|
||||
with open(target_full_path, 'r', encoding='utf-8') as f:
|
||||
the_doc_exist = f.read()
|
||||
print(f" Loaded existing translation: {target_path} ({len(the_doc_exist)} chars)")
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not read existing translation {target_path}: {e}")
|
||||
|
||||
# Translate with appropriate inputs
|
||||
success = await synchronizer.translate_file_with_notice(
|
||||
file_path,
|
||||
target_path,
|
||||
target_lang,
|
||||
the_doc_exist=the_doc_exist,
|
||||
diff_original=diff_original
|
||||
)
|
||||
if success:
|
||||
change_type = "modified" if is_modified else "added"
|
||||
results["translated"].append(f"{target_path} ({change_type})")
|
||||
else:
|
||||
results["failed"].append(target_path)
|
||||
else:
|
||||
results["skipped"].append(file_path)
|
||||
except Exception as e:
|
||||
print(f"Error processing {file_path}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results["failed"].append(file_path)
|
||||
|
||||
# Process OpenAPI JSON files
|
||||
openapi_files_to_sync = sync_plan.get("openapi_files_to_sync", [])
|
||||
print(f"\nProcessing {len(openapi_files_to_sync)} OpenAPI files...")
|
||||
|
||||
# Import OpenAPI translation function (async version)
|
||||
from openapi import translate_openapi_file_async
|
||||
|
||||
for file_info in openapi_files_to_sync[:5]: # Limit to 5 OpenAPI files
|
||||
file_path = file_info["path"]
|
||||
print(f"Processing OpenAPI: {file_path}")
|
||||
|
||||
try:
|
||||
source_full_path = Path(f"../../{file_path}")
|
||||
if not source_full_path.exists():
|
||||
results["skipped"].append(f"{file_path} (source file not found)")
|
||||
continue
|
||||
|
||||
for target_lang in TARGET_LANGUAGES:
|
||||
target_dir = translation_config["languages"][target_lang]["directory"]
|
||||
target_path = file_path.replace(f"{source_dir}/", f"{target_dir}/")
|
||||
target_full_path = Path(f"../../{target_path}")
|
||||
|
||||
# Ensure target directory exists
|
||||
target_full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Run OpenAPI translation pipeline (use await for async version)
|
||||
success = await translate_openapi_file_async(
|
||||
source_file=str(source_full_path),
|
||||
target_lang=target_lang,
|
||||
output_file=str(target_full_path),
|
||||
dify_api_key=api_key
|
||||
)
|
||||
|
||||
if success:
|
||||
results["translated"].append(f"{target_path} (openapi)")
|
||||
print(f"✅ Successfully translated OpenAPI: {file_path} → {target_path}")
|
||||
else:
|
||||
results["failed"].append(target_path)
|
||||
print(f"❌ Failed to translate OpenAPI: {file_path} → {target_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing OpenAPI {file_path}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
results["failed"].append(file_path)
|
||||
|
||||
# Handle docs.json structure sync if needed (INCREMENTAL MODE)
|
||||
if sync_plan.get("structure_changes", {}).get("structure_changed"):
|
||||
print("Syncing docs.json structure (incremental mode)...")
|
||||
try:
|
||||
# Get added files (those we just translated)
|
||||
# Include both markdown files and OpenAPI files
|
||||
added_files = [f["path"] for f in files_to_sync if f["path"].startswith(f"{source_dir}/")]
|
||||
added_files += [f["path"] for f in openapi_files_to_sync if f["path"].startswith(f"{source_dir}/")]
|
||||
|
||||
# Get deleted files from git diff
|
||||
# Use the metadata to get base and head SHAs
|
||||
metadata = sync_plan.get("metadata", {})
|
||||
base_sha = metadata.get("base_sha", "")
|
||||
head_sha = metadata.get("head_sha", "")
|
||||
|
||||
deleted_files = []
|
||||
if base_sha and head_sha:
|
||||
try:
|
||||
print(f"Detecting deleted files between {base_sha[:8]} and {head_sha[:8]}...")
|
||||
|
||||
# Get deleted files from git
|
||||
result = subprocess.run([
|
||||
"git", "diff", "--name-status", "--diff-filter=D",
|
||||
base_sha, head_sha
|
||||
], capture_output=True, text=True, cwd="../../")
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Git diff failed: {result.stderr}")
|
||||
else:
|
||||
print(f"Git diff output:\n{result.stdout}")
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line and line.startswith('D\t'):
|
||||
file_path = line.split('\t')[1]
|
||||
if file_path.startswith(f"{source_dir}/"):
|
||||
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()
|
||||
else:
|
||||
print(f"Warning: Missing SHAs (base: {base_sha}, head: {head_sha})")
|
||||
|
||||
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 source files...")
|
||||
for source_file in deleted_files:
|
||||
for target_lang in TARGET_LANGUAGES:
|
||||
target_dir = translation_config["languages"][target_lang]["directory"]
|
||||
target_file = source_file.replace(f"{source_dir}/", f"{target_dir}/")
|
||||
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 structure: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# 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
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
PR_NUMBER="${{ steps.extract-artifacts.outputs.pr_number }}"
|
||||
SYNC_BRANCH="docs-sync-pr-${PR_NUMBER}"
|
||||
BRANCH_EXISTS="${{ steps.check-branch.outputs.branch_exists }}"
|
||||
HEAD_SHA="${{ steps.extract-artifacts.outputs.head_sha }}"
|
||||
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
# Setup branch
|
||||
if [ "$BRANCH_EXISTS" = "true" ]; then
|
||||
# Fetch and checkout existing translation branch
|
||||
echo "Checking out existing translation branch: $SYNC_BRANCH"
|
||||
git fetch origin "$SYNC_BRANCH"
|
||||
git checkout -B "$SYNC_BRANCH" "origin/$SYNC_BRANCH"
|
||||
else
|
||||
# Create new translation branch (use -B to handle local branch conflicts)
|
||||
echo "Creating new translation branch: $SYNC_BRANCH"
|
||||
git checkout -B "$SYNC_BRANCH"
|
||||
fi
|
||||
|
||||
# Remove English source files from working directory (they shouldn't be in translation PR)
|
||||
# These files exist from line 320 where we checkout en/ files for translation
|
||||
rm -rf en/*.md en/*.mdx 2>/dev/null || true
|
||||
# Also ensure any staged en/ files are unstaged
|
||||
git reset HEAD -- en/ 2>/dev/null || true
|
||||
echo "✓ Removed English source files from working directory and staging area"
|
||||
|
||||
# Commit translation changes only (not English files from PR)
|
||||
git add cn/ jp/ docs.json
|
||||
|
||||
if [ "$BRANCH_EXISTS" = "true" ]; then
|
||||
# Incremental commit
|
||||
git commit -m "🔄 Update translations for commit ${HEAD_SHA:0:8}"$'\n\n'"Auto-generated translations 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"
|
||||
else
|
||||
# Initial commit
|
||||
git commit -m "🌐 Initial translations for PR #${PR_NUMBER}"$'\n\n'"Auto-generated translations for documentation changes in PR #${PR_NUMBER}."$'\n\n'"Last-Processed-Commit: ${HEAD_SHA}"$'\n'"Original-PR: #${PR_NUMBER}"$'\n'"Languages: Chinese (cn), Japanese (jp)"$'\n\n'"🤖 Generated with GitHub Actions"
|
||||
fi
|
||||
|
||||
# Push to translation branch (no force - preserve history)
|
||||
git push origin "$SYNC_BRANCH"
|
||||
|
||||
# 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")
|
||||
|
||||
if [ "$BRANCH_EXISTS" = "false" ]; then
|
||||
# Create new translation PR
|
||||
echo "Creating new translation PR..."
|
||||
|
||||
# Create translation PR body
|
||||
cat > /tmp/translation_pr_body.md <<EOF
|
||||
# 🌐 Auto-generated Translations
|
||||
|
||||
This PR contains automatically generated translations for the documentation changes in PR #${PR_NUMBER}.
|
||||
|
||||
## Original PR
|
||||
**Title:** ${ORIGINAL_PR_TITLE}
|
||||
**Link:** #${PR_NUMBER}
|
||||
|
||||
## What's included
|
||||
- 🇨🇳 Chinese (cn) translations
|
||||
- 🇯🇵 Japanese (jp) translations
|
||||
- 📋 Updated navigation structure in docs.json
|
||||
|
||||
## Review Process
|
||||
1. Review the generated translations for accuracy
|
||||
2. Make any necessary adjustments
|
||||
3. Merge this PR to apply the translations
|
||||
|
||||
## Links
|
||||
- **Original English PR:** #${PR_NUMBER}
|
||||
- **Translation branch:** \`${SYNC_BRANCH}\`
|
||||
|
||||
---
|
||||
🤖 This PR was created automatically by the documentation translation workflow.
|
||||
EOF
|
||||
|
||||
# Create the translation PR
|
||||
TRANSLATION_PR_URL=$(gh pr create \
|
||||
--base main \
|
||||
--head "$SYNC_BRANCH" \
|
||||
--title "🌐 Auto-translations for PR #${PR_NUMBER}: ${ORIGINAL_PR_TITLE}" \
|
||||
--body-file /tmp/translation_pr_body.md 2>&1 || echo "")
|
||||
|
||||
if [ -z "$TRANSLATION_PR_URL" ]; then
|
||||
echo "❌ Failed to create translation PR"
|
||||
echo "creation_successful=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# 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}"
|
||||
fi
|
||||
|
||||
else
|
||||
# Translation PR already exists - just add update comment
|
||||
echo "Translation PR already exists, adding update comment..."
|
||||
|
||||
# Find existing translation PR
|
||||
TRANSLATION_PR_NUMBER=$(gh pr list \
|
||||
--search "head:$SYNC_BRANCH state:open" \
|
||||
--json number \
|
||||
--jq '.[0].number // empty' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$TRANSLATION_PR_NUMBER" ]; then
|
||||
echo "translation_pr_number=$TRANSLATION_PR_NUMBER" >> $GITHUB_OUTPUT
|
||||
echo "creation_successful=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Add tracking comment with last processed commit
|
||||
COMMENT_BODY="<!-- Last-Processed-Commit: ${HEAD_SHA} -->"$'\n'"🔄 **Updated for commit \`${HEAD_SHA:0:8}\`**"$'\n\n'"Latest source changes have been translated and committed to this PR."$'\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 "✅ Translation PR updated: #${TRANSLATION_PR_NUMBER}"
|
||||
else
|
||||
echo "❌ Could not find translation PR"
|
||||
echo "creation_successful=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
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';
|
||||
const branchExists = '${{ steps.check-branch.outputs.branch_exists }}' === 'true';
|
||||
const headSha = '${{ steps.extract-artifacts.outputs.head_sha }}';
|
||||
|
||||
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: [] };
|
||||
}
|
||||
|
||||
if (branchExists) {
|
||||
// Translation PR was updated
|
||||
comment += `🔄 **Translation PR Updated!**\n\n`;
|
||||
comment += `Your latest changes (commit \`${headSha.substring(0, 8)}\`) have been automatically translated and added to the translation PR.\n\n`;
|
||||
} else {
|
||||
// Translation PR was created
|
||||
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 || `https://github.com/${{ github.repository }}/pull/${translationPrNumber}`})\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 (cn)**: Simplified Chinese translations\n';
|
||||
comment += '- 🇯🇵 **Japanese (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 && steps.check-branch.outputs.branch_exists == 'false'
|
||||
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\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
|
||||
552
.github/workflows/sync_docs_update.yml.backup
vendored
Normal file
552
.github/workflows/sync_docs_update.yml.backup
vendored
Normal file
@@ -0,0 +1,552 @@
|
||||
# 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="<!-- Last-Processed-Commit: ${HEAD_SHA} -->"$'\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);
|
||||
}
|
||||
Reference in New Issue
Block a user