no message
This commit is contained in:
@@ -0,0 +1,708 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$SourceDir = "server",
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RemoteDir,
|
||||
[string]$HostName = "sbnews",
|
||||
[string]$SshBin = "ssh.exe",
|
||||
[switch]$AutoIncremental,
|
||||
[string]$From = "",
|
||||
[string]$To = "",
|
||||
[string]$RemoteTmp = "/tmp",
|
||||
[string]$PostInstall = "",
|
||||
[switch]$Sudo,
|
||||
[switch]$KeepRemoteTemp,
|
||||
[switch]$IncludeVendor,
|
||||
[switch]$IncludeEnv,
|
||||
[switch]$IncludeRuntime,
|
||||
[switch]$IncludeUploads,
|
||||
[switch]$IncludePublicBuilds,
|
||||
[switch]$IncludeCrawlerData,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = $Utf8NoBom
|
||||
$OutputEncoding = $Utf8NoBom
|
||||
|
||||
$DeployWatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$script:LastLogMs = 0.0
|
||||
|
||||
function Format-Duration {
|
||||
param([Int64]$Milliseconds)
|
||||
$span = [TimeSpan]::FromMilliseconds([Math]::Max(0, $Milliseconds))
|
||||
if ($span.TotalMinutes -ge 1) {
|
||||
return "{0}m{1:00}.{2:000}s" -f [Math]::Floor($span.TotalMinutes), $span.Seconds, $span.Milliseconds
|
||||
}
|
||||
return "{0}.{1:000}s" -f [Math]::Floor($span.TotalSeconds), $span.Milliseconds
|
||||
}
|
||||
|
||||
function Write-DeployLog {
|
||||
param([string]$Message)
|
||||
$total = [Int64]$DeployWatch.ElapsedMilliseconds
|
||||
$step = $total - $script:LastLogMs
|
||||
$script:LastLogMs = $total
|
||||
Write-Host ("[deploy][+{0}][total {1}] {2}" -f (Format-Duration $step), (Format-Duration $total), $Message)
|
||||
}
|
||||
|
||||
function Fail {
|
||||
param([string]$Message)
|
||||
throw "[deploy][error] $Message"
|
||||
}
|
||||
|
||||
function Require-Command {
|
||||
param([string]$Name)
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
Fail "missing command: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Normalize-DeployDir {
|
||||
param([string]$Value)
|
||||
return $Value.TrimEnd("/", "\")
|
||||
}
|
||||
|
||||
function Normalize-ComparePath {
|
||||
param([string]$Value)
|
||||
return ([System.IO.Path]::GetFullPath($Value).TrimEnd("\", "/") -replace "\\", "/").ToLowerInvariant()
|
||||
}
|
||||
|
||||
function ConvertTo-RemoteSingleQuoted {
|
||||
param([string]$Value)
|
||||
return "'" + $Value.Replace("'", "'\''") + "'"
|
||||
}
|
||||
|
||||
function Invoke-Native {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "command failed ($LASTEXITCODE): $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-NativeOutput {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
$output = & $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "command failed ($LASTEXITCODE): $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
return $output
|
||||
}
|
||||
|
||||
function ConvertTo-WindowsArgument {
|
||||
param([string]$Value)
|
||||
if ($null -eq $Value) {
|
||||
return '""'
|
||||
}
|
||||
if ($Value -notmatch '[\s"]') {
|
||||
return $Value
|
||||
}
|
||||
|
||||
$result = '"'
|
||||
$slashes = 0
|
||||
foreach ($ch in $Value.ToCharArray()) {
|
||||
if ($ch -eq '\') {
|
||||
$slashes++
|
||||
continue
|
||||
}
|
||||
if ($ch -eq '"') {
|
||||
$result += ('\' * (($slashes * 2) + 1))
|
||||
$result += '"'
|
||||
$slashes = 0
|
||||
continue
|
||||
}
|
||||
if ($slashes -gt 0) {
|
||||
$result += ('\' * $slashes)
|
||||
$slashes = 0
|
||||
}
|
||||
$result += $ch
|
||||
}
|
||||
if ($slashes -gt 0) {
|
||||
$result += ('\' * ($slashes * 2))
|
||||
}
|
||||
$result += '"'
|
||||
return $result
|
||||
}
|
||||
|
||||
function Set-ProcessArguments {
|
||||
param(
|
||||
[System.Diagnostics.ProcessStartInfo]$ProcessStartInfo,
|
||||
[string[]]$Arguments
|
||||
)
|
||||
$argumentListProperty = $ProcessStartInfo.GetType().GetProperty("ArgumentList")
|
||||
$argumentList = $null
|
||||
if ($null -ne $argumentListProperty) {
|
||||
$argumentList = $argumentListProperty.GetValue($ProcessStartInfo, $null)
|
||||
}
|
||||
if ($null -ne $argumentList) {
|
||||
foreach ($arg in $Arguments) {
|
||||
[void]$argumentList.Add($arg)
|
||||
}
|
||||
return
|
||||
}
|
||||
$ProcessStartInfo.Arguments = (($Arguments | ForEach-Object { ConvertTo-WindowsArgument $_ }) -join " ")
|
||||
}
|
||||
|
||||
function Start-ProcessWithInputFile {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments,
|
||||
[string]$InputFile
|
||||
)
|
||||
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$psi.FileName = $FilePath
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardInput = $true
|
||||
Set-ProcessArguments -ProcessStartInfo $psi -Arguments $Arguments
|
||||
|
||||
$process = [System.Diagnostics.Process]::Start($psi)
|
||||
try {
|
||||
$fileStream = [System.IO.File]::OpenRead($InputFile)
|
||||
try {
|
||||
$fileStream.CopyTo($process.StandardInput.BaseStream)
|
||||
} finally {
|
||||
$fileStream.Dispose()
|
||||
}
|
||||
$process.StandardInput.Close()
|
||||
$process.WaitForExit()
|
||||
if ($process.ExitCode -ne 0) {
|
||||
Fail "command failed ($($process.ExitCode)): $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
} finally {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Start-ProcessWithInputText {
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string[]]$Arguments,
|
||||
[string]$InputText
|
||||
)
|
||||
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$psi.FileName = $FilePath
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardInput = $true
|
||||
Set-ProcessArguments -ProcessStartInfo $psi -Arguments $Arguments
|
||||
|
||||
$process = [System.Diagnostics.Process]::Start($psi)
|
||||
try {
|
||||
$process.StandardInput.Write($InputText)
|
||||
$process.StandardInput.Close()
|
||||
$process.WaitForExit()
|
||||
if ($process.ExitCode -ne 0) {
|
||||
Fail "command failed ($($process.ExitCode)): $FilePath $($Arguments -join ' ')"
|
||||
}
|
||||
} finally {
|
||||
$process.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-IfExists {
|
||||
param([string]$Path)
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
Remove-Item -LiteralPath $Path -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-FullPayload {
|
||||
Write-DeployLog "building full payload from current working tree: $SourceDir"
|
||||
Write-DeployLog "full include paths: $($FullIncludePaths -join ' ')"
|
||||
|
||||
$payloadTar = Join-Path $WorkDir "payload-copy.tar"
|
||||
$tarArgs = @(
|
||||
"-cf", $payloadTar,
|
||||
"-C", $SourceAbs,
|
||||
"--exclude=.git",
|
||||
"--exclude=.idea",
|
||||
"--exclude=.vscode",
|
||||
"--exclude=.tmp",
|
||||
"--exclude=.gitignore",
|
||||
"--exclude=*.log",
|
||||
"--exclude=release-*.zip",
|
||||
"--exclude=*.zip",
|
||||
"--exclude=__pycache__",
|
||||
"--exclude=*.pyc"
|
||||
)
|
||||
|
||||
if (-not $IncludeEnv) {
|
||||
$tarArgs += "--exclude=.env"
|
||||
}
|
||||
if (-not $IncludeVendor) {
|
||||
$tarArgs += "--exclude=vendor"
|
||||
}
|
||||
if (-not $IncludeRuntime) {
|
||||
$tarArgs += "--exclude=runtime"
|
||||
}
|
||||
if (-not $IncludeUploads) {
|
||||
$tarArgs += "--exclude=public/uploads"
|
||||
}
|
||||
if (-not $IncludePublicBuilds) {
|
||||
$tarArgs += @("--exclude=public/admin", "--exclude=public/mobile")
|
||||
}
|
||||
if (-not $IncludeCrawlerData) {
|
||||
$tarArgs += @(
|
||||
"--exclude=public/dongqiudi-crawler/data",
|
||||
"--exclude=public/dongqiudi-crawler/logs",
|
||||
"--exclude=public/dongqiudi-crawler/venv",
|
||||
"--exclude=public/dongqiudi-crawler/__pycache__"
|
||||
)
|
||||
}
|
||||
|
||||
$tarArgs += $FullIncludePaths
|
||||
Invoke-Native "tar" $tarArgs
|
||||
Invoke-Native "tar" @("-xf", $payloadTar, "-C", $PayloadDir)
|
||||
}
|
||||
|
||||
function Strip-SourcePrefix {
|
||||
param([string]$Path)
|
||||
if ([string]::IsNullOrEmpty($GitPathPrefix)) {
|
||||
return $Path
|
||||
}
|
||||
$prefix = "$GitPathPrefix/"
|
||||
if ($Path.StartsWith($prefix)) {
|
||||
return $Path.Substring($prefix.Length)
|
||||
}
|
||||
return $Path
|
||||
}
|
||||
|
||||
function Test-DeployIncludedPath {
|
||||
param([string]$RelativePath)
|
||||
foreach ($include in $FullIncludePaths) {
|
||||
if ($RelativePath -eq $include -or $RelativePath.StartsWith("$include/")) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-UnderSourceDir {
|
||||
param([string]$GitPath)
|
||||
if ([string]::IsNullOrEmpty($GitPathPrefix)) {
|
||||
return (Test-DeployIncludedPath $GitPath)
|
||||
}
|
||||
if (-not $GitPath.StartsWith("$GitPathPrefix/")) {
|
||||
return $false
|
||||
}
|
||||
return (Test-DeployIncludedPath (Strip-SourcePrefix $GitPath))
|
||||
}
|
||||
|
||||
function Get-ShortCommit {
|
||||
param([string]$Commit)
|
||||
try {
|
||||
return (Invoke-NativeOutput "git" @("-C", $GitCwd, "rev-parse", "--short", $Commit) | Select-Object -First 1)
|
||||
} catch {
|
||||
return $Commit
|
||||
}
|
||||
}
|
||||
|
||||
function Write-FileListLog {
|
||||
param(
|
||||
[string]$Title,
|
||||
[string]$Prefix,
|
||||
[string[]]$Items
|
||||
)
|
||||
Write-DeployLog $Title
|
||||
$list = @($Items | Where-Object { $_ } | Sort-Object -Unique)
|
||||
if ($list.Count -eq 0) {
|
||||
Write-Host " (none)"
|
||||
return
|
||||
}
|
||||
foreach ($item in $list) {
|
||||
Write-Host " $Prefix $item"
|
||||
}
|
||||
}
|
||||
|
||||
function Write-UpdateLog {
|
||||
Write-DeployLog "update log:"
|
||||
if ($Mode -eq "incremental") {
|
||||
$fromShort = Get-ShortCommit $FromCommit
|
||||
$toShort = Get-ShortCommit $ToCommit
|
||||
Write-DeployLog "commit range: $fromShort -> $toShort"
|
||||
|
||||
$logArgs = @("-C", $GitCwd, "-c", "i18n.logOutputEncoding=utf-8", "log", "--encoding=UTF-8", "--no-merges", "--date=format:%Y-%m-%d %H:%M:%S", "--pretty=format: * %h %ad %an %s", "$FromCommit..$ToCommit")
|
||||
if ($SourceIsGitLink) {
|
||||
Write-DeployLog "$SourceDir is a root gitlink, showing root repository commits for the selected range"
|
||||
} elseif (-not [string]::IsNullOrEmpty($GitPathPrefix)) {
|
||||
$logArgs += @("--", $GitPathPrefix)
|
||||
}
|
||||
|
||||
$commitLines = @(& git @logArgs)
|
||||
Write-DeployLog "commits:"
|
||||
if ($commitLines.Count -gt 0) {
|
||||
$commitLines | ForEach-Object { Write-Host $_ }
|
||||
} else {
|
||||
Write-Host " (none)"
|
||||
}
|
||||
|
||||
Write-FileListLog "changed files:" "+" $ChangedDisplayPaths
|
||||
Write-FileListLog "deleted files:" "-" $DeletedDisplayPaths
|
||||
} else {
|
||||
Write-DeployLog "mode: full deploy from current working tree"
|
||||
Write-DeployLog "full include paths: $($FullIncludePaths -join ' ')"
|
||||
$commitLines = @(& git -C $GitCwd -c i18n.logOutputEncoding=utf-8 log -1 --encoding=UTF-8 "--date=format:%Y-%m-%d %H:%M:%S" "--pretty=format: * %h %ad %an %s")
|
||||
Write-DeployLog "latest commit:"
|
||||
if ($commitLines.Count -gt 0) {
|
||||
$commitLines | ForEach-Object { Write-Host $_ }
|
||||
} else {
|
||||
Write-Host " (none)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Build-IncrementalPayload {
|
||||
if ($SourceIsGitLink) {
|
||||
Write-DeployLog "$SourceDir is tracked as a root gitlink; exact file-level diff is unavailable without $SourceDir/.git"
|
||||
Write-DeployLog "building fallback payload from current working tree include paths: $($FullIncludePaths -join ' ')"
|
||||
$script:ChangedDisplayPaths = @($FullIncludePaths)
|
||||
Copy-FullPayload
|
||||
return
|
||||
}
|
||||
|
||||
$diffArgs = @("-C", $GitCwd, "diff", "--name-status", "--find-renames", $FromCommit, $ToCommit)
|
||||
if (-not [string]::IsNullOrEmpty($GitPathPrefix)) {
|
||||
$diffArgs += @("--", $GitPathPrefix)
|
||||
}
|
||||
|
||||
$rawDiff = @(& git @diffArgs)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "git diff failed"
|
||||
}
|
||||
|
||||
$changedGitPaths = New-Object System.Collections.Generic.List[string]
|
||||
$script:ChangedDisplayPaths = @()
|
||||
$script:DeletedDisplayPaths = @()
|
||||
|
||||
foreach ($line in $rawDiff) {
|
||||
if ([string]::IsNullOrWhiteSpace($line)) {
|
||||
continue
|
||||
}
|
||||
$parts = $line -split "`t"
|
||||
$status = $parts[0]
|
||||
if ($status -eq "D") {
|
||||
$path1 = $parts[1]
|
||||
if (Test-UnderSourceDir $path1) {
|
||||
$script:DeletedDisplayPaths += (Strip-SourcePrefix $path1)
|
||||
}
|
||||
} elseif ($status -like "R*" -or $status -like "C*") {
|
||||
if ($parts.Count -lt 3) {
|
||||
continue
|
||||
}
|
||||
$path1 = $parts[1]
|
||||
$path2 = $parts[2]
|
||||
if (Test-UnderSourceDir $path2) {
|
||||
[void]$changedGitPaths.Add($path2)
|
||||
$script:ChangedDisplayPaths += (Strip-SourcePrefix $path2)
|
||||
if ($status -like "R*" -and (Test-UnderSourceDir $path1)) {
|
||||
$script:DeletedDisplayPaths += (Strip-SourcePrefix $path1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$path1 = $parts[1]
|
||||
if (Test-UnderSourceDir $path1) {
|
||||
[void]$changedGitPaths.Add($path1)
|
||||
$script:ChangedDisplayPaths += (Strip-SourcePrefix $path1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$script:ChangedDisplayPaths = @($script:ChangedDisplayPaths | Sort-Object -Unique)
|
||||
$script:DeletedDisplayPaths = @($script:DeletedDisplayPaths | Sort-Object -Unique)
|
||||
$changedGitPaths = @($changedGitPaths | Sort-Object -Unique)
|
||||
|
||||
if ($changedGitPaths.Count -eq 0 -and $script:DeletedDisplayPaths.Count -eq 0) {
|
||||
Write-DeployLog "no changed files detected between $FromCommit and $ToCommit under $SourceDir"
|
||||
$script:NoChanges = $true
|
||||
return
|
||||
}
|
||||
|
||||
if ($script:DeletedDisplayPaths.Count -gt 0) {
|
||||
Set-Content -LiteralPath $DeletedPathsFile -Value $script:DeletedDisplayPaths -Encoding UTF8
|
||||
}
|
||||
|
||||
if ($changedGitPaths.Count -gt 0) {
|
||||
$incrementalTar = Join-Path $WorkDir "incremental.tar"
|
||||
Write-DeployLog "building incremental payload from commit $ToCommit"
|
||||
Invoke-Native "git" (@("-C", $GitCwd, "archive", "--format=tar", "--output=$incrementalTar", $ToCommit, "--") + $changedGitPaths)
|
||||
Invoke-Native "tar" @("-xf", $incrementalTar, "-C", $ExtractDir)
|
||||
|
||||
if ($SourceIsGitRepo) {
|
||||
$extractSource = $ExtractDir
|
||||
} else {
|
||||
$extractSource = Join-Path $ExtractDir $SourceDir
|
||||
}
|
||||
if (Test-Path -LiteralPath $extractSource) {
|
||||
Get-ChildItem -LiteralPath $extractSource -Force | Copy-Item -Destination $PayloadDir -Recurse -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$SourceDir = Normalize-DeployDir $SourceDir
|
||||
$RemoteDir = Normalize-DeployDir $RemoteDir
|
||||
$RemoteTmp = Normalize-DeployDir $RemoteTmp
|
||||
|
||||
if ($AutoIncremental -and ($From -or $To)) {
|
||||
Fail "-AutoIncremental cannot be used with -From/-To"
|
||||
}
|
||||
|
||||
if ($AutoIncremental) {
|
||||
$Mode = "incremental"
|
||||
$FromCommit = "HEAD~1"
|
||||
$ToCommit = "HEAD"
|
||||
} elseif ($From -or $To) {
|
||||
if (-not $From -or -not $To) {
|
||||
Fail "incremental deploy requires both -From and -To"
|
||||
}
|
||||
$Mode = "incremental"
|
||||
$FromCommit = $From
|
||||
$ToCommit = $To
|
||||
} else {
|
||||
$Mode = "full"
|
||||
$FromCommit = ""
|
||||
$ToCommit = ""
|
||||
}
|
||||
|
||||
Require-Command "git"
|
||||
Require-Command "tar"
|
||||
Require-Command $SshBin
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..")).Path
|
||||
$SourceAbs = Join-Path $RepoRoot $SourceDir
|
||||
if (-not (Test-Path -LiteralPath $SourceAbs -PathType Container)) {
|
||||
Fail "source directory not found: $SourceDir"
|
||||
}
|
||||
|
||||
$FullIncludePaths = @("app", "config", "extend", "public/dongqiudi-crawler", "route")
|
||||
$GitCwd = $RepoRoot
|
||||
$GitPathPrefix = $SourceDir
|
||||
$SourceIsGitRepo = $false
|
||||
$SourceIsGitLink = $false
|
||||
|
||||
$sourceGitTop = (& git -C $SourceAbs rev-parse --show-toplevel 2>$null | Select-Object -First 1)
|
||||
if ($LASTEXITCODE -eq 0 -and $sourceGitTop -and (Normalize-ComparePath $sourceGitTop) -eq (Normalize-ComparePath $SourceAbs)) {
|
||||
$GitCwd = $SourceAbs
|
||||
$GitPathPrefix = ""
|
||||
$SourceIsGitRepo = $true
|
||||
} else {
|
||||
& git -C $RepoRoot rev-parse --show-toplevel *> $null
|
||||
if ($LASTEXITCODE -ne 0 -and $Mode -eq "incremental") {
|
||||
Fail "incremental deploy requires git metadata in $SourceDir or repo root"
|
||||
}
|
||||
}
|
||||
|
||||
$lsFiles = @(& git -C $RepoRoot ls-files -s -- $SourceDir)
|
||||
if (-not $SourceIsGitRepo -and ($lsFiles | Where-Object { $_ -match "^160000\s" })) {
|
||||
$SourceIsGitLink = $true
|
||||
}
|
||||
|
||||
if ($Mode -eq "incremental") {
|
||||
& git -C $GitCwd rev-parse --verify "$FromCommit^{commit}" *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "invalid -From commit: $FromCommit"
|
||||
}
|
||||
& git -C $GitCwd rev-parse --verify "$ToCommit^{commit}" *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "invalid -To commit: $ToCommit"
|
||||
}
|
||||
$FromCommit = (Invoke-NativeOutput "git" @("-C", $GitCwd, "rev-parse", $FromCommit) | Select-Object -First 1)
|
||||
$ToCommit = (Invoke-NativeOutput "git" @("-C", $GitCwd, "rev-parse", $ToCommit) | Select-Object -First 1)
|
||||
}
|
||||
|
||||
$TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) "sport-era-deploy"
|
||||
New-Item -ItemType Directory -Force -Path $TempRoot | Out-Null
|
||||
$WorkDir = Join-Path $TempRoot ("deploy-server-" + [System.Guid]::NewGuid().ToString("N"))
|
||||
$PackageRoot = Join-Path $WorkDir "package"
|
||||
$PayloadDir = Join-Path $PackageRoot "payload"
|
||||
$ExtractDir = Join-Path $WorkDir "extract"
|
||||
$ArchivePath = Join-Path $WorkDir ("{0}-{1}.tar.gz" -f ($SourceDir -replace "[\\/]", "-"), $Mode)
|
||||
$DeletedPathsFile = Join-Path $PackageRoot ".deleted-paths.txt"
|
||||
$DeployInfoFile = Join-Path $PackageRoot ".deploy-info.txt"
|
||||
$script:ChangedDisplayPaths = @()
|
||||
$script:DeletedDisplayPaths = @()
|
||||
$script:NoChanges = $false
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Force -Path $PayloadDir, $ExtractDir | Out-Null
|
||||
New-Item -ItemType File -Force -Path $DeletedPathsFile | Out-Null
|
||||
|
||||
if ($Mode -eq "full") {
|
||||
Copy-FullPayload
|
||||
} else {
|
||||
Build-IncrementalPayload
|
||||
if ($script:NoChanges) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Write-UpdateLog
|
||||
|
||||
$deployInfo = @(
|
||||
"mode=$Mode",
|
||||
"source_dir=$SourceDir",
|
||||
"host=$HostName",
|
||||
"remote_dir=$RemoteDir",
|
||||
"built_at=$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
)
|
||||
if ($Mode -eq "incremental") {
|
||||
$deployInfo += @(
|
||||
"auto_incremental=$([int]$AutoIncremental.IsPresent)",
|
||||
"source_is_gitlink=$([int]$SourceIsGitLink)",
|
||||
"from_commit=$FromCommit",
|
||||
"to_commit=$ToCommit",
|
||||
"changed_count=$($script:ChangedDisplayPaths.Count)",
|
||||
"deleted_count=$($script:DeletedDisplayPaths.Count)"
|
||||
)
|
||||
}
|
||||
Set-Content -LiteralPath $DeployInfoFile -Value $deployInfo -Encoding UTF8
|
||||
|
||||
Write-DeployLog "packing archive: $ArchivePath"
|
||||
Invoke-Native "tar" @("-czf", $ArchivePath, "-C", $PackageRoot, ".")
|
||||
$packageSizeKb = [Math]::Ceiling((Get-Item -LiteralPath $ArchivePath).Length / 1KB)
|
||||
Write-DeployLog "package size: ${packageSizeKb}K"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-DeployLog "dry-run enabled, skip upload/install"
|
||||
return
|
||||
}
|
||||
|
||||
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||
$pidValue = $PID
|
||||
$remoteWorkDir = "$RemoteTmp/$($SourceDir -replace '[\\/]', '-')-deploy-$timestamp-$pidValue"
|
||||
$remoteArchive = "$RemoteTmp/$($SourceDir -replace '[\\/]', '-')-$Mode-$timestamp.tar.gz"
|
||||
|
||||
$remoteTmpQ = ConvertTo-RemoteSingleQuoted $RemoteTmp
|
||||
$remoteArchiveQ = ConvertTo-RemoteSingleQuoted $remoteArchive
|
||||
|
||||
Write-DeployLog "uploading archive to ${HostName}:$remoteArchive"
|
||||
$uploadCommand = "mkdir -p $remoteTmpQ && cat > $remoteArchiveQ"
|
||||
Start-ProcessWithInputFile -FilePath $SshBin -Arguments @($HostName, $uploadCommand) -InputFile $ArchivePath
|
||||
|
||||
$postInstallB64 = ""
|
||||
if ($PostInstall) {
|
||||
$postInstallB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($PostInstall))
|
||||
}
|
||||
|
||||
$useSudoValue = if ($Sudo) { "1" } else { "0" }
|
||||
$keepRemoteTempValue = if ($KeepRemoteTemp) { "1" } else { "0" }
|
||||
$remoteCommand = @(
|
||||
"REMOTE_DIR=$(ConvertTo-RemoteSingleQuoted $RemoteDir)",
|
||||
"REMOTE_ARCHIVE=$(ConvertTo-RemoteSingleQuoted $remoteArchive)",
|
||||
"REMOTE_WORK_DIR=$(ConvertTo-RemoteSingleQuoted $remoteWorkDir)",
|
||||
"KEEP_REMOTE_TEMP='$keepRemoteTempValue'",
|
||||
"POST_INSTALL_B64='$postInstallB64'",
|
||||
"USE_SUDO='$useSudoValue'",
|
||||
"bash -s"
|
||||
) -join " "
|
||||
|
||||
$remoteScript = @'
|
||||
set -euo pipefail
|
||||
|
||||
remote_now_ms() {
|
||||
date +%s%3N
|
||||
}
|
||||
|
||||
remote_format_duration() {
|
||||
local ms="$1"
|
||||
local seconds=$((ms / 1000))
|
||||
local millis=$((ms % 1000))
|
||||
local minutes=$((seconds / 60))
|
||||
seconds=$((seconds % 60))
|
||||
|
||||
if [[ "$minutes" -gt 0 ]]; then
|
||||
printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis"
|
||||
else
|
||||
printf '%d.%03ds' "$seconds" "$millis"
|
||||
fi
|
||||
}
|
||||
|
||||
REMOTE_START_MS="$(remote_now_ms)"
|
||||
REMOTE_LAST_LOG_MS="$REMOTE_START_MS"
|
||||
|
||||
remote_log() {
|
||||
local current_ms
|
||||
local total_ms
|
||||
local step_ms
|
||||
current_ms="$(remote_now_ms)"
|
||||
total_ms=$((current_ms - REMOTE_START_MS))
|
||||
step_ms=$((current_ms - REMOTE_LAST_LOG_MS))
|
||||
REMOTE_LAST_LOG_MS="$current_ms"
|
||||
printf '[deploy][remote][+%s][total %s] %s\n' "$(remote_format_duration "$step_ms")" "$(remote_format_duration "$total_ms")" "$*"
|
||||
}
|
||||
|
||||
run_priv() {
|
||||
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||
sudo "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
tar_extract_flags=(--no-same-owner --no-same-permissions --no-overwrite-dir)
|
||||
|
||||
remote_log "prepare remote work dir: $REMOTE_WORK_DIR"
|
||||
mkdir -p "$REMOTE_WORK_DIR"
|
||||
remote_log "extract uploaded archive"
|
||||
tar -xzf "$REMOTE_ARCHIVE" -C "$REMOTE_WORK_DIR"
|
||||
remote_log "ensure remote dir: $REMOTE_DIR"
|
||||
run_priv mkdir -p "$REMOTE_DIR"
|
||||
|
||||
if [[ -d "$REMOTE_WORK_DIR/payload" ]]; then
|
||||
remote_log "install payload into remote dir"
|
||||
tar -cf - -C "$REMOTE_WORK_DIR/payload" . | run_priv tar "${tar_extract_flags[@]}" -xf - -C "$REMOTE_DIR"
|
||||
fi
|
||||
|
||||
if [[ -f "$REMOTE_WORK_DIR/.deleted-paths.txt" ]]; then
|
||||
remote_log "apply deleted paths"
|
||||
while IFS= read -r rel; do
|
||||
[[ -n "$rel" ]] || continue
|
||||
case "$rel" in
|
||||
*".."*|/*)
|
||||
echo "[deploy][remote][warn] skip unsafe delete path: $rel" >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
run_priv rm -rf "$REMOTE_DIR/$rel"
|
||||
done < "$REMOTE_WORK_DIR/.deleted-paths.txt"
|
||||
fi
|
||||
|
||||
if [[ -n "${POST_INSTALL_B64:-}" ]]; then
|
||||
remote_log "run post-install command"
|
||||
POST_INSTALL_CMD="$(printf '%s' "$POST_INSTALL_B64" | base64 -d)"
|
||||
cd "$REMOTE_DIR"
|
||||
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||
sudo bash -lc "$POST_INSTALL_CMD"
|
||||
else
|
||||
bash -lc "$POST_INSTALL_CMD"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${KEEP_REMOTE_TEMP:-0}" != "1" ]]; then
|
||||
remote_log "clean remote temp files"
|
||||
rm -rf "$REMOTE_WORK_DIR" "$REMOTE_ARCHIVE"
|
||||
fi
|
||||
|
||||
remote_log "remote install finished"
|
||||
'@
|
||||
|
||||
Write-DeployLog "installing on server: $RemoteDir"
|
||||
Start-ProcessWithInputText -FilePath $SshBin -Arguments @($HostName, $remoteCommand) -InputText $remoteScript
|
||||
|
||||
Write-DeployLog "deploy finished"
|
||||
Write-DeployLog "mode: $Mode"
|
||||
Write-DeployLog "source: $SourceDir"
|
||||
Write-DeployLog "remote: ${HostName}:$RemoteDir"
|
||||
} finally {
|
||||
if ($WorkDir -and (Test-Path -LiteralPath $WorkDir)) {
|
||||
Remove-Item -LiteralPath $WorkDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/deploy-server.sh --remote-dir <remote_dir> [options]
|
||||
|
||||
Modes:
|
||||
1. Full deploy (no commit ids)
|
||||
./scripts/deploy-server.sh --remote-dir /www/wwwroot/test-server.sbnews.net
|
||||
|
||||
2. Incremental deploy (compare two commits)
|
||||
./scripts/deploy-server.sh \
|
||||
--from <old_commit> \
|
||||
--to <new_commit> \
|
||||
--remote-dir /www/wwwroot/test-server.sbnews.net
|
||||
|
||||
3. Auto incremental deploy (compare latest two commits)
|
||||
./scripts/deploy-server.sh \
|
||||
--auto-incremental \
|
||||
--remote-dir /www/wwwroot/test-server.sbnews.net
|
||||
|
||||
Options:
|
||||
--source-dir <dir> Local source directory, default: server
|
||||
--remote-dir <dir> Remote install directory, required
|
||||
--host <ssh_alias> SSH host/alias, default: sbnews
|
||||
--ssh-bin <cmd> SSH command, default: ssh. Use ssh.exe in WSL to reuse Windows SSH config
|
||||
--auto-incremental Incremental deploy using HEAD~1 as --from and HEAD as --to
|
||||
--from <commit> Base commit for incremental deploy
|
||||
--to <commit> Target commit for incremental deploy
|
||||
--remote-tmp <dir> Remote temp directory, default: /tmp
|
||||
--post-install <cmd> Run command on server after extract/install
|
||||
--sudo Use sudo on server when extracting/removing files
|
||||
--keep-remote-temp Keep remote temp files after install
|
||||
--include-vendor Include vendor directory in full package
|
||||
--include-env Include .env in full package
|
||||
--include-runtime Include runtime directory in full package
|
||||
--include-uploads Include public/uploads directory in full package
|
||||
--include-public-builds Include public/admin and public/mobile in full package
|
||||
--include-crawler-data Include crawler data/logs/venv cache directories in full package
|
||||
--dry-run Build package and print changed/deleted files, do not upload
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- Full deploy packages only these server paths by default:
|
||||
app, config, extend, public/dongqiudi-crawler, route.
|
||||
- Auto incremental deploy compares HEAD~1 and HEAD.
|
||||
- Incremental deploy compares two commit ids and packages exact file contents from --to.
|
||||
- If server is a root gitlink without server/.git metadata, incremental deploy falls back
|
||||
to packaging the current working tree for the default server include paths.
|
||||
- Deleted files between --from and --to are removed on the server.
|
||||
- Upload uses ssh stdin instead of scp: ssh <host> "cat > archive" < local.tar.gz
|
||||
EOF
|
||||
}
|
||||
|
||||
now_ms() {
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - <<'PY'
|
||||
import time
|
||||
print(int(time.time() * 1000))
|
||||
PY
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
python - <<'PY'
|
||||
import time
|
||||
print(int(time.time() * 1000))
|
||||
PY
|
||||
else
|
||||
printf '%s000\n' "$(date +%s)"
|
||||
fi
|
||||
}
|
||||
|
||||
format_duration() {
|
||||
local ms="$1"
|
||||
local seconds=$((ms / 1000))
|
||||
local millis=$((ms % 1000))
|
||||
local minutes=$((seconds / 60))
|
||||
seconds=$((seconds % 60))
|
||||
|
||||
if [[ "$minutes" -gt 0 ]]; then
|
||||
printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis"
|
||||
else
|
||||
printf '%d.%03ds' "$seconds" "$millis"
|
||||
fi
|
||||
}
|
||||
|
||||
DEPLOY_START_MS="$(now_ms)"
|
||||
DEPLOY_LAST_LOG_MS="$DEPLOY_START_MS"
|
||||
|
||||
log() {
|
||||
local current_ms
|
||||
local total_ms
|
||||
local step_ms
|
||||
current_ms="$(now_ms)"
|
||||
total_ms=$((current_ms - DEPLOY_START_MS))
|
||||
step_ms=$((current_ms - DEPLOY_LAST_LOG_MS))
|
||||
DEPLOY_LAST_LOG_MS="$current_ms"
|
||||
printf '[deploy][+%s][total %s] %s\n' "$(format_duration "$step_ms")" "$(format_duration "$total_ms")" "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf '[deploy][error] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"
|
||||
}
|
||||
|
||||
normalize_dir() {
|
||||
local value="$1"
|
||||
value="${value%/}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
safe_remote_single_quote() {
|
||||
printf "%s" "$1" | sed "s/'/'\\\\''/g"
|
||||
}
|
||||
|
||||
SOURCE_DIR="server"
|
||||
REMOTE_DIR=""
|
||||
HOST="sbnews"
|
||||
SSH_BIN="ssh"
|
||||
AUTO_INCREMENTAL=0
|
||||
FROM_COMMIT=""
|
||||
TO_COMMIT=""
|
||||
REMOTE_TMP="/tmp"
|
||||
POST_INSTALL=""
|
||||
USE_SUDO=0
|
||||
KEEP_REMOTE_TEMP=0
|
||||
INCLUDE_VENDOR=0
|
||||
INCLUDE_ENV=0
|
||||
INCLUDE_RUNTIME=0
|
||||
INCLUDE_UPLOADS=0
|
||||
INCLUDE_PUBLIC_BUILDS=0
|
||||
INCLUDE_CRAWLER_DATA=0
|
||||
DRY_RUN=0
|
||||
FULL_INCLUDE_PATHS=(
|
||||
"app"
|
||||
"config"
|
||||
"extend"
|
||||
"public/dongqiudi-crawler"
|
||||
"route"
|
||||
)
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source-dir)
|
||||
[[ $# -ge 2 ]] || fail "--source-dir requires a value"
|
||||
SOURCE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote-dir)
|
||||
[[ $# -ge 2 ]] || fail "--remote-dir requires a value"
|
||||
REMOTE_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--host)
|
||||
[[ $# -ge 2 ]] || fail "--host requires a value"
|
||||
HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-bin)
|
||||
[[ $# -ge 2 ]] || fail "--ssh-bin requires a value"
|
||||
SSH_BIN="$2"
|
||||
shift 2
|
||||
;;
|
||||
--auto-incremental)
|
||||
AUTO_INCREMENTAL=1
|
||||
shift
|
||||
;;
|
||||
--from)
|
||||
[[ $# -ge 2 ]] || fail "--from requires a value"
|
||||
FROM_COMMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--to)
|
||||
[[ $# -ge 2 ]] || fail "--to requires a value"
|
||||
TO_COMMIT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote-tmp)
|
||||
[[ $# -ge 2 ]] || fail "--remote-tmp requires a value"
|
||||
REMOTE_TMP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--post-install)
|
||||
[[ $# -ge 2 ]] || fail "--post-install requires a value"
|
||||
POST_INSTALL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--sudo)
|
||||
USE_SUDO=1
|
||||
shift
|
||||
;;
|
||||
--keep-remote-temp)
|
||||
KEEP_REMOTE_TEMP=1
|
||||
shift
|
||||
;;
|
||||
--include-vendor)
|
||||
INCLUDE_VENDOR=1
|
||||
shift
|
||||
;;
|
||||
--include-env)
|
||||
INCLUDE_ENV=1
|
||||
shift
|
||||
;;
|
||||
--include-runtime)
|
||||
INCLUDE_RUNTIME=1
|
||||
shift
|
||||
;;
|
||||
--include-uploads)
|
||||
INCLUDE_UPLOADS=1
|
||||
shift
|
||||
;;
|
||||
--include-public-builds)
|
||||
INCLUDE_PUBLIC_BUILDS=1
|
||||
shift
|
||||
;;
|
||||
--include-crawler-data)
|
||||
INCLUDE_CRAWLER_DATA=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
fail "unknown argument: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$REMOTE_DIR" ]] || {
|
||||
usage
|
||||
fail "--remote-dir is required"
|
||||
}
|
||||
|
||||
SOURCE_DIR="$(normalize_dir "$SOURCE_DIR")"
|
||||
REMOTE_DIR="$(normalize_dir "$REMOTE_DIR")"
|
||||
REMOTE_TMP="$(normalize_dir "$REMOTE_TMP")"
|
||||
|
||||
if [[ "$AUTO_INCREMENTAL" -eq 1 ]]; then
|
||||
[[ -z "$FROM_COMMIT" && -z "$TO_COMMIT" ]] || fail "--auto-incremental cannot be used with --from/--to"
|
||||
FROM_COMMIT="HEAD~1"
|
||||
TO_COMMIT="HEAD"
|
||||
MODE="incremental"
|
||||
elif [[ -n "$FROM_COMMIT" || -n "$TO_COMMIT" ]]; then
|
||||
[[ -n "$FROM_COMMIT" && -n "$TO_COMMIT" ]] || fail "incremental deploy requires both --from and --to"
|
||||
MODE="incremental"
|
||||
else
|
||||
MODE="full"
|
||||
fi
|
||||
|
||||
require_cmd git
|
||||
require_cmd tar
|
||||
require_cmd "$SSH_BIN"
|
||||
if [[ -n "$POST_INSTALL" ]]; then
|
||||
require_cmd base64
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
SOURCE_ABS="$REPO_ROOT/$SOURCE_DIR"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
[[ -d "$SOURCE_ABS" ]] || fail "source directory not found: $SOURCE_DIR"
|
||||
|
||||
GIT_CWD="$REPO_ROOT"
|
||||
GIT_PATH_PREFIX="$SOURCE_DIR"
|
||||
SOURCE_IS_GIT_REPO=0
|
||||
SOURCE_IS_GITLINK=0
|
||||
if SOURCE_GIT_TOP="$(git -C "$SOURCE_ABS" rev-parse --show-toplevel 2>/dev/null)" && [[ "$(cd "$SOURCE_GIT_TOP" && pwd -P)" == "$SOURCE_ABS" ]]; then
|
||||
GIT_CWD="$SOURCE_ABS"
|
||||
GIT_PATH_PREFIX=""
|
||||
SOURCE_IS_GIT_REPO=1
|
||||
elif ! git -C "$REPO_ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||
[[ "$MODE" == "full" ]] || fail "incremental deploy requires git metadata in $SOURCE_DIR or repo root"
|
||||
fi
|
||||
|
||||
if [[ "$SOURCE_IS_GIT_REPO" -ne 1 ]] && git -C "$REPO_ROOT" ls-files -s -- "$SOURCE_DIR" | awk '{print $1}' | grep -qx '160000'; then
|
||||
SOURCE_IS_GITLINK=1
|
||||
fi
|
||||
|
||||
if [[ "$MODE" == "incremental" ]]; then
|
||||
git -C "$GIT_CWD" rev-parse --verify "${FROM_COMMIT}^{commit}" >/dev/null 2>&1 || fail "invalid --from commit: $FROM_COMMIT"
|
||||
git -C "$GIT_CWD" rev-parse --verify "${TO_COMMIT}^{commit}" >/dev/null 2>&1 || fail "invalid --to commit: $TO_COMMIT"
|
||||
FROM_COMMIT="$(git -C "$GIT_CWD" rev-parse "$FROM_COMMIT")"
|
||||
TO_COMMIT="$(git -C "$GIT_CWD" rev-parse "$TO_COMMIT")"
|
||||
fi
|
||||
|
||||
mkdir -p "$REPO_ROOT/.tmp"
|
||||
WORK_DIR="$(mktemp -d "$REPO_ROOT/.tmp/deploy-server.XXXXXX")"
|
||||
PACKAGE_ROOT="$WORK_DIR/package"
|
||||
PAYLOAD_DIR="$PACKAGE_ROOT/payload"
|
||||
EXTRACT_DIR="$WORK_DIR/extract"
|
||||
ARCHIVE_PATH="$WORK_DIR/${SOURCE_DIR//\//-}-${MODE}.tar.gz"
|
||||
DELETED_PATHS_FILE="$PACKAGE_ROOT/.deleted-paths.txt"
|
||||
DEPLOY_INFO_FILE="$PACKAGE_ROOT/.deploy-info.txt"
|
||||
CHANGED_LOG="$WORK_DIR/changed-paths.txt"
|
||||
DELETED_LOG="$WORK_DIR/deleted-paths.txt"
|
||||
|
||||
cleanup_local() {
|
||||
if [[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" ]]; then
|
||||
chmod -R u+rwX "$WORK_DIR" 2>/dev/null || true
|
||||
rm -rf "$WORK_DIR" || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_local EXIT
|
||||
|
||||
mkdir -p "$PAYLOAD_DIR" "$EXTRACT_DIR"
|
||||
touch "$DELETED_PATHS_FILE" "$CHANGED_LOG" "$DELETED_LOG"
|
||||
|
||||
copy_full_payload() {
|
||||
local tar_args=()
|
||||
tar_args+=(--exclude=.git)
|
||||
tar_args+=(--exclude=.idea)
|
||||
tar_args+=(--exclude=.vscode)
|
||||
tar_args+=(--exclude=.tmp)
|
||||
tar_args+=(--exclude=.gitignore)
|
||||
tar_args+=(--exclude='*.log')
|
||||
tar_args+=(--exclude='release-*.zip')
|
||||
tar_args+=(--exclude='*.zip')
|
||||
tar_args+=(--exclude='__pycache__')
|
||||
tar_args+=(--exclude='*.pyc')
|
||||
|
||||
if [[ "$INCLUDE_ENV" -ne 1 ]]; then
|
||||
tar_args+=(--exclude=.env)
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_VENDOR" -ne 1 ]]; then
|
||||
tar_args+=(--exclude=vendor)
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_RUNTIME" -ne 1 ]]; then
|
||||
tar_args+=(--exclude=runtime)
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_UPLOADS" -ne 1 ]]; then
|
||||
tar_args+=(--exclude=public/uploads)
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_PUBLIC_BUILDS" -ne 1 ]]; then
|
||||
tar_args+=(--exclude=public/admin)
|
||||
tar_args+=(--exclude=public/mobile)
|
||||
fi
|
||||
|
||||
if [[ "$INCLUDE_CRAWLER_DATA" -ne 1 ]]; then
|
||||
tar_args+=(--exclude=public/dongqiudi-crawler/data)
|
||||
tar_args+=(--exclude=public/dongqiudi-crawler/logs)
|
||||
tar_args+=(--exclude=public/dongqiudi-crawler/venv)
|
||||
tar_args+=(--exclude=public/dongqiudi-crawler/__pycache__)
|
||||
fi
|
||||
|
||||
log "building full payload from current working tree: $SOURCE_DIR"
|
||||
log "full include paths: ${FULL_INCLUDE_PATHS[*]}"
|
||||
(
|
||||
cd "$SOURCE_ABS"
|
||||
tar "${tar_args[@]}" -cf - "${FULL_INCLUDE_PATHS[@]}"
|
||||
) | (
|
||||
cd "$PAYLOAD_DIR"
|
||||
tar -xf -
|
||||
)
|
||||
}
|
||||
|
||||
strip_source_prefix() {
|
||||
local path="$1"
|
||||
if [[ -z "$GIT_PATH_PREFIX" ]]; then
|
||||
printf '%s' "$path"
|
||||
else
|
||||
printf '%s' "${path#"$GIT_PATH_PREFIX"/}"
|
||||
fi
|
||||
}
|
||||
|
||||
is_under_source_dir() {
|
||||
local path="$1"
|
||||
if [[ -z "$GIT_PATH_PREFIX" ]]; then
|
||||
is_deploy_included_path "$path"
|
||||
return $?
|
||||
fi
|
||||
[[ "$path" == "$GIT_PATH_PREFIX/"* ]] || return 1
|
||||
is_deploy_included_path "$(strip_source_prefix "$path")"
|
||||
}
|
||||
|
||||
is_deploy_included_path() {
|
||||
local rel="$1"
|
||||
local include
|
||||
for include in "${FULL_INCLUDE_PATHS[@]}"; do
|
||||
if [[ "$rel" == "$include" || "$rel" == "$include/"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
short_commit() {
|
||||
git -C "$GIT_CWD" rev-parse --short "$1" 2>/dev/null || printf '%s' "$1"
|
||||
}
|
||||
|
||||
print_incremental_file_list() {
|
||||
local title="$1"
|
||||
local prefix="$2"
|
||||
local file="$3"
|
||||
|
||||
log "$title"
|
||||
if [[ -s "$file" ]]; then
|
||||
sort -u "$file" | sed '/^$/d' | sed "s/^/ $prefix /"
|
||||
else
|
||||
printf ' (none)\n'
|
||||
fi
|
||||
}
|
||||
|
||||
print_update_log() {
|
||||
local from_short=""
|
||||
local to_short=""
|
||||
local commit_lines=""
|
||||
|
||||
if [[ "$MODE" == "incremental" ]]; then
|
||||
from_short="$(short_commit "$FROM_COMMIT")"
|
||||
to_short="$(short_commit "$TO_COMMIT")"
|
||||
|
||||
log "update log:"
|
||||
log "commit range: $from_short -> $to_short"
|
||||
|
||||
if [[ "$SOURCE_IS_GITLINK" -eq 1 ]]; then
|
||||
log "$SOURCE_DIR is a root gitlink, showing root repository commits for the selected range"
|
||||
commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" 2>/dev/null || true)"
|
||||
elif [[ -n "$GIT_PATH_PREFIX" ]]; then
|
||||
commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" -- "$GIT_PATH_PREFIX" 2>/dev/null || true)"
|
||||
else
|
||||
commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
log "commits:"
|
||||
if [[ -n "$commit_lines" ]]; then
|
||||
printf '%s\n' "$commit_lines"
|
||||
else
|
||||
printf ' (none)\n'
|
||||
fi
|
||||
|
||||
print_incremental_file_list "changed files:" "+" "$CHANGED_LOG"
|
||||
print_incremental_file_list "deleted files:" "-" "$DELETED_LOG"
|
||||
else
|
||||
log "update log:"
|
||||
log "mode: full deploy from current working tree"
|
||||
log "full include paths: ${FULL_INCLUDE_PATHS[*]}"
|
||||
commit_lines="$(git -C "$GIT_CWD" log -1 --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' 2>/dev/null || true)"
|
||||
log "latest commit:"
|
||||
if [[ -n "$commit_lines" ]]; then
|
||||
printf '%s\n' "$commit_lines"
|
||||
else
|
||||
printf ' (none)\n'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
build_incremental_payload() {
|
||||
local raw_diff="$WORK_DIR/diff-name-status.txt"
|
||||
local archive_include="$WORK_DIR/incremental.tar"
|
||||
local diff_args=()
|
||||
|
||||
if [[ "$SOURCE_IS_GITLINK" -eq 1 ]]; then
|
||||
log "$SOURCE_DIR is tracked as a root gitlink; exact file-level diff is unavailable without $SOURCE_DIR/.git"
|
||||
log "building fallback payload from current working tree include paths: ${FULL_INCLUDE_PATHS[*]}"
|
||||
printf '%s\n' "${FULL_INCLUDE_PATHS[@]}" > "$CHANGED_LOG"
|
||||
copy_full_payload
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "$GIT_PATH_PREFIX" ]]; then
|
||||
diff_args+=(-- "$GIT_PATH_PREFIX")
|
||||
fi
|
||||
|
||||
git -C "$GIT_CWD" diff --name-status --find-renames "$FROM_COMMIT" "$TO_COMMIT" "${diff_args[@]}" > "$raw_diff"
|
||||
|
||||
while IFS=$'\t' read -r status path1 path2; do
|
||||
[[ -n "${status:-}" ]] || continue
|
||||
case "$status" in
|
||||
D)
|
||||
is_under_source_dir "$path1" || continue
|
||||
strip_source_prefix "$path1" >> "$DELETED_LOG"
|
||||
;;
|
||||
R*|C*)
|
||||
[[ -n "${path2:-}" ]] || continue
|
||||
is_under_source_dir "$path2" || continue
|
||||
printf '%s\n' "$path2" >> "$CHANGED_LOG"
|
||||
if [[ "$status" == R* ]] && is_under_source_dir "$path1"; then
|
||||
strip_source_prefix "$path1" >> "$DELETED_LOG"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
is_under_source_dir "$path1" || continue
|
||||
printf '%s\n' "$path1" >> "$CHANGED_LOG"
|
||||
;;
|
||||
esac
|
||||
done < "$raw_diff"
|
||||
|
||||
mapfile -t changed_paths < <(sort -u "$CHANGED_LOG" | sed '/^$/d')
|
||||
mapfile -t deleted_paths < <(sort -u "$DELETED_LOG" | sed '/^$/d')
|
||||
|
||||
if [[ "${#changed_paths[@]}" -eq 0 && "${#deleted_paths[@]}" -eq 0 ]]; then
|
||||
log "no changed files detected between $FROM_COMMIT and $TO_COMMIT under $SOURCE_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${#deleted_paths[@]}" -gt 0 ]]; then
|
||||
printf '%s\n' "${deleted_paths[@]}" > "$DELETED_PATHS_FILE"
|
||||
fi
|
||||
|
||||
if [[ "${#changed_paths[@]}" -gt 0 ]]; then
|
||||
log "building incremental payload from commit $TO_COMMIT"
|
||||
git -C "$GIT_CWD" archive --format=tar --output="$archive_include" "$TO_COMMIT" -- "${changed_paths[@]}"
|
||||
tar -xf "$archive_include" -C "$EXTRACT_DIR"
|
||||
|
||||
if [[ "$SOURCE_IS_GIT_REPO" -eq 1 ]]; then
|
||||
(
|
||||
cd "$EXTRACT_DIR"
|
||||
tar -cf - .
|
||||
) | (
|
||||
cd "$PAYLOAD_DIR"
|
||||
tar -xf -
|
||||
)
|
||||
elif [[ -d "$EXTRACT_DIR/$SOURCE_DIR" ]]; then
|
||||
(
|
||||
cd "$EXTRACT_DIR/$SOURCE_DIR"
|
||||
tar -cf - .
|
||||
) | (
|
||||
cd "$PAYLOAD_DIR"
|
||||
tar -xf -
|
||||
)
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "full" ]]; then
|
||||
copy_full_payload
|
||||
else
|
||||
build_incremental_payload
|
||||
fi
|
||||
|
||||
print_update_log
|
||||
|
||||
{
|
||||
printf 'mode=%s\n' "$MODE"
|
||||
printf 'source_dir=%s\n' "$SOURCE_DIR"
|
||||
printf 'host=%s\n' "$HOST"
|
||||
printf 'remote_dir=%s\n' "$REMOTE_DIR"
|
||||
printf 'built_at=%s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
if [[ "$MODE" == "incremental" ]]; then
|
||||
printf 'auto_incremental=%s\n' "$AUTO_INCREMENTAL"
|
||||
printf 'source_is_gitlink=%s\n' "$SOURCE_IS_GITLINK"
|
||||
printf 'from_commit=%s\n' "$FROM_COMMIT"
|
||||
printf 'to_commit=%s\n' "$TO_COMMIT"
|
||||
printf 'changed_count=%s\n' "$(sort -u "$CHANGED_LOG" | sed '/^$/d' | wc -l | tr -d ' ')"
|
||||
printf 'deleted_count=%s\n' "$(sort -u "$DELETED_LOG" | sed '/^$/d' | wc -l | tr -d ' ')"
|
||||
fi
|
||||
} > "$DEPLOY_INFO_FILE"
|
||||
|
||||
log "packing archive: $ARCHIVE_PATH"
|
||||
(
|
||||
cd "$PACKAGE_ROOT"
|
||||
tar -czf "$ARCHIVE_PATH" .
|
||||
)
|
||||
|
||||
log "package size: $(du -h "$ARCHIVE_PATH" | awk '{print $1}')"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
log "dry-run enabled, skip upload/install"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
|
||||
REMOTE_WORK_DIR="$REMOTE_TMP/${SOURCE_DIR//\//-}-deploy-$TIMESTAMP-$$"
|
||||
REMOTE_ARCHIVE="$REMOTE_TMP/${SOURCE_DIR//\//-}-${MODE}-$TIMESTAMP.tar.gz"
|
||||
REMOTE_TMP_Q="$(safe_remote_single_quote "$REMOTE_TMP")"
|
||||
REMOTE_ARCHIVE_Q="$(safe_remote_single_quote "$REMOTE_ARCHIVE")"
|
||||
|
||||
log "uploading archive to $HOST:$REMOTE_ARCHIVE"
|
||||
"$SSH_BIN" "$HOST" "mkdir -p '$REMOTE_TMP_Q' && cat > '$REMOTE_ARCHIVE_Q'" < "$ARCHIVE_PATH"
|
||||
|
||||
POST_INSTALL_B64=""
|
||||
if [[ -n "$POST_INSTALL" ]]; then
|
||||
POST_INSTALL_B64="$(printf '%s' "$POST_INSTALL" | base64 | tr -d '\r\n')"
|
||||
fi
|
||||
|
||||
log "installing on server: $REMOTE_DIR"
|
||||
"$SSH_BIN" "$HOST" \
|
||||
"REMOTE_DIR='$(safe_remote_single_quote "$REMOTE_DIR")' REMOTE_ARCHIVE='$(safe_remote_single_quote "$REMOTE_ARCHIVE")' REMOTE_WORK_DIR='$(safe_remote_single_quote "$REMOTE_WORK_DIR")' KEEP_REMOTE_TEMP='$KEEP_REMOTE_TEMP' POST_INSTALL_B64='$POST_INSTALL_B64' USE_SUDO='$USE_SUDO' bash -s" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
|
||||
remote_now_ms() {
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 - <<'PY'
|
||||
import time
|
||||
print(int(time.time() * 1000))
|
||||
PY
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
python - <<'PY'
|
||||
import time
|
||||
print(int(time.time() * 1000))
|
||||
PY
|
||||
else
|
||||
printf '%s000\n' "$(date +%s)"
|
||||
fi
|
||||
}
|
||||
|
||||
remote_format_duration() {
|
||||
local ms="$1"
|
||||
local seconds=$((ms / 1000))
|
||||
local millis=$((ms % 1000))
|
||||
local minutes=$((seconds / 60))
|
||||
seconds=$((seconds % 60))
|
||||
|
||||
if [[ "$minutes" -gt 0 ]]; then
|
||||
printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis"
|
||||
else
|
||||
printf '%d.%03ds' "$seconds" "$millis"
|
||||
fi
|
||||
}
|
||||
|
||||
REMOTE_START_MS="$(remote_now_ms)"
|
||||
REMOTE_LAST_LOG_MS="$REMOTE_START_MS"
|
||||
|
||||
remote_log() {
|
||||
local current_ms
|
||||
local total_ms
|
||||
local step_ms
|
||||
current_ms="$(remote_now_ms)"
|
||||
total_ms=$((current_ms - REMOTE_START_MS))
|
||||
step_ms=$((current_ms - REMOTE_LAST_LOG_MS))
|
||||
REMOTE_LAST_LOG_MS="$current_ms"
|
||||
printf '[deploy][remote][+%s][total %s] %s\n' "$(remote_format_duration "$step_ms")" "$(remote_format_duration "$total_ms")" "$*"
|
||||
}
|
||||
|
||||
run_priv() {
|
||||
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||
sudo "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
tar_extract_flags=(--no-same-owner --no-same-permissions --no-overwrite-dir)
|
||||
|
||||
remote_log "prepare remote work dir: $REMOTE_WORK_DIR"
|
||||
mkdir -p "$REMOTE_WORK_DIR"
|
||||
remote_log "extract uploaded archive"
|
||||
tar -xzf "$REMOTE_ARCHIVE" -C "$REMOTE_WORK_DIR"
|
||||
remote_log "ensure remote dir: $REMOTE_DIR"
|
||||
run_priv mkdir -p "$REMOTE_DIR"
|
||||
|
||||
if [[ -d "$REMOTE_WORK_DIR/payload" ]]; then
|
||||
remote_log "install payload into remote dir"
|
||||
tar -cf - -C "$REMOTE_WORK_DIR/payload" . | run_priv tar "${tar_extract_flags[@]}" -xf - -C "$REMOTE_DIR"
|
||||
fi
|
||||
|
||||
if [[ -f "$REMOTE_WORK_DIR/.deleted-paths.txt" ]]; then
|
||||
remote_log "apply deleted paths"
|
||||
while IFS= read -r rel; do
|
||||
[[ -n "$rel" ]] || continue
|
||||
case "$rel" in
|
||||
*".."*|/*)
|
||||
echo "[deploy][remote][warn] skip unsafe delete path: $rel" >&2
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
run_priv rm -rf "$REMOTE_DIR/$rel"
|
||||
done < "$REMOTE_WORK_DIR/.deleted-paths.txt"
|
||||
fi
|
||||
|
||||
if [[ -n "${POST_INSTALL_B64:-}" ]]; then
|
||||
remote_log "run post-install command"
|
||||
POST_INSTALL_CMD="$(printf '%s' "$POST_INSTALL_B64" | base64 -d)"
|
||||
cd "$REMOTE_DIR"
|
||||
if [[ "${USE_SUDO:-0}" == "1" ]]; then
|
||||
sudo bash -lc "$POST_INSTALL_CMD"
|
||||
else
|
||||
bash -lc "$POST_INSTALL_CMD"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${KEEP_REMOTE_TEMP:-0}" != "1" ]]; then
|
||||
remote_log "clean remote temp files"
|
||||
rm -rf "$REMOTE_WORK_DIR" "$REMOTE_ARCHIVE"
|
||||
fi
|
||||
|
||||
remote_log "remote install finished"
|
||||
REMOTE_SCRIPT
|
||||
|
||||
log "deploy finished"
|
||||
log "mode: $MODE"
|
||||
log "source: $SOURCE_DIR"
|
||||
log "remote: $HOST:$REMOTE_DIR"
|
||||
Reference in New Issue
Block a user