ci(windows): auto-stage Microsoft Store submission on release tags#3567
Conversation
Wire the Windows Installer workflow to stage a DRAFT Microsoft Store submission on every v* tag push, via the Microsoft Store Developer CLI (msstore). This automates the previously-manual Partner Center upload as part of the weekly (Sunday) release cycle. - Add packaging/windows/publish-store.ps1: finds the .msixupload from create-msix.ps1 and runs `msstore publish <pkg> -id <ProductId> --noCommit`. --noCommit keeps the submission in DRAFT so a maintainer clicks "Submit to Store" — CI never publishes to the live channel on its own. Warns + exits 0 if no .msixupload exists (the MSIX build step is continue-on-error) rather than failing a completed release. - windows-installer.yml: add "Setup Microsoft Store Developer CLI" (microsoft/microsoft-store-apppublisher@v1.1, pinned by SHA) and "Stage Microsoft Store submission (draft)" steps after the release attach. Triple-gated: tag ref AND AETHERSDR_STORE_PRODUCT_ID variable set AND repo secrets available (forks can't read them), so PRs, branch dispatch runs, and forks never touch Partner Center. - docs/WINDOWS-STORE-MSIX.md: flip the section from PLANNED to WIRED and expand the one-time Entra/Partner Center setup into a concrete checklist (app registration as the service account, Manager role, the four secrets + one variable, first-run verification). Dormant until the maintainer completes the Entra/Partner Center setup and sets AETHERSDR_STORE_PRODUCT_ID; until then the workflow behaves exactly as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks @jensenpat — this is a careful, well-documented change. The dormant-by-default design is sound, the layered guard rails (tag-ref + opt-in variable + forks-can't-read-secrets) are correct, and the SHA-pinned action plus --noCommit draft gate are exactly the right defaults for touching a publishing pipeline from CI. Scope is tight (workflow + wrapper + docs), and the publish-store.ps1 error handling is genuinely thoughtful — exit-0 on a missing .msixupload (since the MSIX step is continue-on-error), throw on ambiguous multiple matches, and an explicit $LASTEXITCODE check after & msstore (correct, since $ErrorActionPreference = "Stop" doesn't trap native-exe exit codes).
A couple of things worth verifying before the first live run — both at the msstore CLI boundary, where a wrong flag name fails the very first automated submission:
-
msstore publishargument syntax. The script invokes it positionally with-id:$publishArgs = @("publish", $upload, "-id", $ProductId) $publishArgs += "--noCommit"
The earlier (now-replaced) doc described it as
msstore publish -i <pkg>.msixupload -id <ProductId> --noCommit— i.e. the package behind an-iflag rather than positional. Worth confirming against the actual CLI version the pinnedmicrosoft-store-apppublisher@v1.1action installs: (a) doespublishtake the package path positionally or via-i/--inputDirectory, and (b) are the flags-idand--noCommit(vs--appId/-ncetc.)? If the action is available locally, a quickmsstore publish --helpwould settle both. Not a blocker since the feature is dormant until credentials land, but it's the one path that can't be exercised by CI before a real tag. -
reconfigurefailure mode (minor). IfAETHERSDR_STORE_PRODUCT_IDis set but a secret is later rotated/missing, themsstore reconfigurerun step fails and reddens an otherwise-successful release — slightly at odds with the "never redden a good release" goal thatpublish-store.ps1itself honors. Arguably desirable (you want to notice broken creds), so just flagging it, not asking for a change.
Everything else looks good to me. Nice work keeping this safe-by-construction.
🤖 aethersdr-agent · cost: $3.5370 · model: claude-opus-4-8
There was a problem hiding this comment.
Reviewed the workflow, the publish-store.ps1 wrapper, and the docs. This is careful, well-guarded work — the dormant-by-default design is the right call, and the layered guard rails check out. Thanks @jensenpat. A couple of notes, both minor.
Guard rails — sound
Confirmed against the workflow trigger (on: push: tags: ['v*'] + workflow_dispatch): the whole job only runs on v* tag pushes, so the step's startsWith(github.ref, 'refs/tags/') guard is effectively v*-scoped, and the vars.AETHERSDR_STORE_PRODUCT_ID != '' opt-in plus fork secret isolation give three independent gates before Partner Center is touched. The --noCommit draft gate on top is good defense-in-depth. No concerns on the safety model.
The PS wrapper itself is solid: @(...)-wrapped glob so .Count is reliable, newest-first sort, explicit multi-match refusal, and the deliberate exit-0-on-missing-package (matching the continue-on-error MSIX step) vs. exit-non-zero on a real publish failure. That distinction is exactly right.
Two things worth a second look
1. msstore flag spellings are untestable in CI — verify them manually. The whole path is dormant until credentials land, so a typo in -id or --noCommit won't surface until the first real release tag (and then it reddens or, worse, mis-submits a live product). The msstore CLI is built on System.CommandLine, which is strict about token form — --noCommit vs --no-commit, and -id vs --app-id. Please confirm the exact spellings against the pinned microsoft/microsoft-store-apppublisher@v1.1 CLI version (a quick msstore publish --help locally), since CI can't catch this for you.
2. reconfigure failure can be masked in the combined run: block. The workflow runs msstore reconfigure and then powershell -File publish-store.ps1 in a single step. GitHub's pwsh wrapper only propagates $LASTEXITCODE at the end of the block. If reconfigure fails (e.g., a mis-pasted secret) and the MSIX build happened to produce no .msixupload (the script then exit 0s), the step goes green with no submission and no error — auth breakage stays invisible. Consider either splitting reconfigure into its own step, or adding if ($LASTEXITCODE -ne 0) { throw } right after the reconfigure call so a bad auth config always fails loudly.
Neither blocks merge given the feature is wired-but-dormant. Nice clean separation between the workflow, the wrapper, and the docs.
🤖 aethersdr-agent · cost: $1.8975 · model: claude-opus-4-8
|
Merged — safe-by-construction and dormant until the credentials/variable are set. I verified the security model: SHA-pinned action, secrets passed via 📋 Two things to handle when you activate it (CI can't catch either — both surface only on the first real tagged release):
Activation order reminder (from the PR): set the four secrets first, then set |
## Release prep for v26.6.3 Bumps the version and refreshes documentation for the **58 commits since v26.6.2** (2026-06-07). No tag is created — tagging is the release trigger (the #3567 MS Store automation fires on `v*` tags), so that step is left to the maintainer after this merges. ### Version - `CMakeLists.txt`: `VERSION 26.6.2 → 26.6.3` (drives the MSIX `Identity.Version`). - README.md + AGENTS.md "current version" strings updated to match. ### CHANGELOG.md — v26.6.3 entry Covers: WFM software demodulator, APRS client, PSK Reporter reception map + reusable Qt mapping engine (QGeoView), Direwolf-derived VHF AFSK demodulator, DAX-IQ made fully usable, profile-load recovery hardening, DEXP protocol fix, Multi-Flex ping-watchdog grace, MQTT enhancements, the #3351 MainWindow decomposition + `RadioSession`, the MS Store CI auto-stage, and the fix roll-up. ### Doc refresh (reviewed all repo-root + `docs/` files) - **README.md** — Highlights: added WFM, AetherModem packet/APRS, PSK Reporter map. - **ROADMAP.md** — cycle → `post-v26.6.3`; moved **AetherModem Phase 1** from Queued to **Recently shipped** alongside WFM / AFSK / PSK-Reporter map / DAX-IQ / the decomposition. - **AGENTS.md** — version, added `RadioSession` to Key classes, threads **11 → 12**. - **CONTRIBUTING.md** — thread count 11 → 12. - **docs/architecture/pipelines.md** — added the **AX.25 TNC thread** (#3473) to the list + table; **11 → 12**. - **docs/MODEM.md** — VHF 1200 baud now documents the **Direwolf-derived Profile A+ demodulator (9 slicers)** that replaced the libmodem lane bank (#3527); the APRS client + map are no longer "out of scope" (HF 300-baud section unchanged). Reviewed (via two parallel doc-review agents) and found **no changes needed** in: CONSTITUTION, GOVERNANCE, SUPPORT, SECURITY, CODE_OF_CONDUCT, CLAUDE/GEMINI (pointer files), and the rest of `docs/` (audio-pipeline, vita49-format, tci-*, style/, theming/, qa/, etc.). ### Verification - Builds clean with `VERSION 26.6.3`; full suite **38/38 pass**. ### To roll the release (maintainer) After merge: `git tag -s v26.6.3 && git push origin v26.6.3` → triggers the Windows installer / release workflows (and the MS Store draft-stage once its credentials are set). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ethersdr#3567) Wires the weekly release workflow to stage a DRAFT Microsoft Store submission via the msstore CLI on every v* tag. Safe-by-construction: SHA-pinned action, secrets via env (not inlined), triple-gated (tag-ref + AETHERSDR_STORE_PRODUCT_ID opt-in variable + fork secret isolation), and --noCommit so CI never starts certification. Dormant until the credentials/variable are configured. Co-authored-by: jensenpat <patjensen@gmail.com>
Summary
Automates Microsoft Store publishing as part of the weekly (Sunday) release
cycle. Today the Windows Installer workflow builds the
.msixuploadandattaches it to the GitHub release; a maintainer then uploads it to Partner
Center by hand. This PR wires the upload step so that every
v*tag stages adraft submission to the Store via the Microsoft Store Developer CLI
(
msstore).CI never publishes to the live channel on its own. The submission is staged
with
--noCommit, so it lands as a draft in Partner Center — a maintainerreviews it and clicks Submit to Store to start certification.
The feature is wired but dormant: until the credentials below are set, the
workflow behaves exactly as it does today.
What changed
packaging/windows/publish-store.ps1(new) — finds the.msixuploadproduced by
create-msix.ps1and runsmsstore publish <pkg> -id <ProductId> --noCommit. If the (continue-on-error)MSIX step produced no upload, it warns and exits 0 rather than reddening an
otherwise-successful release.
.github/workflows/windows-installer.yml— adds Setup Microsoft StoreDeveloper CLI (
microsoft/microsoft-store-apppublisher@v1.1, SHA-pinned) andStage Microsoft Store submission (draft) after the release-attach step.
docs/WINDOWS-STORE-MSIX.md— flips the section from PLANNED → WIRED andexpands the one-time Entra/Partner Center setup into a concrete checklist.
Guard rails (all must pass before Partner Center is touched)
refs/tags/ref (never PRs or branch dispatch).AETHERSDR_STORE_PRODUCT_IDrepo variableis set. Absent ⇒ the whole step is a no-op.
reconfigureis a no-op on forks even on atag.
--noCommitkeeps it out of certification until a humansubmits.
Eligibility is already met: the
msstoreGitHub Actions path supportsfree products (AetherSDR qualifies) and requires the app to be already
live in the Store (it is — two prior manual submissions).
Credentials the maintainer (@jensenpat) will provide
These are configured outside the repo (Entra ID + Partner Center) and then
stored in Settings → Secrets and variables → Actions. Posting the full
table so maintainers know exactly what is coming and why:
AZURE_AD_TENANT_IDAZURE_AD_APPLICATION_CLIENT_IDAZURE_AD_APPLICATION_SECRETSELLER_IDAETHERSDR_STORE_PRODUCT_ID← set last to switch the automation onVersion discipline
Each Store submission must carry a higher
Identity.Versionthan the live one.The MSIX version derives from
project(AetherSDR VERSION ...)inCMakeLists.txt; the existing release-prep version bump already guarantees anewer version each release.
Promoting to fully automatic later
Drop
--noCommitinpublish-store.ps1to send each tag straight tocertification, or publish to a flight/insider ring first with
-f <flightId>and promote manually. Keep the draft gate until the weekly cadence is proven.
Testing
yaml.safe_load).merges safely ahead of the credential setup.
💻 Generated with Claude Code (Fable 5.0) with architecture by @jensenpat