Windows 应用自动上架 Microsoft Store 的自动化实践
如何从版本解析到 Store 发布实现全流程自动化,告别手动操作的繁琐流程。
背景
如果你有 Electron 应用想要上架 Microsoft Store,大概会碰到这样的麻烦:Store 不支持分离的安装流程------你得把桌面应用和服务器负载打包成一个完整的 AppX/MSIX 包。
这倒也罢了。问题还在后面呢。每次发新版本,你得:
- 检查桌面和服务器版本是否更新
- 从对应的 tag 检出代码
- 下载并注入服务器负载
- 构建 MSIX 包
- 手动上传到 Microsoft Store
- 配置商店信息和定价
如果每一步都要手动操作,那也太折腾人了。而且容易出错,哪一步做了哪一步没做,自己都未必记得清楚。
其实这也不能怪谁,毕竟手动操作本就容易遗漏。只是我们实在不想每次都这样折腾,于是决定彻底解决这个问题------让整个流程自动跑起来。
关于 HagiCode
本文分享的方案来自我们在 HagiCode 项目中的实践经验。作为一个 AI 代码助手,HagiCode 提供桌面端和 Web 端,需要支持多种分发渠道。在实现 Windows Store 自动上架的过程中,我们总结出了一套完整的自动化方案。
说起来,这大概也算是个意外收获。本来只是为了省点时间,没想到最后做出来这么一套东西。
技术架构分析
这个问题其实涉及多个技术层次的协调。我们可以把它分成五层:
版本协调层
首先需要知道什么时候需要发布新版本。我们需要从 Azure 索引(我们用来存储构建产物的 blob 存储)中解析出桌面和服务器组件的最新版本,然后判断是否需要生成新的 Store 包。
这就像是在问自己:现在该做这件事了吗?
工作空间管理层
AppX 的构建依赖于源代码级别的配置和运行时布局,所以不能简单地用现成的构建产物重新打包。我们需要从桌面仓库的特定 tag 检出代码,确保构建使用的源代码状态是正确的。
毕竟,源代码不对,后面再怎么努力也是白搭。
运行时打包层
这是核心部分。我们需要把服务器负载注入到桌面应用的打包布局中。这样当应用启动时,会检测到打包的运行时并进入 Steam 模式(离线模式)。
这一步做不好,前面所有的努力也都白费了。
构建输出层
使用 electron-builder 生成符合 Store 要求的 MSIX 包。这一步需要在 Windows 环境中运行,因为 AppX 构建需要 Windows SDK。
有些事情就是要在对的地方做,换地方就不行。
发布分发层
最后是发布到 GitHub Releases 和 Microsoft Store。GitHub Release 作为备份和版本追踪,Microsoft Store 则面向终端用户。
一切准备就绪,就差最后这一哆嗦了。
整体架构设计
┌─────────────────────────────────────────────────────────────┐
│ package-release workflow │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ resolve_plan │───▶│ build │ │
│ │ (版本解析与跳过) │ │ (MSIX 构建) │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ skip_summary │ │ publish │ │
│ │ (跳过报告) │ │ (发布) │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ publish_store │ │
│ │ (Store 发布) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
整个流程由 GitHub Actions 驱动,可以定时触发(每 4 小时)或手动触发。手动触发时可以指定特定的桌面版本和服务器版本,或者强制重建。
其实四小时一次也不算频繁,毕竟代码更新总是有快有慢,只是这样比较保险罢了。
实现详解
1. 构建计划解析
第一步是确定是否需要构建新版本。我们需要解析桌面和服务器组件的当前版本,然后检查是否已经存在对应的发布。
javascript
// scripts/resolve-dispatch-build-plan.mjs
export async function resolveDispatchBuildPlan({
eventName,
eventPayload,
desktopAzureSasUrl,
serverAzureSasUrl,
}) {
// 解析触发器输入
const trigger = normalizeTriggerInputs({ eventName, eventPayload });
// 从 Azure 索引解析桌面和服务器版本
const [desktopRelease, serverRelease] = await Promise.all([
resolveIndexRelease({
azureSasUrl: desktopAzureSasUrl,
platformId: 'win-x64'
}),
resolveIndexRelease({
azureSasUrl: serverAzureSasUrl,
platformId: 'win-x64'
})
]);
// 生成发布 tag
const desktopTag = normalizeGitTag(desktopRelease.version);
const releaseTag = deriveStoreReleaseTag(desktopRelease.version, serverRelease.version);
// 比如:desktop-v1.2.3-server-v4.5.6
// 检查是否已存在(避免重复构建)
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 }
};
}
这个步骤的关键在于跳过逻辑------如果相同版本的组合已经构建过,就没有必要再跑一遍完整的构建流程。这样可以节省 CI 成本和时间。
重复做同样的事情,大概也没什么意义。毕竟,时间和资源都是有限的。
2. 工作空间准备
确定要构建后,需要准备一个干净的构建环境。我们使用 git worktree 来从特定的 tag 检出代码。
javascript
// scripts/prepare-packaging-workspace.mjs
export async function preparePackagingWorkspace({
planPath,
platformId,
workspacePath,
desktopSourcePath
}) {
// 使用 git worktree 从特定 tag 检出桌面代码
await runCommand('git', [
'-C', resolvedDesktopSourcePath,
'worktree', 'add', '--detach',
desktopWorkspace,
`refs/tags/${plan.upstream.desktop.tag}`
]);
// 验证工作空间
const validation = await validateDesktopWorkspace({
desktopWorkspace,
storePackageConfig
});
// 创建工作空间清单
const workspaceManifest = {
desktopWorkspace,
runtimeInjectionRoot: validation.runtimeRoot,
desktopTag: plan.upstream.desktop.tag,
desktopRef,
};
return workspaceManifest;
}
使用 worktree 的好处是不影响主工作目录,构建可以并行进行,而且构建完成后可以轻松清理。
毕竟,谁也不希望因为构建把主工作目录搞得乱七八糟。
3. 服务器负载注入
这一步是把服务器负载下载并注入到正确的位置。
javascript
// scripts/stage-server-payload.mjs
export async function stageServerPayload({
planPath,
workspacePath,
platformId,
azureSasUrl
}) {
// 从 Azure 索引下载服务器负载
const assetSource = resolveAssetDownloadUrl({ asset, sasUrl: azureSasUrl });
await downloadFromSource({
sourceUrl: assetSource,
destinationPath: downloadPath
});
// 解压并验证
await extractArchive(downloadPath, extractionPath);
const runtimeRoot = await resolveRuntimeRoot(extractionPath);
const validation = await validateServerPayloadRoot(runtimeRoot, platformId);
// 注入到打包运行时布局
// 目标路径是 resources/portable-fixed/current
// 这个路径会被映射到 AppX 内的 extra/portable-fixed/current
await copyDir(runtimeRoot, targetPath);
}
关键是路径映射------resources/portable-fixed/current 会被打包到 AppX 的 extra/portable-fixed/current,这样应用启动时就会检测到本地运行时并进入离线模式。
路径这东西,错一点都不行。一点偏差,可能就找不到了。
4. MSIX 构建
有了准备好的工作空间和服务器负载,就可以构建 MSIX 包了。
javascript
// scripts/build-appx.mjs
export async function buildAppx({
planPath,
workspacePath,
platformId
}) {
// 生成 Store 特定的 electron-builder 配置覆盖
const overlayConfig = await writeStoreElectronBuilderConfig({
desktopWorkspace: workspaceManifest.desktopWorkspace,
sourceConfigPath: storePackageConfig.desktop.electronBuilderConfigPath,
outputConfigPath: 'electron-builder.store.yml'
});
// 运行桌面构建命令
await runShellCommand(
buildDesktopStoreCommand(overlayConfig.outputPath, desktopScripts),
workspaceManifest.desktopWorkspace
);
// 收集 MSIX 输出
const storeOutputs = await findStoreOutputs(pkgDirectory);
const artifactPath = path.join(
workspaceManifest.outputDirectory,
artifactFileName
);
await copySingleFile(primaryOutput, artifactPath);
}
electron-builder 配置需要包含 Store 特定的元数据,比如包身份、发布者显示名称等。这些信息会在 Store 提交时使用。
配置不对,包也打不出来。这也没什么好说的。
5. 发布到 GitHub
构建完成后,需要把产物发布到 GitHub Releases。
javascript
// scripts/publish-release.mjs
export async function publishRelease({
planPath,
artifactsDir,
outputDir,
forceDryRun
}) {
// 构建发布产物清单
const publicationArtifacts = await buildPublicationArtifacts({
plan,
artifactsDir,
outputDir
});
if (!dryRun) {
// 创建或更新 GitHub Release
const releaseResult = await upsertReleaseNotes(
plan.release.repository,
plan.release.tag,
token,
{ name, body }
);
// 上传资产
for (const upload of publicationArtifacts.uploads) {
await uploadReleaseAsset({
release: releaseResult.release,
filePath: upload.filePath,
fileName: upload.fileName
});
}
}
}
GitHub Release 作为版本追踪和备份,即使 Store 发布失败,也能保留构建产物。
留一条后路总是好的。万一哪天需要回溯呢?
6. 发布到 Microsoft Store
最后一步是使用 Microsoft Store CLI 发布到商店。
yaml
# .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
这一步需要在 Windows runner 上运行,因为 Microsoft Store CLI 需要 Windows 环境。
有些事情就是要在对的地方做,换了环境就不行。
实践指南
配置文件
Store 包配置文件定义了包身份和构建设置:
json
{
"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"
}
}
配置这东西,改一次就够头疼的了,所以最好一次搞对。
工作流触发
可以定时触发或手动触发:
yaml
on:
schedule:
- cron: '0 */4 * * *' # 每 4 小时运行一次
workflow_dispatch:
inputs:
desktop_version:
description: 'Desktop 版本选择器'
required: false
server_version:
description: 'Server 版本选择器'
required: false
force_rebuild:
description: '即使已存在也强制重建'
type: boolean
dry_run:
description: '仅构建,不发布'
type: boolean
自动也好,手动也罢,反正最后能跑起来就行。
关键注意事项
-
Git Tag 要求:桌面仓库必须包含与发布版本对应的 tag(如 v1.2.3),否则构建会失败
-
Azure SAS URL :需要配置
DESKTOP_AZURE_SAS_URL和SERVER_AZURE_SAS_URL环境变量以访问索引 -
Microsoft Store 凭证:需要配置以下密钥:
AZURE_AD_APPLICATION_CLIENT_IDAZURE_AD_APPLICATION_SECRETAZURE_AD_TENANT_IDSELLER_IDMICROSOFT_STORE_PRODUCT_ID
-
运行时验证:服务器负载必须包含必需的运行时文件,否则打包会失败
-
Windows Runner:AppX 构建和 Store 发布需要在 Windows runner 上执行
-
重复检测:计划运行会检查是否存在相同的 release tag,避免不必要的构建
这些坑我们大概都踩过一遍了,所以写出来提醒一下。毕竟,谁愿意重复犯错呢?
总结
Windows Store 自动上架看起来复杂,但拆解成独立步骤后,每一步都不难。关键是:
- 用 git worktree 管理构建环境
- 把服务器负载注入到正确的路径
- 使用 electron-builder 生成 MSIX 包
- 用 Store CLI 完成最终发布
这套方案在 HagiCode 项目中已经稳定运行了几个月,基本实现了"完全自动化"的目标------除了第一次配置凭证和设置,后续的版本发布都不需要人工介入。
其实很多事情都是这样,看起来难,做起来也就那么回事。只是需要一点耐心和尝试罢了。
如果你也在做类似的自动化工作,希望这篇文章能给你一些参考。当然,每个项目的情况不一样,可能需要根据具体需求调整方案。
毕竟,条条大路通罗马,只是有的路好走一点,有的路稍微曲折一点而已......
参考资料
原文与版权说明
感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。
本内容采用人工智能辅助协作,最终内容由作者审核并确认。
- 本文作者: newbe36524
- 原文链接: https://docs.hagicode.com/go?platform=cnblogs&target=%2Fblog%2F2026-05-20-windows-app-automation-to-microsoft-store%2F
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!