Automated Windows App Deployment to Microsoft Store
How to achieve end-to-end automation from version parsing to Store release, saying goodbye to tedious manual operations.
Background
If you have an Electron application you want to list on the Microsoft Store, you'll likely run into this problem: the Store doesn't support separate installation processes—you have to package the desktop application and server payload together into a complete AppX/MSIX package.
That would be manageable enough. But the problem continues. Each time you release a new version, you need to:
- Check if desktop and server versions have been updated
- Check out code from the corresponding tags
- Download and inject the server payload
- Build the MSIX package
- Manually upload to the Microsoft Store
- Configure store information and pricing
If every step requires manual operation, that's too much hassle. And it's error-prone—you might not even remember which steps you've completed and which you haven't.
Actually, this isn't really anyone's fault—manual operations are naturally prone to omissions. It's just that we really didn't want to go through this hassle every time, so we decided to solve this problem once and for all—by automating the entire process.
About HagiCode
The solution shared in this article comes from our practical experience in the HagiCode project. As an AI code assistant, HagiCode provides both desktop and web clients and needs to support multiple distribution channels. In implementing automated Windows Store deployment, we've developed a complete automation solution.
Speaking of which, this was probably an unexpected bonus. What started as a simple time-saver ended up becoming this comprehensive solution.
Technical Architecture Analysis
This problem actually involves coordinating multiple technical layers. We can break it down into five layers:
Version Coordination Layer
First, we need to know when a new version needs to be released. We need to parse the latest versions of desktop and server components from an Azure index (a blob storage we use to store build artifacts), then determine whether a new Store package needs to be generated.
It's like asking yourself: Is it time to do this now?
Workspace Management Layer
AppX builds depend on source code-level configuration and runtime layout, so we can't simply repackage existing build artifacts. We need to check out code from a specific tag in the desktop repository to ensure the correct source code state is used for the build.
After all, if the source code is wrong, everything else is wasted effort.
Runtime Packaging Layer
This is the core part. We need to inject the server payload into the desktop application's packaging layout. This way, when the application starts, it will detect the packaged runtime and enter Steam mode (offline mode).
If this step isn't done well, all previous efforts are wasted.
Build Output Layer
Use electron-builder to generate an MSIX package that meets Store requirements. This step needs to run in a Windows environment because AppX builds require the Windows SDK.
Some things just need to be done in the right place—won't work elsewhere.
Distribution Layer
Finally, publish to GitHub Releases and the Microsoft Store. GitHub Release serves as backup and version tracking, while the Microsoft Store is面向终端用户.
Everything is ready, just needs this final push.
Overall Architecture Design
┌─────────────────────────────────────────────────────────────┐
│ package-release workflow │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ resolve_plan │───▶│ build │ │
│ │ (Version parsing & skipping) │ │ (MSIX build) │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ skip_summary │ │ publish │ │
│ │ (Skip report) │ │ (Publish) │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ publish_store │ │
│ │ (Store publish) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
The entire process is driven by GitHub Actions and can be triggered on a schedule (every 4 hours) or manually. When manually triggered, you can specify specific desktop and server versions, or force a rebuild.
Actually, once every four hours isn't that frequent—code updates vary in speed, but this approach is safer.
Implementation Details
1. Build Plan Resolution
The first step is to determine whether a new version needs to be built. We need to parse the current versions of desktop and server components, then check whether a corresponding release already exists.
// scripts/resolve-dispatch-build-plan.mjs
export async function resolveDispatchBuildPlan({
eventName,
eventPayload,
desktopAzureSasUrl,
serverAzureSasUrl,
}) {
// Parse trigger inputs
const trigger = normalizeTriggerInputs({ eventName, eventPayload });
// Parse desktop and server versions from Azure index
const [desktopRelease, serverRelease] = await Promise.all([
resolveIndexRelease({
azureSasUrl: desktopAzureSasUrl,
platformId: 'win-x64'
}),
resolveIndexRelease({
azureSasUrl: serverAzureSasUrl,
platformId: 'win-x64'
})
]);
// Generate release tag
const desktopTag = normalizeGitTag(desktopRelease.version);
const releaseTag = deriveStoreReleaseTag(desktopRelease.version, serverRelease.version);
// For example: desktop-v1.2.3-server-v4.5.6
// Check if already exists (avoid duplicate builds)
const existingRelease = await findReleaseByTag(packerRepository, releaseTag);
const shouldBuild = !existingRelease || trigger.forceRebuild;
return {
release: { tag: releaseTag, exists: Boolean(existingRelease) },
build: { shouldBuild, skipReason },
upstream: { desktop: { tag: desktopTag }, server }
};
}
The key in this step is the skip logic—if the same version combination has already been built, there's no need to run the entire build process again. This saves CI costs and time.
Doing the same thing repeatedly probably doesn't make much sense. After all, time and resources are limited.
2. Workspace Preparation
Once you've decided to build, you need to prepare a clean build environment. We use git worktree to check out code from a specific tag.
// scripts/prepare-packaging-workspace.mjs
export async function preparePackagingWorkspace({
planPath,
platformId,
workspacePath,
desktopSourcePath
}) {
// Use git worktree to check out desktop code from specific tag
await runCommand('git', [
'-C', resolvedDesktopSourcePath,
'worktree', 'add', '--detach',
desktopWorkspace,
`refs/tags/${plan.upstream.desktop.tag}`
]);
// Validate workspace
const validation = await validateDesktopWorkspace({
desktopWorkspace,
storePackageConfig
});
// Create workspace manifest
const workspaceManifest = {
desktopWorkspace,
runtimeInjectionRoot: validation.runtimeRoot,
desktopTag: plan.upstream.desktop.tag,
desktopRef,
};
return workspaceManifest;
}
The advantage of using worktree is that it doesn't affect the main working directory, builds can run in parallel, and it's easy to clean up after the build completes.
After all, no one wants their main working directory messed up because of a build.
3. Server Payload Injection
This step downloads and injects the server payload into the correct location.
// scripts/stage-server-payload.mjs
export async function stageServerPayload({
planPath,
workspacePath,
platformId,
azureSasUrl
}) {
// Download server payload from Azure index
const assetSource = resolveAssetDownloadUrl({ asset, sasUrl: azureSasUrl });
await downloadFromSource({
sourceUrl: assetSource,
destinationPath: downloadPath
});
// Extract and validate
await extractArchive(downloadPath, extractionPath);
const runtimeRoot = await resolveRuntimeRoot(extractionPath);
const validation = await validateServerPayloadRoot(runtimeRoot, platformId);
// Inject into packaging runtime layout
// Target path is resources/portable-fixed/current
// This path will be mapped to extra/portable-fixed/current in AppX
await copyDir(runtimeRoot, targetPath);
}
The key is the path mapping—resources/portable-fixed/current gets packaged to extra/portable-fixed/current in the AppX, so the application detects the local runtime at startup and enters offline mode.
Paths are unforgiving—one wrong character and it won't be found.
4. MSIX Build
With the prepared workspace and server payload, you can build the MSIX package.
// scripts/build-appx.mjs
export async function buildAppx({
planPath,
workspacePath,
platformId
}) {
// Generate Store-specific electron-builder config overlay
const overlayConfig = await writeStoreElectronBuilderConfig({
desktopWorkspace: workspaceManifest.desktopWorkspace,
sourceConfigPath: storePackageConfig.desktop.electronBuilderConfigPath,
outputConfigPath: 'electron-builder.store.yml'
});
// Run desktop build command
await runShellCommand(
buildDesktopStoreCommand(overlayConfig.outputPath, desktopScripts),
workspaceManifest.desktopWorkspace
);
// Collect MSIX output
const storeOutputs = await findStoreOutputs(pkgDirectory);
const artifactPath = path.join(
workspaceManifest.outputDirectory,
artifactFileName
);
await copySingleFile(primaryOutput, artifactPath);
}
The electron-builder configuration needs to include Store-specific metadata like package identity, publisher display name, etc. This information will be used during Store submission.
If the configuration is wrong, the package won't build. Nothing more to say about that.
5. Publishing to GitHub
After the build completes, you need to publish the artifacts to GitHub Releases.
// scripts/publish-release.mjs
export async function publishRelease({
planPath,
artifactsDir,
outputDir,
forceDryRun
}) {
// Build publication artifact manifest
const publicationArtifacts = await buildPublicationArtifacts({
plan,
artifactsDir,
outputDir
});
if (!dryRun) {
// Create or update GitHub Release
const releaseResult = await upsertReleaseNotes(
plan.release.repository,
plan.release.tag,
token,
{ name, body }
);
// Upload assets
for (const upload of publicationArtifacts.uploads) {
await uploadReleaseAsset({
release: releaseResult.release,
filePath: upload.filePath,
fileName: upload.fileName
});
}
}
}
GitHub Release serves as version tracking and backup—even if the Store publication fails, the build artifacts are preserved.
It's always good to have a backup plan. What if you need to roll back someday?
6. Publishing to Microsoft Store
The final step is publishing to the Store using the Microsoft Store CLI.
# .github/workflows/package-release.yml
publish_store:
runs-on: windows-latest
steps:
- name: Configure Microsoft Store CLI
uses: microsoft/microsoft-store-apppublisher@v1.2
- name: Publish MSIX packages to Store
shell: pwsh
run: |
msstore reconfigure --tenantId $env:AZURE_AD_TENANT_ID ...
msstore publish "$($package.FullName)" -id $env:MICROSOFT_STORE_PRODUCT_ID
This step needs to run on a Windows runner because the Microsoft Store CLI requires a Windows environment.
Some things just need to be done in the right place—won't work in a different environment.
Practical Guide
Configuration Files
The Store package configuration file defines package identity and build settings:
{
"packageIdentity": {
"displayName": "Hagicode",
"publisherDisplayName": "newbe36524",
"publisher": "CN=8B6C8A94-AAE5-4C8B-9202-A29EA42B042F",
"identityName": "newbe36524.Hagicode",
"backgroundColor": "transparent",
"languages": ["en-US", "zh-CN", "zh-Hant"]
},
"supportedWindowsTargets": ["win-x64"],
"desktop": {
"submodulePath": "inputs/hagicode-desktop",
"electronBuilderConfigPath": "electron-builder.yml",
"buildScript": "build:appx",
"runtimeInjectionPath": "resources/portable-fixed/current"
}
}
Configuration is painful to change, so it's best to get it right the first time.
Workflow Triggers
Can be triggered on schedule or manually:
on:
schedule:
- cron: '0 */4 * * *' # Run every 4 hours
workflow_dispatch:
inputs:
desktop_version:
description: 'Desktop version selector'
required: false
server_version:
description: 'Server version selector'
required: false
force_rebuild:
description: 'Force rebuild even if exists'
type: boolean
dry_run:
description: 'Build only, do not publish'
type: boolean
Automatic or manual, as long as it runs in the end.
Key Considerations
Git Tag Requirements: The desktop repository must contain tags corresponding to release versions (e.g., v1.2.3), otherwise the build will fail
Azure SAS URL: Need to configure
DESKTOP_AZURE_SAS_URLandSERVER_AZURE_SAS_URLenvironment variables to access the index-
Microsoft Store Credentials: Need to configure the following secrets:
AZURE_AD_APPLICATION_CLIENT_IDAZURE_AD_APPLICATION_SECRETAZURE_AD_TENANT_IDSELLER_IDMICROSOFT_STORE_PRODUCT_ID
Runtime Validation: Server payload must contain required runtime files, otherwise packaging will fail
Windows Runner: AppX builds and Store publishing need to run on Windows runners
Duplicate Detection: Scheduled runs check for existing release tags to avoid unnecessary builds
We've probably stepped on all these pits, so writing them down as a reminder. After all, who wants to make the same mistakes twice?
Summary
Windows Store automated deployment looks complex, but when broken down into independent steps, each step isn't difficult. The key is:
- Use git worktree to manage build environments
- Inject server payload into the correct path
- Use electron-builder to generate MSIX packages
- Use Store CLI for final publishing
This solution has been running stably in the HagiCode project for several months, basically achieving the goal of "full automation"—except for initial credential and setup configuration, subsequent version releases require no manual intervention.
Actually, many things are like this—they look difficult but are straightforward to do. It just takes some patience and experimentation.
If you're working on similar automation, I hope this article provides some reference. Of course, every project is different and may need adjustments based on specific requirements.
After all, all roads lead to Rome—some roads are just easier to travel than others...
References
- Microsoft Store CLI Documentation
- electron-builder Documentation
- HagiCode GitHub Repository
- HagiCode Official Website
Original Article & License
Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.
- Author: newbe36524
- Original URL: https://docs.hagicode.com/go?platform=devto&target=%2Fblog%2F2026-05-20-windows-app-automation-to-microsoft-store%2F
- License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.
Top comments (0)