Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ jobs:
shell: pwsh
run: ./scripts/Test-InstallerScriptCompiles.ps1 -RequireCompiler

- name: Validate Store submission and source authorization
shell: pwsh
run: |
./scripts/test-microsoft-store-submission.ps1
./scripts/Test-StoreSubmissionSource.Tests.ps1

- name: Validate Store MSIX bundle construction
shell: pwsh
run: ./scripts/test-msix-bundle.ps1
Expand Down Expand Up @@ -2012,3 +2018,33 @@ jobs:
. ./scripts/AlphaPromotion.ps1
Publish-AlphaPromotion -Record ($env:PROMOTION_RECORD | ConvertFrom-Json) `
-Directory promotion-assets -RunId $env:GITHUB_RUN_ID -PreparedAttempt $env:PREPARED_ATTEMPT

store-release-source:
name: Record authorized Store release source
needs: [metadata, release, publish-promotion, build-msix-bundle]
if: >-
${{ !cancelled() && needs.metadata.outputs.isPrerelease == 'false' &&
needs.release.result == 'success' && needs.build-msix-bundle.result == 'success' &&
(needs.publish-promotion.result == 'success' || needs.publish-promotion.result == 'skipped') }}
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: openclaw-msix-store-unsigned-bundle
path: store
- name: Bind Store bundle to published release
shell: pwsh
run: |
[ordered]@{
runId = $env:GITHUB_RUN_ID
runAttempt = $env:GITHUB_RUN_ATTEMPT
releaseTag = $env:OPENCLAW_RELEASE_TAG
sourceSha = $env:OPENCLAW_RELEASE_SOURCE_SHA
bundleSha256 = (Get-FileHash ./store/OpenClaw.msixbundle -Algorithm SHA256).Hash.ToLowerInvariant()
} | ConvertTo-Json | Set-Content store-source.json -Encoding utf8
- uses: actions/upload-artifact@v7
with:
name: store-release-source-${{ github.run_attempt }}
path: store-source.json
if-no-files-found: error
retention-days: 90
188 changes: 188 additions & 0 deletions .github/workflows/submit-microsoft-store.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Submit Microsoft Store

on:
workflow_dispatch:
inputs:
run_id:
description: Successful release workflow run containing the authorized Store bundle
type: string
required: true
workflow_run:
workflows: [Build and Test, Promote alpha to stable]
types: [completed]

permissions:
contents: read
actions: read

concurrency:
group: microsoft-store-submission
cancel-in-progress: false

jobs:
source:
if: >-
github.repository == 'openclaw/openclaw-windows-node' &&
github.ref == 'refs/heads/main' &&
(github.event_name == 'workflow_dispatch' ||
(vars.MSSTORE_ENABLED == 'true' && github.event.workflow_run.conclusion == 'success'))
runs-on: ubuntu-latest
outputs:
eligible: ${{ steps.source.outputs.eligible }}
artifact_id: ${{ steps.source.outputs.artifact_id }}
bundle_name: ${{ steps.source.outputs.bundle_name }}
bundle_sha256: ${{ steps.source.outputs.bundle_sha256 }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Resolve authorized release artifact
id: source
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
SOURCE_RUN_ID: ${{ inputs.run_id || github.event.workflow_run.id }}
run: |
$source = ./scripts/Get-StoreSubmissionSource.ps1 -RunId $env:SOURCE_RUN_ID
if ($null -eq $source) {
if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { throw 'The selected run is not an eligible Store release.' }
'eligible=false' >> $env:GITHUB_OUTPUT
exit 0
}
'eligible=true' >> $env:GITHUB_OUTPUT
"artifact_id=$($source.artifactId)" >> $env:GITHUB_OUTPUT
"bundle_name=$($source.bundleName)" >> $env:GITHUB_OUTPUT
"bundle_sha256=$($source.bundleSha256)" >> $env:GITHUB_OUTPUT
"Store source: run $env:SOURCE_RUN_ID, artifact $($source.artifactId), bundle $($source.bundleName)" >> $env:GITHUB_STEP_SUMMARY

submit-microsoft-store:
needs: source
if: needs.source.outputs.eligible == 'true'
runs-on: ubuntu-latest
environment: microsoft-store
timeout-minutes: 60
permissions:
contents: read
actions: read
id-token: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- name: Download verified Store bundle
uses: actions/download-artifact@v8
with:
artifact-ids: ${{ needs.source.outputs.artifact_id }}
run-id: ${{ inputs.run_id || github.event.workflow_run.id }}
github-token: ${{ github.token }}
merge-multiple: true
path: store

- name: Validate Microsoft Store environment
shell: pwsh
env:
MSSTORE_TENANT_ID: ${{ vars.MSSTORE_TENANT_ID }}
MSSTORE_CLIENT_ID: ${{ vars.MSSTORE_CLIENT_ID }}
MSSTORE_APPLICATION_ID: ${{ vars.MSSTORE_APPLICATION_ID }}
run: |
foreach ($name in @(
'MSSTORE_TENANT_ID'
'MSSTORE_CLIENT_ID'
'MSSTORE_APPLICATION_ID'
)) {
if ([string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($name))) {
throw "The microsoft-store environment is missing $name."
}
}
foreach ($name in @('MSSTORE_TENANT_ID', 'MSSTORE_CLIENT_ID')) {
$value = [Environment]::GetEnvironmentVariable($name)
$parsed = [guid]::Empty
if (-not [guid]::TryParse($value, [ref]$parsed)) {
throw "$name must be a GUID."
}
}
- name: Request short-lived Microsoft Store assertion
id: oidc
shell: pwsh
run: |
$policy = Get-Content -LiteralPath .\store-submission.json -Raw |
ConvertFrom-Json
$requestUrl = $env:ACTIONS_ID_TOKEN_REQUEST_URL
$requestToken = $env:ACTIONS_ID_TOKEN_REQUEST_TOKEN
if ([string]::IsNullOrWhiteSpace($requestUrl) -or
[string]::IsNullOrWhiteSpace($requestToken)) {
throw 'GitHub did not provide an OIDC token endpoint.'
}
$oidcRequestUri = .\scripts\New-GitHubOidcRequestUri.ps1 `
-RequestUri $requestUrl `
-Audience ([string]$policy.oidcAudience)
$response = Invoke-RestMethod `
-Headers @{ Authorization = "Bearer $requestToken" } `
-Uri $oidcRequestUri
$assertion = [string]$response.value
if ([string]::IsNullOrWhiteSpace($assertion)) {
throw 'GitHub returned an empty OIDC assertion.'
}
Write-Output "::add-mask::$assertion"
$assertionPath = Join-Path $env:RUNNER_TEMP 'msstore-assertion.jwt'
[IO.File]::WriteAllText($assertionPath, $assertion)
& chmod 600 $assertionPath
if ($LASTEXITCODE -ne 0) {
throw 'Unable to restrict the OIDC assertion file permissions.'
}
"assertion_path=$assertionPath" >> $env:GITHUB_OUTPUT

- name: Submit bundle and commit Store update
shell: pwsh
env:
MSSTORE_TENANT_ID: ${{ vars.MSSTORE_TENANT_ID }}
MSSTORE_CLIENT_ID: ${{ vars.MSSTORE_CLIENT_ID }}
MSSTORE_APPLICATION_ID: ${{ vars.MSSTORE_APPLICATION_ID }}
MSSTORE_ASSERTION_FILE: ${{ steps.oidc.outputs.assertion_path }}
EXPECTED_BUNDLE_NAME: ${{ needs.source.outputs.bundle_name }}
EXPECTED_BUNDLE_SHA256: ${{ needs.source.outputs.bundle_sha256 }}
run: |
[xml]$identityDocument = Get-Content ./src/OpenClaw.Tray.WinUI/Package.appxmanifest -Raw
$identityName = [string]$identityDocument.Package.Identity.Name
$publisher = [string]$identityDocument.Package.Identity.Publisher
$bundle = @(Get-ChildItem ./store -File -Filter *.msixbundle)
if ($bundle.Count -ne 1 -or $bundle[0].Name -cne $env:EXPECTED_BUNDLE_NAME) {
throw 'Downloaded Store artifact must contain the selected bundle.'
}
if ($env:EXPECTED_BUNDLE_SHA256 -and
(Get-FileHash $bundle[0].FullName -Algorithm SHA256).Hash.ToLowerInvariant() -cne $env:EXPECTED_BUNDLE_SHA256) {
throw 'Store bundle does not match the release witness.'
}
.\scripts\Submit-MicrosoftStore.ps1 `
-BundlePath $bundle[0].FullName `
-ApplicationId $env:MSSTORE_APPLICATION_ID `
-PackageIdentityName $identityName -Publisher $publisher `
-TenantId $env:MSSTORE_TENANT_ID `
-ClientId $env:MSSTORE_CLIENT_ID `
-ClientAssertionFile $env:MSSTORE_ASSERTION_FILE `
-PolicyPath .\store-submission.json `
-EvidencePath .\evidence\store-submission.json

- name: Upload Store submission evidence
if: ${{ always() }}
continue-on-error: true
uses: actions/upload-artifact@v7
with:
name: openclaw-node-store-submission-evidence
path: evidence/store-submission.json
if-no-files-found: error
retention-days: 90

- name: Remove short-lived assertion
if: ${{ always() }}
shell: pwsh
env:
MSSTORE_ASSERTION_FILE: ${{ steps.oidc.outputs.assertion_path }}
run: |
if (-not [string]::IsNullOrWhiteSpace($env:MSSTORE_ASSERTION_FILE)) {
Remove-Item `
-LiteralPath $env:MSSTORE_ASSERTION_FILE `
-Force `
-ErrorAction SilentlyContinue
}
99 changes: 99 additions & 0 deletions docs/MICROSOFT_STORE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Submit a release to Microsoft Store

