@sigit / siti / commits / 0645faa

Add Windows MSIX workflow

paydii committed Jul 24, 2026 at 13:47 UTC 0645faa9342c455c936e9a76c3cc31231a2286c5
6 files changed +385 -1
.github/workflows/release-windows-microsoft-store.yml new
+129
@@ -0,0 +1,129 @@
1 +name: "Release Siti AI Windows Microsoft Store"
2 +
3 +on:
4 + push:
5 + branches:
6 + - release/windows-microsoft-store
7 + workflow_dispatch:
8 +
9 +# Push to the release branch above to build and draft-release the Windows
10 +# Microsoft Store artifacts. The .msi/.exe are for direct download; the MSIX is
11 +# what gets submitted to Partner Center (the Store re-signs it), which is the
12 +# only form that carries the package identity Store services need.
13 +jobs:
14 + publish-tauri:
15 + permissions:
16 + contents: write
17 + runs-on: windows-latest
18 + # Job-level env so the MSIX steps' own `if:` conditions can read these — a
19 + # step-level env is not in scope for that same step's `if`. When the secrets
20 + # are absent the MSIX steps skip and the unsigned .msi/.exe still ship.
21 + env:
22 + MSIX_PUBLISHER: ${{ secrets.MSIX_PUBLISHER }}
23 + MSIX_IDENTITY_NAME: ${{ secrets.MSIX_IDENTITY_NAME }}
24 + steps:
25 + - uses: actions/checkout@v7
26 +
27 + - name: setup node
28 + uses: actions/setup-node@v7
29 + with:
30 + node-version: 22
31 +
32 + # No version pin: reads the exact pnpm from package.json's
33 + # packageManager field (pnpm@11.8.0), matching ci.yml.
34 + - uses: pnpm/action-setup@v6
35 +
36 + - name: Install Rust stable
37 + uses: dtolnay/rust-toolchain@stable
38 +
39 + # The full dependency tree build is slow cold. Cache the cargo registry
40 + # and target/ keyed on Cargo.lock so a repeat run (a manifest tweak,
41 + # another Partner Center validation round) restores in minutes instead of
42 + # rebuilding from scratch.
43 + - name: Cache Rust build
44 + uses: Swatinem/rust-cache@v2
45 + with:
46 + workspaces: src-tauri
47 +
48 + # Validate the MSIX manifest before the long Rust build rather than after
49 + # it. makeappx checks the manifest against its schema regardless of what
50 + # the payload files contain, so a stub exe is enough to catch a schema
51 + # error in seconds instead of after a full build. Skipped when the MSIX
52 + # secrets are absent so the .msi/.exe build still ships.
53 + - name: Validate MSIX manifest (fast fail)
54 + if: ${{ env.MSIX_PUBLISHER != '' && env.MSIX_IDENTITY_NAME != '' }}
55 + shell: pwsh
56 + run: ./scripts/build-msix.ps1 -ValidateOnly
57 +
58 + - name: Install frontend dependencies
59 + run: pnpm install --frozen-lockfile
60 +
61 + - name: Patch src-tauri/.cargo/config.toml for native Windows build
62 + shell: pwsh
63 + run: |
64 + # The .cargo/config.toml is authored for macOS hosts: the [env]
65 + # section force-sets CC/CXX to /usr/bin/clang and a macOS deployment
66 + # target, none of which exist on this runner. Strip that section so
67 + # Rust falls back to the runner's native MSVC toolchain. The
68 + # per-Apple-target and Android sections are inert here and left alone.
69 + $path = "src-tauri/.cargo/config.toml"
70 + if (Test-Path $path) {
71 + $lines = Get-Content $path
72 + $out = [System.Collections.Generic.List[string]]::new()
73 + $skipSection = $false
74 +
75 + foreach ($line in $lines) {
76 + if ($line -match '^\[env\]') {
77 + $skipSection = $true
78 + continue
79 + }
80 + if ($skipSection -and $line -match '^\[') {
81 + $skipSection = $false
82 + }
83 + if (-not $skipSection) {
84 + $out.Add($line)
85 + }
86 + }
87 +
88 + $out | Set-Content $path -Encoding UTF8
89 + Write-Host "Patched src-tauri/.cargo/config.toml — removed macOS-only [env] section"
90 + } else {
91 + Write-Host "No src-tauri/.cargo/config.toml to patch"
92 + }
93 +
94 + - name: Setup MSVC developer environment
95 + uses: ilammy/msvc-dev-cmd@v1
96 +
97 + - name: Build App
98 + shell: pwsh
99 + run: pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json
100 +
101 + # The .msi/.exe above are for direct download only. A Store MSI/EXE
102 + # submission is just a hosted installer URL, so it never gets package
103 + # identity and Windows.Services.Store add-ons would fail from it. The MSIX
104 + # below is what goes to Partner Center. It is deliberately left unsigned:
105 + # the Store re-signs with the publisher cert. Skipped when the MSIX
106 + # secrets are absent.
107 + - name: Pack MSIX for Partner Center
108 + if: ${{ env.MSIX_PUBLISHER != '' && env.MSIX_IDENTITY_NAME != '' }}
109 + shell: pwsh
110 + run: ./scripts/build-msix.ps1
111 +
112 + - name: Extract version from tauri.prod-windows-microsoftstore.conf.json
113 + id: get_version
114 + shell: pwsh
115 + run: |
116 + $json = Get-Content src-tauri/tauri.prod-windows-microsoftstore.conf.json | ConvertFrom-Json
117 + echo "VERSION=$($json.version)" >> $env:GITHUB_ENV
118 +
119 + - name: Release
120 + uses: softprops/action-gh-release@v2
121 + with:
122 + draft: true
123 + prerelease: false
124 + body: "See the assets to download this version and install."
125 + name: "Siti AI v${{ env.VERSION }} (Windows Microsoft Store)"
126 + files: |
127 + src-tauri/target/release/bundle/msi/*.msi
128 + src-tauri/target/release/bundle/nsis/*.exe
129 + src-tauri/target/release/bundle/msix/*.msix
.gitignore
+4
@@ -60,3 +60,7 @@ keystore.properties
60 /xcode/*.xcodeproj/
61 /xcode/build/
62 /xcode/.swiftpm/
63 +
64 +# Local AI/agent tooling state — never commit to this public repo
65 +.claude/
66 +.playwright-mcp/
package.json
+1 -1
@@ -1,7 +1,7 @@
1 {
2 "name": "sitiai",
3 "private": true,
4 - "version": "0.1.0",
4 + "version": "1.0.0",
5 "type": "module",
6 "packageManager": "pnpm@11.8.0",
7 "scripts": {
scripts/build-msix.ps1 new
+157
@@ -0,0 +1,157 @@
1 +<#
2 +.SYNOPSIS
3 + Pack the built Windows app into an MSIX for Microsoft Store submission.
4 +
5 +.DESCRIPTION
6 + Runs after `pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json`.
7 + Tauri emits an .exe plus .msi/.exe installers; this assembles a full-trust
8 + MSIX layout around the raw .exe and calls makeappx.
9 +
10 + The MSIX is the Store artifact. The .msi/.exe stay useful for direct
11 + download, but a Store MSI/EXE submission gets no package identity, so
12 + in-app purchase only works from this MSIX.
13 +
14 + Leave the package unsigned for Partner Center — the Store re-signs it with
15 + the publisher certificate. Sign it locally only to sideload for testing.
16 +
17 +.PARAMETER IdentityName
18 + Package/Identity/@Name from Partner Center (Product identity page),
19 + e.g. "SplitfireAB.SitiAI". Defaults to $env:MSIX_IDENTITY_NAME.
20 +
21 +.PARAMETER Publisher
22 + Package/Identity/@Publisher from Partner Center, the full subject string,
23 + e.g. "CN=1234ABCD-....". Defaults to $env:MSIX_PUBLISHER.
24 +
25 +.PARAMETER Version
26 + Four-part version. The Store requires the revision (fourth) part to be 0.
27 + Defaults to the version in tauri.prod-windows-microsoftstore.conf.json.
28 +
29 +.PARAMETER ValidateOnly
30 + Check the manifest against makeappx's schema using stub payload files,
31 + without needing a real build. Manifest schema errors are otherwise only
32 + discovered after the ~50-minute Rust build, so CI runs this first to fail
33 + in seconds instead.
34 +#>
35 +[CmdletBinding()]
36 +param(
37 + [string]$IdentityName = $env:MSIX_IDENTITY_NAME,
38 + [string]$Publisher = $env:MSIX_PUBLISHER,
39 + [string]$Version,
40 + [string]$Configuration = "release",
41 + [switch]$ValidateOnly
42 +)
43 +
44 +$ErrorActionPreference = "Stop"
45 +
46 +$repoRoot = Split-Path -Parent $PSScriptRoot
47 +$tauriDir = Join-Path $repoRoot "src-tauri"
48 +$confPath = Join-Path $tauriDir "tauri.prod-windows-microsoftstore.conf.json"
49 +$conf = Get-Content $confPath -Raw | ConvertFrom-Json
50 +$productName = $conf.productName
51 +
52 +# Cargo names the raw binary in target/release/ after Cargo.toml's [package]
53 +# name, NOT tauri.conf.json's productName — that friendly name only gets
54 +# applied to the bundled .msi/.exe Tauri's bundler produces afterwards. Read
55 +# it from Cargo.toml directly rather than hardcoding, since it's the actual
56 +# source of truth for what cargo build emits.
57 +$cargoTomlPath = Join-Path $tauriDir "Cargo.toml"
58 +$cargoNameLine = Select-String -Path $cargoTomlPath -Pattern '^\s*name\s*=\s*"([^"]+)"' | Select-Object -First 1
59 +if (-not $cargoNameLine) {
60 + throw "Could not find [package] name in $cargoTomlPath."
61 +}
62 +$cargoBinName = $cargoNameLine.Matches[0].Groups[1].Value
63 +
64 +if (-not $IdentityName) {
65 + throw "IdentityName is required. Pass -IdentityName or set MSIX_IDENTITY_NAME. Find it in Partner Center under Product identity."
66 +}
67 +if (-not $Publisher) {
68 + throw "Publisher is required. Pass -Publisher or set MSIX_PUBLISHER. It is the full 'CN=...' string from Partner Center, not the display name."
69 +}
70 +
71 +if (-not $Version) {
72 + # tauri.conf.json carries a three-part version; MSIX wants four parts with a
73 + # trailing 0, since the Store reserves the revision part for itself.
74 + $Version = "$($conf.version).0"
75 +}
76 +if ($Version -notmatch '^\d+\.\d+\.\d+\.0$') {
77 + throw "Version must be four parts ending in 0 (got '$Version'). The Microsoft Store rejects packages whose revision part is non-zero."
78 +}
79 +
80 +$targetDir = Join-Path $tauriDir "target\$Configuration"
81 +$exePath = Join-Path $targetDir "$cargoBinName.exe"
82 +if (-not $ValidateOnly -and -not (Test-Path $exePath)) {
83 + throw "Built executable not found at $exePath. Run 'pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json' first."
84 +}
85 +
86 +$layoutDir = Join-Path $targetDir "msix-layout"
87 +$assetsDir = Join-Path $layoutDir "Assets"
88 +if (Test-Path $layoutDir) { Remove-Item $layoutDir -Recurse -Force }
89 +New-Item -ItemType Directory -Path $assetsDir -Force | Out-Null
90 +
91 +# Renamed to the friendly name here (not $cargoBinName) so it matches the
92 +# Executable= attribute AppxManifest.xml already declares — the source
93 +# filename and the packaged filename don't have to match.
94 +if ($ValidateOnly) {
95 + # A stub stands in for the real binary: makeappx validates the manifest
96 + # against its schema regardless of what the payload files actually contain,
97 + # which is the whole point of running this before the build.
98 + Set-Content (Join-Path $layoutDir "$productName.exe") "stub" -Encoding ASCII
99 +} else {
100 + Copy-Item $exePath (Join-Path $layoutDir "$productName.exe")
101 +}
102 +
103 +# Tauri's WebView2 install modes are installer-only concepts; an MSIX cannot
104 +# run a bootstrapper, so the package relies on the Evergreen WebView2 runtime
105 +# that ships with Windows 11 and is serviced onto Windows 10. Any WebView2
106 +# loader emitted next to the .exe still needs to travel with it.
107 +Get-ChildItem $targetDir -Filter "WebView2Loader.dll" -ErrorAction SilentlyContinue |
108 + ForEach-Object { Copy-Item $_.FullName $layoutDir }
109 +
110 +# Exactly the logos AppxManifest.xml references.
111 +$logos = @(
112 + "Square44x44Logo.png",
113 + "Square71x71Logo.png",
114 + "Square150x150Logo.png",
115 + "StoreLogo.png"
116 +)
117 +foreach ($logo in $logos) {
118 + $source = Join-Path $tauriDir "icons\$logo"
119 + if (-not (Test-Path $source)) {
120 + throw "Missing icon $source. Regenerate the Windows icon set with 'pnpm tauri icon'."
121 + }
122 + Copy-Item $source (Join-Path $assetsDir $logo)
123 +}
124 +
125 +$manifest = Get-Content (Join-Path $tauriDir "msix\AppxManifest.xml") -Raw
126 +$manifest = $manifest.Replace("__IDENTITY_NAME__", $IdentityName)
127 +$manifest = $manifest.Replace("__PUBLISHER__", $Publisher)
128 +$manifest = $manifest.Replace("__VERSION__", $Version)
129 +Set-Content (Join-Path $layoutDir "AppxManifest.xml") $manifest -Encoding UTF8
130 +
131 +$makeappx = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter "makeappx.exe" -ErrorAction SilentlyContinue |
132 + Where-Object { $_.FullName -match "x64" } |
133 + Sort-Object FullName -Descending |
134 + Select-Object -First 1
135 +if (-not $makeappx) {
136 + throw "makeappx.exe not found. Install the Windows 10/11 SDK."
137 +}
138 +
139 +$outputDir = Join-Path $targetDir "bundle\msix"
140 +New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
141 +$msixPath = Join-Path $outputDir "$productName-$Version.msix"
142 +if (Test-Path $msixPath) { Remove-Item $msixPath -Force }
143 +
144 +& $makeappx.FullName pack /d $layoutDir /p $msixPath /o
145 +if ($LASTEXITCODE -ne 0) {
146 + throw "makeappx pack failed with exit code $LASTEXITCODE"
147 +}
148 +
149 +if ($ValidateOnly) {
150 + # Throw the stub package away — it contains a 4-byte "exe" and would be
151 + # actively dangerous to mistake for a real artifact later in the job.
152 + Remove-Item $msixPath -Force
153 + Remove-Item $layoutDir -Recurse -Force
154 + Write-Host "AppxManifest.xml passed makeappx schema validation."
155 +} else {
156 + Write-Host "MSIX written to $msixPath"
157 +}
src-tauri/msix/AppxManifest.xml new
+68
@@ -0,0 +1,68 @@
1 +<?xml version="1.0" encoding="utf-8"?>
2 +<!--
3 + MSIX manifest for the Microsoft Store submission.
4 +
5 + Tauri only bundles .msi/.exe, and a Store MSI/EXE submission is nothing but
6 + a hosted installer URL — the Store never licenses those, so they get no
7 + package identity and Windows.Services.Store add-ons cannot work from them.
8 + This manifest wraps the same built .exe as a full-trust MSIX, which is what
9 + gives the app the Store identity that StoreContext needs.
10 +
11 + __IDENTITY_NAME__, __PUBLISHER__ and __VERSION__ are placeholders filled in
12 + by scripts/build-msix.ps1. Name and Publisher must match the values Partner
13 + Center shows under Product identity for this app, character for character,
14 + or the Store rejects the upload.
15 +
16 + Siti AI is a Splitfire AB product (see NOTICE), so Publisher (CN=...) and
17 + PublisherDisplayName come from the same Partner Center account as the other
18 + Splitfire AB apps. Only the Identity Name below is unique to Siti AI's product.
19 +-->
20 +<Package
21 + xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
22 + xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
23 + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
24 + IgnorableNamespaces="uap rescap">
25 +
26 + <Identity
27 + Name="__IDENTITY_NAME__"
28 + Publisher="__PUBLISHER__"
29 + Version="__VERSION__"
30 + ProcessorArchitecture="x64" />
31 +
32 + <Properties>
33 + <DisplayName>Siti AI</DisplayName>
34 + <!--
35 + PublisherDisplayName must match the account's registered publisher
36 + display name in Partner Center EXACTLY, or upload is rejected at
37 + acceptance validation (not at makeappx time).
38 + -->
39 + <PublisherDisplayName>Splitfire AB Developer</PublisherDisplayName>
40 + <Logo>Assets\StoreLogo.png</Logo>
41 + </Properties>
42 +
43 + <Dependencies>
44 + <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
45 + </Dependencies>
46 +
47 + <Resources>
48 + <Resource Language="en-us" />
49 + </Resources>
50 +
51 + <Applications>
52 + <Application Id="SitiAI" Executable="Siti AI.exe" EntryPoint="Windows.FullTrustApplication">
53 + <uap:VisualElements
54 + DisplayName="Siti AI"
55 + Description="Private, on-device AI assistant."
56 + BackgroundColor="transparent"
57 + Square150x150Logo="Assets\Square150x150Logo.png"
58 + Square44x44Logo="Assets\Square44x44Logo.png">
59 + <uap:DefaultTile Square71x71Logo="Assets\Square71x71Logo.png" />
60 + </uap:VisualElements>
61 + </Application>
62 + </Applications>
63 +
64 + <Capabilities>
65 + <Capability Name="internetClient" />
66 + <rescap:Capability Name="runFullTrust" />
67 + </Capabilities>
68 +</Package>
src-tauri/tauri.prod-windows-microsoftstore.conf.json new
+26
@@ -0,0 +1,26 @@
1 +{
2 + "productName": "Siti AI",
3 + "identifier": "ai.siti.Siti",
4 + "version": "1.0.0",
5 + "bundle": {
6 + "icon": [
7 + "icons/icon.ico",
8 + "icons/Square30x30Logo.png",
9 + "icons/Square44x44Logo.png",
10 + "icons/Square71x71Logo.png",
11 + "icons/Square89x89Logo.png",
12 + "icons/Square107x107Logo.png",
13 + "icons/Square142x142Logo.png",
14 + "icons/Square150x150Logo.png",
15 + "icons/Square284x284Logo.png",
16 + "icons/Square310x310Logo.png",
17 + "icons/StoreLogo.png"
18 + ],
19 + "publisher": "Splitfire AB",
20 + "windows": {
21 + "webviewInstallMode": {
22 + "type": "offlineInstaller"
23 + }
24 + }
25 + }
26 +}