Skip to content

Commit cd19e8d

Browse files
committed
Add workflow to translate arbitrary PR docs
1 parent 457f97b commit cd19e8d

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
name: GPT Translate by PR
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
pr:
7+
description: "Pull request URL or number (e.g., 123 or https://github.com/org/repo/pull/123)"
8+
required: true
9+
10+
permissions:
11+
id-token: write
12+
pull-requests: write
13+
checks: write
14+
statuses: write
15+
contents: write
16+
17+
jobs:
18+
gpt_translate:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Parse PR input
23+
id: pr
24+
uses: actions/github-script@v7
25+
env:
26+
PR_INPUT: ${{ inputs.pr }}
27+
with:
28+
script: |
29+
const raw = process.env.PR_INPUT || '';
30+
if (!raw || raw.trim() === '') {
31+
throw new Error('PR input is required.');
32+
}
33+
34+
const trimmed = raw.trim();
35+
let prNumber = null;
36+
37+
if (/^\d+$/.test(trimmed)) {
38+
prNumber = parseInt(trimmed, 10);
39+
} else {
40+
const matches = trimmed.match(/\d+/g);
41+
if (matches && matches.length > 0) {
42+
prNumber = parseInt(matches[matches.length - 1], 10);
43+
}
44+
}
45+
46+
if (!prNumber || Number.isNaN(prNumber)) {
47+
throw new Error(`Unable to extract pull request number from input: ${trimmed}`);
48+
}
49+
50+
const { data: pr } = await github.rest.pulls.get({
51+
owner: context.repo.owner,
52+
repo: context.repo.repo,
53+
pull_number: prNumber,
54+
});
55+
56+
core.setOutput('pr_number', String(prNumber));
57+
core.setOutput('head_ref', pr.head.ref);
58+
core.setOutput('head_sha', pr.head.sha);
59+
core.setOutput('base_ref', pr.base.ref);
60+
core.setOutput('title', pr.title);
61+
core.setOutput('html_url', pr.html_url);
62+
63+
- name: Collect changed documentation files
64+
id: collect
65+
uses: actions/github-script@v7
66+
with:
67+
script: |
68+
const prNumber = parseInt('${{ steps.pr.outputs.pr_number }}', 10);
69+
if (!prNumber) {
70+
throw new Error('PR number is missing.');
71+
}
72+
73+
const isDoc = (path) =>
74+
path.startsWith('docs/en/') &&
75+
(path.endsWith('.md') || path.endsWith('.json'));
76+
77+
const toCnPath = (path) =>
78+
`docs/cn/${path.slice('docs/en/'.length)}`;
79+
80+
const files = await github.paginate(
81+
github.rest.pulls.listFiles,
82+
{
83+
owner: context.repo.owner,
84+
repo: context.repo.repo,
85+
pull_number: prNumber,
86+
per_page: 100,
87+
}
88+
);
89+
90+
const inputSet = new Set();
91+
const removedSet = new Set();
92+
93+
for (const file of files) {
94+
const { filename, status, previous_filename: prev } = file;
95+
96+
if (status === 'removed' && isDoc(filename)) {
97+
removedSet.add(toCnPath(filename));
98+
continue;
99+
}
100+
101+
if (status === 'renamed' && prev && isDoc(prev)) {
102+
removedSet.add(toCnPath(prev));
103+
}
104+
105+
if (isDoc(filename) && status !== 'removed') {
106+
inputSet.add(`./${filename}`);
107+
}
108+
}
109+
110+
core.setOutput('input_files', Array.from(inputSet).join(' '));
111+
core.setOutput('removed_cn', Array.from(removedSet).join(' '));
112+
core.setOutput('has_inputs', inputSet.size > 0 ? 'true' : 'false');
113+
core.setOutput('has_removals', removedSet.size > 0 ? 'true' : 'false');
114+
115+
- name: Exit if no documentation changes
116+
if: steps.collect.outputs.has_inputs != 'true' && steps.collect.outputs.has_removals != 'true'
117+
run: |
118+
echo "No English documentation additions, updates, or deletions detected for PR #${{ steps.pr.outputs.pr_number }} (${{ steps.pr.outputs.html_url }})."
119+
exit 0
120+
121+
- name: Checkout repository
122+
uses: actions/checkout@v4
123+
with:
124+
fetch-depth: 0
125+
126+
- name: Checkout PR revision
127+
if: steps.collect.outputs.has_inputs == 'true'
128+
run: |
129+
set -euo pipefail
130+
PR=${{ steps.pr.outputs.pr_number }}
131+
git fetch origin pull/${PR}/head
132+
git checkout -B translation-source-${PR} FETCH_HEAD
133+
134+
- name: Snapshot existing translation branches
135+
if: steps.collect.outputs.has_inputs == 'true'
136+
id: snapshot
137+
run: |
138+
git ls-remote --heads origin 'translation-*' | awk '{print $2}' | sed 's#refs/heads/##' | sort > /tmp/translation-branches-before.txt
139+
echo "before=/tmp/translation-branches-before.txt" >> "$GITHUB_OUTPUT"
140+
141+
- name: Run GPT Translate
142+
if: steps.collect.outputs.has_inputs == 'true'
143+
uses: BohuTANG/[email protected]
144+
with:
145+
github_token: ${{ secrets.GITHUB_TOKEN }}
146+
api_key: ${{ secrets.API_KEY }}
147+
base_url: ${{ secrets.BASE_URL }}
148+
ai_model: ${{ secrets.LLM_MODEL }}
149+
refine_ai_model: ${{ secrets.REFINE_LLM_MODEL }}
150+
target_lang: "Simplified-Chinese"
151+
system_prompt: ".github/workflows/prompt.txt"
152+
refine_system_prompt: ".github/workflows/refine_prompt.txt"
153+
temperature: ${{ secrets.TEMPERATURE }}
154+
refine_temperature: ${{ secrets.REFINE_TEMPERATURE }}
155+
input_files: "${{ steps.collect.outputs.input_files }}"
156+
output_files: "docs/cn/**/*.{md,json}"
157+
pr_title: "Add LLM Translations V2 for PR #${{ steps.pr.outputs.pr_number }}"
158+
159+
- name: Identify translation branch
160+
if: steps.collect.outputs.has_inputs == 'true'
161+
id: branch
162+
env:
163+
SNAPSHOT_FILE: ${{ steps.snapshot.outputs.before }}
164+
run: |
165+
git ls-remote --heads origin 'translation-*' | awk '{print $2}' | sed 's#refs/heads/##' | sort > /tmp/translation-branches-after.txt
166+
comm -13 "$SNAPSHOT_FILE" /tmp/translation-branches-after.txt > /tmp/new-translation-branches.txt
167+
branch=$(tail -n 1 /tmp/new-translation-branches.txt)
168+
if [ -n "$branch" ]; then
169+
echo "Discovered translation branch: $branch"
170+
echo "branch=$branch" >> "$GITHUB_OUTPUT"
171+
else
172+
echo "Unable to determine translation branch created by GPT workflow."
173+
echo "branch=" >> "$GITHUB_OUTPUT"
174+
fi
175+
176+
- name: Apply deletions to translation branch
177+
if: >
178+
steps.collect.outputs.has_inputs == 'true' &&
179+
steps.collect.outputs.has_removals == 'true' &&
180+
steps.branch.outputs.branch != ''
181+
env:
182+
REMOVED_FILES: ${{ steps.collect.outputs.removed_cn }}
183+
TRANSLATION_BRANCH: ${{ steps.branch.outputs.branch }}
184+
run: |
185+
set -euo pipefail
186+
git fetch origin "$TRANSLATION_BRANCH"
187+
git checkout "$TRANSLATION_BRANCH"
188+
189+
for file in $REMOVED_FILES; do
190+
if [ -f "$file" ]; then
191+
rm -f "$file"
192+
echo "Removed $file"
193+
fi
194+
done
195+
196+
find docs/cn -mindepth 1 -type d -empty -print -delete
197+
198+
if git status --porcelain | grep .; then
199+
git config user.name "github-actions[bot]"
200+
git config user.email "github-actions[bot]@users.noreply.github.com"
201+
git add -A
202+
git commit -m "chore: sync deletions for PR #${{ steps.pr.outputs.pr_number }}"
203+
git push origin "$TRANSLATION_BRANCH"
204+
else
205+
echo "No deletions to commit."
206+
fi
207+
208+
- name: Prepare deletion-only changes
209+
if: steps.collect.outputs.has_inputs != 'true' && steps.collect.outputs.has_removals == 'true'
210+
env:
211+
REMOVED_FILES: ${{ steps.collect.outputs.removed_cn }}
212+
run: |
213+
set -euo pipefail
214+
for file in $REMOVED_FILES; do
215+
if [ -f "$file" ]; then
216+
rm -f "$file"
217+
echo "Removed $file"
218+
fi
219+
done
220+
find docs/cn -mindepth 1 -type d -empty -print -delete
221+
222+
- name: Open deletion-only translation PR
223+
if: steps.collect.outputs.has_inputs != 'true' && steps.collect.outputs.has_removals == 'true'
224+
uses: peter-evans/create-pull-request@v6
225+
with:
226+
token: ${{ secrets.GITHUB_TOKEN }}
227+
branch: translation-pr-${{ steps.pr.outputs.pr_number }}
228+
base: main
229+
commit-message: "chore: sync deletions for PR #${{ steps.pr.outputs.pr_number }}"
230+
title: "AI Translate cleanup for PR #${{ steps.pr.outputs.pr_number }}"
231+
body: |
232+
This automated PR removes translated files that no longer have an English source from PR #${{ steps.pr.outputs.pr_number }}.

0 commit comments

Comments
 (0)