2026-09-06 17:04:11 +08:00
|
|
|
param(
|
|
|
|
|
[string]$Version = '0.76.0-rc.hardware1',
|
|
|
|
|
[string]$OutputDirectory = '.deploy-artifacts'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
$PSNativeCommandUseErrorActionPreference = $true
|
|
|
|
|
|
|
|
|
|
$projectRoot = (Resolve-Path -LiteralPath (Split-Path -Parent $PSScriptRoot)).Path
|
|
|
|
|
$outputRoot = [IO.Path]::GetFullPath((Join-Path $projectRoot $OutputDirectory))
|
|
|
|
|
if (-not $outputRoot.StartsWith($projectRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) {
|
|
|
|
|
throw 'OutputDirectory must resolve inside the project workspace'
|
|
|
|
|
}
|
|
|
|
|
if ($Version -notmatch '^[0-9A-Za-z][0-9A-Za-z._-]{0,79}$') {
|
|
|
|
|
throw 'Version contains unsupported characters'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
|
|
|
|
|
$bundleName = "data-terminal-cloud-$Version"
|
|
|
|
|
$stagingRoot = Join-Path $outputRoot ('.staging-' + [Guid]::NewGuid().ToString('N'))
|
|
|
|
|
$payloadRoot = Join-Path $stagingRoot $bundleName
|
|
|
|
|
New-Item -ItemType Directory -Force -Path $payloadRoot | Out-Null
|
|
|
|
|
|
|
|
|
|
try {
|
2026-09-12 08:57:29 +08:00
|
|
|
$candidatePaths = @(& git -c core.quotePath=false ls-files --cached --others --exclude-standard -- `
|
|
|
|
|
cmd internal migrations openapi web deploy scripts test docs compose.yaml go.mod go.sum README.md CHANGELOG.md .env.example .dockerignore)
|
2026-09-06 17:04:11 +08:00
|
|
|
if ($LASTEXITCODE -ne 0) { throw 'git file enumeration failed' }
|
|
|
|
|
$candidatePaths = $candidatePaths | Where-Object {
|
|
|
|
|
$_ -and
|
|
|
|
|
$_ -notmatch '(^|/)(node_modules|dist|coverage|runtime|release)(/|$)' -and
|
|
|
|
|
$_ -notmatch '(^|/)\.env$' -and
|
|
|
|
|
$_ -notmatch '\.(pem|key|p12|pfx)$'
|
|
|
|
|
} | Sort-Object -Unique
|
|
|
|
|
if ($candidatePaths.Count -lt 20) { throw 'deployment file set is unexpectedly small' }
|
|
|
|
|
|
|
|
|
|
foreach ($relativePath in $candidatePaths) {
|
|
|
|
|
$sourcePath = [IO.Path]::GetFullPath((Join-Path $projectRoot $relativePath))
|
|
|
|
|
if (-not $sourcePath.StartsWith($projectRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or -not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
|
|
|
|
|
throw "Unsafe or missing deployment source path: $relativePath"
|
|
|
|
|
}
|
|
|
|
|
$targetPath = Join-Path $payloadRoot $relativePath
|
|
|
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $targetPath) | Out-Null
|
|
|
|
|
Copy-Item -LiteralPath $sourcePath -Destination $targetPath
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sourceCommit = (& git rev-parse HEAD).Trim()
|
|
|
|
|
if ($LASTEXITCODE -ne 0 -or $sourceCommit -notmatch '^[0-9a-f]{40}$') { throw 'source commit could not be resolved' }
|
|
|
|
|
$statusLines = @(& git status --porcelain=v1)
|
|
|
|
|
if ($LASTEXITCODE -ne 0) { throw 'working tree status could not be resolved' }
|
|
|
|
|
$files = Get-ChildItem -LiteralPath $payloadRoot -File -Recurse | Sort-Object FullName | ForEach-Object {
|
|
|
|
|
[ordered]@{
|
|
|
|
|
path = $_.FullName.Substring($payloadRoot.Length + 1).Replace('\','/')
|
|
|
|
|
size_bytes = $_.Length
|
|
|
|
|
sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$manifest = [ordered]@{
|
|
|
|
|
schema_version = 'server-deployment-bundle-v1'
|
|
|
|
|
candidate_version = $Version
|
|
|
|
|
generated_at = [DateTime]::UtcNow.ToString('o')
|
|
|
|
|
source_commit = $sourceCommit
|
|
|
|
|
working_tree_dirty = $statusLines.Count -gt 0
|
2026-10-08 10:06:44 +08:00
|
|
|
schema_migration_target = [int](($candidatePaths | Where-Object { $_ -match '^migrations/[0-9]+_.+\.up\.sql$' } | ForEach-Object { [int](($_ -split '/')[1] -split '_')[0] } | Measure-Object -Maximum).Maximum)
|
2026-09-06 17:04:11 +08:00
|
|
|
hardware_runtime = 'volc_hardware_agent'
|
|
|
|
|
files = @($files)
|
|
|
|
|
}
|
|
|
|
|
$manifestPath = Join-Path $payloadRoot 'deploy-manifest.json'
|
|
|
|
|
[IO.File]::WriteAllText($manifestPath, (($manifest | ConvertTo-Json -Depth 8) + [Environment]::NewLine), [Text.UTF8Encoding]::new($false))
|
|
|
|
|
|
|
|
|
|
$checksumLines = Get-ChildItem -LiteralPath $payloadRoot -File -Recurse | Where-Object Name -ne 'SHA256SUMS' | Sort-Object FullName | ForEach-Object {
|
|
|
|
|
$relative = $_.FullName.Substring($payloadRoot.Length + 1).Replace('\','/')
|
|
|
|
|
"{0} {1}" -f (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant(), $relative
|
|
|
|
|
}
|
|
|
|
|
[IO.File]::WriteAllLines((Join-Path $payloadRoot 'SHA256SUMS'), [string[]]$checksumLines, [Text.UTF8Encoding]::new($false))
|
|
|
|
|
|
|
|
|
|
$archivePath = Join-Path $outputRoot ($bundleName + '.zip')
|
|
|
|
|
if (Test-Path -LiteralPath $archivePath) { Remove-Item -LiteralPath $archivePath -Force }
|
|
|
|
|
Compress-Archive -LiteralPath $payloadRoot -DestinationPath $archivePath -CompressionLevel Optimal
|
|
|
|
|
$archiveHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
|
|
|
"$archiveHash $([IO.Path]::GetFileName($archivePath))" | Set-Content -LiteralPath ($archivePath + '.sha256') -Encoding ascii
|
|
|
|
|
[ordered]@{ archive = $archivePath; sha256 = $archiveHash; files = $files.Count; source_commit = $sourceCommit; working_tree_dirty = $statusLines.Count -gt 0 } | ConvertTo-Json -Compress
|
|
|
|
|
} finally {
|
|
|
|
|
$resolvedStaging = [IO.Path]::GetFullPath($stagingRoot)
|
|
|
|
|
if ($resolvedStaging.StartsWith($outputRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $resolvedStaging)) {
|
|
|
|
|
Remove-Item -LiteralPath $resolvedStaging -Recurse -Force
|
|
|
|
|
}
|
|
|
|
|
}
|