Skip to content

ci(translate): run only the legs a push can change - #575

Merged
rohitg00 merged 1 commit into
mainfrom
ci/translate-changed-shards
Oct 10, 2026
Merged

rohitg00 merged 1 commit into
mainfrom
ci/translate-changed-shards

Conversation

@rohitg00

Copy link
Copy Markdown
Owner

Summary

Every qualifying push to main runs the full translate matrix: 10 automatic languages x 20 phases = 200 lesson legs, 11 interface-string legs, plus prepare and coverage, so 213 jobs of about 60 s each. On the #570 merge every leg ended with "no changes". A one-lesson edit also fans out to all 200 lesson legs.

With this change a push runs only the legs its commit range can change. Manual runs keep their scope.

How a push chooses its legs

scripts/translate_matrix.py now builds both matrices. On a push it diffs github.event.before..github.sha and selects:

  • lesson legs for each phase whose phases/<phase>/**/docs/en.md changed, for every automatic (ci: true) language;
  • every phase for a language that newly has ci: true; a metadata-only registry edit (dir, locale, names, _comment) selects nothing;
  • interface-string legs for every site language when site/ui-strings.json or scripts/translate_ui_strings.py changed, and for languages newly listed as ci or site.

It falls back to today's full matrix when:

  • the push also changes the code the legs run: translate.yml, translate_lessons.py and the modules it imports (build_catalog.py, _lib.py), or .github/translate-requirements.txt. A test walks the imports of both translator scripts so this list cannot drift silently;
  • the range cannot be read: an all-zeros before, a force-push (github.event.forced), a before that is not a commit id, a before commit that cannot be fetched, or a languages.json at before that cannot be parsed.

Other details:

  • Fetch: the prepare checkout stays at depth 1. The script fetches only the before commit with git fetch --depth=1. Measured locally against GitHub: +284 KiB and 1 s for the feat(site): server-render lessons and their translations #570 range, +84 KiB and 1 s for feat(paths): connect career routes to practical delivery evidence #559. A fetch-depth: 0 checkout is 103 MiB and took 44 s.
  • Empty matrices: translate and ui-strings have an if: on the prepare outputs, so an empty matrix skips the job instead of failing. coverage keeps !cancelled() && needs.translate.result != 'skipped'. The "no registered non-source languages selected" error now applies only to an invalid manual request.
  • The lesson matrix is an include list, so one push can combine changed phases with a newly automatic language without a cross product.
  • workflow_dispatch selects the same legs as before. I ran the old jq step and the new script on 14 inputs (blank, one phase, mixed valid and invalid codes, source-only, whitespace, traversal, unknown phase): outputs and error messages are identical.
  • Security posture is unchanged: persist-credentials: false, the same job permissions, and the event values reach the script only through env. A before value that is not a commit id is never passed to git.
  • scripts/translate_matrix.py is a push trigger, but it is not in the full-matrix list, because no leg runs it. A selector-only change runs only prepare.
  • There is no README translation job in this workflow. readme-counts-sync in curriculum.yml is a single job, so it has no fan-out to reduce.

Replay on real ranges

I ran the script against real history in a full clone. Each row runs the script at the pushed commit with that push's real before SHA, which I read from the run's check suite. "Today" counts the full matrix at that commit plus prepare and coverage. The minutes column uses the per-job durations of the actual run, each rounded up to a whole minute. It compares all jobs with only the jobs the new selection keeps.

Range What changed Today (jobs) After (jobs) Selected Runner min in that run
#570 a795cb5..8e8a069 languages.json metadata, and translate.yml (the new coverage job) 213 213 full, because translate.yml changed 313 -> 313
#570, languages.json change alone (synthetic commit) metadata only 213 1 0 lesson, 0 UI n/a
#559 5190afd..d75fb61 two lessons in 14-agent-engineering 213 12 10 lesson legs (phase 14 x 10 languages) 424 -> 102
#436 6331da9..ff34db8 one lesson in 11-llm-engineering (8 automatic languages then) 170 10 8 lesson legs 295 -> 64
#534 f6dbae7..6f73b43 site/ui-strings.json 170 9 8 UI legs (all UI languages then) 264 -> 17
#567 b6a7a17..6d4e13a ui-strings.json and translate_ui_strings.py 213 12 11 UI legs 321 -> 20
#556 9abf3d7..a0e2361 languages.json, ui-strings.json, translate_ui_strings.py, translate.yml 213 213 full, because translate.yml changed 4,080 -> 4,080
#556, languages.json change alone (synthetic commit) pt-BR and fa become ci, zh-TW becomes site 213 45 40 lesson legs (pt-BR, fa x 20 phases), 3 UI legs (zh-TW, pt-BR, fa) n/a
all-zeros before new branch 213 213 full n/a
force-push forced: true 213 213 full n/a
unknown before commit fetch fails ("not our ref") 213 213 full n/a

