fix(部署): 完整文件树一致时兼容 Squash 提交身份

This commit is contained in:
WuYongJie 2026-10-19 19:08:30 +08:00
parent 55b1d9d7dc
commit 7322ba1bde
3 changed files with 97 additions and 3 deletions

View File

@ -28,6 +28,15 @@
#
set -euo pipefail
release_includes_commit() {
git merge-base --is-ancestor "$1" "$2" 2>/dev/null && return 0
# Squash 改变提交身份;仅接受历史中完整文件树一致的提交。
local previous_tree release_trees
previous_tree="$(git rev-parse "${1}^{tree}" 2>/dev/null)" || return 1
release_trees="$(git log --format=%T "$2" 2>/dev/null)" || return 1
printf '%s\n' "$release_trees" | grep -Fx -- "$previous_tree" >/dev/null
}
server='218.93.190.75'
key_path=''
password_file=''
@ -522,7 +531,7 @@ if [ -n "$previous_sha" ]; then
if [ "$previous_sha_exists" -eq 0 ]; then
fail "线上当前版本 ${previous_sha} 在本地和 origin 都找不到(可能是未推送的分支),发布会把它覆盖。请让发布人先推送并合并进 main;确需撤回时显式添加 --allow-rollback。"
fi
if ! git merge-base --is-ancestor "$previous_sha" "$deploy_sha"; then
if ! release_includes_commit "$previous_sha" "$deploy_sha"; then
fail "线上当前版本($(git log -1 --format='%h %an %ad %s' --date=format:'%m-%d %H:%M' "$previous_sha"))不在本次发布的版本里,继续发布会把它及之后别人上线的改动撤掉。请先 git fetch 并把最新 main(或线上热修复分支)合入当前分支后重试;确需撤回时显式添加 --allow-rollback。"
fi
fi

View File

