Skip to content

Commit 4c3067a

Browse files
guchenheclaude
authored andcommitted
Add workflow to retrigger translation on PR approval (#625)
Closes the gap where first-time contributor PRs require maintainer approval, but approval events don't trigger the translation workflow. New workflow: sync_docs_on_approval.yml - Listens to pull_request_review (submitted, approved) - Re-runs the most recent Analyze workflow for the PR - This triggers the existing Execute chain with fresh artifacts - Posts "approval received" comment before starting Note: Fork/author/reviewer checks are commented out for testing. Search for "TODO: UNCOMMENT" to restore after testing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 976b021 commit 4c3067a

2 files changed

Lines changed: 319 additions & 1 deletion

File tree

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# Workflow for retriggering translation sync when a maintainer approves a fork PR
2+
# This closes the gap where first-time contributors' PRs require approval,
3+
# but approval events don't trigger the existing workflow chain.
4+
name: Retrigger Sync on Approval
5+
6+
on:
7+
pull_request_review:
8+
types: [submitted]
9+
workflow_dispatch:
10+
inputs:
11+
pr_number:
12+
description: 'PR number to simulate approval for (for testing)'
13+
required: true
14+
type: string
15+
skip_checks:
16+
description: 'Skip fork/author checks (for testing internal PRs)'
17+
type: boolean
18+
default: true
19+
20+
permissions:
21+
contents: read
22+
pull-requests: write
23+
actions: write # Needed to re-run workflows
24+
25+
jobs:
26+
retrigger-on-approval:
27+
runs-on: ubuntu-latest
28+
# Only run for approved reviews OR manual dispatch
29+
if: github.event_name == 'workflow_dispatch' || github.event.review.state == 'approved'
30+
31+
steps:
32+
- name: Check if retrigger is needed
33+
id: check
34+
uses: actions/github-script@v7
35+
with:
36+
script: |
37+
const isManualTrigger = context.eventName === 'workflow_dispatch';
38+
const skipChecks = isManualTrigger && '${{ inputs.skip_checks }}' === 'true';
39+
40+
let prNumber, prAuthor, prHeadRepo, prBaseRepo, reviewer, reviewerAssociation;
41+
42+
if (isManualTrigger) {
43+
// Manual trigger - fetch PR details
44+
prNumber = parseInt('${{ inputs.pr_number }}');
45+
console.log(`Manual trigger for PR #${prNumber} (skip_checks: ${skipChecks})`);
46+
47+
const { data: pr } = await github.rest.pulls.get({
48+
owner: context.repo.owner,
49+
repo: context.repo.repo,
50+
pull_number: prNumber
51+
});
52+
53+
prAuthor = pr.user.login;
54+
prHeadRepo = pr.head.repo.full_name;
55+
prBaseRepo = pr.base.repo.full_name;
56+
reviewer = context.actor; // Person who triggered the workflow
57+
reviewerAssociation = 'MEMBER'; // Assume maintainer for manual trigger
58+
} else {
59+
// Pull request review trigger
60+
prNumber = context.payload.pull_request.number;
61+
prAuthor = context.payload.pull_request.user.login;
62+
prHeadRepo = context.payload.pull_request.head.repo.full_name;
63+
prBaseRepo = context.payload.pull_request.base.repo.full_name;
64+
reviewer = context.payload.review.user.login;
65+
reviewerAssociation = context.payload.review.author_association;
66+
}
67+
68+
console.log(`PR #${prNumber} approved by ${reviewer} (${reviewerAssociation})`);
69+
console.log(`Author: ${prAuthor}`);
70+
console.log(`Head repo: ${prHeadRepo}`);
71+
console.log(`Base repo: ${prBaseRepo}`);
72+
73+
// TODO: UNCOMMENT THESE CHECKS AFTER TESTING
74+
// Check 1: Is this a fork PR?
75+
// const isFork = prHeadRepo !== prBaseRepo;
76+
// if (!isFork) {
77+
// console.log('Not a fork PR - approval gate not needed, skipping retrigger');
78+
// core.setOutput('should_retrigger', 'false');
79+
// core.setOutput('reason', 'not_fork');
80+
// return;
81+
// }
82+
// console.log('PR is from a fork - approval gate applies');
83+
84+
// Check 2: Is the PR author already trusted? If so, no approval gate was needed
85+
// const trustedAssociations = ['OWNER', 'MEMBER', 'COLLABORATOR'];
86+
// const authorAssociation = context.payload.pull_request.author_association;
87+
// if (trustedAssociations.includes(authorAssociation)) {
88+
// console.log(`PR author ${prAuthor} is already trusted (${authorAssociation}) - no approval gate needed`);
89+
// core.setOutput('should_retrigger', 'false');
90+
// core.setOutput('reason', 'author_trusted');
91+
// return;
92+
// }
93+
// console.log(`PR author ${prAuthor} is not trusted (${authorAssociation}) - approval gate applies`);
94+
95+
// Check 3: Is the reviewer a trusted maintainer?
96+
// if (!trustedAssociations.includes(reviewerAssociation)) {
97+
// console.log(`Reviewer ${reviewer} is not a maintainer (${reviewerAssociation}) - cannot unlock approval gate`);
98+
// core.setOutput('should_retrigger', 'false');
99+
// core.setOutput('reason', 'reviewer_not_maintainer');
100+
// return;
101+
// }
102+
// console.log(`Reviewer ${reviewer} is a maintainer - approval is valid`);
103+
console.log('⚠️ CHECKS COMMENTED OUT FOR TESTING - skipping fork/author/reviewer checks');
104+
105+
// Check 4: Does translation PR already exist?
106+
const syncBranch = `docs-sync-pr-${prNumber}`;
107+
try {
108+
const { data: branches } = await github.rest.repos.listBranches({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
per_page: 100
112+
});
113+
114+
const branchExists = branches.some(b => b.name === syncBranch);
115+
if (branchExists) {
116+
console.log(`Translation branch ${syncBranch} already exists`);
117+
118+
// Check if there's an open PR for it
119+
const { data: prs } = await github.rest.pulls.list({
120+
owner: context.repo.owner,
121+
repo: context.repo.repo,
122+
head: `${context.repo.owner}:${syncBranch}`,
123+
state: 'open'
124+
});
125+
126+
if (prs.length > 0) {
127+
console.log(`Translation PR #${prs[0].number} already exists and is open`);
128+
core.setOutput('should_retrigger', 'false');
129+
core.setOutput('reason', 'translation_pr_exists');
130+
core.setOutput('translation_pr_number', prs[0].number.toString());
131+
core.setOutput('pr_number', prNumber.toString());
132+
return;
133+
}
134+
}
135+
} catch (e) {
136+
console.log(`Error checking for translation branch: ${e.message}`);
137+
// Continue anyway - we'll try to create it
138+
}
139+
140+
// Check 5: Find most recent Analyze run for this PR
141+
console.log('Looking for Analyze workflow runs for this PR...');
142+
const { data: runs } = await github.rest.actions.listWorkflowRuns({
143+
owner: context.repo.owner,
144+
repo: context.repo.repo,
145+
workflow_id: 'sync_docs_analyze.yml',
146+
event: 'pull_request',
147+
per_page: 100
148+
});
149+
150+
console.log(`Found ${runs.workflow_runs.length} Analyze workflow runs`);
151+
152+
// Find the most recent run matching this PR
153+
let matchingRun = null;
154+
for (const run of runs.workflow_runs) {
155+
if (run.pull_requests && run.pull_requests.some(pr => pr.number === prNumber)) {
156+
matchingRun = run;
157+
break; // Runs are sorted by created_at desc, so first match is most recent
158+
}
159+
}
160+
161+
if (!matchingRun) {
162+
console.log('No Analyze workflow run found for this PR');
163+
console.log('This could mean the PR has no documentation changes, or the run is too old');
164+
core.setOutput('should_retrigger', 'false');
165+
core.setOutput('reason', 'no_analyze_run');
166+
core.setOutput('pr_number', prNumber.toString());
167+
return;
168+
}
169+
170+
console.log(`Found Analyze run #${matchingRun.id}`);
171+
console.log(` Status: ${matchingRun.status}`);
172+
console.log(` Conclusion: ${matchingRun.conclusion}`);
173+
console.log(` Created: ${matchingRun.created_at}`);
174+
console.log(` Head SHA: ${matchingRun.head_sha}`);
175+
176+
// All checks passed - we should retrigger
177+
core.setOutput('should_retrigger', 'true');
178+
core.setOutput('analyze_run_id', matchingRun.id.toString());
179+
core.setOutput('pr_number', prNumber.toString());
180+
core.setOutput('reviewer', reviewer);
181+
182+
- name: Post approval received comment
183+
if: steps.check.outputs.should_retrigger == 'true'
184+
uses: actions/github-script@v7
185+
with:
186+
script: |
187+
const prNumber = parseInt('${{ steps.check.outputs.pr_number }}');
188+
const reviewer = '${{ steps.check.outputs.reviewer }}';
189+
190+
const comment = `## 🌐 Multi-language Sync\n\n` +
191+
`✅ **Approval received from @${reviewer}** - starting translation sync.\n\n` +
192+
`Translation will begin shortly. A sync PR will be created automatically.`;
193+
194+
await github.rest.issues.createComment({
195+
owner: context.repo.owner,
196+
repo: context.repo.repo,
197+
issue_number: prNumber,
198+
body: comment
199+
});
200+
201+
console.log(`Posted approval received comment on PR #${prNumber}`);
202+
203+
- name: Re-run Analyze workflow
204+
if: steps.check.outputs.should_retrigger == 'true'
205+
uses: actions/github-script@v7
206+
with:
207+
script: |
208+
const runId = parseInt('${{ steps.check.outputs.analyze_run_id }}');
209+
210+
console.log(`Re-running Analyze workflow run #${runId}...`);
211+
212+
try {
213+
await github.rest.actions.reRunWorkflow({
214+
owner: context.repo.owner,
215+
repo: context.repo.repo,
216+
run_id: runId
217+
});
218+
219+
console.log('Analyze workflow re-run triggered successfully');
220+
console.log('This will trigger the Execute workflow chain with fresh artifacts');
221+
} catch (error) {
222+
console.log(`Failed to re-run workflow: ${error.message}`);
223+
224+
// If re-run fails (e.g., run is too old), post a comment explaining
225+
const prNumber = parseInt('${{ steps.check.outputs.pr_number }}');
226+
await github.rest.issues.createComment({
227+
owner: context.repo.owner,
228+
repo: context.repo.repo,
229+
issue_number: prNumber,
230+
body: `## 🌐 Multi-language Sync\n\n` +
231+
`⚠️ **Could not automatically start translation**\n\n` +
232+
`The workflow run is too old to re-run. Please push a small commit ` +
233+
`(e.g., add a newline to any file) to trigger a fresh translation workflow.\n\n` +
234+
`Alternatively, a maintainer can manually trigger the workflow from the Actions tab.`
235+
});
236+
237+
throw error;
238+
}
239+
240+
- name: Handle translation PR already exists
241+
if: steps.check.outputs.reason == 'translation_pr_exists'
242+
uses: actions/github-script@v7
243+
with:
244+
script: |
245+
const prNumber = parseInt('${{ steps.check.outputs.pr_number }}');
246+
const translationPrNumber = '${{ steps.check.outputs.translation_pr_number }}';
247+
248+
const comment = `## 🌐 Multi-language Sync\n\n` +
249+
`ℹ️ Translation PR [#${translationPrNumber}](https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${translationPrNumber}) already exists.\n\n` +
250+
`Future commits to this PR will automatically update the translation PR.`;
251+
252+
await github.rest.issues.createComment({
253+
owner: context.repo.owner,
254+
repo: context.repo.repo,
255+
issue_number: prNumber,
256+
body: comment
257+
});
258+
259+
console.log(`Posted info comment about existing translation PR #${translationPrNumber}`);
260+
261+
- name: Handle no Analyze run found
262+
if: steps.check.outputs.reason == 'no_analyze_run'
263+
uses: actions/github-script@v7
264+
with:
265+
script: |
266+
const prNumber = parseInt('${{ steps.check.outputs.pr_number }}');
267+
268+
// Only post comment if this PR likely should have had an Analyze run
269+
// (i.e., it has documentation file changes)
270+
// For now, we'll post a gentle informational comment
271+
272+
const comment = `## 🌐 Multi-language Sync\n\n` +
273+
`ℹ️ **No pending translation sync found for this PR.**\n\n` +
274+
`This could mean:\n` +
275+
`- The PR doesn't contain source documentation changes (en/ files)\n` +
276+
`- The original workflow run is too old\n\n` +
277+
`If you expected translations, please push a small commit to trigger a fresh workflow run.`;
278+
279+
await github.rest.issues.createComment({
280+
owner: context.repo.owner,
281+
repo: context.repo.repo,
282+
issue_number: prNumber,
283+
body: comment
284+
});
285+
286+
console.log(`Posted info comment about no Analyze run found`);

CLAUDE.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,28 @@ All language settings in `tools/translate/config.json` (single source of truth):
7373
- **Moved files**: Detected via `group_path` changes, cn/jp relocated using index-based navigation
7474
- **Renamed files**: Detected when deleted+added in same location, physical files renamed with extension preserved
7575

76+
### First-Time Contributor Approval Flow
77+
78+
For PRs from forks by contributors who are not OWNER/MEMBER/COLLABORATOR:
79+
80+
1. **PR opened** → Analyze workflow runs → Execute workflow checks for approval
81+
2. **No approval found** → Execute skips translation, posts "pending approval" comment
82+
3. **Maintainer approves PR**`sync_docs_on_approval.yml` triggers automatically
83+
4. **"Approval received" comment posted** → Analyze workflow re-runs with fresh artifacts
84+
5. **Execute workflow runs** → Finds approval → Creates translation PR
85+
86+
**Approval requirements**:
87+
- Reviewer must have OWNER, MEMBER, or COLLABORATOR association
88+
- Approval triggers immediate translation (no additional push needed)
89+
- Each approval posts a new comment preserving the timeline
90+
91+
**Edge cases**:
92+
- If translation PR already exists when approval happens → info comment posted, no re-run
93+
- If Analyze run is too old to re-run → error comment with instructions to push a small commit
94+
- Internal PRs (same repo, not fork) → no approval gate, auto-translates immediately
95+
96+
**Manual trigger**: If approval flow fails, maintainers can manually trigger via Actions → Execute Documentation Sync → Run workflow (enter PR number)
97+
7698
## Development Commands
7799

78100
```bash
@@ -128,6 +150,12 @@ SUCCESS: Moved cn/test-file to new location
128150
SUCCESS: Moved jp/test-file to new location
129151
```
130152

153+
**Approval flow issues** (first-time contributors):
154+
- **Translation not starting after approval**: Check Actions tab for `Retrigger Sync on Approval` workflow status
155+
- **"Could not automatically start translation"**: Analyze run too old - push a small commit to trigger fresh workflow
156+
- **Approval from non-maintainer**: Only OWNER/MEMBER/COLLABORATOR approvals unlock the gate
157+
- **Multiple "pending approval" comments**: Normal - each commit triggers Execute which posts if no approval found
158+
131159
## Translation A/B Testing
132160

133161
For comparing translation quality between models or prompt variations:
@@ -152,4 +180,8 @@ python compare.py results/<folder>/
152180
- `tools/translate/termbase_i18n.md` - Translation terminology database
153181
- `tools/translate/sync_and_translate.py` - Core translation + surgical reconciliation logic
154182
- `tools/translate-test-dify/` - Translation A/B testing framework
155-
- `.github/workflows/sync_docs_*.yml` - Auto-translation workflow triggers
183+
- `.github/workflows/sync_docs_analyze.yml` - Analyzes PR changes, uploads artifacts
184+
- `.github/workflows/sync_docs_execute.yml` - Creates translation PRs (triggered by Analyze)
185+
- `.github/workflows/sync_docs_update.yml` - Updates existing translation PRs
186+
- `.github/workflows/sync_docs_cleanup.yml` - Cleans up sync PRs when original PR closes
187+
- `.github/workflows/sync_docs_on_approval.yml` - Retriggers translation on maintainer approval

0 commit comments

Comments
 (0)