build: production Windows release pipeline (PyInstaller, SBOM, CI, signing hook)
Some checks failed
CI / Test Python 3.10 (push) Has been cancelled
CI / Test Python 3.11 (push) Has been cancelled
CI / Test Python 3.12 (push) Has been cancelled

This commit is contained in:
Dr Jones
2026-05-22 20:05:00 -07:00
parent b852fd264f
commit 08683ab328
10 changed files with 659 additions and 95 deletions

56
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Release Build
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
version:
description: "Optional version override (e.g. 1.2.0)"
required: false
default: ""
jobs:
release:
name: Build Windows release
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Production release build
shell: pwsh
run: |
$args = @()
if ("${{ github.event.inputs.version }}") { $args += "-Version", "${{ github.event.inputs.version }}" }
& ./scripts/release_build.ps1 @args
- name: Upload release artifacts
uses: actions/upload-artifact@v4
with:
name: ProxyGod-windows-amd64
path: |
releases/**/*
dist/ProxyChainManager.exe
dist/ProxyChainManager.exe.sha256
if-no-files-found: error
- name: Create GitHub Release (tag push only)
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: |
releases/**/*.zip
releases/**/ProxyChainManager.exe
releases/**/ProxyChainManager.exe.sha256
releases/**/RELEASE_MANIFEST.json
releases/**/SBOM.json
generate_release_notes: true

4
.gitignore vendored
View File

@@ -28,3 +28,7 @@ signup_draft.json
# (downloaded from go-gost release with verified SHA256), not source code # (downloaded from go-gost release with verified SHA256), not source code
proxy_chain_manager/_bundled/ proxy_chain_manager/_bundled/
# Release output (scripts/release_build.ps1) — upload as CI artifacts / GitHub Release
releases/
build/version_info.txt

View File

@@ -56,12 +56,9 @@ a = Analysis(
) )
pyz = PYZ(a.pure) pyz = PYZ(a.pure)
exe = EXE( # Windows VERSIONINFO — generated by scripts/generate_version_info.py before build.
pyz, _version_file = _ROOT / 'build' / 'version_info.txt'
a.scripts, _exe_kwargs = dict(
a.binaries,
a.datas,
[],
name='ProxyChainManager', name='ProxyChainManager',
debug=False, debug=False,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
@@ -77,3 +74,14 @@ exe = EXE(
entitlements_file=None, entitlements_file=None,
uac_admin=True, uac_admin=True,
) )
if _version_file.is_file():
_exe_kwargs['version'] = str(_version_file)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
**_exe_kwargs,
)

View File

