@splitfireai / agent / commits / 64c738d

ci: link the C runtime statically and file a Package Request issue for winget

winget's validator runs the bare exe on a console and expects exit 0, and the exe has to load on a machine without the VC++ redistributable. - Link the MSVC runtime statically on Windows and fail the release build if the exe still imports vcruntime or ucrt DLLs (packaging/windows/check-static-crt.ps1). - Put splitfire-agent.exe at the zip root so it matches RelativeFilePath. - Print the missing-API-key hint and exit 0 when the TUI is launched bare. - File the winget-pkgs issue from the Package Request form fields, wait a few minutes, then open the PR with "Resolves #N". WINGET_PR_TOKEN needs read:org and read:discussion for the PR body edit. - Move the manifests to schema 1.12.0 and set License to Apache-2.0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Seto committed Oct 13, 2026 at 13:51 UTC 64c738d114487d4344369b02d130da399be194e0
8 files changed +103 -39
.cargo/config.toml new
+7
@@ -0,0 +1,7 @@
1 +# Link the MSVC C runtime statically on Windows. Without this the binary
2 +# imports VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set, and on a
3 +# machine without the Visual C++ Redistributable it dies at load time with
4 +# STATUS_DLL_NOT_FOUND (0xC0000135) before main runs. winget ships splitfire-agent as a
5 +# portable exe, so there is no installer to bring the runtime along.
6 +[target.'cfg(all(windows, target_env = "msvc"))']
7 +rustflags = ["-C", "target-feature=+crt-static"]
.github/workflows/release-winget.yml
+43 -29
@@ -135,11 +135,20 @@ jobs:
135 shell: pwsh
136 run: Invoke-WebRequest -Uri "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe
137
138 - # File the tracking issue first, as a separate identity (WINGET_ISSUE_TOKEN),
139 - # so the manifest PR that wingetcreate opens next can resolve it. winget-pkgs
140 - # asks submitters to link an issue; filing it here means the "Set the winget
141 - # PR body" step can tick "Linked to an issue" and add "Resolves #N".
142 - - name: File the winget tracking issue
138 + # winget-pkgs convention: file the "please add/update this package"
139 + # Package Request issue *first*, then open the manifest PR that fulfils
140 + # it with "Resolves #N". The other way round reads as a request and its
141 + # fix posted seconds apart.
142 + #
143 + # The issue and the PR are authored by two separate PATs, both with
144 + # public_repo scope on microsoft/winget-pkgs (fork + open issues/PRs):
145 + # WINGET_ISSUE_TOKEN files the Package Request issue.
146 + # WINGET_PR_TOKEN opens the manifest PR. It also needs read:org and
147 + # read:discussion: the "Set the winget PR body" step
148 + # calls `gh pr edit`, whose GraphQL query resolves
149 + # org/team and discussion fields and fails with a
150 + # scope error without them.
151 + - name: File the winget Package Request issue
152 id: issue
153 shell: bash
154 env:
@@ -148,38 +157,43 @@ jobs:
157 set -euo pipefail
158
159 if [ -z "${GH_TOKEN:-}" ]; then
151 - echo "::error::WINGET_ISSUE_TOKEN is not set. It needs a PAT that can open issues on microsoft/winget-pkgs."
160 + echo "::error::WINGET_ISSUE_TOKEN is not set. It needs a PAT (public_repo) for the account that should author the Package Request issue."
161 exit 1
162 fi
163
164 VERSION="${{ steps.release.outputs.version }}"
165 TAG="${{ steps.release.outputs.tag }}"
157 - BASE="https://github.com/${REPO}/releases/download/${TAG}"
158 -
159 - if [ "${{ steps.exists.outputs.found }}" = "false" ]; then
160 - TITLE="New package: ${PACKAGE_IDENTIFIER} version ${VERSION}"
161 - ACTION="Add"
162 - else
163 - TITLE="Update: ${PACKAGE_IDENTIFIER} to version ${VERSION}"
164 - ACTION="Update"
165 - fi
166 -
167 - BODY=$(cat <<EOF
168 - ${ACTION} ${PACKAGE_IDENTIFIER} to version ${VERSION}.
169 -
170 - - Release: https://github.com/${REPO}/releases/tag/${TAG}
171 - - Installer (x64): ${BASE}/splitfire-agent-windows-x64.zip
166 + INSTALLER="https://github.com/${REPO}/releases/download/${TAG}/splitfire-agent-windows-x64.zip"
167 +
168 + # winget-pkgs' Package Request is an issue *form*
169 + # (.github/ISSUE_TEMPLATE/package_request.yml): the title must be
170 + # "[Package Request]: ..." and the body has to carry the labelled
171 + # fields below (Installer Url especially). A free-text body lands as
172 + # Needs-Triage and can't be acted on.
173 + ISSUE_BODY=$(cat <<EOF
174 + Download Page Url: https://github.com/${REPO}/releases/tag/${TAG}
175 + Publisher: SplitFireAI
176 + Package Name: SplitFire Agent
177 + Description: Music-domain ACP agent for theory, production, industry, culture and live shows.
178 + Package Version: ${VERSION}
179 + Installer Url: ${INSTALLER}
180 EOF
181 )
182
183 ISSUE_URL=$(gh issue create \
184 --repo microsoft/winget-pkgs \
177 - --title "$TITLE" \
178 - --body "$BODY")
185 + --title "[Package Request]: ${PACKAGE_IDENTIFIER} ${VERSION}" \
186 + --body "$ISSUE_BODY")
187 +
188 + echo "Opened issue ${ISSUE_URL}"
189 + echo "issue_url=${ISSUE_URL}" >> "$GITHUB_OUTPUT"
190 + echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT"
191
180 - echo "Filed ${ISSUE_URL}"
181 - ISSUE_NUMBER="${ISSUE_URL##*/}"
182 - echo "issue_number=${ISSUE_NUMBER}" >> "$GITHUB_OUTPUT"
192 + # Leave a gap so the issue and the PR aren't posted seconds apart.
193 + - name: Wait before opening the PR
194 + if: steps.issue.outputs.issue_number != ''
195 + shell: bash
196 + run: sleep $(( RANDOM % 61 + 150 ))
197
198 - name: Submit new package
199 id: submit_new
@@ -189,7 +203,7 @@ jobs:
203 WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
204 run: |
205 if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
192 - Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
206 + Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
207 }
208
209 $version = "${{ steps.release.outputs.version }}"
@@ -223,7 +237,7 @@ jobs:
237 WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
238 run: |
239 if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
226 - Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
240 + Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
241 }
242
243 $tag = "${{ steps.release.outputs.tag }}"
@@ -289,7 +303,7 @@ jobs:
303 "- [x] Have you checked that there aren't other open [pull requests](https://github.com/microsoft/winget-pkgs/pulls) for the same manifest update/change?" \
304 "- [x] Have you validated your manifest locally with \`winget validate --manifest <path>\`?" \
305 "- [x] Have you tested your manifest locally with \`winget install --manifest <path>\`?" \
292 - "- [x] Does your manifest conform to the [1.6 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.6.0)?" \
306 + "- [x] Does your manifest conform to the [1.12 schema](https://github.com/microsoft/winget-pkgs/tree/master/doc/manifest/schema/1.12.0)?" \
307 "- [x] This PR is linked to an issue.")
308
309 gh pr edit "$PR_NUMBER" \
.github/workflows/release.yml
+8 -2
@@ -25,13 +25,19 @@ jobs:
25 with:
26 targets: ${{ matrix.target }}
27 - run: cargo build --release --locked --bin splitfire-agent --target ${{ matrix.target }}
28 + # The exe must not import the VC++ runtime DLLs (see .cargo/config.toml), or it fails to
29 + # load on a machine without the redistributable.
30 + - name: Check static CRT
31 + if: matrix.os == 'windows-latest'
32 + shell: pwsh
33 + run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/splitfire-agent.exe
34 - name: Package archive
35 shell: bash
36 run: |
31 - BIN=target/${{ matrix.target }}/release/splitfire-agent
37 ARCHIVE=splitfire-agent-${{ matrix.name }}
38 if [ "${{ matrix.os }}" = "windows-latest" ]; then
34 - 7z a "$ARCHIVE.zip" "$BIN.exe"
39 + # exe at the zip root, matching RelativeFilePath in the winget manifest
40 + (cd "target/${{ matrix.target }}/release" && 7z a "$GITHUB_WORKSPACE/$ARCHIVE.zip" splitfire-agent.exe)
41 echo "asset=$ARCHIVE.zip" >> "$GITHUB_ENV"
42 else
43 tar -czf "$ARCHIVE.tar.gz" -C "target/${{ matrix.target }}/release" splitfire-agent
packaging/windows/check-static-crt.ps1 new
+35
@@ -0,0 +1,35 @@
1 +# Fails if a Windows binary depends on the Visual C++ runtime DLLs.
2 +#
3 +# splitfire-agent is linked with +crt-static (see .cargo/config.toml) so it runs on a
4 +# clean Windows install. If RUSTFLAGS overrides that, or a native dependency
5 +# drags in the DLL runtime, the exe still builds and passes tests on a runner
6 +# that has the redistributable installed, then fails to load on users'
7 +# machines. This check catches it before release.
8 +param(
9 + [Parameter(Mandatory = $true)]
10 + [string]$Path
11 +)
12 +
13 +$ErrorActionPreference = 'Stop'
14 +
15 +$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
16 +$dumpbin = & $vswhere -latest -products * -find 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' |
17 + Select-Object -First 1
18 +if (-not $dumpbin) {
19 + throw 'dumpbin.exe not found; is the MSVC toolset installed?'
20 +}
21 +
22 +$output = & $dumpbin /nologo /dependents $Path
23 +if ($LASTEXITCODE -ne 0) {
24 + throw "dumpbin failed on $Path"
25 +}
26 +$output | Write-Host
27 +
28 +$runtime = $output |
29 + Where-Object { $_ -match '^\s+(vcruntime|msvcp|concrt|ucrtbase|api-ms-win-crt-)\S*\.dll\s*$' } |
30 + ForEach-Object { $_.Trim() }
31 +if ($runtime) {
32 + Write-Host "::error::$Path depends on the Visual C++ runtime: $($runtime -join ', ')"
33 + exit 1
34 +}
35 +Write-Host "$Path does not depend on the Visual C++ runtime."
packaging/winget/SplitFireAI.splitfire-agent.installer.yaml.in
+2 -2
@@ -1,4 +1,4 @@
1 -# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json
2 PackageIdentifier: SplitFireAI.splitfire-agent
3 PackageVersion: @VERSION@
4 InstallerType: zip
@@ -14,4 +14,4 @@ Installers:
14 InstallerUrl: https://github.com/@REPO@/releases/download/@TAG@/splitfire-agent-windows-x64.zip
15 InstallerSha256: @SHA_X64@
16 ManifestType: installer
17 -ManifestVersion: 1.6.0
17 +ManifestVersion: 1.12.0
packaging/winget/SplitFireAI.splitfire-agent.locale.en-US.yaml.in
+3 -3
@@ -1,4 +1,4 @@
1 -# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.12.0.schema.json
2 PackageIdentifier: SplitFireAI.splitfire-agent
3 PackageVersion: @VERSION@
4 PackageLocale: en-US
@@ -7,7 +7,7 @@ PublisherUrl: https://github.com/SplitFireAI
7 PublisherSupportUrl: https://github.com/@REPO@/issues
8 PackageName: SplitFire Agent
9 PackageUrl: https://github.com/@REPO@
10 -License: MIT OR Apache-2.0
10 +License: Apache-2.0
11 LicenseUrl: https://github.com/@REPO@/blob/main/LICENSE
12 ShortDescription: Music-domain ACP agent for theory, production, industry, culture and live shows.
13 Description: |-
@@ -29,4 +29,4 @@ Documentations:
29 - DocumentLabel: Readme
30 DocumentUrl: https://github.com/@REPO@/blob/main/README.md
31 ManifestType: defaultLocale
32 -ManifestVersion: 1.6.0
32 +ManifestVersion: 1.12.0
packaging/winget/SplitFireAI.splitfire-agent.yaml.in
+2 -2
@@ -1,6 +1,6 @@
1 -# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json
1 +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.12.0.schema.json
2 PackageIdentifier: SplitFireAI.splitfire-agent
3 PackageVersion: @VERSION@
4 DefaultLocale: en-US
5 ManifestType: version
6 -ManifestVersion: 1.6.0
6 +ManifestVersion: 1.12.0
src/main.rs
+3 -1
@@ -85,10 +85,12 @@ async fn main() -> anyhow::Result<()> {
85 } else {
86 let llm = LlmConfig::from_env(&INFO.env);
87 if llm.api_key.is_none() {
88 - anyhow::bail!(
88 + // Exit 0: winget's validator launches the bare exe on a console and expects success.
89 + eprintln!(
90 "No Onde API key configured. Set ONDE_API_KEY or run `{} --setup`.",
91 INFO.name
92 );
93 + return Ok(());
94 }
95 let mut tui = TuiConfig::current_exe(INFO.name, opts.yolo)?;
96 tui.env.push((INFO.env.var("SURFACE"), "tui".to_string()));