Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pr-review-requested.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ jobs:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Send Slack notification on PR review requested
if: github.event.requested_reviewer.login == 'danblackadder' || github.event.requested_reviewer.login == 'aterga' || github.event.requested_reviewer.login == 'sea-snake' || github.event.requested_reviewer.login == 'lmuntaner'
if: github.event.requested_reviewer.login == 'aterga' || github.event.requested_reviewer.login == 'sea-snake'
Copy link
Copy Markdown
Collaborator

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?

uses: ./.github/actions/slack
with:
WEBHOOK_URL: ${{ secrets.SLACK_PRIVATE_IDENTITY_WEBHOOK_URL }}
MESSAGE: |
<@${{ github.event.requested_reviewer.login == 'danblackadder' && 'U07FDBKDHEH' || github.event.requested_reviewer.login == 'aterga' && 'U02EAPEDT3J' || github.event.requested_reviewer.login == 'sea-snake' && 'U07QU79GX0T' || github.event.requested_reviewer.login == 'lmuntaner' && 'U02TEQHKV35' }}>: New PR ready for review: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>.
<@${{ github.event.requested_reviewer.login == 'aterga' && 'U02EAPEDT3J' || github.event.requested_reviewer.login == 'sea-snake' && 'U07QU79GX0T' }}>: New PR ready for review: <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}>.
130 changes: 130 additions & 0 deletions .github/workflows/translations-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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)?
rule_diff_range=""
if [ "$EVENT_NAME" = "push" ] && [ -n "${EVENT_BEFORE:-}" ] && [ -n "${EVENT_AFTER:-}" ]; 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
Comment thread
sea-snake marked this conversation as resolved.
Outdated
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking — unpinned @anthropic-ai/claude-code install

All three workflows run npm install -g @anthropic-ai/claude-code without a version pin. A supply-chain compromise of this package would grant arbitrary code execution in CI with access to GIX_BOT_PAT and ANTHROPIC_API_KEY.

Consider pinning to a specific version (npm install -g @anthropic-ai/claude-code@1.x.y) and updating deliberately via Dependabot or Renovate. Same applies to translations-context-audit.yml line 48 and translations-feedback.yml line 153.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pinned @anthropic-ai/claude-code@2.1.116 across all three workflows in 0679e58. Will bump deliberately when upgrading.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — pin applied across all three files. Looks good.


- 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,Read,Write,Edit,Glob,Grep,Agent"

- 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 }}"
64 changes: 64 additions & 0 deletions .github/workflows/translations-context-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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: 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,Read,Write,Edit,Glob,Grep,Agent"

- 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 }}"
151 changes: 151 additions & 0 deletions .github/workflows/translations-feedback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Translations Feedback

# Applies reviewer feedback on translation PRs and (for broad feedback)
# opens rule-proposal PRs that, once merged, trigger Translations Check.
on:
pull_request_review:
types: [submitted]
pull_request_review_comment:
Comment thread
sea-snake marked this conversation as resolved.
types: [created]
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: read

concurrency:
group: translations-feedback-pr-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: false

jobs:
gate:
runs-on: ubuntu-latest
outputs:
run_claude: ${{ steps.gate.outputs.run_claude }}
pr_number: ${{ steps.gate.outputs.pr_number }}
pr_head_ref: ${{ steps.gate.outputs.pr_head_ref }}
pr_head_repo: ${{ steps.gate.outputs.pr_head_repo }}
steps:
- name: Gate
id: gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
COMMENT_USER: ${{ github.event.comment.user.login || github.event.review.user.login }}
ISSUE_PR_URL: ${{ github.event.issue.pull_request.url }}
PR_NUMBER_DIRECT: ${{ github.event.pull_request.number }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
set -euo pipefail

# issue_comment fires for both issues and PRs — require pull_request presence.
if [ "$EVENT_NAME" = "issue_comment" ] && [ -z "${ISSUE_PR_URL:-}" ]; then
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "Not a PR comment — skipping"
exit 0
fi

# Allowlist (same as the previous launchd scripts).
case "$COMMENT_USER" in
AntonioVentilii|aterga|sea-snake|marc0olo|mducroux|copilot-pull-request-reviewer|copilot-pull-request-reviewer\[bot\]|Copilot) ;;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking — Copilot in the commenter allowlist creates an indirect injection path

Including copilot-pull-request-reviewer[bot] and Copilot in the allowlist means Copilot's automated reviews can trigger this workflow. Copilot's review text is influenced by PR content (code, comments, strings in the diff). An attacker with push access who puts adversarial content on a chore/translate-* branch could produce a Copilot review that carries injection text into Claude.