@@ -66,35 +66,56 @@ YOU -> VPN (outer tunnel) -> Hop 1 -> Hop 2 -> ... -> Exit hop -> Internet
--- ---
## Build to Desktop (recommended) ## Build (Windows)
Requires **Python 3.10+** and **Windows 10/11 x64**. Python is not needed to *run* the built `.exe`.
1. Clone (or `git pull` in existing repo):
```cmd
git clone https://gitea.thetempleofdoom.com/drjones/proxy-god.git
cd proxy-god
```
2. Install Python if needed:
```cmd ```cmd
winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements
``` ```
3. Build:
```powershell ### Production release (recommended for shipping)
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\setup_and_build.ps1
Full pipeline: tests → bundled GOST → PyInstaller → SHA256 + SBOM + manifest → zip.
```cmd
build_release.bat
``` ```
Equivalent paths: Or:
- `build_exe.bat`
- `FirstRun_Build_And_Install.bat`
Artifacts: ```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\release_build.ps1
```
Output: `releases\ProxyGod-v{version}-windows-amd64.zip` and `releases\v{version}\` (exe, checksum, SBOM, manifest). See [docs/RELEASE_PROCESS.md](docs/RELEASE_PROCESS.md).
Tag-driven CI build (GitHub):
```bash
git tag v1.0.0
git push origin v1.0.0
```
### Developer build (Desktop copy)
Faster iteration — copies `dist\ProxyChainManager.exe` to Desktop + shortcut.
```cmd
build_exe.bat
```
Or `FirstRun_Build_And_Install.bat` (same script, pauses on error).
| Path | Purpose | | Path | Purpose |
|------|------| |------|------|
| `dist\ProxyChainManager.exe` | Built binary | | `dist\ProxyChainManager.exe` | Built binary |
| `Desktop\ProxyChainManager.exe` | Refreshed desktop copy | | `releases\` | Production release folders + zip (from `release_build.ps1`) |
| `Desktop\Proxy God.lnk` | Shortcut target for daily use | | `Desktop\ProxyChainManager.exe` | Dev build desktop copy |
| `Desktop\Proxy God.lnk` | Shortcut |
Recreate shortcut only:
Recreate missing shortcut:
```powershell ```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\create_desktop_shortcut.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\create_desktop_shortcut.ps1
``` ```

27
build_release.bat Normal file
View File

@@ -0,0 +1,27 @@
@echo off
setlocal
title Proxy God — production release build
cd /d "%~dp0"
echo.
echo === Proxy God: production release (tests + SBOM + zip) ===
echo.
where py >nul 2>nul && py -3 -c "import sys; assert sys.version_info>=(3,10)" 2>nul && goto RUN
where python >nul 2>nul && python -c "import sys; assert sys.version_info>=(3,10)" 2>nul && goto RUN
echo [X] Python 3.10+ not found on PATH.
pause
exit /b 1
:RUN
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\release_build.ps1" %*
if errorlevel 1 (
echo.
echo Release build FAILED.
pause
exit /b 1
)
echo.
pause
endlocal

View File

@@ -1,20 +1,92 @@
# Release Process # Release Process
<!-- stewardship-standard: release-process-v1 -->
## Before Tagging Production Windows builds use **PyInstaller** with a gated pipeline in `scripts/release_build.ps1`.
- Confirm the default branch builds, runs, or flashes as documented. ## Quick commands
- Confirm no secrets, private data, generated dependency trees, or raw binaries are accidentally committed.
- Confirm license and upstream provenance are documented.
- Update CHANGELOG.md.
- Attach binaries only as release assets with SHA256 checksums and source commit references.
## Release Notes | Goal | Command |
|------|---------|
| **Production release** | `build_release.bat` or `powershell -File .\scripts\release_build.ps1` |
| **Dev build (Desktop copy)** | `build_exe.bat` or `powershell -File .\scripts\setup_and_build.ps1` |
| **Skip tests (dev only)** | `powershell -File .\scripts\setup_and_build.ps1 -SkipTests` |
Include: ## Production pipeline (`release_build.ps1`)
- Purpose of the release. 1. Resolve Python 3.10+
- Commit hash or tag. 2. Install `requirements.txt` + `dev-requirements.txt` (PyInstaller **6.10.0** pinned)
- Build environment. 3. **Test gate**: `compileall` + `unittest discover` (skip with `-SkipTests` — not for prod)
- Known limitations. 4. Download & SHA-verify bundled `gost.exe``proxy_chain_manager/_bundled/`
- Verification performed. 5. Generate Windows **VERSIONINFO**`build/version_info.txt`
6. **PyInstaller** one-file build via `ProxyChainManager.spec`
7. Optional **Authenticode** sign (see below)
8. SHA256 sidecar for the exe
9. **SBOM** (`SBOM.json` + `requirements-frozen.txt`)
10. **RELEASE_MANIFEST.json** (version, commit, sha256, build time)
11. Zip → `releases/ProxyGod-v{version}-windows-amd64.zip`
### Output layout
```
releases/
v1.0.0/
ProxyChainManager.exe
ProxyChainManager.exe.sha256
RELEASE_MANIFEST.json
SBOM.json
requirements-frozen.txt
LICENSE
OPERATOR_RUNBOOK.md
ProxyGod-v1.0.0-windows-amd64.zip
dist/
ProxyChainManager.exe # same binary (developer convenience)
ProxyChainManager.exe.sha256
```
## Version numbering
Resolved in order:
1. `-Version` parameter to `release_build.ps1`
2. Exact git tag on current commit (`git describe --tags --exact-match`)
3. `git describe --tags --always --dirty`
4. `proxy_chain_manager.__version__`
Tag releases with `v1.2.3` — CI **release.yml** runs automatically on `v*` tags.
## Authenticode signing (optional)
Set before building:
```powershell
$env:SIGN_CERT_PATH = "C:\certs\proxygod.pfx"
$env:SIGN_CERT_PASSWORD = "your-password" # optional if pfx has no password
powershell -File .\scripts\release_build.ps1
```
Requires **Windows SDK** (`signtool.exe` on PATH). Without a cert, the build completes unsigned (SmartScreen may warn on first run).
## CI / GitHub Releases
- **Every push/PR**: `.github/workflows/test.yml` — unit tests only
- **Tag `v*` or manual dispatch**: `.github/workflows/release.yml` — full release build + artifact upload + GitHub Release assets
```bash
git tag v1.0.0
git push origin v1.0.0
```
## Before tagging (checklist)
- [ ] `python -m unittest discover -s tests -v` passes locally
- [ ] CHANGELOG.md updated
- [ ] No secrets in `settings.json` / signup JSON committed
- [ ] `proxy_chain_manager/_bundled/gost.exe` will be fetched at build time (or pre-staged)
- [ ] Verify SHA256 after build: `Get-FileHash releases\v*\ProxyChainManager.exe -Algorithm SHA256`
## Verify a release artifact
```powershell
Get-FileHash -Algorithm SHA256 releases\v1.0.0\ProxyChainManager.exe
Get-Content releases\v1.0.0\ProxyChainManager.exe.sha256
Get-Content releases\v1.0.0\RELEASE_MANIFEST.json | ConvertFrom-Json
```

221
scripts/_build_common.ps1 Normal file
View File

@@ -0,0 +1,221 @@
#Requires -Version 5.1
<#
Shared build helpers for Proxy God / ProxyChainManager.
Dot-sourced by setup_and_build.ps1 and release_build.ps1 — do not run directly.
#>
Set-StrictMode -Version Latest
function Get-RepoRoot {
# This file lives in scripts/ — parent is the repository root.
return (Split-Path -Parent $PSScriptRoot)
}
function Resolve-PythonExe {
if (Get-Command py -ErrorAction SilentlyContinue) {
try {
$out = (& py -3 -c "import sys; print(sys.executable)" 2>$null).Trim()
if ($out -and (Test-Path -LiteralPath $out)) { return $out }
} catch { }
}
$p = Get-Command python.exe -ErrorAction SilentlyContinue
if ($p) { return $p.Source }
throw @(
"Python 3.10+ not found (tried 'py -3' and 'python'). Install, then re-run:",
" winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements",
"Or https://www.python.org/downloads/ (enable 'Add python.exe to PATH')."
) -join "`n"
}
function Get-GitCommit {
try {
$c = (git -C (Get-RepoRoot) rev-parse --short HEAD 2>$null).Trim()
if ($c) { return $c }
} catch { }
return "unknown"
}
function Get-ReleaseVersion {
param([string]$Override = "")
if ($Override) { return $Override.Trim() }
$root = Get-RepoRoot
try {
$tag = (git -C $root describe --tags --exact-match 2>$null).Trim()
if ($tag) { return $tag.TrimStart("v") }
} catch { }
try {
$desc = (git -C $root describe --tags --always --dirty 2>$null).Trim()
if ($desc) { return ($desc -replace '^v', '') }
} catch { }
$py = Resolve-PythonExe
$v = (& $py -c "from proxy_chain_manager import __version__; print(__version__)" 2>$null).Trim()
if ($v) { return $v }
return "0.0.0-dev"
}
function Install-BuildDependencies {
param([string]$PythonExe, [string]$Root)
Write-Host "`n== pip (upgrade) =="
& $PythonExe -m pip install --upgrade pip
Write-Host "`n== runtime dependencies =="
& $PythonExe -m pip install -r (Join-Path $Root "requirements.txt")
Write-Host "`n== build dependencies =="
& $PythonExe -m pip install -r (Join-Path $Root "dev-requirements.txt")
}
function Invoke-UnitTests {
param([string]$PythonExe, [string]$Root)
Write-Host "`n== compileall =="
& $PythonExe -m compileall -q (Join-Path $Root "proxy_chain_manager")
if ($LASTEXITCODE -ne 0) { throw "compileall failed (exit $LASTEXITCODE)" }
Write-Host "`n== unittest discover =="
Push-Location $Root
try {
& $PythonExe -m unittest discover -s tests -v
if ($LASTEXITCODE -ne 0) { throw "unittest failed (exit $LASTEXITCODE)" }
} finally {
Pop-Location
}
}
function Invoke-StageBundledGost {
param([string]$Root)
Write-Host "`n== Stage bundled GOST =="
$prep = Join-Path $Root "scripts\prepare_bundled_gost.ps1"
& powershell -NoProfile -ExecutionPolicy Bypass -File $prep
if ($LASTEXITCODE -ne 0) { throw "prepare_bundled_gost.ps1 failed (exit $LASTEXITCODE)" }
}
function Invoke-GenerateVersionInfo {
param(
[string]$PythonExe,
[string]$Root,
[string]$Version,
[string]$Commit = ""
)
Write-Host "`n== Version metadata ($Version) =="
$gen = Join-Path $Root "scripts\generate_version_info.py"
$out = Join-Path $Root "build\version_info.txt"
$args = @($gen, "--version", $Version, "--out", $out)
if ($Commit) { $args += @("--commit", $Commit) }
& $PythonExe @args
if ($LASTEXITCODE -ne 0) { throw "generate_version_info.py failed (exit $LASTEXITCODE)" }
}
function Invoke-PyInstallerBuild {
param([string]$PythonExe, [string]$Root)
Write-Host "`n== PyInstaller (ProxyChainManager.spec) =="
$spec = Join-Path $Root "ProxyChainManager.spec"
if (-not (Test-Path $spec)) { throw "Spec not found: $spec" }
& $PythonExe -m PyInstaller --noconfirm --clean $spec
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed (exit $LASTEXITCODE)" }
$exe = Join-Path $Root "dist\ProxyChainManager.exe"
if (-not (Test-Path $exe)) { throw "Build failed: missing $exe" }
return $exe
}
function Invoke-CodeSign {
param([string]$ExePath)
$cert = $env:SIGN_CERT_PATH
if (-not $cert) {
Write-Host " (signing skipped — set SIGN_CERT_PATH to a .pfx to Authenticode-sign)"
return
}
if (-not (Test-Path $cert)) { throw "SIGN_CERT_PATH not found: $cert" }
$signtool = "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe"
if (-not (Test-Path $signtool)) {
$signtool = (Get-Command signtool.exe -ErrorAction SilentlyContinue).Source
}
if (-not $signtool) { throw "signtool.exe not found — install Windows SDK" }
Write-Host "`n== Authenticode sign =="
$args = @("sign", "/fd", "SHA256", "/f", $cert, "/tr", "http://timestamp.digicert.com", "/td", "SHA256")
if ($env:SIGN_CERT_PASSWORD) {
$args += @("/p", $env:SIGN_CERT_PASSWORD)
}
$args += $ExePath
& $signtool @args
if ($LASTEXITCODE -ne 0) { throw "signtool sign failed (exit $LASTEXITCODE)" }
}
function Write-ExeHashSidecar {
param([string]$ExePath)
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $ExePath).Hash.ToLower()
$sidecar = "$ExePath.sha256"
"$hash *$(Split-Path -Leaf $ExePath)" | Out-File -FilePath $sidecar -Encoding ascii -Force
Write-Host "SHA256: $hash -> $sidecar"
return @{ Hash = $hash; Sidecar = $sidecar }
}
function Write-Sbom {
param(
[string]$PythonExe,
[string]$OutDir,
[string]$Version
)
Write-Host "`n== SBOM =="
$freeze = Join-Path $OutDir "requirements-frozen.txt"
& $PythonExe -m pip freeze | Out-File -FilePath $freeze -Encoding utf8
$sbom = Join-Path $OutDir "SBOM.json"
$pkgs = @()
Get-Content $freeze | ForEach-Object {
if ($_ -match '^([^=]+)==(.+)$') {
$pkgs += @{ name = $Matches[1]; version = $Matches[2] }
}
}
$manifest = @{
product = "Proxy God"
version = $Version
generated = (Get-Date).ToUniversalTime().ToString("o")
python = (& $PythonExe -c "import sys; print(sys.version.split()[0])").Trim()
packages = $pkgs
}
($manifest | ConvertTo-Json -Depth 4) | Out-File -FilePath $sbom -Encoding utf8
Write-Host "Wrote $sbom and $freeze"
return @{ Sbom = $sbom; Freeze = $freeze }
}
function Write-ReleaseManifest {
param(
[string]$OutDir,
[string]$Version,
[string]$Commit,
[string]$ExePath,
[string]$Sha256,
[hashtable]$Extra = @{}
)
$manifest = @{
product = "Proxy God"
executable = "ProxyChainManager.exe"
version = $Version
git_commit = $Commit
built_at_utc = (Get-Date).ToUniversalTime().ToString("o")
platform = "windows-amd64"
sha256 = $Sha256
uac_admin = $true
bundled_gost = $true
pyinstaller = "6.10.0"
}
foreach ($k in $Extra.Keys) { $manifest[$k] = $Extra[$k] }
$path = Join-Path $OutDir "RELEASE_MANIFEST.json"
($manifest | ConvertTo-Json -Depth 4) | Out-File -FilePath $path -Encoding utf8
Write-Host "Wrote $path"
return $path
}
function New-ReleaseZip {
param(
[string]$ReleaseDir,
[string]$Version,
[string]$Root
)
$zipName = "ProxyGod-v$Version-windows-amd64.zip"
$zipPath = Join-Path $Root "releases\$zipName"
$releasesRoot = Split-Path $zipPath -Parent
if (-not (Test-Path $releasesRoot)) {
New-Item -ItemType Directory -Path $releasesRoot | Out-Null
}
if (Test-Path $zipPath) { Remove-Item -LiteralPath $zipPath -Force }
Compress-Archive -Path (Join-Path $ReleaseDir "*") -DestinationPath $zipPath -Force
Write-Host "Release zip: $zipPath"
return $zipPath
}

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Generate a PyInstaller version-info file for ProxyChainManager.exe.
Usage:
python scripts/generate_version_info.py --version 1.2.3 --out build/version_info.txt
The output is consumed by ProxyChainManager.spec when the file exists.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
def _parse_version(version: str) -> tuple[int, int, int, int]:
parts = re.findall(r"\d+", version)
nums = [int(x) for x in parts[:4]]
while len(nums) < 4:
nums.append(0)
return nums[0], nums[1], nums[2], nums[3]
def build_version_info(version: str, commit: str = "") -> str:
filevers = prodvers = _parse_version(version)
commit_suffix = f" ({commit})" if commit else ""
return f"""# UTF-8
# Generated by scripts/generate_version_info.py — do not edit by hand.
VSVersionInfo(
ffi=FixedFileInfo(
filevers={filevers},
prodvers={prodvers},
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'040904B0',
[StringStruct(u'CompanyName', u'Proxy God'),
StringStruct(u'FileDescription', u'Proxy God — multi-hop proxy chain manager'),
StringStruct(u'FileVersion', u'{version}{commit_suffix}'),
StringStruct(u'InternalName', u'ProxyChainManager'),
StringStruct(u'LegalCopyright', u'MIT License'),
StringStruct(u'OriginalFilename', u'ProxyChainManager.exe'),
StringStruct(u'ProductName', u'Proxy God'),
StringStruct(u'ProductVersion', u'{version}{commit_suffix}')])
]
),
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
]
)
"""
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--version", required=True)
ap.add_argument("--commit", default="")
ap.add_argument("--out", required=True)
args = ap.parse_args()
out = Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(build_version_info(args.version, args.commit), encoding="utf-8")
print(f"Wrote {out}")
if __name__ == "__main__":
main()

108
scripts/release_build.ps1 Normal file
View File

@@ -0,0 +1,108 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Production release build for Proxy God (ProxyChainManager.exe).
.DESCRIPTION
Full pipeline:
1. Resolve Python 3.10+
2. Install pinned runtime + build deps
3. Run compileall + unittest (gate)
4. Stage SHA-verified bundled gost.exe
5. Generate Windows VERSIONINFO for the exe
6. PyInstaller one-file build (ProxyChainManager.spec)
7. Optional Authenticode sign (SIGN_CERT_PATH / SIGN_CERT_PASSWORD)
8. SHA256 sidecar + SBOM + RELEASE_MANIFEST.json
9. Zip release folder -> releases/ProxyGod-v{version}-windows-amd64.zip
.PARAMETER Version
Override semver (default: git tag > git describe > __version__).
.PARAMETER SkipTests
Skip compileall/unittest (not recommended for production).
.PARAMETER SkipSign
Skip Authenticode even if SIGN_CERT_PATH is set.
.PARAMETER NoZip
Skip creating the release zip archive.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\release_build.ps1
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\release_build.ps1 -Version 1.2.0
#>
[CmdletBinding()]
param(
[string]$Version = "",
[switch]$SkipTests,
[switch]$SkipSign,
[switch]$NoZip
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot\_build_common.ps1"
$root = Get-RepoRoot
Set-Location $root
$py = Resolve-PythonExe
Write-Host "Using: $py"
& $py -c "import sys; assert sys.version_info >= (3, 10); print(sys.version)"
$ver = Get-ReleaseVersion -Override $Version
$commit = Get-GitCommit
Write-Host "Release version: $ver (commit $commit)"
Install-BuildDependencies -PythonExe $py -Root $root
if (-not $SkipTests) {
Invoke-UnitTests -PythonExe $py -Root $root
} else {
Write-Warning "Skipping test gate (-SkipTests)"
}
Invoke-StageBundledGost -Root $root
Invoke-GenerateVersionInfo -PythonExe $py -Root $root -Version $ver -Commit $commit
$exe = Invoke-PyInstallerBuild -PythonExe $py -Root $root
if (-not $SkipSign) {
Invoke-CodeSign -ExePath $exe
}
$hashInfo = Write-ExeHashSidecar -ExePath $exe
# Assemble release directory (versioned folder under releases/)
$releaseDir = Join-Path $root "releases\v$ver"
if (Test-Path $releaseDir) { Remove-Item -Recurse -Force $releaseDir }
New-Item -ItemType Directory -Path $releaseDir | Out-Null
Copy-Item -LiteralPath $exe -Destination (Join-Path $releaseDir "ProxyChainManager.exe") -Force
Copy-Item -LiteralPath $hashInfo.Sidecar -Destination (Join-Path $releaseDir "ProxyChainManager.exe.sha256") -Force
Copy-Item -LiteralPath (Join-Path $root "LICENSE") -Destination $releaseDir -ErrorAction SilentlyContinue
Copy-Item -LiteralPath (Join-Path $root "docs\OPERATOR_RUNBOOK.md") -Destination $releaseDir -ErrorAction SilentlyContinue
$sbom = Write-Sbom -PythonExe $py -OutDir $releaseDir -Version $ver
Write-ReleaseManifest -OutDir $releaseDir -Version $ver -Commit $commit `
-ExePath $exe -Sha256 $hashInfo.Hash -Extra @{
sbom_file = (Split-Path -Leaf $sbom.Sbom)
requirements_frozen = (Split-Path -Leaf $sbom.Freeze)
}
# Also refresh dist/ sidecars for developers
Copy-Item -LiteralPath $hashInfo.Sidecar -Destination (Join-Path $root "dist\ProxyChainManager.exe.sha256") -Force
if (-not $NoZip) {
$zip = New-ReleaseZip -ReleaseDir $releaseDir -Version $ver -Root $root
Write-Host "`n== Release complete =="
Write-Host " Folder: $releaseDir"
Write-Host " Zip: $zip"
} else {
Write-Host "`n== Release complete (no zip) =="
Write-Host " Folder: $releaseDir"
}
Write-Host "`nVerify:"
Write-Host " Get-FileHash -Algorithm SHA256 releases\v$ver\ProxyChainManager.exe"

