Skip to content

ci(windows): auto-stage Microsoft Store submission on release tags#3567

Merged
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:feature/msstore-ci-publish
Jun 14, 2026
Merged

ci(windows): auto-stage Microsoft Store submission on release tags#3567
ten9876 merged 1 commit into
aethersdr:mainfrom
jensenpat:feature/msstore-ci-publish

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Summary

Automates Microsoft Store publishing as part of the weekly (Sunday) release
cycle. Today the Windows Installer workflow builds the .msixupload and
attaches 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 a
draft 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 maintainer
reviews 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 .msixupload
    produced by create-msix.ps1 and runs
    msstore 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 Store
    Developer CLI
    (microsoft/microsoft-store-apppublisher@v1.1, SHA-pinned) and
    Stage Microsoft Store submission (draft) after the release-attach step.
  • docs/WINDOWS-STORE-MSIX.md — flips the section from PLANNED → WIRED and
    expands the one-time Entra/Partner Center setup into a concrete checklist.

Guard rails (all must pass before Partner Center is touched)

  1. Tag-only — runs only on a refs/tags/ ref (never PRs or branch dispatch).
  2. Opt-in — skipped unless the AETHERSDR_STORE_PRODUCT_ID repo variable
    is set. Absent ⇒ the whole step is a no-op.
  3. Forks can't read secrets — so reconfigure is a no-op on forks even on a
    tag.
  4. Draft gate--noCommit keeps it out of certification until a human
    submits.

Eligibility is already met: the msstore GitHub Actions path supports
free 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:

Concept What it is How it's obtained Stored in GitHub as
Directory A Microsoft Entra ID tenant on the Partner Center account Partner Center → Account settings → TenantsCreate Microsoft Entra ID (free; skipped if one exists)
Service account An Entra app registration + client secret entra.microsoft.com → App registrations → New registration; Certificates & secrets → new secret
Authorization The app holds the Manager role Partner Center → User management → Microsoft Entra applications → add the app, assign Manager
Tenant ID Entra → Overview copy secret AZURE_AD_TENANT_ID
Client ID App registration's Application (client) ID copy secret AZURE_AD_APPLICATION_CLIENT_ID
Client Secret the secret value (shown once) copy at creation secret AZURE_AD_APPLICATION_SECRET
Seller ID Partner Center → Account settings → Identifiers copy secret SELLER_ID
Store product ID 12-char ID from the product's Partner Center URL copy variable AETHERSDR_STORE_PRODUCT_ID ← set last to switch the automation on

⚠️ Set AETHERSDR_STORE_PRODUCT_ID last, after the four secrets are in
place. The workflow gates on this variable being non-empty, so setting it is
what flips the feature from dormant to active. (This is also why the PR does
not pre-create it — a placeholder would enable the step before the secrets
exist and fail every release.)

Version discipline

Each Store submission must carry a higher Identity.Version than the live one.
The MSIX version derives from project(AetherSDR VERSION ...) in
CMakeLists.txt; the existing release-prep version bump already guarantees a
newer version each release.

Promoting to fully automatic later

Drop --noCommit in publish-store.ps1 to send each tag straight to
certification, 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

  • Workflow YAML validated (yaml.safe_load).
  • Dormant by default — no behavior change until the variable is set, so this
    merges safely ahead of the credential setup.

💻 Generated with Claude Code (Fable 5.0) with architecture by @jensenpat

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>
@jensenpat jensenpat marked this pull request as ready for review June 14, 2026 02:49
@jensenpat jensenpat requested review from a team as code owners June 14, 2026 02:49

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. msstore publish argument 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 -i flag rather than positional. Worth confirming against the actual CLI version the pinned microsoft-store-apppublisher@v1.1 action installs: (a) does publish take the package path positionally or via -i/--inputDirectory, and (b) are the flags -id and --noCommit (vs --appId/-nc etc.)? If the action is available locally, a quick msstore publish --help would 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.

  2. reconfigure failure mode (minor). If AETHERSDR_STORE_PRODUCT_ID is set but a secret is later rotated/missing, the msstore reconfigure run step fails and reddens an otherwise-successful release — slightly at odds with the "never redden a good release" goal that publish-store.ps1 itself 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

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@ten9876 ten9876 merged commit 88319d8 into aethersdr:main Jun 14, 2026
6 checks passed
@ten9876

ten9876 commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Merged — safe-by-construction and dormant until the credentials/variable are set. I verified the security model: SHA-pinned action, secrets passed via env: (not inlined into the shell), triple-gated (tag-ref + AETHERSDR_STORE_PRODUCT_ID opt-in + fork secret isolation), and --noCommit draft gate. Nice work.

📋 Two things to handle when you activate it (CI can't catch either — both surface only on the first real tagged release):

  1. Verify the msstore flag spellings before the first live tag. The whole path is dormant until creds land, so a typo in -id / --noCommit won't show up until a real v* tag — and then it reddens the release or (worse) mis-submits a live product. msstore is built on System.CommandLine, which is strict about token form (--noCommit vs --no-commit, -id vs --app-id). Run msstore publish --help against the pinned microsoft-store-apppublisher@v1.1 CLI version and confirm the exact spellings in publish-store.ps1.

  2. Make a reconfigure auth failure fail loudly. msstore reconfigure and the publish script share one run: block, and GitHub's pwsh wrapper only propagates $LASTEXITCODE at the end. If reconfigure fails (mis-pasted secret) and the MSIX step produced no .msixupload (the script then exit 0s), the step goes green with broken auth invisible. Either split reconfigure into its own step, or add if ($LASTEXITCODE -ne 0) { throw } right after the reconfigure call.

Activation order reminder (from the PR): set the four secrets first, then set AETHERSDR_STORE_PRODUCT_ID last — that variable is the on-switch.

ten9876 added a commit that referenced this pull request Jun 14, 2026
## 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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants