Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/mirror.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: mirror

# The public iOS and React Native repositories are generated from this one, so a merge to main
# rebuilds them and pushes the result. Their own publish workflows take it from there: bumping a
# package version in a pull request here is the whole release ritual.
#
# MIRROR_TOKEN is a fine-grained personal access token with Contents and Workflows write access to
# both mirrors; the built-in GITHUB_TOKEN cannot reach another repository.
on:
push:
branches: [main]
workflow_dispatch:

jobs:
mirror:
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- platform: ios
repository: Xget7/splatkit-ios
- platform: react-native
repository: Xget7/react-native-splatkit
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"

# Without this the failure is an opaque git authentication error four steps later.
- name: Check the mirror token exists
run: |
if [ -z "${MIRROR_TOKEN}" ]; then
echo "::error::MIRROR_TOKEN is not set. Create a fine-grained token with Contents" \
"and Workflows write access to ${REPOSITORY} and add it as a repository secret."
exit 1
fi
env:
MIRROR_TOKEN: ${{ secrets.MIRROR_TOKEN }}
REPOSITORY: ${{ matrix.repository }}

# The export refuses binaries, oversized files and anything matching a secret pattern, so a
# bad file fails here rather than reaching a public repository.
- id: export
run: |
directory=$(python3 scripts/export-ios-source.py --platform "$PLATFORM" \
| python3 -c "import json,sys; print(json.load(sys.stdin)['directory'])")
echo "directory=$directory" >> "$GITHUB_OUTPUT"
env:
PLATFORM: ${{ matrix.platform }}

- name: Push the generated tree
run: |
git clone --depth 1 \
"https://x-access-token:${MIRROR_TOKEN}@github.com/${REPOSITORY}.git" mirror
rsync -a --delete --exclude .git "${DIRECTORY}/" mirror/
cd mirror
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
echo "No change to mirror."
exit 0
fi
git commit -m "$(git -C "$GITHUB_WORKSPACE" log -1 --format=%s "$GITHUB_SHA")" \
-m "Generated from ${GITHUB_REPOSITORY}@${GITHUB_SHA}."
git push
env:
MIRROR_TOKEN: ${{ secrets.MIRROR_TOKEN }}
REPOSITORY: ${{ matrix.repository }}
DIRECTORY: ${{ steps.export.outputs.directory }}
52 changes: 48 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,76 @@
name: release

# Publishing runs from a tag so the released bytes come from a commit that exists.
# Tag with the version in build.gradle.kts, e.g. `git tag v0.1.0-alpha01 && git push --tags`;
# the deployment is released on Central without a portal step.
# The version in packages/splatkit-android/build.gradle.kts is the release trigger: a merge to
# main that changes it publishes the new version to Maven Central and tags the commit. Maven
# Central takes ten minutes or more to appear on repo1, so the decision is made from the diff
# rather than by asking the registry what exists, which would race with its own mirror.
on:
push:
tags: ["v*"]
branches: [main]
workflow_dispatch:

jobs:
publish:
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2

- id: state
name: Decide whether the version changed
run: |
coordinates() {
git show "$1:packages/splatkit-android/build.gradle.kts" 2>/dev/null \
| sed -n 's/.*coordinates(.*"splatkit-android", "\([^"]*\)").*/\1/p'
}
version=$(coordinates HEAD)
test -n "$version"
echo "version=$version" >> "$GITHUB_OUTPUT"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "new=true" >> "$GITHUB_OUTPUT"
elif [ "$version" = "$(coordinates HEAD^)" ]; then
echo "new=false" >> "$GITHUB_OUTPUT"
echo "splatkit-android stays at $version; nothing to release."
else
echo "new=true" >> "$GITHUB_OUTPUT"
fi

- uses: actions/setup-java@v4
if: steps.state.outputs.new == 'true'
with:
distribution: temurin
java-version: "17"
- uses: android-actions/setup-android@v3
if: steps.state.outputs.new == 'true'
with:
packages: "platforms;android-36 build-tools;36.0.0 ndk;27.1.12297006 cmake;3.22.1"
- uses: gradle/actions/setup-gradle@v4
if: steps.state.outputs.new == 'true'

- name: Upload the release to Maven Central
if: steps.state.outputs.new == 'true'
working-directory: apps/android-dev
run: ./gradlew :splatkit:publishToMavenCentral --no-daemon
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}

- name: Tag the release commit
if: steps.state.outputs.new == 'true'
run: |
if git ls-remote --exit-code --tags origin "v$VERSION" >/dev/null 2>&1; then
echo "v$VERSION already tagged."
exit 0
fi
git tag "v$VERSION"
git push origin "v$VERSION"
gh release create "v$VERSION" --title "SplatKit Android $VERSION" --generate-notes \
$(case "$VERSION" in *alpha*|*beta*|*rc*) echo --prerelease ;; esac)
env:
VERSION: ${{ steps.state.outputs.version }}
GH_TOKEN: ${{ github.token }}
7 changes: 3 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,10 @@ Adapter changes also need the Android adapter host tests in its [README](package
Check build wiring changes in a fresh React Native app outside this repository, installing the `npm pack` tarball.
`prepack` fetches and checksum-verifies the iOS `SplatKitCore.xcframework`; `SPLATKIT_IOS_XCFRAMEWORK_PATH` substitutes a local `scripts/package-ios.sh` build.

## Publish the React Native package
## Release

Release `splatkit-android` and `splatkit-ios` first, then match the Android version in `android/build.gradle` and `scripts/ios-xcframework.json`.
Export with `scripts/export-ios-source.py --platform react-native` and push to [react-native-splatkit](https://github.com/Xget7/react-native-splatkit).
A `v<package.json version>` tag there runs `.github/workflows/publish.yml`, which needs the `NPM_TOKEN` secret and publishes prereleases under the `next` dist-tag.
Bumping a version is the release: merge it to `main` and the workflows publish, tag and mirror on their own.
[docs/RELEASING.md](docs/RELEASING.md) has the whole flow, including the one part still done by hand, building the iOS XCFramework on a Mac.

## Lint the C++

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Use the native view, forward lifecycle and load worlds asynchronously; see each
| Per-view render policy and capabilities | Yes | Yes |
| React Native policy prop and events | iPhone 17 Pro validated | Mi 9 validated |

React Native: `npm install @splatkit/react-native`, published from [react-native-splatkit](https://github.com/Xget7/react-native-splatkit); the [example app](apps/react-native/README.md) starts from zero.
React Native: `npm install @splatkit/react-native@next`, published from [react-native-splatkit](https://github.com/Xget7/react-native-splatkit); the [example app](apps/react-native/README.md) starts from zero.
One policy prop drives both adapters.

`splat-core` owns formats, hierarchy and navigation; `splatkit-engine` owns orchestration; each native SDK owns its GPU resources and view lifecycle.
Expand Down
6 changes: 3 additions & 3 deletions apps/react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Once the collider is ready a thumb stick appears; it is this app's own control,
The SDK draws no walking UI of its own.

Inside this monorepo the app installs [`@splatkit/react-native`](../../packages/react-native-splatkit/README.md) from `../../packages`, and builds the Android SDK from source, so the example always exercises the current API.
Outside it, `npm install @splatkit/react-native` is the only change.
Outside it, `npm install @splatkit/react-native@next` is the only change.

Linking the package rather than unpacking it costs the example one extra piece of Metro config, in [`metro.config.js`](metro.config.js).
The package keeps React and React Native as devDependencies, so from the linked directory Metro resolves them to the package's own `node_modules` and the bundle ends up with two copies of each.
Expand All @@ -19,7 +19,7 @@ An app that installs the package from npm needs none of this.
```sh
npx @react-native-community/cli@latest init MyApp --version 0.87.1
cd MyApp
npm install @splatkit/react-native
npm install @splatkit/react-native@next
```

Then match what this app changes from the template:
Expand All @@ -33,7 +33,7 @@ Then match what this app changes from the template:

## Run this app

Needs Node 22.11 or newer, the Android SDK with NDK, Xcode 26 and CocoaPods.
Needs Node 22.13 or newer, the Android SDK with NDK, Xcode 26 and CocoaPods.
Worlds are `.spz`, `.ply` or `.lodsplat` files and are not committed.

```sh
Expand Down
65 changes: 65 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Releasing

A version number is the release trigger.
Bump it in a pull request, merge to `main`, and the workflows publish; merge anything else and they do nothing.
Nobody tags by hand.

## The three artifacts

| Artifact | Version lives in | Published by | Registry |
| --- | --- | --- | --- |
| `io.github.xget7:splatkit-android` | `packages/splatkit-android/build.gradle.kts`, the `coordinates(...)` call | `.github/workflows/release.yml` | Maven Central |
| `@splatkit/react-native` | `packages/react-native-splatkit/package.json` | `publish.yml` in the React Native mirror | npm |
| splatkit-ios `SplatKitCore.xcframework` | `Package.swift`, the binary target URL | a person, see below | GitHub Releases |

## What a merge to main does

1. `mirror.yml` regenerates both public repositories with `scripts/export-ios-source.py` and pushes them.
The export refuses binaries, oversized files and anything matching a secret pattern, so a bad file fails the job instead of reaching a public repository.
2. `release.yml` compares the Maven coordinate against the previous commit.
If it changed, it publishes to Maven Central, tags the commit and opens a GitHub release.
3. The React Native mirror's own `publish.yml` asks npm whether `package.json`'s version exists.
If it does not, it publishes with provenance, tags and releases.

Both publish steps are no-ops when the version did not move, so an ordinary merge is safe.

### The `latest` dist-tag

Prereleases publish under the `next` dist-tag.
While every published version is a prerelease, the workflow also points `latest` at the newest one, because otherwise plain `npm install @splatkit/react-native` hands out whatever was published first.
Once a stable version owns `latest`, the workflow stops touching it.

## Cutting an iOS release

The XCFramework is built on a Mac, so this part is still manual.

```sh
rm -rf build/ios-distribution # a cached Xcode SDK path breaks configure after an upgrade
scripts/package-ios.sh # prints the artifact path and its checksum
```

Then, in one pull request:

1. Set the binary target's `url` and `checksum` in `Package.swift` to the new tag and the printed checksum.
2. Set the same version and checksum in `packages/react-native-splatkit/scripts/ios-xcframework.json`, which the npm package fetches at `prepack`.
3. Update the version named in `README.md` and in both iOS READMEs.

Merge it, then create the release the URL now points at:

```sh
gh release create v0.1.0-alphaN -R Xget7/splatkit-ios --prerelease \
--title "SplatKit iOS 0.1.0 alpha N" --notes "..." \
build/ios-distribution/package.*/SplatKitCore.xcframework.zip
```

Create the iOS release before any npm publish that pins it: `npm prepack` downloads the XCFramework and verifies the checksum, so a missing release fails the publish.

## Secrets

| Secret | Where | Used by |
| --- | --- | --- |
| `MIRROR_TOKEN` | this repository | `mirror.yml`, to push to both public repositories. A fine-grained token with Contents and Workflows write access to each; the built-in `GITHUB_TOKEN` cannot reach another repository. |
| `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, `SIGNING_KEY`, `SIGNING_KEY_PASSWORD` | this repository | `release.yml` |
| `NPM_TOKEN` | the React Native mirror | `publish.yml` |

No workflow that runs on a pull request touches any of them, and none uses `pull_request_target`, so a fork's pull request can run the checks but cannot reach a credential.
Loading
Loading