@ondeinference / onde / commits / 62908d8

Update release skill

keypair34 committed Apr 26, 2026 at 17:55 UTC 62908d8ca3864499588281ea7a5e224cedc10e4e
1 file changed +209 -20
.agents/skills/sdk-releases/SKILL.md
+209 -20
@@ -1,6 +1,6 @@
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 Swift (XCFramework → onde-swift) and Dart (pub.dev), tag validation, GitHub Release creation, and common pitfalls. Apply whenever bumping versions or troubleshooting release workflows.
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 ---
@@ -14,6 +14,11 @@ The complete release pipeline for shipping Onde across all registries:
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 +
19 +All four SDK workflows share the same tag trigger. A single tag push fires all
20 +four in parallel. When only a subset needs (re-)publishing, use the **manual
21 +publish fallback** documented below to avoid collateral re-runs.
22
23 ---
24
@@ -95,6 +100,47 @@ on:
100
101 ---
102
103 +## CI Pipeline: Rust SDK (crates.io)
104 +
105 +**Workflow:** `.github/workflows/release-sdk-rust.yml`
106 +**Trigger:** Tag push matching `[0-9]+.[0-9]+.[0-9]+` or `workflow_dispatch`
107 +**Runner:** `macos-26`
108 +
109 +### Flow
110 +
111 +```
112 +Tag push (e.g. 1.0.0)
113 + │
114 + ├─ 1. Checkout + install stable Rust
115 + │
116 + ├─ 2. Validate tag == Cargo.toml version
117 + │ └─ Fails fast if mismatched
118 + │
119 + ├─ 3. cargo publish --dry-run
120 + │ └─ Compiles and packages; catches errors before real publish
121 + │
122 + └─ 4. cargo publish (tag push only)
123 + └─ Authenticates via CARGO_REGISTRY_TOKEN secret
124 +```
125 +
126 +### Key details
127 +
128 +- **`workflow_dispatch`** runs the dry-run only (the publish step has
129 + `if: startsWith(github.ref, 'refs/tags/')`) — useful for validation.
130 +- **Runner is `macos-26`** because the crate depends on `mistralrs` with Metal
131 + features, which requires macOS + Xcode to compile.
132 +- **crates.io is immutable** — once a version is published, it cannot be
133 + re-published. You must bump to the next version. `cargo yank` hides a version
134 + from dependency resolution but does not delete it.
135 +
136 +### Required secret
137 +
138 +`CARGO_REGISTRY_TOKEN` — a crates.io API token scoped to `publish-update` for
139 +the `onde` crate. Create at https://crates.io/settings/tokens. Best practice:
140 +scope it to a single crate rather than using a global token.
141 +
142 +---
143 +
144 ## CI Pipeline: Swift SDK
145
146 **Workflow:** `.github/workflows/release-sdk-swift.yml`
@@ -162,6 +208,44 @@ lockfile. If a release needs to be redone:
208
209 ---
210
211 +## CI Pipeline: npm SDK (React Native)
212 +
213 +**Workflow:** `.github/workflows/release-sdk-npm.yml`
214 +**Trigger:** Tag push matching `[0-9]+.[0-9]+.[0-9]+` or `workflow_dispatch`
215 +**Runner:** `ubuntu-latest`
216 +
217 +### Flow
218 +
219 +```
220 +Tag push (e.g. 1.0.0)
221 + │
222 + ├─ validate job:
223 + │ ├─ Set up Node.js 22
224 + │ ├─ Read version from sdk/react-native/package.json
225 + │ ├─ Validate tag == package.json version
226 + │ ├─ npm install + npm run build
227 + │ └─ npm publish --dry-run --access public
228 + │
229 + └─ publish job (tag push only, needs: validate):
230 + ├─ npm install + npm run build
231 + ├─ Check if version already exists on npm
232 + │ └─ If yes → skip publish (idempotent, exits 0)
233 + └─ npm publish --access public
234 +```
235 +
236 +### npm idempotency guard
237 +
238 +The publish step checks `npm view <package>@<version>` before publishing. If the
239 +version already exists, it **skips** with a success exit code. This makes the npm
240 +workflow safe to re-run — unlike Dart/Rust, it won't fail on a duplicate version.
241 +
242 +### Required secret
243 +
244 +`NPM_TOKEN` — a granular npm access token scoped to the `@ondeinference` org
245 +with read+write packages. Create at npmjs.com → Access Tokens.
246 +
247 +---
248 +
249 ## CI Pipeline: Dart SDK
250
251 **Workflow:** `.github/workflows/release-sdk-dart.yml`
@@ -241,9 +325,15 @@ All four must match. If any pair diverges, the relevant CI job fails.
325
326 | Secret | Used by | Purpose |
327 |--------|---------|---------|
328 +| `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. |
329 | `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. |
330 | `PUB_CREDENTIALS` | `release-sdk-dart.yml` | Authenticate with pub.dev for `flutter pub publish`. Full JSON from `~/.config/dart/pub-credentials.json`. |
331 | `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. |
332 +| `GRESIQ_API_KEY_DEV` | All release workflows | Build-time env for dev environment API key. |
333 +| `GRESIQ_API_SECRET_DEV` | All release workflows | Build-time env for dev environment API secret. |
334 +| `GRESIQ_API_KEY_PRODUCTION` | All release workflows | Build-time env for production API key. |
335 +| `GRESIQ_API_SECRET_PRODUCTION` | All release workflows | Build-time env for production API secret. |
336 +| `HF_TOKEN` | All release workflows | HuggingFace token baked in at build time (`option_env!`). |
337
338 ---
339
@@ -259,10 +349,15 @@ All four must match. If any pair diverges, the relevant CI job fails.
349 | Forgot to update `sdk/dart/CHANGELOG.md` | pub.dev shows stale changelog | Prepend new `## 0.1.3` section before tagging |
350 | Forgot to update `sdk/react-native/CHANGELOG.md` | npm shows stale changelog | Prepend new `## 0.1.3` section before tagging |
351 | Tag has `v` prefix (`v0.1.3`) | CI does not trigger — tag pattern requires bare semver | Delete the tag, re-tag without `v` |
352 +| `CARGO_REGISTRY_TOKEN` missing | `cargo publish` fails with auth error | Create a scoped token at crates.io/settings/tokens, add as repo secret |
353 | `ONDE_SWIFT_PAT` expired | `onde-swift` push fails with 403 | Regenerate PAT at github.com/settings/tokens, update repo secret |
354 | `PUB_CREDENTIALS` expired | `flutter pub publish` fails with 401 | Run `dart pub login` locally, copy new credentials JSON to repo secret |
355 | `NPM_TOKEN` expired | `npm publish` fails with 401/403 | Regenerate token at npmjs.com → Access Tokens, update repo secret |
265 -| npm version already published | `npm publish` exits with "already exists" | npm does not allow re-publishing the same version — bump to the next version |
356 +| 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. |
357 +| npm version already published | npm publish step skips (idempotency guard) | Safe — the workflow exits 0. No action needed. |
358 +| pub.dev version already published | `flutter pub publish` fails | pub.dev is immutable — bump to next version |
359 +| 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**. |
360 +| All 4 workflows fire but only 1–2 need re-publishing | Collateral re-runs of Swift/npm/Dart/Rust | Don't delete+re-push the tag. Use **Manual Publish Fallback** for the specific SDKs. |
361 | `onde-swift` tag already exists | Tag push skipped (warning emitted) | Delete remote tag first: `git push origin :refs/tags/0.1.3` |
362 | 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` |
363 | 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) |
@@ -271,6 +366,58 @@ All four must match. If any pair diverges, the relevant CI job fails.
366
367 ---
368
369 +## Manual Publish Fallback
370 +
371 +When a tag was pushed before a workflow existed on `main`, or when you need to
372 +publish only a subset of SDKs without re-triggering all four workflows:
373 +
374 +### Rust (crates.io)
375 +
376 +```bash
377 +# Dry-run first
378 +cargo publish --dry-run
379 +
380 +# Publish
381 +cargo publish
382 +```
383 +
384 +Requires `~/.cargo/credentials.toml` or `CARGO_REGISTRY_TOKEN` env var.
385 +
386 +### Dart (pub.dev)
387 +
388 +```bash
389 +cd sdk/dart
390 +
391 +# Dry-run
392 +flutter pub publish --dry-run
393 +
394 +# Publish
395 +flutter pub publish --force
396 +```
397 +
398 +Requires `~/.config/dart/pub-credentials.json` from `dart pub login`.
399 +
400 +### npm (React Native)
401 +
402 +```bash
403 +cd sdk/react-native
404 +npm run build
405 +npm publish --access public
406 +```
407 +
408 +Requires `npm login` or `NPM_TOKEN` env var.
409 +
410 +### When to use manual publish
411 +
412 +| Scenario | Approach |
413 +|----------|----------|
414 +| Tag exists but workflow wasn't on `main` yet | Merge workflow, then publish manually |
415 +| One SDK's CI failed, others succeeded | Fix the issue, publish that SDK manually |
416 +| Need to avoid re-triggering Swift XCFramework build | Publish Rust/Dart/npm manually |
417 +| Testing a new workflow before first real tag | Use `workflow_dispatch` for dry-run only |
418 +
419 +---
420 +
421 ## Re-releasing a Version (Emergency Fix)
422
423 If a release was botched and you need to re-publish the same version:
@@ -296,23 +443,36 @@ git tag 0.1.3
443 git push origin main 0.1.3
444 ```
445
299 -### Dart (pub.dev)
446 +### Dart (pub.dev) / Rust (crates.io)
447
301 -**pub.dev does not allow re-publishing the same version.** If the Dart package
302 -was already published, you must bump to `0.1.4` instead. This is a pub.dev
303 -policy — there is no workaround.
448 +**Both pub.dev and crates.io are immutable.** Once a version is published, it
449 +cannot be re-published. You must bump to `0.1.4` instead.
450 +
451 +- **crates.io:** `cargo yank --version 0.1.3` hides the version from dependency
452 + resolution but does not delete it.
453 +- **pub.dev:** No yank equivalent. The version remains visible and installable.
454
455 If the `publish` job failed (i.e. the package was NOT published), you can
456 safely re-tag after fixing the issue.
457
458 +### npm
459 +
460 +The npm workflow's idempotency guard means re-running it is safe — it skips
461 +if the version already exists. However, npm also does not allow re-publishing
462 +the same version with different content. Bump to the next version if the
463 +published content was wrong.
464 +
465 ---
466
467 ## End-to-End Release Timeline
468
469 ```
313 -Developer pushes tag 0.1.3 to onde
470 +Developer pushes tag 1.0.0 to onde
471 │
315 - ├─── release-sdk-swift.yml fires ──────────────────────────────────────┐
472 + ├─── release-sdk-rust.yml fires ──────────────────────────────────────┐
473 + │ ~2 min (dry-run + publish to crates.io) │
474 + │ │
475 + ├─── release-sdk-swift.yml fires ──────────────────────────────────────┤
476 │ ~15 min (XCFramework build) │
477 │ Creates GitHub Release on onde │
478 │ Pushes commit + tag to onde-swift │
@@ -320,32 +480,47 @@ Developer pushes tag 0.1.3 to onde
480 │ ├─ onde-swift ci.yml fires (push to main) │
481 │ │ ~5 min (validate + build 5 platforms) │
482 │ │ │
323 - │ └─ onde-swift release.yml fires (tag 0.1.3) │
483 + │ └─ onde-swift release.yml fires (tag 1.0.0) │
484 │ ~5 min (validate + create GitHub Release) │
485 │ │
486 ├─── release-sdk-dart.yml fires ───────────────────────────────────────┤
487 │ ~3 min (validate + publish to pub.dev) │
488 │ │
489 + ├─── release-sdk-npm.yml fires ───────────────────────────────────────┤
490 + │ ~3 min (validate + publish to npm) │
491 + │ │
492 └─── Swift Package Index picks up onde-swift tag ──────────────────────┘
493 ~30 min (SPI polling interval)
494 ```
495
333 -All three CI workflows (`release-sdk-swift`, `release-sdk-dart`, `onde-swift/release`)
334 -run **in parallel** from the single tag push. The only ordering dependency is
335 -that `release-sdk-swift` must create the `onde` GitHub Release (with XCFramework
336 -assets) **before** pushing the tag to `onde-swift` — this is guaranteed by the
337 -step order within the workflow.
496 +All four CI workflows fire **in parallel** from the single tag push. The only
497 +ordering dependency is that `release-sdk-swift` must create the `onde` GitHub
498 +Release (with XCFramework assets) **before** pushing the tag to `onde-swift` —
499 +this is guaranteed by the step order within the workflow.
500 +
501 +### Merge train
502 +
503 +Always merge through the branch train before tagging:
504 +
505 +```
506 +feature/* → development → main → tag
507 +```
508 +
509 +Never tag from a feature branch. The tag must point to a commit on `main` that
510 +contains all workflow files and fixes. If a workflow is added on a feature branch
511 +and the tag is pushed before merging, the workflow won't exist at the tagged
512 +commit and CI won't fire.
513
514 ---
515
516 ## Distribution Registry Reference
517
343 -| Registry | Package Name | Import | Workflow |
344 -|----------|-------------|--------|----------|
345 -| crates.io | `onde` | `onde = "0.x"` | Manual `cargo publish` |
346 -| Swift Package Index | `onde-swift` (org: `ondeinference`) | `import Onde` | `release-sdk-swift.yml` → `onde-swift/release.yml` |
347 -| pub.dev | `onde_inference` | `import 'package:onde_inference/onde_inference.dart'` | `release-sdk-dart.yml` |
348 -| npm | `@ondeinference/react-native` | `import { OndeChatEngine } from "@ondeinference/react-native"` | Manual `npm publish --access public` |
518 +| Registry | Package Name | Import | Workflow | Immutable? |
519 +|----------|-------------|--------|----------|------------|
520 +| crates.io | `onde` | `onde = "1.0"` | `release-sdk-rust.yml` | Yes (yank hides, doesn't delete) |
521 +| Swift Package Index | `onde-swift` (org: `ondeinference`) | `import Onde` | `release-sdk-swift.yml` → `onde-swift/release.yml` | Tags are immutable by convention |
522 +| pub.dev | `onde_inference` | `import 'package:onde_inference/onde_inference.dart'` | `release-sdk-dart.yml` | Yes (no yank) |
523 +| npm | `@ondeinference/react-native` | `import { OndeChatEngine } from "@ondeinference/react-native"` | `release-sdk-npm.yml` | Yes (unpublish within 72h only) |
524
525 ---
526
@@ -414,5 +589,19 @@ The `--access public` flag is required for scoped packages on first publish.
589
590 ---
591
592 +## Registry Immutability Summary
593 +
594 +All four registries are effectively immutable once a version is published:
595 +
596 +- **crates.io** — `cargo yank` removes from resolver but the tarball stays forever
597 +- **pub.dev** — No retraction mechanism at all
598 +- **npm** — `npm unpublish` works within 72 hours, but only if no dependents
599 +- **Swift (onde-swift tags)** — Convention: never force-push tags. Delete + re-create if needed.
600 +
601 +**Bottom line:** get the release right before publishing. Use `--dry-run` locally
602 +and via `workflow_dispatch` before pushing a real tag.
603 +
604 +---
605 +
606 *Update this skill when adding new SDK targets (Python, Ruby, Kotlin), changing
607 CI runners, or modifying the tag/version validation logic.*
\ No newline at end of file