-
Notifications
You must be signed in to change notification settings - Fork 0
280 lines (242 loc) · 10.4 KB
/
Copy pathdata-contract.yml
File metadata and controls
280 lines (242 loc) · 10.4 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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
name: data-contract
# Guards the app <-> game-data contract. CI does not extract from the GGPK here
# (except the manual extract mode): the data comes from the app's own server, so
# the suite tests exactly the bytes production serves - or is about to serve.
#
# Three ways in:
# push to main - download the CURRENT release (what production serves
# right now) and run the Contract suite against it.
# dispatch w/ version - sent by the app's patch watcher for a freshly staged
# release: download that release, verify its checksum,
# run the suite, and on green call the activation
# endpoint so production swaps to it atomically.
# dispatch mode=extract - full GGPK extraction (the version input, or whatever
# GGG currently serves if omitted); for changes to the
# extractor itself. Never activates anything.
#
# Repo settings this needs:
# vars.DATA_BASE_URL - the app's base URL, e.g. https://exile2exile.example
# secrets.DATA_ACTIVATE_TOKEN - shared secret for POST /api/data/activate
on:
push:
branches:
- main
paths:
- 'app/**'
- 'config/**'
- 'database/**'
- 'routes/**'
- 'tests/Contract/**'
- 'tools/poe-data-extract/**'
- 'composer.json'
- 'composer.lock'
- 'phpunit.xml'
workflow_dispatch:
inputs:
version:
description: 'Staged release to validate and (on green) activate, e.g. 4.5.5.0'
required: false
default: ''
sha256:
description: 'Expected sha256 of the release tarball'
required: false
default: ''
mode:
description: 'validate = download from the app, extract = full GGPK extraction'
type: choice
options:
- validate
- extract
default: validate
permissions:
contents: read
# One run per staged version (a re-dispatch supersedes the last), one per ref
# for code pushes.
concurrency:
group: data-contract-${{ inputs.version || github.ref }}
cancel-in-progress: true
jobs:
validate:
if: github.event_name == 'push' || inputs.mode != 'extract'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2
with:
php-version: '8.4'
tools: composer:v2
coverage: none
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- name: Cache Composer packages
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Node Dependencies
run: npm ci
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
# The Contract suite renders real pages (Inertia views resolve the Vite
# manifest), so the assets must exist even though no JS test runs here.
- name: Build Assets
run: npm run build
# A dispatch names the staged release to validate; a code push validates
# against whatever production serves right now (its own version stamp).
- name: Resolve release version
id: release
env:
BASE_URL: ${{ vars.DATA_BASE_URL }}
INPUT_VERSION: ${{ inputs.version }}
INPUT_SHA256: ${{ inputs.sha256 }}
run: |
if [ -z "$BASE_URL" ]; then
echo "::error::vars.DATA_BASE_URL is not configured."
exit 1
fi
if [ -n "$INPUT_VERSION" ]; then
VERSION="$INPUT_VERSION"
CACHE_SUFFIX="$INPUT_SHA256"
else
STAMP=$(curl -fsSL --retry 3 "$BASE_URL/tree/current/version.json")
VERSION=$(echo "$STAMP" | jq -r '.patch // empty')
# Key the tarball cache on the tarball's own checksum: the version.json
# `v` stamp only moves with the tree data, so a re-extraction of the same
# patch (an extractor change) would otherwise keep serving a stale cache.
CACHE_SUFFIX=$(curl -fsSL --retry 3 "$BASE_URL/api/data/releases/$VERSION.tar.gz.sha256" \
|| echo "$STAMP" | jq -r '.v // empty')
fi
if [ -z "$VERSION" ]; then
echo "::error::Could not resolve a release version (no input, no live stamp)."
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "cache-key=game-data-$VERSION-$CACHE_SUFFIX" >> "$GITHUB_OUTPUT"
echo "Validating release $VERSION"
# Repeat runs against the same release hit the Actions cache instead of
# re-downloading the tarball from the app.
- name: Cache release tarball
id: tarball-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: data.tar.gz
key: ${{ steps.release.outputs.cache-key }}
- name: Download release tarball
if: steps.tarball-cache.outputs.cache-hit != 'true'
env:
BASE_URL: ${{ vars.DATA_BASE_URL }}
run: curl -fsSL --retry 3 -o data.tar.gz "$BASE_URL/api/data/releases/${{ steps.release.outputs.version }}.tar.gz"
# Proves CI got exactly the artifact the watcher staged - even if another
# patch landed in between.
- name: Verify tarball checksum
if: inputs.sha256 != ''
run: echo "${{ inputs.sha256 }} data.tar.gz" | sha256sum -c
- name: Unpack release into the checkout
run: tar -xzf data.tar.gz
- name: Copy Environment File
run: cp .env.example .env
- name: Generate Application Key
run: php artisan key:generate
# Some Contract tests (e.g. the unique-mod GGPK-affix-fallback test) read the
# real PoB-uniques cache, not a fixture - storage/game-data is gitignored and
# production keeps this synced via its own cron, so CI needs its own copy.
# Public repo, works unauthenticated (lower GitHub API rate limit only).
- name: Sync PoB uniques
run: php artisan poe2:sync-pob-uniques
- name: Contract tests (real data)
run: php artisan test --parallel --testsuite=Contract
# The swap on the server is atomic and idempotent; production never serves
# a release that did not pass the suite above.
- name: Activate the validated release
if: inputs.version != ''
env:
BASE_URL: ${{ vars.DATA_BASE_URL }}
run: |
curl -fsS --retry 3 -X POST "$BASE_URL/api/data/activate" \
-H "Authorization: Bearer ${{ secrets.DATA_ACTIVATE_TOKEN }}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"version":"${{ steps.release.outputs.version }}"}'
# Manual-only: exercises the extractor pipeline end to end against the GGPK
# patch CDN. Use it for pull requests that change tools/poe-data-extract. It
# never activates anything on the server.
extract:
if: github.event_name == 'workflow_dispatch' && inputs.mode == 'extract'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2.37.2
with:
php-version: '8.4'
tools: composer:v2
coverage: none
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: 'npm'
- name: Install Node Dependencies
run: npm ci
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Copy Environment File
run: cp .env.example .env
- name: Generate Application Key
run: php artisan key:generate
# No committed pin anywhere (tools/poe-data-extract/config.json is
# gitignored, regenerated fresh on every run - see resolvePatch.mjs): an
# explicit version input wins, otherwise ask GGG's own patch server live via
# the same command the extractor itself falls back to.
- name: Resolve the patch to extract
id: datapatch
env:
INPUT_VERSION: ${{ inputs.version }}
run: |
if [ -n "$INPUT_VERSION" ]; then
echo "patch=$INPUT_VERSION" >> "$GITHUB_OUTPUT"
else
patch=$(php artisan poe2:current-patch)
echo "patch=$patch" >> "$GITHUB_OUTPUT"
fi
- name: Cache GGPK bundles
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: tools/poe-data-extract/.cache
key: ggpk-${{ steps.datapatch.outputs.patch }}
- name: Install extractor dependencies
working-directory: tools/poe-data-extract
run: npm install
# Full extract (icons + webp): the Contract suite checks that referenced
# icons exist on disk, so JSON-only is not enough here.
- name: Extract GGPK data
env:
PATCH: ${{ steps.datapatch.outputs.patch }}
run: npm run refresh:data
# Same as the validate job: page-rendering Contract tests need the manifest.
- name: Build Assets
run: npm run build
# Some Contract tests (e.g. the unique-mod GGPK-affix-fallback test) read the
# real PoB-uniques cache, not a fixture - storage/game-data is gitignored and
# production keeps this synced via its own cron, so CI needs its own copy.
# Public repo, works unauthenticated (lower GitHub API rate limit only).
- name: Sync PoB uniques
run: php artisan poe2:sync-pob-uniques
- name: Contract tests (real data)
run: php artisan test --parallel --testsuite=Contract