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
5 changes: 3 additions & 2 deletions .dev/STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ Roadmap detail lives in [PLAN.md](PLAN.md), not here.
- **Tech-debt Wave 1** (#158–#168, milestone 1) — A (#158, guardrails) and B (#159, init
parity guard; corpus measured: 31/248 seeded pairs fail, 25 in intro.zh-cn) **merged**;
C (#160, failure reporting — red-by-design: rebase can fail, bulk exits non-zero,
`--resume` retries errored) open. D–K follow in any order. Boundaries the wave must
respect:
`--resume` retries errored) **merged**; D (#161, canonical review-workflow template —
five of six copies could never fire) open. E–K follow in any order. Boundaries the wave
must respect:
[`D-2026-07-24-tech-debt-audit-boundaries.md`](decisions/D-2026-07-24-tech-debt-audit-boundaries.md).
- **PR #71** — Malayalam (`ml`) draft; awaiting native-reviewer calibration batch.
Glossary PR **#69** (ja) open, awaiting native review + a `LANGUAGE_CONFIGS` entry.
Expand Down
26 changes: 26 additions & 0 deletions .dev/log/2026-07-24-pr-d-canonical-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 2026-07-24 — #161 (PR D): one canonical review-workflow template

The fleet-template PR. Six copies of the review workflow, five unfireable (no `labeled`
trigger — the label lands after `opened`, so the gate never passes; the v0.21.0
six-PRs-merged-unreviewed shape). Fix is structural, not a six-way patch:
`examples/review-translations.yml` is canonical, `translate setup` renders it at runtime
(examples dir threaded from the entry point like the glossary dir — same Jest-CJS reason),
docs quote its shape, and `workflow-templates.test.ts` (branch-naming mold) fails when any
copy loses a structural line. Scaffolder also: `@v0` replaces the stale `v0.9.0` literals
(F27; and F137's release-checklist gap closes because there is nothing left to bump),
writes `rebase-translations.yml` verbatim from its canonical file, source workflow gains
`issue_comment` resync + composite guard (F91), `checkout@v7` (F135), and the
paths-filter normalizer gets its missing `g` flag (`./lectures/` emitted
`lectures//**/*.md`). `doctor` now warns on review workflows without `labeled` (F7's
second half).

F127 landed as its docs half only — option (b), removing the dead review `target-language`
knob everywhere and documenting the suffix convention — because option (a) (honouring the
input) touches `inputs.ts`/`index.ts` and this PR is declared no-dist-action-rebuild.
If review mode ever honours the input, the docs come back with it; that is a deliberate
non-decision recorded here.

Substitution contract worth knowing: `generateTargetWorkflowYaml` is the identity when
rendered with the template's own example values (tested), so the canonical file doubles as
a golden output; a template edit that breaks a substitution key throws at scaffold time
and fails the unit test.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Changed
- **One canonical review-workflow template, and `translate setup` scaffolds repos that actually work** (#161, audit F7/F136/F27/F91/F135/F137): the review workflow existed in six divergent copies — the scaffolder plus five doc pages — and five of them could never fire on Action sync PRs, because the `action-translation` label is applied *after* the PR opens and `types: [opened, synchronize]` never sees it. That is the shape behind the v0.21.0 incident where six PRs of real translated content merged unreviewed. The scaffolder's copy was also pinned to `v0.9.0` (25 releases stale, the release checklist never knew to bump it) with no `permissions:` or `concurrency:` and `checkout@v4` against the docs' `@v7`. **Now**: `examples/review-translations.yml` is the single canonical template — `translate setup` renders it (substituting `source-repo`/`source-language`/`docs-folder`) and every doc page quotes its shape, locked by a drift test in the `branch-naming` mold that fails when any copy loses the `labeled`/`reopened` triggers, the label guard, `permissions`, or the per-PR `concurrency` group. The scaffolder emits the floating `@v0` (nothing left for a release to forget), also writes `rebase-translations.yml` from its canonical template, and its source workflow gains the `issue_comment` resync trigger and composite guard the docs teach — plus the `g`-flag fix in the paths-filter normalizer, which emitted a broken `lectures//**/*.md` for `./lectures/`. `translate doctor` now warns on a review workflow whose trigger list lacks `labeled` instead of passing the repo.
- **Review-mode docs stop teaching a dead knob** (#161, audit F127 remnant): review mode has never read a `target-language` input — it detects the language from the repository-name suffix and, since v0.23.0, warns when it can't. The input row, the quickstart line, and the scaffolded workflow key are gone; every review-workflow snippet now documents the suffix convention instead. (Honouring the input is a separate decision, deliberately not taken here.) Also corrected: the required-inputs table now admits `rebase` as a mode, the CLI reference no longer names a scaffold file (`translation-sync.yml`) that setup never wrote, and its `.translate/` samples show `<tool version>` instead of a hard-coded `0.8.0`.
- **Failure is no longer optional: rebase mode can fail, bulk runs exit non-zero, and `backward --resume` retries errored files** (#160, audit F37/F39/F66 — turns previously-green runs red *by design*; red here means failures that were always happening are now reported). Three paths reported success over failure. **Rebase mode could not fail**: `runRebase` counted per-PR errors and finished with an info line, so a run that failed every PR completed green; it now calls `core.setFailed` when any PR errored. Worse, `rebaseSinglePR` force-reset the PR branch *before* knowing translation succeeded — on partial failure it committed only the successful files onto the freshly-reset branch, silently dropping the errored files' previous translations from the PR, then commented "the translation content is preserved". It now throws **before** any branch reset, leaving the branch untouched, and returns a typed `rebased`/`skipped` outcome so skipped PRs are no longer counted — or commented on — as rebased. **Bulk CLI runs exited 0 however many files failed**: `backward` and `forward` bulk (including a `forward --github` wave where every push and PR creation failed) now exit 1 with a count when any file errored, as does single-file `forward`. The `backward` aggregate report gains optional `filesErrored` + `erroredFiles` fields (absent on pre-#160 reports, which still parse). **`backward --resume` treated errored files as done**, so a retry silently skipped exactly the files that needed retrying — a documented choice, deliberately reversed: resume now retries errored files and drops their stale error records, so repeated `--resume` converges on a clean run instead of a permanently partial one.
- **Terminal failures now carry a stack trace, decoded through the committed sourcemap** (#160, audit F29): no handler logged `error.stack` anywhere — 116 catch sites discard it — so a crash in a 2.9 MB bundle reported one message with no pointer into the code. The Action's terminal handler now logs the stack, and a new `dist-action/run.cjs` entry shim (now `action.yml`'s `main:`) enables source-map support before the bundle is compiled, so frames decode to `src/` locations — measured: `src/inputs.ts:25` instead of `index.js:21610`. A shim is the only placement that works: a `node24` action's `runs:` block has no `env:` key for `NODE_OPTIONS`, and enabling inside the bundle is too late. The CLI's terminal catches print the stack (dimmed) below the error message.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '\translate-resync'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 2
- uses: quantecon/action-translation@v0
Expand Down
24 changes: 19 additions & 5 deletions docs/user/action-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Runs in the **target** (translated) repository, triggered when a translation PR

| Input | Description |
|-------|-------------|
| `mode` | Operation mode: `sync` or `review` |
| `mode` | Operation mode: `sync`, `review`, or `rebase` |
| `anthropic-api-key` | Anthropic API key for Claude |
| `github-token` | GitHub token for API access (cross-repo requires a PAT with `repo` scope) |

Expand All @@ -70,12 +70,13 @@ Runs in the **target** (translated) repository, triggered when a translation PR
|-------|---------|-------------|
| `source-repo` | *(required)* | Source repository for English content (`owner/repo`) |
| `source-language` | `en` | Source language code |
| `target-language` | *(required for review)* | Target language code |
| `docs-folder` | `lectures/` | Documentation folder |
| `max-suggestions` | `5` | Maximum findings in review comment |
| `claude-model` | `claude-sonnet-5` | Claude model for review |
| `auto-merge-mode` | `off` | `off` or `shadow`. Shadow records the would-auto-merge decision in the verdict block and outputs without acting on it. `active` is not implemented and fails loudly ([#103](https://github.com/QuantEcon/action-translation/issues/103)) |

There is no `target-language` input in review mode: the language is detected from the repository-name suffix (`lecture-python-intro.zh-cn` → `zh-cn`), which selects the glossary for terminology review. A repo whose name carries no language suffix logs a warning and reviews without a glossary.

### Rebase mode inputs

| Input | Default | Description |
Expand Down Expand Up @@ -205,12 +206,26 @@ name: Review Translations

on:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, labeled, reopened]

jobs:
review:
if: contains(github.event.pull_request.labels.*.name, 'action-translation')
# `labeled` matters: the sync applies its labels after opening the PR.
# The second clause ignores `labeled` events for every other label.
if: >
contains(github.event.pull_request.labels.*.name, 'action-translation') &&
(github.event.action != 'labeled' || github.event.label.name == 'action-translation')
runs-on: ubuntu-latest

permissions:
contents: read
pull-requests: write

# One review per PR — supersede an in-flight review instead of running both
concurrency:
group: review-translations-${{ github.event.pull_request.number }}
cancel-in-progress: true

steps:
- uses: actions/checkout@v7
with:
Expand All @@ -221,7 +236,6 @@ jobs:
mode: review
source-repo: 'QuantEcon/lecture-python-intro'
source-language: 'en'
target-language: 'zh-cn'
docs-folder: 'lectures/'
max-suggestions: 5
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
Expand Down
6 changes: 3 additions & 3 deletions docs/user/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ The `.translate/` folder in the target repo stores persistent sync metadata. It
source-language: en
target-language: zh-cn
docs-folder: lectures
tool-version: 0.8.0 # Version that last wrote this config
tool-version: <tool version> # Version that last wrote this config
```

Provides defaults so CLI flags don't need to be repeated every invocation.
Expand All @@ -530,7 +530,7 @@ synced-at: "2026-03-06" # ISO date of last sync
model: claude-sonnet-5 # Model used for translation
mode: NEW # Translation mode: NEW / UPDATE / RESYNC
section-count: 5 # Source section count at sync time
tool-version: 0.8.0 # Version that performed this sync
tool-version: <tool version> # Version that performed this sync
```

### How each command uses `.translate/`
Expand Down Expand Up @@ -592,7 +592,7 @@ npx translate setup --source <owner/repo> --target-language <code> [options]

1. Derives target repo name: `{source-repo}.{lang}` (e.g., `lecture-python-intro.zh-cn`)
2. Creates GitHub repo via `gh repo create` and clones it locally
3. Writes `.translate/config.yml`, `.github/workflows/translation-sync.yml`, `.gitignore`, `README.md`
3. Writes `.translate/config.yml`, `.github/workflows/review-translations.yml`, `.github/workflows/rebase-translations.yml`, `.gitignore`, `README.md`
4. Makes initial commit and pushes

**Requirements:** The `gh` CLI must be installed and authenticated (`gh auth login`).
Expand Down
11 changes: 7 additions & 4 deletions docs/user/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ name: Review Translations

on:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, labeled, reopened]

jobs:
review:
if: contains(github.event.pull_request.labels.*.name, 'action-translation')
# `labeled` matters: the sync applies its labels after opening the PR.
# The second clause ignores `labeled` events for every other label.
if: >
contains(github.event.pull_request.labels.*.name, 'action-translation') &&
(github.event.action != 'labeled' || github.event.label.name == 'action-translation')
runs-on: ubuntu-latest

permissions:
Expand All @@ -97,13 +101,12 @@ jobs:
mode: review
source-repo: 'YourOrg/your-source-repo'
source-language: 'en'
target-language: 'zh-cn'
docs-folder: 'lectures/'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
```

This posts an AI-generated quality review comment on each translation PR, including a translation score, diff quality score, and improvement suggestions.
This posts an AI-generated quality review comment on each translation PR, including a translation score, diff quality score, and improvement suggestions. There is no `target-language` input in review mode — the language is detected from the repository-name suffix (`your-repo.zh-cn` → `zh-cn`).

## What happens next

Expand Down
11 changes: 8 additions & 3 deletions docs/user/tutorials/add-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,15 @@ name: Review Translations

on:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, labeled, reopened]

jobs:
review:
if: contains(github.event.pull_request.labels.*.name, 'action-translation')
# `labeled` matters: the sync applies its labels after opening the PR.
# The second clause ignores `labeled` events for every other label.
if: >
contains(github.event.pull_request.labels.*.name, 'action-translation') &&
(github.event.action != 'labeled' || github.event.label.name == 'action-translation')
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -306,12 +310,13 @@ jobs:
mode: review
source-repo: 'QuantEcon/lecture-python-intro'
source-language: 'en'
target-language: 'ja'
docs-folder: 'lectures'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
```

There is no `target-language` input in review mode — the language is detected from the repository-name suffix (`lecture-python-intro.ja` → `ja`).

### Secrets

Add these secrets to the relevant repos:
Expand Down
3 changes: 2 additions & 1 deletion docs/user/tutorials/connect-existing.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,13 @@ jobs:
mode: review
source-repo: 'QuantEcon/lecture-python-intro'
source-language: 'en'
target-language: 'zh-cn'
docs-folder: 'lectures'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
```

There is no `target-language` input here: review mode detects the language from the repository-name suffix (`lecture-python-intro.zh-cn` → `zh-cn`). A repo without a language suffix logs a warning and reviews without a glossary.

:::{tip}
The `labeled` event type is important — without it, the workflow won't trigger if the `action-translation` label is added after the PR is opened. The `github.event.label.name` check keeps it from firing again for each *other* label the sync applies, and the `concurrency` group collapses the `opened` and `labeled` events a single sync produces into one review. Without both, one sync bills several reviews of the same diff ([#96](https://github.com/QuantEcon/action-translation/issues/96)).
:::
Expand Down
11 changes: 7 additions & 4 deletions docs/user/tutorials/fresh-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,15 @@ name: Review Translations

on:
pull_request:
types: [opened, synchronize]
types: [opened, synchronize, labeled, reopened]

jobs:
review:
if: contains(github.event.pull_request.labels.*.name, 'action-translation')
# `labeled` matters: the sync applies its labels after opening the PR.
# The second clause ignores `labeled` events for every other label.
if: >
contains(github.event.pull_request.labels.*.name, 'action-translation') &&
(github.event.action != 'labeled' || github.event.label.name == 'action-translation')
runs-on: ubuntu-latest

permissions:
Expand All @@ -343,13 +347,12 @@ jobs:
mode: review
source-repo: 'QuantEcon/lecture-python-intro'
source-language: 'en'
target-language: 'zh-cn'
docs-folder: 'lectures'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
```

Add the `ANTHROPIC_API_KEY` secret to the **target** repo as well (for the review workflow).
There is no `target-language` input in review mode — the language is detected from the repository-name suffix (`lecture-python-intro.zh-cn` → `zh-cn`). Add the `ANTHROPIC_API_KEY` secret to the **target** repo as well (for the review workflow).

### 4d: Target repo — Secret

Expand Down
54 changes: 54 additions & 0 deletions examples/review-translations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Review Translation PRs
#
# Install this workflow in the TARGET (translated) repository at:
# .github/workflows/review-translations.yml
#
# It posts an AI quality review on every translation PR the sync opens.
# The review's target language is not an input: it is detected from the
# repository-name suffix (lecture-python-intro.zh-cn -> zh-cn).
#
# This file is the single canonical template: `translate setup` scaffolds
# new target repos from it (substituting source-repo, source-language and
# docs-folder), and the docs quote the same shape. A drift test
# (src/cli/__tests__/workflow-templates.test.ts) keeps every copy aligned.

name: Review Translations

on:
pull_request:
types: [opened, synchronize, labeled, reopened]

jobs:
review:
# `labeled` in the trigger list matters: the sync applies its labels after
# opening the PR, so without it a PR labeled late is never reviewed. The
# second clause then ignores `labeled` events for every OTHER label — a
# sync applies labels one call at a time, and each would otherwise start
# a full (billed) review of the same diff (#96).
if: >
contains(github.event.pull_request.labels.*.name, 'action-translation') &&
(github.event.action != 'labeled' || github.event.label.name == 'action-translation')
runs-on: ubuntu-latest

permissions:
contents: read
pull-requests: write

# One review per PR — supersede an in-flight review instead of running both
concurrency:
group: review-translations-${{ github.event.pull_request.number }}
cancel-in-progress: true

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2

- uses: QuantEcon/action-translation@v0
with:
mode: review
source-repo: 'QuantEcon/lecture-python-intro'
source-language: 'en'
docs-folder: 'lectures'
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
github-token: ${{ secrets.GITHUB_TOKEN }}
Loading
Loading