1 ---
2 name: sdk-releases
3 description: End-to-end SDK release process for all Onde distribution channels. Covers version bump checklist, CI/CD pipelines for Rust (crates.io), Swift (XCFramework → onde-swift), Dart (pub.dev), and npm, tag validation, GitHub Release creation, manual publish fallback, and common pitfalls. Apply whenever bumping versions or troubleshooting release workflows.
4 allowed-tools: Read, Write, Edit, Glob, Grep, Bash
5 user-invocable: true
6 ---
7
8 # Skill: SDK Releases
9
10 ## What This Skill Covers
11
12 The complete release pipeline for shipping Onde across all registries:
13
14 - **Rust crate** → crates.io (`onde`)
15 - **Swift package** → Swift Package Index (`onde-swift`)
16 - **Dart/Flutter package** → pub.dev (`onde_inference`)
17 - **React Native** → npm (`@ondeinference/react-native`)
18 - **Kotlin Multiplatform** → Maven Central (`com.ondeinference:onde-inference`)
19
20 All five SDK workflows share the same tag trigger. A single tag push fires all
21 five in parallel. When only a subset needs (re-)publishing, use the **manual
22 publish fallback** documented below to avoid collateral re-runs.
23
24 ---
25
26 ## Version Bump Checklist
27
28 Every release starts with a version bump. **All version sources must be updated
29 in a single commit before tagging.**
30
31 | # | File | Field | Example |
32 |---|------|-------|---------|
33 | 1 | `Cargo.toml` (root) | `version` | `version = "0.1.3"` |
34 | 2 | `sdk/dart/pubspec.yaml` | `version` | `version: 0.1.3` |
35 | 3 | `sdk/dart/CHANGELOG.md` | New `## 0.1.3` section | Prepend at top of file |
36 | 4 | `sdk/react-native/package.json` | `version` | `"version": "0.1.3"` |
37 | 5 | `sdk/react-native/rust/Cargo.toml` | `version` | `version = "0.1.3"` |
38 | 6 | `sdk/react-native/CHANGELOG.md` | New `## 0.1.3` section | Prepend at top of file |
39 | 7 | `sdk/kotlin/gradle.properties` | `VERSION_NAME` | `VERSION_NAME=0.1.3` |
40 | 8 | `sdk/dart/rust/Cargo.lock` | `onde` package version | Run `cd sdk/dart/rust && cargo update -p onde` |
41 | 9 | `sdk/react-native/rust/Cargo.lock` | `onde` package version | Run `cd sdk/react-native/rust && cargo update -p onde` |
42 | 10 | `Cargo.lock` (root) | `onde` package version | Run `cargo check` at repo root |
43
44 ### Files you do NOT manually edit
45
46 | File | Why |
47 |------|-----|
48 | `onde-swift/Package.swift` | CI rewrites the `url:` + `checksum:` automatically |
49 | `onde-swift/Sources/Onde/onde.swift` | CI copies the freshly generated UniFFI glue |
50
51 ### Quick bump commands
52
53 ```bash
54 # 1. Edit Cargo.toml version (manual)
55 # 2. Edit sdk/dart/pubspec.yaml version (manual)
56 # 3. Prepend new section to sdk/dart/CHANGELOG.md (manual)
57 # 4. Edit sdk/react-native/package.json version (manual)
58 # 5. Edit sdk/react-native/rust/Cargo.toml version (manual)
59 # 6. Prepend new section to sdk/react-native/CHANGELOG.md (manual)
60 # 7. Edit sdk/kotlin/gradle.properties VERSION_NAME (manual)
61
62 # 8. Sync lockfiles
63 cargo check # updates root Cargo.lock
64 cd sdk/dart/rust && cargo update -p onde # updates Dart SDK's Cargo.lock
65 cd sdk/react-native/rust && cargo update -p onde # updates React Native SDK's Cargo.lock
66
67 # 9. Verify
68 grep '^version' Cargo.toml # "0.1.3"
69 grep '^version:' sdk/dart/pubspec.yaml # 0.1.3
70 grep '"version"' sdk/react-native/package.json # "0.1.3"
71 grep '^version' sdk/react-native/rust/Cargo.toml # "0.1.3"
72 grep 'VERSION_NAME' sdk/kotlin/gradle.properties # 1.0.0
73 grep 'name = "onde"' -A1 Cargo.lock # version = "0.1.3"
74 grep 'name = "onde"' -A1 sdk/dart/rust/Cargo.lock # version = "0.1.3"
75 grep 'name = "onde"' -A1 sdk/react-native/rust/Cargo.lock # version = "0.1.3"
76
77 # 10. Commit and tag
78 git add -A
79 git commit -m "0.1.3"
80 git tag 0.1.3
81 git push origin main 0.1.3
82 ```
83
84 ---
85
86 ## Tag Format
87
88 All tags are **bare semver** — no `v` prefix.
89
90 ```
91 0.1.3 ✅ correct
92 v0.1.3 ❌ will NOT trigger CI
93 0.1.3-beta ❌ will NOT match the tag pattern
94 ```
95
96 All five CI workflows use the same trigger pattern:
97
98 ```yaml
99 on:
100 push:
101 tags:
102 - "[0-9]+.[0-9]+.[0-9]+"
103 ```
104
105 ---
106
107 ## CI Pipeline: Rust SDK (crates.io)
108
109 **Workflow:** `.github/workflows/release-sdk-rust.yml`
110 **Trigger:** Tag push matching `[0-9]+.[0-9]+.[0-9]+` or `workflow_dispatch`
111 **Runner:** `macos-26`
112
113 ### Flow
114
115 ```
116 Tag push (e.g. 1.0.0)
117 │
118 ├─ 1. Checkout + install stable Rust
119 │
120 ├─ 2. Validate tag == Cargo.toml version
121 │ └─ Fails fast if mismatched
122 │
123 ├─ 3. cargo publish --dry-run
124 │ └─ Compiles and packages; catches errors before real publish
125 │
126 └─ 4. cargo publish (tag push only)
127 └─ Authenticates via CARGO_REGISTRY_TOKEN secret
128 ```
129
130 ### Key details
131
132 - **`workflow_dispatch`** runs the dry-run only (the publish step has
133 `if: startsWith(github.ref, 'refs/tags/')`) — useful for validation.
134 - **Runner is `macos-26`** because the crate depends on `mistralrs` with Metal
135 features, which requires macOS + Xcode to compile.
136 - **crates.io is immutable** — once a version is published, it cannot be
137 re-published. You must bump to the next version. `cargo yank` hides a version
138 from dependency resolution but does not delete it.
139
140 ### Required secret
141
142 `CARGO_REGISTRY_TOKEN` — a crates.io API token scoped to `publish-update` for
143 the `onde` crate. Create at https://crates.io/settings/tokens. Best practice:
144 scope it to a single crate rather than using a global token.
145
146 ---
147
148 ## CI Pipeline: Swift SDK
149
150 **Workflow:** `.github/workflows/release-sdk-swift.yml`
151 **Trigger:** Tag push matching `[0-9]+.[0-9]+.[0-9]+`
152 **Runner:** `macos-15`
153
154 ### Flow
155
156 ```
157 Tag push (e.g. 0.1.3)
158 │
159 ├─ 1. Build XCFramework
160 │ └─ .github/scripts/build-swift-xcframework.sh
161 │ ├─ Extracts version from Cargo.toml via tomllib
162 │ ├─ Compiles staticlibs for 5 Apple targets
163 │ ├─ Generates UniFFI Swift bindings
164 │ ├─ Assembles XCFramework, zips, computes SHA-256
165 │ └─ Writes dist/swift/{version.txt, OndeFramework.checksum.txt}
166 │
167 ├─ 2. Read version + checksum into step outputs
168 │
169 ├─ 3. Validate tag == Cargo.toml version
170 │ └─ Fails fast if mismatched (e.g. tagged 0.1.3 but Cargo.toml says 0.1.2)
171 │
172 ├─ 4. Upload CI artifacts (every run, including workflow_dispatch)
173 │
174 ├─ 5. Create GitHub Release on `onde` (tag push only)
175 │ └─ Attaches OndeFramework.xcframework.zip + checksum
176 │
177 ├─ 6. Checkout ondeinference/onde-swift (using ONDE_SWIFT_PAT)
178 │
179 ├─ 7. Rewrite Package.swift
180 │ └─ Python regex replaces .binaryTarget url: + checksum:
181 │
182 ├─ 8. Copy generated onde.swift → onde-swift/Sources/Onde/
183 │
184 └─ 9. Commit + tag onde-swift
185 ├─ git commit -m "Release 0.1.3" (skipped if no changes)
186 ├─ git push origin HEAD:main
187 └─ git tag -a "0.1.3" -m "Release 0.1.3" (skipped if tag exists)
188 └─ Annotated tag, never force-pushed
189 ```
190
191 ### What happens on `onde-swift` after the push
192
193 The `onde` CI pushes a commit (to `main`) and a tag (e.g. `0.1.3`) to `onde-swift`.
194 This triggers **two** workflows on `onde-swift`:
195
196 1. **`ci.yml`** (on push to `main`) — validates manifest + builds all 5 platforms
197 2. **`release.yml`** (on tag push) — validates package, then creates a GitHub
198 Release with installation instructions and a link to the upstream `onde` changelog
199
200 Both run concurrently without conflict — they use different concurrency groups
201 (`ci-refs/heads/main` vs the tag ref).
202
203 ### Tag immutability
204
205 Tags on `onde-swift` are **never force-pushed**. SPM caches the resolved commit
206 SHA per tag in `Package.resolved`. Moving a tag silently breaks every consumer's
207 lockfile. If a release needs to be redone:
208
209 1. Delete the remote tag: `git push origin :refs/tags/0.1.3`
210 2. Delete the GitHub Release on both `onde` and `onde-swift`
211 3. Fix the issue, re-tag, push again
212
213 ---
214
215 ## CI Pipeline: npm SDK (React Native)
216
217 **Workflow:** `.github/workflows/release-sdk-npm.yml`
218 **Trigger:** Tag push matching `[0-9]+.[0-9]+.[0-9]+` or `workflow_dispatch`
219 **Runner:** `ubuntu-latest`
220
221 ### Flow
222
223 ```
224 Tag push (e.g. 1.0.0)
225 │
226 ├─ validate job:
227 │ ├─ Set up Node.js 22
228 │ ├─ Read version from sdk/react-native/package.json
229 │ ├─ Validate tag == package.json version
230 │ ├─ npm install + npm run build
231 │ └─ npm publish --dry-run --access public
232 │
233 └─ publish job (tag push only, needs: validate):
234 ├─ npm install + npm run build
235 ├─ Check if version already exists on npm
236 │ └─ If yes → skip publish (idempotent, exits 0)
237 └─ npm publish --access public
238 ```
239
240 ### npm idempotency guard
241
242 The publish step checks `npm view <package>@<version>` before publishing. If the
243 version already exists, it **skips** with a success exit code. This makes the npm
244 workflow safe to re-run — unlike Dart/Rust, it won't fail on a duplicate version.
245
246 ### Required secret
247
248 `NPM_TOKEN` — a granular npm access token scoped to the `@ondeinference` org
249 with read+write packages. Create at npmjs.com → Access Tokens.
250
251 ---
252
253 ## CI Pipeline: Dart SDK
254
255 **Workflow:** `.github/workflows/release-sdk-dart.yml`
256 **Trigger:** Tag push matching `[0-9]+.[0-9]+.[0-9]+`
257 **Runner:** `ubuntu-latest`
258
259 ### Flow
260
261 ```
262 Tag push (e.g. 0.1.3)
263 │
264 ├─ validate job:
265 │ ├─ Set up Flutter (version from sdk/dart/.flutter-version)
266 │ ├─ Read version from pubspec.yaml
267 │ ├─ Validate tag == pubspec.yaml version
268 │ │ └─ Fails fast if mismatched
269 │ ├─ flutter pub get
270 │ ├─ flutter analyze --no-fatal-infos
271 │ └─ flutter pub publish --dry-run
272 │
273 └─ publish job (tag push only, needs: validate):
274 ├─ Set up Flutter
275 ├─ flutter pub get
276 ├─ Write PUB_CREDENTIALS to ~/.config/dart/pub-credentials.json
277 └─ flutter pub publish --force
278 ```
279
280 ### Tag validation (Dart)
281
282 The Dart workflow independently validates the tag against `pubspec.yaml`:
283
284 ```yaml
285 if [ "$TAG" != "$PUBSPEC_VERSION" ]; then
286 echo "::error::Tag '$TAG' does not match pubspec.yaml version '$PUBSPEC_VERSION'."
287 exit 1
288 fi
289 ```
290
291 This means **both** `Cargo.toml` and `pubspec.yaml` must have the same version
292 as the git tag, or both workflows fail.
293
294 ### Required secret
295
296 `PUB_CREDENTIALS` — the full JSON content of `~/.config/dart/pub-credentials.json`
297 from a machine that has run `dart pub login`. Add as a repository secret on `onde`.
298
299 ---
300
301 ## GitHub Releases (two repos)
302
303 | Repo | Created by | Contains |
304 |------|-----------|----------|
305 | `onde` | `release-sdk-swift.yml` (step 5) | XCFramework zip + checksum, auto-generated release notes |
306 | `onde-swift` | `onde-swift` `release.yml` | Installation snippet, platform table, link to upstream changelog |
307
308 Both repos need GitHub Releases because they serve different audiences:
309
310 - **`onde`** — Rust consumers, asset hosting for the XCFramework binary
311 - **`onde-swift`** — Swift developers, Swift Package Index surfaces these as version notes
312
313 ---
314
315 ## Version Sources and Validation Matrix
316
317 | Source | Read by | Validated against |
318 |--------|---------|-------------------|
319 | `Cargo.toml` `version` | `build-swift-xcframework.sh` (tomllib) | Git tag (Swift workflow) |
320 | `sdk/dart/pubspec.yaml` `version` | `release-sdk-dart.yml` (grep + awk) | Git tag (Dart workflow) |
321 | Git tag (`github.ref_name`) | Both workflows | Both version files above |
322 | `onde-swift` tag | Created by `release-sdk-swift.yml` | Same value as the `onde` tag |
323
324 All four must match. If any pair diverges, the relevant CI job fails.
325
326 ---
327
328 ## Secrets Required
329
330 | Secret | Used by | Purpose |
331 |--------|---------|---------|
332 | `CARGO_REGISTRY_TOKEN` | `release-sdk-rust.yml` | Authenticate with crates.io for `cargo publish`. Scoped to `publish-update` for the `onde` crate. Create at https://crates.io/settings/tokens. |
333 | `ONDE_SWIFT_PAT` | `release-sdk-swift.yml` | Push commits + tags to `ondeinference/onde-swift`. Must be a PAT (not `GITHUB_TOKEN`) so it triggers workflows on `onde-swift`. Needs `contents: write` scope. |
334 | `PUB_CREDENTIALS` | `release-sdk-dart.yml` | Authenticate with pub.dev for `flutter pub publish`. Full JSON from `~/.config/dart/pub-credentials.json`. |
335 | `NPM_TOKEN` | `release-sdk-npm.yml` | Authenticate with npm for `npm publish`. Granular access token scoped to the `@ondeinference` org with read+write packages. Create at npmjs.com → Access Tokens. |
336 | `ORG_GRADLE_PROJECT_MAVENCENTRALUSERNAME` | `release-sdk-kotlin.yml` | Sonatype Central Portal token username. Generate at central.sonatype.com → Profile → Generate User Token. |
337 | `ORG_GRADLE_PROJECT_MAVENCENTRALPASSWORD` | `release-sdk-kotlin.yml` | Sonatype Central Portal token password. Same dialog as above. |
338 | `ORG_GRADLE_PROJECT_SIGNINGKEYID` | `release-sdk-kotlin.yml` | Last 8 chars of GPG key fingerprint used to sign Maven Central artifacts. |
339 | `ORG_GRADLE_PROJECT_SIGNINGKEY` | `release-sdk-kotlin.yml` | ASCII-armored GPG private key. Export with `gpg --armor --export-secret-keys <KEY_ID>`. |
340 | `ORG_GRADLE_PROJECT_SIGNINGPASSWORD` | `release-sdk-kotlin.yml` | Passphrase for the GPG signing key. |
341 | `GRESIQ_API_KEY_DEV` | All release workflows | Build-time env for dev environment API key. |
342 | `GRESIQ_API_SECRET_DEV` | All release workflows | Build-time env for dev environment API secret. |
343 | `GRESIQ_API_KEY_PRODUCTION` | All release workflows | Build-time env for production API key. |
344 | `GRESIQ_API_SECRET_PRODUCTION` | All release workflows | Build-time env for production API secret. |
345 | `HF_TOKEN` | All release workflows | HuggingFace token baked in at build time (`option_env!`). |
346
347 ---
348
349 ## Common Pitfalls
350
351 | Pitfall | Symptom | Fix |
352 |---------|---------|-----|
353 | Forgot to bump `pubspec.yaml` | Dart CI fails: "Tag '0.1.3' does not match pubspec.yaml version '0.1.2'" | Bump `sdk/dart/pubspec.yaml`, amend commit, re-tag |
354 | Forgot to bump `sdk/react-native/package.json` | npm CI fails: "Tag '0.1.3' does not match package.json version '0.1.2'" | Bump `sdk/react-native/package.json`, amend commit, re-tag |
355 | Forgot to bump `sdk/react-native/rust/Cargo.toml` | npm SDK's Rust bridge crate version drifts from the main crate | Bump `sdk/react-native/rust/Cargo.toml`, run `cd sdk/react-native/rust && cargo update -p onde` |
356 | Forgot to update `sdk/dart/rust/Cargo.lock` | Dart SDK's Rust bridge builds against stale `onde` version | Run `cd sdk/dart/rust && cargo update -p onde` |
357 | Forgot to update `sdk/react-native/rust/Cargo.lock` | npm SDK's Rust bridge builds against stale `onde` version | Run `cd sdk/react-native/rust && cargo update -p onde` |
358 | Forgot to update `sdk/dart/CHANGELOG.md` | pub.dev shows stale changelog | Prepend new `## 0.1.3` section before tagging |
359 | Forgot to bump `sdk/react-native/CHANGELOG.md` | npm shows stale changelog | Prepend new `## 0.1.3` section before tagging |
360 | Forgot to bump `sdk/kotlin/gradle.properties` `VERSION_NAME` | Kotlin CI fails: tag vs gradle.properties mismatch | Bump `VERSION_NAME`, amend commit, re-tag |
361 | Tag has `v` prefix (`v0.1.3`) | CI does not trigger — tag pattern requires bare semver | Delete the tag, re-tag without `v` |
362 | `CARGO_REGISTRY_TOKEN` missing | `cargo publish` fails with auth error | Create a scoped token at crates.io/settings/tokens, add as repo secret |
363 | `ONDE_SWIFT_PAT` expired | `onde-swift` push fails with 403 | Regenerate PAT at github.com/settings/tokens, update repo secret |
364 | `PUB_CREDENTIALS` expired | `flutter pub publish` fails with 401 | Run `dart pub login` locally, copy new credentials JSON to repo secret |
365 | `NPM_TOKEN` expired | `npm publish` fails with 401/403 | Regenerate token at npmjs.com → Access Tokens, update repo secret |
366 | crates.io version already published | `cargo publish` fails with "already uploaded" | crates.io is immutable — bump to next version. `cargo yank` hides but doesn't delete. |
367 | npm version already published | npm publish step skips (idempotency guard) | Safe — the workflow exits 0. No action needed. |
368 | pub.dev version already published | `flutter pub publish` fails | pub.dev is immutable — bump to next version |
369 | Maven Central version already published | `publishAndReleaseToMavenCentral` fails | Maven Central is immutable — bump to next version |
370 | Maven Central namespace not verified | Deployment fails: "Namespace 'com.ondeinference' is not allowed" | Verify namespace on central.sonatype.com (add DNS TXT record, wait for Sonatype to poll) |
371 | Tag pushed before workflow exists on `main` | Workflow never fires for that tag | Use `workflow_dispatch` for dry-run, then publish manually. See **Manual Publish Fallback**. |
372 | All 5 workflows fire but only 1–2 need re-publishing | Collateral re-runs of Swift/npm/Dart/Rust/Kotlin | Don't delete+re-push the tag. Use **Manual Publish Fallback** for the specific SDKs. |
373 | `onde-swift` tag already exists | Tag push skipped (warning emitted) | Delete remote tag first: `git push origin :refs/tags/0.1.3` |
374 | Force-pushed a tag on `onde-swift` | SPM users get stale `Package.resolved` | Never force-push. Delete + re-create instead. Advise consumers to run `swift package resolve` |
375 | XCFramework URL 404 | `swift package resolve` fails on consumer side | Ensure the `onde` GitHub Release was created BEFORE the `onde-swift` tag was pushed (the workflow handles this order automatically) |
376 | `Package.swift` regex didn't match | Python `RuntimeError` in CI | Check that `onde-swift/Package.swift` still has a `.binaryTarget(name: "OndeFramework", ...)` block |
377 | Dart `flutter analyze` fails | `validate` job fails, `publish` is skipped | Fix analysis issues, re-tag |
378
379 ---
380
381 ## Manual Publish Fallback
382
383 When a tag was pushed before a workflow existed on `main`, or when you need to
384 publish only a subset of SDKs without re-triggering all four workflows:
385
386 ### Rust (crates.io)
387
388 ```bash
389 # Dry-run first
390 cargo publish --dry-run
391
392 # Publish
393 cargo publish
394 ```
395
396 Requires `~/.cargo/credentials.toml` or `CARGO_REGISTRY_TOKEN` env var.
397
398 ### Dart (pub.dev)
399
400 ```bash
401 cd sdk/dart
402
403 # Dry-run
404 flutter pub publish --dry-run
405
406 # Publish
407 flutter pub publish --force
408 ```
409
410 Requires `~/.config/dart/pub-credentials.json` from `dart pub login`.
411
412 ### Kotlin (Maven Central)
413
414 ```bash
415 cd sdk/kotlin
416
417 # Dry-run
418 ./gradlew :lib:publishToMavenCentral --dry-run --no-configuration-cache
419
420 # Publish and release
421 ./gradlew :lib:publishAndReleaseToMavenCentral --no-configuration-cache
422 ```
423
424 Requires `~/.gradle/gradle.properties` with `mavenCentralUsername`,
425 `mavenCentralPassword`, `signingKeyId`, `signingKey`, `signingPassword`.
426 Also requires the `com.ondeinference` namespace to be verified on
427 central.sonatype.com.
428
429 ### npm (React Native)
430
431 ```bash
432 cd sdk/react-native
433 npm run build
434 npm publish --access public
435 ```
436
437 Requires `npm login` or `NPM_TOKEN` env var.
438
439 ### When to use manual publish
440
441 | Scenario | Approach |
442 |----------|----------|
443 | Tag exists but workflow wasn't on `main` yet | Merge workflow, then publish manually |
444 | One SDK's CI failed, others succeeded | Fix the issue, publish that SDK manually |
445 | Need to avoid re-triggering Swift XCFramework build | Publish Rust/Dart/npm/Kotlin manually |
446 | Testing a new workflow before first real tag | Use `workflow_dispatch` for dry-run only |
447
448 ---
449
450 ## Re-releasing a Version (Emergency Fix)
451
452 If a release was botched and you need to re-publish the same version:
453
454 ### Swift (onde-swift)
455
456 ```bash
457 # 1. Delete the remote tag on onde-swift
458 git -C /path/to/onde-swift push origin :refs/tags/0.1.3
459
460 # 2. Delete the GitHub Release on onde-swift (via GitHub UI or gh CLI)
461 gh release delete 0.1.3 --repo ondeinference/onde-swift --yes
462
463 # 3. Delete the GitHub Release on onde
464 gh release delete 0.1.3 --repo ondeinference/onde --yes
465
466 # 4. Delete the local and remote tag on onde
467 git tag -d 0.1.3
468 git push origin :refs/tags/0.1.3
469
470 # 5. Fix the issue, commit, re-tag, push
471 git tag 0.1.3
472 git push origin main 0.1.3
473 ```
474
475 ### Dart (pub.dev) / Rust (crates.io) / Kotlin (Maven Central)
476
477 **pub.dev, crates.io, and Maven Central are all immutable.** Once a version is
478 published, it cannot be re-published. You must bump to `0.1.4` instead.
479
480 - **crates.io:** `cargo yank --version 0.1.3` hides the version from dependency
481 resolution but does not delete it.
482 - **pub.dev:** No yank equivalent. The version remains visible and installable.
483
484 If the `publish` job failed (i.e. the package was NOT published), you can
485 safely re-tag after fixing the issue.
486
487 ### npm
488
489 The npm workflow's idempotency guard means re-running it is safe — it skips
490 if the version already exists. However, npm also does not allow re-publishing
491 the same version with different content. Bump to the next version if the
492 published content was wrong.
493
494 ---
495
496 ## End-to-End Release Timeline
497
498 ```
499 Developer merges feature → development → main (--no-ff) then tags
500 │
501 ├─── release-sdk-rust.yml fires ──────────────────────────────────────┐
502 │ ~2 min (dry-run + publish to crates.io) │
503 │ │
504 ├─── release-sdk-swift.yml fires ──────────────────────────────────────┤
505 │ ~15 min (XCFramework build) │
506 │ Creates GitHub Release on onde │
507 │ Pushes commit + tag to onde-swift │
508 │ │ │
509 │ ├─ onde-swift ci.yml fires (push to main) │
510 │ │ ~5 min (validate + build 5 platforms) │
511 │ │ │
512 │ └─ onde-swift release.yml fires (tag 1.0.0) │
513 │ ~5 min (validate + create GitHub Release) │
514 │ │
515 ├─── release-sdk-dart.yml fires ───────────────────────────────────────┤
516 │ ~3 min (validate + publish to pub.dev) │
517 │ │
518 ├─── release-sdk-npm.yml fires ───────────────────────────────────────┤
519 │ ~3 min (validate + publish to npm) │
520 │ │
521 ├─── release-sdk-kotlin.yml fires ────────────────────────────────────┤
522 │ ~10 min (Android ABIs + JVM dylib + publish to Maven Central) │
523 │ │
524 └─── Swift Package Index picks up onde-swift tag ──────────────────────┘
525 ~30 min (SPI polling interval)
526 klibs.io picks up Maven Central artifact within ~30 days
527 ```
528
529 All five CI workflows fire **in parallel** from the single tag push. The only
530 ordering dependency is that `release-sdk-swift` must create the `onde` GitHub
531 Release (with XCFramework assets) **before** pushing the tag to `onde-swift` —
532 this is guaranteed by the step order within the workflow.
533
534 ### Merge train
535
536 Always merge through the branch train before tagging. Use `--no-ff` to
537 create explicit merge commits — they serve as bookmarks in the history
538 showing where each feature landed.
539
540 ```bash
541 git checkout development && git merge feature/my-branch --no-ff --no-edit
542 git checkout main && git merge development --no-ff --no-edit
543 git push origin development main
544 git tag X.Y.Z && git push origin X.Y.Z
545 ```
546
547 Never tag from a feature branch. The tag must point to a commit on `main` that
548 contains all workflow files and fixes. If a workflow is added on a feature branch
549 and the tag is pushed before merging, the workflow won't exist at the tagged
550 commit and CI won't fire.
551
552 **Why `--no-ff`:** Multi-commit feature branches (like the Kotlin SDK with 35
553 files across 6 commits) look like they were committed directly to `main` with
554 fast-forward merges. `--no-ff` preserves the branch context. It also allows
555 reverting an entire feature with a single `git revert -m 1 <merge-commit>`.
556
557 ---
558
559 ## Distribution Registry Reference
560
561 | Registry | Package Name | Import | Workflow | Immutable? |
562 |----------|-------------|--------|----------|------------|
563 | crates.io | `onde` | `onde = "1.0"` | `release-sdk-rust.yml` | Yes (yank hides, doesn't delete) |
564 | Swift Package Index | `onde-swift` (org: `ondeinference`) | `import Onde` | `release-sdk-swift.yml` → `onde-swift/release.yml` | Tags are immutable by convention |
565 | pub.dev | `onde_inference` | `import 'package:onde_inference/onde_inference.dart'` | `release-sdk-dart.yml` | Yes (no yank) |
566 | npm | `@ondeinference/react-native` | `import { OndeChatEngine } from "@ondeinference/react-native"` | `release-sdk-npm.yml` | Yes (unpublish within 72h only) |
567 | Maven Central | `com.ondeinference:onde-inference` | `implementation("com.ondeinference:onde-inference:1.0.0")` | `release-sdk-kotlin.yml` | Yes (cannot re-publish same version) |
568 | klibs.io | `onde-inference` | Same Maven Central coordinate | Auto-indexed from Maven Central | N/A (discovery only) |
569
570 ---
571
572 ---
573
574 ## React Native npm SDK
575
576 **Package:** `@ondeinference/react-native` (scoped under `@ondeinference` org on npm)
577 **Location:** `sdk/react-native/`
578 **Architecture:** Expo native module wrapping Rust via C FFI
579
580 ### How it works
581
582 ```
583 TypeScript (React Native)
584 │ import { OndeChatEngine } from "@ondeinference/react-native"
585 ▼
586 Expo Module (Swift / Kotlin)
587 │ OndeInferenceModule — calls C FFI functions via @_silgen_name (iOS) / JNI (Android)
588 ▼
589 Rust C FFI bridge (sdk/react-native/rust/)
590 │ extern "C" functions with JSON serialization, global tokio::Runtime
591 ▼
592 onde crate (src/)
593 │ ChatEngine — tokio::sync::Mutex, mistral.rs inference
594 ▼
595 mistral.rs → Metal (iOS) / CPU (Android)
596 ```
597
598 ### Key files
599
600 | File | Purpose |
601 |------|---------|
602 | `sdk/react-native/package.json` | npm package — `@ondeinference/react-native` |
603 | `sdk/react-native/expo-module.config.json` | Expo autolinking config |
604 | `sdk/react-native/src/index.ts` | Public TypeScript API — `OndeChatEngine`, free functions, JSON ↔ camelCase conversion |
605 | `sdk/react-native/src/types.ts` | TypeScript type definitions mirroring Rust types |
606 | `sdk/react-native/src/OndeInferenceModule.ts` | `requireNativeModule("OndeInference")` bridge |
607 | `sdk/react-native/rust/src/lib.rs` | C FFI exports (`extern "C"`) + Android JNI wrappers |
608 | `sdk/react-native/ios/OndeInferenceModule.swift` | Swift Expo module — calls Rust via `@_silgen_name` |
609 | `sdk/react-native/android/.../OndeInferenceModule.kt` | Kotlin Expo module — calls Rust via JNI `external fun` |
610 | `sdk/react-native/scripts/build-rust.sh` | Cross-compile Rust for iOS (staticlib) + Android (cdylib) |
611
612 ### Building native libraries
613
614 ```bash
615 # iOS (requires rustup targets: aarch64-apple-ios, aarch64-apple-ios-sim)
616 cd sdk/react-native && ./scripts/build-rust.sh ios
617
618 # Android (requires ANDROID_NDK_HOME)
619 cd sdk/react-native && ./scripts/build-rust.sh android
620
621 # Both
622 cd sdk/react-native && ./scripts/build-rust.sh all
623 ```
624
625 ### Publishing to npm
626
627 ```bash
628 cd sdk/react-native
629 npm run build # TypeScript → build/
630 npm publish --access public
631 ```
632
633 The `--access public` flag is required for scoped packages on first publish.
634
635 ---
636
637 ## Registry Immutability Summary
638
639 All four registries are effectively immutable once a version is published:
640
641 - **crates.io** — `cargo yank` removes from resolver but the tarball stays forever
642 - **pub.dev** — No retraction mechanism at all
643 - **npm** — `npm unpublish` works within 72 hours, but only if no dependents
644 - **Maven Central** — No retraction mechanism. Once published, it's permanent.
645 - **Swift (onde-swift tags)** — Convention: never force-push tags. Delete + re-create if needed.
646
647 **Bottom line:** get the release right before publishing. Use `--dry-run` locally
648 and via `workflow_dispatch` before pushing a real tag.
649
650 ---
651
652 *Update this skill when adding new SDK targets (Python, Ruby, Kotlin), changing
653 CI runners, or modifying the tag/version validation logic.*