mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
481 lines
16 KiB
PowerShell
481 lines
16 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Version,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$AppArtifactsRoot,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$InstallerArtifactsRoot,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$OutputDir,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$PrivateKeyPath,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Channel = "stable",
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$S3Endpoint = "",
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$S3Bucket = "",
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$S3Region = ""
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Get-PlatformConfigurations {
|
|
return @(
|
|
@{
|
|
Platform = "windows-x64"
|
|
ArtifactName = "app-payload-windows-x64"
|
|
},
|
|
@{
|
|
Platform = "windows-x86"
|
|
ArtifactName = "app-payload-windows-x86"
|
|
},
|
|
@{
|
|
Platform = "linux-x64"
|
|
ArtifactName = "app-payload-linux-x64"
|
|
}
|
|
)
|
|
}
|
|
|
|
function Resolve-AppDirectory {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$SearchRoot,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Version
|
|
)
|
|
|
|
$preferred = Get-ChildItem -LiteralPath $SearchRoot -Recurse -Directory -Filter "app-$Version" -ErrorAction SilentlyContinue |
|
|
Select-Object -First 1
|
|
if ($preferred) {
|
|
return $preferred.FullName
|
|
}
|
|
|
|
$fallback = Get-ChildItem -LiteralPath $SearchRoot -Recurse -Directory -Filter "app-*" -ErrorAction SilentlyContinue |
|
|
Sort-Object FullName |
|
|
Select-Object -First 1
|
|
return $fallback?.FullName
|
|
}
|
|
|
|
function Invoke-AwsCommandIfPossible {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string[]]$Arguments,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$IgnoreFailure
|
|
)
|
|
|
|
if ([string]::IsNullOrWhiteSpace($S3Endpoint) -or [string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
return
|
|
}
|
|
|
|
$previousRequestChecksumCalculation = $env:AWS_REQUEST_CHECKSUM_CALCULATION
|
|
$previousResponseChecksumValidation = $env:AWS_RESPONSE_CHECKSUM_VALIDATION
|
|
|
|
# Rainyun's S3-compatible endpoint rejects AWS CLI v2's default checksum headers
|
|
# during multipart uploads. Restrict checksum behavior to API-required cases only.
|
|
$env:AWS_REQUEST_CHECKSUM_CALCULATION = "WHEN_REQUIRED"
|
|
$env:AWS_RESPONSE_CHECKSUM_VALIDATION = "WHEN_REQUIRED"
|
|
|
|
try {
|
|
if ($IgnoreFailure) {
|
|
& aws @Arguments 2>$null
|
|
}
|
|
else {
|
|
& aws @Arguments
|
|
}
|
|
}
|
|
finally {
|
|
if ($null -eq $previousRequestChecksumCalculation) {
|
|
Remove-Item Env:AWS_REQUEST_CHECKSUM_CALCULATION -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
$env:AWS_REQUEST_CHECKSUM_CALCULATION = $previousRequestChecksumCalculation
|
|
}
|
|
|
|
if ($null -eq $previousResponseChecksumValidation) {
|
|
Remove-Item Env:AWS_RESPONSE_CHECKSUM_VALIDATION -ErrorAction SilentlyContinue
|
|
}
|
|
else {
|
|
$env:AWS_RESPONSE_CHECKSUM_VALIDATION = $previousResponseChecksumValidation
|
|
}
|
|
}
|
|
|
|
if ($LASTEXITCODE -ne 0 -and -not $IgnoreFailure) {
|
|
throw "aws command failed: aws $($Arguments -join ' ')"
|
|
}
|
|
}
|
|
|
|
function Get-S3Key {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Prefix,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RelativePath
|
|
)
|
|
|
|
$trimmedPrefix = $Prefix.Trim('/').Replace('\', '/')
|
|
$trimmedRelativePath = $RelativePath.TrimStart('\', '/').Replace('\', '/')
|
|
return "$trimmedPrefix/$trimmedRelativePath"
|
|
}
|
|
|
|
function Get-RelativePath {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Root,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Path
|
|
)
|
|
|
|
$rootPath = [System.IO.Path]::GetFullPath($Root)
|
|
if (-not $rootPath.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
|
|
$rootPath += [System.IO.Path]::DirectorySeparatorChar
|
|
}
|
|
|
|
$pathValue = [System.IO.Path]::GetFullPath($Path)
|
|
return [System.IO.Path]::GetRelativePath($rootPath, $pathValue)
|
|
}
|
|
|
|
function Get-RemoteS3Keys {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Prefix
|
|
)
|
|
|
|
$keys = [System.Collections.Generic.List[string]]::new()
|
|
$continuationToken = $null
|
|
|
|
do {
|
|
$arguments = @(
|
|
"--endpoint-url", $S3Endpoint,
|
|
"--region", $S3Region,
|
|
"s3api", "list-objects-v2",
|
|
"--bucket", $S3Bucket,
|
|
"--prefix", $Prefix,
|
|
"--output", "json"
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($continuationToken)) {
|
|
$arguments += @("--continuation-token", $continuationToken)
|
|
}
|
|
|
|
$json = Invoke-AwsCommandIfPossible -Arguments $arguments
|
|
|
|
if ([string]::IsNullOrWhiteSpace($json)) {
|
|
break
|
|
}
|
|
|
|
$response = $json | ConvertFrom-Json
|
|
if ($response.Contents) {
|
|
foreach ($item in $response.Contents) {
|
|
if (-not [string]::IsNullOrWhiteSpace($item.Key)) {
|
|
$keys.Add($item.Key)
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($response.IsTruncated -and -not [string]::IsNullOrWhiteSpace($response.NextContinuationToken)) {
|
|
$continuationToken = $response.NextContinuationToken
|
|
}
|
|
else {
|
|
$continuationToken = $null
|
|
}
|
|
} while (-not [string]::IsNullOrWhiteSpace($continuationToken))
|
|
|
|
return $keys
|
|
}
|
|
|
|
function Upload-DirectoryToS3 {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LocalRoot,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RemotePrefix,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[switch]$DeleteExtraRemoteObjects
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $LocalRoot)) {
|
|
throw "Local upload root not found: $LocalRoot"
|
|
}
|
|
|
|
$files = Get-ChildItem -LiteralPath $LocalRoot -Recurse -File | Sort-Object FullName
|
|
$uploadedKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
|
|
if ($files.Count -eq 0) {
|
|
Write-Host "No files found under $LocalRoot; skipping upload."
|
|
}
|
|
|
|
$index = 0
|
|
foreach ($file in $files) {
|
|
$index++
|
|
$relativePath = Get-RelativePath -Root $LocalRoot -Path $file.FullName
|
|
$key = Get-S3Key -Prefix $RemotePrefix -RelativePath $relativePath
|
|
$null = $uploadedKeys.Add($key)
|
|
|
|
if ($index -eq 1 -or $index % 25 -eq 0 -or $index -eq $files.Count) {
|
|
Write-Host "Uploading $index/$($files.Count): $key"
|
|
}
|
|
|
|
Invoke-AwsCommandIfPossible -Arguments @(
|
|
"--endpoint-url", $S3Endpoint,
|
|
"--region", $S3Region,
|
|
"s3api", "put-object",
|
|
"--bucket", $S3Bucket,
|
|
"--key", $key,
|
|
"--body", $file.FullName
|
|
)
|
|
}
|
|
|
|
if ($DeleteExtraRemoteObjects) {
|
|
$remoteKeys = Get-RemoteS3Keys -Prefix $RemotePrefix.Trim('/').Replace('\', '/')
|
|
foreach ($remoteKey in $remoteKeys) {
|
|
if (-not $uploadedKeys.Contains($remoteKey)) {
|
|
Write-Host "Deleting stale remote object: $remoteKey"
|
|
Invoke-AwsCommandIfPossible -Arguments @(
|
|
"--endpoint-url", $S3Endpoint,
|
|
"--region", $S3Region,
|
|
"s3api", "delete-object",
|
|
"--bucket", $S3Bucket,
|
|
"--key", $remoteKey
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Upload-FileToS3 {
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$LocalPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$RemoteKey
|
|
)
|
|
|
|
if (-not (Test-Path -LiteralPath $LocalPath)) {
|
|
throw "Local upload file not found: $LocalPath"
|
|
}
|
|
|
|
Invoke-AwsCommandIfPossible -Arguments @(
|
|
"--endpoint-url", $S3Endpoint,
|
|
"--region", $S3Region,
|
|
"s3api", "put-object",
|
|
"--bucket", $S3Bucket,
|
|
"--key", $RemoteKey.Trim('/').Replace('\', '/'),
|
|
"--body", $LocalPath
|
|
)
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $PrivateKeyPath)) {
|
|
throw "Private key file not found: $PrivateKeyPath"
|
|
}
|
|
|
|
$toolProject = Join-Path $PSScriptRoot "..\PenguinLogisticsOnlineNetworkDistributionSystem\src\Plonds.Tool\Plonds.Tool.csproj"
|
|
if (-not (Test-Path -LiteralPath $toolProject)) {
|
|
throw "PLONDS tool project not found: $toolProject"
|
|
}
|
|
|
|
$supportedPlatforms = Get-PlatformConfigurations
|
|
$publishedRoot = Join-Path $OutputDir "published"
|
|
$releaseAssetsRoot = Join-Path $OutputDir "release-assets"
|
|
$baselineRoot = Join-Path $OutputDir "_baselines"
|
|
|
|
New-Item -ItemType Directory -Path $publishedRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $releaseAssetsRoot -Force | Out-Null
|
|
New-Item -ItemType Directory -Path $baselineRoot -Force | Out-Null
|
|
|
|
foreach ($config in $supportedPlatforms) {
|
|
$platform = $config.Platform
|
|
$platformBaselineRoot = Join-Path $baselineRoot $platform
|
|
$baselineCurrentDir = Join-Path $platformBaselineRoot "current"
|
|
$baselineVersionPath = Join-Path $platformBaselineRoot "version.txt"
|
|
|
|
New-Item -ItemType Directory -Path $baselineCurrentDir -Force | Out-Null
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($S3Endpoint) -and -not [string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
Invoke-AwsCommandIfPossible -Arguments @(
|
|
"--endpoint-url", $S3Endpoint,
|
|
"--region", $S3Region,
|
|
"s3", "sync",
|
|
"s3://$S3Bucket/lanmountain/update/baselines/$platform/current/",
|
|
$baselineCurrentDir,
|
|
"--only-show-errors"
|
|
) -IgnoreFailure
|
|
|
|
Invoke-AwsCommandIfPossible -Arguments @(
|
|
"--endpoint-url", $S3Endpoint,
|
|
"--region", $S3Region,
|
|
"s3", "cp",
|
|
"s3://$S3Bucket/lanmountain/update/baselines/$platform/version.txt",
|
|
$baselineVersionPath,
|
|
"--only-show-errors"
|
|
) -IgnoreFailure
|
|
}
|
|
}
|
|
|
|
$repoBaseUrl = if ([string]::IsNullOrWhiteSpace($S3Endpoint) -or [string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
$null
|
|
}
|
|
else {
|
|
"$($S3Endpoint.TrimEnd('/'))/$S3Bucket/lanmountain/update/repo/sha256"
|
|
}
|
|
|
|
$installerBaseUrl = if ([string]::IsNullOrWhiteSpace($S3Endpoint) -or [string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
$null
|
|
}
|
|
else {
|
|
"$($S3Endpoint.TrimEnd('/'))/$S3Bucket/lanmountain/update/installers"
|
|
}
|
|
|
|
$legacySnapshots = @{}
|
|
foreach ($config in $supportedPlatforms) {
|
|
$platform = $config.Platform
|
|
$platformBaselineRoot = Join-Path $baselineRoot $platform
|
|
$baselineCurrentDir = Join-Path $platformBaselineRoot "current"
|
|
$baselineVersionPath = Join-Path $platformBaselineRoot "version.txt"
|
|
$snapshotRoot = Join-Path $platformBaselineRoot "previous-snapshot"
|
|
|
|
if (Test-Path -LiteralPath $snapshotRoot) {
|
|
Remove-Item -LiteralPath $snapshotRoot -Recurse -Force
|
|
}
|
|
New-Item -ItemType Directory -Path $snapshotRoot -Force | Out-Null
|
|
|
|
$previousVersion = if (Test-Path -LiteralPath $baselineVersionPath) {
|
|
(Get-Content -LiteralPath $baselineVersionPath -Raw).Trim()
|
|
}
|
|
else {
|
|
"0.0.0"
|
|
}
|
|
|
|
$baselineHasContent = Get-ChildItem -LiteralPath $baselineCurrentDir -Force -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
if ($baselineHasContent) {
|
|
Copy-Item -LiteralPath (Join-Path $baselineCurrentDir '*') -Destination $snapshotRoot -Recurse -Force
|
|
$snapshotDir = $snapshotRoot
|
|
}
|
|
else {
|
|
$snapshotDir = Join-Path $platformBaselineRoot "empty"
|
|
New-Item -ItemType Directory -Path $snapshotDir -Force | Out-Null
|
|
}
|
|
|
|
$legacySnapshots[$platform] = @{
|
|
PreviousVersion = $previousVersion
|
|
PreviousDir = $snapshotDir
|
|
}
|
|
}
|
|
|
|
$publishArguments = @(
|
|
"run",
|
|
"--project", $toolProject,
|
|
"--",
|
|
"publish",
|
|
"--version", $Version,
|
|
"--app-artifacts-root", $AppArtifactsRoot,
|
|
"--installer-artifacts-root", $InstallerArtifactsRoot,
|
|
"--output-dir", $publishedRoot,
|
|
"--private-key", $PrivateKeyPath,
|
|
"--baseline-root", $baselineRoot,
|
|
"--channel", $Channel
|
|
)
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($repoBaseUrl)) {
|
|
$publishArguments += @("--repo-base-url", $repoBaseUrl)
|
|
}
|
|
if (-not [string]::IsNullOrWhiteSpace($installerBaseUrl)) {
|
|
$publishArguments += @("--installer-base-url", $installerBaseUrl)
|
|
}
|
|
|
|
& dotnet @publishArguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "PLONDS publish command failed."
|
|
}
|
|
|
|
foreach ($config in $supportedPlatforms) {
|
|
$platform = $config.Platform
|
|
$artifactRoot = Join-Path $AppArtifactsRoot $config.ArtifactName
|
|
if (-not (Test-Path -LiteralPath $artifactRoot)) {
|
|
throw "App payload artifact root not found for ${platform}: $artifactRoot"
|
|
}
|
|
|
|
$currentAppDir = Resolve-AppDirectory -SearchRoot $artifactRoot -Version $Version
|
|
if ([string]::IsNullOrWhiteSpace($currentAppDir)) {
|
|
throw "Unable to locate app payload directory for $platform under $artifactRoot"
|
|
}
|
|
|
|
$distributionId = "plonds-$Version-$platform"
|
|
$manifestPath = Join-Path $publishedRoot "manifests/$distributionId/plonds-filemap.json"
|
|
$manifestSignaturePath = "$manifestPath.sig"
|
|
|
|
$legacyOutputDir = Join-Path $OutputDir "legacy/$platform"
|
|
New-Item -ItemType Directory -Path $legacyOutputDir -Force | Out-Null
|
|
|
|
$legacyState = $legacySnapshots[$platform]
|
|
& (Join-Path $PSScriptRoot "Generate-DeltaPackage.ps1") `
|
|
-PreviousVersion $legacyState.PreviousVersion `
|
|
-CurrentVersion $Version `
|
|
-PreviousDir $legacyState.PreviousDir `
|
|
-CurrentDir $currentAppDir `
|
|
-OutputDir $legacyOutputDir
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Generate-DeltaPackage.ps1 failed for $platform"
|
|
}
|
|
|
|
$legacyManifestPath = Join-Path $legacyOutputDir "files.json"
|
|
$legacySignaturePath = Join-Path $legacyOutputDir "files.json.sig"
|
|
& (Join-Path $PSScriptRoot "Sign-FileMap.ps1") `
|
|
-FilesJsonPath $legacyManifestPath `
|
|
-PrivateKeyPath $PrivateKeyPath `
|
|
-OutputPath $legacySignaturePath
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to sign legacy manifest for $platform"
|
|
}
|
|
|
|
Copy-Item -LiteralPath $manifestPath -Destination (Join-Path $releaseAssetsRoot "plonds-filemap-$platform.json") -Force
|
|
Copy-Item -LiteralPath $manifestSignaturePath -Destination (Join-Path $releaseAssetsRoot "plonds-filemap-$platform.json.sig") -Force
|
|
Copy-Item -LiteralPath (Join-Path $publishedRoot "meta/distributions/$distributionId.json") -Destination (Join-Path $releaseAssetsRoot "plonds-distribution-$platform.json") -Force
|
|
Copy-Item -LiteralPath (Join-Path $publishedRoot "meta/channels/$Channel/$platform/latest.json") -Destination (Join-Path $releaseAssetsRoot "plonds-latest-$platform.json") -Force
|
|
|
|
Copy-Item -LiteralPath $legacyManifestPath -Destination (Join-Path $releaseAssetsRoot "files-$platform.json") -Force
|
|
Copy-Item -LiteralPath $legacySignaturePath -Destination (Join-Path $releaseAssetsRoot "files-$platform.json.sig") -Force
|
|
Copy-Item -LiteralPath (Join-Path $legacyOutputDir "update.zip") -Destination (Join-Path $releaseAssetsRoot "update-$platform.zip") -Force
|
|
}
|
|
|
|
if (-not [string]::IsNullOrWhiteSpace($S3Endpoint) -and -not [string]::IsNullOrWhiteSpace($S3Bucket)) {
|
|
Upload-DirectoryToS3 -LocalRoot $publishedRoot -RemotePrefix "lanmountain/update"
|
|
|
|
foreach ($config in $supportedPlatforms) {
|
|
$platform = $config.Platform
|
|
$platformBaselineRoot = Join-Path $baselineRoot $platform
|
|
$baselineCurrentDir = Join-Path $platformBaselineRoot "current"
|
|
$baselineVersionPath = Join-Path $platformBaselineRoot "version.txt"
|
|
|
|
Upload-DirectoryToS3 `
|
|
-LocalRoot $baselineCurrentDir `
|
|
-RemotePrefix "lanmountain/update/baselines/$platform/current" `
|
|
-DeleteExtraRemoteObjects
|
|
|
|
Upload-FileToS3 `
|
|
-LocalPath $baselineVersionPath `
|
|
-RemoteKey "lanmountain/update/baselines/$platform/version.txt"
|
|
}
|
|
}
|
|
|
|
Write-Host "PLONDS publish staging completed."
|
|
Write-Host "Published root: $publishedRoot"
|
|
Write-Host "Release assets: $releaseAssetsRoot"
|