Use this guide to submit an already validated release bundle, retry a failed
Store delivery without rebuilding, or enable automatic Store delivery. This
workflow does not change signing, release approval, or install/upgrade acceptance.
GitHub releases and the existing unsigned Store artifact handoff remain available.

## Configure the publishing identity

1. Link the correct Microsoft Entra tenant to Partner Center. Add a dedicated
Entra application to Partner Center with the **Manager (Windows)** role.
The Store product must already have a completed, published first submission
and completed age ratings. Do not reuse the Azure code-signing identity.
2. Add one federated credential to that Entra application:
- Issuer: `https://token.actions.githubusercontent.com`
- Audience: `api://AzureADTokenExchange`
- Subject: `repo:openclaw/openclaw-windows-node:environment:microsoft-store`
Verify the repository's current OIDC subject configuration before provisioning;
immutable-subject settings affect the exact subject. The dedicated application
must have no client secrets, certificates, or other federated writers.
3. In the repository's **Settings > Environments > microsoft-store**, set these
environment variables (not secrets):
- `MSSTORE_TENANT_ID`: Entra tenant GUID.
- `MSSTORE_CLIENT_ID`: dedicated application's client GUID.
- `MSSTORE_APPLICATION_ID`: Partner Center Store product ID, not the client GUID.
4. Allow deployments from the `main` **branch** in this environment. The submission
workflow always runs trusted main code, including when it consumes a tag's
bundle. Restrict the environment to main, and configure release-owner approval
if required by your publishing policy. Tag-only protection will block this job.
5. Leave the repository variable `MSSTORE_ENABLED` unset until the first manual
submission and the required Store install/upgrade acceptance have succeeded.
No persistent Store password is required.

