feat(i18n): add bloom_get_language() OS-locale FFI #14
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |