Skip to content

ci(translations): a content PR may delete locale siblings, never add or edit one - #292

Merged
hotlong merged 1 commit into
mainfrom
claude/pm-dispatch-objectos-ju9td1
Oct 5, 2026
Merged

hotlong merged 1 commit into
mainfrom
claude/pm-dispatch-objectos-ju9td1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #291

The ownership gate read a git diff --name-only list. That format names a deleted path exactly like an edited one, so once TRANSLATION_BOT_LOGIN is set the gate would reject the retire/rename deletion that AGENTS.md step 3 requires.

With this PR, a non-translation-account PR may delete locale artifacts, and is still rejected if it adds or modifies one. The translation account's rule is unchanged.

What changed

  • .github/workflows/translations.yml
    • The Ownership step now lists git diff --name-status --no-renames "origin/${BASE_REF}...HEAD".
    • The argv is unchanged: --actor "$PR_AUTHOR" --files changed.txt.
    • The Output step still builds its own --name-only list (changed-output.txt), untouched.
  • .github/scripts/check-translation-ownership.mjs
    • Each line is parsed as a status letter, a tab, then the path. The usage line and header document that format.
    • A human PR passes when every locale artifact in the list is D. It fails on any A, M or T.
    • Three kinds of malformed line are refused as a misinvocation (exit 1), whether the variable is set or unset: a line with no status (--name-only), an unknown status, and an R/C line without two paths. If the workflow drifts back to --name-only, the gate goes red on the next PR, not on the day enforcement starts.
    • The unset branch is untouched.
    • The old failure text said siblings "go with" a deleted English page. It now says deletion is the one allowed change and points at step 3.
    • New --self-test with 21 cases. Each case runs the real entry point with the workflow's argv twice: once with the variable set to a placeholder login, once unset.
  • tools/ci-scripts/run-self-tests.mjs
    • Lists the new self-test, so pnpm turbo run test in the required build job runs it.
    • This file is outside the claim's named surface, but the runner's own guard forces the edit. With the self-test present and not listed, the runner exits 1: "declares --self-test but is not listed in SELF_TESTED: .github/scripts/check-translation-ownership.mjs".
    • The header paragraph that said the gate "declares no self-test at all" is updated to match.

Rename: read as delete + add

The gate reads a rename as what it does to the tree: the old path is deleted and the new path is added.

  • The workflow passes --no-renames, so git writes a rename that way.
  • A list made without that flag can carry an R100 OLD NEW line. The gate expands it the same way.
  • A C line counts as the new path added, with the source untouched.

A renamed locale file still fails a human PR, because its new path is a translation added by hand. I measured both list forms, --no-renames and the R line. Both exit 1 and name only the new path.

Why not treat a rename as a modification of both paths. The two readings disagree on one case: a locale file renamed to a non-locale path. With rename detection on, that verdict would also depend on git's ≥50% similarity guess. Delete + add is the only reading where the same tree change gets the same verdict whichever flags made the list.

Measured on d2944dc

  • Enforced means TRANSLATION_BOT_LOGIN=placeholder-translator. The human actor is objectstack-fleet[bot].
  • Every list is real git diff --name-status --no-renames output.
diff exit verdict line
(a) PR #290, 7612ffe...9c11af4, 14 D 0 ✓ 14 file(s) changed: 14 translation artifact(s) deleted, none added or modified.
(b) add one + modify one locale file 1 This PR adds or modifies 2 translation artifact(s):
(b) delete one locale file, modify another 1 This PR adds or modifies 1 translation artifact(s):, naming only the modified one
(b) rename a locale file, both list forms 1 This PR adds or modifies 1 translation artifact(s):, naming the new path
(c) translation account edits English + a locale file 1 ✗ translation PRs may only touch translation artifacts.
(d) unset, every diff above 0 ⚠ TRANSLATION_BOT_LOGIN is not set — ownership is not enforced yet.
control English only, enforced 0 ✓ 1 file(s) changed, no translation artifacts touched.