The fork-rejection gate and PR title filter narrow this considerably — the attacker needs direct push access to the repo. If Bash(gh:*) is scoped down (see blocking comment above), the blast radius becomes manageable.

Worth noting as a defense-in-depth consideration. One option: exclude Copilot from this allowlist and rely on human reviewers to relay feedback that Claude should act on.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed this is a real defense-in-depth consideration. Keeping Copilot in the allowlist for now because it's a material part of the review loop (Copilot catches things humans miss and translation PRs historically surface issues that way). The risk is bounded by: (1) push access required — external contributors can't seed injection content, (2) the gh scoping just landed in 0679e58, (3) the post-run path audit catches any unexpected file changes. Happy to revisit — if the team decides Copilot should be relegated to "reply-only", that's a one-line change to the allowlist plus a Step 0 case in feedback.md to acknowledge without acting. Let me know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rationale is sound. With the gh scoping in place and the PAT being repo-scoped, keeping Copilot in the allowlist is a reasonable trade-off — the residual risk is bounded and the review value is real. Acknowledged, no further action needed.

*)
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "Commenter '$COMMENT_USER' not in allowlist — skipping"
exit 0
;;
esac

# Avoid self-trigger loops from gix-bot's own replies.
if [ "$COMMENT_USER" = "gix-bot" ]; then
Comment thread
sea-snake marked this conversation as resolved.
Outdated
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "Self-reply — skipping"
exit 0
fi

pr_number="${PR_NUMBER_DIRECT:-$ISSUE_NUMBER}"
if [ -z "$pr_number" ]; then
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "No PR number resolvable — skipping"
exit 0
fi

pr_json=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$pr_number")
pr_title=$(echo "$pr_json" | jq -r .title)
pr_head_ref=$(echo "$pr_json" | jq -r .head.ref)
pr_head_repo=$(echo "$pr_json" | jq -r .head.repo.full_name)
pr_state=$(echo "$pr_json" | jq -r .state)
pr_merged=$(echo "$pr_json" | jq -r .merged)

Comment thread
sea-snake marked this conversation as resolved.
# Only run on translation PRs.
if ! echo "$pr_title" | grep -q '^chore(fe):'; then
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "PR title '$pr_title' is not a translation PR — skipping"
exit 0
fi

# For closed PRs, only consider those merged within the last 14 days
# (matching the previous launchd behaviour).
if [ "$pr_state" = "closed" ] && [ "$pr_merged" = "true" ]; then
merged_at=$(echo "$pr_json" | jq -r .merged_at)
cutoff=$(date -u -d '14 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-14d +%Y-%m-%dT%H:%M:%SZ)
if [ "$merged_at" \< "$cutoff" ]; then
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "PR merged before $cutoff — skipping"
exit 0
fi
elif [ "$pr_state" = "closed" ]; then
echo "run_claude=false" >> "$GITHUB_OUTPUT"
echo "PR is closed without merge — skipping"
exit 0
fi

echo "run_claude=true" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
echo "pr_head_ref=$pr_head_ref" >> "$GITHUB_OUTPUT"
echo "pr_head_repo=$pr_head_repo" >> "$GITHUB_OUTPUT"

run-claude:
needs: gate
if: needs.gate.outputs.run_claude == '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

- name: Run Claude
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GH_TOKEN: ${{ secrets.GIX_BOT_PAT }}
PR_NUMBER: ${{ needs.gate.outputs.pr_number }}
PR_HEAD_REF: ${{ needs.gate.outputs.pr_head_ref }}
PR_HEAD_REPO: ${{ needs.gate.outputs.pr_head_repo }}
run: |
prompt=$(envsubst '${PR_NUMBER} ${PR_HEAD_REF} ${PR_HEAD_REPO}' < src/frontend/src/lib/locales/prompts/feedback.md)
claude -p "$prompt" \
--dangerously-skip-permissions \
--allowedTools "Bash,Read,Write,Edit,Glob,Grep,Agent"

- name: Notify Slack on failure
if: ${{ failure() }}
uses: ./.github/actions/slack
with:
WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
MESSAGE: "Translations feedback failed on PR #${{ needs.gate.outputs.pr_number }}: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
13 changes: 13 additions & 0 deletions src/frontend/src/lib/locales/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Translation rules

This file is the index. Individual rule sets live in [`rules/`](./rules/) and are imported below. Add a new rule by appending to the relevant `rules/<topic>.md` file — or creating a new one and adding an `@rules/...` line here.

Rules are added via "rule proposal" pull requests (title prefix `chore(fe): propose translation rule`). Merging such a PR signals approval and triggers a sweep of existing translations against the new rule.

## General (applies to all languages)

@rules/general.md

## Language-specific

@rules/russian.md
Loading
Loading