Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions .github/scripts/i18n/build_pending_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

Environment:
LOCALE, LOCALE_SLUG, MODE, SHARD_INDEX, SHARD_TOTAL, optional
PENDING_LIMIT, and GITHUB_OUTPUT.
PENDING_LIMIT, CANARY_SOURCE_PATH, and GITHUB_OUTPUT.

Outputs:
Writes docs-i18n-<locale_slug>-s<index>of<total>.txt under --openclaw-sync-dir.
Expand Down Expand Up @@ -93,6 +93,7 @@ def build_pending_manifest(
shard_index: int,
shard_total: int,
pending_limit: int = 0,
canary_source_path: str = "",
) -> PendingResult:
locale_dirs = {path.name for path in docs_root.iterdir() if is_locale_dir(path)}
pending_path = openclaw_sync_dir / f"docs-i18n-{locale_slug}-s{shard_index}of{shard_total}.txt"
Expand All @@ -118,9 +119,21 @@ def build_pending_manifest(
pending_files = sorted(pending_files)
shard_files = [file for index, file in enumerate(pending_files) if index % shard_total == shard_index]
if pending_limit:
# Full canary intentionally translates a tiny deterministic sample; the
# follow-up locale batch still runs the complete manifest after the gate.
shard_files = shard_files[:pending_limit]
if canary_source_path:
canary_source = (docs_root / canary_source_path).resolve()
try:
canary_source.relative_to(docs_root.resolve())
except ValueError as exc:
raise SystemExit(f"configured canary source must stay under docs: {canary_source_path}") from exc
if canary_source not in shard_files:
raise SystemExit(f"configured canary source is not pending in this shard: {canary_source_path}")
# Prefer a user-visible page with known glossary coverage so the
# canary proves both translation and the deployed page content.
shard_files = [canary_source]
else:
# Full canary publishes a real one-page probe before expensive batches,
# so choose the smallest deterministic sample to cap token and review cost.
shard_files = sorted(shard_files, key=lambda file: (file.stat().st_size, file.as_posix()))[:pending_limit]

pending_path.parent.mkdir(parents=True, exist_ok=True)
pending_path.write_text("\n".join(str(file) for file in shard_files) + ("\n" if shard_files else ""), encoding="utf-8")
Expand Down Expand Up @@ -156,7 +169,7 @@ def parse_args() -> argparse.Namespace:

Examples:
LOCALE=fr LOCALE_SLUG=fr MODE=incremental SHARD_INDEX=0 SHARD_TOTAL=1 python .github/scripts/i18n/build_pending_manifest.py
LOCALE=zh-CN LOCALE_SLUG=zh-cn MODE=full SHARD_INDEX=1 SHARD_TOTAL=4 PENDING_LIMIT=1 python .github/scripts/i18n/build_pending_manifest.py
LOCALE=zh-CN LOCALE_SLUG=zh-cn MODE=full SHARD_INDEX=0 SHARD_TOTAL=1 PENDING_LIMIT=1 CANARY_SOURCE_PATH=channels/line.md python .github/scripts/i18n/build_pending_manifest.py
""",
)
parser.add_argument("--docs-root", default="docs", type=Path)
Expand All @@ -177,6 +190,7 @@ def main() -> None:
shard_index=shard_index,
shard_total=shard_total,
pending_limit=pending_limit,
canary_source_path=os.environ.get("CANARY_SOURCE_PATH", ""),
)
append_output(result)

Expand Down
83 changes: 77 additions & 6 deletions .github/scripts/i18n/commit_locale_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
This script owns the per-locale commit/push control plane for full
translation recovery. It expects locale files to have already been applied
and validated, then commits only docs/<locale> and that locale's translation
memory. It retries rebase/push conflicts while guarding against source
metadata moving after artifact application.
memory. Canary commits are additionally restricted to the sampled page and
locale translation memory. It retries rebase/push conflicts while guarding
against source metadata moving after artifact application.

Parameters:
--locale: Locale directory to commit. Default: LOCALE environment variable.
--base-source-sha: Source SHA observed after artifact application. Default:
BASE_SOURCE_SHA environment variable.
--artifact-dir: Downloaded locale artifact directory. Default: ARTIFACT_DIR.
--attempts: Push/rebase retry count. Default: 5.

Outputs:
Expand All @@ -22,7 +24,8 @@

Examples:
LOCALE=fr BASE_SOURCE_SHA=abc python .github/scripts/i18n/commit_locale_artifact.py
python .github/scripts/i18n/commit_locale_artifact.py --locale zh-CN --base-source-sha abc --attempts 3
ARTIFACT_ROLE=canary ARTIFACT_DIR=.openclaw-sync/i18n-artifacts/zh-cn-s0of1 LOCALE=zh-CN BASE_SOURCE_SHA=abc python .github/scripts/i18n/commit_locale_artifact.py
python .github/scripts/i18n/commit_locale_artifact.py --locale zh-CN --base-source-sha abc --artifact-dir .openclaw-sync/i18n-artifacts/zh-cn-s0of1 --attempts 3
"""

from __future__ import annotations
Expand All @@ -31,6 +34,7 @@
import json
import os
import subprocess
import sys
import time
from pathlib import Path

Expand Down Expand Up @@ -84,12 +88,69 @@ def has_locale_changes(locale: str) -> bool:
return bool(result.stdout.strip())


def commit_locale(locale: str, base_source_sha: str, attempts: int) -> bool:
def pending_allowed(locale: str, locale_slug: str, shard_index: str, shard_total: str) -> set[str]:
pending_file = Path(".openclaw-sync") / f"docs-i18n-{locale_slug}-s{shard_index}of{shard_total}.txt"
allowed = {f"docs/.i18n/{locale}.tm.jsonl"}
if not pending_file.exists():
raise SystemExit(f"missing canary pending manifest: {pending_file}")
docs_root = Path("docs").resolve()
for line in pending_file.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
source = Path(line.strip()).resolve()
rel = source.relative_to(docs_root).as_posix()
allowed.add(f"docs/{locale}/{rel}")
return allowed


def artifact_allowed(locale: str, artifact_dir: str) -> set[str]:
artifact = Path(artifact_dir)
if not artifact.exists():
raise SystemExit(f"missing canary artifact directory: {artifact}")
deleted = [line for line in (artifact / "deleted-files.txt").read_text(encoding="utf-8").splitlines() if line.strip()]
if deleted:
raise SystemExit(f"canary artifact unexpectedly included deleted paths: {', '.join(deleted)}")
allowed = set()
for line in (artifact / "changed-files.txt").read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
if line == f"docs/.i18n/{locale}.tm.jsonl" or line.startswith(f"docs/{locale}/"):
allowed.add(line)
continue
raise SystemExit(f"canary artifact changed path outside locale scope: {line}")
return allowed


def enforce_canary_scope(locale: str, allowed: set[str]) -> None:
status = git_stdout(["status", "--porcelain", "--untracked-files=all", "--", f"docs/{locale}", f"docs/.i18n/{locale}.tm.jsonl"])
changed = {line[3:] for line in status.splitlines() if line.strip()}
bad = sorted(path for path in changed if path not in allowed)
if bad:
print("Canary commit touched paths outside the sampled page contract:", file=sys.stderr)
for path in bad:
print(path, file=sys.stderr)
raise SystemExit(1)


def commit_locale(
locale: str,
base_source_sha: str,
attempts: int,
artifact_role: str = "",
locale_slug: str = "",
shard_index: str = "0",
shard_total: str = "1",
artifact_dir: str = "",
) -> bool:
if not has_locale_changes(locale):
print(f"No {locale} translation changes.")
write_output("committed", "false")
return False

if artifact_role == "canary":
allowed = artifact_allowed(locale, artifact_dir) if artifact_dir else pending_allowed(locale, locale_slug or locale, shard_index, shard_total)
enforce_canary_scope(locale, allowed)

git_stdout(["config", "user.name", "openclaw-docs-i18n[bot]"])
git_stdout(["config", "user.email", "openclaw-docs-i18n[bot]@users.noreply.github.com"])
git_stdout(["add", f"docs/{locale}", f"docs/.i18n/{locale}.tm.jsonl"])
Expand Down Expand Up @@ -121,11 +182,12 @@ def parse_args() -> argparse.Namespace:

Examples:
LOCALE=fr BASE_SOURCE_SHA=abc python .github/scripts/i18n/commit_locale_artifact.py
python .github/scripts/i18n/commit_locale_artifact.py --locale zh-CN --base-source-sha abc --attempts 3
python .github/scripts/i18n/commit_locale_artifact.py --locale zh-CN --base-source-sha abc --artifact-dir .openclaw-sync/i18n-artifacts/zh-cn-s0of1 --attempts 3
""",
)
parser.add_argument("--locale", default=os.environ.get("LOCALE", ""))
parser.add_argument("--base-source-sha", default=os.environ.get("BASE_SOURCE_SHA", ""))
parser.add_argument("--artifact-dir", default=os.environ.get("ARTIFACT_DIR", ""))
parser.add_argument("--attempts", default=5, type=int)
return parser.parse_args()

Expand All @@ -138,7 +200,16 @@ def main() -> None:
raise SystemExit("missing base source sha: pass --base-source-sha or set BASE_SOURCE_SHA")
if args.attempts < 1:
raise SystemExit("attempts must be >= 1")
commit_locale(args.locale, args.base_source_sha, args.attempts)
commit_locale(
args.locale,
args.base_source_sha,
args.attempts,
artifact_role=os.environ.get("ARTIFACT_ROLE", ""),
locale_slug=os.environ.get("LOCALE_SLUG", args.locale),
shard_index=os.environ.get("SHARD_INDEX", "0"),
shard_total=os.environ.get("SHARD_TOTAL", "1"),
artifact_dir=args.artifact_dir,
)


if __name__ == "__main__":
Expand Down
Loading