Skip to content

Commit e264dc4

Browse files
authored
fix: commands and autotranslate (#682)
1 parent cd064f5 commit e264dc4

3 files changed

Lines changed: 194 additions & 44 deletions

File tree

.github/workflows/ArticlesAutoTranslate.yml

Lines changed: 181 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
name: Articles Auto Translate
2-
run-name: ${{ github.event.label.name }} - ${{ github.event.issue.title }}
2+
run-name: auto - ${{ github.event.issue.title }}
33

44
on:
55
issues:
66
types: [labeled]
77

88
env:
9-
# Git helper functions for push retry
9+
# Git helper functions for push retry with exponential backoff
1010
PUSH_RETRY_SCRIPT: |
1111
push_with_retry() {
1212
local branch=${1:-main}
13-
local max_retries=${2:-3}
14-
13+
local max_retries=${2:-4}
14+
1515
for i in $(seq 1 $max_retries); do
1616
if git push origin "$branch"; then
1717
echo "Push to $branch successful"
1818
return 0
1919
else
20-
echo "Push failed, attempt $i/$max_retries"
20+
wait_time=$((2 ** i))
21+
echo "Push failed, attempt $i/$max_retries. Waiting ${wait_time}s..."
2122
if [ $i -lt $max_retries ]; then
23+
sleep $wait_time
24+
git fetch origin "$branch" || true
2225
git pull --rebase origin "$branch" || true
23-
sleep 5
2426
fi
2527
fi
2628
done
@@ -29,38 +31,100 @@ env:
2931
}
3032
3133
jobs:
34+
# First job: Validate the trigger and extract information
35+
validate:
36+
name: Validate Auto-Translate Trigger
37+
runs-on: ubuntu-latest
38+
# Only run if the label added is "auto"
39+
if: github.event.label.name == 'auto'
40+
outputs:
41+
lang_code: ${{ steps.extract.outputs.lang_code }}
42+
lang_label: ${{ steps.extract.outputs.lang_label }}
43+
is_valid: ${{ steps.extract.outputs.is_valid }}
44+
45+
steps:
46+
- name: Validate issue and extract language
47+
id: extract
48+
run: |
49+
# Extract language code from issue title (e.g., "[zh] Article Title" -> "zh")
50+
title="${{ github.event.issue.title }}"
51+
LANG_CODE=$(echo "$title" | sed -E 's/^\[([a-zA-Z]+)\].*/\1/' | tr '[:upper:]' '[:lower:]')
52+
53+
# Map language codes to language label names
54+
declare -A LANG_MAP=(
55+
["zh"]="chinese"
56+
["es"]="spanish"
57+
["pt"]="portuguese"
58+
["it"]="italian"
59+
["ja"]="japanese"
60+
["ko"]="korean"
61+
["uk"]="ukrainian"
62+
)
63+
64+
LANG_LABEL="${LANG_MAP[$LANG_CODE]:-}"
65+
66+
# Validate
67+
if [ -z "$LANG_CODE" ] || [ -z "$LANG_LABEL" ]; then
68+
echo "is_valid=false" >> $GITHUB_OUTPUT
69+
echo "::error::Could not extract valid language from title: $title"
70+
echo "::error::Expected format: [lang] Title (e.g., [zh] Article Title)"
71+
exit 1
72+
fi
73+
74+
# Validate issue body is not empty
75+
if [ -z "${{ github.event.issue.body }}" ]; then
76+
echo "is_valid=false" >> $GITHUB_OUTPUT
77+
echo "::error::Issue body is empty. Please provide article URL."
78+
exit 1
79+
fi
80+
81+
echo "lang_code=$LANG_CODE" >> $GITHUB_OUTPUT
82+
echo "lang_label=$LANG_LABEL" >> $GITHUB_OUTPUT
83+
echo "is_valid=true" >> $GITHUB_OUTPUT
84+
echo "Language code: $LANG_CODE, Language label: $LANG_LABEL"
85+
3286
auto-translate:
87+
name: Auto Translate Article
88+
needs: validate
89+
if: needs.validate.outputs.is_valid == 'true'
3390
runs-on: ubuntu-latest
3491
permissions:
3592
issues: write
3693
contents: write
3794

95+
# Use language-specific concurrency group so different languages can run in parallel
96+
# but same-language translations are queued
97+
concurrency:
98+
group: auto-translate-${{ needs.validate.outputs.lang_code }}
99+
cancel-in-progress: false
100+
101+
env:
102+
LANG_CODE: ${{ needs.validate.outputs.lang_code }}
103+
LANG_LABEL: ${{ needs.validate.outputs.lang_label }}
104+
38105
steps:
39-
### Queue lock: prevent concurrent conflicts
106+
### Queue lock: additional protection for same-language concurrent runs
40107
- uses: softprops/turnstyle@v1
41108
with:
42-
poll-interval-seconds: 10
109+
poll-interval-seconds: 15
43110
env:
44111
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45112

46-
### Check issue body
47-
- name: Validate issue body
48-
if: ${{ !github.event.issue.body }}
49-
run: |
50-
echo "::error::Issue body is empty. Please provide article URL."
51-
exit 1
52-
53-
### Extract language code
54-
- name: Extract language code
55-
id: lang
56-
run: |
57-
title="${{ github.event.issue.title }}"
58-
LANG_CODE=$(echo "$title" | sed -E 's/^\[([a-zA-Z]+)\].*/\1/')
59-
echo "lang=$LANG_CODE" >> $GITHUB_OUTPUT
60-
echo "LANG_CODE=$LANG_CODE" >> $GITHUB_ENV
113+
- name: Add processing comment
114+
uses: actions/github-script@v7
115+
with:
116+
script: |
117+
await github.rest.issues.createComment({
118+
issue_number: context.issue.number,
119+
owner: context.repo.owner,
120+
repo: context.repo.repo,
121+
body: 'Auto-translation workflow started for language: **${{ env.LANG_CODE }}**. This may take a few minutes...'
122+
});
61123
62124
### Checkout repository
63125
- uses: actions/checkout@v5
126+
with:
127+
fetch-depth: 0
64128

65129
### Configure Git
66130
- name: Setup Git
@@ -88,24 +152,27 @@ jobs:
88152
run: |
89153
original_file="${{ steps.fetch.outputs.markdown_file_path }}"
90154
filename=$(basename "$original_file")
91-
BACKUP_PATH="/tmp/$filename"
155+
# Sanitize filename
156+
safe_filename=$(echo "$filename" | sed 's/[^a-zA-Z0-9._-]/-/g')
157+
BACKUP_PATH="/tmp/$safe_filename"
92158
cp "$original_file" "$BACKUP_PATH"
93159
echo "path=$BACKUP_PATH" >> $GITHUB_OUTPUT
160+
echo "filename=$safe_filename" >> $GITHUB_OUTPUT
94161
echo "BACKUP_PATH=$BACKUP_PATH" >> $GITHUB_ENV
162+
echo "SAFE_FILENAME=$safe_filename" >> $GITHUB_ENV
95163
echo "Markdown backed up to $BACKUP_PATH"
96164
97-
### Save English raw text and commit to main branch
165+
### Save English raw text ONLY to _raw folder on main branch
166+
### Do NOT copy to language folder yet - that happens after translation
98167
- name: Commit raw article to main
99168
run: |
100169
file="${{ steps.fetch.outputs.markdown_file_path }}"
101-
base=$(basename "$file")
102-
lang="${{ steps.lang.outputs.lang }}"
170+
base="${{ env.SAFE_FILENAME }}"
103171
104172
mkdir -p "./articles/_raw/"
105173
cp "$file" "./articles/_raw/$base"
106-
[ -f "./articles/$lang/$base" ] || cp "$file" "./articles/$lang/$base"
107174
108-
git add -f "./articles/_raw/$base" "./articles/$lang/$base" || true
175+
git add -f "./articles/_raw/$base" || true
109176
git commit -m "Add raw article: $base" || echo "Nothing to commit."
110177
111178
git fetch origin main
@@ -130,36 +197,67 @@ jobs:
130197
rm -rf ./articles/_tmp/
131198
132199
git fetch origin
133-
git checkout -B auto-translate origin/auto-translate
134200
201+
# Create or checkout auto-translate branch
202+
if git show-ref --verify --quiet refs/remotes/origin/auto-translate; then
203+
git checkout -B auto-translate origin/auto-translate
204+
else
205+
git checkout -b auto-translate
206+
fi
207+
208+
# Merge main to get the raw article
135209
git merge --strategy=recursive --strategy-option=theirs main || true
136210
211+
### Ensure language directory exists
212+
- name: Prepare language directory
213+
run: |
214+
lang="${{ env.LANG_CODE }}"
215+
mkdir -p "./articles/$lang/"
216+
137217
### Auto-translate article (use secure path)
138218
- name: Translate article
139219
uses: freeCodeCamp/articles-auto-translate-action@main
140220
with:
141221
with_issue_title: "${{ github.event.issue.title }}"
142222
with_issue_body: "${{ github.event.issue.body }}"
143-
with_label_name: "${{ github.event.label.name }}"
223+
with_label_name: "${{ env.LANG_LABEL }}"
144224
with_github_token: "${{ github.token }}"
145-
with_original_markdown_file_path: "${{ steps.backup-md.outputs.path }}"
225+
with_original_markdown_file_path: "${{ env.BACKUP_PATH }}"
146226
with_task_fetch_to_save_path: "./articles/_raw/"
147227
with_task_translate_openai_api_key: "${{ secrets.OPENAI_API_KEY }}"
148228
with_task_translate_to_save_path: "./articles/{lang}/"
149229

150-
### Commit translated file
230+
### Verify translation was created and commit
151231
- name: Commit translated article
232+
id: commit-translation
152233
run: |
153-
base=$(basename "$BACKUP_PATH")
234+
base="${{ env.SAFE_FILENAME }}"
154235
lang="${{ env.LANG_CODE }}"
155236
translated="./articles/$lang/$base"
156237
157-
[ -f "$translated" ] || { echo "Translated file missing."; exit 1; }
238+
echo "Checking for translated file: $translated"
239+
240+
if [ ! -f "$translated" ]; then
241+
echo "::error::Translated file not found at: $translated"
242+
echo "Listing articles directory contents:"
243+
find ./articles -type f -name "*.md" | head -20
244+
exit 1
245+
fi
246+
247+
# Verify translation actually happened (file should differ from original)
248+
original="./articles/_raw/$base"
249+
if [ -f "$original" ]; then
250+
if diff -q "$original" "$translated" > /dev/null 2>&1; then
251+
echo "::warning::Translated file is identical to original - translation may have failed"
252+
else
253+
echo "Translation verified: files differ from original"
254+
fi
255+
fi
158256
159257
git add "$translated"
160-
git commit -m "Add translated article: $base" || echo "Nothing to commit."
258+
git commit -m "Add translated article ($lang): $base" || echo "Nothing to commit."
161259
162-
git fetch origin auto-translate
260+
git fetch origin auto-translate || true
163261
164262
# Safe rebase, resolve conflicts
165263
git stash push -u -m "Auto-stash before rebase" || true
@@ -174,9 +272,10 @@ jobs:
174272
eval "$PUSH_RETRY_SCRIPT"
175273
push_with_retry auto-translate
176274
177-
### Clean up temp directory
275+
### Clean up temp directory on main
178276
- name: Cleanup temp directory
179277
run: |
278+
git checkout main
180279
rm -rf ./articles/_tmp/
181280
git add -u ./articles/_tmp/ || true
182281
git commit -m "Cleanup _tmp directory" || echo "Nothing to commit."
@@ -188,4 +287,47 @@ jobs:
188287
189288
# Use push retry function for final cleanup
190289
eval "$PUSH_RETRY_SCRIPT"
191-
push_with_retry main
290+
push_with_retry main
291+
292+
### Add success comment
293+
- name: Add success comment
294+
if: success()
295+
uses: actions/github-script@v7
296+
with:
297+
script: |
298+
const langCode = '${{ env.LANG_CODE }}';
299+
const filename = '${{ env.SAFE_FILENAME }}';
300+
301+
const message = `Auto-translation completed successfully!
302+
303+
**Files created:**
304+
- Raw article: \`articles/_raw/${filename}\`
305+
- Translated article (auto-translate branch): \`articles/${langCode}/${filename}\`
306+
307+
**Next steps:**
308+
1. Contributors can proofread the translation on the \`auto-translate\` branch
309+
2. Use \`/postedit\` command to claim this article for post-editing
310+
3. When ready, create a PR from \`auto-translate\` to \`main\`
311+
312+
[View translated file](https://github.com/${{ github.repository }}/blob/auto-translate/articles/${langCode}/${filename})
313+
[Edit in github.dev](https://github.dev/${{ github.repository }}/blob/auto-translate/articles/${langCode}/${filename})`;
314+
315+
await github.rest.issues.createComment({
316+
issue_number: context.issue.number,
317+
owner: context.repo.owner,
318+
repo: context.repo.repo,
319+
body: message
320+
});
321+
322+
### Add failure comment
323+
- name: Add failure comment
324+
if: failure()
325+
uses: actions/github-script@v7
326+
with:
327+
script: |
328+
await github.rest.issues.createComment({
329+
issue_number: context.issue.number,
330+
owner: context.repo.owner,
331+
repo: context.repo.repo,
332+
body: 'Auto-translation workflow failed. Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.'
333+
});

scripts/getProjectCard.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const PROJECT_QUERY = `
3333
* @param {Object} params - Parameters from GitHub Actions
3434
* @returns {Object} - Project card information
3535
*/
36-
export async function getProjectCard({ github, context, core }) {
36+
async function getProjectCard({ github, context, core }) {
3737
const issueNodeId = context.payload.issue.node_id;
3838

3939
const result = await github.graphql(PROJECT_QUERY, { id: issueNodeId });
@@ -77,11 +77,15 @@ export async function getProjectCard({ github, context, core }) {
7777
* Main function to get project card and set outputs
7878
* @param {Object} params - Parameters from GitHub Actions
7979
*/
80-
export async function main({ github, context, core }) {
80+
async function main({ github, context, core }) {
8181
try {
8282
const cardInfo = await getProjectCard({ github, context, core });
8383

84-
console.table({ cardId, projectUrl, projectName });
84+
console.table({
85+
cardId: cardInfo.cardId,
86+
projectUrl: cardInfo.projectUrl,
87+
projectName: cardInfo.projectName
88+
});
8589

8690
core.setOutput("card_id", cardInfo.cardId);
8791
core.setOutput("project_url", cardInfo.projectUrl);
@@ -90,3 +94,5 @@ export async function main({ github, context, core }) {
9094
core.setFailed(error.message);
9195
}
9296
}
97+
98+
module.exports = { getProjectCard, main };

scripts/validateCommand.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @param {string} comment - The comment text to validate
44
* @returns {Object} - Command validation result
55
*/
6-
export function validateCommand(comment) {
6+
function validateCommand(comment) {
77
const commandMap = {
88
"/postedit": { command: "postedit", targetStatus: "in Postediting" },
99
"/post-edit": { command: "postedit", targetStatus: "in Postediting" },
@@ -24,7 +24,7 @@ export function validateCommand(comment) {
2424
* Main function to validate command and set outputs
2525
* @param {Object} params - Parameters from GitHub Actions
2626
*/
27-
export async function main({ github, context, core }) {
27+
async function main({ github, context, core }) {
2828
const comment = context.payload.comment.body;
2929
const result = validateCommand(comment);
3030

@@ -38,3 +38,5 @@ export async function main({ github, context, core }) {
3838
);
3939
}
4040
}
41+
42+
module.exports = { validateCommand, main };

0 commit comments

Comments
 (0)