View File

@@ -1,75 +1,48 @@
#Requires -Version 5.1 #Requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
After git pull: upgrade pip, install deps, build ProxyChainManager.exe, copy to Desktop, create "Proxy God.lnk". Developer build: deps, tests (optional), PyInstaller, copy to Desktop, shortcut.
.DESCRIPTION .DESCRIPTION
Requires Python 3.10+ on PATH (python.exe) or the Windows py launcher (py -3). Lighter than release_build.ps1 — skips SBOM/zip/manifest. For daily dev iteration.
Does not auto-install Python; see README for winget one-liner if needed. Production releases: use scripts\release_build.ps1 or build_release.bat
#>
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
Set-Location $root
function Resolve-PythonExe { .PARAMETER SkipTests
if (Get-Command py -ErrorAction SilentlyContinue) { Skip unittest gate (faster iteration).
try { #>
$out = (& py -3 -c "import sys; print(sys.executable)" 2>$null).Trim() [CmdletBinding()]
if ($out -and (Test-Path -LiteralPath $out)) { return $out } param([switch]$SkipTests)
} catch { }
} $ErrorActionPreference = "Stop"
$p = Get-Command python.exe -ErrorAction SilentlyContinue . "$PSScriptRoot\_build_common.ps1"
if ($p) { return $p.Source }
throw ( $root = Get-RepoRoot
"Python 3.10+ not found (tried 'py -3' and 'python'). Install, then re-run:`n" + Set-Location $root
" winget install Python.Python.3.12 --accept-package-agreements --accept-source-agreements`n" +
"Or https://www.python.org/downloads/ (enable 'Add python.exe to PATH')."
)
}
$py = Resolve-PythonExe $py = Resolve-PythonExe
Write-Host "Using: $py" Write-Host "Using: $py"
& $py -c "import sys; assert sys.version_info >= (3, 10), 'Need Python 3.10+'; print(sys.version)" & $py -c "import sys; assert sys.version_info >= (3, 10); print(sys.version)"
Write-Host "`n== pip (upgrade) ==" $ver = Get-ReleaseVersion
& $py -m pip install --upgrade pip Install-BuildDependencies -PythonExe $py -Root $root
Write-Host "`n== dependencies + PyInstaller ==" if (-not $SkipTests) {
& $py -m pip install -r "$root\requirements.txt" Invoke-UnitTests -PythonExe $py -Root $root
# Pinned dev dep (PyInstaller version) so builds are reproducible.
& $py -m pip install -r "$root\dev-requirements.txt"
Write-Host "`n== Stage bundled GOST binary =="
& powershell -NoProfile -ExecutionPolicy Bypass -File "$root\scripts\prepare_bundled_gost.ps1"
if ($LASTEXITCODE -ne 0) { throw "prepare_bundled_gost.ps1 failed (exit $LASTEXITCODE)" }
Write-Host "`n== PyInstaller (using ProxyChainManager.spec) =="
# Always build from the spec — it bundles signup_extension/, world_map.png,
# customtkinter assets, and the right hidden imports. Do NOT override with
# CLI flags or the data files will be missing from the .exe.
$spec = Join-Path $root "ProxyChainManager.spec"
if (-not (Test-Path $spec)) { throw "Spec not found: $spec" }
& $py -m PyInstaller --noconfirm --clean $spec
if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed (exit $LASTEXITCODE)" }
$distExe = Join-Path $root "dist\ProxyChainManager.exe"
if (-not (Test-Path $distExe)) {
throw "Build failed: missing $distExe"
} }
# SHA256 sidecar so the release artifact is self-verifiable. Invoke-StageBundledGost -Root $root
$distHashFile = "$distExe.sha256" Invoke-GenerateVersionInfo -PythonExe $py -Root $root -Version $ver -Commit (Get-GitCommit)
$distHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $distExe).Hash.ToLower()
"$distHash *ProxyChainManager.exe" | Out-File -FilePath $distHashFile -Encoding ascii -Force $exe = Invoke-PyInstallerBuild -PythonExe $py -Root $root
Write-Host "SHA256: $distHash$distHashFile" $hashInfo = Write-ExeHashSidecar -ExePath $exe
$desk = [Environment]::GetFolderPath("Desktop") $desk = [Environment]::GetFolderPath("Desktop")
$deskExe = Join-Path $desk "ProxyChainManager.exe" $deskExe = Join-Path $desk "ProxyChainManager.exe"
Copy-Item -LiteralPath $distExe -Destination $deskExe -Force Copy-Item -LiteralPath $exe -Destination $deskExe -Force
Copy-Item -LiteralPath $distHashFile -Destination "$deskExe.sha256" -Force Copy-Item -LiteralPath $hashInfo.Sidecar -Destination "$deskExe.sha256" -Force
Write-Host "Copied: $deskExe (+ .sha256)" Write-Host "Copied: $deskExe (+ .sha256)"
Write-Host "`n== Desktop shortcut ==" Write-Host "`n== Desktop shortcut =="
& powershell -NoProfile -ExecutionPolicy Bypass -File "$root\scripts\create_desktop_shortcut.ps1" & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $root "scripts\create_desktop_shortcut.ps1")
Write-Host "`nDone. Launch from Desktop: Proxy God.lnk (or ProxyChainManager.exe)" Write-Host "`nDone. Launch from Desktop: Proxy God.lnk (or ProxyChainManager.exe)"