diff --git a/.github/scripts/i18n/mdx_repair_scope.py b/.github/scripts/i18n/mdx_repair_scope.py new file mode 100644 index 000000000..7fcc653c6 --- /dev/null +++ b/.github/scripts/i18n/mdx_repair_scope.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Guard the generated-doc MDX repair step. + +Definition: + This script records the locale files that were already untracked before the + MDX repair agent ran, then verifies that the repair only changed allowed + locale-controlled paths and did not create additional untracked locale files. + +Parameters: + command: snapshot or enforce. + --baseline: File path used to store the pre-repair untracked locale snapshot. + --workspace: Git workspace root. Default: GITHUB_WORKSPACE or current dir. + --locale: Locale directory name. Default: LOCALE environment variable. + +Outputs: + snapshot writes the baseline file and prints its path. + enforce prints a short success message or exits non-zero with offending paths. + +Examples: + LOCALE=fr python .github/scripts/i18n/mdx_repair_scope.py snapshot --baseline .openclaw-sync/mdx/fr.repair-baseline.txt + LOCALE=fr python .github/scripts/i18n/mdx_repair_scope.py enforce --baseline .openclaw-sync/mdx/fr.repair-baseline.txt +""" + +from __future__ import annotations + +import argparse +import os +import subprocess +from pathlib import Path + + +def git_lines(workspace: Path, args: list[str]) -> list[str]: + result = subprocess.run(["git", *args], cwd=workspace, check=True, text=True, stdout=subprocess.PIPE) + return [line.strip() for line in result.stdout.splitlines() if line.strip()] + + +def untracked_locale_files(workspace: Path, locale: str) -> list[str]: + return sorted(git_lines(workspace, ["ls-files", "--others", "--exclude-standard", "--", f"docs/{locale}"])) + + +def write_lines(path: Path, lines: list[str]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8") + + +def read_lines(path: Path) -> set[str]: + if not path.exists(): + raise SystemExit(f"missing repair scope baseline: {path}") + return {line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()} + + +def is_allowed_changed_path(path: str, locale: str) -> bool: + return path.startswith(f"docs/{locale}/") or path == f"docs/.i18n/{locale}.tm.jsonl" + + +def snapshot_scope(workspace: Path, locale: str, baseline: Path) -> list[str]: + files = untracked_locale_files(workspace, locale) + write_lines(baseline, files) + print(f"Recorded {len(files)} pre-repair untracked locale file(s) in {baseline}") + return files + + +def enforce_scope(workspace: Path, locale: str, baseline: Path) -> None: + baseline_files = read_lines(baseline) + staged_paths = git_lines(workspace, ["diff", "--cached", "--name-only"]) + if staged_paths: + print("Docs MDX repair staged files; forbidden:") + print("\n".join(staged_paths)) + raise SystemExit(1) + + changed_paths = git_lines(workspace, ["diff", "--name-only"]) + bad_paths = [path for path in changed_paths if not is_allowed_changed_path(path, locale)] + if bad_paths: + print("Docs MDX repair touched forbidden paths:") + print("\n".join(bad_paths)) + raise SystemExit(1) + + current_untracked = set(untracked_locale_files(workspace, locale)) + new_untracked = sorted(current_untracked - baseline_files) + if new_untracked: + print("Docs MDX repair created untracked locale files; forbidden:") + print("\n".join(new_untracked)) + raise SystemExit(1) + + # Full translation can legitimately create new locale pages before repair. + # The baseline makes the guard focus on repair-stage side effects only. + print( + f"Docs MDX repair scope ok: {len(changed_paths)} changed path(s), " + f"{len(current_untracked)} pre-existing untracked locale file(s)" + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Snapshot and enforce the translated MDX repair scope.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Outputs: + snapshot writes the baseline file. enforce exits non-zero on forbidden repair edits. + +Examples: + LOCALE=fr python .github/scripts/i18n/mdx_repair_scope.py snapshot --baseline .openclaw-sync/mdx/fr.repair-baseline.txt + LOCALE=fr python .github/scripts/i18n/mdx_repair_scope.py enforce --baseline .openclaw-sync/mdx/fr.repair-baseline.txt +""", + ) + parser.add_argument("command", choices=["snapshot", "enforce"]) + parser.add_argument("--baseline", required=True, type=Path) + parser.add_argument("--workspace", default=os.environ.get("GITHUB_WORKSPACE", "."), type=Path) + parser.add_argument("--locale", default=os.environ.get("LOCALE", "")) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + if not args.locale: + raise SystemExit("missing locale: pass --locale or set LOCALE") + + workspace = args.workspace.resolve() + if args.command == "snapshot": + snapshot_scope(workspace, args.locale, args.baseline) + else: + enforce_scope(workspace, args.locale, args.baseline) + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/i18n/tests/test_i18n_scripts.py b/.github/scripts/i18n/tests/test_i18n_scripts.py index 9fb7a9e42..dcac5d3de 100644 --- a/.github/scripts/i18n/tests/test_i18n_scripts.py +++ b/.github/scripts/i18n/tests/test_i18n_scripts.py @@ -34,6 +34,7 @@ def load_module(name: str): prepare = load_module("prepare") pending = load_module("build_pending_manifest") package_artifact = load_module("package_artifact") +mdx_repair_scope = load_module("mdx_repair_scope") apply_artifacts = load_module("apply_artifacts") read_source_metadata = load_module("read_source_metadata") prune_stale_locale_pages = load_module("prune_stale_locale_pages") @@ -175,9 +176,14 @@ def test_full_workflow_keeps_only_weekly_and_manual_triggers(self) -> None: def test_full_workflow_gates_batches_after_canary(self) -> None: text = (REPO_ROOT / ".github/workflows/translate-all.yml").read_text(encoding="utf-8") + reusable = (REPO_ROOT / ".github/workflows/translate-locale-reusable.yml").read_text(encoding="utf-8") for index in range(1, 7): self.assertIn(f"translate-batch-{index}:", text) self.assertIn("needs.translate-canary.result == 'success'", text) + self.assertIn("artifact_role: canary", text) + self.assertIn("inputs.commit_locale || inputs.artifact_role == 'canary'", reusable) + self.assertIn("inputs.artifact_role == 'canary' || steps.apply.outputs.changed_count != '0'", reusable) + self.assertIn("inputs.commit_locale && steps.apply.outputs.changed_count != '0'", reusable) self.assertIn("provider-preflight:", text) self.assertIn("Translate Full completed with failed or cancelled work", text) @@ -451,6 +457,46 @@ def test_package_artifact_failure_writes_visible_github_status(self) -> None: self.assertIn("failed=true", output.read_text(encoding="utf-8")) self.assertIn("failed_reason=translation failed", output.read_text(encoding="utf-8")) + def test_mdx_repair_scope_allows_preexisting_untracked_locale_files_only(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + repo = Path(tmp) + init_repo(repo) + baseline = repo / ".openclaw-sync/mdx/fr.repair-baseline.txt" + (repo / "docs/fr").mkdir(parents=True) + (repo / "docs/index.md").write_text("# Index\n", encoding="utf-8") + (repo / "docs/fr/tracked.md").write_text("# Tracked FR\n", encoding="utf-8") + run_git(repo, "add", ".") + run_git(repo, "commit", "-m", "initial") + + (repo / "docs/fr/from-translation.md").write_text("# New FR\n", encoding="utf-8") + mdx_repair_scope.snapshot_scope(repo, "fr", baseline) + + (repo / "docs/fr/tracked.md").write_text("# Tracked FR repaired\n", encoding="utf-8") + mdx_repair_scope.enforce_scope(repo, "fr", baseline) + + (repo / "docs/index.md").write_text("# Source side effect\n", encoding="utf-8") + with self.assertRaises(SystemExit): + mdx_repair_scope.enforce_scope(repo, "fr", baseline) + (repo / "docs/index.md").write_text("# Index\n", encoding="utf-8") + + (repo / "docs/index.md").write_text("# Staged source side effect\n", encoding="utf-8") + run_git(repo, "add", "docs/index.md") + with self.assertRaises(SystemExit): + mdx_repair_scope.enforce_scope(repo, "fr", baseline) + run_git(repo, "restore", "--staged", "docs/index.md") + (repo / "docs/index.md").write_text("# Index\n", encoding="utf-8") + + (repo / "docs/fr/from-repair.md").write_text("# Repair side effect\n", encoding="utf-8") + with self.assertRaises(SystemExit): + mdx_repair_scope.enforce_scope(repo, "fr", baseline) + + baseline.write_text(baseline.read_text(encoding="utf-8") + "docs/fr/from-repair.md\n", encoding="utf-8") + run_git(repo, "add", "docs/fr/from-repair.md") + (repo / "docs/fr/staged-from-repair.md").write_text("# Staged repair side effect\n", encoding="utf-8") + run_git(repo, "add", "docs/fr/staged-from-repair.md") + with self.assertRaises(SystemExit): + mdx_repair_scope.enforce_scope(repo, "fr", baseline) + def test_full_summary_ignores_canary_as_locale_success_and_reports_missing(self) -> None: with tempfile.TemporaryDirectory() as tmp: artifacts = Path(tmp) diff --git a/.github/workflows/translate-finalize-reusable.yml b/.github/workflows/translate-finalize-reusable.yml index f1257526d..cb745a4ea 100644 --- a/.github/workflows/translate-finalize-reusable.yml +++ b/.github/workflows/translate-finalize-reusable.yml @@ -68,6 +68,10 @@ jobs: if: steps.apply.outputs.changed_count != '0' run: sudo apt-get update && sudo apt-get install -y librsvg2-bin + - name: Install Playwright browser + if: steps.apply.outputs.changed_count != '0' + run: npx playwright install --with-deps chromium + - name: Check aggregate docs if: steps.apply.outputs.changed_count != '0' run: npm run docs:check diff --git a/.github/workflows/translate-locale-reusable.yml b/.github/workflows/translate-locale-reusable.yml index 4601b2a43..d5596da6c 100644 --- a/.github/workflows/translate-locale-reusable.yml +++ b/.github/workflows/translate-locale-reusable.yml @@ -185,6 +185,14 @@ jobs: node .openclaw-sync/check-docs-mdx.mjs "docs/${LOCALE}" \ --json-out ".openclaw-sync/mdx/${LOCALE}.json" + - name: Snapshot translated MDX repair scope + if: steps.stale.outputs.skip != 'true' && steps.translate_docs.outcome == 'success' && steps.mdx_check.outcome == 'failure' + env: + LOCALE: ${{ inputs.locale }} + run: | + python .github/scripts/i18n/mdx_repair_scope.py snapshot \ + --baseline "${RUNNER_TEMP}/${LOCALE}.repair-baseline.txt" + - name: Repair translated MDX id: mdx_repair if: steps.stale.outputs.skip != 'true' && steps.translate_docs.outcome == 'success' && steps.mdx_check.outcome == 'failure' @@ -208,27 +216,8 @@ jobs: env: LOCALE: ${{ inputs.locale }} run: | - set -euo pipefail - bad_paths="$( - git diff --name-only | while IFS= read -r path; do - case "$path" in - "docs/${LOCALE}"/*|"docs/.i18n/${LOCALE}.tm.jsonl") ;; - *) printf '%s\n' "$path" ;; - esac - done - )" - if [ -n "$bad_paths" ]; then - echo "Docs MDX repair touched forbidden paths:" - printf '%s\n' "$bad_paths" - exit 1 - fi - - untracked_locale="$(git ls-files --others --exclude-standard -- "docs/${LOCALE}")" - if [ -n "$untracked_locale" ]; then - echo "Docs MDX repair created untracked locale files; forbidden:" - printf '%s\n' "$untracked_locale" - exit 1 - fi + python .github/scripts/i18n/mdx_repair_scope.py enforce \ + --baseline "${RUNNER_TEMP}/${LOCALE}.repair-baseline.txt" - name: Recheck translated MDX id: mdx_recheck @@ -287,9 +276,9 @@ jobs: exit 1 commit-locale: - name: Commit ${{ inputs.locale }} artifact + name: Finalize ${{ inputs.locale }} artifact needs: translate - if: inputs.commit_locale && needs.translate.result == 'success' + if: needs.translate.result == 'success' && (inputs.commit_locale || inputs.artifact_role == 'canary') runs-on: ubuntu-latest permissions: actions: write @@ -326,27 +315,31 @@ jobs: --expected-locales "${EXPECTED_LOCALES}" - name: Set up Node for locale validation - if: steps.apply.outputs.changed_count != '0' + if: inputs.artifact_role == 'canary' || steps.apply.outputs.changed_count != '0' uses: actions/setup-node@v6 with: node-version: 24 cache: npm - name: Install docs dependencies - if: steps.apply.outputs.changed_count != '0' + if: inputs.artifact_role == 'canary' || steps.apply.outputs.changed_count != '0' run: npm ci - name: Install validation system dependencies - if: steps.apply.outputs.changed_count != '0' + if: inputs.artifact_role == 'canary' || steps.apply.outputs.changed_count != '0' run: sudo apt-get update && sudo apt-get install -y librsvg2-bin - - name: Check docs before locale commit - if: steps.apply.outputs.changed_count != '0' + - name: Install Playwright browser + if: inputs.artifact_role == 'canary' || steps.apply.outputs.changed_count != '0' + run: npx playwright install --with-deps chromium + + - name: Check docs before artifact finalization + if: inputs.artifact_role == 'canary' || steps.apply.outputs.changed_count != '0' run: npm run docs:check - name: Commit locale refresh id: locale_commit - if: steps.apply.outputs.changed_count != '0' + if: inputs.commit_locale && steps.apply.outputs.changed_count != '0' env: BASE_SOURCE_SHA: ${{ steps.apply.outputs.base_source_sha }} LOCALE: ${{ inputs.locale }} @@ -356,7 +349,7 @@ jobs: python .github/scripts/i18n/commit_locale_artifact.py - name: Dispatch locale docs deploy - if: steps.locale_commit.outputs.committed == 'true' + if: inputs.commit_locale && steps.locale_commit.outputs.committed == 'true' env: GH_TOKEN: ${{ github.token }} run: | diff --git a/.gitignore b/.gitignore index 12711f39d..4720e4203 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ dist/ node_modules/ *.log .DS_Store + +__pycache__/ +*.py[cod] + +# Local workflow shell extraction is a test artifact, not sync metadata. +.openclaw-sync/workflow-shell-check/ diff --git a/docs/.i18n/translation-workflow.md b/docs/.i18n/translation-workflow.md index 090736c28..dd7db0d36 100644 --- a/docs/.i18n/translation-workflow.md +++ b/docs/.i18n/translation-workflow.md @@ -5,41 +5,47 @@ Internal note for the docs publish pipeline. This file is under `docs/.i18n`, wh ## Goals - English docs deploy quickly after every source docs sync. -- Locale translation does not run for every hot `main` commit. -- Translation work is debounced so a burst of docs commits becomes one translation wave. -- Locale jobs translate only pages whose source hash changed since the last successful locale output. -- Successful locale outputs are committed together, even if one or more locale jobs fail. -- A weekly reconciliation reruns every locale/page path to repair missed or flaky translations. +- Incremental translation does not run for every hot `main` commit. +- Full reconciliation is a recovery path, not a release path. +- Full reconciliation runs automatically only on the weekly schedule, or manually when an operator starts it. +- A failed full run can be retried for one locale without rerunning every locale. +- Provider/key failures stop before locale fan-out. +- A tiny canary sample must succeed before follow-up full batches start. +- Locale translation failures are visible as failed GitHub jobs, even when diagnostic artifacts were uploaded. -## Event flow +## Event Flow 1. `openclaw/openclaw` syncs English docs into `openclaw/docs`. 2. GitHub Pages deploys English/source changes immediately from the sync commit. -3. `Translate All` is triggered by the sync commit, release dispatch, manual dispatch, or weekly schedule. -4. The coordinator waits a cooldown window before starting translation. -5. After the cooldown, the coordinator reads the current `origin/main` source metadata. -6. If a newer docs sync arrived during cooldown, the coordinator uses the newer source state. -7. Per-locale translation jobs run in parallel with `fail-fast: false`. -8. Each locale job uploads an artifact for the requested source SHA. -9. The finalizer downloads available artifacts, ignores stale or failed payloads, and pushes one aggregate i18n commit. -10. After the aggregate commit lands, the finalizer dispatches the Pages deploy once. -11. The Pages workflow dispatches live smoke after deployment. +3. `Translate Incremental` debounces source-doc pushes and translates stale locale pages. +4. `Translate Full` runs only from the weekly schedule or `workflow_dispatch`. +5. Both workflows read the current `origin/main` source metadata after debounce. +6. Both workflows run the shared OpenAI provider/key preflight before any locale job. +7. Full translation plans one canary locale sample and bounded follow-up batches of up to three locales. +8. If the canary fails, follow-up full batches do not start. +9. Full locale jobs validate, commit, and dispatch deploy independently after that locale succeeds. +10. Incremental locale jobs still upload artifacts for the aggregate finalizer. +11. Failed locale jobs upload failure metadata before failing the job, so artifacts and CI status agree. -## Debounce policy +## Trigger Policy -The coordinator waits 1 hour after a docs sync or release dispatch, then re-reads `origin/main`. +`Translate Full` deliberately does not listen to release dispatches or glossary pushes. Release and glossary changes converge through the weekly full run. For urgent recovery, manually run `Translate Full` with `target_locale=all` or a single locale slug. -The default cooldown is controlled by the publish repo variable `OPENCLAW_DOCS_TRANSLATION_COOLDOWN_SECONDS`, which defaults to `3600`. Repository dispatch callers may override it with `client_payload.cooldown_seconds`, and manual runs may set `cooldown_seconds`. +Top-level full workflow concurrency is serialized with `cancel-in-progress: false`. A new full run waits for a running full run instead of cancelling it. -If `.openclaw-sync/source.json` changed during the wait, it waits again from the newer state. If `main` keeps moving, the wait is capped by `OPENCLAW_DOCS_TRANSLATION_MAX_WAIT_SECONDS`, which defaults to the cooldown value. The newest observed state is translated after the cap. +Manual `target_locale` accepts `all` or one locale slug such as `fr`, `ja-jp`, or `zh-cn`. A single-locale rerun uses that locale for the canary sample, then schedules only that locale in the first full batch. -Manual and weekly runs do not wait by default. +## Debounce Policy -## Incremental translation +The coordinator waits after push-triggered incremental runs. The default cooldown is controlled by `OPENCLAW_DOCS_TRANSLATION_COOLDOWN_SECONDS`, which defaults to `3600`. Manual and weekly runs do not wait by default unless the manual input sets `cooldown_seconds`. + +If `.openclaw-sync/source.json` changed during a wait, the workflow waits again from the newer state. If `main` keeps moving, the wait is capped by `OPENCLAW_DOCS_TRANSLATION_MAX_WAIT_SECONDS`, which defaults to the cooldown value. + +## Incremental Translation Each translated page stores `x-i18n.source_hash`. Locale jobs compare the current English page hash with the stored locale hash. -Normal runs translate only: +Normal incremental runs translate only: - missing locale pages - locale pages with stale `x-i18n.source_hash` @@ -47,14 +53,32 @@ Normal runs translate only: Internal files under `docs/.i18n/**` are not translation inputs. Push-triggered runs that only change internal i18n files skip before the locale matrix. -If a locale job fails, its artifact is marked failed and carries no payload. The finalizer still commits successful locales. The failed locale remains stale and is picked up by the next incremental run because its source hashes still do not match. +Incremental translation uses the provider/key preflight before expanding the locale matrix. If the key is invalid, model access is denied, or quota is exhausted, the preflight job fails and locale jobs are not scheduled. + +## Full Translation + +Full mode forces every source page for the selected locale into the pending manifest instead of relying on changed source hashes. + +The weekly all-locale plan is: + +```text +provider/key preflight + -> canary locale sample + -> batch 1, up to 3 locales + -> batch 2, up to 3 locales + -> ... + -> status summary +``` + +The canary is a deterministic one-document sample from the first selected locale. It uploads a `canary` artifact, applies it through the same artifact validation path as locale commits, and runs the aggregate docs check without committing or publishing. If it fails translation or validation, later batches are skipped. If it succeeds, the selected locales, including the canary locale, run in normal full batches. If a later locale fails, already successful locales remain committed and published, and the failed locale can be rerun manually. -## Artifact contract +## Artifact Contract -Each locale job uploads one artifact named with locale and source SHA: +Each locale job uploads one artifact named with role, locale, shard, and source SHA: ```text -i18n-zh-cn- +i18n-zh-cn-s0of1- +i18n-canary-zh-cn-s0of1- ``` Artifact contents: @@ -67,45 +91,51 @@ payload/docs//** payload/docs/.i18n/.tm.jsonl ``` -`metadata.json` includes the locale, locale slug, source SHA, pending count, changed count, and any failure reason. The finalizer rejects artifacts whose `source_sha` does not match the current `.openclaw-sync/source.json`. +`metadata.json` includes the artifact role, locale, locale slug, source SHA, pending count, changed count, deleted count, step outcomes, and failure reason. A failed translation writes an empty payload contract, uploads the artifact, then fails the job. Full status summaries count canary artifacts separately and do not treat a canary artifact as a successful locale refresh. -The source repo release workflow dispatches one `translate-all-release` event. The coordinator still accepts old per-locale release events for compatibility, but those are only a fallback. +## Commit And Deploy Policy -## Aggregate commit +Full locale jobs are the commit and publish unit. After a locale succeeds, a separate write-permission commit job downloads that locale artifact, applies it to latest `main`, runs `npm run docs:check`, commits only `docs//**` and `docs/.i18n/.tm.jsonl`, pushes with rebase/retry under the shared locale finalizer concurrency, and dispatches `pages.yml`. -The finalizer owns the only locale push in the normal path. +Artifact application is intentionally conservative when source metadata has moved. The apply step uses latest `main`, copies only payload pages whose embedded `x-i18n.source_hash` still matches the current source page, and skips stale translation memory. If `main` moves again between apply/validation and push, the commit script skips that locale commit so the next manual or weekly run can re-evaluate from the new base. -Commit message: +Incremental translation keeps the aggregate finalizer. The finalizer downloads available artifacts, applies valid successful payloads, rejects stale or failed artifacts, runs `npm run docs:check`, pushes one aggregate i18n commit, dispatches `pages.yml`, and fails when required locale artifacts are missing or failed. -```text -chore(i18n): refresh translations -``` - -The commit may contain a partial locale set. The job summary lists applied locales, locales with no changes, missing or failed locales, stale artifacts, and invalid artifacts. - -## Weekly reconciliation +## Automatic Verification -The weekly run uses `full` mode. It forces a full reconciliation across every locale and every source page instead of relying only on changed source hashes. +The script test suite validates the recovery controls: -Glossary changes also force full reconciliation because glossary guidance can affect pages whose source hashes did not change. +- `Translate Full` has no release dispatch trigger. +- glossary pushes do not trigger `Translate Full`. +- weekly and manual triggers remain present. +- manual single-locale planning selects only that locale. +- full canary manifests keep the total pending count but translate only a bounded sample. +- provider/key preflight classifies invalid key, model access, and quota failures. +- canary success gates follow-up full batches. +- full worker fan-out stays within the small-batch budget. +- full status summaries report locale success, failure, skip reason, and artifact counts from metadata. +- failed artifact metadata produces visible GitHub output status. +- locale artifact application still rejects missing, failed, stale, and invalid artifacts. -Expected behavior: +Run locally: -- regenerate or verify every locale page -- prune stale locale pages -- refresh translation memory as needed -- still use parallel locale jobs -- still commit one aggregate result -- still tolerate individual locale failures - -The weekly run is the repair mechanism for LLM flakiness, partial failures, and missed incremental updates. - -## Deployment policy - -English deploys from source sync commits. +```bash +python -m unittest .github/scripts/i18n/tests/test_i18n_scripts.py +python .github/scripts/i18n/workflow_shell_check.py --check-bash +python .github/scripts/i18n/budget_check.py +``` -Translations deploy after the aggregate i18n commit. The finalizer dispatches GitHub Pages once because GitHub suppresses normal push-triggered workflow runs from `GITHUB_TOKEN` commits. The Pages workflow dispatches live smoke after deployment so the smoke test checks the deployed site instead of racing the deploy. +## Manual Verification -A hot docs day should produce many fast English deploys, but only a small number of locale deploys. +Before merging workflow recovery changes: -If external deploy providers such as Mintlify watch every push, the aggregate i18n commit is the load reducer. Avoid restoring per-locale pushes to `main`. +1. Trigger `Translate Full` with a deliberately invalid translation key in a test context and confirm the provider preflight fails before locale jobs start. +2. Trigger or simulate a canary failure and confirm follow-up full batches are skipped. +3. Trigger `Translate Full` with `target_locale=fr` and confirm only `fr` runs. +4. Trigger a small manual full run and confirm a successful locale commits independently and dispatches `pages.yml`. +5. Observe or simulate a later locale failure and confirm earlier successful locale commits remain published. +6. Rerun only the failed locale with `target_locale=` and confirm it commits independently. +7. Confirm release events do not start `Translate Full`. +8. Confirm glossary-only changes do not start `Translate Full`. +9. Check GitHub Actions summaries for selected locales, canary/batch status, artifact counts, and explicit failures. +10. Confirm the final diff from any locale commit contains only `docs//**` and `docs/.i18n/.tm.jsonl`.