-
Notifications
You must be signed in to change notification settings - Fork 10
201 lines (179 loc) · 8.15 KB
/
Copy pathupload-inflection.yml
File metadata and controls
201 lines (179 loc) · 8.15 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
name: Upload Inflection Data to Cloudflare R2
# Publishes the inflection data to Cloudflare R2. On push to the
# `inflection` branch it reads the data version from
# priv/localize/localize_inflection_sha (plus the pipeline revision)
# and uploads that version when it is not already on R2 — i.e. the
# version changed or was never uploaded. A push that leaves the
# version unchanged finds it already on R2 and short-circuits to a
# cheap no-op (no filter, so the never-uploaded case fires on any
# push). Also reusable from ci.yml (post-merge on main, ahead of the
# test matrix) and manually dispatchable for recovery.
on:
push:
branches: [inflection]
workflow_call:
workflow_dispatch:
env:
MIX_ENV: test
# Toolchain pins kept in step with the developer toolchain
# (`mise current`) and with upload-locales.yml. ETF files are
# serialized with the term_to_binary `:deterministic` option, so
# the generated bytes are reproducible across OTP versions; the
# pre-upload manifest check is the backstop if that ever changes.
ELIXIR_VERSION: "1.20.2"
OTP_VERSION: "29.0.3"
R2_BUCKET: content
R2_PREFIX: inflection
# The conformance suite loads every supported locale's lexicon into
# :persistent_term (BEAM literal area), so bump the literal super
# carrier above its default. See plans/MF2_NAMESPACE_INFLECTION.md.
ELIXIR_ERL_OPTIONS: "+MIscs 3072"
permissions:
contents: read
jobs:
build-and-upload:
name: Build and upload inflection data
runs-on: ubuntu-latest
# Serialize publish runs so overlapping pushes cannot upload
# concurrently; the queued run sees the version already on R2
# and skips.
concurrency:
group: inflection-publish
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v4
# The pin lives in priv/localize/localize_inflection_sha and the
# pipeline revision in Localize.Inflection.Provider — both read
# without an Elixir compile for the cheap short-circuit check.
- name: Read data version
id: version
run: |
sha=$(tr -d '[:space:]' < priv/localize/localize_inflection_sha)
revision=$(grep -o '@data_revision [0-9]*' lib/localize/inflection/provider.ex | grep -o '[0-9]*')
data_version="${sha:0:12}-r${revision}"
echo "data_version=${data_version}" >> "$GITHUB_OUTPUT"
echo "upstream_sha=${sha}" >> "$GITHUB_OUTPUT"
echo "Data version: ${data_version}"
- name: Install rclone
run: |
curl -fsSL https://rclone.org/install.sh | sudo bash
- name: Configure rclone for Cloudflare R2
env:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
run: |
mkdir -p ~/.config/rclone
cat > ~/.config/rclone/rclone.conf << RCLONE
[r2]
type = s3
provider = Cloudflare
access_key_id = ${R2_ACCESS_KEY_ID}
secret_access_key = ${R2_SECRET_ACCESS_KEY}
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
acl = private
no_check_bucket = true
RCLONE
- name: Check if data version already exists on R2
id: check_existing
env:
DATA_VERSION: ${{ steps.version.outputs.data_version }}
run: |
existing=$(rclone lsf "r2:${R2_BUCKET}/${R2_PREFIX}/${DATA_VERSION}/" --include "*.etf" 2>/dev/null | wc -l | tr -d '[:space:]')
if [ "${existing}" -gt 0 ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "::notice::Inflection data ${DATA_VERSION} already exists on R2 with ${existing} file(s); skipping."
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Set up Elixir
if: steps.check_existing.outputs.skip != 'true'
uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: Cache deps and build
if: steps.check_existing.outputs.skip != 'true'
uses: actions/cache@v5
with:
path: |
deps
_build
key: ${{ runner.os }}-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-mix-inflection-upload-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-otp${{ env.OTP_VERSION }}-elixir${{ env.ELIXIR_VERSION }}-mix-inflection-upload-
- name: Cache upstream inflection sources
if: steps.check_existing.outputs.skip != 'true'
uses: actions/cache@v5
with:
path: data/inflection
key: inflection-source-${{ steps.version.outputs.upstream_sha }}-${{ hashFiles('data/mix/tasks/localize.inflection.download.ex') }}
- name: Cache generated artifacts
if: steps.check_existing.outputs.skip != 'true'
uses: actions/cache@v5
id: artifact-cache
with:
path: priv/localize/inflection
key: inflection-artifacts-${{ steps.version.outputs.upstream_sha }}-${{ hashFiles('data/inflection_gen/**', 'data/mix/tasks/localize.inflection.*.ex') }}
- name: Install dependencies
if: steps.check_existing.outputs.skip != 'true'
run: mix deps.get
- name: Compile
if: steps.check_existing.outputs.skip != 'true'
run: mix compile --warnings-as-errors
- name: Download and generate inflection data
if: steps.check_existing.outputs.skip != 'true' && steps.artifact-cache.outputs.cache-hit != 'true'
run: |
mix localize.inflection.download
mix localize.inflection.generate
- name: Run inflection conformance suites
if: steps.check_existing.outputs.skip != 'true'
run: mix test test/localize/inflection/
# The committed manifest pins the exact bytes downloads are
# verified against. Generation is deterministic, so the bytes
# built here must match it; if they do not, uploading would make
# every download fail integrity verification — fail before
# touching R2 instead. Read the committed manifest from git
# because generation rewrites it in the working tree.
- name: Verify generated data matches the committed hash manifest
if: steps.check_existing.outputs.skip != 'true'
run: |
git show HEAD:priv/localize/inflection_hashes.etf > /tmp/committed_inflection_hashes.etf
mix run --no-start -e '
hashes = File.read!("/tmp/committed_inflection_hashes.etf") |> :erlang.binary_to_term()
mismatches =
Enum.filter(hashes, fn {file, expected} ->
path = "priv/localize/inflection/#{file}"
not File.exists?(path) or :crypto.hash(:sha256, File.read!(path)) != expected
end)
if mismatches == [] do
IO.puts("All #{map_size(hashes)} generated files match the committed manifest.")
else
IO.puts("::error::Generated inflection data does not match the committed inflection_hashes.etf: #{inspect(Enum.map(mismatches, &elem(&1, 0)))}")
IO.puts("Regenerate on the mise-current toolchain and commit the refreshed manifest.")
System.halt(1)
end
'
- name: Upload to R2
if: steps.check_existing.outputs.skip != 'true'
env:
DATA_VERSION: ${{ steps.version.outputs.data_version }}
run: |
echo "Uploading to r2:${R2_BUCKET}/${R2_PREFIX}/${DATA_VERSION}/"
rclone sync \
priv/localize/inflection/ \
"r2:${R2_BUCKET}/${R2_PREFIX}/${DATA_VERSION}/" \
--include "*.etf" \
--transfers 16 \
--s3-upload-concurrency 2 \
--stats 10s \
--stats-one-line \
--verbose
- name: Skipped summary
if: steps.check_existing.outputs.skip == 'true'
env:
DATA_VERSION: ${{ steps.version.outputs.data_version }}
run: |
echo "Skipped: inflection data ${DATA_VERSION} already present."
echo "Bump the upstream pin or data revision in Localize.Inflection.Provider and push a new tag to trigger a real upload."