Repository navigation
Expand file tree
/
Copy pathaction.yml
More file actions
266 lines (246 loc) · 10.2 KB
/
Copy pathaction.yml
File metadata and controls
266 lines (246 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
name: "TextSetu"
description: "Sync translations with TextSetu — upload sources, download translations, and open a pull request with the result."
author: "TextSetu"
branding:
icon: "globe"
color: "purple"
# Modelled on crowdin/github-action, which is the shape teams in this space
# already know: declare WHAT you want with inputs rather than assembling CLI
# invocations, and let the action open the translations PR. That last part is why
# the action, not the CLI, is what most teams actually adopt — a pull request is
# reviewable, and a bot commit straight to main is not.
#
# Deliberately a composite action, not a Docker one: Docker actions cost a ~30s
# image pull on every run and this is a thin wrapper around an npm package.
inputs:
api_key:
description: "TextSetu API token. Use a PROJECT token (tsu_proj_…) from a repo secret."
required: true
api_url:
description: "API base URL. Only needed for a self-hosted deployment."
required: false
project:
description: "Project id, overriding `projectId` in textsetu.json."
required: false
config:
description: "Path to textsetu.json, if it is not at the repo root."
required: false
cli_version:
description: "Version of @textsetu/cli to run. Pin this for reproducible builds."
required: false
default: "latest"
# ---- What to do ----
upload_sources:
description: "Upload source strings to TextSetu."
required: false
default: "true"
upload_translations:
description: >-
Also upload local TRANSLATION files. Off by default because it is the
destructive direction: a stale local target overwrites work a reviewer
already approved.
required: false
default: "false"
download_translations:
description: "Download translations into the working tree."
required: false
default: "false"
download_status:
description: "Which translations to download: all | approved | pending_review | draft."
required: false
default: "approved"
locales:
description: "Space-separated locales to limit to, e.g. `fr de`."
required: false
branch:
description: "Target an open TextSetu translation branch instead of main."
required: false
dry_run:
description: "Print the resolved file plan and change nothing."
required: false
default: "false"
# ---- The CI gate ----
check_threshold:
description: >-
If set (0–100), run `status --check` at this threshold and FAIL the job when
any locale is below it. Leave empty to skip the gate.
required: false
# ---- What to do with downloaded files ----
create_pull_request:
description: "Commit downloaded translations to a branch and open a pull request."
required: false
default: "true"
localization_branch_name:
description: "Branch the translations PR is opened from."
required: false
default: "l10n_textsetu"
pull_request_title:
description: "Title of the translations PR."
required: false
default: "New TextSetu translations"
pull_request_body:
description: "Body of the translations PR."
required: false
default: "Pulled from TextSetu by the TextSetu GitHub Action."
pull_request_base_branch_name:
description: "Base branch for the PR. Defaults to the branch the workflow ran on."
required: false
pull_request_labels:
description: "Comma-separated labels to add to the PR."
required: false
commit_message:
description: "Commit message for the translations commit."
required: false
default: "chore(i18n): update translations from TextSetu"
github_token:
description: "Token used to push the branch and open the PR. Needs contents:write and pull-requests:write."
required: false
default: ${{ github.token }}
# ---- Escape hatch ----
command:
description: >-
Run an arbitrary CLI command instead of the steps above, e.g. `formats`.
When set, every other action input except api_key/api_url/config is ignored.
required: false
command_args:
description: "Extra arguments appended to `command`."
required: false
outputs:
changed:
description: "`true` if downloading translations modified any tracked file."
value: ${{ steps.diff.outputs.changed }}
pull_request_url:
description: "URL of the pull request that was created or updated, if any."
value: ${{ steps.pr.outputs.url }}
runs:
using: "composite"
steps:
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Resolve common flags
id: flags
shell: bash
run: |
FLAGS=""
[ -n "${{ inputs.api_url }}" ] && FLAGS="$FLAGS --api-url ${{ inputs.api_url }}"
[ -n "${{ inputs.project }}" ] && FLAGS="$FLAGS --project ${{ inputs.project }}"
[ -n "${{ inputs.config }}" ] && FLAGS="$FLAGS --config ${{ inputs.config }}"
echo "flags=$FLAGS" >> "$GITHUB_OUTPUT"
echo "cli=npx --yes @textsetu/cli@${{ inputs.cli_version }}" >> "$GITHUB_OUTPUT"
- name: Arbitrary command
if: inputs.command != ''
shell: bash
env:
TEXTSETU_API_KEY: ${{ inputs.api_key }}
run: |
${{ steps.flags.outputs.cli }} ${{ inputs.command }} \
${{ steps.flags.outputs.flags }} ${{ inputs.command_args }}
- name: Upload sources
if: inputs.command == '' && inputs.upload_sources == 'true'
shell: bash
env:
TEXTSETU_API_KEY: ${{ inputs.api_key }}
run: |
ARGS=""
[ "${{ inputs.upload_translations }}" = "true" ] && ARGS="$ARGS --translations"
[ "${{ inputs.dry_run }}" = "true" ] && ARGS="$ARGS --dry-run"
[ -n "${{ inputs.branch }}" ] && ARGS="$ARGS --branch ${{ inputs.branch }}"
[ -n "${{ inputs.locales }}" ] && ARGS="$ARGS --locale ${{ inputs.locales }}"
${{ steps.flags.outputs.cli }} push ${{ steps.flags.outputs.flags }} $ARGS
- name: Download translations
if: inputs.command == '' && inputs.download_translations == 'true'
shell: bash
env:
TEXTSETU_API_KEY: ${{ inputs.api_key }}
run: |
ARGS="--status ${{ inputs.download_status }}"
[ "${{ inputs.dry_run }}" = "true" ] && ARGS="$ARGS --dry-run"
[ -n "${{ inputs.branch }}" ] && ARGS="$ARGS --branch ${{ inputs.branch }}"
[ -n "${{ inputs.locales }}" ] && ARGS="$ARGS --locale ${{ inputs.locales }}"
# --force is unconditional here, and that is correct for CI: a runner's
# working tree is a fresh clone, so the CLI's lockfile conflict guard has
# no local edit to protect — it would only ever be comparing against
# whatever is committed. Exiting 5 on a committed translation edit would
# wedge the workflow permanently.
${{ steps.flags.outputs.cli }} pull ${{ steps.flags.outputs.flags }} $ARGS --force
- name: Detect changes
id: diff
if: inputs.command == '' && inputs.download_translations == 'true' && inputs.dry_run != 'true'
shell: bash
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
git status --short
else
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No translation changes."
fi
- name: Commit and open a pull request
id: pr
if: >-
inputs.command == '' &&
inputs.download_translations == 'true' &&
inputs.create_pull_request == 'true' &&
inputs.dry_run != 'true' &&
steps.diff.outputs.changed == 'true'
shell: bash
env:
GH_TOKEN: ${{ inputs.github_token }}
run: |
set -euo pipefail
BRANCH="${{ inputs.localization_branch_name }}"
BASE="${{ inputs.pull_request_base_branch_name }}"
[ -z "$BASE" ] && BASE="${GITHUB_REF_NAME}"
git config user.name "textsetu[bot]"
git config user.email "textsetu[bot]@users.noreply.github.com"
# Force-push a single-commit branch rather than accumulating history: the
# branch represents "the translations as they are now", and a growing
# chain of "update translations" commits makes the PR unreviewable.
git checkout -B "$BRANCH"
git add -A
git commit -m "${{ inputs.commit_message }}"
git push --force origin "$BRANCH"
LABELS=""
[ -n "${{ inputs.pull_request_labels }}" ] && LABELS="--label ${{ inputs.pull_request_labels }}"
# Reuse the open PR if there is one — a new PR per run would bury the
# reviewer, and the branch is force-pushed anyway.
URL=$(gh pr list --head "$BRANCH" --base "$BASE" --state open --json url --jq '.[0].url // empty')
if [ -z "$URL" ]; then
URL=$(gh pr create --head "$BRANCH" --base "$BASE" \
--title "${{ inputs.pull_request_title }}" \
--body "${{ inputs.pull_request_body }}" $LABELS)
else
echo "Updated the existing pull request."
fi
echo "url=$URL" >> "$GITHUB_OUTPUT"
echo "$URL"
# Last, so the gate reflects what was just synced. Its exit code 4 is what
# fails the job.
- name: Check translation completeness
if: inputs.command == '' && inputs.check_threshold != ''
shell: bash
env:
TEXTSETU_API_KEY: ${{ inputs.api_key }}
run: |
ARGS="--check --threshold ${{ inputs.check_threshold }}"
[ -n "${{ inputs.branch }}" ] && ARGS="$ARGS --branch ${{ inputs.branch }}"
[ -n "${{ inputs.locales }}" ] && ARGS="$ARGS --locale ${{ inputs.locales }}"
${{ steps.flags.outputs.cli }} status ${{ steps.flags.outputs.flags }} $ARGS
- name: Summary
if: always()
shell: bash
run: |
{
echo "### TextSetu"
echo ""
echo "| Step | Value |"
echo "| --- | --- |"
echo "| CLI | \`@textsetu/cli@${{ inputs.cli_version }}\` |"
echo "| Uploaded sources | ${{ inputs.upload_sources }} |"
echo "| Downloaded translations | ${{ inputs.download_translations }} (${{ inputs.download_status }}) |"
echo "| Changed files | ${{ steps.diff.outputs.changed || 'n/a' }} |"
echo "| Pull request | ${{ steps.pr.outputs.url || 'none' }} |"
echo "| Completeness gate | ${{ inputs.check_threshold || 'skipped' }} |"
} >> "$GITHUB_STEP_SUMMARY"