Browse Source

👷 Update translation PR branches with PR Push (#16224)

pull/16225/head
Sebastián Ramírez 1 day ago
committed by GitHub
parent
commit
24ecb120f4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      .github/pr-push.yml
  2. 24
      .github/workflows/translate.yml
  3. 78
      scripts/translate.py
  4. 47
      scripts/translation_git.py
  5. 63
      tests/test_translate.py

1
.github/pr-push.yml

@ -1,2 +1,3 @@
workflows: workflows:
- .github/workflows/pre-commit.yml - .github/workflows/pre-commit.yml
- .github/workflows/translate.yml

24
.github/workflows/translate.yml

@ -13,6 +13,7 @@ on:
command: command:
description: Command to run description: Command to run
type: choice type: choice
required: true
options: options:
- translate-page - translate-page
- translate-lang - translate-lang
@ -23,18 +24,12 @@ on:
language: language:
description: Language to translate to as a letter code (e.g. "es" for Spanish) description: Language to translate to as a letter code (e.g. "es" for Spanish)
type: string type: string
required: false required: true
default: ""
en_path: en_path:
description: File path in English to translate (e.g. docs/en/docs/index.md) description: File path in English to translate (e.g. docs/en/docs/index.md)
type: string type: string
required: false required: false
default: "" default: ""
commit_in_place:
description: Commit changes directly instead of making a PR
type: boolean
required: false
default: false
max: max:
description: Maximum number of items to translate (e.g. 10) description: Maximum number of items to translate (e.g. 10)
type: number type: number
@ -130,9 +125,11 @@ jobs:
COMMAND: ${{ matrix.command }} COMMAND: ${{ matrix.command }}
MAX: ${{ github.event.inputs.max }} MAX: ${{ github.event.inputs.max }}
- name: Get PR Submit token - name: Get PR Submit token
if: github.event_name == 'schedule'
id: pr-submit id: pr-submit
uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1 uses: tiangolo/pr-submit@d802fdf59bde80bc3eb8bd3259f4cbeec63de4aa # 0.0.1
- name: Create pull request - name: Create pull request
if: github.event_name == 'schedule'
run: | run: |
gh auth setup-git gh auth setup-git
uv run ./scripts/translate.py make-pr uv run ./scripts/translate.py make-pr
@ -140,3 +137,16 @@ jobs:
GITHUB_TOKEN: ${{ steps.pr-submit.outputs.token }} GITHUB_TOKEN: ${{ steps.pr-submit.outputs.token }}
LANGUAGE: ${{ matrix.lang }} LANGUAGE: ${{ matrix.lang }}
COMMAND: ${{ matrix.command }} COMMAND: ${{ matrix.command }}
- name: Get PR Push token
if: github.event_name == 'workflow_dispatch'
id: pr-push
uses: tiangolo/pr-push@f336b3817f32ea9b8273a8c15f8ecb739ac38167 # 0.0.4
- name: Commit and push changes
if: github.event_name == 'workflow_dispatch'
env:
COMMAND: ${{ matrix.command }}
GH_TOKEN: ${{ steps.pr-push.outputs.token }}
LANGUAGE: ${{ matrix.lang }}
run: |
gh auth setup-git
uv run ./scripts/translate.py push

78
scripts/translate.py

@ -1,6 +1,5 @@
import json import json
import secrets import secrets
import subprocess
from collections.abc import Iterable from collections.abc import Iterable
from functools import lru_cache from functools import lru_cache
from os import sep as pathsep from os import sep as pathsep
@ -10,11 +9,16 @@ from typing import Annotated
import git import git
import typer import typer
import yaml import yaml
from doc_parsing_utils import check_translation
from github import Github from github import Github
from pydantic_ai import Agent from pydantic_ai import Agent
from rich import print from rich import print
from scripts.doc_parsing_utils import check_translation
from scripts.translation_git import (
commit_translation_changes,
has_translation_changes,
)
non_translated_sections = ( non_translated_sections = (
f"reference{pathsep}", f"reference{pathsep}",
"release-notes.md", "release-notes.md",
@ -31,6 +35,7 @@ general_prompt_path = Path(__file__).absolute().parent / "general-llm-prompt.md"
general_prompt = general_prompt_path.read_text(encoding="utf-8") general_prompt = general_prompt_path.read_text(encoding="utf-8")
app = typer.Typer() app = typer.Typer()
repository_path = Path(__file__).absolute().parent.parent
@lru_cache @lru_cache
@ -421,25 +426,11 @@ def make_pr(
command: Annotated[str | None, typer.Option(envvar="COMMAND")] = None, command: Annotated[str | None, typer.Option(envvar="COMMAND")] = None,
github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")], github_token: Annotated[str, typer.Option(envvar="GITHUB_TOKEN")],
github_repository: Annotated[str, typer.Option(envvar="GITHUB_REPOSITORY")], github_repository: Annotated[str, typer.Option(envvar="GITHUB_REPOSITORY")],
commit_in_place: Annotated[
bool, typer.Option(envvar="COMMIT_IN_PLACE", show_default=True)
] = False,
) -> None: ) -> None:
print("Setting up GitHub Actions git user") repo = git.Repo(repository_path)
repo = git.Repo(Path(__file__).absolute().parent.parent) if not has_translation_changes(repository_path):
if not repo.is_dirty(untracked_files=True): print("No translation changes to commit")
print("Repository is clean, no changes to commit")
return return
subprocess.run(["git", "config", "user.name", "pr-submit[bot]"], check=True)
subprocess.run(
["git", "config", "user.email", "pr-submit[bot]@users.noreply.github.com"],
check=True,
)
current_branch = repo.active_branch.name
if current_branch == "master" and commit_in_place:
print("Can't commit directly to master")
raise typer.Exit(code=1)
if not commit_in_place:
branch_name = "translate" branch_name = "translate"
if language: if language:
branch_name += f"-{language}" branch_name += f"-{language}"
@ -447,23 +438,16 @@ def make_pr(
branch_name += f"-{command}" branch_name += f"-{command}"
branch_name += f"-{secrets.token_hex(4)}" branch_name += f"-{secrets.token_hex(4)}"
print(f"Creating a new branch {branch_name}") print(f"Creating a new branch {branch_name}")
subprocess.run(["git", "checkout", "-b", branch_name], check=True) repo.git.checkout("-b", branch_name)
else: message = commit_translation_changes(
branch_name = current_branch repo_path=repository_path,
print(f"Committing in place on branch {branch_name}") bot_name="pr-submit[bot]",
print("Adding updated files") language=language,
git_path = Path("docs") command=command,
subprocess.run(["git", "add", str(git_path)], check=True) )
print("Committing updated file") assert message is not None
message = "🌐 Update translations"
if language:
message += f" for {language}"
if command:
message += f" ({command})"
subprocess.run(["git", "commit", "-m", message], check=True)
print("Pushing branch") print("Pushing branch")
subprocess.run(["git", "push", "origin", branch_name], check=True) repo.git.push("origin", branch_name)
if not commit_in_place:
print("Creating PR") print("Creating PR")
g = Github(github_token) g = Github(github_token)
gh_repo = g.get_repo(github_repository) gh_repo = g.get_repo(github_repository)
@ -473,12 +457,30 @@ def make_pr(
+ f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md." + f"\n\nIt uses the prompt file https://github.com/fastapi/fastapi/blob/master/docs/{language}/llm-prompt.md."
+ "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR." + "\n\nIn most cases, it's better to make PRs updating that file so that the LLM can do a better job generating the translations than suggesting changes in this PR."
) )
pr = gh_repo.create_pull( pr = gh_repo.create_pull(title=message, body=body, base="master", head=branch_name)
title=message, body=body, base="master", head=branch_name
)
print(f"Created PR: {pr.number}") print(f"Created PR: {pr.number}")
print("Finished") print("Finished")
@app.command()
def push(
*,
language: Annotated[str | None, typer.Option(envvar="LANGUAGE")] = None,
command: Annotated[str | None, typer.Option(envvar="COMMAND")] = None,
github_ref_name: Annotated[str, typer.Option(envvar="GITHUB_REF_NAME")],
) -> None:
repo = git.Repo(repository_path)
message = commit_translation_changes(
repo_path=repository_path,
bot_name="pr-push[bot]",
language=language,
command=command,
)
if message is None:
return
print(f"Pushing changes to {github_ref_name}")
repo.git.push("origin", f"HEAD:{github_ref_name}")
if __name__ == "__main__": if __name__ == "__main__":
app() app()

47
scripts/translation_git.py

@ -0,0 +1,47 @@
import subprocess
from pathlib import Path
def has_translation_changes(repo_path: Path) -> bool:
result = subprocess.run(
["git", "status", "--porcelain", "--", "docs"],
cwd=repo_path,
check=True,
capture_output=True,
encoding="utf-8",
)
return bool(result.stdout)
def commit_translation_changes(
*,
repo_path: Path,
bot_name: str,
language: str | None,
command: str | None,
) -> str | None:
if not has_translation_changes(repo_path):
print("No translation changes to commit")
return None
print("Setting up GitHub App git user")
subprocess.run(["git", "config", "user.name", bot_name], cwd=repo_path, check=True)
subprocess.run(
[
"git",
"config",
"user.email",
f"{bot_name}@users.noreply.github.com",
],
cwd=repo_path,
check=True,
)
print("Adding updated files")
subprocess.run(["git", "add", "docs"], cwd=repo_path, check=True)
message = "🌐 Update translations"
if language:
message += f" for {language}"
if command:
message += f" ({command})"
print("Committing updated files")
subprocess.run(["git", "commit", "-m", message], cwd=repo_path, check=True)
return message

63
tests/test_translate.py

@ -0,0 +1,63 @@
import subprocess
from pathlib import Path
from scripts.translation_git import commit_translation_changes
def run_git(repo_path: Path, *args: str) -> str:
result = subprocess.run(
["git", *args],
cwd=repo_path,
check=True,
capture_output=True,
encoding="utf-8",
)
return result.stdout.strip()
def test_commit_translation_changes(tmp_path: Path) -> None:
run_git(tmp_path, "init")
run_git(tmp_path, "config", "user.name", "Test User")
run_git(tmp_path, "config", "user.email", "[email protected]")
run_git(tmp_path, "config", "commit.gpgsign", "false")
docs_path = tmp_path / "docs"
docs_path.mkdir()
translation_path = docs_path / "translation.md"
translation_path.write_text("Original\n")
unrelated_path = tmp_path / "unrelated.txt"
unrelated_path.write_text("Original\n")
run_git(tmp_path, "add", ".")
run_git(tmp_path, "commit", "-m", "Initial commit")
translation_path.write_text("Translated\n")
unrelated_path.write_text("Unrelated change\n")
message = commit_translation_changes(
repo_path=tmp_path,
bot_name="pr-push[bot]",
language="es",
command="update-outdated",
)
assert message == "🌐 Update translations for es (update-outdated)"
assert run_git(tmp_path, "log", "-1", "--format=%s") == message
assert run_git(tmp_path, "log", "-1", "--format=%an") == "pr-push[bot]"
assert (
run_git(tmp_path, "log", "-1", "--format=%ae")
== "pr-push[bot]@users.noreply.github.com"
)
assert (
run_git(tmp_path, "diff-tree", "--no-commit-id", "--name-only", "-r", "HEAD")
== "docs/translation.md"
)
assert run_git(tmp_path, "diff", "--name-only") == "unrelated.txt"
commit_sha = run_git(tmp_path, "rev-parse", "HEAD")
result = commit_translation_changes(
repo_path=tmp_path,
bot_name="pr-push[bot]",
language="es",
command="update-outdated",
)
assert result is None
assert run_git(tmp_path, "rev-parse", "HEAD") == commit_sha
Loading…
Cancel
Save