onde-cli 0.3.1__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. onde_cli-0.4.1/.agents/AGENTS.md +70 -0
  2. {onde_cli-0.3.1 → onde_cli-0.4.1}/.agents/skills/distribution/SKILL.md +45 -9
  3. onde_cli-0.4.1/.agents/skills/git/SKILL.md +70 -0
  4. onde_cli-0.4.1/.github/workflows/release-crates.yml +217 -0
  5. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/release-github.yml +18 -5
  6. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/release-homebrew.yml +1 -1
  7. onde_cli-0.4.1/.github/workflows/release-mcp-registry.yml +130 -0
  8. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/release-npm.yml +2 -4
  9. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/release-nuget.yml +25 -4
  10. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/release-pub.yml +1 -113
  11. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/release-pypi.yml +2 -4
  12. onde_cli-0.4.1/AGENTS.md +70 -0
  13. onde_cli-0.4.1/CLAUDE.md +70 -0
  14. {onde_cli-0.3.1 → onde_cli-0.4.1}/Cargo.lock +1214 -695
  15. {onde_cli-0.3.1 → onde_cli-0.4.1}/Cargo.toml +9 -7
  16. {onde_cli-0.3.1 → onde_cli-0.4.1}/LICENSE +1 -1
  17. {onde_cli-0.3.1 → onde_cli-0.4.1}/PKG-INFO +3 -3
  18. {onde_cli-0.3.1 → onde_cli-0.4.1}/README.md +17 -3
  19. onde_cli-0.4.1/docs/mcp-registry.md +109 -0
  20. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/onde-cli/README.md +1 -1
  21. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/package-main.json.tmpl +1 -0
  22. {onde_cli-0.3.1 → onde_cli-0.4.1}/nuget/onde-cli/README.md +3 -1
  23. {onde_cli-0.3.1 → onde_cli-0.4.1}/pub/onde_cli/.gitignore +1 -0
  24. {onde_cli-0.3.1 → onde_cli-0.4.1}/pub/onde_cli/.pubignore +1 -0
  25. onde_cli-0.4.1/pub/onde_cli/CHANGELOG.md +15 -0
  26. {onde_cli-0.3.1 → onde_cli-0.4.1}/pub/onde_cli/LICENSE +1 -1
  27. {onde_cli-0.3.1 → onde_cli-0.4.1}/pub/onde_cli/README.md +14 -4
  28. onde_cli-0.4.1/pub/onde_cli/lib/onde_cli.dart +216 -0
  29. {onde_cli-0.3.1 → onde_cli-0.4.1}/pub/onde_cli/pubspec.yaml +2 -3
  30. {onde_cli-0.3.1 → onde_cli-0.4.1}/pypi/README.md +1 -1
  31. onde_cli-0.4.1/server.json +70 -0
  32. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/app.rs +384 -33
  33. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/chat.rs +14 -1
  34. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/finetune.rs +617 -35
  35. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/gguf.rs +493 -42
  36. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/gresiq.rs +35 -1
  37. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/hf_search.rs +3 -3
  38. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/main.rs +8 -0
  39. onde_cli-0.4.1/src/mcp.rs +336 -0
  40. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/ui.rs +105 -21
  41. onde_cli-0.3.1/.agents/skills/git/SKILL.md +0 -43
  42. onde_cli-0.3.1/.github/workflows/release-crates.yml +0 -104
  43. onde_cli-0.3.1/pub/onde_cli/CHANGELOG.md +0 -7
  44. onde_cli-0.3.1/pub/onde_cli/lib/onde_cli.dart +0 -99
  45. {onde_cli-0.3.1 → onde_cli-0.4.1}/.github/workflows/ci.yml +0 -0
  46. {onde_cli-0.3.1 → onde_cli-0.4.1}/.gitignore +0 -0
  47. {onde_cli-0.3.1 → onde_cli-0.4.1}/.nuget/NuGet/NuGet.Config +0 -0
  48. {onde_cli-0.3.1 → onde_cli-0.4.1}/.nvmrc +0 -0
  49. {onde_cli-0.3.1 → onde_cli-0.4.1}/assets/ondeinference-apps.png +0 -0
  50. {onde_cli-0.3.1 → onde_cli-0.4.1}/build.rs +0 -0
  51. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/README.md.tmpl +0 -0
  52. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/onde-cli/.gitignore +0 -0
  53. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/onde-cli/package-lock.json +0 -0
  54. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/onde-cli/package.json +0 -0
  55. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/onde-cli/src/index.ts +0 -0
  56. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/onde-cli/tsconfig.json +0 -0
  57. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/package.json.tmpl +0 -0
  58. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/scripts/render-main-package.cjs +0 -0
  59. {onde_cli-0.3.1 → onde_cli-0.4.1}/npm/scripts/render-platform-package.cjs +0 -0
  60. {onde_cli-0.3.1 → onde_cli-0.4.1}/nuget/.gitignore +0 -0
  61. {onde_cli-0.3.1 → onde_cli-0.4.1}/nuget/onde-cli/Onde.Cli.csproj +0 -0
  62. {onde_cli-0.3.1 → onde_cli-0.4.1}/nuget/onde-cli/Program.cs +0 -0
  63. {onde_cli-0.3.1 → onde_cli-0.4.1}/pub/onde_cli/bin/onde.dart +0 -0
  64. {onde_cli-0.3.1 → onde_cli-0.4.1}/pypi/pyproject.toml +0 -0
  65. {onde_cli-0.3.1 → onde_cli-0.4.1}/pyproject.toml +0 -0
  66. {onde_cli-0.3.1 → onde_cli-0.4.1}/rust-toolchain.toml +0 -0
  67. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/hf.rs +0 -0
  68. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/hf_clone.rs +0 -0
  69. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/hf_upload.rs +0 -0
  70. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/merge.rs +0 -0
  71. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/project.rs +0 -0
  72. {onde_cli-0.3.1 → onde_cli-0.4.1}/src/token.rs +0 -0
