Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
392 lines
17 KiB
YAML
392 lines
17 KiB
YAML
name: Submit Desktop to Microsoft Store
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
release_tag:
|
|
required: true
|
|
type: string
|
|
store_environment:
|
|
required: true
|
|
type: string
|
|
submit:
|
|
required: true
|
|
type: boolean
|
|
target_commit:
|
|
required: true
|
|
type: string
|
|
verify_release_tag:
|
|
default: true
|
|
required: false
|
|
type: boolean
|
|
workflow_dispatch:
|
|
inputs:
|
|
release_tag:
|
|
description: Plain stable tag to package, such as v1.2.3
|
|
required: true
|
|
type: string
|
|
target_commit:
|
|
description: Commit expected to be referenced by the stable tag
|
|
required: true
|
|
type: string
|
|
store_environment:
|
|
description: Complete Partner Center account and application profile
|
|
options:
|
|
- microsoft-store-test
|
|
- microsoft-store-production
|
|
required: true
|
|
type: choice
|
|
submit:
|
|
default: false
|
|
description: Submit the validated package to Partner Center
|
|
required: true
|
|
type: boolean
|
|
verify_release_tag:
|
|
default: true
|
|
description: Require the release tag to resolve to the target commit
|
|
required: true
|
|
type: boolean
|
|
|
|
concurrency:
|
|
group: desktop-store-${{ inputs.store_environment }}-${{ inputs.release_tag }}
|
|
cancel-in-progress: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
|
|
jobs:
|
|
build-and-submit:
|
|
name: Build and submit stable Store package
|
|
environment: ${{ inputs.store_environment }}
|
|
runs-on: windows-latest
|
|
timeout-minutes: 60
|
|
env:
|
|
TUTTI_DESKTOP_RELEASE_TAG: ${{ inputs.release_tag }}
|
|
TUTTI_DESKTOP_RELEASE_TARGET: ${{ inputs.target_commit }}
|
|
TUTTI_MICROSOFT_STORE_PRODUCT_ID: ${{ vars.TUTTI_MICROSOFT_STORE_PRODUCT_ID }}
|
|
TUTTI_STORE_APPLICATION_ID: ${{ vars.TUTTI_STORE_APPLICATION_ID }}
|
|
TUTTI_STORE_DISPLAY_NAME: ${{ vars.TUTTI_STORE_DISPLAY_NAME }}
|
|
TUTTI_STORE_IDENTITY_NAME: ${{ vars.TUTTI_STORE_IDENTITY_NAME }}
|
|
TUTTI_STORE_PUBLISHER: ${{ vars.TUTTI_STORE_PUBLISHER }}
|
|
TUTTI_STORE_PUBLISHER_DISPLAY_NAME: ${{ vars.TUTTI_STORE_PUBLISHER_DISPLAY_NAME }}
|
|
TUTTI_STORE_MSIX_MIN_VERSION: 10.0.17763.0
|
|
|
|
steps:
|
|
- name: Validate stable submission request
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
if ($env:TUTTI_DESKTOP_RELEASE_TAG -notmatch '^v(?<version>(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))$') {
|
|
throw "Microsoft Store submission requires a plain stable tag, got $env:TUTTI_DESKTOP_RELEASE_TAG"
|
|
}
|
|
$versionParts = @($Matches.version.Split('.') | ForEach-Object { [int]$_ })
|
|
if ($versionParts.Where({ $_ -gt 65535 }).Count -gt 0) {
|
|
throw "Microsoft Store version components must not exceed 65535: $($Matches.version)"
|
|
}
|
|
"TUTTI_DESKTOP_BUILD_VERSION=$($Matches.version)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
|
|
|
$required = @(
|
|
'TUTTI_MICROSOFT_STORE_PRODUCT_ID',
|
|
'TUTTI_STORE_APPLICATION_ID',
|
|
'TUTTI_STORE_DISPLAY_NAME',
|
|
'TUTTI_STORE_IDENTITY_NAME',
|
|
'TUTTI_STORE_PUBLISHER',
|
|
'TUTTI_STORE_PUBLISHER_DISPLAY_NAME'
|
|
)
|
|
$missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) })
|
|
if ($missing.Count -gt 0) {
|
|
throw "Missing Microsoft Store environment variables: $($missing -join ', ')"
|
|
}
|
|
|
|
- name: Checkout release target
|
|
uses: actions/checkout@v6
|
|
with:
|
|
fetch-depth: 0
|
|
ref: ${{ inputs.target_commit }}
|
|
|
|
- name: Verify stable tag points to release target
|
|
if: ${{ inputs.verify_release_tag }}
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$tagCommit = (git rev-list -n 1 "refs/tags/$env:TUTTI_DESKTOP_RELEASE_TAG").Trim()
|
|
$headCommit = (git rev-parse HEAD).Trim()
|
|
if (-not $tagCommit) { throw "Tag $env:TUTTI_DESKTOP_RELEASE_TAG was not found" }
|
|
if ($tagCommit -ne $headCommit) {
|
|
throw "Tag $env:TUTTI_DESKTOP_RELEASE_TAG points to $tagCommit, expected $headCommit"
|
|
}
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@v6
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v6
|
|
with:
|
|
node-version-file: .node-version
|
|
cache: pnpm
|
|
cache-dependency-path: pnpm-lock.yaml
|
|
|
|
- name: Setup Go
|
|
uses: actions/setup-go@v6
|
|
with:
|
|
go-version: "1.24.5"
|
|
cache-dependency-path: |
|
|
go.work
|
|
go.work.sum
|
|
packages/agent/daemon/go.mod
|
|
packages/agent/daemon/go.sum
|
|
services/tuttid/go.mod
|
|
services/tuttid/go.sum
|
|
|
|
- name: Install dependencies
|
|
shell: pwsh
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Install Electron runtime
|
|
shell: pwsh
|
|
run: pnpm --filter @tutti-os/desktop exec install-electron
|
|
|
|
- name: Align package version with stable tag
|
|
working-directory: apps/desktop
|
|
shell: pwsh
|
|
run: node scripts/apply-ci-release-version.mjs "$env:TUTTI_DESKTOP_RELEASE_TAG"
|
|
|
|
- name: Build Windows Store package
|
|
shell: pwsh
|
|
env:
|
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
|
run: pnpm --filter @tutti-os/desktop build:win:store
|
|
|
|
- id: package
|
|
name: Validate Store package identity and version
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
$packages = @(Get-ChildItem 'apps/desktop/dist' -Filter '*.appx' -File)
|
|
if ($packages.Count -ne 1) {
|
|
throw "Expected exactly one AppX package, found $($packages.Count)"
|
|
}
|
|
|
|
$package = $packages[0]
|
|
$manifestRoot = Join-Path $env:RUNNER_TEMP 'tutti-store-manifest'
|
|
if (Test-Path $manifestRoot) { Remove-Item -Recurse -Force $manifestRoot }
|
|
[System.IO.Compression.ZipFile]::ExtractToDirectory($package.FullName, $manifestRoot)
|
|
[xml]$manifest = Get-Content (Join-Path $manifestRoot 'AppxManifest.xml') -Raw
|
|
$identity = $manifest.Package.Identity
|
|
$identityName = $identity.GetAttribute('Name')
|
|
$publisher = $identity.GetAttribute('Publisher')
|
|
$version = $identity.GetAttribute('Version')
|
|
$architecture = $identity.GetAttribute('ProcessorArchitecture')
|
|
$minVersion = $manifest.SelectSingleNode("/*[local-name()='Package']/*[local-name()='Dependencies']/*[local-name()='TargetDeviceFamily']").GetAttribute('MinVersion')
|
|
$maxVersionTested = $manifest.SelectSingleNode("/*[local-name()='Package']/*[local-name()='Dependencies']/*[local-name()='TargetDeviceFamily']").GetAttribute('MaxVersionTested')
|
|
$expectedVersion = "$env:TUTTI_DESKTOP_BUILD_VERSION.0"
|
|
try {
|
|
$parsedMinVersion = [version]$minVersion
|
|
$parsedMaxVersionTested = [version]$maxVersionTested
|
|
$requiredMinVersion = [version]$env:TUTTI_STORE_MSIX_MIN_VERSION
|
|
} catch {
|
|
throw "Invalid Store package Windows version metadata: MinVersion=$minVersion, MaxVersionTested=$maxVersionTested"
|
|
}
|
|
$packageDisplayName = $manifest.SelectSingleNode("/*[local-name()='Package']/*[local-name()='Properties']/*[local-name()='DisplayName']").InnerText
|
|
$application = $manifest.SelectSingleNode("/*[local-name()='Package']/*[local-name()='Applications']/*[local-name()='Application']")
|
|
$applicationId = $application.GetAttribute('Id')
|
|
$executable = $application.GetAttribute('Executable')
|
|
$entryPoint = $application.GetAttribute('EntryPoint')
|
|
$visualElements = $application.SelectSingleNode("*[local-name()='VisualElements']")
|
|
$applicationDisplayName = $visualElements.GetAttribute('DisplayName')
|
|
$applicationBackgroundColor = $visualElements.GetAttribute('BackgroundColor')
|
|
$applicationAppListEntry = $visualElements.GetAttribute('AppListEntry')
|
|
$desktopShortcut = $application.SelectSingleNode("*[local-name()='Extensions']/*[local-name()='Extension' and @Category='windows.shortcut']/*[local-name()='Shortcut']")
|
|
|
|
if ($identityName -ne $env:TUTTI_STORE_IDENTITY_NAME) {
|
|
throw "Store identity mismatch: $identityName"
|
|
}
|
|
if ($publisher -ne $env:TUTTI_STORE_PUBLISHER) {
|
|
throw "Store publisher mismatch: $publisher"
|
|
}
|
|
if ($version -ne $expectedVersion) {
|
|
throw "Store version mismatch: $version, expected $expectedVersion"
|
|
}
|
|
if ($architecture -ne 'x64') {
|
|
throw "Store architecture mismatch: $architecture"
|
|
}
|
|
if ($parsedMinVersion -lt $requiredMinVersion) {
|
|
throw "Store package MinVersion $minVersion is below the Microsoft Store MSIX floor $env:TUTTI_STORE_MSIX_MIN_VERSION"
|
|
}
|
|
if ($parsedMaxVersionTested -lt $parsedMinVersion) {
|
|
throw "Store package MaxVersionTested $maxVersionTested is below MinVersion $minVersion"
|
|
}
|
|
if ($packageDisplayName -ne $env:TUTTI_STORE_DISPLAY_NAME) {
|
|
throw "Store package display name mismatch: $packageDisplayName"
|
|
}
|
|
if ($applicationId -ne $env:TUTTI_STORE_APPLICATION_ID) {
|
|
throw "Store application id mismatch: $applicationId"
|
|
}
|
|
if ($executable -ne 'app\Tutti.exe') {
|
|
throw "Store executable mismatch: $executable"
|
|
}
|
|
if ($entryPoint -ne 'Windows.FullTrustApplication') {
|
|
throw "Store entry point mismatch: $entryPoint"
|
|
}
|
|
if ($applicationDisplayName -ne 'Tutti') {
|
|
throw "Installed application display name mismatch: $applicationDisplayName"
|
|
}
|
|
if ($applicationBackgroundColor -ne 'transparent') {
|
|
throw "Installed application tile background must be transparent: $applicationBackgroundColor"
|
|
}
|
|
if ($applicationAppListEntry -ne 'default') {
|
|
throw "Installed application must appear in the Start menu: $applicationAppListEntry"
|
|
}
|
|
if ($null -eq $desktopShortcut) {
|
|
throw "Store manifest does not declare the Tutti desktop shortcut"
|
|
}
|
|
if ($desktopShortcut.GetAttribute('File') -ne '$(Desktop)\Tutti.lnk') {
|
|
throw "Store desktop shortcut path mismatch: $($desktopShortcut.GetAttribute('File'))"
|
|
}
|
|
if ($desktopShortcut.GetAttribute('Icon') -ne 'app\Tutti.exe') {
|
|
throw "Store desktop shortcut icon mismatch: $($desktopShortcut.GetAttribute('Icon'))"
|
|
}
|
|
$storeAssets = @(
|
|
'StoreLogo.png',
|
|
'Square44x44Logo.png',
|
|
'Square150x150Logo.png',
|
|
'Wide310x150Logo.png'
|
|
)
|
|
foreach ($assetName in $storeAssets) {
|
|
$sourceAsset = Join-Path $PWD "apps\desktop\build\appx\$assetName"
|
|
$packagedAsset = Join-Path $manifestRoot "assets\$assetName"
|
|
if (-not (Test-Path -LiteralPath $packagedAsset -PathType Leaf)) {
|
|
throw "Store package is missing branded asset: $assetName"
|
|
}
|
|
$sourceHash = (Get-FileHash -LiteralPath $sourceAsset -Algorithm SHA256).Hash
|
|
$packagedHash = (Get-FileHash -LiteralPath $packagedAsset -Algorithm SHA256).Hash
|
|
if ($packagedHash -ne $sourceHash) {
|
|
throw "Store package did not use branded asset: $assetName"
|
|
}
|
|
}
|
|
$loginProtocol = $manifest.SelectSingleNode("//*[local-name()='Protocol' and @Name='tutti']")
|
|
if ($null -eq $loginProtocol) {
|
|
throw "Store manifest does not register the tutti login protocol"
|
|
}
|
|
$fullTrustCapability = $manifest.SelectSingleNode("//*[local-name()='Capability' and @Name='runFullTrust']")
|
|
if ($null -eq $fullTrustCapability) {
|
|
throw "Store manifest does not declare runFullTrust"
|
|
}
|
|
|
|
$sha256 = (Get-FileHash $package.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
@{
|
|
architecture = $architecture
|
|
applicationDisplayName = $applicationDisplayName
|
|
applicationId = $applicationId
|
|
commit = (git rev-parse HEAD).Trim()
|
|
executable = $executable
|
|
identityName = $identityName
|
|
package = $package.Name
|
|
packageDisplayName = $packageDisplayName
|
|
productId = $env:TUTTI_MICROSOFT_STORE_PRODUCT_ID
|
|
publisher = $publisher
|
|
minVersion = $minVersion
|
|
maxVersionTested = $maxVersionTested
|
|
submissionPackage = [System.IO.Path]::ChangeExtension($package.Name, '.msix')
|
|
sha256 = $sha256
|
|
tag = $env:TUTTI_DESKTOP_RELEASE_TAG
|
|
version = $version
|
|
} | ConvertTo-Json | Set-Content 'apps/desktop/dist/store-submission.json' -Encoding utf8
|
|
|
|
"path=$($package.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
"sha256=$sha256" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
"minVersion=$minVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
"maxVersionTested=$maxVersionTested" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
|
|
- id: submission
|
|
name: Prepare MSIX Store submission package
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$appxPath = '${{ steps.package.outputs.path }}'
|
|
$msixPath = [System.IO.Path]::ChangeExtension($appxPath, '.msix')
|
|
Copy-Item -LiteralPath $appxPath -Destination $msixPath -Force
|
|
$appxSha256 = (Get-FileHash -LiteralPath $appxPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
$msixSha256 = (Get-FileHash -LiteralPath $msixPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if ($appxSha256 -ne $msixSha256) {
|
|
throw "MSIX submission alias changed the validated package bytes"
|
|
}
|
|
"path=$msixPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
"sha256=$msixSha256" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
|
|
|
|
- name: Upload validated Store submission artifact
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: tutti-desktop-store-${{ inputs.release_tag }}
|
|
path: |
|
|
apps/desktop/dist/*.appx
|
|
apps/desktop/dist/*.msix
|
|
apps/desktop/dist/store-submission.json
|
|
if-no-files-found: error
|
|
retention-days: 30
|
|
|
|
- name: Setup Microsoft Store Developer CLI
|
|
if: ${{ inputs.submit }}
|
|
uses: microsoft/microsoft-store-apppublisher@v1.1
|
|
with:
|
|
version: v0.4.1
|
|
|
|
- name: Authenticate Partner Center
|
|
if: ${{ inputs.submit }}
|
|
shell: pwsh
|
|
env:
|
|
MICROSOFT_STORE_CLIENT_ID: ${{ secrets.MICROSOFT_STORE_CLIENT_ID }}
|
|
MICROSOFT_STORE_CLIENT_SECRET: ${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}
|
|
MICROSOFT_STORE_SELLER_ID: ${{ secrets.MICROSOFT_STORE_SELLER_ID }}
|
|
MICROSOFT_STORE_TENANT_ID: ${{ secrets.MICROSOFT_STORE_TENANT_ID }}
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$required = @(
|
|
'MICROSOFT_STORE_CLIENT_ID',
|
|
'MICROSOFT_STORE_CLIENT_SECRET',
|
|
'MICROSOFT_STORE_SELLER_ID',
|
|
'MICROSOFT_STORE_TENANT_ID'
|
|
)
|
|
$missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) })
|
|
if ($missing.Count -gt 0) { throw "Missing Partner Center credentials: $($missing -join ', ')" }
|
|
msstore reconfigure `
|
|
--tenantId $env:MICROSOFT_STORE_TENANT_ID `
|
|
--sellerId $env:MICROSOFT_STORE_SELLER_ID `
|
|
--clientId $env:MICROSOFT_STORE_CLIENT_ID `
|
|
--clientSecret $env:MICROSOFT_STORE_CLIENT_SECRET
|
|
|
|
- name: Submit package for Microsoft certification
|
|
if: ${{ inputs.submit }}
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$msixPath = '${{ steps.submission.outputs.path }}'
|
|
$packageSize = (Get-Item -LiteralPath $msixPath).Length
|
|
"Microsoft Store upload package size: $packageSize bytes"
|
|
msstore publish $msixPath -id "$env:TUTTI_MICROSOFT_STORE_PRODUCT_ID" -ut 600
|
|
|
|
- name: Record submission summary
|
|
shell: pwsh
|
|
run: |
|
|
@"
|
|
## Microsoft Store package
|
|
|
|
- Environment: `${{ inputs.store_environment }}`
|
|
- Product ID: `$env:TUTTI_MICROSOFT_STORE_PRODUCT_ID`
|
|
- Tag: `$env:TUTTI_DESKTOP_RELEASE_TAG`
|
|
- Commit: `$(git rev-parse HEAD)`
|
|
- Store version: `${{ steps.package.outputs.version }}`
|
|
- Windows MinVersion: `${{ steps.package.outputs.minVersion }}`
|
|
- MaxVersionTested: `${{ steps.package.outputs.maxVersionTested }}`
|
|
- SHA-256: `${{ steps.package.outputs.sha256 }}`
|
|
- MSIX SHA-256: `${{ steps.submission.outputs.sha256 }}`
|
|
- Submitted: `${{ inputs.submit }}`
|
|
|
|
The workflow stops after Partner Center accepts the submission request; it does not wait for certification.
|
|
"@ | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
|