Skip to content

Commit 1ebf0be

Browse files
committed
Release: harden Microsoft Store submission
1 parent 2677049 commit 1ebf0be

3 files changed

Lines changed: 235 additions & 80 deletions

File tree

.github/workflows/release.yml

Lines changed: 5 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -327,82 +327,8 @@ jobs:
327327
if: startsWith(github.ref, 'refs/tags/')
328328
shell: pwsh
329329
run: |
330-
$productId = "${{ vars.STORE_PRODUCT_ID }}"
331-
332-
# 1. Stage the new package into a draft submission without committing,
333-
# so the "What's New" notes can be attached to the same submission.
334-
msstore publish "dist/Q1View-windows-x64.msix" -id $productId --noCommit
335-
if ($LASTEXITCODE -ne 0) { throw "msstore publish (staging) failed with exit code $LASTEXITCODE" }
336-
337-
# 2. Inject the curated What's New text (from CHANGELOG, prepared above)
338-
# and keep listing copy pinned to docs/STORE_LISTING.md. updateMetadata
339-
# submits the whole listing JSON, so rewrite the canonical text fields
340-
# too; otherwise stale hard-wrapped Partner Center metadata can be
341-
# re-committed while we only meant to update release notes.
342-
$notes = ""
343-
if (Test-Path STORE_WHATS_NEW.txt) { $notes = (Get-Content STORE_WHATS_NEW.txt -Raw).Trim() }
344-
if ($notes) {
345-
try {
346-
function Get-StoreListingBlock($headingPattern) {
347-
if (-not (Test-Path docs/STORE_LISTING.md)) { return "" }
348-
$lines = Get-Content docs/STORE_LISTING.md -Encoding utf8
349-
$inSection = $false
350-
$inFence = $false
351-
$block = New-Object System.Collections.Generic.List[string]
352-
foreach ($line in $lines) {
353-
if (-not $inSection) {
354-
if ($line -match $headingPattern) { $inSection = $true }
355-
continue
356-
}
357-
if ($line -match '^```') {
358-
if ($inFence) { break }
359-
$inFence = $true
360-
continue
361-
}
362-
if ($inFence) { $block.Add($line) }
363-
}
364-
return (($block -join "`n").Trim())
365-
}
366-
367-
$storeDescription = Get-StoreListingBlock '^## Description '
368-
$storeShortDescription = Get-StoreListingBlock '^## Short description '
369-
370-
# msstore can interleave informational lines with the JSON on
371-
# stdout, so slice from the first '{' to the last '}' before parsing.
372-
$raw = (msstore submission get $productId | Out-String)
373-
$open = $raw.IndexOf('{'); $close = $raw.LastIndexOf('}')
374-
if ($open -lt 0 -or $close -lt $open) { throw "submission get returned no JSON object" }
375-
$json = $raw.Substring($open, $close - $open + 1) | ConvertFrom-Json
376-
$script:releaseNotesUpdated = 0
377-
$script:descriptionUpdated = 0
378-
$script:shortDescriptionUpdated = 0
379-
function Set-ListingText($node, $releaseNotes, $description, $shortDescription) {
380-
if ($null -eq $node) { return }
381-
if ($node -is [pscustomobject]) {
382-
foreach ($p in $node.PSObject.Properties) {
383-
if ($p.Name -eq 'releaseNotes') { $p.Value = $releaseNotes; $script:releaseNotesUpdated++ }
384-
elseif ($description -and $p.Name -eq 'description') { $p.Value = $description; $script:descriptionUpdated++ }
385-
elseif ($shortDescription -and $p.Name -eq 'shortDescription') { $p.Value = $shortDescription; $script:shortDescriptionUpdated++ }
386-
else { Set-ListingText $p.Value $releaseNotes $description $shortDescription }
387-
}
388-
} elseif ($node -is [System.Collections.IEnumerable] -and $node -isnot [string]) {
389-
foreach ($item in $node) { Set-ListingText $item $releaseNotes $description $shortDescription }
390-
}
391-
}
392-
Set-ListingText $json $notes $storeDescription $storeShortDescription
393-
if ($script:releaseNotesUpdated -gt 0) {
394-
msstore submission updateMetadata $productId ($json | ConvertTo-Json -Depth 50 -Compress)
395-
Write-Host "Updated What's New in $($script:releaseNotesUpdated) listing(s)."
396-
Write-Host "Updated Store description in $($script:descriptionUpdated) listing(s)."
397-
Write-Host "Updated Store short description in $($script:shortDescriptionUpdated) listing(s)."
398-
} else {
399-
Write-Host "::warning::No 'releaseNotes' field found in submission metadata; What's New left unchanged."
400-
}
401-
} catch {
402-
Write-Host "::warning::What's New update failed (package still submitted): $($_.Exception.Message)"
403-
}
404-
}
405-
406-
# 3. Commit / submit the draft for certification.
407-
msstore submission publish $productId
408-
if ($LASTEXITCODE -ne 0) { throw "msstore submission publish failed with exit code $LASTEXITCODE" }
330+
.\build\Publish-MicrosoftStore.ps1 `
331+
-ProductId "${{ vars.STORE_PRODUCT_ID }}" `
332+
-PackagePath "dist/Q1View-windows-x64.msix" `
333+
-WhatsNewPath "STORE_WHATS_NEW.txt" `
334+
-StoreListingPath "docs/STORE_LISTING.md"

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ All notable changes to Q1View are documented here. Releases follow [semantic ver
88

99
---
1010

11+
## [2.7.5] — 2026-07-11
12+
13+
### Packaging
14+
- Microsoft Store release automation now checks every `msstore` exit code, emits verbose Store CLI logs, and recovers from a rejected draft by deleting the failed pending submission, restaging the MSIX, and retrying the publish step.
15+
16+
---
17+
1118
## [2.7.4] — 2026-07-10
1219

1320
### Fixed
@@ -289,7 +296,8 @@ All notable changes to Q1View are documented here. Releases follow [semantic ver
289296

290297
---
291298

292-
[Unreleased]: https://github.com/chammoru/Q1View/compare/v2.7.4...HEAD
299+
[Unreleased]: https://github.com/chammoru/Q1View/compare/v2.7.5...HEAD
300+
[2.7.5]: https://github.com/chammoru/Q1View/compare/v2.7.4...v2.7.5
293301
[2.7.4]: https://github.com/chammoru/Q1View/compare/v2.7.3...v2.7.4
294302
[2.7.3]: https://github.com/chammoru/Q1View/compare/v2.7.2...v2.7.3
295303
[2.7.2]: https://github.com/chammoru/Q1View/compare/v2.7.1...v2.7.2

build/Publish-MicrosoftStore.ps1

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
param(
2+
[Parameter(Mandatory = $true)]
3+
[string]$ProductId,
4+
5+
[Parameter(Mandatory = $true)]
6+
[string]$PackagePath,
7+
8+
[string]$WhatsNewPath = "STORE_WHATS_NEW.txt",
9+
[string]$StoreListingPath = "docs/STORE_LISTING.md"
10+
)
11+
12+
$ErrorActionPreference = "Stop"
13+
14+
function Invoke-MsStoreCommand {
15+
param(
16+
[Parameter(Mandatory = $true)]
17+
[string]$Description,
18+
19+
[Parameter(Mandatory = $true)]
20+
[string[]]$Arguments,
21+
22+
[int]$Attempts = 1,
23+
[switch]$AllowFailure
24+
)
25+
26+
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
27+
Write-Host "::group::$Description (attempt $attempt/$Attempts)"
28+
& msstore @Arguments
29+
$exitCode = $LASTEXITCODE
30+
Write-Host "::endgroup::"
31+
32+
if ($exitCode -eq 0) {
33+
return $true
34+
}
35+
36+
if ($attempt -lt $Attempts) {
37+
$delay = [Math]::Min(60, 10 * $attempt)
38+
Write-Host "::warning::$Description failed with exit code $exitCode; retrying in $delay seconds."
39+
Start-Sleep -Seconds $delay
40+
}
41+
}
42+
43+
if ($AllowFailure) {
44+
Write-Host "::warning::$Description failed after $Attempts attempt(s); continuing."
45+
return $false
46+
}
47+
48+
throw "$Description failed after $Attempts attempt(s)."
49+
}
50+
51+
function Remove-PendingStoreSubmission {
52+
Invoke-MsStoreCommand `
53+
-Description "Delete pending Microsoft Store submission" `
54+
-Arguments @("submission", "delete", $ProductId, "--no-confirm", "--verbose") `
55+
-AllowFailure | Out-Null
56+
}
57+
58+
function Stage-StorePackage {
59+
Invoke-MsStoreCommand `
60+
-Description "Stage Microsoft Store package" `
61+
-Arguments @("publish", $PackagePath, "-id", $ProductId, "--noCommit", "--verbose") `
62+
-Attempts 2 | Out-Null
63+
}
64+
65+
function Publish-StoreSubmission {
66+
Invoke-MsStoreCommand `
67+
-Description "Publish Microsoft Store submission" `
68+
-Arguments @("submission", "publish", $ProductId, "--verbose") `
69+
-Attempts 2 | Out-Null
70+
}
71+
72+
function Get-StoreListingBlock {
73+
param([Parameter(Mandatory = $true)][string]$HeadingPattern)
74+
75+
if (-not (Test-Path $StoreListingPath)) {
76+
return ""
77+
}
78+
79+
$lines = Get-Content $StoreListingPath -Encoding utf8
80+
$inSection = $false
81+
$inFence = $false
82+
$block = New-Object System.Collections.Generic.List[string]
83+
84+
foreach ($line in $lines) {
85+
if (-not $inSection) {
86+
if ($line -match $HeadingPattern) {
87+
$inSection = $true
88+
}
89+
continue
90+
}
91+
if ($line -match '^```') {
92+
if ($inFence) {
93+
break
94+
}
95+
$inFence = $true
96+
continue
97+
}
98+
if ($inFence) {
99+
$block.Add($line)
100+
}
101+
}
102+
103+
return (($block -join "`n").Trim())
104+
}
105+
106+
function Set-ListingText {
107+
param(
108+
$Node,
109+
[string]$ReleaseNotes,
110+
[string]$Description,
111+
[string]$ShortDescription
112+
)
113+
114+
if ($null -eq $Node) {
115+
return
116+
}
117+
118+
if ($Node -is [pscustomobject]) {
119+
foreach ($property in $Node.PSObject.Properties) {
120+
if ($property.Name -eq "releaseNotes") {
121+
$property.Value = $ReleaseNotes
122+
$script:ReleaseNotesUpdated++
123+
} elseif ($Description -and $property.Name -eq "description") {
124+
$property.Value = $Description
125+
$script:DescriptionUpdated++
126+
} elseif ($ShortDescription -and $property.Name -eq "shortDescription") {
127+
$property.Value = $ShortDescription
128+
$script:ShortDescriptionUpdated++
129+
} else {
130+
Set-ListingText $property.Value $ReleaseNotes $Description $ShortDescription
131+
}
132+
}
133+
} elseif ($Node -is [System.Collections.IEnumerable] -and $Node -isnot [string]) {
134+
foreach ($item in $Node) {
135+
Set-ListingText $item $ReleaseNotes $Description $ShortDescription
136+
}
137+
}
138+
}
139+
140+
function Update-StoreMetadata {
141+
if (-not (Test-Path $WhatsNewPath)) {
142+
Write-Host "::warning::$WhatsNewPath not found; Store What's New left unchanged."
143+
return $true
144+
}
145+
146+
$notes = (Get-Content $WhatsNewPath -Raw).Trim()
147+
if (-not $notes) {
148+
Write-Host "::warning::Store What's New is empty; Store metadata left unchanged."
149+
return $true
150+
}
151+
152+
$storeDescription = Get-StoreListingBlock '^## Description '
153+
$storeShortDescription = Get-StoreListingBlock '^## Short description '
154+
155+
$raw = (msstore submission get $ProductId --verbose | Out-String)
156+
if ($LASTEXITCODE -ne 0) {
157+
throw "msstore submission get failed with exit code $LASTEXITCODE"
158+
}
159+
160+
$open = $raw.IndexOf('{')
161+
$close = $raw.LastIndexOf('}')
162+
if ($open -lt 0 -or $close -lt $open) {
163+
throw "submission get returned no JSON object"
164+
}
165+
166+
$json = $raw.Substring($open, $close - $open + 1) | ConvertFrom-Json
167+
$script:ReleaseNotesUpdated = 0
168+
$script:DescriptionUpdated = 0
169+
$script:ShortDescriptionUpdated = 0
170+
Set-ListingText $json $notes $storeDescription $storeShortDescription
171+
172+
if ($script:ReleaseNotesUpdated -le 0) {
173+
Write-Host "::warning::No 'releaseNotes' field found in submission metadata; What's New left unchanged."
174+
return $true
175+
}
176+
177+
$metadata = $json | ConvertTo-Json -Depth 50 -Compress
178+
$ok = Invoke-MsStoreCommand `
179+
-Description "Update Microsoft Store metadata" `
180+
-Arguments @("submission", "updateMetadata", $ProductId, $metadata, "--verbose") `
181+
-Attempts 2 `
182+
-AllowFailure
183+
184+
if ($ok) {
185+
Write-Host "Updated What's New in $($script:ReleaseNotesUpdated) listing(s)."
186+
Write-Host "Updated Store description in $($script:DescriptionUpdated) listing(s)."
187+
Write-Host "Updated Store short description in $($script:ShortDescriptionUpdated) listing(s)."
188+
}
189+
190+
return $ok
191+
}
192+
193+
try {
194+
Stage-StorePackage
195+
} catch {
196+
Write-Host "::warning::Initial package staging failed: $($_.Exception.Message)"
197+
Remove-PendingStoreSubmission
198+
Stage-StorePackage
199+
}
200+
201+
$metadataUpdated = $false
202+
try {
203+
$metadataUpdated = Update-StoreMetadata
204+
} catch {
205+
Write-Host "::warning::Store metadata update failed: $($_.Exception.Message)"
206+
}
207+
208+
if (-not $metadataUpdated) {
209+
Write-Host "::warning::Restaging package without metadata update to recover from a rejected draft."
210+
Remove-PendingStoreSubmission
211+
Stage-StorePackage
212+
}
213+
214+
try {
215+
Publish-StoreSubmission
216+
} catch {
217+
Write-Host "::warning::Submission publish failed: $($_.Exception.Message)"
218+
Remove-PendingStoreSubmission
219+
Stage-StorePackage
220+
Publish-StoreSubmission
221+
}

0 commit comments

Comments
 (0)