-
-
Notifications
You must be signed in to change notification settings - Fork 2
427 lines (394 loc) · 17.6 KB
/
release.yml
File metadata and controls
427 lines (394 loc) · 17.6 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
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
name: Release
# Fires on a version tag push (e.g. `v0.3.2`). Gates on the Tests workflow
# passing for the exact same commit, then creates/updates the GitHub Release
# and publishes the package to npm as @bloomengine/engine.
#
# The /release Claude Code skill in .claude/skills/release/ drives this end
# to end: it bumps the version in package.json, commits, tags, pushes, and
# waits for this workflow to go green.
#
# Authentication: npm trusted publishing. Both packages — @bloomengine/engine
# (job: publish-npm) and @bloomengine/jolt-prebuilt (job: publish-jolt-prebuilt)
# — must be configured on npmjs.com with this workflow as a trusted publisher.
# `id-token: write` on each publish job is enough — `npm publish` exchanges
# the GitHub OIDC token for a short-lived publish credential, no NPM_TOKEN
# secret needed. Provenance attestation is automatic under this flow.
#
# Job order:
# await-tests — gate on Tests workflow passing for the tag SHA
# github-release — create/keep the GH Release object
# build-jolt-prebuilt — matrix: build libJolt + libbloom_jolt for every
# (os, arch) variant on its native runner
# publish-jolt-prebuilt — assemble the artifacts into npm/jolt-prebuilt/lib/
# and publish @bloomengine/jolt-prebuilt
# publish-npm — publish @bloomengine/engine, which depends on the
# jolt-prebuilt version just published
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: "Existing tag to (re-)publish a release for (e.g. v0.3.2)"
required: true
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false # never cancel a release mid-flight
permissions:
contents: write
jobs:
# ---------------------------------------------------------------------------
# Gate: wait for `Tests` to pass on this commit before we create the GitHub
# Release. Same pattern as Perry's release-packages.yml — query the API by
# workflow filename + head_sha and poll until it completes. Fails loud if
# Tests failed so we don't ship a known-broken tag.
# ---------------------------------------------------------------------------
await-tests:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Wait for Tests workflow to pass
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
EVENT: ${{ github.event_name }}
run: |
set -euo pipefail
if [ "$EVENT" = "workflow_dispatch" ]; then
echo "workflow_dispatch — bypassing test gate (manual republish)."
exit 0
fi
echo "Gating release on commit $SHA"
deadline=$(( SECONDS + 45 * 60 ))
retry_on_error=1
while :; do
set +e
api_out=$(gh api \
"/repos/$REPO/actions/workflows/test.yml/runs?head_sha=$SHA&per_page=1" \
2>&1)
rc=$?
set -e
if [ $rc -ne 0 ]; then
echo " gh api failed (rc=$rc):"
echo "$api_out" | awk '{print " " $0}'
if [ $retry_on_error -gt 0 ]; then
retry_on_error=0
echo " retrying once after 10s"
sleep 10
continue
fi
echo " gh api still failing after retry — failing gate."
exit 1
fi
retry_on_error=1
status=$(echo "$api_out" | jq -r '.workflow_runs[0].status // empty')
conclusion=$(echo "$api_out" | jq -r '.workflow_runs[0].conclusion // empty')
url=$(echo "$api_out" | jq -r '.workflow_runs[0].html_url // empty')
if [ -z "$status" ]; then
echo " no Tests run found yet for $SHA — waiting"
elif [ "$status" = "completed" ]; then
if [ "$conclusion" = "success" ]; then
echo " OK Tests passed: $url"
break
else
echo " FAIL Tests $conclusion: $url"
exit 1
fi
else
echo " Tests: $status ($url) — waiting"
fi
if [ $SECONDS -ge $deadline ]; then
echo " timed out waiting for Tests"
exit 1
fi
sleep 30
done
echo "Gate passed — proceeding to publish."
# ---------------------------------------------------------------------------
# Create (or update) the GitHub Release for this tag. The /release skill
# usually creates the release *body* from the CLI after pushing the tag;
# this job is a safety net: if the tag exists but no release does yet, it
# creates one with auto-generated notes.
# ---------------------------------------------------------------------------
github-release:
needs: await-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve tag
id: tag
env:
DISPATCH_TAG: ${{ github.event.inputs.tag }}
run: |
if [ -n "$DISPATCH_TAG" ]; then
TAG="$DISPATCH_TAG"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release (if missing)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
if gh release view "$TAG" >/dev/null 2>&1; then
echo "Release $TAG already exists — leaving body alone."
exit 0
fi
echo "Creating release $TAG with auto-generated notes."
gh release create "$TAG" \
--title "$TAG" \
--generate-notes
- name: Verify package.json version matches tag
env:
VERSION: ${{ steps.tag.outputs.version }}
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "WARNING package.json version ($PKG_VERSION) does not match tag ($VERSION)."
echo " This is normally a bug — the /release skill should bump package.json"
echo " and commit before tagging. Continuing, but investigate."
else
echo "OK package.json version matches tag ($VERSION)"
fi
# ---------------------------------------------------------------------------
# Build the JoltPhysics + bloom_jolt static libraries for every (os, arch)
# combination Bloom supports. Each matrix entry runs on its native runner
# (Apple cross-compiles on macos-14, Android NDK on Linux, etc.) and uploads
# its archives as a per-target artifact for the publish step below to
# assemble. fail-fast: false so a broken slice (typically a platform-specific
# cmake quirk) doesn't cancel the other 16 builds — the publish job won't
# run anyway, but the failing job's logs land alongside the green ones for
# easier triage.
#
# The 17 variants here mirror the directory layout documented in
# npm/jolt-prebuilt/README.md. Adding a new target = new matrix entry +
# README update + corresponding arch mapping in native/shared/build.rs.
# ---------------------------------------------------------------------------
build-jolt-prebuilt:
needs: await-tests
strategy:
fail-fast: false
matrix:
include:
# Apple — built on macos-14 (Apple Silicon). Apple cross-compiles
# use the Xcode generator because the make/ninja generators don't
# reliably pick up iOS/tvOS/watchOS SDK toolchain quirks.
- target: macos-arm64
runner: macos-14
cmake_flags: ""
- target: macos-x64
runner: macos-14
cmake_flags: "-DCMAKE_OSX_ARCHITECTURES=x86_64"
- target: ios-arm64
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=iphoneos"
- target: ios-arm64-sim
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=iphonesimulator"
- target: ios-x64-sim
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_SYSROOT=iphonesimulator"
- target: tvos-arm64
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=appletvos"
- target: tvos-arm64-sim
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=appletvsimulator"
- target: tvos-x64-sim
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_SYSROOT=appletvsimulator"
- target: watchos-arm64
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=watchOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=watchos"
- target: watchos-arm64-sim
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=watchOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_SYSROOT=watchsimulator"
- target: watchos-x64-sim
runner: macos-14
cmake_flags: "-G Xcode -DCMAKE_SYSTEM_NAME=watchOS -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_SYSROOT=watchsimulator"
# Linux — native runners (no cross-compile). GitHub provides arm64
# Linux runners as of late 2025; if the arm runner becomes flaky,
# fall back to a cross-compile from ubuntu-22.04 with
# `aarch64-linux-gnu-gcc`.
- target: linux-x64
runner: ubuntu-22.04
cmake_flags: ""
- target: linux-arm64
runner: ubuntu-22.04-arm
cmake_flags: ""
# Windows — MSVC via the default Visual Studio generator.
- target: win32-x64
runner: windows-latest
cmake_flags: "-A x64"
# Android — cross-compiled from Linux via the NDK's cmake
# toolchain. ANDROID_NDK_HOME is set by the github-hosted ubuntu
# image (currently NDK 26+; check if a future release pins a
# specific NDK version).
- target: android-arm64
runner: ubuntu-22.04
cmake_flags: "-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-21"
- target: android-armv7
runner: ubuntu-22.04
cmake_flags: "-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_ABI=armeabi-v7a -DANDROID_PLATFORM=android-21"
- target: android-x64
runner: ubuntu-22.04
cmake_flags: "-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake -DANDROID_ABI=x86_64 -DANDROID_PLATFORM=android-21"
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure cmake
shell: bash
run: |
cd native/third_party/bloom_jolt
mkdir -p build
cd build
cmake .. -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flags }}
- name: Build
shell: bash
run: cmake --build native/third_party/bloom_jolt/build --config Release --parallel
- name: Stage prebuilt libs
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts/${{ matrix.target }}
# Generator differences (Xcode → Release/, Make/Ninja → ./ or lib/,
# MSBuild → Release/x64/) mean the libs aren't at one fixed path.
# Find them by name instead.
find native/third_party/bloom_jolt/build -type f \
\( -name 'libJolt.a' -o -name 'Jolt.lib' \
-o -name 'libbloom_jolt.a' -o -name 'bloom_jolt.lib' \) \
-exec cp {} artifacts/${{ matrix.target }}/ \;
ls -la artifacts/${{ matrix.target }}/
# Sanity: both archives must be present, else the upload below
# silently ships a half-broken slice that bites consumers later.
if [ "$(ls artifacts/${{ matrix.target }}/ | wc -l)" -lt 2 ]; then
echo "::error::expected libJolt + libbloom_jolt for ${{ matrix.target }}, got:"
ls -la artifacts/${{ matrix.target }}/
exit 1
fi
- uses: actions/upload-artifact@v4
with:
name: jolt-prebuilt-${{ matrix.target }}
path: artifacts/${{ matrix.target }}/
if-no-files-found: error
retention-days: 7
# ---------------------------------------------------------------------------
# Assemble all per-target artifacts into npm/jolt-prebuilt/lib/<target>/ and
# publish @bloomengine/jolt-prebuilt via OIDC trusted publishing. Idempotent:
# the "already published" check short-circuits re-runs (workflow_dispatch on
# an old tag won't republish the same version, won't fail either).
# ---------------------------------------------------------------------------
publish-jolt-prebuilt:
needs: build-jolt-prebuilt
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v5
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Download all jolt-prebuilt artifacts
uses: actions/download-artifact@v4
with:
path: npm/jolt-prebuilt/lib
pattern: jolt-prebuilt-*
- name: Strip the jolt-prebuilt- artifact-name prefix from each subdir
shell: bash
run: |
set -euo pipefail
cd npm/jolt-prebuilt/lib
for d in jolt-prebuilt-*; do
mv "$d" "${d#jolt-prebuilt-}"
done
ls -la
- name: Check if version already published
id: check
run: |
PKG_NAME=$(node -p "require('./npm/jolt-prebuilt/package.json').name")
PKG_VERSION=$(node -p "require('./npm/jolt-prebuilt/package.json').version")
if npm view "$PKG_NAME@$PKG_VERSION" version >/dev/null 2>&1; then
echo "$PKG_NAME@$PKG_VERSION is already on the registry — skipping publish."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "$PKG_NAME@$PKG_VERSION not yet published — will publish."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.check.outputs.skip == 'false'
working-directory: npm/jolt-prebuilt
run: npm publish --provenance --access public
# ---------------------------------------------------------------------------
# Publish the package to npm as @bloomengine/engine. Runs after the GitHub
# Release so a failure here doesn't leave a dangling release-but-no-package
# state. Also waits on publish-jolt-prebuilt — the engine declares
# @bloomengine/jolt-prebuilt as a dependency, so the prebuilt package must
# be on the registry first or `npm publish` of the engine will fail
# resolution. Skips cleanly if the version is already on the registry,
# which keeps re-runs idempotent (workflow_dispatch on an existing tag
# won't double-publish or fail).
#
# We check out submodules recursively because scripts/prepack.sh refuses
# to ship a tarball without the JoltPhysics sources materialised — the
# package vendors them rather than relying on a postinstall git clone.
# ---------------------------------------------------------------------------
publish-npm:
needs: [github-release, publish-jolt-prebuilt]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for npm provenance attestations
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: actions/setup-node@v5
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Resolve tag
id: tag
env:
DISPATCH_TAG: ${{ github.event.inputs.tag }}
run: |
if [ -n "$DISPATCH_TAG" ]; then
TAG="$DISPATCH_TAG"
else
TAG="${GITHUB_REF#refs/tags/}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
- name: Verify package.json version matches tag
env:
VERSION: ${{ steps.tag.outputs.version }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
if [ "$PKG_VERSION" != "$VERSION" ]; then
echo "::error::Tag $TAG ($VERSION) does not match package.json ($PKG_VERSION) — refusing to publish."
exit 1
fi
- name: Check if version already published
id: check
run: |
PKG_NAME=$(node -p "require('./package.json').name")
PKG_VERSION=$(node -p "require('./package.json').version")
if npm view "$PKG_NAME@$PKG_VERSION" version >/dev/null 2>&1; then
echo "$PKG_NAME@$PKG_VERSION is already on the registry — skipping publish."
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "$PKG_NAME@$PKG_VERSION not yet published — will publish."
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.check.outputs.skip == 'false'
run: npm publish --provenance --access public