Some checks failed
Windows project source guards / test (push) Has been cancelled
361 lines
15 KiB
PowerShell
361 lines
15 KiB
PowerShell
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateSet("dev", "stable")]
|
|
[string]$Mode,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern("^[0-9a-f]{40}$")]
|
|
[string]$Commit,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidatePattern("^\d+\.\d+\.\d+$")]
|
|
[string]$Version,
|
|
|
|
[string]$RepoRoot = "C:\Users\privacy\ZapretKVN-local-release",
|
|
[string]$ManifestPath = ""
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
Set-StrictMode -Version Latest
|
|
|
|
function Invoke-Native([string]$Executable, [string[]]$Arguments) {
|
|
& $Executable @Arguments | ForEach-Object { Write-Host $_ }
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "$Executable failed with exit code $LASTEXITCODE"
|
|
}
|
|
}
|
|
|
|
function Get-Sha256([string]$Path) {
|
|
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
}
|
|
|
|
function Assert-StableCoreLock([string]$LockFile) {
|
|
$lock = Get-Content -LiteralPath $LockFile -Raw | ConvertFrom-Json
|
|
if ([int]$lock.schema -ne 1 -or [string]$lock.platform -ne "windows-x64") {
|
|
throw "Unsupported core lock format: $LockFile"
|
|
}
|
|
$required = @{
|
|
"xray-core" = "XTLS/Xray-core"
|
|
"sing-box-extended" = "shtorm-7/sing-box-extended"
|
|
"hysteria" = "HyNetworks/hysteria"
|
|
}
|
|
foreach ($entry in $required.GetEnumerator()) {
|
|
$matches = @($lock.sources | Where-Object { [string]$_.id -eq $entry.Key })
|
|
if ($matches.Count -ne 1) {
|
|
throw "Core lock must contain exactly one $($entry.Key) source"
|
|
}
|
|
$source = $matches[0]
|
|
if ([string]$source.repository -ne $entry.Value -or [string]$source.channel -ne "stable") {
|
|
throw "Core lock source $($entry.Key) is not from the approved stable channel"
|
|
}
|
|
if ($source.release_prerelease -ne $false) {
|
|
throw "Stable release cannot use prerelease core source $($entry.Key)"
|
|
}
|
|
if ([string]$source.release_tag -ne [string]$source.version) {
|
|
throw "Core release tag/version mismatch for $($entry.Key)"
|
|
}
|
|
if ([string]$source.asset_name -ne [string]$source.archive) {
|
|
throw "Core asset/archive mismatch for $($entry.Key)"
|
|
}
|
|
if ([int64]$source.asset_size -le 0) {
|
|
throw "Core source $($entry.Key) has no verified asset size"
|
|
}
|
|
}
|
|
if ([string]$lock.amnezia.repository -ne "amnezia-vpn/amneziawg-go" -or
|
|
[string]$lock.amnezia.channel -ne "official-tags" -or
|
|
[string]$lock.amnezia.commit -notmatch '^[0-9a-f]{40}$' -or
|
|
[string]$lock.amnezia.sha256 -notmatch '^[0-9a-f]{64}$') {
|
|
throw "Core lock must pin the official Amnezia module and source digest"
|
|
}
|
|
return $lock
|
|
}
|
|
|
|
function Stop-ReleaseProcesses([string]$Root) {
|
|
$ownedRoots = @(
|
|
[IO.Path]::GetFullPath((Join-Path $Root "dist\ZapretKVN")),
|
|
[IO.Path]::GetFullPath((Join-Path $Root "core"))
|
|
)
|
|
$names = @("ZapretKVN", "sing-box", "xray", "hysteria", "zapret-amnezia", "tun2socks")
|
|
foreach ($process in Get-Process -Name $names -ErrorAction SilentlyContinue) {
|
|
$path = $null
|
|
try { $path = $process.Path } catch { $path = $null }
|
|
if (-not $path) { continue }
|
|
$resolved = [IO.Path]::GetFullPath($path)
|
|
$owned = $ownedRoots | Where-Object {
|
|
$resolved.StartsWith($_, [StringComparison]::OrdinalIgnoreCase)
|
|
}
|
|
if ($owned) {
|
|
Write-Host "[release] stopping $($process.ProcessName) from $resolved"
|
|
Stop-Process -Id $process.Id -Force
|
|
Wait-Process -Id $process.Id -Timeout 15 -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-Workspace([string]$Root, [string]$ExpectedCommit, [string]$ExpectedVersion) {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $Root ".git"))) {
|
|
throw "Release workspace is missing: $Root"
|
|
}
|
|
Set-Location -LiteralPath $Root
|
|
Invoke-Native "git" @("fetch", "origin", "main", "--tags")
|
|
Invoke-Native "git" @("switch", "--detach", $ExpectedCommit)
|
|
$actualCommit = (& git rev-parse HEAD).Trim()
|
|
if ($actualCommit -ne $ExpectedCommit) {
|
|
throw "Windows workspace SHA mismatch: $actualCommit"
|
|
}
|
|
$source = Get-Content -LiteralPath (Join-Path $Root "xray_fluent\constants.py") -Raw
|
|
if (-not $source.Contains("APP_VERSION = `"$ExpectedVersion`"")) {
|
|
throw "APP_VERSION does not match $ExpectedVersion"
|
|
}
|
|
$unexpected = @(
|
|
git status --short --untracked-files=all |
|
|
Where-Object { $_ -notmatch "^\?\? package-zapret-kvn\.ps1$" }
|
|
)
|
|
if ($unexpected.Count -ne 0) {
|
|
$unexpected | Write-Host
|
|
throw "Windows release workspace contains unexpected changes"
|
|
}
|
|
}
|
|
|
|
function Install-VerifiedCore([string]$Root) {
|
|
$lockFile = Join-Path $Root "scripts\core-lock.windows-x64.json"
|
|
$null = Assert-StableCoreLock $lockFile
|
|
$archive = Join-Path $Root ".cache\core-bundle\core-windows-x64.7z"
|
|
$stamp = "$archive.inputs.sha256"
|
|
$lockHash = (& py -3 (Join-Path $Root "scripts\core_bundle_fingerprint.py") --root $Root --lock $lockFile | Out-String).Trim()
|
|
if ($LASTEXITCODE -ne 0 -or $lockHash -notmatch '^[0-9a-f]{64}$') { throw "Cannot fingerprint current core sources" }
|
|
$cachedHash = if (Test-Path -LiteralPath $stamp) {
|
|
(Get-Content -LiteralPath $stamp -Raw).Trim().ToLowerInvariant()
|
|
} else { "" }
|
|
if (-not (Test-Path -LiteralPath $archive) -or $cachedHash -ne $lockHash) {
|
|
Write-Host "[release] rebuilding pinned core bundle"
|
|
& (Join-Path $Root "scripts\build_core_bundle.ps1")
|
|
} else {
|
|
Write-Host "[release] reusing core bundle for verified inputs $lockHash"
|
|
}
|
|
& (Join-Path $Root "scripts\install_core_bundle.ps1")
|
|
}
|
|
|
|
function Get-CoreProof([string]$Root) {
|
|
$lockFile = Join-Path $Root "scripts\core-lock.windows-x64.json"
|
|
$lock = Assert-StableCoreLock $lockFile
|
|
$manifestPath = Join-Path $Root "core\core-manifest.windows-x64.json"
|
|
if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) {
|
|
throw "Installed core manifest is missing: $manifestPath"
|
|
}
|
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
|
|
$expectedInputs = (& py -3 (Join-Path $Root "scripts\core_bundle_fingerprint.py") --root $Root --lock $lockFile | Out-String).Trim()
|
|
if ($LASTEXITCODE -ne 0 -or [string]$manifest.inputs_sha256 -ne $expectedInputs -or
|
|
[string]$manifest.lock_sha256 -ne (Get-Sha256 $lockFile)) {
|
|
throw "Installed cores were built from different source/patch inputs"
|
|
}
|
|
$sources = @((@($lock.sources) + @($lock.amnezia)) | Where-Object {
|
|
[string]$_.id -in @("xray-core", "sing-box-extended", "hysteria", "amnezia")
|
|
} | ForEach-Object {
|
|
[ordered]@{
|
|
id = [string]$_.id
|
|
version = [string]$_.version
|
|
repository = [string]$_.repository
|
|
channel = [string]$_.channel
|
|
release_prerelease = [bool]$_.release_prerelease
|
|
archive_sha256 = [string]$_.sha256
|
|
asset_size = [int64]$_.asset_size
|
|
}
|
|
})
|
|
return [ordered]@{
|
|
lock_sha256 = Get-Sha256 $lockFile
|
|
manifest_sha256 = Get-Sha256 $manifestPath
|
|
sources = $sources
|
|
singbox_build = $manifest.singbox_build
|
|
inputs_sha256 = $manifest.inputs_sha256
|
|
}
|
|
}
|
|
|
|
function Ensure-DependenciesAndTests([string]$Root) {
|
|
$python = Join-Path $Root ".venv\Scripts\python.exe"
|
|
if (-not (Test-Path -LiteralPath $python)) {
|
|
Invoke-Native "py" @("-3", "-m", "venv", (Join-Path $Root ".venv"))
|
|
}
|
|
Invoke-Native $python @("-m", "pip", "install", "-r", (Join-Path $Root "requirements.txt"))
|
|
Invoke-Native $python @("-m", "pip", "check")
|
|
Invoke-Native $python @("-m", "unittest", "discover", "-s", "tests", "-v")
|
|
}
|
|
|
|
function Build-Application([string]$Root) {
|
|
Stop-ReleaseProcesses $Root
|
|
$python = Join-Path $Root ".venv\Scripts\python.exe"
|
|
Invoke-Native $python @((Join-Path $Root "build.py"), "--no-zip")
|
|
$exe = Join-Path $Root "dist\ZapretKVN\ZapretKVN.exe"
|
|
if (-not (Test-Path -LiteralPath $exe -PathType Leaf)) {
|
|
throw "Built ZapretKVN.exe is missing"
|
|
}
|
|
return $exe
|
|
}
|
|
|
|
function Assert-CleanPayload([string]$Root) {
|
|
$portable = Join-Path $Root "dist\ZapretKVN"
|
|
if (-not (Test-Path -LiteralPath $portable -PathType Container)) {
|
|
throw "Clean release payload is missing: $portable"
|
|
}
|
|
|
|
$allowedTopLevel = @("ZapretKVN.exe", "_internal", "assets", "core", "data", "zapret")
|
|
$unexpectedTopLevel = @(
|
|
Get-ChildItem -LiteralPath $portable -Force |
|
|
Where-Object { $allowedTopLevel -notcontains $_.Name }
|
|
)
|
|
if ($unexpectedTopLevel.Count -ne 0) {
|
|
$unexpectedTopLevel | ForEach-Object { Write-Host $_.FullName }
|
|
throw "Release payload contains unexpected top-level files"
|
|
}
|
|
|
|
$data = Join-Path $portable "data"
|
|
if (-not (Test-Path -LiteralPath $data -PathType Container)) {
|
|
throw "Release payload is missing data/templates"
|
|
}
|
|
$unexpectedData = @(
|
|
Get-ChildItem -LiteralPath $data -Force |
|
|
Where-Object { $_.Name -ne "templates" }
|
|
)
|
|
if ($unexpectedData.Count -ne 0) {
|
|
$unexpectedData | ForEach-Object { Write-Host $_.FullName }
|
|
throw "Release payload contains runtime data; only data/templates is allowed"
|
|
}
|
|
if (-not (Test-Path -LiteralPath (Join-Path $data "templates") -PathType Container)) {
|
|
throw "Release payload is missing data/templates"
|
|
}
|
|
|
|
$ruleSetRoot = Join-Path $portable "core\rule-set"
|
|
$requiredRuleSets = @(
|
|
"geosite-ru-blocked.srs",
|
|
"geoip-ru-blocked.srs",
|
|
"geosite-category-ru.srs",
|
|
"geoip-ru.srs"
|
|
)
|
|
foreach ($name in $requiredRuleSets) {
|
|
$path = Join-Path $ruleSetRoot $name
|
|
if (-not (Test-Path -LiteralPath $path -PathType Leaf) -or (Get-Item -LiteralPath $path).Length -le 0) {
|
|
throw "Release payload is missing bundled sing-box rule-set: $path"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-ShippedTemplates([string]$Root) {
|
|
$sourceRoot = (Resolve-Path (Join-Path $Root "data\templates")).Path
|
|
$templates = @(Get-ChildItem -LiteralPath $sourceRoot -Recurse -File)
|
|
foreach ($template in $templates) {
|
|
$relative = $template.FullName.Substring($sourceRoot.Length).TrimStart("\")
|
|
foreach ($destinationRoot in @(
|
|
(Join-Path $Root "dist\ZapretKVN\data\templates"),
|
|
(Join-Path $Root "dist\ZapretKVN\assets\template-update")
|
|
)) {
|
|
$destination = Join-Path $destinationRoot $relative
|
|
if (-not (Test-Path -LiteralPath $destination -PathType Leaf)) {
|
|
throw "Missing shipped template: $destination"
|
|
}
|
|
if ((Get-Sha256 $template.FullName) -ne (Get-Sha256 $destination)) {
|
|
throw "Shipped template mismatch: $destination"
|
|
}
|
|
}
|
|
}
|
|
|
|
$portable = Join-Path $Root "dist\ZapretKVN"
|
|
$singbox = Join-Path $portable "core\sing-box.exe"
|
|
$workingDirectory = Join-Path $portable "core"
|
|
$singboxTemplates = @(Get-ChildItem -LiteralPath (Join-Path $portable "data\templates\sing-box") -Filter "*.json" -File)
|
|
foreach ($template in $singboxTemplates) {
|
|
Invoke-Native $singbox @("check", "-D", $workingDirectory, "-c", $template.FullName)
|
|
}
|
|
return $templates.Count
|
|
}
|
|
|
|
function Find-SevenZip {
|
|
$command = Get-Command 7z.exe -ErrorAction SilentlyContinue
|
|
if ($command) { return $command.Source }
|
|
$candidate = Join-Path $env:ProgramFiles "7-Zip\7z.exe"
|
|
if (Test-Path -LiteralPath $candidate) { return $candidate }
|
|
throw "7-Zip is not installed"
|
|
}
|
|
|
|
function New-StableAssets([string]$Root, [string]$ReleaseVersion) {
|
|
$sevenZip = Find-SevenZip
|
|
$tag = "v$ReleaseVersion"
|
|
$dist = Join-Path $Root "dist"
|
|
$portable = Join-Path $dist "ZapretKVN"
|
|
$coreBundle = Join-Path $Root ".cache\core-bundle\core-windows-x64.7z"
|
|
$sfx = Join-Path $dist "ZapretKVN-$tag-windows-x64.exe"
|
|
$zip = Join-Path $dist "ZapretKVN-$tag-windows-x64.zip"
|
|
$checksum = "$zip.sha256"
|
|
$archive = Join-Path $dist "ZapretKVN-$tag-windows-x64.7z"
|
|
$releaseCore = Join-Path $dist "ZapretKVN-cores-$tag-windows-x64.7z"
|
|
$outputs = @($sfx, $zip, $checksum, $archive, $releaseCore)
|
|
|
|
foreach ($output in $outputs) {
|
|
if (Test-Path -LiteralPath $output) {
|
|
Remove-Item -LiteralPath $output -Force
|
|
}
|
|
}
|
|
Push-Location -LiteralPath $portable
|
|
try {
|
|
Invoke-Native $sevenZip @("a", "-bd", "-t7z", "-mx=5", "-sfx", $sfx, "*")
|
|
Invoke-Native $sevenZip @("a", "-bd", "-tzip", "-mx=5", $zip, "*")
|
|
Invoke-Native $sevenZip @("a", "-bd", "-t7z", "-mx=5", $archive, "*")
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
Copy-Item -LiteralPath $coreBundle -Destination $releaseCore
|
|
$zipHash = Get-Sha256 $zip
|
|
[IO.File]::WriteAllText($checksum, "$zipHash`n", [Text.Encoding]::ASCII)
|
|
foreach ($testFile in @($sfx, $zip, $archive, $releaseCore)) {
|
|
Invoke-Native $sevenZip @("t", "-bd", $testFile)
|
|
}
|
|
|
|
return @($outputs | ForEach-Object {
|
|
$item = Get-Item -LiteralPath $_
|
|
if ($item.Length -le 0) { throw "Empty release asset: $($_)" }
|
|
[ordered]@{
|
|
name = $item.Name
|
|
path = $item.FullName
|
|
size = $item.Length
|
|
sha256 = Get-Sha256 $item.FullName
|
|
}
|
|
})
|
|
}
|
|
|
|
Assert-Workspace $RepoRoot $Commit $Version
|
|
Stop-ReleaseProcesses $RepoRoot
|
|
Install-VerifiedCore $RepoRoot
|
|
$coreProof = Get-CoreProof $RepoRoot
|
|
Ensure-DependenciesAndTests $RepoRoot
|
|
$exePath = Build-Application $RepoRoot
|
|
Assert-CleanPayload $RepoRoot
|
|
$templateCount = Test-ShippedTemplates $RepoRoot
|
|
$geoipJson = & (Join-Path $RepoRoot ".venv\Scripts\python.exe") (Join-Path $RepoRoot "scripts\prepare_geoip.py") --verify (Join-Path $RepoRoot "dist\ZapretKVN\assets")
|
|
if ($LASTEXITCODE -ne 0) { throw "Packaged GeoIP verification failed" }
|
|
$geoipProof = $geoipJson | ConvertFrom-Json
|
|
$assets = if ($Mode -eq "stable") { @(New-StableAssets $RepoRoot $Version) } else { @() }
|
|
|
|
if (-not $ManifestPath) {
|
|
$ManifestPath = Join-Path $RepoRoot ".cache\release\v$Version\$Mode-manifest.json"
|
|
}
|
|
$manifestDirectory = Split-Path -Parent $ManifestPath
|
|
New-Item -ItemType Directory -Force -Path $manifestDirectory | Out-Null
|
|
$manifest = [ordered]@{
|
|
schema = 1
|
|
mode = $Mode
|
|
version = $Version
|
|
commit = $Commit
|
|
generated_at_utc = [DateTime]::UtcNow.ToString("o")
|
|
executable = [ordered]@{
|
|
path = $exePath
|
|
size = (Get-Item -LiteralPath $exePath).Length
|
|
sha256 = Get-Sha256 $exePath
|
|
}
|
|
templates_verified = $templateCount
|
|
core = $coreProof
|
|
geoip = $geoipProof
|
|
assets = $assets
|
|
}
|
|
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $ManifestPath -Encoding utf8
|
|
Write-Host "[release] $Mode gate complete: $ManifestPath"
|