Skip to content

Build And Sign Package #77

Build And Sign Package

Build And Sign Package #77

name: Build And Sign Package
on:
push:
branches:
- flight-released
- released
workflow_dispatch:
inputs:
store_only:
description: Only build and publish the Microsoft Store package
required: false
type: boolean
default: false
jobs:
build:
if: github.event_name != 'workflow_dispatch' || inputs.store_only != true
runs-on: windows-latest
strategy:
matrix:
platform: ${{ github.ref_name == 'flight-released' && fromJSON('["x64"]') || fromJSON('["x86", "x64", "ARM64"]') }}
env:
BRANCH_NAME: ${{ github.ref_name }}
steps:
- uses: actions/checkout@v4
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
- name: Add WindowsAppSDKSelfContained
shell: pwsh
run: |
$csprojPath = "GalgameManager/GalgameManager.csproj"
# 读取 csproj 文件
[xml]$xml = Get-Content $csprojPath
# 筛选出包含 <TargetFramework> 的 PropertyGroup(主 PropertyGroup)
$propertyGroup = $xml.Project.PropertyGroup | Where-Object { $_.TargetFramework }
if (!$propertyGroup) {
Write-Host "未找到包含 <TargetFramework> 的 PropertyGroup,改为使用第一个 PropertyGroup。"
$propertyGroup = $xml.Project.PropertyGroup | Select-Object -First 1
if (!$propertyGroup) {
Write-Error "没有可用的 PropertyGroup。"
exit 1
}
}
# 如果当前没有 WindowsAppSDKSelfContained,则创建该节点并追加
if (!$propertyGroup.WindowsAppSDKSelfContained) {
$newElement = $xml.CreateElement("WindowsAppSDKSelfContained")
$newElement.InnerText = "true"
$propertyGroup.AppendChild($newElement) | Out-Null
}
# 保存
$xml.Save($csprojPath)
- name: Set PUBLISHER
run: |
$manifestPath = "GalgameManager\Package.appxmanifest"
if (!(Test-Path $manifestPath)) {
Write-Error "无法找到 AppxManifest.xml 文件,路径为 $manifestPath"
exit 1
}
[xml]$xml = Get-Content $manifestPath
$xml.Package.Identity.Name = "37126GoldenPotato137.PotatoVN.sideload"
$xml.Package.Identity.Publisher = "${{ vars.PUBLISHER }}"
$xml.Save($manifestPath)
- name: Update app icon
shell: pwsh
run: |
xcopy "GalgameManager\Assets\Pictures\icon-sideload\*" "GalgameManager\Assets\" /s /e /y
echo "Updated app icon"
- name: Create the app package
run: |
msbuild GalgameManager\GalgameManager.csproj /restore `
/p:Platform=${{ matrix.platform }} `
/p:Configuration=Release `
/p:UapAppxPackageBuildMode=SideloadOnly `
/p:AppxPackageDir=..\publish\ `
/p:GenerateAppxPackageOnBuild=true `
- name: Move package
shell: pwsh
run: |
# 将 msix 移动到 publish 根目录
$subFolder = Get-ChildItem -Path publish -Directory | Select-Object -First 1
Get-ChildItem -Path $subFolder.FullName | ForEach-Object {Write-Output $_.Name}
$msixFile = Get-ChildItem -Path $subFolder.FullName -Filter "*.msix" -File | Select-Object -First 1
if ($msixFile) { Move-Item $msixFile.FullName -Destination publish -Force }
- name: Rename MSIX file to include branch name
shell: pwsh
run: |
ls publish
$platform = "${{ matrix.platform }}"
$branch = "${{ env.BRANCH_NAME }}"
$msixFiles = Get-ChildItem -Path publish -Filter "*.msix" -File
foreach ($file in $msixFiles) {
$newName = "PotatoVN-$platform-$branch.msix"
Rename-Item $file.FullName -NewName $newName
}
ls publish
- name: Upload build artifacts
id: upload-artifact
uses: actions/upload-artifact@v4
with:
name: PotatoVN-${{ matrix.platform }}-${{ env.BRANCH_NAME }}
path: |
publish/PotatoVN-${{ matrix.platform }}-${{ env.BRANCH_NAME }}.msix
- name: Sign the package with SignPath
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '87787551-8846-4890-a0a9-b34f834c4c0f'
project-slug: 'PotatoVN'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'signed_package'
- name: Upload signed package
uses: actions/upload-artifact@v4
with:
name: PotatoVN-${{ matrix.platform }}-${{ env.BRANCH_NAME }}-signed
path: signed_package
upload_to_r2:
runs-on: ubuntu-latest
needs: build
strategy:
matrix:
platform: ${{ (github.ref_name == 'flight-released' || github.ref_name == 'main') && fromJSON('["x64"]') || fromJSON('["x86", "x64", "ARM64"]') }}
steps:
- name: Checkout code to read version
uses: actions/checkout@v4
- name: Get package version
id: get_version
run: |
sudo apt-get update && sudo apt-get install -y xmlstarlet
# 使用带有命名空间处理的 xmlstarlet 命令来健壮地提取版本号
VERSION=$(xmlstarlet sel \
-N x="http://schemas.microsoft.com/appx/manifest/foundation/windows10" \
-t -v "/x:Package/x:Identity/@Version" \
GalgameManager/Package.appxmanifest)
echo "Extracted version: $VERSION"
if [ -z "$VERSION" ]; then
echo "Error: Could not extract version number."
exit 1
fi
# 将版本号设置为该步骤的输出,供后续步骤使用
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Download signed artifact
uses: actions/download-artifact@v4
with:
name: PotatoVN-${{ matrix.platform }}-${{ github.ref_name }}-signed
path: signed-package
- name: Upload to R2
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
AWS_REGION: "auto"
shell: bash
run: |
if [[ "${{ github.ref_name }}" == "released" ]]; then
S3_FOLDER="release"
elif [[ "${{ github.ref_name }}" == "flight-released" ]]; then
S3_FOLDER="flight-released"
else
echo "Branch is not configured for deployment."
exit 1
fi
SIGNED_FILE=$(find signed-package -type f -name "*.msix")
if [ -z "$SIGNED_FILE" ]; then
echo "Error: Signed MSIX file not found in downloaded artifact."
exit 1
fi
REMOTE_FILENAME="${{ steps.get_version.outputs.version }}_${{ matrix.platform }}.msix"
S3_PATH="s3://potatovn-release/$S3_FOLDER/$REMOTE_FILENAME"
echo "Uploading local file '$SIGNED_FILE' to R2 destination '$S3_PATH'"
aws s3api put-object \
--endpoint-url https://248c03263a47d926c6c479d9cc47ef6c.r2.cloudflarestorage.com \
--bucket potatovn-release \
--key "$S3_FOLDER/$REMOTE_FILENAME" \
--body "$SIGNED_FILE" \
--cli-read-timeout 0
release:
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
models: read
env:
BRANCH_NAME: ${{ github.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install xmlstarlet
run: sudo apt-get update && sudo apt-get install -y xmlstarlet
- name: Resolve version and release tag
id: meta
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=$(xmlstarlet sel \
-N x="http://schemas.microsoft.com/appx/manifest/foundation/windows10" \
-t -v "/x:Package/x:Identity/@Version" \
GalgameManager/Package.appxmanifest)
if [ -z "$VERSION" ]; then
echo "Error: Could not extract version number."
exit 1
fi
if [[ "$BRANCH_NAME" == "released" ]]; then
TAG="v$VERSION"
CHANNEL="stable"
else
TAG="v$VERSION-flight"
CHANNEL="flight"
fi
# 同名 release 已存在时(例如同版本重复推送),追加 run number 防止冲突
if gh release view "$TAG" >/dev/null 2>&1; then
TAG="$TAG-${{ github.run_number }}"
fi
echo "Resolved tag: $TAG (channel: $CHANNEL, version: $VERSION)"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "channel=$CHANNEL" >> "$GITHUB_OUTPUT"
- name: Determine base commit of previous release
id: base
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_BEFORE: ${{ github.event.before }}
CHANNEL: ${{ steps.meta.outputs.channel }}
run: |
BASE=""
# 1) 上一个同通道 GitHub Release 的 tag
if [[ "$CHANNEL" == "flight" ]]; then
JQ_FILTER='[.[] | select(.tag_name | test("-flight"))] | .[0].tag_name'
else
JQ_FILTER='[.[] | select(.tag_name | test("-flight") | not)] | .[0].tag_name'
fi
PREV_TAG=$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=30" --jq "$JQ_FILTER" 2>/dev/null || true)
if [[ -n "$PREV_TAG" && "$PREV_TAG" != "null" ]] && git rev-parse -q --verify "refs/tags/$PREV_TAG" >/dev/null 2>&1; then
BASE=$(git rev-list -n 1 "$PREV_TAG")
echo "Base commit from previous release tag: $PREV_TAG"
fi
# 2) push 事件记录的分支旧 HEAD
if [[ -z "$BASE" && -n "$EVENT_BEFORE" && "$EVENT_BEFORE" != "0000000000000000000000000000000000000000" ]] && git cat-file -e "$EVENT_BEFORE^{commit}" 2>/dev/null; then
BASE="$EVENT_BEFORE"
echo "Base commit from event.before"
fi
# 3) 分支上最近一条版本号提交
if [[ -z "$BASE" ]]; then
BASE=$(git log -i -E --grep='^v ?[0-9]+\.[0-9]+' -n 1 --format='%H' || true)
echo "Base commit from latest version-bump commit"
fi
# 4) 兜底,保证区间永远有效
if [[ -z "$BASE" ]]; then
BASE="HEAD~30"
echo "Base commit fallback: HEAD~30"
fi
echo "base=$BASE" >> "$GITHUB_OUTPUT"
- name: Build filtered commit list
run: |
# fix(<scope>-dev) 是开发过程中产生的问题,用户从未遇到,不进入发布日志
git log --no-merges --format='%s' "${{ steps.base.outputs.base }}..HEAD" \
| grep -viE '^fix\([^)]*-dev\)\s*:' \
| sed 's/^/* /' > commits.txt || true
if [[ ! -s commits.txt ]]; then
echo "* 常规维护与内部改进" > commits.txt
fi
cat commits.txt
- name: Generate release notes with GitHub Models
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
{
echo "你是 PotatoVN(一款 Windows 上的 Galgame 管理工具)的发布经理。以下是本次发布区间内的 git commit 列表(conventional commits 格式,描述为中文):"
echo
cat commits.txt
echo
echo "请将其整理为面向普通用户的中文 GitHub Release 更新日志,要求:"
echo "1. 按以下分组输出(没有内容的分组直接省略):✨ 新功能 / 🛠 改进优化 / 🐛 问题修复 / 🖥 同步服务器(scope 为 server 或 service 的改动归入此组)"
echo "2. 用通俗的语言改写每条 commit,突出用户能感受到的变化,略去技术实现细节;相关联的 commit 合并为一条"
echo "3. 以下内容一律忽略,不得出现在日志中:版本号提交(如 v1.10.0.5)、Update data、翻译同步(l10n/crowdin)、文档、测试、构建/CI 等内部杂项"
echo "4. 列表中若仍有 fix(xxx-dev) 形式的提交:这些是开发过程中产生、用户从未遇到的问题,一律不得出现"
echo "5. 分组标题用三级标题(###),条目用无序列表;只输出 markdown 正文,不要一级标题,不要任何解释性前后缀"
echo "6. 直接输出正文本身,绝对不要使用 \`\`\` 代码块包裹输出"
} > prompt.txt
jq -n --rawfile prompt prompt.txt \
'{model: "openai/gpt-4o", messages: [{role: "user", content: $prompt}], temperature: 0.3}' > payload.json
HTTP_CODE=$(curl -sS -w '%{http_code}' -o response.json \
-H "Authorization: Bearer $GH_TOKEN" -H "Content-Type: application/json" \
"https://models.github.ai/inference/chat/completions" -d @payload.json)
if [[ "$HTTP_CODE" == "200" ]] && jq -e '.choices[0].message.content | length > 0' response.json >/dev/null 2>&1; then
jq -r '.choices[0].message.content' response.json > notes.md
echo "Release notes generated by GitHub Models (openai/gpt-4o)."
else
echo "::warning::AI generation failed (HTTP $HTTP_CODE), falling back to filtered commit list."
head -c 500 response.json || true
{ echo "### 更新内容"; echo; cat commits.txt; } > notes.md
fi
echo "----- notes.md -----"
cat notes.md
- name: Download signed artifacts
uses: actions/download-artifact@v4
with:
pattern: PotatoVN-*-${{ env.BRANCH_NAME }}-signed
path: signed
merge-multiple: true
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.meta.outputs.tag }}
CHANNEL: ${{ steps.meta.outputs.channel }}
run: |
mapfile -t FILES < <(find signed -type f -name "*.msix")
if [ ${#FILES[@]} -eq 0 ]; then
echo "Error: No signed msix found in downloaded artifacts."
exit 1
fi
echo "Attaching ${#FILES[@]} package(s):"
printf '%s\n' "${FILES[@]}"
ARGS=(create "$TAG" "${FILES[@]}" --target "$GITHUB_SHA" --title "$TAG" --notes-file notes.md)
if [[ "$CHANNEL" == "flight" ]]; then
ARGS+=(--prerelease)
else
ARGS+=(--latest)
fi
gh release "${ARGS[@]}"
# 发布商店包(Microsoft Store Developer CLI)
# 与 sideload 产物不同:商店包使用仓库中签入的商店身份,不执行 Set PUBLISHER / 图标替换,
# 也不需要 SignPath 签名(商店审核通过后由微软重新签名)
# msstore publish 会提交并启动认证;workflow_dispatch 的 store_only 输入可只重跑此 job,
# 避免重复生成 GitHub Release 或再次上传 R2 产物
publish_store:
if: github.ref_name == 'released' || github.ref_name == 'flight-released'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
- name: Add WindowsAppSDKSelfContained
shell: pwsh
run: |
$csprojPath = "GalgameManager/GalgameManager.csproj"
# 读取 csproj 文件
[xml]$xml = Get-Content $csprojPath
# 筛选出包含 <TargetFramework> 的 PropertyGroup(主 PropertyGroup)
$propertyGroup = $xml.Project.PropertyGroup | Where-Object { $_.TargetFramework }
if (!$propertyGroup) {
Write-Host "未找到包含 <TargetFramework> 的 PropertyGroup,改为使用第一个 PropertyGroup。"
$propertyGroup = $xml.Project.PropertyGroup | Select-Object -First 1
if (!$propertyGroup) {
Write-Error "没有可用的 PropertyGroup。"
exit 1
}
}
# 如果当前没有 WindowsAppSDKSelfContained,则创建该节点并追加
if (!$propertyGroup.WindowsAppSDKSelfContained) {
$newElement = $xml.CreateElement("WindowsAppSDKSelfContained")
$newElement.InnerText = "true"
$propertyGroup.AppendChild($newElement) | Out-Null
}
# 保存
$xml.Save($csprojPath)
# 商店提交需要 .msixupload;AppxBundle 打包三架构为一个 bundle。
# bundle 路径会强制走一次性证书签名,命令行构建没有证书会报 MSB4044,
# 因此生成一个主体与商店发布者匹配的临时自签证书(每次运行即弃,商店审核后由微软重签)
- name: Generate temporary signing certificate
id: cert
shell: pwsh
run: |
$cert = New-SelfSignedCertificate -Type Custom `
-Subject "CN=AB6A9A1A-DC58-4454-A578-33528FD22B56" `
-KeyUsage DigitalSignature `
-FriendlyName "PotatoVNStoreBuildTemp" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
echo "thumbprint=$($cert.Thumbprint)" >> $env:GITHUB_OUTPUT
- name: Create the store package
run: |
msbuild GalgameManager\GalgameManager.csproj /restore `
/p:Platform=x64 `
/p:Configuration=Release `
/p:UapAppxPackageBuildMode=StoreUpload `
/p:AppxBundle=Always `
/p:AppxBundlePlatforms="x86|x64|arm64" `
/p:AppxPackageDir=..\publish-store\ `
/p:GenerateAppxPackageOnBuild=true `
/p:AppxPackageSigningEnabled=true `
/p:PackageCertificateThumbprint="${{ steps.cert.outputs.thumbprint }}"
- name: Locate msixupload
id: package
shell: pwsh
run: |
$file = Get-ChildItem -Path publish-store -Filter "*.msixupload" -Recurse -File | Select-Object -First 1
if (!$file) {
Write-Host "::error::未找到 .msixupload 产物"
exit 1
}
Write-Host "Store package: $($file.FullName)"
echo "path=$($file.FullName)" >> $env:GITHUB_OUTPUT
- name: Configure Microsoft Store CLI
uses: microsoft/microsoft-store-apppublisher@v1.1
- name: Reconfigure store credentials
run: msstore reconfigure `
--tenantId ${{ secrets.PARTNER_CENTER_TENANT_ID }} `
--sellerId ${{ secrets.PARTNER_CENTER_SELLER_ID }} `
--clientId ${{ secrets.PARTNER_CENTER_CLIENT_ID }} `
--clientSecret ${{ secrets.PARTNER_CENTER_CLIENT_SECRET }}
- name: Publish to Microsoft Store (stable channel)
if: github.ref_name == 'released'
run: msstore publish "${{ steps.package.outputs.path }}" -id ${{ vars.STORE_PRODUCT_ID }}
- name: Publish to Microsoft Store (flight channel)
if: github.ref_name == 'flight-released'
shell: pwsh
run: |
$flightId = "${{ vars.STORE_FLIGHT_ID }}"
if ([string]::IsNullOrWhiteSpace($flightId)) {
Write-Host "::error::未配置 STORE_FLIGHT_ID 仓库变量:请先在 Partner Center 创建 Package Flight,再将其 flightId 设为仓库变量 STORE_FLIGHT_ID"
exit 1
}
msstore publish "${{ steps.package.outputs.path }}" -id ${{ vars.STORE_PRODUCT_ID }} -f $flightId