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