@@ -0,0 +1,70 @@
1
+ # AGENTS.md
2
+
3
+ `onde-cli` is a Rust TUI (`onde` binary) for managing an Onde Inference account and running a local model pipeline: LoRA fine-tune → adapter merge → GGUF export → local chat test → Hugging Face upload → model assignment to an Onde app. npm, PyPI, NuGet, pub.dev, Homebrew, and crates.io are thin wrappers around this one binary; the Rust crate is the source of truth.
4
+
5
+ ## Build, test, lint
6
+
7
+ ```sh
8
+ cargo build # debug build
9
+ cargo run # launch the TUI
10
+ cargo run -- --mcp # run as an MCP server over stdio (no TUI)
11
+ cargo test --locked # unit tests (heavy ones are #[ignore])
12
+ cargo clippy --all-targets -- -D warnings
13
+ cargo fmt -- --check
14
+ ```
15
+
16
+ The build **requires** these env vars (read from `.env` via `build.rs`): `ONDE_APP_ID`, `ONDE_APP_SECRET`, `GRESIQ_API_KEY`, `GRESIQ_API_SECRET`. `HF_TOKEN` is optional. They are baked in at compile time via `env!(...)` in `src/app.rs`. Changing `.env` triggers a full rebuild.
17
+
18
+ On macOS, candle's `metal` + `accelerate` features are added automatically. **Fine-tuning always runs on CPU** — Metal's backward pass in candle 0.10.2 produces corrupt gradients (1e6–1e29 in magnitude, where CPU gives ~40–65 for identical losses). Use `ONDE_FINETUNE_METAL=1` only to test newer candle versions, and verify with `ONDE_FINETUNE_GRAD_DEBUG=1` (per-step loss/max_abs/norm).
19
+
20
+ Linux CI requires `libclang-dev` (`sudo apt-get install -y libclang-dev`).
21
+
22
+ ### Tests that load real models
23
+
24
+ `src/gguf.rs` has two `#[ignore]` integration tests that export a ~1GB GGUF and run inference through `onde::mistralrs`. They need a Qwen3-0.6B model cached in the Onde App Group container and skip gracefully when it is absent.
25
+
26
+ ```sh
27
+ cargo test gguf::tests::exported_qwen3_gguf_is_runnable -- --ignored --nocapture
28
+ cargo test gguf::tests::finetune_merge_export_run -- --ignored --nocapture
29
+ ```
30
+
31
+ Env knobs for these: `ONDE_TEST_MODEL_DIR`, `ONDE_TEST_DTYPE` (`f16`/`q8_0`), `ONDE_TEST_LR`, `ONDE_TEST_DATA_PATH`, `ONDE_TEST_EPOCHS`, `ONDE_TEST_MAX_SEQ_LEN`, `ONDE_TEST_EVAL_PROMPT` (`||`-separated prompts), `ONDE_TEST_SYSTEM_PROMPT`. This makes the full-pipeline test double as a headless fine-tune runner for a custom dataset.
32
+
33
+ ### Debugging the TUI
34
+
35
+ `main.rs` redirects both stdout and stderr to `~/.cache/onde/debug.log` before ratatui takes the alternate screen, because `mistral.rs` writes to both fds and would otherwise tear up the display. To see what the app or the inference engine is doing, tail that log; `println!`/`eprintln!`/`log::*` all land there, not on screen.
36
+
37
+ ## Architecture — `src/`
38
+
39
+ | File(s) | Responsibility |
40
+ |---|---|
41
+ | `main.rs` | Entry point; redirects stdout/stderr to `~/.cache/onde/debug.log` before ratatui takes over |
42
+ | `app.rs` | `App` struct + `Screen` state machine + `AuthEvent` channel (`tokio::select!` over an `mpsc` channel and a `crossterm` `EventStream`); `App::apply` folds background progress into state |
43
+ | `ui.rs` | Pure ratatui render of `App`; no SDK imports (re-exports types through `app.rs`) |
44
+ | `finetune.rs` | Hand-written LoRA trainer (candle, CPU): Qwen forward pass (RMSNorm, RoPE, GQA, optional Qwen3 QK-norm), LoRA A/B trained on q/v projections in F32, writes `lora_adapter.safetensors`. Gradients are sanitized (non-finite elements zeroed) and globally norm-clipped in a scaled space before each AdamW step; a step is skipped (loudly) only when the norm is zero or non-finite. **Skipping this guard corrupts every weight after the first bad step.** |
45
+ | `merge.rs` | Folds LoRA adapter into base weights (`W + scale·(B@A)`); writes `model.safetensors` plus copied config/tokenizer |
46
+ | `gguf.rs` | Hand-rolled GGUF writer (no llama.cpp). Tensor dims are written innermost-first — reverse of safetensors shape, because candle reverses on read; `head_dim` comes from config and is emitted as `attention.key_length`/`value_length` (Qwen3 decouples it from `hidden_size/num_heads`); `token_type` is an INT32 array; `general.architecture` is chosen from `model_type` so Qwen3 routes to candle's `quantized_qwen3` loader |
47
+ | `chat.rs` | Local GGUF chat via `onde::mistralrs::GgufModelBuilder`, the same engine the Onde SDK uses, so a model can be tested before publishing |
48
+ | `hf_upload.rs` / `hf_clone.rs` / `hf_search.rs` / `hf.rs` | Hugging Face Hub: upload the GGUF, check/create a repo, search models, and resolve/merge the local HF cache (incl. the macOS Onde App Group container) |
49
+ | `gresiq.rs` | `smbcloud-gresiq-sdk` wrapper for apps and the model catalog. "Deploying" a model means `assign_model(app_id, model_id)` against a catalog entry; the end app then fetches that assignment through the Onde SDK's `load_assigned_model` and downloads the GGUF. The CLI can only assign models that already exist in the GresIQ catalog |
50
+ | `token.rs` | Auth token persistence |
51
+ | `project.rs` | Per-project fine-tune workspaces under `~/.onde` |
52
+
53
+ **Background task rule:** network/IO → `tokio::spawn`; CPU-heavy tensor work (`finetune`, `merge`, `gguf`) → `std::thread::spawn` to avoid starving the async runtime.
54
+
55
+ **Adding a feature pattern:** new `Screen` variant → key handler → `AuthEvent` variant → background task that streams progress events.
56
+
57
+ ### The `onde` dependency
58
+
59
+ The `onde` crate provides the inference engine (`onde::mistralrs`, a vendored mistral.rs) and `onde::inference::models::SUPPORTED_MODEL_INFO` (the supported-model catalog the inference picker mirrors). It is normally the published crate; `Cargo.toml` has commented `path`/`[patch.crates-io]` blocks for developing against local checkouts of `onde`, the `smbcloud-*` crates, and `candle` — never commit with those uncommented.
60
+
61
+ ## Conventions
62
+
63
+ - **Git:** merge feature/release/hotfix branches with `--no-ff`. Tag the merge commit on `main`. Never tag a branch tip that hasn't been merged. See `.agents/skills/git/SKILL.md`.
64
+ - **Commits:** include `Co-Authored-By: siGit Code <297239231+sigitc@users.noreply.github.com>` trailer when committing with the agent.
65
+ - **`[patch.crates-io]` and `path =` deps:** commented out in `Cargo.toml` for local dev against `onde`/`smbcloud-*`/`candle`. Never commit with these uncommented.
66
+ - **Distribution:** before touching any wrapper package (npm, PyPI, NuGet, pub.dev, Homebrew), read `.agents/skills/distribution/SKILL.md`. Keep all channel versions aligned with the Rust crate version in `Cargo.toml`.
67
+ - **MCP Registry:** the listing's ownership markers ship inside the published npm and NuGet artifacts, so a version published without them can never be listed. See `docs/mcp-registry.md` before touching `server.json` or either marker.
68
+ - **Wrapper packages** are in `npm/`, `pypi/`, `nuget/`, `pub/`. The npm base manifest is rewritten at release time by `npm/scripts/render-main-package.cjs`; do not hand-edit committed package.json versions as the source of truth for releases.
69
+ - **Clippy** runs with `-D warnings` in CI; all lints must pass.
70
+ - **Rust edition:** 2024; toolchain: stable.
@@ -82,14 +82,16 @@ Good fit for .NET developers and CI environments that already use `dotnet tool i
82
82
 
83
83
  ### pub.dev
84
84
 
85
- Use a Dart package with a command entrypoint and a library that launches the bundled native binary.
85
+ Use a Dart package with a command entrypoint and a library that launches the native binary.
86
86
 
87
87
  Important details:
88
88
 
89
89
  - executable command should stay `onde`
90
- - use `Isolate.resolvePackageUri` to find the installed package path
91
90
  - keep the runtime-id mapping explicit
92
- - do not overcomplicate the wrapper with platform-specific Dart APIs unless necessary
91
+ - download the correct binary from GitHub Releases on first run
92
+ - cache downloads under `~/.onde/cli`
93
+ - keep the wrapper thin, the Rust binary is still the product
94
+ - publish the pub.dev package only after the matching GitHub Release exists
93
95
 
94
96
  Good fit for Dart and Flutter developers who want the same CLI through familiar tooling.
95
97
 
@@ -108,6 +110,34 @@ If a wrapper uses one naming scheme and a workflow uploads another, the installe
108
110
 
109
111
  ## Release workflow principles
110
112
 
113
+ ### One tag trigger, then fan out
114
+
115
+ `release-crates.yml` is the only workflow that reacts to a tag push. It verifies the
116
+ tag against `Cargo.toml` and `pubspec.yaml`, publishes the crate, then dispatches
117
+ `release-github`, `release-npm`, `release-nuget`, and `release-pypi` with the same tag.
118
+ `release-github` in turn dispatches `release-homebrew` and `release-pub` after the
119
+ release assets exist.
120
+
121
+ ```
122
+ tag push
123
+ └─ release-crates (verify -> publish -> dispatch)
124
+ ├─ release-github ─┬─ release-homebrew
125
+ │ └─ release-pub
126
+ ├─ release-npm
127
+ ├─ release-nuget
128
+ └─ release-pypi
129
+ ```
130
+
131
+ This mirrors `smbcloud-cli`. Two properties matter:
132
+
133
+ - crates.io is the gate. If verify or publish fails, no wrapper publishes a version
134
+ that has no crate behind it.
135
+ - every dispatch passes `ref: <tag>`, not `ref: main`, so downstream workflows run the
136
+ workflow definition from the release state instead of whatever `main` happens to be.
137
+
138
+ Do not add `push: tags:` to a wrapper workflow. Several workflows racing on the same tag
139
+ is what lets the pub.dev launcher publish before its GitHub Release assets exist.
140
+
111
141
  ### Build first, package second
112
142
 
113
143
  Always build native binaries in a target matrix first.
@@ -141,14 +171,18 @@ Safer checks:
141
171
 
142
172
  pub.dev behaves differently from npm and NuGet in one important way:
143
173
 
144
- - files ignored by git are generally excluded from publish input
174
+ - large bundled binaries quickly hit package size limits
145
175
 
146
- That means staged native binaries for `pub/onde_cli/native/` must be available to the package at publish time.
176
+ That makes the safest design a thin Dart launcher that downloads the right native binary from GitHub Releases at runtime instead of shipping every platform binary inside the package.
147
177
 
148
- Two practical consequences:
178
+ Practical consequences:
149
179
 
150
- - do not ignore `pub/onde_cli/native/` in the root `.gitignore` if CI stages binaries there
151
- - a `.pubignore` can hide local tool noise, but do not hide the native payload you intend to publish
180
+ - `pub/onde_cli/native/` should stay out of the published package
181
+ - do not set `publish_to: https://pub.dev` in `pubspec.yaml`; omit `publish_to` for normal pub.dev publishing
182
+ - use package-level ignore rules so local native staging does not leak into publish input
183
+ - keep release asset names aligned with the GitHub Release workflow, because the launcher depends on them
184
+ - treat `~/.onde/cli` as the local cache root for downloaded binaries
185
+ - do not let the pub.dev workflow race ahead of GitHub Releases; dispatch it after `release-github.yml` has created the assets
152
186
 
153
187
  Run these during verification:
154
188
 
@@ -211,7 +245,9 @@ At minimum, check:
211
245
  - manually bumping the committed npm manifest and assuming that is what npm publishes
212
246
  - staged native binaries land in the wrong directory before packaging
213
247
  - package builds successfully but installs without the native binary
214
- - pub.dev package omits staged assets because gitignore excludes them
248
+ - pub.dev launcher points at the wrong GitHub Release asset name or runtime id
249
+ - pub.dev workflow publishes before the matching GitHub Release assets exist
250
+ - pub.dev package still includes bundled binaries and blows the upload size limit
215
251
  - smoke test invokes a TUI-first binary in a way that does not make sense for CI
216
252
  - README badges and install commands drift out of sync with real package names
217
253
 
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: git
3
+ description: Git workflow rules for this repository.
4
+ allowed-tools: Read, Write, Edit, Glob, Grep, AskUserQuestion
5
+ user-invocable: true
6
+ ---
7
+
8
+ # Git skill
9
+
10
+ ## Core rule
11
+
12
+ - Use `--no-ff`. Important.
13
+
14
+ ## What this means in practice
15
+
16
+ - Do not fast-forward merge feature branches, release branches, or hotfix branches.
17
+ - Prefer explicit merge commits so the branch history stays visible.
18
+ - Release prep goes through `development` on its way to `main`, with `--no-ff` at every hop.
19
+ - Tag the merge commit that contains the final release state.
20
+
21
+ ## Branch flow
22
+
23
+ Feature branches merge into `development`. Releases go:
24
+
25
+ ```
26
+ release/vX.Y.Z -> development -> main
27
+ ```
28
+
29
+ Never merge a release branch straight into `main` — `development` has to carry the
30
+ release state too, or the next feature branch starts from a base that is missing it.
31
+
32
+ ## Release guidance
33
+
34
+ ```bash
35
+ # 1. cut the release branch from development, commit the version bumps
36
+ git checkout development
37
+ git checkout -b release/vX.Y.Z
38
+ git commit -am "Prepare vX.Y.Z"
39
+
40
+ # 2. release branch -> development
41
+ git checkout development
42
+ git merge --no-ff release/vX.Y.Z -m "Merge release prep for vX.Y.Z"
43
+
44
+ # 3. development -> main
45
+ git checkout main
46
+ git merge --no-ff development -m "Merge development for vX.Y.Z"
47
+
48
+ # 4. tag the main merge commit, then push
49
+ git tag vX.Y.Z
50
+ git push origin development
51
+ git push origin main
52
+ git push origin vX.Y.Z
53
+ ```
54
+
55
+ The tag belongs on the step-3 merge commit on `main`, not on the release branch and not
56
+ on `development`. Pushing the tag is what triggers the wrapper publish workflows, which
57
+ read the version from the tag.
58
+
59
+ If the work is already on `main`, make a normal commit and tag that commit. Do not rewrite history just to avoid a merge commit.
60
+
61
+ ## Avoid
62
+
63
+ - `git merge --ff`
64
+ - `git pull --ff-only` as a default recommendation for this repo's merge policy
65
+ - Tagging a branch tip that has not been merged into `main` when the intended release source of truth is `main`
66
+ - Merging a release branch directly into `main`, skipping `development`
67
+
68
+ ## Why
69
+
70
+ The repo wants explicit history. Release commits should be easy to find, easy to audit, and clearly connected to the branch that introduced them.
@@ -0,0 +1,217 @@
1
+ name: Release crates.io
2
+
3
+ # This is the only workflow that reacts to a tag push. It verifies the tag,
4
+ # publishes the crate, and then dispatches every distribution workflow with the
5
+ # same tag. Wrappers never publish for a crate release that failed.
6
+
7
+ on:
8
+ push:
9
+ tags:
10
+ - "v*.*.*"
11
+ workflow_dispatch:
12
+ inputs:
13
+ tag:
14
+ description: "Release tag (e.g. v0.4.0)"
15
+ required: true
16
+
17
+ permissions:
18
+ contents: read
19
+ actions: write
20
+
21
+ concurrency:
22
+ group: release-crates-${{ github.ref }}
23
+ cancel-in-progress: false
24
+
25
+ env:
26
+ CARGO_TERM_COLOR: always
27
+ ONDE_APP_ID: ${{ secrets.ONDE_APP_ID }}
28
+ ONDE_APP_SECRET: ${{ secrets.ONDE_APP_SECRET }}
29
+ GRESIQ_API_KEY: ${{ secrets.GRESIQ_API_KEY }}
30
+ GRESIQ_API_SECRET: ${{ secrets.GRESIQ_API_SECRET }}
31
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
32
+
33
+ jobs:
34
+ verify:
35
+ name: Verify release
36
+ runs-on: ubuntu-latest
37
+ outputs:
38
+ version: ${{ steps.release.outputs.version }}
39
+ tag: ${{ steps.release.outputs.tag }}
40
+ steps:
41
+ - name: Checkout
42
+ uses: actions/checkout@v6
43
+ with:
44
+ ref: ${{ github.event.inputs.tag || github.ref }}
45
+
46
+ - name: Read Rust toolchain
47
+ shell: bash
48
+ run: |
49
+ rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
50
+ if [ -z "$rust_toolchain" ]; then
51
+ echo "Failed to read Rust toolchain from rust-toolchain.toml" >&2
52
+ exit 1
53
+ fi
54
+ echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
55
+
56
+ - name: Install Rust toolchain
57
+ uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
58
+ with:
59
+ toolchain: ${{ env.RUST_TOOLCHAIN }}
60
+ components: clippy, rustfmt
61
+
62
+ - name: Cache cargo registry and build artifacts
63
+ uses: Swatinem/rust-cache@v2
64
+
65
+ - name: Set the release version
66
+ id: release
67
+ shell: bash
68
+ run: |
69
+ if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
70
+ release_tag="${GITHUB_REF_NAME}"
71
+ else
72
+ release_tag="${{ github.event.inputs.tag }}"
73
+ release_tag="${release_tag#refs/tags/}"
74
+ fi
75
+ release_version="${release_tag#v}"
76
+
77
+ echo "tag=${release_tag}" >> "$GITHUB_OUTPUT"
78
+ echo "version=${release_version}" >> "$GITHUB_OUTPUT"
79
+
80
+ - name: Ensure tag matches Cargo.toml version
81
+ shell: bash
82
+ run: |
83
+ version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)"
84
+ if [ -z "$version" ]; then
85
+ echo "::error::Failed to read package version from Cargo.toml"
86
+ exit 1
87
+ fi
88
+ if [ "$version" != "${{ steps.release.outputs.version }}" ]; then
89
+ echo "::error::Cargo.toml is ${version} but the tag is ${{ steps.release.outputs.version }}"
90
+ exit 1
91
+ fi
92
+
93
+ - name: Ensure the pub.dev wrapper matches the tag
94
+ shell: bash
95
+ run: |
96
+ pub_version="$(sed -n 's/^version: \(.*\)/\1/p' pub/onde_cli/pubspec.yaml | head -n 1)"
97
+ if [ "$pub_version" != "${{ steps.release.outputs.version }}" ]; then
98
+ echo "::error::pubspec.yaml is ${pub_version} but the tag is ${{ steps.release.outputs.version }}"
99
+ exit 1
100
+ fi
101
+
102
+ - name: Check formatting
103
+ run: cargo fmt --all -- --check
104
+
105
+ - name: Run clippy
106
+ run: cargo clippy --all-targets --all-features -- -D warnings
107
+
108
+ - name: Run tests
109
+ run: cargo test --all-features --locked --verbose
110
+
111
+ - name: Run cargo check
112
+ run: cargo check --locked
113
+
114
+ - name: Verify package contents
115
+ run: cargo package --locked --allow-dirty
116
+
117
+ publish:
118
+ name: Publish to crates.io
119
+ runs-on: ubuntu-latest
120
+ needs: verify
121
+ environment:
122
+ name: crates-io
123
+ permissions:
124
+ contents: read
125
+ steps:
126
+ - name: Checkout
127
+ uses: actions/checkout@v6
128
+ with:
129
+ ref: ${{ github.event.inputs.tag || github.ref }}
130
+
131
+ - name: Read Rust toolchain
132
+ shell: bash
133
+ run: |
134
+ rust_toolchain="$(sed -n 's/^channel = "\(.*\)"/\1/p' rust-toolchain.toml | head -n 1)"
135
+ echo "RUST_TOOLCHAIN=${rust_toolchain}" >> "$GITHUB_ENV"
136
+
137
+ - name: Install Rust toolchain
138
+ uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
139
+ with:
140
+ toolchain: ${{ env.RUST_TOOLCHAIN }}
141
+
142
+ - name: Cache cargo registry and build artifacts
143
+ uses: Swatinem/rust-cache@v2
144
+
145
+ - name: Publish crate
146
+ env:
147
+ CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
148
+ shell: bash
149
+ run: |
150
+ version="${{ needs.verify.outputs.version }}"
151
+ if curl -sSf "https://crates.io/api/v1/crates/onde-cli/${version}" >/dev/null 2>&1; then
152
+ echo "onde-cli ${version} is already on crates.io, skipping publish"
153
+ exit 0
154
+ fi
155
+ cargo publish --locked
156
+
157
+ dispatch-distributions:
158
+ name: Trigger distribution workflows
159
+ needs: [verify, publish]
160
+ runs-on: ubuntu-latest
161
+
162
+ steps:
163
+ - name: Trigger GitHub release
164
+ uses: actions/github-script@v7
165
+ with:
166
+ script: |
167
+ await github.rest.actions.createWorkflowDispatch({
168
+ owner: context.repo.owner,
169
+ repo: context.repo.repo,
170
+ workflow_id: 'release-github.yml',
171
+ ref: '${{ needs.verify.outputs.tag }}',
172
+ inputs: {
173
+ tag: '${{ needs.verify.outputs.tag }}'
174
+ }
175
+ })
176
+
177
+ - name: Trigger npm release
178
+ uses: actions/github-script@v7
179
+ with:
180
+ script: |
181
+ await github.rest.actions.createWorkflowDispatch({
182
+ owner: context.repo.owner,
183
+ repo: context.repo.repo,
184
+ workflow_id: 'release-npm.yml',
185
+ ref: '${{ needs.verify.outputs.tag }}',
186
+ inputs: {
187
+ tag: '${{ needs.verify.outputs.tag }}'
188
+ }
189
+ })
190
+
191
+ - name: Trigger NuGet release
192
+ uses: actions/github-script@v7
193
+ with:
194
+ script: |
195
+ await github.rest.actions.createWorkflowDispatch({
196
+ owner: context.repo.owner,
197
+ repo: context.repo.repo,
198
+ workflow_id: 'release-nuget.yml',
199
+ ref: '${{ needs.verify.outputs.tag }}',
200
+ inputs: {
201
+ tag: '${{ needs.verify.outputs.tag }}'
202
+ }
203
+ })
204
+
205
+ - name: Trigger PyPI release
206
+ uses: actions/github-script@v7
207
+ with:
208
+ script: |
209
+ await github.rest.actions.createWorkflowDispatch({
210
+ owner: context.repo.owner,
211
+ repo: context.repo.repo,
212
+ workflow_id: 'release-pypi.yml',
213
+ ref: '${{ needs.verify.outputs.tag }}',
214
+ inputs: {
215
+ tag: '${{ needs.verify.outputs.tag }}'
216
+ }
217
+ })
@@ -1,13 +1,11 @@
1
1
  name: GitHub Release
2
2
 
3
+ # Dispatched by release-crates.yml once the crate publish succeeds.
3
4
  on:
4
- push:
5
- tags:
6
- - "v*.*.*"
7
5
  workflow_dispatch:
8
6
  inputs:
9
7
  tag:
10
- description: "Release tag (e.g. v0.3.1)"
8
+ description: "Release tag (e.g. v0.4.0)"
11
9
  required: true
12
10
 
13
11
  permissions:
@@ -184,9 +182,24 @@ jobs:
184
182
  owner: context.repo.owner,
185
183
  repo: context.repo.repo,
186
184
  workflow_id: 'release-homebrew.yml',
187
- ref: 'main',
185
+ ref: '${{ steps.tag.outputs.tag }}',
188
186
  inputs: {
189
187
  tag: '${{ steps.tag.outputs.tag }}'
190
188
  }
191
189
  })
192
190
  console.log('Dispatched release-homebrew.yml for tag ${{ steps.tag.outputs.tag }}')
191
+
192
+ - name: Trigger pub.dev release
193
+ uses: actions/github-script@v7
194
+ with:
195
+ script: |
196
+ await github.rest.actions.createWorkflowDispatch({
197
+ owner: context.repo.owner,
198
+ repo: context.repo.repo,
199
+ workflow_id: 'release-pub.yml',
200
+ ref: '${{ steps.tag.outputs.tag }}',
201
+ inputs: {
202
+ tag: '${{ steps.tag.outputs.tag }}'
203
+ }
204
+ })
205
+ console.log('Dispatched release-pub.yml for tag ${{ steps.tag.outputs.tag }}')
@@ -4,7 +4,7 @@ on:
4
4
  workflow_dispatch:
5
5
  inputs:
6
6
  tag:
7
- description: "Release tag (e.g. v0.3.1)"
7
+ description: "Release tag (e.g. v0.4.0)"
8
8
  required: true
9
9
 
10
10
  permissions:
@@ -0,0 +1,130 @@
1
+ name: MCP Registry Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ tag:
7
+ description: "Release tag (e.g. v0.4.1)"
8
+ required: true
9
+
10
+ # OIDC is how we authenticate to the MCP Registry: the registry trusts a GitHub
11
+ # Actions token issued for this repository, so no long-lived secret is needed.
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+
16
+ jobs:
17
+ publish:
18
+ name: Publish server.json to the MCP Registry
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ # Unlike the other release workflows, this one builds nothing from source:
22
+ # it uploads metadata. So it checks out the branch it was dispatched on
23
+ # rather than the tag — `server.json` may have landed after the tag was
24
+ # cut — and uses the tag input only to pick the version to publish.
25
+ - name: Checkout
26
+ uses: actions/checkout@v6
27
+
28
+ - name: Set the release version
29
+ shell: bash
30
+ run: |
31
+ release_version="${{ github.event.inputs.tag }}"
32
+ release_version="${release_version#v}"
33
+
34
+ echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
35
+
36
+ - name: Check the server name is consistent across packages
37
+ shell: bash
38
+ run: |
39
+ server_name="$(jq -r '.name' server.json)"
40
+ echo "Server name: ${server_name}"
41
+
42
+ # npm proves ownership through `mcpName` in the published package.json,
43
+ # rendered at release time from this template.
44
+ grep -q "\"mcpName\": \"${server_name}\"" npm/package-main.json.tmpl
45
+
46
+ # NuGet proves ownership through an `mcp-name:` marker in the package README.
47
+ grep -q "mcp-name: ${server_name}" nuget/onde-cli/README.md
48
+
49
+ - name: Set the version in server.json
50
+ shell: bash
51
+ run: |
52
+ jq --arg v "${RELEASE_VERSION}" \
53
+ '.version = $v | .packages |= map(.version = $v)' \
54
+ server.json > server.tmp
55
+ mv server.tmp server.json
56
+ cat server.json
57
+
58
+ - name: Verify the published packages carry the ownership marker
59
+ shell: bash
60
+ run: |
61
+ # The registry stores metadata only, and validates it by fetching each
62
+ # listed package and looking for a marker naming this server. Checking
63
+ # the sources isn't enough: a package published before the marker
64
+ # landed will still be missing it. Both comparisons are case-sensitive.
65
+ server_name="$(jq -r '.name' server.json)"
66
+
67
+ # npm is queryable within seconds of publishing, but the npm release
68
+ # workflow may still be building its platform packages when this runs.
69
+ for attempt in $(seq 1 20); do
70
+ if npm view "@ondeinference/cli@${RELEASE_VERSION}" version; then
71
+ break
72
+ fi
73
+ if [ "${attempt}" -eq 20 ]; then
74
+ echo "@ondeinference/cli@${RELEASE_VERSION} is still not on npm after 10 minutes." >&2
75
+ exit 1
76
+ fi
77
+ echo "Waiting for @ondeinference/cli@${RELEASE_VERSION} on npm (attempt ${attempt}/20)..."
78
+ sleep 30
79
+ done
80
+
81
+ npm_mcp_name="$(npm view "@ondeinference/cli@${RELEASE_VERSION}" mcpName)"
82
+ if [ "${npm_mcp_name}" != "${server_name}" ]; then
83
+ echo "@ondeinference/cli@${RELEASE_VERSION} declares mcpName '${npm_mcp_name}', expected '${server_name}'." >&2
84
+ echo "npm versions are immutable — cut a new release with the marker in place." >&2
85
+ exit 1
86
+ fi
87
+
88
+ # nuget.org validates a package before it reaches the flat container,
89
+ # so a version pushed minutes ago is not queryable yet. Poll instead
90
+ # of failing the release on that race.
91
+ for attempt in $(seq 1 20); do
92
+ nuget_versions="$(curl -fsSL "https://api.nuget.org/v3-flatcontainer/onde.cli/index.json")"
93
+ if echo "${nuget_versions}" | jq -e --arg v "${RELEASE_VERSION}" '.versions | index($v)' >/dev/null; then
94
+ echo "Onde.Cli ${RELEASE_VERSION} is indexed."
95
+ break
96
+ fi
97
+ if [ "${attempt}" -eq 20 ]; then
98
+ echo "Onde.Cli ${RELEASE_VERSION} is still not indexed on nuget.org after 10 minutes." >&2
99
+ exit 1
100
+ fi
101
+ echo "Waiting for nuget.org to index ${RELEASE_VERSION} (attempt ${attempt}/20)..."
102
+ sleep 30
103
+ done
104
+
105
+ curl -fsSL -o package.nupkg \
106
+ "https://api.nuget.org/v3-flatcontainer/onde.cli/${RELEASE_VERSION}/onde.cli.${RELEASE_VERSION}.nupkg"
107
+ if ! unzip -p package.nupkg README.md | grep -q "mcp-name: ${server_name}"; then
108
+ echo "Onde.Cli ${RELEASE_VERSION} README is missing the 'mcp-name: ${server_name}' marker." >&2
109
+ exit 1
110
+ fi
111
+ rm package.nupkg
112
+
113
+ - name: Install mcp-publisher
114
+ shell: bash
115
+ run: |
116
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
117
+
118
+ - name: Authenticate to the MCP Registry
119
+ shell: bash
120
+ run: ./mcp-publisher login github-oidc
121
+
122
+ - name: Publish
123
+ shell: bash
124
+ run: ./mcp-publisher publish
125
+
126
+ - name: Verify the server is listed
127
+ shell: bash
128
+ run: |
129
+ server_name="$(jq -r '.name' server.json)"
130
+ curl -fsSL "https://registry.modelcontextprotocol.io/v0.1/servers?search=${server_name}" | jq .
@@ -1,13 +1,11 @@
1
1
  name: NPM Release
2
2
 
3
+ # Dispatched by release-crates.yml once the crate publish succeeds.
3
4
  on:
4
- push:
5
- tags:
6
- - "v*.*.*"
7
5
  workflow_dispatch:
8
6
  inputs:
9
7
  tag:
10
- description: "Release tag (e.g. v0.3.1)"
8
+ description: "Release tag (e.g. v0.4.0)"
11
9
  required: true
12
10
 
13
11
  permissions: