184 lines
11 KiB
PowerShell
184 lines
11 KiB
PowerShell
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$script:XasRepositoryUrl = 'https://code.it.aisuan.cn/wenhuadian/xas-skills.git'
|
|
|
|
function Invoke-XasGit {
|
|
param([string]$RepositoryRoot, [string[]]$Arguments)
|
|
# Capture errors without printing Git's possibly credential-bearing diagnostics.
|
|
$oldPreference = $ErrorActionPreference
|
|
try {
|
|
$ErrorActionPreference = 'Continue'
|
|
# Prevent read-only status checks from refreshing the index on disk.
|
|
$output = @(& git --no-optional-locks -C $RepositoryRoot @Arguments 2>&1)
|
|
$code = $LASTEXITCODE
|
|
return [pscustomobject]@{ ExitCode = $code; Output = (($output | ForEach-Object { [string]$_ }) -join "`n") }
|
|
}
|
|
catch {
|
|
return [pscustomobject]@{ ExitCode = 1; Output = '' }
|
|
}
|
|
finally { $ErrorActionPreference = $oldPreference }
|
|
}
|
|
|
|
function Invoke-XasCheckedGit {
|
|
param([string]$RepositoryRoot, [string[]]$Arguments, [scriptblock]$GitRunner, [string]$Failure)
|
|
$result = & $GitRunner $RepositoryRoot $Arguments
|
|
if ($null -eq $result -or $result.ExitCode -ne 0) { throw $Failure }
|
|
return ([string]$result.Output).Trim()
|
|
}
|
|
|
|
function Invoke-XasFetch {
|
|
param([string]$RepositoryRoot, [scriptblock]$GitRunner)
|
|
$result = & $GitRunner $RepositoryRoot @('fetch', '--no-tags', 'origin', 'refs/heads/main:refs/remotes/origin/main')
|
|
if ($null -ne $result -and $result.ExitCode -eq 0) { return }
|
|
$detail = if ($null -eq $result) { '' } else { [string]$result.Output }
|
|
# Classify locally; never repeat raw diagnostics, URLs, or credentials.
|
|
if ($detail -match "couldn't find remote ref|could not find remote branch|remote ref.+(?:not found|does not exist)") {
|
|
throw '远端 main 尚未发布或已删除;请联系仓库维护者。'
|
|
}
|
|
if ($detail -match 'authentication failed|could not read (?:username|password)|terminal prompts disabled|access denied|permission denied|(?:HTTP|error|status|returned).{0,20}\b(?:401|403)\b') {
|
|
throw 'Git 认证或权限检查失败;请确认已登录公司 Git 且有仓库读取权限。'
|
|
}
|
|
if ($detail -match 'could not resolve|failed to connect|connection (?:timed out|reset|refused)|unable to access|SSL|TLS|certificate|network is unreachable|RPC failed') {
|
|
throw '连接公司 Git 失败;请检查网络、代理以及 TLS 证书配置。'
|
|
}
|
|
throw '获取 origin/main 失败;请检查 Git 连接状态和远端 main,然后重试。'
|
|
}
|
|
|
|
function Get-XasUpdateState {
|
|
param([string]$RepositoryRoot, [scriptblock]$GitRunner)
|
|
$root = [IO.Path]::GetFullPath($RepositoryRoot).TrimEnd([char[]]'\/')
|
|
$gitRoot = Invoke-XasCheckedGit $root @('rev-parse', '--show-toplevel') $GitRunner '当前目录不是可更新的 Git 仓库。'
|
|
if (-not [string]::Equals($root, [IO.Path]::GetFullPath($gitRoot).TrimEnd([char[]]'\/'), [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw '更新脚本必须位于仓库根目录。'
|
|
}
|
|
$url = Invoke-XasCheckedGit $root @('remote', 'get-url', 'origin') $GitRunner '找不到 origin;请重新克隆公司仓库。'
|
|
if ($url.TrimEnd('/') -cne $script:XasRepositoryUrl -and $url.TrimEnd('/') -cne $script:XasRepositoryUrl.Substring(0, $script:XasRepositoryUrl.Length - 4)) {
|
|
throw 'origin 不是指定的公司 xas-skills 仓库;请检查远端配置。'
|
|
}
|
|
$branch = Invoke-XasCheckedGit $root @('branch', '--show-current') $GitRunner '无法读取当前分支。'
|
|
if ($branch -cne 'main') { throw '更新只支持 main 分支;请先切换分支。' }
|
|
$head = Invoke-XasCheckedGit $root @('rev-parse', '--verify', 'HEAD') $GitRunner '仓库尚无提交;首次发布后才能更新。'
|
|
if ($head -notmatch '^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$') { throw '无法确认当前提交。' }
|
|
$upstream = Invoke-XasCheckedGit $root @('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}') $GitRunner 'main 尚未跟踪 origin/main;请先配置跟踪分支。'
|
|
if ($upstream -cne 'origin/main') { throw 'main 必须跟踪 origin/main 才能更新。' }
|
|
$status = Invoke-XasCheckedGit $root @('status', '--porcelain', '--untracked-files=all') $GitRunner '无法检查工作区状态。'
|
|
if (-not [string]::IsNullOrWhiteSpace($status)) { throw '工作区有修改或未跟踪文件;请先保留并处理这些内容。' }
|
|
$counts = Invoke-XasCheckedGit $root @('rev-list', '--left-right', '--count', 'HEAD...origin/main') $GitRunner 'origin/main 不存在或无法比较;首次发布后请重新克隆。'
|
|
if ($counts -notmatch '^(\d+)\s+(\d+)$') { throw '无法确认本地与远端的提交关系。' }
|
|
$ahead = [long]$Matches[1]
|
|
$behind = [long]$Matches[2]
|
|
if ($ahead -gt 0 -and $behind -gt 0) { throw '本地 main 与 origin/main 已分叉;请人工处理后再更新。' }
|
|
if ($ahead -gt 0) { throw '本地 main 有尚未推送的提交;请人工处理后再更新。' }
|
|
return [pscustomobject]@{ Head = $head; Behind = $behind }
|
|
}
|
|
|
|
function Invoke-XasSyncInstalled {
|
|
param([string]$RepositoryRoot, [string]$SkillsRoot, [switch]$Preview)
|
|
Import-Module (Join-Path $RepositoryRoot 'scripts/XasSkills.psm1') -Force
|
|
$installed = @(Get-XasInstalledSkillNames -SkillsRoot $SkillsRoot)
|
|
$catalog = @(Get-XasSkillCatalog -RepositoryRoot $RepositoryRoot)
|
|
$available = @($catalog | ForEach-Object { $_.Name })
|
|
$selected = @()
|
|
foreach ($name in $installed) {
|
|
if ($available -contains $name) { $selected += $name }
|
|
else { Write-Warning ("上游已移除技能 {0};保留已安装目录,请人工检查。" -f $name) }
|
|
}
|
|
if ($selected.Count -eq 0) {
|
|
Write-Host '没有需要同步的已安装技能。'
|
|
return
|
|
}
|
|
Invoke-XasInstall -RepositoryRoot $RepositoryRoot -SkillsRoot $SkillsRoot -Skill $selected -Preview:$Preview
|
|
}
|
|
|
|
function Start-XasUpdateContinuation {
|
|
param([string]$RepositoryRoot, [string]$SkillsRoot, [string]$ExpectedHead)
|
|
$token = [Guid]::NewGuid().ToString('N')
|
|
$executable = [Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
|
|
$entrypoint = Join-Path $RepositoryRoot 'update.ps1'
|
|
$start = New-Object Diagnostics.ProcessStartInfo
|
|
$start.FileName = $executable
|
|
$start.Arguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "' + $entrypoint + '" -ContinueUpdate ' + $token
|
|
$start.UseShellExecute = $false
|
|
$start.CreateNoWindow = $true
|
|
$start.RedirectStandardOutput = $true
|
|
$start.RedirectStandardError = $true
|
|
$start.StandardOutputEncoding = New-Object Text.UTF8Encoding($false)
|
|
$start.StandardErrorEncoding = New-Object Text.UTF8Encoding($false)
|
|
$start.EnvironmentVariables['XAS_SKILLS_UPDATE_TOKEN'] = $token
|
|
$start.EnvironmentVariables['XAS_SKILLS_UPDATE_ROOT'] = $RepositoryRoot
|
|
$start.EnvironmentVariables['XAS_SKILLS_UPDATE_TARGET'] = $SkillsRoot
|
|
$start.EnvironmentVariables['XAS_SKILLS_UPDATE_HEAD'] = $ExpectedHead
|
|
$process = New-Object Diagnostics.Process
|
|
$process.StartInfo = $start
|
|
try {
|
|
if (-not $process.Start()) { throw '无法启动更新后的 PowerShell 脚本。' }
|
|
$stdout = $process.StandardOutput.ReadToEndAsync()
|
|
$stderr = $process.StandardError.ReadToEndAsync()
|
|
$process.WaitForExit()
|
|
$outText = $stdout.GetAwaiter().GetResult()
|
|
$errText = $stderr.GetAwaiter().GetResult()
|
|
if (-not [string]::IsNullOrWhiteSpace($outText)) { Write-Host $outText.TrimEnd() }
|
|
if (-not [string]::IsNullOrWhiteSpace($errText)) { Write-Host $errText.TrimEnd() }
|
|
if ($process.ExitCode -ne 0) { throw '仓库拉取已完成,但技能同步失败;请处理以上错误后重新运行 update.ps1。' }
|
|
}
|
|
finally { $process.Dispose() }
|
|
}
|
|
|
|
function Invoke-XasUpdate {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RepositoryRoot,
|
|
[Parameter(Mandatory = $true)][string]$SkillsRoot,
|
|
[switch]$Preview,
|
|
[scriptblock]$GitRunner = { param($root, $arguments) Invoke-XasGit $root $arguments },
|
|
[scriptblock]$RestartRunner = { param($root, $target, $head) Start-XasUpdateContinuation $root $target $head },
|
|
[scriptblock]$SyncRunner = { param($root, $target, $preview) Invoke-XasSyncInstalled $root $target -Preview:$preview }
|
|
)
|
|
$state = Get-XasUpdateState $RepositoryRoot $GitRunner
|
|
if ($Preview) {
|
|
Write-Host '预览:将获取 origin/main,验证只允许快进,再以新版脚本同步已安装技能。'
|
|
Write-Host '本次不访问远端、不写入;以下技能预览基于当前本地仓库。'
|
|
& $SyncRunner $RepositoryRoot $SkillsRoot $true
|
|
return
|
|
}
|
|
# Discover existing installation conflicts before changing the source checkout.
|
|
$null = & $SyncRunner $RepositoryRoot $SkillsRoot $true
|
|
# Explicit refspec updates the exact tracking ref subsequently compared/merged.
|
|
Invoke-XasFetch $RepositoryRoot $GitRunner
|
|
$state = Get-XasUpdateState $RepositoryRoot $GitRunner
|
|
$null = Invoke-XasCheckedGit $RepositoryRoot @('merge', '--ff-only', '--no-edit', 'origin/main') $GitRunner '快进更新失败;请检查仓库状态后重试。'
|
|
$state = Get-XasUpdateState $RepositoryRoot $GitRunner
|
|
if ($state.Behind -ne 0) { throw '更新后 HEAD 与 origin/main 不一致;未同步技能。' }
|
|
& $RestartRunner $RepositoryRoot $SkillsRoot $state.Head
|
|
}
|
|
|
|
function Invoke-XasUpdateContinuation {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RepositoryRoot,
|
|
[Parameter(Mandatory = $true)][string]$Token,
|
|
[scriptblock]$GitRunner = { param($root, $arguments) Invoke-XasGit $root $arguments },
|
|
[scriptblock]$SyncRunner = { param($root, $target, $preview) Invoke-XasSyncInstalled $root $target -Preview:$preview }
|
|
)
|
|
$expectedToken = [Environment]::GetEnvironmentVariable('XAS_SKILLS_UPDATE_TOKEN', 'Process')
|
|
$expectedRoot = [Environment]::GetEnvironmentVariable('XAS_SKILLS_UPDATE_ROOT', 'Process')
|
|
$target = [Environment]::GetEnvironmentVariable('XAS_SKILLS_UPDATE_TARGET', 'Process')
|
|
$expectedHead = [Environment]::GetEnvironmentVariable('XAS_SKILLS_UPDATE_HEAD', 'Process')
|
|
# Consume the receipt before validation. It cannot cause repeated continuation.
|
|
foreach ($key in @('TOKEN', 'ROOT', 'TARGET', 'HEAD')) {
|
|
[Environment]::SetEnvironmentVariable(('XAS_SKILLS_UPDATE_' + $key), $null, 'Process')
|
|
}
|
|
if ($Token -notmatch '^[0-9a-f]{32}$' -or $Token -cne $expectedToken -or [string]::IsNullOrWhiteSpace($expectedRoot) -or [string]::IsNullOrWhiteSpace($target) -or $expectedHead -notmatch '^[0-9a-fA-F]{40}([0-9a-fA-F]{24})?$') {
|
|
throw '内部更新续接无效;请直接运行 update.ps1。'
|
|
}
|
|
if (-not [string]::Equals([IO.Path]::GetFullPath($RepositoryRoot).TrimEnd([char[]]'\/'), [IO.Path]::GetFullPath($expectedRoot).TrimEnd([char[]]'\/'), [StringComparison]::OrdinalIgnoreCase)) {
|
|
throw '更新续接的仓库目录不匹配。'
|
|
}
|
|
$state = Get-XasUpdateState $RepositoryRoot $GitRunner
|
|
if ($state.Head -cne $expectedHead -or $state.Behind -ne 0) { throw '更新续接时仓库状态已改变;请重新运行 update.ps1。' }
|
|
& $SyncRunner $RepositoryRoot $target $false
|
|
}
|
|
|
|
Export-ModuleMember -Function Invoke-XasUpdate, Invoke-XasUpdateContinuation
|