diff options
Diffstat (limited to 'scripts/Publish-Release.ps1')
| -rw-r--r-- | scripts/Publish-Release.ps1 | 208 |
1 files changed, 135 insertions, 73 deletions
diff --git a/scripts/Publish-Release.ps1 b/scripts/Publish-Release.ps1 index 3ab7bda..b34986a 100644 --- a/scripts/Publish-Release.ps1 +++ b/scripts/Publish-Release.ps1 @@ -1,5 +1,4 @@ param( - [string]$Version = '1.0.0', [string]$OutputRoot = 'build\Release\publish', [switch]$SkipRuntimeCheck ) @@ -11,96 +10,159 @@ $buildRootPath = [IO.Path]::GetFullPath((Join-Path $repository 'build')) if (-not $outputRootPath.StartsWith($buildRootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { throw 'Release output must remain inside the repository build directory.' } -$bundleName = "agent-sandbox-mcp-$Version-win-x64" + +$appProject = Join-Path $repository 'src\Agent.App\Agent.App.csproj' +$adminProject = Join-Path $repository 'src\Agent.Admin\Agent.Admin.Host\Agent.Admin.Host.csproj' +function Get-ApplicationVersion([string]$project) { + $value = (& dotnet msbuild $project -nologo -getProperty:Version | Out-String).Trim() + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($value)) { + throw "Could not resolve the Application Version for $project." + } + return $value +} + +$version = Get-ApplicationVersion $appProject +$adminVersion = Get-ApplicationVersion $adminProject +if ($version -ne $adminVersion) { throw "Main/admin Application Version mismatch: $version / $adminVersion" } +if ($version -notmatch '^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$') { + throw "Application Version is not SemVer: $version" +} +$releaseVersion = [Version]($version -replace '[-+].*$', '') + +$bundleName = 'winagent-sandbox-mcp' +$assetBase = "$bundleName-v$version-win-x64" $bundle = Join-Path $outputRootPath $bundleName -$zip = Join-Path $outputRootPath "$bundleName.zip" +$zip = Join-Path $outputRootPath "$assetBase.zip" $checksum = "$zip.sha256" -$stagingRoot = Join-Path $outputRootPath (".staging-" + [Guid]::NewGuid().ToString('N')) +$stagingRoot = Join-Path $outputRootPath ('.staging-' + [Guid]::NewGuid().ToString('N')) $stagingBundle = Join-Path $stagingRoot $bundleName +$forbiddenNames = @('agent.local.yaml', 'local', 'logs', 'state', 'applied-configurations', 'TestResults', 'artifacts', '.staging') + +function Assert-ReleaseIdentity([string]$path, [string]$assemblyPath, [string]$label) { + $info = [Diagnostics.FileVersionInfo]::GetVersionInfo($path) + $fileVersion = [Version]$info.FileVersion + if ($fileVersion.Major -ne $releaseVersion.Major -or + $fileVersion.Minor -ne $releaseVersion.Minor -or + $fileVersion.Build -ne $releaseVersion.Build) { + throw "$label FileVersion '$($info.FileVersion)' does not match Application Version '$version'." + } + $assemblyVersion = [Reflection.AssemblyName]::GetAssemblyName($assemblyPath).Version + if ($assemblyVersion.Major -ne $releaseVersion.Major -or + $assemblyVersion.Minor -ne $releaseVersion.Minor -or + $assemblyVersion.Build -ne $releaseVersion.Build) { + throw "$label AssemblyVersion '$assemblyVersion' does not match Application Version '$version'." + } + if ($info.ProductVersion -notmatch ('^' + [Regex]::Escape($version) + '(?:\+[0-9A-Za-z.-]+)?$')) { + throw "$label InformationalVersion '$($info.ProductVersion)' is not compatible with '$version'." + } + [PSCustomObject]@{ + Label = $label + ApplicationVersion = $version + AssemblyVersion = $assemblyVersion.ToString() + FileVersion = $info.FileVersion + InformationalVersion = $info.ProductVersion + } +} + +function Assert-NoUserState([string]$root) { + $forbidden = Get-ChildItem -LiteralPath $root -Force -Recurse | + Where-Object Name -in $forbiddenNames | + Select-Object -First 1 + if ($forbidden) { + $relative = [IO.Path]::GetRelativePath($root, $forbidden.FullName) + throw "Release artifact must not contain user state, local assets, or test artifacts: $relative" + } +} New-Item -ItemType Directory -Force -Path $outputRootPath | Out-Null if (Test-Path -LiteralPath $zip) { Remove-Item -LiteralPath $zip -Force } if (Test-Path -LiteralPath $checksum) { Remove-Item -LiteralPath $checksum -Force } New-Item -ItemType Directory -Path $stagingBundle | Out-Null -$appProject = Join-Path $repository 'src\Agent.App\Agent.App.csproj' -$adminProject = Join-Path $repository 'src\Agent.Admin\Agent.Admin.Host\Agent.Admin.Host.csproj' -dotnet restore $appProject -r win-x64 --locked-mode -if ($LASTEXITCODE -ne 0) { throw "win-x64 locked restore failed with exit code $LASTEXITCODE" } -dotnet restore $adminProject -r win-x64 --locked-mode -if ($LASTEXITCODE -ne 0) { throw "win-x64 admin locked restore failed with exit code $LASTEXITCODE" } -dotnet publish $appProject -c Release -r win-x64 --self-contained true --no-restore --output $stagingBundle -p:Version=$Version -if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" } -$adminBundle = Join-Path $stagingBundle 'admin' -dotnet publish $adminProject -c Release -r win-x64 --self-contained true --no-restore --output $adminBundle -p:Version=$Version -if ($LASTEXITCODE -ne 0) { throw "admin dotnet publish failed with exit code $LASTEXITCODE" } +try { + dotnet restore $appProject -r win-x64 --locked-mode + if ($LASTEXITCODE -ne 0) { throw "win-x64 locked restore failed with exit code $LASTEXITCODE" } + dotnet restore $adminProject -r win-x64 --locked-mode + if ($LASTEXITCODE -ne 0) { throw "win-x64 admin locked restore failed with exit code $LASTEXITCODE" } + dotnet publish $appProject -c Release -r win-x64 --self-contained true --no-restore --output $stagingBundle + if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed with exit code $LASTEXITCODE" } + $adminBundle = Join-Path $stagingBundle 'admin' + dotnet publish $adminProject -c Release -r win-x64 --self-contained true --no-restore --output $adminBundle + if ($LASTEXITCODE -ne 0) { throw "admin dotnet publish failed with exit code $LASTEXITCODE" } -$required = @( - 'agent-mcp.exe', 'admin\agent-mcp-admin.exe', 'agent.yaml', 'config\locales\en.json', 'config\locales\ja.json', - 'prompts\user-system.md', 'skills', 'README.md' -) -foreach ($relative in $required) { - if (-not (Test-Path -LiteralPath (Join-Path $stagingBundle $relative))) { throw "Release Bundle is missing $relative" } -} -foreach ($relative in @('agent.local.yaml', 'config\agent.local.example.yaml')) { - if (Test-Path -LiteralPath (Join-Path $stagingBundle $relative)) { throw "Release Bundle must not contain local override input: $relative" } -} + $required = @( + 'agent-mcp.exe', 'admin\agent-mcp-admin.exe', 'agent.yaml', 'config\locales\en.json', 'config\locales\ja.json', + 'prompts\user-system.md', 'skills', 'README.md', 'CHANGELOG.md' + ) + foreach ($relative in $required) { + if (-not (Test-Path -LiteralPath (Join-Path $stagingBundle $relative))) { throw "Release Bundle is missing $relative" } + } + Assert-NoUserState $stagingBundle -$manifestPath = Join-Path $stagingBundle 'publish-manifest.sha256' -Get-ChildItem -LiteralPath $stagingBundle -File -Recurse | - Where-Object FullName -ne $manifestPath | - Sort-Object FullName | - ForEach-Object { - $relative = [IO.Path]::GetRelativePath($stagingBundle, $_.FullName).Replace('\', '/') - $hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$hash $relative" - } | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM + $mainVersionInfo = Assert-ReleaseIdentity (Join-Path $stagingBundle 'agent-mcp.exe') (Join-Path $stagingBundle 'agent-mcp.dll') 'main' + $adminVersionInfo = Assert-ReleaseIdentity (Join-Path $stagingBundle 'admin\agent-mcp-admin.exe') (Join-Path $stagingBundle 'admin\agent-mcp-admin.dll') 'admin' -Compress-Archive -LiteralPath $stagingBundle -DestinationPath $zip -CompressionLevel Optimal -$zipHash = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() -"$zipHash $([IO.Path]::GetFileName($zip))" | Set-Content -LiteralPath $checksum -Encoding ascii + $manifestPath = Join-Path $stagingBundle 'publish-manifest.sha256' + Get-ChildItem -LiteralPath $stagingBundle -File -Recurse | + Where-Object FullName -ne $manifestPath | + Sort-Object FullName | + ForEach-Object { + $relative = [IO.Path]::GetRelativePath($stagingBundle, $_.FullName).Replace('\', '/') + $hash = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "$hash $relative" + } | Set-Content -LiteralPath $manifestPath -Encoding utf8NoBOM -$freshRoot = Join-Path ([IO.Path]::GetTempPath()) ("agent-mcp-release-check-" + [Guid]::NewGuid().ToString('N')) -New-Item -ItemType Directory -Path $freshRoot | Out-Null -try { - Expand-Archive -LiteralPath $zip -DestinationPath $freshRoot - $freshBundle = Join-Path $freshRoot $bundleName - $sourceReferences = Get-ChildItem -LiteralPath $freshBundle -File -Recurse | - Where-Object Extension -in @('.yaml', '.json', '.md', '.config') | - Select-String -SimpleMatch -Pattern $repository - if ($sourceReferences) { throw 'Release Bundle contains a source-tree reference.' } - if (-not $SkipRuntimeCheck) { - Push-Location $freshBundle - try { - & .\agent-mcp.exe show .\agent.yaml - if ($LASTEXITCODE -ne 0) { throw "Bundle configuration check failed with exit code $LASTEXITCODE." } + Compress-Archive -LiteralPath $stagingBundle -DestinationPath $zip -CompressionLevel Optimal + $zipHash = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash.ToLowerInvariant() + "$zipHash $([IO.Path]::GetFileName($zip))" | Set-Content -LiteralPath $checksum -Encoding ascii + + $freshRoot = Join-Path ([IO.Path]::GetTempPath()) ('winagent-sandbox-mcp-release-check-' + [Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Path $freshRoot | Out-Null + try { + Expand-Archive -LiteralPath $zip -DestinationPath $freshRoot + $freshItems = @(Get-ChildItem -LiteralPath $freshRoot -Force) + if ($freshItems.Count -ne 1 -or -not $freshItems[0].PSIsContainer -or $freshItems[0].Name -ne $bundleName) { + throw "ZIP must contain exactly one unversioned root directory named '$bundleName'." + } + $freshBundle = Join-Path $freshRoot $bundleName + Assert-NoUserState $freshBundle + $sourceReferences = Get-ChildItem -LiteralPath $freshBundle -File -Recurse | + Where-Object Extension -in @('.yaml', '.json', '.md', '.config') | + Select-String -SimpleMatch -Pattern $repository + if ($sourceReferences) { throw 'Release Bundle contains a source-tree reference.' } + foreach ($line in Get-Content -LiteralPath (Join-Path $freshBundle 'publish-manifest.sha256')) { + if ($line -notmatch '^([0-9a-f]{64}) (.+)$') { throw "Invalid manifest entry: $line" } + $actual = (Get-FileHash -LiteralPath (Join-Path $freshBundle $Matches[2]) -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $Matches[1]) { throw "Manifest hash mismatch: $($Matches[2])" } + } + if (-not $SkipRuntimeCheck) { + Push-Location $freshBundle + try { + & .\agent-mcp.exe show .\agent.yaml + if ($LASTEXITCODE -ne 0) { throw "Bundle configuration check failed with exit code $LASTEXITCODE." } + } + finally { Pop-Location } } - finally { Pop-Location } } -} -finally { - if (Test-Path -LiteralPath $freshRoot) { Remove-Item -LiteralPath $freshRoot -Recurse -Force } -} + finally { + if (Test-Path -LiteralPath $freshRoot) { Remove-Item -LiteralPath $freshRoot -Recurse -Force } + } -# The ZIP is reproducible defaults only. The deployment directory keeps explicitly local, -# publish-excluded inputs while all versioned defaults and binaries are replaced. -try { - if (Test-Path -LiteralPath $bundle) { - $resolvedBundle = (Resolve-Path -LiteralPath $bundle).Path - if (-not $resolvedBundle.StartsWith($outputRootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { - throw 'Existing deployment Bundle escaped the validated output root.' - } - Get-ChildItem -LiteralPath $resolvedBundle -Force | - Where-Object Name -notin @('agent.local.yaml', 'local', 'logs', 'state') | - Remove-Item -Recurse -Force + $bundleFullPath = [IO.Path]::GetFullPath($bundle) + if (-not $bundleFullPath.StartsWith($outputRootPath + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase) -or + [IO.Path]::GetFileName($bundleFullPath) -ne $bundleName) { + throw 'Fixed Release Bundle target escaped the validated output root.' } - else { New-Item -ItemType Directory -Path $bundle | Out-Null } - Copy-Item -Path (Join-Path $stagingBundle '*') -Destination $bundle -Recurse -Force + if (Test-Path -LiteralPath $bundleFullPath) { Remove-Item -LiteralPath $bundleFullPath -Recurse -Force } + Move-Item -LiteralPath $stagingBundle -Destination $bundleFullPath + Assert-NoUserState $bundleFullPath + + $mainVersionInfo + $adminVersionInfo + Write-Output "Bundle: $bundle" + Write-Output "ZIP: $zip" + Write-Output "SHA-256: $zipHash" } finally { if (Test-Path -LiteralPath $stagingRoot) { Remove-Item -LiteralPath $stagingRoot -Recurse -Force } } - -Write-Output "Bundle: $bundle" -Write-Output "ZIP: $zip" -Write-Output "SHA-256: $checksum" |
