Skip to content
Open
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
121 changes: 121 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -766,3 +766,124 @@ jobs:
exit 1
;;
esac

# Publishing the Store package is the last manual step in the release: the appx
# had to be downloaded from this run's artifacts and uploaded by hand in Partner
# Center. That is also how 1.8.0 ended up with two different packages under one
# version — the artifact was downloaded twice and both copies were uploaded.
#
# This job lives in build.yml rather than beside publish-winget.yml because the
# appx never becomes a release asset: the GitHub release carries only the NSIS
# installer, so `release: published` has nothing to hand a downstream workflow.
# The package exists solely as this run's artifact.
publish-msstore:
name: Publish to Microsoft Store
runs-on: windows-latest
needs:
- build-windows-store
- publish-release
# Stable tags only, and never a fork: an RC reaching the Store would go through
# certification and land on every user's machine as an automatic update.
# `!contains(…, '-')` is what separates v1.9.1 from v1.9.1-rc.2.
if: >-
${{ vars.MSSTORE_PRODUCT_ID != ''
&& ((github.event_name == 'push' && github.ref_type == 'tag' && !contains(github.ref_name, '-'))
|| (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '' && !contains(github.event.inputs.release_tag, '-'))) }}
steps:
# Same all-or-nothing gate as the macOS signing job: a half-configured
# publisher is a misnamed secret, and the quiet failure mode — shipping
# nothing while the release looks complete — is the one worth making loud.
- name: Resolve Store credentials
id: store
shell: bash
env:
AZURE_AD_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
AZURE_AD_APPLICATION_CLIENT_ID: ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }}
AZURE_AD_APPLICATION_SECRET: ${{ secrets.AZURE_AD_APPLICATION_SECRET }}
SELLER_ID: ${{ secrets.SELLER_ID }}
run: |
required=(AZURE_AD_TENANT_ID AZURE_AD_APPLICATION_CLIENT_ID
AZURE_AD_APPLICATION_SECRET SELLER_ID)
missing=()
for name in "${required[@]}"; do
[[ -n "${!name}" ]] || missing+=("$name")
done

if [[ ${#missing[@]} -eq 0 ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "Store credentials present — the package will be submitted."
elif [[ ${#missing[@]} -eq ${#required[@]} ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::No Store credentials configured; upload the appx by hand in Partner Center."
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::error::Store publishing is partially configured; missing: ${missing[*]}"
exit 1
fi

- name: Download Store package
if: steps.store.outputs.enabled == 'true'
uses: actions/download-artifact@v4
with:
name: openscreen-windows-store
path: artifacts/store

- name: Configure Microsoft Store CLI
if: steps.store.outputs.enabled == 'true'
uses: microsoft/microsoft-store-apppublisher@v1.1

- name: Submit the package to the Store
id: submit
if: steps.store.outputs.enabled == 'true'
shell: pwsh
env:
PRODUCT_ID: ${{ vars.MSSTORE_PRODUCT_ID }}
run: |
msstore reconfigure `
--tenantId ${{ secrets.AZURE_AD_TENANT_ID }} `
--sellerId ${{ secrets.SELLER_ID }} `
--clientId ${{ secrets.AZURE_AD_APPLICATION_CLIENT_ID }} `
--clientSecret ${{ secrets.AZURE_AD_APPLICATION_SECRET }}

# Resolve the appx rather than hardcode its name: artifactName carries the
# version, and download-artifact keeps the release/<version>/ prefix.
$appx = Get-ChildItem artifacts/store -Recurse -Filter '*.appx' | Select-Object -First 1
if (-not $appx) { throw 'no .appx in the downloaded artifact' }
if (@(Get-ChildItem artifacts/store -Recurse -Filter '*.appx').Count -ne 1) {
throw 'more than one .appx in the artifact — refusing to guess which one to submit'
}
Write-Output "Submitting $($appx.Name) to product $env:PRODUCT_ID"
msstore publish $appx.FullName -id $env:PRODUCT_ID

# Report what happened, not what was configured. Keyed off `enabled` alone
# under always(), this claimed "Submitted to the Store" when `msstore
# publish` had failed, and "skipped, no credentials" when the resolver had
# rejected a half-configured one — a release summary asserting success over
# a package that never left the runner, which is the failure mode this whole
# job exists to remove.
#
# Values go through env rather than being interpolated into the script: the
# outcome of a step is not attacker-controlled here, but `${{ }}` expanding
# straight into shell source is the habit worth not having.
- name: Summary
if: always()
shell: bash
env:
RESOLVE: ${{ steps.store.outcome }}
ENABLED: ${{ steps.store.outputs.enabled }}
SUBMIT: ${{ steps.submit.outcome }}
run: |
case "${RESOLVE}:${ENABLED}:${SUBMIT}" in
success:true:success)
echo "Submitted to the Store. Certification still has to pass before it goes live."
;;
success:false:*)
echo "Store publishing skipped: no credentials configured. Upload the appx from this run's artifacts by hand."
;;
failure:*)
echo "Store credentials are half-configured, so nothing was submitted. The job log names the missing secrets."
;;
*)
echo "Store submission did not complete (submit step: ${SUBMIT:-not reached}). The package is still attached to this run as an artifact."
;;
esac >> "$GITHUB_STEP_SUMMARY"
29 changes: 29 additions & 0 deletions technical-documentation/engineering/release-and-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,35 @@ The certificate account needs Developer ID signing capability, and the Apple acc

Rotate the certificate by exporting a replacement P12, base64-encoding it without line-wrap changes, updating the P12/password/name secrets together, testing a stable-format manual build, then revoking the old certificate if required. Rotate the app-specific password in Apple ID settings, replace `APPLE_APP_SPECIFIC_PASSWORD`, verify notarization, and revoke the old password. `APPLE_ID` and `APPLE_TEAM_ID` normally change only when the owning account or team changes.

## Microsoft Store publishing

`build.yml`'s `publish-msstore` job submits the appx to the Store through the [Microsoft Store Developer CLI](https://learn.microsoft.com/en-us/windows/apps/publish/msstore-dev-cli/github-actions). It runs for **stable versions only**, whether that comes from a pushed `vX.Y.Z` tag or from a manual `workflow_dispatch` whose `release_tag` is a stable one. An RC is excluded either way: it would go through certification and land on every user's machine as an automatic update.

| Name | Kind | Purpose |
|---|---|---|
| `MSSTORE_PRODUCT_ID` | Variable | Store product ID (`9MXQ1HQJL5G5`). Gates the whole job, so a fork never publishes to our listing. |
| `AZURE_AD_TENANT_ID` | Secret | Entra tenant associated with the Partner Center account. |
| `AZURE_AD_APPLICATION_CLIENT_ID` | Secret | Application (client) ID of the Entra app registration. |
| `AZURE_AD_APPLICATION_SECRET` | Secret | Client secret of that registration. The only real credential here. |
| `SELLER_ID` | Secret | Publisher/Seller ID from Partner Center account settings. |

With none of them set the job warns and skips, leaving the appx to be uploaded by hand from the run's artifacts; with some but not all it fails, on the same reasoning as the Apple path.

One-time setup, in order — each step depends on the previous one:

1. Associate an Entra tenant with the Partner Center account.
2. Register an application in Entra ID and create a client secret for it.
3. In Partner Center, under **Account settings → User management → Microsoft Entra applications**, add that application and give it the **Manager** role. Tenant and client IDs alone are not enough; the failure is an authorization error at submit time.
4. Set the four secrets and the variable.

Note what this does and does not remove. It removes the manual upload — which is worth having in itself: 1.8.0 ended up with two different packages under one version because the artifact was downloaded twice and both copies uploaded, and Partner Center rejects that outright. It does **not** remove certification: every submission still waits for Microsoft to validate it, and the update only goes live afterwards.

Two constraints from Microsoft's documentation: automated updates through GitHub Actions are supported **for free products only**, and the app must already be published and live in the Store — the API cannot create a listing, only submit to an existing one.

`msstore submission updateMetadata` can also drive the Store listing text from a versioned `metadata.json`, which would replace the CSV export/import round-trip. Not wired up here.

Rotate by issuing a new client secret on the Entra registration, updating `AZURE_AD_APPLICATION_SECRET`, publishing one release to confirm, then deleting the old secret. The tenant, client and seller IDs change only when the registration or account does.

## Discord secrets and variables

| Name | Kind | Used for |
Expand Down
Loading