Files
wehub-resource-sync 3a46cbf880
Site (mkdocs + mdBook) → GitHub Pages / Build mkdocs (root) + mdBook (/book/) (push) Has been cancelled
Site (mkdocs + mdBook) → GitHub Pages / Deploy to GitHub Pages (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:13:20 +08:00

185 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Mirror sync reminder.
當 PR 改了 zh-TW canonical .md 檔(path 內含 stages/ branches/ tracks/ resources/
walkthroughs/ 或 README.md / CONTRIBUTING.md)、但沒同時改對應的 .en.md / .zh-Hans.md
mirror、產生一個 PR comment body 寫到 .mirror-sync-comment.md、提示 contributor。
Soft reminder — 不擋 PR。zh-TW canonical、mirror sync 是 Path Bslower cadence)。
Usage:
python scripts/check-mirror-sync.py --pr-base origin/main
GitHub Actions integration:
Sets GitHub Actions output `gap_detected=true|false`.
Workflow uses sticky comment action to keep one comment per PR.
"""
from __future__ import annotations
import argparse
import os
import subprocess
import sys
from pathlib import Path
MIRROR_SUFFIXES = ['.en.md', '.zh-Hans.md']
def get_changed_files(base_ref: str) -> list[Path]:
"""Get list of files changed in PR vs base_ref."""
try:
out = subprocess.check_output(
['git', 'diff', '--name-only', f'{base_ref}...HEAD'],
encoding='utf-8',
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
print(f'⚠ git diff failed: {e}', file=sys.stderr)
# Fall back to single-commit diff
try:
out = subprocess.check_output(
['git', 'diff', '--name-only', 'HEAD~1'],
encoding='utf-8',
)
except Exception:
return []
return [Path(f.strip()) for f in out.splitlines() if f.strip()]
def is_canonical_md(path: Path) -> bool:
"""zh-TW canonical = .md but not .en.md / .zh-Hans.md."""
name = str(path)
if not name.endswith('.md'):
return False
for suffix in MIRROR_SUFFIXES:
if name.endswith(suffix):
return False
return True
def detect_sync_gaps(
changed: list[Path], repo_root: Path
) -> list[tuple[str, list[str]]]:
"""
Return list of (canonical_file_changed, [missing_mirror_files]) tuples.
Only flag canonical files whose mirrors EXIST in repo (so contributor
isn't asked to create a new mirror from scratch).
"""
changed_set = {str(p) for p in changed}
gaps: list[tuple[str, list[str]]] = []
for f in changed:
s = str(f).replace('\\', '/') # normalize for Windows
if not is_canonical_md(Path(s)):
continue
# For X.md, compute X.en.md and X.zh-Hans.md candidate paths
stem = s[:-3] # strip .md
candidates = [(suf, stem + suf) for suf in MIRROR_SUFFIXES]
missing = []
for suf, candidate_path in candidates:
cand_full = repo_root / candidate_path
if not cand_full.exists():
continue # no mirror exists yet — don't pressure contributor
# Mirror exists. Check if it was also changed in this PR.
normalized = candidate_path.replace('\\', '/')
if normalized not in changed_set and candidate_path not in changed_set:
missing.append(candidate_path)
if missing:
gaps.append((s, missing))
return gaps
def write_comment_body(gaps: list[tuple[str, list[str]]], out_path: Path) -> None:
"""Write GitHub PR comment body listing the sync gaps."""
lines = [
'## 🌐 Mirror Sync Reminder',
'',
'zh-TW canonical files were updated in this PR, but the following mirror',
'files were **not** updated in the same change set:',
'',
]
for canonical, missing in gaps:
lines.append(f'- **`{canonical}`** modified — missing sync to:')
for m in missing:
lines.append(f' - `{m}`')
lines.extend(
[
'',
'> 💡 **This is a soft reminder, not a blocker.** zh-TW is the canonical',
'> version of this repo. Mirror files (`.en.md` / `.zh-Hans.md`) sync',
'> is on a slower cadence (Path B). Update when convenient — or open a',
'> separate sync PR later.',
'',
'> If the canonical change is purely cosmetic (typo, whitespace, etc.),',
'> mirror sync can be safely skipped.',
'',
]
)
out_path.write_text('\n'.join(lines), encoding='utf-8')
def set_gh_output(name: str, value: str) -> None:
"""Set a GitHub Actions output variable."""
out_file = os.environ.get('GITHUB_OUTPUT')
if out_file:
with open(out_file, 'a', encoding='utf-8') as fh:
fh.write(f'{name}={value}\n')
# Also print for local debug
print(f'{name}={value}')
def main() -> int:
# Force UTF-8 stdout
if hasattr(sys.stdout, 'reconfigure'):
try:
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
except Exception:
pass
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--pr-base',
default='origin/main',
help='Base ref to diff against (default: origin/main)',
)
parser.add_argument(
'--comment-out',
default='.mirror-sync-comment.md',
help='Path to write PR comment body',
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
changed = get_changed_files(args.pr_base)
if not changed:
print('No changed files detected.')
set_gh_output('gap_detected', 'false')
return 0
gaps = detect_sync_gaps(changed, repo_root)
if not gaps:
print('✓ All canonical changes have synced mirrors (or no mirror exists yet).')
set_gh_output('gap_detected', 'false')
return 0
print(f'⚠ Detected {len(gaps)} canonical file(s) with unsynced mirrors:')
for canonical, missing in gaps:
print(f' - {canonical}: missing {", ".join(missing)}')
write_comment_body(gaps, repo_root / args.comment_out)
set_gh_output('gap_detected', 'true')
return 0
if __name__ == '__main__':
sys.exit(main())