summaryrefslogtreecommitdiff
path: root/scripts/Publish-Release.ps1
blob: d794bb5e99cc2a55c7a3ba1f700af0e819ea13d1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
param(
    [string]$Version = '1.0.0',
    [string]$OutputRoot = 'build\Release\publish',
    [switch]$SkipRuntimeCheck
)

$ErrorActionPreference = 'Stop'
$repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$outputRootPath = [IO.Path]::GetFullPath((Join-Path $repository $OutputRoot))
$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"
$bundle = Join-Path $outputRootPath $bundleName
$zip = Join-Path $outputRootPath "$bundleName.zip"
$checksum = "$zip.sha256"
$stagingRoot = Join-Path $outputRootPath (".staging-" + [Guid]::NewGuid().ToString('N'))
$stagingBundle = Join-Path $stagingRoot $bundleName

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'
dotnet restore $appProject -r win-x64 --locked-mode
if ($LASTEXITCODE -ne 0) { throw "win-x64 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" }

$required = @(
    'agent-mcp.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" }
}

$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

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()) ("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 sandbox resolve --config .\agent.yaml --lang en
            if ($LASTEXITCODE -ne 0) { throw "Bundle resolution check failed with exit code $LASTEXITCODE." }
        }
        finally { Pop-Location }
    }
}
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
    }
    else { New-Item -ItemType Directory -Path $bundle | Out-Null }
    Copy-Item -Path (Join-Path $stagingBundle '*') -Destination $bundle -Recurse -Force
}
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"