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
+}