505 lines
26 KiB
YAML
505 lines
26 KiB
YAML
# Upload APPX to Microsoft Store after a new release is published.
|
|
# Downloads the .appx artifact from the GitHub release and submits
|
|
# a new Store submission via the Partner Center Ingestion API.
|
|
|
|
name: ms-store-upload
|
|
|
|
on:
|
|
release:
|
|
types: [released]
|
|
push:
|
|
branches: [ ms ]
|
|
|
|
jobs:
|
|
upload-to-store:
|
|
runs-on: windows-2022
|
|
environment: build
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Use Node.js 24.x
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 24.x
|
|
|
|
- name: Install R2 dependencies if store upload may run
|
|
if: ${{ contains(github.event.head_commit.message, '[r2]') }}
|
|
run: npm i -D @aws-sdk/client-s3
|
|
|
|
- name: Determine release tag
|
|
id: tag
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: pwsh
|
|
run: |
|
|
$tag = "${{ github.event.release.tag_name }}"
|
|
if (-not $tag) {
|
|
Write-Host "No release tag from event, fetching latest release..."
|
|
$tag = (gh release view --repo "${{ github.repository }}" --json tagName -q '.tagName')
|
|
if (-not $tag) { throw 'Could not determine release tag.' }
|
|
}
|
|
Write-Host "Release tag: $tag"
|
|
"tag_name=$tag" >> $env:GITHUB_OUTPUT
|
|
|
|
- name: Download APPX from release
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
shell: pwsh
|
|
run: |
|
|
$tag = "${{ steps.tag.outputs.tag_name }}"
|
|
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
|
gh release download "$tag" --repo "${{ github.repository }}" --pattern '*.appx' -D dist
|
|
Write-Host 'Downloaded APPX files:'
|
|
Get-ChildItem -Path dist -Filter '*.appx' -Recurse | Format-Table FullName, Length
|
|
|
|
- name: Setup Microsoft Store Developer CLI
|
|
uses: microsoft/microsoft-store-apppublisher@v1.1
|
|
|
|
- name: Configure Microsoft Store Developer CLI
|
|
env:
|
|
MS_TENANT_ID: ${{ secrets.MS_TENANT_ID }}
|
|
MS_SELLER_ID: ${{ secrets.MS_SELLER_ID }}
|
|
MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }}
|
|
MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }}
|
|
run: msstore reconfigure --tenantId "$env:MS_TENANT_ID" --sellerId "$env:MS_SELLER_ID" --clientId "$env:MS_CLIENT_ID" --clientSecret "$env:MS_CLIENT_SECRET"
|
|
shell: pwsh
|
|
|
|
- name: Debug Microsoft Store app access
|
|
continue-on-error: true
|
|
env:
|
|
MS_PRODUCT_ID: ${{ secrets.MS_PRODUCT_ID }}
|
|
MS_APP_ID: ${{ secrets.MS_APP_ID }}
|
|
MSSTORE_LOG_FILE: dist/msstore-logs/msstore-${{ github.run_id }}-${{ github.run_attempt }}.log
|
|
run: |
|
|
function Get-VisibleProductIds([string] $appsListOutput) {
|
|
$cleanOutput = [regex]::Replace($appsListOutput, '\x1B\[[0-9;?]*[ -/]*[@-~]', '')
|
|
return @(
|
|
$cleanOutput -split "`r?`n" |
|
|
Where-Object { $_.Trim().StartsWith('│') } |
|
|
ForEach-Object {
|
|
$columns = $_.Trim().Split('│') | ForEach-Object { $_.Trim() }
|
|
if ($columns.Count -ge 5) {
|
|
$productId = $columns[2]
|
|
$packageId = $columns[4]
|
|
if ($productId -and $productId -ne 'ProductId' -and $packageId -and $packageId -ne 'PackageId') {
|
|
$productId
|
|
}
|
|
}
|
|
} |
|
|
Select-Object -Unique
|
|
)
|
|
}
|
|
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $env:MSSTORE_LOG_FILE) | Out-Null
|
|
Set-Content -Path $env:MSSTORE_LOG_FILE -Value "=== Microsoft Store debug log $(Get-Date -Format o) ==="
|
|
|
|
function Write-StoreLog([string] $message) {
|
|
Add-Content -Path $env:MSSTORE_LOG_FILE -Value $message
|
|
}
|
|
|
|
$msstoreVersion = & msstore --version 2>&1 | Out-String
|
|
Write-Host "msstore CLI version: $msstoreVersion"
|
|
Write-StoreLog "msstore CLI version: $msstoreVersion"
|
|
|
|
$configuredProductId = "$env:MS_PRODUCT_ID".Trim()
|
|
if (-not $configuredProductId) {
|
|
$configuredProductId = "$env:MS_APP_ID".Trim()
|
|
}
|
|
|
|
if ($configuredProductId) {
|
|
Write-Host "Configured Store Product ID: $configuredProductId"
|
|
} else {
|
|
Write-Host 'Configured Store Product ID: <empty>'
|
|
}
|
|
|
|
Write-Host 'Package identifiers in this repo are not valid Store Product IDs:'
|
|
Write-Host ' Electron appId: org.electerm.electerm'
|
|
Write-Host ' APPX applicationId: electerm'
|
|
Write-Host ' APPX identityName: 64159ZHAOXudong.electerm'
|
|
Write-Host 'Accessible Microsoft Store packaged apps for this account:'
|
|
Write-StoreLog "Configured Store Product ID: $configuredProductId"
|
|
Write-StoreLog 'Package identifiers in this repo are not valid Store Product IDs:'
|
|
Write-StoreLog ' Electron appId: org.electerm.electerm'
|
|
Write-StoreLog ' APPX applicationId: electerm'
|
|
Write-StoreLog ' APPX identityName: 64159ZHAOXudong.electerm'
|
|
Write-StoreLog 'Accessible Microsoft Store packaged apps for this account:'
|
|
|
|
Write-StoreLog ''
|
|
Write-StoreLog "=== msstore apps list $(Get-Date -Format o) ==="
|
|
$appsListOutput = (& msstore apps list 2>&1 | Tee-Object -FilePath $env:MSSTORE_LOG_FILE -Append | Out-String)
|
|
$appsListExitCode = $LASTEXITCODE
|
|
$global:LASTEXITCODE = 0
|
|
Write-Host $appsListOutput
|
|
Write-StoreLog "msstore apps list exit code: $appsListExitCode"
|
|
|
|
$productIds = Get-VisibleProductIds $appsListOutput
|
|
|
|
if ($appsListExitCode -ne 0) {
|
|
Write-Host "msstore apps list exited with code $appsListExitCode. This usually means the configured Entra app can authenticate but does not have Partner Center access to packaged apps for this seller account."
|
|
} elseif ($productIds.Count -eq 0) {
|
|
Write-Host 'msstore apps list succeeded but no packaged Product IDs were detected. This usually means no packaged Store apps are visible to this account.'
|
|
} elseif (-not $configuredProductId) {
|
|
if ($productIds.Count -eq 1) {
|
|
Write-Host "Exactly one packaged Product ID is visible: $($productIds[0])"
|
|
} else {
|
|
Write-Host "Multiple packaged Product IDs are visible: $($productIds -join ', ')"
|
|
}
|
|
} elseif ($configuredProductId -notin $productIds) {
|
|
Write-Host "Configured Store Product ID '$configuredProductId' was not found in the accessible app list."
|
|
Write-Host "Visible Product IDs: $($productIds -join ', ')"
|
|
} else {
|
|
Write-Host "Configured Store Product ID '$configuredProductId' is visible to this account."
|
|
}
|
|
shell: pwsh
|
|
|
|
- name: Upload APPX to Microsoft Store and submit for certification
|
|
env:
|
|
MS_PRODUCT_ID: ${{ secrets.MS_PRODUCT_ID }}
|
|
MS_APP_ID: ${{ secrets.MS_APP_ID }}
|
|
MS_TENANT_ID: ${{ secrets.MS_TENANT_ID }}
|
|
MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }}
|
|
MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }}
|
|
MSSTORE_LOG_FILE: dist/msstore-logs/msstore-${{ github.run_id }}-${{ github.run_attempt }}.log
|
|
run: |
|
|
# ── Initialise log file ─────────────────────────────────────────────────
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $env:MSSTORE_LOG_FILE) | Out-Null
|
|
Set-Content -Path $env:MSSTORE_LOG_FILE -Value "=== ms-store-upload log $(Get-Date -Format o) ==="
|
|
|
|
function Write-StoreLog([string] $message) {
|
|
Add-Content -Path $env:MSSTORE_LOG_FILE -Value $message
|
|
}
|
|
|
|
# Retry wrapper for Invoke-RestMethod with exponential backoff.
|
|
# The Partner Center Ingestion API is behind Azure Front Door and can
|
|
# return transient 504 / 502 / 503 responses under load.
|
|
function Invoke-WithRetry {
|
|
param(
|
|
[Parameter(Mandatory=$true)] [scriptblock] $Action,
|
|
[string] $Description = 'API call',
|
|
[int] $MaxRetries = 5,
|
|
[int] $BaseDelaySec = 10
|
|
)
|
|
for ($i = 0; $i -lt $MaxRetries; $i++) {
|
|
try {
|
|
return & $Action
|
|
} catch {
|
|
$statusCode = $null
|
|
if ($_.Exception.Response) {
|
|
$statusCode = [int]$_.Exception.Response.StatusCode
|
|
}
|
|
$isTransient = $false
|
|
if ($statusCode -in 408, 429, 500, 502, 503, 504) { $isTransient = $true }
|
|
if ($_.Exception.Message -match 'timed?\s*out|gateway|service unavailable|connection was closed|unable to connect') { $isTransient = $true }
|
|
$attempt = $i + 1
|
|
if ($isTransient -and $attempt -lt $MaxRetries) {
|
|
$delay = $BaseDelaySec * [Math]::Pow(2, $i)
|
|
Write-Host " ⚠ ${Description}: transient error (HTTP $statusCode) on attempt $attempt/$MaxRetries — retrying in ${delay}s..."
|
|
Write-StoreLog "${Description}: transient error (HTTP $statusCode) attempt $attempt/$MaxRetries, retry in ${delay}s. $($_.Exception.Message)"
|
|
Start-Sleep -Seconds $delay
|
|
} else {
|
|
Write-StoreLog "${Description}: FAILED on attempt $attempt (HTTP $statusCode). $($_.Exception.Message)"
|
|
throw
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-VisibleProductIds([string] $appsListOutput) {
|
|
$cleanOutput = [regex]::Replace($appsListOutput, '\x1B\[[0-9;?]*[ -/]*[@-~]', '')
|
|
return @(
|
|
$cleanOutput -split "`r?`n" |
|
|
Where-Object { $_.Trim().StartsWith('│') } |
|
|
ForEach-Object {
|
|
$columns = $_.Trim().Split('│') | ForEach-Object { $_.Trim() }
|
|
if ($columns.Count -ge 5) {
|
|
$productId = $columns[2]
|
|
$packageId = $columns[4]
|
|
if ($productId -and $productId -ne 'ProductId' -and $packageId -and $packageId -ne 'PackageId') {
|
|
$productId
|
|
}
|
|
}
|
|
} |
|
|
Select-Object -Unique
|
|
)
|
|
}
|
|
|
|
try {
|
|
|
|
$appx = Get-ChildItem -Path dist -Filter '*.appx' -Recurse | Select-Object -First 1
|
|
if (-not $appx) { throw 'No .appx file found under dist/' }
|
|
|
|
Write-StoreLog ''
|
|
Write-StoreLog "=== publish preflight $(Get-Date -Format o) ==="
|
|
Write-StoreLog "APPX file: $($appx.FullName)"
|
|
|
|
# Soft preflight: enumerate accessible packaged apps.
|
|
# 'msstore apps list' can fail on its own (e.g. a CLI regression pulled in by
|
|
# 'version: latest') even when the Entra app and the configured Product ID are
|
|
# perfectly valid. The actual submission below calls the Dev Center Ingestion API
|
|
# directly and does not depend on this command, so when MS_PRODUCT_ID is explicitly
|
|
# configured we trust it and proceed instead of aborting the whole build.
|
|
$appsListOutput = (& msstore apps list 2>&1 | Tee-Object -FilePath $env:MSSTORE_LOG_FILE -Append | Out-String)
|
|
$appsListExitCode = $LASTEXITCODE
|
|
$global:LASTEXITCODE = 0
|
|
Write-StoreLog "msstore apps list exit code: $appsListExitCode"
|
|
$productIds = Get-VisibleProductIds $appsListOutput
|
|
|
|
$productId = "$env:MS_PRODUCT_ID".Trim()
|
|
if (-not $productId) {
|
|
$productId = "$env:MS_APP_ID".Trim()
|
|
}
|
|
|
|
if (-not $productId) {
|
|
# No explicitly configured id: we must rely on the app list.
|
|
if ($appsListExitCode -eq 0 -and $productIds.Count -eq 1) {
|
|
$productId = $productIds[0]
|
|
Write-Host "Resolved MS Product ID from account app list: $productId"
|
|
} else {
|
|
$visibleProductIds = if ($productIds.Count) { $productIds -join ', ' } else { 'none detected' }
|
|
throw "Missing MS_PRODUCT_ID or MS_APP_ID secret and could not resolve a single Product ID from 'msstore apps list' (exit $appsListExitCode). Visible Product IDs for this account: $visibleProductIds"
|
|
}
|
|
} elseif ($appsListExitCode -ne 0 -or $productIds.Count -eq 0) {
|
|
Write-Host "WARNING: 'msstore apps list' failed (exit $appsListExitCode) but MS_PRODUCT_ID is explicitly configured; proceeding with the configured Product ID."
|
|
Write-StoreLog "WARNING: 'msstore apps list' failed (exit $appsListExitCode); proceeding with configured Product ID: $productId"
|
|
} elseif ($productId -notin $productIds) {
|
|
Write-Host "WARNING: Configured Store Product ID '$productId' was not found in the accessible app list; proceeding with the configured value anyway."
|
|
Write-StoreLog "WARNING: Configured Store Product ID '$productId' not in accessible app list; proceeding."
|
|
} else {
|
|
Write-Host "Configured Store Product ID '$productId' is visible to this account."
|
|
}
|
|
|
|
$knownWrongAppIds = @(
|
|
'org.electerm.electerm',
|
|
'electerm',
|
|
'64159ZHAOXudong.electerm'
|
|
)
|
|
if ($knownWrongAppIds -contains $productId) {
|
|
throw "Configured store identifier '$productId' is a package identifier. Use the Microsoft Store Partner Center product id instead."
|
|
}
|
|
|
|
Write-Host "Resolved publish Product ID: $productId"
|
|
Write-StoreLog "Resolved publish Product ID: $productId"
|
|
|
|
# ── Direct Ingestion API publish ───────────────────────────────────────
|
|
# msstore publish copies listing features verbatim from the live submission.
|
|
# If any locale has >20 features the Ingestion API rejects the update.
|
|
# We bypass msstore publish and call the API directly so we can cap features.
|
|
|
|
Write-StoreLog ''
|
|
Write-StoreLog "=== direct-api publish $(Get-Date -Format o) ==="
|
|
|
|
# 1. Get OAuth token for the Partner Center Ingestion API
|
|
$tokenParts = @(
|
|
"grant_type=client_credentials",
|
|
"client_id=$([uri]::EscapeDataString($env:MS_CLIENT_ID))",
|
|
"client_secret=$([uri]::EscapeDataString($env:MS_CLIENT_SECRET))",
|
|
"scope=$([uri]::EscapeDataString('https://manage.devcenter.microsoft.com/.default'))"
|
|
)
|
|
$tokenResp = Invoke-WithRetry -Description 'Get OAuth token' {
|
|
Invoke-RestMethod `
|
|
-Uri "https://login.microsoftonline.com/$env:MS_TENANT_ID/oauth2/v2.0/token" `
|
|
-Method Post -ContentType 'application/x-www-form-urlencoded' `
|
|
-Body ($tokenParts -join '&')
|
|
}
|
|
$token = $tokenResp.access_token
|
|
Write-StoreLog "OAuth token acquired."
|
|
$h = @{ Authorization = "Bearer $token" }
|
|
$apiBase = "https://manage.devcenter.microsoft.com/v1.0/my/applications/$productId"
|
|
|
|
# 2. Get app info; delete any pending submission
|
|
Write-Host "Getting app info..."
|
|
$appInfo = Invoke-WithRetry -Description 'Get app info' { Invoke-RestMethod -Uri $apiBase -Headers $h }
|
|
Write-StoreLog "App info fetched. lastPublishedSubmission: $($appInfo.lastPublishedApplicationSubmission.id)"
|
|
if ($appInfo.pendingApplicationSubmission.id) {
|
|
$pendingId = $appInfo.pendingApplicationSubmission.id
|
|
Write-Host "Deleting pending submission $pendingId ..."
|
|
Write-StoreLog "Deleting pending submission: $pendingId"
|
|
Invoke-WithRetry -Description 'Delete pending submission' { Invoke-RestMethod -Uri "$apiBase/submissions/$pendingId" -Method Delete -Headers $h } | Out-Null
|
|
Write-Host "Deleted pending submission."
|
|
Write-StoreLog "Deleted pending submission."
|
|
}
|
|
|
|
# 3. Fetch last published submission to use as the body for POST /submissions.
|
|
# The API does NOT auto-clone — it requires the full submission object.
|
|
$lastPubSubId = $appInfo.lastPublishedApplicationSubmission.id
|
|
if (-not $lastPubSubId) { throw 'No published submission found to clone from.' }
|
|
Write-Host "Fetching published submission $lastPubSubId as template..."
|
|
Write-StoreLog "Fetching published submission: $lastPubSubId"
|
|
$pubSub = Invoke-WithRetry -Description 'Fetch published submission template' { Invoke-RestMethod -Uri "$apiBase/submissions/$lastPubSubId" -Headers $h }
|
|
|
|
# 4. Create new submission using the published submission data as the request body.
|
|
# A 504/502 gateway timeout can still result in the submission being created
|
|
# server-side. A naive retry would then fail with HTTP 409 Conflict
|
|
# (InvalidState) because the Ingestion API only allows one pending
|
|
# submission at a time. After any failure we re-fetch app info and reuse
|
|
# the pending submission if one now exists instead of blindly retrying.
|
|
Write-Host "Creating new submission..."
|
|
Write-StoreLog "Creating new submission (POST with published data as body)..."
|
|
$pubSubBytes = [System.Text.Encoding]::UTF8.GetBytes(($pubSub | ConvertTo-Json -Depth 30 -Compress))
|
|
$newSub = $null
|
|
$maxCreateRetries = 5
|
|
$submissionReady = $false
|
|
for ($i = 0; $i -lt $maxCreateRetries -and -not $submissionReady; $i++) {
|
|
try {
|
|
$newSub = Invoke-RestMethod -Uri "$apiBase/submissions" -Method Post -Headers $h -ContentType 'application/json' -Body $pubSubBytes
|
|
$submissionReady = $true
|
|
} catch {
|
|
$statusCode = $null
|
|
if ($_.Exception.Response) {
|
|
$statusCode = [int]$_.Exception.Response.StatusCode
|
|
}
|
|
$errBody = ''
|
|
if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
|
|
$errBody = $_.ErrorDetails.Message
|
|
}
|
|
$attempt = $i + 1
|
|
$isRecoverable = ($statusCode -in 409, 502, 503, 504) -or
|
|
($errBody -match 'Conflict|InvalidState') -or
|
|
($_.Exception.Message -match 'timed?\s*out|gateway|service unavailable|connection was closed')
|
|
if ($isRecoverable -and $attempt -lt $maxCreateRetries) {
|
|
$delay = 10 * [Math]::Pow(2, $i)
|
|
Write-Host " ⚠ Create new submission: HTTP $statusCode on attempt $attempt/$maxCreateRetries — checking for pending submission in ${delay}s..."
|
|
Write-StoreLog "Create new submission: HTTP $statusCode attempt $attempt/$maxCreateRetries, retry in ${delay}s. $errBody"
|
|
Start-Sleep -Seconds $delay
|
|
# Re-fetch app info — the failed POST may have created a pending submission.
|
|
# Use Invoke-WithRetry because the API may be experiencing widespread 504s.
|
|
try {
|
|
$appInfoRetry = Invoke-WithRetry -Description 'Re-fetch app info for pending submission' { Invoke-RestMethod -Uri $apiBase -Headers $h }
|
|
if ($appInfoRetry.pendingApplicationSubmission.id) {
|
|
$pendingSubId = $appInfoRetry.pendingApplicationSubmission.id
|
|
Write-Host " Found pending submission $pendingSubId — reusing it."
|
|
Write-StoreLog "Reusing existing pending submission: $pendingSubId"
|
|
$pendingSub = Invoke-WithRetry -Description 'Fetch pending submission details' { Invoke-RestMethod -Uri "$apiBase/submissions/$pendingSubId" -Headers $h }
|
|
$newSub = [PSCustomObject]@{
|
|
id = $pendingSubId
|
|
fileUploadUrl = $pendingSub.fileUploadUrl
|
|
}
|
|
$submissionReady = $true
|
|
}
|
|
} catch {
|
|
Write-Host " Could not re-fetch app info or submission: $($_.Exception.Message)"
|
|
Write-StoreLog "Re-fetch failed: $($_.Exception.Message)"
|
|
}
|
|
} else {
|
|
Write-StoreLog "Create new submission: FAILED on attempt $attempt (HTTP $statusCode). $errBody"
|
|
throw
|
|
}
|
|
}
|
|
}
|
|
if (-not $newSub) { throw 'Failed to create new submission after retries.' }
|
|
$subId = $newSub.id
|
|
$fileUploadUrl = $newSub.fileUploadUrl
|
|
Write-Host "Submission ID: $subId"
|
|
Write-StoreLog "Submission ID: $subId"
|
|
|
|
# 5. Get full data for the new submission
|
|
$subData = Invoke-WithRetry -Description 'Get new submission data' { Invoke-RestMethod -Uri "$apiBase/submissions/$subId" -Headers $h }
|
|
|
|
# 6. Replace packages: mark existing for deletion, add new one as PendingUpload
|
|
$newPackages = @()
|
|
foreach ($pkg in @($subData.applicationPackages)) {
|
|
$pkg.fileStatus = 'PendingDelete'
|
|
$newPackages += $pkg
|
|
}
|
|
$newPackages += [PSCustomObject]@{ fileName = $appx.Name; fileStatus = 'PendingUpload' }
|
|
$subData.applicationPackages = $newPackages
|
|
Write-StoreLog "applicationPackages: $($newPackages.Count) entries (last = new PendingUpload)."
|
|
|
|
# Auto-publish after certification passes.
|
|
$subData.targetPublishMode = 'Immediate'
|
|
|
|
# 7. Push updated submission metadata
|
|
Write-Host "Updating submission metadata..."
|
|
Write-StoreLog "PUT submission metadata..."
|
|
$updateBodyBytes = [System.Text.Encoding]::UTF8.GetBytes(($subData | ConvertTo-Json -Depth 30 -Compress))
|
|
Invoke-WithRetry -Description 'PUT submission metadata' {
|
|
Invoke-RestMethod -Uri "$apiBase/submissions/$subId" -Method Put -Headers $h `
|
|
-ContentType 'application/json' -Body $updateBodyBytes
|
|
} | Out-Null
|
|
Write-Host "Submission metadata updated."
|
|
Write-StoreLog "Submission metadata updated."
|
|
|
|
# 8. Create ZIP and upload to Azure Blob SAS URL
|
|
# (SAS URL carries its own auth — do not include the Bearer token here)
|
|
$zipPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'electerm-appx-upload.zip')
|
|
if (Test-Path $zipPath) { Remove-Item $zipPath }
|
|
Compress-Archive -Path $appx.FullName -DestinationPath $zipPath
|
|
$zipSizeMB = [Math]::Round((Get-Item $zipPath).Length / 1MB, 1)
|
|
Write-Host "Uploading package ZIP ($zipSizeMB MB)..."
|
|
Write-StoreLog "Uploading package ZIP ($zipSizeMB MB) to Azure Blob SAS URL..."
|
|
$blobHeaders = @{ 'x-ms-blob-type' = 'BlockBlob'; 'Content-Type' = 'application/octet-stream' }
|
|
Invoke-WithRetry -Description 'Upload package ZIP to Azure Blob' { Invoke-RestMethod -Uri $fileUploadUrl -Method Put -Headers $blobHeaders -InFile $zipPath } | Out-Null
|
|
Write-Host "Package uploaded."
|
|
Write-StoreLog "Package uploaded."
|
|
|
|
# 9. Commit submission to trigger certification.
|
|
# targetPublishMode = 'Immediate' means it will go live automatically after passing certification.
|
|
Write-Host "Committing submission to trigger certification..."
|
|
Write-StoreLog "Committing submission (Immediate publish mode)..."
|
|
$commitResp = Invoke-WithRetry -Description 'Commit submission' { Invoke-RestMethod -Uri "$apiBase/submissions/$subId/commit" -Method Post -Headers $h -ContentType 'application/json' }
|
|
Write-Host "Submission committed. Certification will start shortly."
|
|
Write-StoreLog "Commit response status: $($commitResp.status)"
|
|
if ($commitResp.statusDetails) {
|
|
$errors = @($commitResp.statusDetails.errors)
|
|
$warnings = @($commitResp.statusDetails.warnings)
|
|
if ($errors.Count -gt 0) {
|
|
foreach ($e in $errors) {
|
|
Write-Host " ERROR: $($e.code) - $($e.details)"
|
|
Write-StoreLog " Commit error: $($e.code) - $($e.details)"
|
|
}
|
|
throw "Submission commit returned $($errors.Count) error(s)."
|
|
}
|
|
if ($warnings.Count -gt 0) {
|
|
foreach ($w in $warnings) {
|
|
Write-Host " WARNING: $($w.code) - $($w.details)"
|
|
Write-StoreLog " Commit warning: $($w.code) - $($w.details)"
|
|
}
|
|
}
|
|
}
|
|
Write-StoreLog "Done (committed, Immediate mode). SubmissionId: $subId"
|
|
|
|
# 10. Verify submission status after commit
|
|
Write-Host "Verifying submission status..."
|
|
Start-Sleep -Seconds 10
|
|
$subStatus = Invoke-WithRetry -Description 'Check submission status' { Invoke-RestMethod -Uri "$apiBase/submissions/$subId" -Headers $h }
|
|
Write-Host "Submission status: $($subStatus.status)"
|
|
Write-StoreLog "Post-commit submission status: $($subStatus.status)"
|
|
if ($subStatus.status -eq 'CommitFailed') {
|
|
$errDetails = if ($subStatus.statusDetails.errors) { $subStatus.statusDetails.errors | ForEach-Object { "$($_.code): $($_.details)" } | Out-String } else { 'no details' }
|
|
throw "Submission commit failed. Status: $($subStatus.status). Details: $errDetails"
|
|
}
|
|
|
|
} catch {
|
|
$errMsg = $_.ToString()
|
|
$errDetail = $_.Exception.Message
|
|
$errStack = $_.ScriptStackTrace
|
|
Write-Host "ERROR: $errMsg"
|
|
Write-StoreLog ''
|
|
Write-StoreLog "=== FATAL ERROR $(Get-Date -Format o) ==="
|
|
Write-StoreLog "Message : $errMsg"
|
|
Write-StoreLog "Detail : $errDetail"
|
|
Write-StoreLog "Stack :"
|
|
Write-StoreLog "$errStack"
|
|
throw $_
|
|
}
|
|
shell: pwsh
|
|
|
|
- name: Upload Microsoft Store raw log to R2
|
|
if: ${{ contains(github.event.head_commit.message, '[r2]') }}
|
|
env:
|
|
MSSTORE_LOG_FILE: dist/msstore-logs/msstore-${{ github.run_id }}-${{ github.run_attempt }}.log
|
|
CF_R2_ACCOUNT_ID: ${{ secrets.CF_R2_ACCOUNT_ID }}
|
|
CF_R2_BUCKET: ${{ secrets.CF_R2_BUCKET }}
|
|
CF_R2_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
|
CF_R2_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }}
|
|
run: |
|
|
if (-not (Test-Path $env:MSSTORE_LOG_FILE)) {
|
|
Write-Host "Microsoft Store raw log file was not created: $env:MSSTORE_LOG_FILE"
|
|
exit 0
|
|
}
|
|
|
|
$key = "store-logs/ms-store-upload/${{ github.run_id }}-${{ github.run_attempt }}.log"
|
|
node build/bin/upload-file-to-r2.js "$env:MSSTORE_LOG_FILE" "$key"
|
|
shell: pwsh
|