For comparison, the same (a) run on main's gate exits 1: "This PR edits 14 translation artifact(s)".

(d) byte for byte. I compared main's gate fed the old --name-only list against this gate fed the new list, with the variable unset. Output is identical on four diffs: PR #290, English-only, add+modify, and the translation-account diff.

The only difference is on a diff that contains a rename. The count line now includes the rename's source path, so it reads 2 instead of 1. --name-only used to hide that path.

Ablation (one-off, not kept)

Run with objectstack's scripts/ablation-replace.mjs in WRAP mode, against the committed tree. For each leg:

  • the anchor count went 1 → 0 and blob 2a4dd41 changed;
  • the self-test ran;
  • the restore was proven: blob == HEAD 2a4dd41, and git diff HEAD is empty.

The three legs:

  1. Deletion exemption removed (written counts every locale path). Self-test exit 1 with 6 cases red, including PR docs: delete the 14 locale siblings that still forecast federation as unshipped #290's 14 siblings, a page retirement and a page rename.
  2. Human side unable to fail (written always empty). Exit 1 with 8 cases red: add, modify, meta, T, mixed, both rename forms, and C.
  3. R expansion drops the source path. Exit 1 with 1 case red: the translation account renaming English into a locale path.

Gates on d2944dc

  • pnpm turbo run build --force --concurrency=2 (os-verify-lock): VERDICT command-exit 0; Tasks 1 successful, 0 cached.
  • pnpm turbo run test --force --concurrency=2 (os-verify-lock): VERDICT command-exit 0; ✓ 8 self-test(s) passed.
  • check-translation-ownership.mjs --self-test: exit 0, ✓ self-test: 21 case(s), each run enforced and unset ….
  • check-locale-surface.mjs: exit 0.
  • check-translations.mjs: exit 0, ✓ translations gate passed.
  • Ownership gate with the workflow argv on this PR's own diff (3 M, no locale paths):
    • unset: exit 0, with the warning;
    • enforced: exit 0, ✓ 3 file(s) changed, no translation artifacts touched.
  • check-translation-output.mjs --files on the Output step's own --name-only list: exit 0, blocking on 0 changed translations.
  • gen-zh-hant.mjs --check: exit 0, 71 files.
  • check-node-floor.mjs: exit 0.

Acceptance notes

  • The translation account's side. Its rule is unchanged: it may touch locale artifacts only. What changed is that it now sees both paths of a rename.
    • Main's gate, on a translation-account PR that renames content/docs/c.mdx to content/docs/c2.ja.mdx, exits 0. That is because --name-only lists only the new path.
    • With this PR, the English source path is in the list and the run exits 1.
    • A plain deletion of an English file failed before and still fails.
  • Docs.
  • Quoted paths (dormant). With core.quotePath on, git C-quotes a path that contains non-ASCII bytes, and a quoted path does not match the content/docs/ prefix. Today 0 of the 472 tracked content/docs paths would be quoted. This behaviour is the same as before this PR.
  • The Output step's list. check-translation-output.mjs:95 still documents its list as --name-only, which remains true for that script.

Generated by Claude Code

…ct adding or editing one

The ownership gate read a `git diff --name-only` list, which names a
deleted path exactly like an edited one. Once TRANSLATION_BOT_LOGIN is
set, that rejects the retire/rename deletion AGENTS.md step 3 requires.

The Ownership step now lists `git diff --name-status --no-renames`, and
the gate reads each status. A human PR may delete a locale artifact, and
an addition or modification still fails. A rename is the old path
deleted plus the new path added, also when the list carries an R line,
so a renamed translation still fails. The translation account's rule is
unchanged, and it now also sees the source side of a rename.

The gate gains a --self-test of 21 cases, each run through the real
entry point with the variable set and unset, and the self-test runner
lists it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
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.

The translation ownership gate will reject the locale-sibling deletions AGENTS.md step 3 requires, once TRANSLATION_BOT_LOGIN is set

2 participants