@ -50,6 +50,21 @@ $buildkitConfig = Join-Path $repoRoot 'deploy/buildkitd.toml'
$regctlVersion = 'v0.11.5'
$regctlSha256 = '9b83975d464e4ee5a9f899a3ead26072ed67e9a8ed0f582897bfad8141b45938'
function Test-CaiwuReleaseIncludesCommit {
param([string]$PreviousSha, [string]$ReleaseSha)
git merge-base --is-ancestor $PreviousSha $ReleaseSha 2>$null
if ($LASTEXITCODE -eq 0) { return $true }
# Squash 会改变提交身份;只接受发布历史中完整文件树一致的提交。
# 不按提交说明、部分文件或 patch 相似度放行。
$previousTree = (git rev-parse "${PreviousSha}^{tree}" 2>$null) -join ''
if ($LASTEXITCODE -ne 0) { return $false }
$releaseTrees = @(git log --format=%T $ReleaseSha 2>$null)
if ($LASTEXITCODE -ne 0) { return $false }
return $releaseTrees -contains $previousTree
}
function Invoke-Native {
param(
[Parameter(Mandatory = $true)]
@ -440,8 +455,7 @@ try {
if (-not $previousShaExists) {
throw "线上当前版本 $previousSha 在本地和 origin 都找不到(可能是未推送的分支),发布会把它覆盖。请让发布人先推送并合并进 main;确需撤回时显式添加 -AllowRollback。服务器未发生任何变更。"
}
git merge-base --is-ancestor $previousSha $deploySha 2>$null
if ($LASTEXITCODE -ne 0) {
if (-not (Test-CaiwuReleaseIncludesCommit -PreviousSha $previousSha -ReleaseSha $deploySha)) {
$previousInfo = (git log -1 --format='%h %an %ad %s' --date=format:'%m-%d %H:%M' $previousSha) -join ''
throw "线上当前版本($previousInfo)不在本次发布的版本里,继续发布会把它及之后别人上线的改动撤掉。请先 git fetch 并把最新 main(或线上热修复分支)合入当前分支后重试;确需撤回时显式添加 -AllowRollback。服务器未发生任何变更。"
}

View File

@ -0,0 +1,71 @@
param([string]$BashPath='bash')
$ErrorActionPreference='Stop'
$repoRoot=Split-Path $PSScriptRoot -Parent
$psSource=[IO.File]::ReadAllText((Join-Path $repoRoot 'scripts/deploy-from-workstation.ps1'))
$parseErrors=$null; $tokens=$null
$ast=[Management.Automation.Language.Parser]::ParseInput($psSource,[ref]$tokens,[ref]$parseErrors)
if($parseErrors.Count){throw 'Deployment PowerShell syntax invalid'}
$function=$ast.Find({param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Test-CaiwuReleaseIncludesCommit'},$true)
if(-not $function){throw 'PowerShell inclusion guard missing'}
. ([ScriptBlock]::Create($function.Extent.Text))
$bashSource=[IO.File]::ReadAllText((Join-Path $repoRoot 'scripts/deploy-from-mac.sh')).Replace("`r`n","`n")
$bashFunction=[regex]::Match($bashSource,'(?ms)^release_includes_commit\(\) \{\n.*?^\}').Value
if(-not $bashFunction){throw 'Bash inclusion guard missing'}
$fixture=Join-Path ([IO.Path]::GetTempPath()) ('caiwu-release-inclusion-'+[guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $fixture | Out-Null
$expectedFixture=[IO.Path]::GetFullPath($fixture)
function Invoke-FixtureGit {
param([Parameter(ValueFromRemainingArguments=$true)][string[]]$GitArgs)
$output=@(git @GitArgs 2>&1)
if($LASTEXITCODE -ne 0){throw "Fixture Git failed: $($GitArgs[0])"}
return $output
}
function New-FixtureCommit {
param([string]$Value,[string]$Message)
[IO.File]::WriteAllText((Join-Path $fixture 'value.txt'),$Value)
Invoke-FixtureGit add value.txt | Out-Null
Invoke-FixtureGit commit -m $Message | Out-Null
return (Invoke-FixtureGit rev-parse HEAD) -join ''
}
Push-Location $fixture
try {
Invoke-FixtureGit init -b main | Out-Null
Invoke-FixtureGit config user.name 'Release guard fixture' | Out-Null
Invoke-FixtureGit config user.email 'release-guard@example.invalid' | Out-Null
$base=New-FixtureCommit 'base' 'base'
Invoke-FixtureGit switch -c hotfix | Out-Null
$hotfix=New-FixtureCommit 'hotfix content' 'hotfix'
Invoke-FixtureGit switch main | Out-Null
$squashed=New-FixtureCommit 'hotfix content' 'squashed identity'
$release=New-FixtureCommit 'next authorized change' 'new release'
Invoke-FixtureGit switch -c unrelated $base | Out-Null
$unrelated=New-FixtureCommit 'different hotfix content' 'hotfix'
$cases=@(
@{name='direct ancestor';previous=$base;release=$release;expected=$true},
@{name='same commit';previous=$hotfix;release=$hotfix;expected=$true},
@{name='identical squash tree at head';previous=$hotfix;release=$squashed;expected=$true},
@{name='identical squash tree in release ancestry';previous=$hotfix;release=$release;expected=$true},
@{name='real unmerged change';previous=$unrelated;release=$release;expected=$false},
@{name='rollback to before hotfix';previous=$hotfix;release=$base;expected=$false},
@{name='missing deployed commit';previous=('0'*40);release=$release;expected=$false},
@{name='missing release commit';previous=$hotfix;release=('0'*40);expected=$false}
)
foreach($case in $cases){
$actual=Test-CaiwuReleaseIncludesCommit -PreviousSha $case.previous -ReleaseSha $case.release
if($actual -ne $case.expected){throw "PowerShell failed: $($case.name)"}
$code=$bashFunction+"`nrelease_includes_commit "+'"$1" "$2"'
& $BashPath --noprofile --norc -c $code guard $case.previous $case.release
$bashActual=$LASTEXITCODE -eq 0
if($bashActual -ne $case.expected){throw "Bash failed: $($case.name)"}
}
"PASS: $($cases.Count) real Git cases for both PowerShell and Bash guards"
} finally {
Pop-Location
$resolved=(Resolve-Path -LiteralPath $fixture).Path
if([IO.Path]::GetFullPath($resolved) -ne $expectedFixture -or (Get-Item -LiteralPath $resolved).Attributes.HasFlag([IO.FileAttributes]::ReparsePoint)){
throw 'Refusing cleanup outside the exact generated fixture directory'
}
Remove-Item -LiteralPath $resolved -Recurse -Force
}
exit 0