Both #570 and #556 also edited translate.yml, so the rule keeps their full matrix. The synthetic rows apply only the languages.json change on top of before. They show that the registry logic alone selects nothing for #570 and only the new languages for #556.

Estimated savings

  • Last 30 days (12 push runs, Sep 24 to Oct 10): 2,201 jobs become 831, and runner time drops from 8,900 to 6,816 minutes, about 2,080 minutes saved (24%).
  • All 16 completed push runs since Aug 9: 2,665 jobs become 1,132, and runner time drops from 19,636 to 17,192 minutes (2,444 saved). Most of the remaining minutes are real translation work for new languages, such as 9,038 minutes in the fix(i18n): make translation publishing retry-safe #401 run and 4,080 in feat(i18n): add requested site languages and 33 README locales #556. This change keeps that work.
  • A run that translates nothing drops from about 170 to 213 jobs (250 to 320 runner minutes) to 9 to 12 jobs (15 to 100 minutes). Far fewer jobs also compete with other workflows for the account's concurrent runner slots.
  • In those 16 runs, 4 edited translate.yml and 1 edited build_catalog.py and _lib.py. Those keep the full matrix by design. This PR's merge edits translate.yml, so it runs the full matrix once.

Not covered

  • Recovery after a failed, timed-out or cancelled leg. Today the next qualifying push reruns every leg, so it resumes the missed phase. After this change that phase resumes only when its English changes again, when translation code changes, or when someone runs the workflow manually with blank inputs. docs/i18n.md now says so. The same gap applies when the translate concurrency group replaces a pending run with a newer one, because the newer run diffs only its own range. A follow-up could also select phases whose published cache on the translations branch is stale (that needs a 27 MiB depth-1 fetch in prepare).
  • translate_lessons.py, build_catalog.py, _lib.py and translate-requirements.txt are still not push triggers, as before. They force the full matrix only when they arrive with a triggering change.
  • I have not checked these on a GitHub runner: matrix.include from JSON, the if: skip of an empty matrix, and the depth-1 fetch of before. Locally, a depth-1 clone fetched before from GitHub and selected the expected 10 legs for feat(paths): connect career routes to practical delivery evidence #559.

Tests

  • python3 scripts/test_translate_workflow.py: 34 tests pass on Python 3.14 and 3.12. 17 of them are new push-selection tests that use temporary git repos: one lesson, unrelated phase files, metadata-only and removal registry edits, newly ci, site promoted to ci, newly site, the UI sources, each full-run source, a merged lesson and new language, a moved lesson and a removed phase, a multi-commit range, the unreadable ranges (zeros, force-push, unknown commit, empty, option-shaped), an unparseable previous registry, and a shallow clone that fetches before. Other new tests check that the prepare step passes the event only through env, that the if: guards exist, and that the full-run list covers the import closure of each leg.
  • I changed five selection rules and removed the translate if:, one at a time. The suite failed each time.
  • npx --yes @action-validator/cli passes on translate.yml and curriculum.yml.
  • test_translate_ui_strings.py, test_audit_readme_locales.py, build_readme_i18n.py --check and node --test site/test_ui_i18n.js pass.

Every qualifying push ran 10 languages x 20 phases of lesson legs plus 11
interface-string legs, about 212 jobs, even when the push changed one
lesson or only registry metadata. On the #570 merge every leg ended with
"no changes".

scripts/translate_matrix.py now builds both matrices. For a push it diffs
github.event.before..github.sha and selects:

- lesson legs for each phase whose docs/en.md changed, for every
  automatic language;
- every phase for a language that newly has ci: true;
- interface-string legs for every site language when
  site/ui-strings.json or scripts/translate_ui_strings.py changed, and
  for languages newly listed as ci or site.

A push that also changes the code the legs run (translate.yml,
translate_lessons.py and the modules it imports, or the runtime
requirements) runs every leg. So does a range that cannot be read: an
all-zeros before, a force-push, or a before commit that cannot be
fetched. The prepare checkout stays at depth 1; the script fetches only
the before commit at depth 1 (0.1 to 0.3 MiB and about 1 s in local
measurements, against 103 MiB for a full-history checkout).

workflow_dispatch keeps its scope: the requested languages, or the
automatic set, times the requested phase or every phase. An empty
matrix now skips its job instead of failing the run, and the
"no registered non-source languages selected" error applies only to an
invalid manual request.

The lesson matrix is now an include list, so one push can combine
changed phases with a newly automatic language without a cross product.
A change to the selector itself triggers the workflow, which then runs
only the prepare job unless the same push changed translation inputs.
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: rohitg00/ai-engineering-from-scratch/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 456fe87c-834f-405f-9649-7c9d84a254b2

📥 Commits

Reviewing files that changed from the base of the PR and between a5b10f4 and 99eefc8.


📒 Files selected for processing (5)
  • .github/workflows/curriculum.yml
  • .github/workflows/translate.yml
  • docs/i18n.md
  • scripts/test_translate_workflow.py
  • scripts/translate_matrix.py

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.



📝 Walkthrough

Walkthrough

The translation workflow now uses a selector script to generate lesson and interface-string job matrices. For pushes, the selector uses changes in the push range and language registry data. Workflow triggers, tests, and documentation now reflect this selection.

Changes

Translation job selection

Layer / File(s) Summary
Selection inputs and outputs
scripts/translate_matrix.py, scripts/test_translate_workflow.py
The selector validates requested languages and phases, identifies automatic lesson and interface languages, and outputs separate lesson and UI matrices. Tests cover manual requests, automatic selection, and invalid phases.
Push-range selection and fallback
scripts/translate_matrix.py, scripts/test_translate_workflow.py
Push changes select lesson phases and interface languages based on changed paths and registry changes. Tests cover multi-commit ranges, newly automatic languages, source changes, and fallback when range or registry data is unavailable.
Workflow integration and documentation
.github/workflows/curriculum.yml, .github/workflows/translate.yml, docs/i18n.md, scripts/test_translate_workflow.py
The workflow passes event and push metadata to the selector and skips lesson or UI jobs when the corresponding output is empty. Trigger filters, workflow checks, and documentation include the selector and its selection behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant PrepareJob
  participant Selector as translate_matrix.py
  participant GitRepository
  participant TranslationJobs
  GitHubActions->>PrepareJob: Start workflow with event and push metadata
  PrepareJob->>Selector: Pass event, requested languages and phase, SHAs, and forced-push flag
  Selector->>GitRepository: Read registry, phases, and push-range changes
  GitRepository-->>Selector: Return registry data and changed paths
  Selector-->>PrepareJob: Return lessons and ui_langs
  PrepareJob->>TranslationJobs: Run lesson and UI jobs when their outputs are nonempty
Loading

Merge Risk: 🔵 Low · up to 99eef

A failed translation leg may need a manual rerun. The documented recovery path makes this a bounded operational risk, not a merge blocker.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 63 functions across 2 files. (3 skipped: 3… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check Passed The description clearly explains the push-based translation matrix selection, fallback behavior, workflow changes, tests, and known limitations. It is directly related to the changeset.
Title check Passed The title concisely and accurately describes the primary change: running only translation workflow legs that a push can change.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 63 functions across 2 files. (3 skipped: 3 unsupported.)


  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR


🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rohitg00
rohitg00 merged commit 1f9e500 into main Oct 10, 2026
7 checks passed
@rohitg00
rohitg00 deleted the ci/translate-changed-shards branch October 10, 2026 10:26

This branch was successfully deployed

1 active deployment
Preview — 99eefc88 Deployed Oct 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant