@ondeinference / cli / commits / 9d97139

ci: add scoop and winget release workflows

Add release-scoop.yml and release-winget.yml, dispatched from release-github.yml after assets are checksummed. Scoop generates a bucket manifest from the .sha256 sidecar; winget uses wingetcreate to update or submit to microsoft/winget-pkgs. Winget manifest templates live under packaging/winget/ with @PLACEHOLDER@ substitution. Also add .sha256 checksum generation to release-github.yml and gitignore .playwright-mcp/. Co-Authored-By: siGit Code v1.5.13-acp <noreply@sigit.si>

Seto Elkahfi committed Oct 4, 2026 at 18:47 UTC 9d9713901d888eabf8009aff55dc71715ec30e3f
7 files changed +438
.github/workflows/release-github.yml
+51
@@ -122,6 +122,25 @@ jobs:
122 echo "Archive: ${ARCHIVE_NAME}"
123 echo "SHA256: $(cat "${ARCHIVE_NAME}.sha256")"
124
125 + # Scoop and winget consume the raw Windows binaries rather than a
126 + # tarball, and both need a SHA256 sidecar. Windows runners use bash from
127 + # Git for Windows, which has sha256sum; macOS only has shasum.
128 + - name: Checksum release assets
129 + shell: bash
130 + run: |
131 + cd release
132 + for asset in *; do
133 + case "${asset}" in *.sha256) continue ;; esac
134 +
135 + if command -v sha256sum >/dev/null 2>&1; then
136 + sha256sum "${asset}" | awk '{print $1}' > "${asset}.sha256"
137 + else
138 + shasum -a 256 "${asset}" | awk '{print $1}' > "${asset}.sha256"
139 + fi
140 +
141 + echo "${asset}: $(cat "${asset}.sha256")"
142 + done
143 +
144 - name: Upload binary artifact
145 uses: actions/upload-artifact@v4
146 with:
@@ -203,3 +222,35 @@ jobs:
222 }
223 })
224 console.log('Dispatched release-pub.yml for tag ${{ steps.tag.outputs.tag }}')
225 +
226 + # These channels publish outside this repo (a Scoop bucket,
227 + # microsoft/winget-pkgs) and read checksums off the release created
228 + # above, so they hang off this job rather than firing on the tag
229 + # directly.
230 + - name: Trigger OS package manager releases
231 + uses: actions/github-script@v7
232 + with:
233 + script: |
234 + const tag = '${{ steps.tag.outputs.tag }}'
235 + const workflows = [
236 + 'release-scoop.yml',
237 + 'release-winget.yml',
238 + ]
239 +
240 + for (const workflow_id of workflows) {
241 + try {
242 + await github.rest.actions.createWorkflowDispatch({
243 + owner: context.repo.owner,
244 + repo: context.repo.repo,
245 + workflow_id,
246 + ref: tag,
247 + inputs: { tag },
248 + })
249 + console.log(`Dispatched ${workflow_id} for tag ${tag}`)
250 + } catch (error) {
251 + // One packaging channel being unconfigured (a missing secret,
252 + // a bucket repo that does not exist yet) should not take the
253 + // rest of the fan-out down with it.
254 + core.warning(`Failed to dispatch ${workflow_id}: ${error.message}`)
255 + }
256 + }
.github/workflows/release-scoop.yml new
+148
@@ -0,0 +1,148 @@
1 +name: Scoop Release
2 +
3 +# Dispatched by release-github.yml once the release and its checksums exist.
4 +on:
5 + workflow_dispatch:
6 + inputs:
7 + tag:
8 + description: "Release tag (e.g. v0.4.0)"
9 + required: true
10 +
11 +concurrency:
12 + # A re-pushed or re-dispatched tag fires this a second time. Queue the
13 + # duplicate behind the original instead of cancelling it: cancelling a run
14 + # mid-publish can leave a channel half-uploaded, whereas a queued duplicate
15 + # just hits the "already published" check and exits clean.
16 + group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
17 + cancel-in-progress: false
18 +
19 +permissions:
20 + contents: read
21 +
22 +env:
23 + REPO: ondeinference/onde-cli
24 +
25 +jobs:
26 + update-scoop-bucket:
27 + name: Update Scoop bucket
28 + runs-on: ubuntu-latest
29 +
30 + steps:
31 + - name: Resolve tag and version
32 + id: release
33 + shell: bash
34 + run: |
35 + TAG="${{ github.event.inputs.tag }}"
36 + VERSION="${TAG#v}"
37 +
38 + echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
39 + echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
40 +
41 + - name: Read SHA256 checksums from release
42 + id: sha
43 + env:
44 + GH_TOKEN: ${{ github.token }}
45 + shell: bash
46 + run: |
47 + mkdir -p artifacts
48 +
49 + gh release download "${{ steps.release.outputs.tag }}" \
50 + --repo "${{ env.REPO }}" \
51 + --pattern "onde-win-*.exe.sha256" \
52 + --dir artifacts/
53 +
54 + AMD64_SHA=$(cat artifacts/onde-win-amd64.exe.sha256)
55 + ARM64_SHA=$(cat artifacts/onde-win-arm64.exe.sha256)
56 +
57 + echo "amd64=${AMD64_SHA}" >> "$GITHUB_OUTPUT"
58 + echo "arm64=${ARM64_SHA}" >> "$GITHUB_OUTPUT"
59 +
60 + echo "AMD64 SHA256: ${AMD64_SHA}"
61 + echo "ARM64 SHA256: ${ARM64_SHA}"
62 +
63 + - name: Checkout Scoop bucket
64 + uses: actions/checkout@v6
65 + with:
66 + repository: ondeinference/scoop-bucket
67 + token: ${{ secrets.SCOOP_BUCKET_TOKEN }}
68 + path: scoop-bucket
69 +
70 + - name: Generate manifest
71 + shell: bash
72 + run: |
73 + VERSION="${{ steps.release.outputs.version }}"
74 + TAG="${{ steps.release.outputs.tag }}"
75 + AMD64_SHA="${{ steps.sha.outputs.amd64 }}"
76 + ARM64_SHA="${{ steps.sha.outputs.arm64 }}"
77 +
78 + mkdir -p scoop-bucket/bucket
79 +
80 + # The `#/onde.exe` URL fragment is Scoop's rename-on-download
81 + # syntax: the release asset is onde-win-amd64.exe, but the shim has
82 + # to end up as onde.exe for `onde` to work on PATH.
83 + cat > scoop-bucket/bucket/onde.json <<MANIFEST
84 + {
85 + "version": "${VERSION}",
86 + "description": "Onde Inference CLI — fine-tune models and manage your Onde Inference account from the terminal.",
87 + "homepage": "https://ondeinference.com/cli",
88 + "license": "MIT OR Apache-2.0",
89 + "architecture": {
90 + "64bit": {
91 + "url": "https://github.com/${REPO}/releases/download/${TAG}/onde-win-amd64.exe#/onde.exe",
92 + "hash": "${AMD64_SHA}"
93 + },
94 + "arm64": {
95 + "url": "https://github.com/${REPO}/releases/download/${TAG}/onde-win-arm64.exe#/onde.exe",
96 + "hash": "${ARM64_SHA}"
97 + }
98 + },
99 + "bin": "onde.exe",
100 + "checkver": {
101 + "github": "https://github.com/${REPO}"
102 + },
103 + "autoupdate": {
104 + "architecture": {
105 + "64bit": {
106 + "url": "https://github.com/${REPO}/releases/download/v\$version/onde-win-amd64.exe#/onde.exe"
107 + },
108 + "arm64": {
109 + "url": "https://github.com/${REPO}/releases/download/v\$version/onde-win-arm64.exe#/onde.exe"
110 + }
111 + },
112 + "hash": {
113 + "url": "\$url.sha256"
114 + }
115 + }
116 + }
117 + MANIFEST
118 +
119 + echo "Generated manifest:"
120 + cat scoop-bucket/bucket/onde.json
121 +
122 + - name: Validate manifest JSON
123 + shell: bash
124 + run: |
125 + if ! jq empty scoop-bucket/bucket/onde.json; then
126 + echo "::error::Generated Scoop manifest is not valid JSON" >&2
127 + exit 1
128 + fi
129 +
130 + for field in version bin architecture; do
131 + if [ "$(jq -r "has(\"${field}\")" scoop-bucket/bucket/onde.json)" != "true" ]; then
132 + echo "::error::Scoop manifest is missing the '${field}' field" >&2
133 + exit 1
134 + fi
135 + done
136 +
137 + - name: Commit and push
138 + shell: bash
139 + run: |
140 + VERSION="${{ steps.release.outputs.version }}"
141 +
142 + cd scoop-bucket
143 + git config user.name "github-actions[bot]"
144 + git config user.email "github-actions[bot]@users.noreply.github.com"
145 + git add bucket/onde.json
146 + git diff --cached --quiet && echo "No changes to commit" && exit 0
147 + git commit -m "Update onde to ${VERSION}"
148 + git push
.github/workflows/release-winget.yml new
+178
@@ -0,0 +1,178 @@
1 +name: winget Release
2 +
3 +# Dispatched by release-github.yml once the release and its checksums exist.
4 +#
5 +# winget manifests live in microsoft/winget-pkgs, and `wingetcreate update`
6 +# only works on a package that is already there. So this workflow branches:
7 +#
8 +# - package already in winget-pkgs -> `wingetcreate update`, which rewrites
9 +# the URLs and re-hashes the installers for us.
10 +# - package not there yet -> render the manifests in packaging/winget/ and
11 +# `wingetcreate submit` them as a new package.
12 +#
13 +# The second path only runs once, but keeping it here means a first release
14 +# does not need someone to sit at a Windows box running `wingetcreate new`
15 +# interactively.
16 +on:
17 + workflow_dispatch:
18 + inputs:
19 + tag:
20 + description: "Release tag (e.g. v0.4.0)"
21 + required: true
22 +
23 +concurrency:
24 + # A re-pushed or re-dispatched tag fires this a second time. Queue the
25 + # duplicate behind the original instead of cancelling it: cancelling a run
26 + # mid-publish can leave a channel half-updated, whereas a queued duplicate
27 + # just hits the "already published" check and exits clean.
28 + group: ${{ github.workflow }}-${{ github.event.inputs.tag || github.ref_name }}
29 + cancel-in-progress: false
30 +
31 +permissions:
32 + contents: read
33 +
34 +env:
35 + REPO: ondeinference/onde-cli
36 + PACKAGE_IDENTIFIER: OndeInference.onde-cli
37 +
38 +jobs:
39 + submit-winget-manifest:
40 + name: Submit winget manifest
41 + # wingetcreate is a Windows-only tool.
42 + runs-on: windows-latest
43 +
44 + steps:
45 + - name: Check out repository
46 + uses: actions/checkout@v6
47 +
48 + - name: Resolve tag, version and release date
49 + id: release
50 + shell: bash
51 + env:
52 + GH_TOKEN: ${{ github.token }}
53 + run: |
54 + TAG="${{ github.event.inputs.tag }}"
55 + VERSION="${TAG#v}"
56 + # wingetcreate wants ReleaseDate as YYYY-MM-DD.
57 + DATE=$(gh release view "$TAG" --repo "$REPO" --json publishedAt --jq '.publishedAt[0:10]')
58 +
59 + echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
60 + echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
61 + echo "date=${DATE}" >> "$GITHUB_OUTPUT"
62 +
63 + - name: Check whether the package is already in winget-pkgs
64 + id: exists
65 + shell: bash
66 + env:
67 + GH_TOKEN: ${{ github.token }}
68 + run: |
69 + # manifests are filed under the lowercased first letter of the
70 + # publisher, e.g. manifests/o/OndeInference/onde-cli.
71 + PUBLISHER="${PACKAGE_IDENTIFIER%%.*}"
72 + NAME="${PACKAGE_IDENTIFIER#*.}"
73 + FIRST=$(echo "${PUBLISHER:0:1}" | tr '[:upper:]' '[:lower:]')
74 + PATH_IN_REPO="manifests/${FIRST}/${PUBLISHER}/${NAME}"
75 +
76 + if gh api "repos/microsoft/winget-pkgs/contents/${PATH_IN_REPO}" >/dev/null 2>&1; then
77 + echo "Found ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting an update."
78 + echo "found=true" >> "$GITHUB_OUTPUT"
79 + else
80 + echo "No ${PACKAGE_IDENTIFIER} at ${PATH_IN_REPO}; submitting it as a new package."
81 + echo "found=false" >> "$GITHUB_OUTPUT"
82 + fi
83 +
84 + - name: Render manifests for a first submission
85 + if: steps.exists.outputs.found == 'false'
86 + shell: bash
87 + env:
88 + GH_TOKEN: ${{ github.token }}
89 + run: |
90 + TAG="${{ steps.release.outputs.tag }}"
91 + VERSION="${{ steps.release.outputs.version }}"
92 +
93 + mkdir -p artifacts manifests
94 +
95 + gh release download "$TAG" \
96 + --repo "$REPO" \
97 + --pattern "onde-win-*.exe.sha256" \
98 + --dir artifacts/
99 +
100 + # The schema wants 64 hex characters; the community validation
101 + # pipeline writes them uppercase, so match that.
102 + SHA_AMD64=$(tr -d '[:space:]' < artifacts/onde-win-amd64.exe.sha256 | tr '[:lower:]' '[:upper:]')
103 + SHA_ARM64=$(tr -d '[:space:]' < artifacts/onde-win-arm64.exe.sha256 | tr '[:lower:]' '[:upper:]')
104 +
105 + RELEASE_DATE="${{ steps.release.outputs.date }}"
106 +
107 + for template in packaging/winget/*.yaml.in; do
108 + out="manifests/$(basename "$template" .in)"
109 + sed \
110 + -e "s|@VERSION@|${VERSION}|g" \
111 + -e "s|@TAG@|${TAG}|g" \
112 + -e "s|@REPO@|${REPO}|g" \
113 + -e "s|@SHA_AMD64@|${SHA_AMD64}|g" \
114 + -e "s|@SHA_ARM64@|${SHA_ARM64}|g" \
115 + -e "s|@RELEASE_DATE@|${RELEASE_DATE}|g" \
116 + "$template" > "$out"
117 +
118 + echo "--- ${out} ---"
119 + cat "$out"
120 + done
121 +
122 + - name: Download wingetcreate
123 + shell: pwsh
124 + run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe
125 +
126 + - name: Submit new package
127 + if: steps.exists.outputs.found == 'false'
128 + shell: pwsh
129 + env:
130 + # wingetcreate reads the token from this variable. Passing it as
131 + # --token instead makes the tool warn that the token can end up in
132 + # logs, which on a shared runner is not a warning worth ignoring.
133 + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }}
134 + run: |
135 + if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
136 + 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."
137 + }
138 +
139 + $version = "${{ steps.release.outputs.version }}"
140 +
141 + # --no-open because the runner has no browser to open the PR in.
142 + .\wingetcreate.exe submit manifests `
143 + --prtitle "New package: $env:PACKAGE_IDENTIFIER version $version" `
144 + --no-open
145 +
146 + if ($LASTEXITCODE -ne 0) {
147 + Write-Error "wingetcreate submit failed with exit code $LASTEXITCODE"
148 + }
149 +
150 + - name: Submit manifest update
151 + if: steps.exists.outputs.found == 'true'
152 + shell: pwsh
153 + env:
154 + WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_TOKEN }}
155 + run: |
156 + if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
157 + 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."
158 + }
159 +
160 + $tag = "${{ steps.release.outputs.tag }}"
161 + $version = "${{ steps.release.outputs.version }}"
162 + $base = "https://github.com/${env:REPO}/releases/download/$tag"
163 +
164 + # The trailing |x64 and |arm64 tell wingetcreate which installer
165 + # entry each URL replaces. Without them it guesses from the file
166 + # name, and "onde-win-amd64.exe" is not a spelling it recognises.
167 + .\wingetcreate.exe update $env:PACKAGE_IDENTIFIER `
168 + --version $version `
169 + --urls "$base/onde-win-amd64.exe|x64" "$base/onde-win-arm64.exe|arm64" `
170 + --release-notes-url "https://github.com/${env:REPO}/releases/tag/$tag" `
171 + --release-date "${{ steps.release.outputs.date }}" `
172 + --prtitle "Update $env:PACKAGE_IDENTIFIER to version $version" `
173 + --submit `
174 + --no-open
175 +
176 + if ($LASTEXITCODE -ne 0) {
177 + Write-Error "wingetcreate failed with exit code $LASTEXITCODE"
178 + }
.gitignore
+3
@@ -15,3 +15,6 @@
15 /pub/onde_cli/pubspec.lock
16
17 .env
18 +
19 +# Playwright MCP session artifacts
20 +.playwright-mcp/
packaging/winget/OndeInference.onde-cli.installer.yaml.in new
+18
@@ -0,0 +1,18 @@
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
2 +PackageIdentifier: OndeInference.onde-cli
3 +PackageVersion: @VERSION@
4 +InstallerType: portable
5 +# `Commands` is what winget names the shim it drops on PATH. The release
6 +# assets are onde-win-<arch>.exe, but the command has to be `onde`.
7 +Commands:
8 + - onde
9 +ReleaseDate: "@RELEASE_DATE@"
10 +Installers:
11 + - Architecture: x64
12 + InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/onde-win-amd64.exe
13 + InstallerSha256: @SHA_AMD64@
14 + - Architecture: arm64
15 + InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/onde-win-arm64.exe
16 + InstallerSha256: @SHA_ARM64@
17 +ManifestType: installer
18 +ManifestVersion: 1.6.0
packaging/winget/OndeInference.onde-cli.locale.en-US.yaml.in new
+34
@@ -0,0 +1,34 @@
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
2 +PackageIdentifier: OndeInference.onde-cli
3 +PackageVersion: @VERSION@
4 +PackageLocale: en-US
5 +Publisher: OndeInference
6 +PublisherUrl: https://ondeinference.com/
7 +PublisherSupportUrl: https://github.com/@REPO@/issues
8 +PackageName: Onde CLI
9 +PackageUrl: https://ondeinference.com/cli
10 +License: MIT OR Apache-2.0
11 +LicenseUrl: https://github.com/@REPO@/blob/main/LICENSE
12 +ShortDescription: Terminal UI to fine-tune models and manage your Onde Inference account.
13 +Description: |-
14 + Onde CLI is a terminal user interface for Onde Inference. Fine-tune
15 + models with LoRA, merge adapters, export to GGUF, test locally, and
16 + upload to Hugging Face — all from one binary. Manage your Onde Inference
17 + account and assign models to your apps without leaving the terminal.
18 +Moniker: onde
19 +Tags:
20 + - ai
21 + - cli
22 + - fine-tune
23 + - gguf
24 + - lora
25 + - inference
26 + - llm
27 + - terminal
28 + - tui
29 +ReleaseNotesUrl: https://github.com/@REPO@/releases/tag/@TAG@
30 +Documentations:
31 + - DocumentLabel: Readme
32 + DocumentUrl: https://github.com/@REPO@/blob/main/README.md
33 +ManifestType: defaultLocale
34 +ManifestVersion: 1.6.0
packaging/winget/OndeInference.onde-cli.yaml.in new
+6
@@ -0,0 +1,6 @@
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json
2 +PackageIdentifier: OndeInference.onde-cli
3 +PackageVersion: @VERSION@
4 +DefaultLocale: en-US
5 +ManifestType: version
6 +ManifestVersion: 1.6.0