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