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