Skip to content

Commit 61b37d8

Browse files
guchenheclaude
andauthored
Fix stale PR translation revert issue (#630)
* Fix stale PR translation revert issue When PR A is created before PR B but PR B merges first, the translation workflow for PR A was reverting all of PR B's changes. This happened because the translation workflow used PR A's working directory state (which is a snapshot from before PR B existed) rather than applying only PR A's changes. Root cause: - setup_translation_branch() for new branches did: checkout -b branch → reset --soft origin/main → reset This kept PR's working directory which could be stale - For incremental branches, merge_docs_json_for_incremental_update() took the English section from PR HEAD, which was also stale for old PRs Fix: - For NEW branches: Create branch directly from origin/main (not from PR's working directory). This ensures we start with the latest state including all changes from PRs merged after this PR was created. - For EXISTING branches: Merge main's docs.json structure with our translations (instead of taking EN section from stale PR) - For BOTH: Selectively checkout only the files that the PR actually changed from PR's head, rather than bringing in the entire working directory. This prevents overwriting files from other PRs. Example issue (PR #593): - PR #593 only added one file - Translation PR #611 tried to delete 11 files and revert massive docs.json changes - This was because it used PR #593's stale state from before other PRs merged 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix: Use PR's docs.json for finding file positions in navigation The initial fix had a side effect: since we start from main's docs.json, and PR's new files aren't in main's English section yet, sync_docs_json_incremental() couldn't find where to place new files in the translation navigation. Fix: Add `reference_sha` parameter to sync_docs_json_incremental() that loads PR's docs.json for finding file positions, while still modifying main's translation sections. This ensures: 1. Main's docs.json structure is preserved (no reverts) 2. New files are found in PR's docs.json 3. Translations are added at the correct positions This also removes the unused _apply_pr_english_section_to_main() method. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix EN section not updated when using reference_sha When the translation branch starts from main, the PR's docs.json structural changes (new file entries in EN section) were not being incorporated. This caused the translation PR to have mismatched navigation entries. The fix now also updates the EN section of the working directory's docs.json when processing added files found in the reference docs.json (from the PR). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Also remove deleted files from EN section in stale PR scenario When processing deleted files, the sync now also removes them from the EN section of docs.json. This is needed when the translation branch starts from main, which may still have the deleted file entries. Verified with comprehensive local testing covering 10 scenarios: - Basic stale PR, multiple files, modifications, deletions - Nested groups, new dropdowns, mixed operations - Backward compatibility, incremental syncs, structure changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 61466c3 commit 61b37d8

2 files changed

Lines changed: 202 additions & 56 deletions

File tree

tools/translate/sync_and_translate.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,11 +1383,19 @@ def sync_docs_json_incremental(
13831383
deleted_files: List[str] = None,
13841384
renamed_files: List[Tuple[str, str]] = None,
13851385
base_sha: str = None,
1386-
head_sha: str = None
1386+
head_sha: str = None,
1387+
reference_sha: str = None
13871388
) -> List[str]:
13881389
"""
13891390
Incrementally sync docs.json structure - only processes changed files.
13901391
Preserves existing dropdown names and only updates affected pages.
1392+
1393+
Args:
1394+
reference_sha: If provided, use this commit's docs.json for finding file positions
1395+
in the English section. This is needed when the working directory's
1396+
docs.json (from main) doesn't have the new files yet (stale PR scenario).
1397+
The translation sections are still modified in the working directory's
1398+
docs.json.
13911399
"""
13921400
sync_log = []
13931401
added_files = added_files or []
@@ -1447,7 +1455,7 @@ def sync_docs_json_incremental(
14471455
sync_log.append("ERROR: No languages found in navigation")
14481456
return sync_log
14491457

1450-
# Find language sections
1458+
# Find language sections in working directory docs.json (for modifying translations)
14511459
source_section = None
14521460
target_sections = {}
14531461

@@ -1457,6 +1465,40 @@ def sync_docs_json_incremental(
14571465
elif lang_data.get("language") in self.target_languages:
14581466
target_sections[lang_data.get("language")] = lang_data
14591467

1468+
# If reference_sha is provided, load that docs.json for finding file positions
1469+
# This is needed when working directory (main) doesn't have the new files yet
1470+
reference_source_section = source_section
1471+
if reference_sha and added_files:
1472+
try:
1473+
import subprocess
1474+
result = subprocess.run(
1475+
["git", "show", f"{reference_sha}:docs.json"],
1476+
cwd=self.base_dir,
1477+
capture_output=True,
1478+
text=True,
1479+
check=True
1480+
)
1481+
ref_docs = json.loads(result.stdout)
1482+
ref_navigation = ref_docs.get("navigation", {})
1483+
1484+
# Handle both structures
1485+
ref_languages = None
1486+
if "languages" in ref_navigation and isinstance(ref_navigation["languages"], list):
1487+
ref_languages = ref_navigation["languages"]
1488+
elif "versions" in ref_navigation and len(ref_navigation["versions"]) > 0:
1489+
if "languages" in ref_navigation["versions"][0]:
1490+
ref_languages = ref_navigation["versions"][0]["languages"]
1491+
1492+
if ref_languages:
1493+
for lang_data in ref_languages:
1494+
if lang_data.get("language") == self.source_language:
1495+
reference_source_section = lang_data
1496+
sync_log.append(f"INFO: Using reference docs.json from {reference_sha[:8]} for finding file positions")
1497+
break
1498+
except Exception as e:
1499+
sync_log.append(f"WARNING: Could not load reference docs.json from {reference_sha}: {e}")
1500+
# Fall back to working directory's source section
1501+
14601502
if not source_section:
14611503
sync_log.append("ERROR: Source language section not found")
14621504
return sync_log
@@ -1469,7 +1511,8 @@ def sync_docs_json_incremental(
14691511
continue
14701512

14711513
# Find which dropdown contains this file in source language section
1472-
result = self.find_dropdown_containing_file(source_file, source_section)
1514+
# Use reference_source_section (from PR's docs.json) to find new file positions
1515+
result = self.find_dropdown_containing_file(source_file, reference_source_section)
14731516
if not result:
14741517
sync_log.append(f"WARNING: Could not find {source_file} in source language navigation")
14751518
continue
@@ -1478,9 +1521,10 @@ def sync_docs_json_incremental(
14781521
sync_log.append(f"INFO: Found {source_file} in '{source_dropdown_name}' dropdown at location {file_location}")
14791522

14801523
# Get the source language dropdown for reference
1524+
# Use reference_source_section (from PR's docs.json) for new file structure
14811525
source_dropdown = None
14821526
source_dropdown_index = -1
1483-
for i, dropdown in enumerate(source_section.get("dropdowns", [])):
1527+
for i, dropdown in enumerate(reference_source_section.get("dropdowns", [])):
14841528
if dropdown.get("dropdown") == source_dropdown_name:
14851529
source_dropdown = dropdown
14861530
source_dropdown_index = i
@@ -1490,6 +1534,40 @@ def sync_docs_json_incremental(
14901534
sync_log.append(f"WARNING: Could not find source language dropdown '{source_dropdown_name}'")
14911535
continue
14921536

1537+
# STALE PR FIX: Also update the working directory's EN section if the file
1538+
# was found in reference docs.json but doesn't exist in current EN section.
1539+
# This happens when the translation branch is based on main but the PR's
1540+
# docs.json hasn't merged yet.
1541+
if reference_source_section != source_section:
1542+
# Find or create corresponding dropdown in working directory's EN section
1543+
wd_en_dropdown = None
1544+
wd_en_dropdowns = source_section.get("dropdowns", [])
1545+
if source_dropdown_index >= 0 and source_dropdown_index < len(wd_en_dropdowns):
1546+
wd_en_dropdown = wd_en_dropdowns[source_dropdown_index]
1547+
1548+
if not wd_en_dropdown:
1549+
# Dropdown doesn't exist in working directory - create it
1550+
wd_en_dropdown = {
1551+
"dropdown": source_dropdown.get("dropdown", ""),
1552+
"icon": source_dropdown.get("icon", "book-open"),
1553+
"pages": []
1554+
}
1555+
source_section.setdefault("dropdowns", [])
1556+
# Ensure we have enough slots
1557+
while len(source_section["dropdowns"]) <= source_dropdown_index:
1558+
source_section["dropdowns"].append(None)
1559+
source_section["dropdowns"][source_dropdown_index] = wd_en_dropdown
1560+
sync_log.append(f"INFO: Created EN dropdown '{wd_en_dropdown['dropdown']}' from PR")
1561+
1562+
if wd_en_dropdown:
1563+
# Add the page to EN section at the correct location
1564+
if "pages" not in wd_en_dropdown:
1565+
wd_en_dropdown["pages"] = []
1566+
1567+
added_to_en = self.add_page_at_location(wd_en_dropdown, source_file, file_location, source_dropdown)
1568+
if added_to_en:
1569+
sync_log.append(f"INFO: Added {source_file} to EN section (from PR's docs.json)")
1570+
14931571
# Add to each target language
14941572
for target_lang, target_section in target_sections.items():
14951573
target_file = self.convert_path_to_target_language(source_file, target_lang)
@@ -1548,6 +1626,14 @@ def sync_docs_json_incremental(
15481626

15491627
sync_log.append(f"INFO: Processing deletion of {source_file}")
15501628

1629+
# STALE PR FIX: Also remove from EN section if it exists there
1630+
# This is needed when starting from main's docs.json which may still have the file
1631+
for en_dropdown in source_section.get("dropdowns", []):
1632+
if "pages" in en_dropdown:
1633+
if self.remove_page_from_structure(en_dropdown["pages"], source_file):
1634+
sync_log.append(f"INFO: Removed {source_file} from EN section")
1635+
break
1636+
15511637
# Remove from each target language
15521638
for target_lang, target_section in target_sections.items():
15531639
target_file = self.convert_path_to_target_language(source_file, target_lang)

0 commit comments

Comments
 (0)