XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

STHApplication

【MAUI】寄当家——安卓端交易管理系统

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/STHApplication

chore: clean superseded release artifacts after verified delivery

c80edf1
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

6 个文件 +213 -6
Modified README.md +2 -0
@@ -8,6 +8,8 @@ Android .NET MAUI 应用与独立 ASP.NET Core 登录授权服务器。管理员
8 8 - `artifacts/寄当家-服务器-Windows.zip`:Windows x64 服务端,包含运行时,无需另装 .NET。
9 9 - `artifacts/寄当家-源码.zip`:客户端、业务库、服务端、测试与构建脚本。
10 10
11 `python tools/package-delivery.py` 完成三份交付文件与 SHA256 校验后,自动清理旧版安装包和临时测试构建副本,保留当前交付文件、验收报告、签名密钥、原始参考资料及数据。可用 `tools/clean-release-artifacts.ps1 -IncludeTestBuildOutputs -WhatIf` 预览清理范围;缺少新版文件或校验失败时不执行清理。
12
11 13 解压服务器包,用 PowerShell 运行 `启动服务器.ps1`。默认仅监听 `http://127.0.0.1:5188`。首次启动在服务器的 `data/initial-admin.txt` 生成管理员账号和随机密码。该文件只在首次创建管理员时生成;数据库和此文件需要保留。管理员在 APP 的「账户与授权」中修改密码。
12 14
13 15 本源码环境可运行:
Modified artifacts/SHA256.json +2 -2
@@ -11,7 +11,7 @@
11 11 },
12 12 {
13 13 "name": "寄当家-源码.zip",
14 "bytes": 11920672,
15 "sha256": "179ed91e084041e1e4725729b558a0590bcde539b6884fdd56b90be6945f3375"
14 "bytes": 11925241,
15 "sha256": "922db09bbbef4ead07aec70a8300333b783bc54db5cf06b1dd02c4661abaaa2a"
16 16 }
17 17 ]
Added tools/check-release-cleanup.ps1 +73 -0
@@ -0,0 +1,73 @@
1 $ErrorActionPreference = 'Stop'
2 $taskWorkspace = Split-Path $PSScriptRoot -Parent
3 $testRoot = Join-Path $taskWorkspace ("artifacts\test-data\release-cleanup-" + [guid]::NewGuid().ToString('N'))
4 $checkCount = 0
5 function Check([bool]$condition, [string]$message) {
6 if (!$condition) { throw $message }
7 $script:checkCount++
8 }
9 function New-Case([string]$name) {
10 $caseRoot = Join-Path $testRoot $name
11 foreach ($relative in @('tools', 'src\JiDangJia', 'artifacts')) {
12 New-Item -ItemType Directory -Path (Join-Path $caseRoot $relative) -Force | Out-Null
13 }
14 Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'clean-release-artifacts.ps1') -Destination (Join-Path $caseRoot 'tools')
15 Set-Content -LiteralPath (Join-Path $caseRoot 'src\JiDangJia\JiDangJia.csproj') -Value '<Project><PropertyGroup><ApplicationDisplayVersion>2.5.2</ApplicationDisplayVersion><ApplicationVersion>252</ApplicationVersion></PropertyGroup></Project>' -Encoding utf8
16 $records = foreach ($file in @('寄当家-2.5.2.apk', '寄当家-服务器-Windows.zip', '寄当家-源码.zip')) {
17 $path = Join-Path $caseRoot "artifacts\$file"
18 Set-Content -LiteralPath $path -Value 'synthetic current release' -Encoding utf8
19 [pscustomobject]@{name=$file;bytes=(Get-Item -LiteralPath $path).Length;sha256=(Get-FileHash -LiteralPath $path).Hash}
20 }
21 $records | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $caseRoot 'artifacts\SHA256.json') -Encoding utf8
22 Set-Content -LiteralPath (Join-Path $caseRoot 'artifacts\寄当家-2.5.1.apk') -Value 'synthetic old release'
23 return $caseRoot
24 }
25 function Must-Refuse([string]$caseRoot) {
26 $refused = $false
27 try { & (Join-Path $caseRoot 'tools\clean-release-artifacts.ps1') -IncludeTestBuildOutputs | Out-Null }
28 catch { $refused = $true }
29 Check $refused 'Unsafe cleanup was accepted'
30 Check (Test-Path -LiteralPath (Join-Path $caseRoot 'artifacts\寄当家-2.5.1.apk')) 'Cleanup partially ran before validation finished'
31 }
32
33 $case = New-Case 'valid'
34 Set-Content -LiteralPath (Join-Path $case 'artifacts\寄当家-2.5.3.apk') -Value 'synthetic future release'
35 Set-Content -LiteralPath (Join-Path $case 'artifacts\unrelated.zip') -Value 'preserve me'
36 $generated = Join-Path $case 'artifacts\test-data\fixture\build\obj'
37 New-Item -ItemType Directory -Path $generated -Force | Out-Null
38 Set-Content -LiteralPath (Join-Path $generated 'generated.tmp') -Value 'rebuildable'
39 $stats = & (Join-Path $case 'tools\clean-release-artifacts.ps1') -IncludeTestBuildOutputs -WhatIf
40 Check ($stats.RemovedTargets -eq 0 -and (Test-Path -LiteralPath $generated)) 'Preview changed files'
41 $stats = & (Join-Path $case 'tools\clean-release-artifacts.ps1') -IncludeTestBuildOutputs
42 Check ($stats.RemovedTargets -eq 2) 'Wrong cleanup scope'
43 foreach ($file in @('寄当家-2.5.2.apk', '寄当家-服务器-Windows.zip', '寄当家-源码.zip', '寄当家-2.5.3.apk', 'unrelated.zip')) {
44 Check (Test-Path -LiteralPath (Join-Path $case "artifacts\$file")) "Required or unrelated file was removed: $file"
45 }
46 Check (!(Test-Path -LiteralPath $generated)) 'Generated output was retained'
47 $stats = & (Join-Path $case 'tools\clean-release-artifacts.ps1') -IncludeTestBuildOutputs
48 Check ($stats.RemovedTargets -eq 0) 'Cleanup was not idempotent'
49
50 $case = New-Case 'bad-hash'
51 Add-Content -LiteralPath (Join-Path $case 'artifacts\寄当家-2.5.2.apk') -Value 'damaged'
52 Must-Refuse $case
53 $case = New-Case 'missing-latest'
54 Remove-Item -LiteralPath (Join-Path $case 'artifacts\寄当家-服务器-Windows.zip')
55 Must-Refuse $case
56 foreach ($protected in @('books.db', 'jidangjia.keystore')) {
57 $case = New-Case $protected
58 $generated = Join-Path $case 'artifacts\test-data\fixture\build\obj'
59 New-Item -ItemType Directory -Path $generated -Force | Out-Null
60 Set-Content -LiteralPath (Join-Path $generated $protected) -Value 'synthetic protected content'
61 Must-Refuse $case
62 Check (Test-Path -LiteralPath (Join-Path $generated $protected)) 'Protected content was removed'
63 }
64 $case = New-Case 'junction'
65 $outside = Join-Path $testRoot 'outside'
66 New-Item -ItemType Directory -Path $outside -Force | Out-Null
67 Set-Content -LiteralPath (Join-Path $outside 'sentinel.txt') -Value 'preserve me'
68 $generated = Join-Path $case 'artifacts\test-data\fixture\build'
69 New-Item -ItemType Directory -Path $generated -Force | Out-Null
70 New-Item -ItemType Junction -Path (Join-Path $generated 'obj') -Target $outside | Out-Null
71 Must-Refuse $case
72 Check (Test-Path -LiteralPath (Join-Path $outside 'sentinel.txt')) 'Junction target was removed'
73 Write-Output "$checkCount release cleanup checks passed"
Added tools/clean-release-artifacts.ps1 +122 -0
@@ -0,0 +1,122 @@
1 [CmdletBinding(SupportsShouldProcess)]
2 param([switch]$IncludeTestBuildOutputs)
3
4 $ErrorActionPreference = 'Stop'
5 $taskWorkspace = [IO.Path]::GetFullPath((Split-Path $PSScriptRoot -Parent))
6 $taskPrefix = $taskWorkspace.TrimEnd('\', '/') + [IO.Path]::DirectorySeparatorChar
7 $taskArtifacts = Join-Path $taskWorkspace 'artifacts'
8 [xml]$project = Get-Content -LiteralPath (Join-Path $taskWorkspace 'src\JiDangJia\JiDangJia.csproj') -Raw
9 $currentVersion = [version]$project.SelectSingleNode('//ApplicationDisplayVersion').InnerText
10 $currentCode = [int]$project.SelectSingleNode('//ApplicationVersion').InnerText
11 $currentApk = "寄当家-$currentVersion.apk"
12 $requiredFiles = @($currentApk, '寄当家-服务器-Windows.zip', '寄当家-源码.zip')
13 $manifest = @(Get-Content -LiteralPath (Join-Path $taskArtifacts 'SHA256.json') -Raw | ConvertFrom-Json)
14
15 # Never remove an older release until the complete replacement is present and verified.
16 foreach ($name in $requiredFiles) {
17 $record = @($manifest | Where-Object { $_.name -ceq $name })
18 $file = Get-Item -LiteralPath (Join-Path $taskArtifacts $name)
19 if ($record.Count -ne 1 -or $file.PSIsContainer -or
20 $file.Length -ne $record[0].bytes -or
21 (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash -ne $record[0].sha256) {
22 throw "新版交付文件尚未通过校验,取消清理:$name"
23 }
24 }
25
26 $targets = [Collections.Generic.List[string]]::new()
27 function Add-CleanupTarget([string]$path) {
28 if (!(Test-Path -LiteralPath $path)) { return }
29 $resolved = [IO.Path]::GetFullPath((Get-Item -LiteralPath $path -Force).FullName)
30 if (!$resolved.StartsWith($taskPrefix, [StringComparison]::OrdinalIgnoreCase)) {
31 throw '清理目标超出项目目录'
32 }
33 # Check ancestors as well as descendants; refuse junctions and symbolic links.
34 $ancestor = $resolved
35 while ($ancestor -ne $taskWorkspace) {
36 if (((Get-Item -LiteralPath $ancestor -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
37 throw '清理目标含符号链接或目录联接,取消清理'
38 }
39 $ancestor = Split-Path $ancestor -Parent
40 }
41 $entries = @((Get-Item -LiteralPath $resolved -Force))
42 if ($entries[0].PSIsContainer) {
43 $entries += @(Get-ChildItem -LiteralPath $resolved -Recurse -Force)
44 }
45 foreach ($entry in $entries) {
46 if (($entry.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
47 ($entry.PSIsContainer -and $entry.Name -in @('data', 'private-vault', 'credentials', 'reference', 'verification')) -or
48 (!$entry.PSIsContainer -and ($entry.Name -match '\.db($|[-.])|\.(keystore|pfx|p12)$' -or
49 $entry.Name -in @('initial-admin.txt', 'private-vault.key', 'signing-password.txt', 'signing-key-password.txt')))) {
50 throw '清理目标含数据、密钥或链接,取消清理'
51 }
52 }
53 if (!$targets.Contains($resolved)) { $targets.Add($resolved) }
54 }
55
56 foreach ($file in Get-ChildItem -LiteralPath $taskArtifacts -File -Force) {
57 if ($file.Name -match '^寄当家-(?:(?:服务器-Windows|源码)-)?(?<version>\d+\.\d+\.\d+(?:\.\d+)?)\.(?:apk|zip)(?:\.idsig)?$' -and
58 [version]$Matches.version -lt $currentVersion) {
59 Add-CleanupTarget $file.FullName
60 }
61 }
62 foreach ($name in @('aligned-release.apk', 'aligned-release.apk.idsig', 'signature-check.apk', 'signature-check.apk.idsig')) {
63 Add-CleanupTarget (Join-Path $taskArtifacts $name)
64 }
65
66 if ($IncludeTestBuildOutputs) {
67 $testRoot = Join-Path $taskArtifacts 'test-data'
68 if (Test-Path -LiteralPath $testRoot) {
69 foreach ($directory in Get-ChildItem -LiteralPath $testRoot -Directory -Force) {
70 foreach ($name in @('bin', 'obj')) {
71 Add-CleanupTarget (Join-Path $directory.FullName "build\$name")
72 }
73 }
74 $communityRoot = Join-Path $testRoot 'community-revision'
75 if (Test-Path -LiteralPath $communityRoot) {
76 foreach ($directory in Get-ChildItem -LiteralPath $communityRoot -Directory -Force) {
77 if ($directory.Name -match '^(?:runtime-[a-f0-9]{32}|funding-runtime-\d+)$' -and
78 (Test-Path -LiteralPath (Join-Path $directory.FullName 'JiDangJia.Server.dll'))) {
79 Add-CleanupTarget $directory.FullName
80 }
81 }
82 }
83 $releaseCopies = Join-Path $testRoot 'funding-release'
84 if (Test-Path -LiteralPath $releaseCopies) {
85 foreach ($file in Get-ChildItem -LiteralPath $releaseCopies -File -Force) {
86 if ($file.Name -match '^release-(?<code>\d+)\.apk(?:\.idsig)?$' -and [int]$Matches.code -le $currentCode) {
87 Add-CleanupTarget $file.FullName
88 }
89 }
90 }
91 foreach ($relative in @('polish-release.apk', 'server-connectivity\prior-local-2.1.0.apk', 'finance-rental\release-inspect.apk')) {
92 Add-CleanupTarget (Join-Path $testRoot $relative)
93 }
94 }
95 # These generated copies are superseded by the validated delivery files above.
96 Add-CleanupTarget (Join-Path $taskWorkspace 'src\JiDangJia.Server\bin\Release\net10.0\publish')
97 Add-CleanupTarget (Join-Path $taskWorkspace 'src\JiDangJia\bin\Debug')
98 Add-CleanupTarget (Join-Path $taskWorkspace 'src\JiDangJia\obj\Debug')
99 }
100
101 $removedBytes = 0L
102 $plannedBytes = 0L
103 $removedCount = 0
104 foreach ($path in $targets) {
105 $item = Get-Item -LiteralPath $path -Force
106 $bytes = if ($item.PSIsContainer) {
107 (Get-ChildItem -LiteralPath $path -Recurse -File -Force | Measure-Object Length -Sum).Sum
108 } else { $item.Length }
109 $plannedBytes += $bytes
110 if ($PSCmdlet.ShouldProcess($path, '删除旧版或重复的发布产物')) {
111 Remove-Item -LiteralPath $path -Recurse -Force
112 $removedBytes += $bytes
113 $removedCount++
114 }
115 }
116 [pscustomobject]@{
117 Version = "$currentVersion"
118 PlannedTargets = $targets.Count
119 PlannedBytes = $plannedBytes
120 RemovedTargets = $removedCount
121 RemovedBytes = $removedBytes
122 }
Modified tools/package-delivery.py +10 -2
@@ -1,10 +1,15 @@
1 1 from pathlib import Path
2 2 from zipfile import ZipFile, ZIP_DEFLATED
3 import hashlib, json, shutil
3 import hashlib, json, shutil, subprocess
4 from xml.etree import ElementTree
4 5
5 6 root=Path(__file__).resolve().parent.parent
6 7 art=root/'artifacts'
7 8 server=art/'server-win-x64'
9 version=ElementTree.parse(root/'src'/'JiDangJia'/'JiDangJia.csproj').findtext('./PropertyGroup/ApplicationDisplayVersion')
10 assert version and (art/f'寄当家-{version}.apk').is_file(), '当前版本安装包尚未生成'
11 cleanup_shell=shutil.which('pwsh') or shutil.which('powershell')
12 if not cleanup_shell:raise RuntimeError('发布清理需要 PowerShell')
8 13 def private_file(p):
9 14 return ('private-vault' in p.parts or p.name == 'private-vault.key'
10 15 or p.name.startswith(('accounts.db', 'identity.db'))
@@ -36,9 +41,11 @@ with ZipFile(art/'寄当家-源码.zip','w',ZIP_DEFLATED,compresslevel=6) as z:
36 41 for p in (art/'verification').glob('*'):
37 42 if p.is_file():z.write(p,p.relative_to(root))
38 43 z.write(root/'tools'/'check-published-funding-server.cjs','tools/check-published-funding-server.cjs')
44 z.write(root/'tools'/'clean-release-artifacts.ps1','tools/clean-release-artifacts.ps1')
45 z.write(root/'tools'/'check-release-cleanup.ps1','tools/check-release-cleanup.ps1')
39 46
40 47 manifest=[]
41 for name in ['寄当家-2.5.2.apk','寄当家-服务器-Windows.zip','寄当家-源码.zip']:
48 for name in [f'寄当家-{version}.apk','寄当家-服务器-Windows.zip','寄当家-源码.zip']:
42 49 p=art/name
43 50 assert p.exists() and p.stat().st_size>0
44 51 manifest.append({'name':name,'bytes':p.stat().st_size,'sha256':hashlib.file_digest(p.open('rb'),'sha256').hexdigest()})
@@ -48,3 +55,4 @@ for name in ['寄当家-服务器-Windows.zip','寄当家-源码.zip']:
48 55 assert not any('credentials/' in x or 'test-data/' in x or private_file(Path(x)) for x in z.namelist())
49 56 (art/'SHA256.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2),encoding='utf-8')
50 57 for item in manifest:print(item['name'],round(item['bytes']/1024/1024,2),'MiB')
58 subprocess.run([cleanup_shell,'-NoProfile','-File',str(root/'tools'/'clean-release-artifacts.ps1'),'-IncludeTestBuildOutputs'],check=True)
Modified tools/publish-android.ps1 +4 -2
@@ -19,5 +19,7 @@ dotnet publish src/JiDangJia/JiDangJia.csproj -c Release -f net10.0-android -p:A
19 19 if($LASTEXITCODE-ne 0){throw 'Android 发布失败'}
20 20 $apk=Get-ChildItem -LiteralPath 'src/JiDangJia/bin/Release/net10.0-android/publish' -Filter '*-Signed.apk' | Select-Object -First 1
21 21 if(!$apk){throw '未找到签名安装包'}
22 Copy-Item -LiteralPath $apk.FullName -Destination (Join-Path $workspace 'artifacts\寄当家-2.5.2.apk') -Force
23 Write-Output '安装包已生成:artifacts\寄当家-2.5.2.apk'
22 [xml]$project=Get-Content -LiteralPath 'src/JiDangJia/JiDangJia.csproj' -Raw
23 $releaseVersion=$project.SelectSingleNode('//ApplicationDisplayVersion').InnerText
24 Copy-Item -LiteralPath $apk.FullName -Destination (Join-Path $workspace "artifacts\寄当家-$releaseVersion.apk") -Force
25 Write-Output "安装包已生成:artifacts\寄当家-$releaseVersion.apk"