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