## Submit manually

Merge the workflow onto main first. Select the successful run ID for a stable/correction tag **Build and Test** run or an approved **Promote alpha to stable** run.
The ID is the number after `/actions/runs/` in its URL. The source run must have
completed successfully and retained its unexpired authorized Store bundle.

From **Actions > Submit Microsoft Store > Run workflow**, select `main` and
enter `run_id`. The equivalent CLI command is:

```powershell
gh workflow run submit-microsoft-store.yml `
--repo openclaw/openclaw-windows-node --ref main `
-f run_id=RELEASE_RUN_ID
```

Manual dispatch deliberately does not require `MSSTORE_ENABLED`. It uses the same
source checks, protected environment, OIDC identity, and publisher as automatic
submission. It never rebuilds a package or republishes the GitHub release.

Only the multi-architecture Store `.msixbundle` is uploaded. The publisher checks
its identity against repository policy and the actual Partner Center product
before creating a draft. It preserves listing, pricing, availability, and other
product metadata while replacing the packages with the selected bundle.

## Enable automatic delivery

After the manual rehearsal and release acceptance, set the **repository** Actions
variable `MSSTORE_ENABLED` to `true`. It must be repository-scoped because the
source job checks it before entering the protected environment. Successful eligible
release runs then trigger **Submit Microsoft Store** through `workflow_run`.
PRs, ordinary main builds, failed runs, and prereleases do not submit. Ineligible
automatic runs are skipped; selecting an ineligible run manually fails explicitly.

