-
Notifications
You must be signed in to change notification settings - Fork 177
chore(ci): automate translation maintenance #3796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
4e207ae
960fae4
012f2ee
54ecf4a
0679e58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| name: Translations Check | ||
|
|
||
| # Creates translation PRs for languages that have missing `msgstr` entries | ||
| # or are affected by newly-added rules in src/frontend/src/lib/locales/CLAUDE.md. | ||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - "src/frontend/src/lib/locales/**.po" | ||
| - "src/frontend/src/lib/locales/CLAUDE.md" | ||
| - "src/frontend/src/lib/locales/rules/**.md" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
| concurrency: | ||
| group: translations-check | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| precheck: | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| needs_run: ${{ steps.decide.outputs.needs_run }} | ||
| rule_diff_range: ${{ steps.decide.outputs.rule_diff_range }} | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Decide whether to invoke Claude | ||
| id: decide | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| EVENT_BEFORE: ${{ github.event.before }} | ||
| EVENT_AFTER: ${{ github.event.after }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| # Trigger 1: rule file(s) changed in the pushed commit range (new rule merged)? | ||
| # EVENT_BEFORE is the all-zero SHA on branch creation / force pushes | ||
| # from non-existent history; guard against that and verify both ends | ||
| # resolve before asking git to diff them. | ||
| rule_diff_range="" | ||
| zero_sha="0000000000000000000000000000000000000000" | ||
| if [ "$EVENT_NAME" = "push" ] \ | ||
| && [ -n "${EVENT_BEFORE:-}" ] && [ "$EVENT_BEFORE" != "$zero_sha" ] \ | ||
| && [ -n "${EVENT_AFTER:-}" ] && [ "$EVENT_AFTER" != "$zero_sha" ] \ | ||
| && git cat-file -e "$EVENT_BEFORE^{commit}" 2>/dev/null \ | ||
| && git cat-file -e "$EVENT_AFTER^{commit}" 2>/dev/null; then | ||
| rule_diff_range="$EVENT_BEFORE..$EVENT_AFTER" | ||
| if git diff --quiet "$EVENT_BEFORE" "$EVENT_AFTER" -- src/frontend/src/lib/locales/CLAUDE.md src/frontend/src/lib/locales/rules/; then | ||
| rules_changed=false | ||
| else | ||
| rules_changed=true | ||
| fi | ||
| else | ||
| rules_changed=false | ||
| fi | ||
|
|
||
| # Trigger 2: any .po file on main has empty msgstr and no open PR for it? | ||
| open_pr_branches=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --search "chore(fe): update" --json headRefName --jq '.[].headRefName' 2>/dev/null || true) | ||
|
|
||
| has_missing=false | ||
| for po in src/frontend/src/lib/locales/*.po; do | ||
| lang=$(basename "$po" .po) | ||
| [ "$lang" = "en" ] && continue | ||
| if echo "$open_pr_branches" | grep -qx "chore/translate-$lang"; then | ||
| continue | ||
| fi | ||
| # Skip the header entry (msgid "") then look for empty msgstr | ||
| if awk '/^msgid ""$/{skip=1;next}/^msgid /{skip=0}{if(!skip)print}' "$po" | grep -q '^msgstr ""$'; then | ||
| has_missing=true | ||
| break | ||
| fi | ||
| done | ||
|
|
||
| # Always run on workflow_dispatch | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
| needs_run=true | ||
| elif [ "$rules_changed" = true ] || [ "$has_missing" = true ]; then | ||
| needs_run=true | ||
| else | ||
| needs_run=false | ||
| fi | ||
|
|
||
| echo "rules_changed=$rules_changed" | ||
| echo "has_missing=$has_missing" | ||
| echo "needs_run=$needs_run" | ||
| echo "rule_diff_range=$rule_diff_range" | ||
| echo "needs_run=$needs_run" >> "$GITHUB_OUTPUT" | ||
| echo "rule_diff_range=$rule_diff_range" >> "$GITHUB_OUTPUT" | ||
|
|
||
| run-claude: | ||
| needs: precheck | ||
| if: needs.precheck.outputs.needs_run == 'true' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| token: ${{ secrets.GIX_BOT_PAT }} | ||
| fetch-depth: 0 | ||
|
|
||
| - uses: ./.github/actions/setup-node | ||
|
|
||
| - run: npm ci | ||
|
|
||
| - name: Configure git identity | ||
| run: | | ||
| git config --global user.name "gix-bot" | ||
| git config --global user.email "gix-bot@users.noreply.github.com" | ||
|
|
||
| - name: Install Claude Code CLI | ||
| run: npm install -g @anthropic-ai/claude-code | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking — unpinned All three workflows run Consider pinning to a specific version (
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pinned
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed — pin applied across all three files. Looks good. |
||
|
|
||
| - name: Snapshot branch refs before Claude runs | ||
| run: git for-each-ref --format='%(refname:short)=%(objectname)' refs/heads/ > "$RUNNER_TEMP/pre-refs.txt" | ||
|
|
||
| - name: Run Claude | ||
| env: | ||
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| GH_TOKEN: ${{ secrets.GIX_BOT_PAT }} | ||
| RULE_DIFF_RANGE: ${{ needs.precheck.outputs.rule_diff_range }} | ||
| run: | | ||
| prompt=$(envsubst '${RULE_DIFF_RANGE}' < src/frontend/src/lib/locales/prompts/check.md) | ||
| claude -p "$prompt" \ | ||
| --dangerously-skip-permissions \ | ||
| --allowedTools "Bash(git:*),Bash(gh:*),Bash(npm:*),Bash(awk:*),Bash(grep:*),Bash(sed:*),Bash(cat:*),Bash(basename:*),Bash(echo:*),Bash(jq:*),Bash(wc:*),Bash(head:*),Bash(tail:*),Bash(date:*),Bash(find:*),Bash(ls:*),Bash(mkdir:*),Bash(cd:*),Bash(envsubst:*),Bash(printf:*),Bash(sort:*),Bash(uniq:*),Bash(tr:*),Read,Write,Edit,Glob,Grep,Agent" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking — All three workflows grant The check and context-audit workflows have lower prompt-injection risk than feedback (their prompts are static/repo-controlled), but least-privilege still applies: if these workflows ever gain user-influenced inputs, the broad Per-workflow scoping (verify exact glob syntax):
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied in 0679e58 — same scoping across all three workflows:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed — the scoping change is applied consistently across all three workflow files. Blocking concern resolved. |
||
|
|
||
| - name: Audit Claude changes | ||
| if: always() | ||
| env: | ||
| ALLOWED_PATHS_REGEX: '^src/frontend/src/lib/locales/[a-z][a-z-]*\.po$' | ||
| run: | | ||
| set -euo pipefail | ||
| violations=() | ||
| while IFS='=' read -r ref sha; do | ||
| pre_sha=$(grep "^${ref}=" "$RUNNER_TEMP/pre-refs.txt" 2>/dev/null | cut -d= -f2 || true) | ||
| if [ -z "$pre_sha" ]; then | ||
| base="origin/main" | ||
| elif [ "$pre_sha" != "$sha" ]; then | ||
| base="$pre_sha" | ||
| else | ||
| continue | ||
| fi | ||
| files=$(git diff --name-only "$base..$sha" 2>/dev/null || true) | ||
| [ -z "$files" ] && continue | ||
| while IFS= read -r f; do | ||
| if ! echo "$f" | grep -qE "$ALLOWED_PATHS_REGEX"; then | ||
| violations+=("$ref: $f") | ||
| fi | ||
| done <<< "$files" | ||
| done < <(git for-each-ref --format='%(refname:short)=%(objectname)' refs/heads/) | ||
|
|
||
| if [ ${#violations[@]} -gt 0 ]; then | ||
| echo "::error::Claude modified files outside the allowed path set for this workflow:" | ||
| printf ' %s\n' "${violations[@]}" | ||
| exit 1 | ||
| fi | ||
| echo "Audit passed — all changed files are within the allowed path set." | ||
|
|
||
| - name: Notify Slack on failure | ||
| if: ${{ failure() }} | ||
| uses: ./.github/actions/slack | ||
| with: | ||
| WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | ||
| MESSAGE: "Translations check failed: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| name: Translations Context Audit | ||
|
|
||
| # Scans Svelte sources for translatable strings that are ambiguous without a | ||
| # translator-facing `context` annotation (e.g. short labels like "Cancel" used | ||
| # in several places with different meanings) and opens a single sweep PR that | ||
| # converts them to `$t({ message, context })` form. | ||
| # | ||
| # Runs weekly to pick up newly-added ambiguous strings. `workflow_dispatch` is | ||
| # also available for on-demand sweeps. Reviewer feedback on the resulting PR is | ||
| # handled by the translations-feedback workflow. | ||
|
|
||
| on: | ||
| schedule: | ||
| # Mondays at 10:00 UTC | ||
| - cron: "0 10 * * 1" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
| concurrency: | ||
| group: translations-context-audit | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| run-claude: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| token: ${{ secrets.GIX_BOT_PAT }} | ||
| fetch-depth: 0 | ||
|
|
||
| - uses: ./.github/actions/setup-node | ||
|
|
||
| - run: npm ci | ||
|
|
||
| - name: Configure git identity | ||
| run: | | ||
| git config --global user.name "gix-bot" | ||
| git config --global user.email "gix-bot@users.noreply.github.com" | ||
|
|
||
| - name: Install Claude Code CLI | ||
| run: npm install -g @anthropic-ai/claude-code | ||
|
|
||
| - name: Snapshot branch refs before Claude runs | ||
| run: git for-each-ref --format='%(refname:short)=%(objectname)' refs/heads/ > "$RUNNER_TEMP/pre-refs.txt" | ||
|
|
||
| - name: Run Claude | ||
| env: | ||
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| GH_TOKEN: ${{ secrets.GIX_BOT_PAT }} | ||
| run: | | ||
| claude -p "$(cat src/frontend/src/lib/locales/prompts/context-audit.md)" \ | ||
| --dangerously-skip-permissions \ | ||
| --allowedTools "Bash(git:*),Bash(gh:*),Bash(npm:*),Bash(awk:*),Bash(grep:*),Bash(sed:*),Bash(cat:*),Bash(basename:*),Bash(echo:*),Bash(jq:*),Bash(wc:*),Bash(head:*),Bash(tail:*),Bash(date:*),Bash(find:*),Bash(ls:*),Bash(mkdir:*),Bash(cd:*),Bash(envsubst:*),Bash(printf:*),Bash(sort:*),Bash(uniq:*),Bash(tr:*),Read,Write,Edit,Glob,Grep,Agent" | ||
|
|
||
| - name: Audit Claude changes | ||
| if: always() | ||
| env: | ||
| ALLOWED_PATHS_REGEX: '^src/frontend/src/lib/locales/[a-z][a-z-]*\.po$|^src/frontend/src/(lib|routes)/.*\.svelte$' | ||
| run: | | ||
| set -euo pipefail | ||
| violations=() | ||
| while IFS='=' read -r ref sha; do | ||
| pre_sha=$(grep "^${ref}=" "$RUNNER_TEMP/pre-refs.txt" 2>/dev/null | cut -d= -f2 || true) | ||
| if [ -z "$pre_sha" ]; then | ||
| base="origin/main" | ||
| elif [ "$pre_sha" != "$sha" ]; then | ||
| base="$pre_sha" | ||
| else | ||
| continue | ||
| fi | ||
| files=$(git diff --name-only "$base..$sha" 2>/dev/null || true) | ||
| [ -z "$files" ] && continue | ||
| while IFS= read -r f; do | ||
| if ! echo "$f" | grep -qE "$ALLOWED_PATHS_REGEX"; then | ||
| violations+=("$ref: $f") | ||
| fi | ||
| done <<< "$files" | ||
| done < <(git for-each-ref --format='%(refname:short)=%(objectname)' refs/heads/) | ||
|
|
||
| if [ ${#violations[@]} -gt 0 ]; then | ||
| echo "::error::Claude modified files outside the allowed path set for this workflow:" | ||
| printf ' %s\n' "${violations[@]}" | ||
| exit 1 | ||
| fi | ||
| echo "Audit passed — all changed files are within the allowed path set." | ||
|
|
||
| - name: Notify Slack on failure | ||
| if: ${{ failure() }} | ||
| uses: ./.github/actions/slack | ||
| with: | ||
| WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | ||
| MESSAGE: "Translations context audit failed: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of hard coding this, could we take members of the Identity team?