removed unnecessary attribute listings. Closes #19 #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Sync Translations (i18n) | |
| # EN push → i18n-sync issue per locale → assign copilot-swe-agent via COPILOT_GITHUB_TOKEN (PAT). | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'docs/**' | |
| - 'src/theme/**' | |
| - '.github/i18n-locales.json' | |
| - '.github/workflows/i18n-translation.yml' | |
| - '.github/scripts/i18n-sync-issues.mjs' | |
| pull_request: | |
| types: [opened, ready_for_review] | |
| paths: | |
| - 'i18n/**' | |
| workflow_dispatch: | |
| inputs: | |
| compare_all_docs: | |
| description: List all English doc paths (full resync) instead of git diff only | |
| type: boolean | |
| default: false | |
| base_ref: | |
| description: Git ref to compare against HEAD (ignored when compare_all_docs is true) | |
| type: string | |
| default: HEAD~1 | |
| note: | |
| description: Optional note included in sync issues | |
| type: string | |
| required: false | |
| concurrency: | |
| group: i18n-translation-${{ github.event.pull_request.number || github.sha }} | |
| cancel-in-progress: false | |
| jobs: | |
| create-sync-issues: | |
| name: Open and assign i18n-sync issues | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'push' && | |
| !contains(github.event.head_commit.message, '[skip-i18n]')) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: List changed English source files | |
| id: changed | |
| env: | |
| COMPARE_ALL_DOCS: ${{ github.event_name == 'workflow_dispatch' && inputs.compare_all_docs || 'false' }} | |
| BASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.base_ref || 'HEAD~1' }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$COMPARE_ALL_DOCS" = "true" ]; then | |
| mapfile -t files < <( | |
| git ls-files docs/ docs/_components/ src/theme/ 2>/dev/null || true | |
| ) | |
| elif git rev-parse "$BASE_REF" >/dev/null 2>&1; then | |
| mapfile -t files < <(git diff --name-only "$BASE_REF" HEAD) | |
| elif git rev-parse HEAD~1 >/dev/null 2>&1; then | |
| mapfile -t files < <(git diff --name-only HEAD~1 HEAD) | |
| else | |
| mapfile -t files < <(git diff-tree --no-commit-id --name-only -r HEAD) | |
| fi | |
| { | |
| echo "files<<EOF" | |
| printf '%s\n' "${files[@]}" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Prepare issue payloads | |
| id: prepare | |
| env: | |
| I18N_COMMIT_SHA: ${{ github.sha }} | |
| I18N_EVENT_NAME: ${{ github.event_name }} | |
| I18N_DISPATCH_NOTE: ${{ github.event_name == 'workflow_dispatch' && inputs.note || '' }} | |
| I18N_PUSH_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} | |
| GITHUB_ACTOR: ${{ github.actor }} | |
| I18N_CHANGED_FILES: ${{ steps.changed.outputs.files }} | |
| run: node .github/scripts/i18n-sync-issues.mjs | |
| - name: Report when nothing to sync | |
| if: steps.prepare.outputs.has_work != 'true' | |
| run: | | |
| echo "::warning::No i18n-sync issue created." | |
| echo "The git diff for this run had no changes under docs/, docs/_components/, or src/theme/." | |
| echo "Push an English doc change on main, or re-run manually with compare_all_docs=true." | |
| - name: Verify Copilot PAT secret | |
| if: steps.prepare.outputs.has_work == 'true' | |
| run: | | |
| if [ -z "${{ secrets.COPILOT_GITHUB_TOKEN }}" ]; then | |
| echo "::error::Missing repository secret COPILOT_GITHUB_TOKEN. See AGENTS.md → Automated i18n sync → Setup." | |
| exit 1 | |
| fi | |
| - name: Create issues and assign Copilot | |
| if: steps.prepare.outputs.has_work == 'true' | |
| uses: actions/github-script@v8 | |
| env: | |
| ISSUES_JSON: ${{ steps.prepare.outputs.issues }} | |
| COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const config = JSON.parse(fs.readFileSync('.github/i18n-locales.json', 'utf8')); | |
| const items = JSON.parse(process.env.ISSUES_JSON); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const repoSlug = `${owner}/${repo}`; | |
| const assignee = config.copilotAssignee ?? 'copilot-swe-agent[bot]'; | |
| const baseBranch = config.baseBranch ?? 'main'; | |
| const copilotToken = process.env.COPILOT_GITHUB_TOKEN; | |
| const agentAssignment = { | |
| target_repo: repoSlug, | |
| base_branch: baseBranch, | |
| custom_instructions: | |
| 'Read AGENTS.md and .github/copilot-instructions.md. Patch i18n translation mirrors only; run npm ci && npm run build; open a PR to main.', | |
| }; | |
| async function assignCopilot(issueNumber) { | |
| const response = await fetch( | |
| `https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/assignees`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${copilotToken}`, | |
| Accept: 'application/vnd.github+json', | |
| 'X-GitHub-Api-Version': '2022-11-28', | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| assignees: [assignee], | |
| agent_assignment: agentAssignment, | |
| }), | |
| }, | |
| ); | |
| if (!response.ok) { | |
| const detail = await response.text(); | |
| core.setFailed( | |
| `Could not assign ${assignee} to issue #${issueNumber} (${response.status}): ${detail}`, | |
| ); | |
| return false; | |
| } | |
| core.info(`Assigned ${assignee} to issue #${issueNumber}`); | |
| return true; | |
| } | |
| for (const item of items) { | |
| const labelQuery = [item.labels[0], item.locale].join(','); | |
| const { data: openIssues } = await github.rest.issues.listForRepo({ | |
| owner, | |
| repo, | |
| state: 'open', | |
| labels: labelQuery, | |
| per_page: 20, | |
| }); | |
| const existing = openIssues.find((issue) => | |
| issue.title.startsWith(`i18n(${item.locale}):`), | |
| ); | |
| if (existing) { | |
| await github.rest.issues.update({ | |
| owner, | |
| repo, | |
| issue_number: existing.number, | |
| state: 'closed', | |
| state_reason: 'not_planned', | |
| }); | |
| core.info(`Closed superseded i18n-sync issue #${existing.number} for ${item.locale}`); | |
| } | |
| const createResponse = await fetch( | |
| `https://api.github.com/repos/${owner}/${repo}/issues`, | |
| { | |
| method: 'POST', | |
| headers: { | |
| Authorization: `Bearer ${copilotToken}`, | |
| Accept: 'application/vnd.github+json', | |
| 'X-GitHub-Api-Version': '2022-11-28', | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| title: item.title, | |
| body: item.body, | |
| labels: item.labels, | |
| assignees: [assignee], | |
| agent_assignment: agentAssignment, | |
| }), | |
| }, | |
| ); | |
| if (!createResponse.ok) { | |
| const detail = await createResponse.text(); | |
| core.setFailed( | |
| `Could not create i18n-sync issue for ${item.locale} (${createResponse.status}): ${detail}`, | |
| ); | |
| continue; | |
| } | |
| const created = await createResponse.json(); | |
| core.info(`Created i18n-sync issue #${created.number} for ${item.locale}`); | |
| const alreadyAssigned = (created.assignees ?? []).some( | |
| (user) => user.login === assignee, | |
| ); | |
| if (!alreadyAssigned) { | |
| await assignCopilot(created.number); | |
| } | |
| } | |
| request-review: | |
| name: Request locale team review | |
| if: >- | |
| github.event_name == 'pull_request' && | |
| !github.event.pull_request.draft | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Request review from locale teams | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const config = JSON.parse(fs.readFileSync('.github/i18n-locales.json', 'utf8')); | |
| const pr = context.payload.pull_request; | |
| const prLabels = new Set((pr.labels || []).map((label) => label.name)); | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| per_page: 100, | |
| }); | |
| const changedPaths = files.map((file) => file.filename); | |
| const teams = new Set(); | |
| for (const locale of config.locales) { | |
| const localePrefix = `i18n/${locale.code}/`; | |
| const touchesLocale = | |
| prLabels.has(locale.code) || | |
| changedPaths.some((path) => path.startsWith(localePrefix)); | |
| if (touchesLocale && locale.reviewTeam) { | |
| teams.add(locale.reviewTeam); | |
| } | |
| } | |
| if (teams.size === 0) { | |
| core.info('No locale teams matched this PR; skipping review request.'); | |
| return; | |
| } | |
| const labelsToAdd = new Set(['i18n']); | |
| for (const locale of config.locales) { | |
| if (teams.has(locale.reviewTeam)) { | |
| labelsToAdd.add(locale.code); | |
| for (const label of locale.prLabels) { | |
| labelsToAdd.add(label); | |
| } | |
| } | |
| } | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: [...labelsToAdd], | |
| }); | |
| await github.rest.pulls.requestReviewers({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| team_reviewers: [...teams], | |
| }); | |
| core.info(`Requested review from teams: ${[...teams].join(', ')}`); |