diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index ad1dd99..c4aadd1 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -122,6 +122,25 @@ jobs: echo "Archive: ${ARCHIVE_NAME}" echo "SHA256: $(cat "${ARCHIVE_NAME}.sha256")" + # Scoop and winget consume the raw Windows binaries rather than a + # tarball, and both need a SHA256 sidecar. Windows runners use bash from + # Git for Windows, which has sha256sum; macOS only has shasum. + - name: Checksum release assets + shell: bash + run: | + cd release + for asset in *; do + case "${asset}" in *.sha256) continue ;; esac + + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "${asset}" | awk '{print $1}' > "${asset}.sha256" + else + shasum -a 256 "${asset}" | awk '{print $1}' > "${asset}.sha256" + fi + + echo "${asset}: $(cat "${asset}.sha256")" + done + - name: Upload binary artifact uses: actions/upload-artifact@v4 with: @@ -203,3 +222,35 @@ jobs: } }) console.log('Dispatched release-pub.yml for tag ${{ steps.tag.outputs.tag }}') + + # These channels publish outside this repo (a Scoop bucket, + # microsoft/winget-pkgs) and read checksums off the release created + # above, so they hang off this job rather than firing on the tag + # directly. + - name: Trigger OS package manager releases + uses: actions/github-script@v7 + with: + script: | + const tag = '${{ steps.tag.outputs.tag }}' + const workflows = [ + 'release-scoop.yml', + 'release-winget.yml', + ] + + for (const workflow_id of workflows) { + try { + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id, + ref: tag, + inputs: { tag }, + }) + console.log(`Dispatched ${workflow_id} for tag ${tag}`) + } catch (error) { + // One packaging channel being unconfigured (a missing secret, + // a bucket repo that does not exist yet) should not take the + // rest of the fan-out down with it. + core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`) + } + } diff --git a/.github/workflows/release-scoop.yml b/.github/workflows/release-scoop.yml new file mode 100644 index 0000000..7fe2e7f --- /dev/null +++ b/.github/workflows/release-scoop.yml @@ -0,0 +1,148 @@ +name: Scoop Release + +# Dispatched by release-github.yml once the release and its checksums exist. +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g. v0.4.0)" + required: true + +concurrency: + # A re-pushed or re-dispatched tag fires this a second time. Queue the + # duplicate behind the original instead of cancelling it: cancelling a run + # mid-publish can leave a channel half-uploaded, whereas a queued duplicate + # just hits the "already published" check and exits clean. + group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +env: + REPO: ondeinference/onde-cli + +jobs: + update-scoop-bucket: + name: Update Scoop bucket + runs-on: ubuntu-latest + + steps: + - name: Resolve tag and version + id: release + shell: bash + run: | + TAG="${{ github.event.inputs.tag }}" + VERSION="${TAG#v}" + + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Read SHA256 checksums from release + id: sha + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + mkdir -p artifacts + + gh release download "${{ steps.release.outputs.tag }}" \ + --repo "${{ env.REPO }}" \ + --pattern "onde-win-*.exe.sha256" \ + --dir artifacts/ + + AMD64_SHA=$(cat artifacts/onde-win-amd64.exe.sha256) + ARM64_SHA=$(cat artifacts/onde-win-arm64.exe.sha256) + + echo "amd64=${AMD64_SHA}" >> "$GITHUB_OUTPUT" + echo "arm64=${ARM64_SHA}" >> "$GITHUB_OUTPUT" + + echo "AMD64 SHA256: ${AMD64_SHA}" + echo "ARM64 SHA256: ${ARM64_SHA}" + + - name: Checkout Scoop bucket + uses: actions/checkout@v6 + with: + repository: ondeinference/scoop-bucket + token: ${{ secrets.SCOOP_BUCKET_TOKEN }} + path: scoop-bucket + + - name: Generate manifest + shell: bash + run: | + VERSION="${{ steps.release.outputs.version }}" + TAG="${{ steps.release.outputs.tag }}" + AMD64_SHA="${{ steps.sha.outputs.amd64 }}" + ARM64_SHA="${{ steps.sha.outputs.arm64 }}" + + mkdir -p scoop-bucket/bucket + + # The `#/onde.exe` URL fragment is Scoop's rename-on-download + # syntax: the release asset is onde-win-amd64.exe, but the shim has + # to end up as onde.exe for `onde` to work on PATH. + cat > scoop-bucket/bucket/onde.json <&2 + exit 1 + fi + + for field in version bin architecture; do + if [ "$(jq -r "has(\"${field}\")" scoop-bucket/bucket/onde.json)" != "true" ]; then + echo "::error::Scoop manifest is missing the '${field}' field" >&2 + exit 1 + fi + done + + - name: Commit and push + shell: bash + run: | + VERSION="${{ steps.release.outputs.version }}" + + cd scoop-bucket + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add bucket/onde.json + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "Update onde to ${VERSION}" + git push diff --git a/.github/workflows/release-winget.yml b/.github/workflows/release-winget.yml new file mode 100644 index 0000000..082efc3 --- /dev/null +++ b/.github/workflows/release-winget.yml @@ -0,0 +1,178 @@ +name: winget Release + +# Dispatched by release-github.yml once the release and its checksums exist. +# +# winget manifests live in microsoft/winget-pkgs, and `wingetcreate update` +# only works on a package that is already there. So this workflow branches: +# +# - package already in winget-pkgs -> `wingetcreate update`, which rewrites +# the URLs and re-hashes the installers for us. +# - package not there yet -> render the manifests in packaging/winget/ and +# `wingetcreate submit` them as a new package. +# +# The second path only runs once, but keeping it here means a first release +# does not need someone to sit at a Windows box running `wingetcreate new` +# interactively. +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag (e.g. v0.4.0)" + required: true + +concurrency: + # A re-pushed or re-dispatched tag fires this a second time. Queue the + # duplicate behind the original instead of cancelling it: cancelling a run + # mid-publish can leave a channel half-updated, whereas a queued duplicate + # just hits the "already published" check and exits clean. + group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + +permissions: + contents: read + +env: + REPO: ondeinference/onde-cli + PACKAGE_IDENTIFIER: OndeInference.onde-cli + +jobs: + submit-winget-manifest: + name: Submit winget manifest + # wingetcreate is a Windows-only tool. + runs-on: windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Resolve tag, version and release date + id: release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${{ github.event.inputs.tag }}" + VERSION="${TAG#v}" + # wingetcreate wants ReleaseDate as YYYY-MM-DD. + DATE=$(gh release view "$TAG" --repo "$REPO" --json publishedAt --jq '.publishedAt[0:10]') + + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "date=${DATE}" >> "$GITHUB_OUTPUT" + + - name: Check whether the package is already in winget-pkgs + id: exists + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + # manifests are filed under the lowercased first letter of the + # publisher, e.g. manifests/o/OndeInference/onde-cli. + PUBLISHER="${PACKAGE_IDENTIFIER%%.*}" + NAME="${PACKAGE_IDENTIFIER#*.}" + FIRST=$(echo "${PUBLISHER:0:1}" | tr '[:upper:]' '[:lower:]') + PATH_IN_REPO="manifests/${FIRST}/${PUBLISHER}/${NAME}" + + if gh api "repos/microsoft/winget-pkgs/contents/${PATH_IN_REPO}" >/dev/null 2>&1; then + echo "Found ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting an update." + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "No ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting it as a new package." + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Render manifests for a first submission + if: steps.exists.outputs.found == 'false' + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${{ steps.release.outputs.tag }}" + VERSION="${{ steps.release.outputs.version }}" + + mkdir -p artifacts manifests + + gh release download "$TAG" \ + --repo "$REPO" \ + --pattern "onde-win-*.exe.sha256" \ + --dir artifacts/ + + # The schema wants 64 hex characters; the community validation + # pipeline writes them uppercase, so match that. + SHA_AMD64=$(tr -d '[:space:]' < artifacts/onde-win-amd64.exe.sha256 | tr '[:lower:]' '[:upper:]') + SHA_ARM64=$(tr -d '[:space:]' < artifacts/onde-win-arm64.exe.sha256 | tr '[:lower:]' '[:upper:]') + + RELEASE_DATE="${{ steps.release.outputs.date }}" + + for template in packaging/winget/*.yaml.in; do + out="manifests/$(basename "$template" .in)" + sed \ + -e "s|@VERSION@|${VERSION}|g" \ + -e "s|@TAG@|${TAG}|g" \ + -e "s|@REPO@|${REPO}|g" \ + -e "s|@SHA_AMD64@|${SHA_AMD64}|g" \ + -e "s|@SHA_ARM64@|${SHA_ARM64}|g" \ + -e "s|@RELEASE_DATE@|${RELEASE_DATE}|g" \ + "$template" > "$out" + + echo "--- ${out} ---" + cat "$out" + done + + - name: Download wingetcreate + shell: pwsh + run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe + + - name: Submit new package + if: steps.exists.outputs.found == 'false' + shell: pwsh + env: + # wingetcreate reads the token from this variable. Passing it as + # --token instead makes the tool warn that the token can end up in + # logs, which on a shared runner is not a warning worth ignoring. + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + run: | + if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { + Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + } + + $version = "${{ steps.release.outputs.version }}" + + # --no-open because the runner has no browser to open the PR in. + .\wingetcreate.exe submit manifests ` + --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" ` + --no-open + + if ($LASTEXITCODE -ne 0) { + Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE" + } + + - name: Submit manifest update + if: steps.exists.outputs.found == 'true' + shell: pwsh + env: + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }} + run: | + if (-not $env:WINGET_CREATE_GITHUB_TOKEN) { + Write-Error "WINGET_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR." + } + + $tag = "${{ steps.release.outputs.tag }}" + $version = "${{ steps.release.outputs.version }}" + $base = "https://github.com/${env:REPO}/releases/download/$tag" + + # The trailing |x64 and |arm64 tell wingetcreate which installer + # entry each URL replaces. Without them it guesses from the file + # name, and "onde-win-amd64.exe" is not a spelling it recognises. + .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER ` + --version $version ` + --urls "$base/onde-win-amd64.exe|x64" "$base/onde-win-arm64.exe|arm64" ` + --release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" ` + --release-date "${{ steps.release.outputs.date }}" ` + --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" ` + --submit ` + --no-open + + if ($LASTEXITCODE -ne 0) { + Write-Error "wingetcreate failed with exit code $LASTEXITCODE" + } diff --git a/.gitignore b/.gitignore index 1c0d079..9636fb1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ /pub/onde_cli/pubspec.lock .env + +# Playwright MCP session artifacts +.playwright-mcp/ diff --git a/Cargo.lock b/Cargo.lock index 53fda14..8cda5ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -740,9 +740,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1517,9 +1517,9 @@ checksum = "b2972feb8dffe7bc8c5463b1dacda1b0dfbed3710e50f977d965429692d74cd8" [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" dependencies = [ "serde", ] @@ -5070,9 +5070,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", diff --git a/packaging/winget/OndeInference.onde-cli.installer.yaml.in b/packaging/winget/OndeInference.onde-cli.installer.yaml.in new file mode 100644 index 0000000..a6cb79d --- /dev/null +++ b/packaging/winget/OndeInference.onde-cli.installer.yaml.in @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json +PackageIdentifier: OndeInference.onde-cli +PackageVersion: @VERSION@ +InstallerType: portable +# `Commands` is what winget names the shim it drops on PATH. The release +# assets are onde-win-.exe, but the command has to be `onde`. +Commands: + - onde +ReleaseDate: "@RELEASE_DATE@" +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/onde-win-amd64.exe + InstallerSha256: @SHA_AMD64@ + - Architecture: arm64 + InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/onde-win-arm64.exe + InstallerSha256: @SHA_ARM64@ +ManifestType: installer +ManifestVersion: 1.6.0 diff --git a/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in b/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in new file mode 100644 index 0000000..14b68f4 --- /dev/null +++ b/packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in @@ -0,0 +1,34 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json +PackageIdentifier: OndeInference.onde-cli +PackageVersion: @VERSION@ +PackageLocale: en-US +Publisher: OndeInference +PublisherUrl: https://ondeinference.com/ +PublisherSupportUrl: https://github.com/@REPO@/issues +PackageName: Onde CLI +PackageUrl: https://ondeinference.com/cli +License: MIT OR Apache-2.0 +LicenseUrl: https://github.com/@REPO@/blob/main/LICENSE +ShortDescription: Terminal UI to fine-tune models and manage your Onde Inference account. +Description: |- + Onde CLI is a terminal user interface for Onde Inference. Fine-tune + models with LoRA, merge adapters, export to GGUF, test locally, and + upload to Hugging Face — all from one binary. Manage your Onde Inference + account and assign models to your apps without leaving the terminal. +Moniker: onde +Tags: + - ai + - cli + - fine-tune + - gguf + - lora + - inference + - llm + - terminal + - tui +ReleaseNotesUrl: https://github.com/@REPO@/releases/tag/@TAG@ +Documentations: + - DocumentLabel: Readme + DocumentUrl: https://github.com/@REPO@/blob/main/README.md +ManifestType: defaultLocale +ManifestVersion: 1.6.0 diff --git a/packaging/winget/OndeInference.onde-cli.yaml.in b/packaging/winget/OndeInference.onde-cli.yaml.in new file mode 100644 index 0000000..60369b5 --- /dev/null +++ b/packaging/winget/OndeInference.onde-cli.yaml.in @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json +PackageIdentifier: OndeInference.onde-cli +PackageVersion: @VERSION@ +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.6.0 diff --git a/server.json b/server.json index cd007b8..43ef549 100644 --- a/server.json +++ b/server.json @@ -1,8 +1,8 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.ondeinference/onde-cli", - "title": "Onde Inference CLI", - "description": "Manage your Onde Inference account and models: apps, model catalog, fine-tuned GGUF deploys.", + "title": "Onde Inference CLI: Fine-tune & Deploy Local LLMs", + "description": "MCP server to fine-tune LoRA, export GGUF, and deploy local LLMs on Onde Inference.", "version": "0.4.1", "websiteUrl": "https://ondeinference.com/cli", "repository": { @@ -50,17 +50,28 @@ ], "_meta": { "io.modelcontextprotocol.registry/publisher-provided": { - "categories": ["developer-tools", "ai"], + "categories": ["developer-tools", "ai", "machine-learning"], "keywords": [ "onde", - "inference", + "onde inference", + "mcp", + "mcp server", "llm", + "local llm", "fine-tuning", "lora", + "lora adapter", "gguf", + "quantization", + "qwen", + "safetensors", "hugging face", "model deployment", - "on-device" + "model catalog", + "on-device", + "edge ai", + "ai agents", + "rust cli" ], "products": { "Onde Inference": "https://ondeinference.com"