Files
tutti/.github/workflows/desktop-store-submit.yml
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

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