返回提交历史
Modified
README.md
+2
-0
Modified
artifacts/SHA256.json
+2
-2
Added
tools/check-release-cleanup.ps1
+73
-0
Added
tools/clean-release-artifacts.ps1
+122
-0
Modified
tools/package-delivery.py
+10
-2
Modified
tools/publish-android.ps1
+4
-2
XFEstudio/STHApplication
chore: clean superseded release artifacts after verified delivery
afa578a
代码差异
6 个文件
+213
-6
@@ -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
本源码环境可运行:
@@ -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
]
@@ -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"
@@ -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
}
@@ -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)
@@ -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"