Remove the variable or set it to `false` to pause future automatic submissions.
This does not cancel a submission already in progress. Both manual and automatic
runs share one concurrency group and never cancel an active Store upload.

## Verify and recover

A successful submission means Partner Center returned `CommitStarted`, not that
certification or public rollout has finished. Retain the Store workflow URL and
its 90-day submission evidence artifact. The evidence includes the bundle SHA-256,
product ID, exact submission ID, metadata hashes, and commit acceptance. Follow
certification and publication in Partner Center before declaring Store delivery
complete. The current policy requests a 100% package rollout and preserves the
product's publication mode, including manual or scheduled publication.

Before dispatch, finish or explicitly discard any existing draft in Partner
Center. Automation rejects existing drafts and never replaces them. Do not edit
an API-created draft in the portal while automation runs. A failure before commit
can remove only the draft created by this run. A lost or ambiguous commit response
preserves that draft: inspect Partner Center before retrying. Never blindly rerun
a submission that may already have been accepted. If the release artifact expired,
use the release process to produce and validate a new candidate.

See Microsoft's [submission API](https://learn.microsoft.com/en-us/windows/uwp/monetize/manage-app-submissions)
and [application identity resource](https://learn.microsoft.com/en-us/windows/uwp/monetize/get-app-data).

## Source binding

The release pipeline records `store-release-source-<attempt>` only after stable
GitHub publication and bundle validation succeed. The submission workflow checks
that witness against the source run, its current attempt, the published stable
tag and commit, and the downloaded bundle hash. Promotion keeps its product source
separate from the main pipeline commit. Releases created before this witness was
introduced cannot be used as manual submission sources.
16 changes: 10 additions & 6 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -730,14 +730,14 @@ extract the matching architecture archive and follow its instructions,
including explicitly trusting the included public certificate.

Unsigned Store submission packages, the multi-architecture bundle, and their
metadata remain Actions artifacts only. Upload the unsigned bundle to Partner
Center for one architecture-selecting submission. No production signing step
is applied to those Store inputs.
metadata remain Actions artifacts only. Use [Store submission](MICROSOFT_STORE.md)
to upload the validated unsigned bundle from a successful release run. No
production signing step is applied to those Store inputs.

Store distribution remains paused: automatic Partner Center submission,
Automatic Partner Center submission is staged behind `MSSTORE_ENABLED`.
Store-signed retrieval and publication, and official lifecycle acceptance
remain follow-up work in #1375. Release submission artifacts do not clear those
rollout gates.
remain release gates tracked in #1375. Submission automation and release
artifacts do not clear those rollout gates.

Store versions still end in `.0`. Official tagged builds now reserve distinct
package versions as described below; reruns reuse the same reservation.
Expand Down Expand Up @@ -1061,3 +1061,7 @@ only receive a stable tag through the controlled promotion workflow above.
`X.Y.Z-alpha.(N+1)`.
- CI computes GitVersion outputs for artifact naming, while product builds use
GitVersion-backed assembly metadata.

## Automated Store submission

See [Microsoft Store submission](MICROSOFT_STORE.md) for protected OIDC setup, manual submission from an existing release run, automatic activation, and recovery. Automatic submission remains off until the repository variable `MSSTORE_ENABLED` is `true`; the existing release and artifact handoffs are unchanged.
Loading
Loading