infinipaint/.github/workflows/windows_release.yml
copilot 1c1d63f376
Some checks are pending
Windows Release / windows-x86_64 (push) Waiting to run
Ship 0.8 fog/freistellen and fix Windows CI for friends
Publish the portable zip to public Forgejo from the private GitHub
builder, and force Ninja so MSVC 195 deps build without CMake 4.2.
2026-10-25 18:07:47 +01:00

246 lines
10 KiB
YAML

name: Windows Release
# Continuous Windows portable build on every push to main.
# Uses GitHub-hosted windows-latest (MSVC) as a *private* builder.
# Public download: Forgejo release tag windows-continuous.
# See docs/windows-runner.md
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: write
concurrency:
group: infinipaint-windows-release
cancel-in-progress: true
jobs:
windows-x86_64:
runs-on: windows-latest
timeout-minutes: 360
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Setup Ninja
uses: seanmiddleditch/gha-setup-ninja@v5
- name: Install Conan
shell: pwsh
run: |
python -m pip install --upgrade pip
python -m pip install "conan>=2,<3"
conan --version
# Fresh runners: detect MSVC (windows-latest is currently 195 / VS 18).
conan profile detect --force
$default = Join-Path $env:USERPROFILE ".conan2\profiles\default"
$verLine = Select-String -Path $default -Pattern 'compiler.version=' | Select-Object -First 1
$msvcVer = ($verLine.Line -split '=')[1].Trim()
if (-not $msvcVer) { throw "could not read compiler.version from default profile" }
# Host profile matching the runner (repo win-x86_64 still pins 194 for local Tower).
# Force Ninja: recipes that tool_require cmake<4 cannot use "Visual Studio 18 2026"
# (needs CMake >= 4.2). Ninja works with older CMake + MSVC 195.
$p = Join-Path $env:USERPROFILE ".conan2\profiles\win-ci"
@"
[settings]
arch=x86_64
build_type=Release
compiler=msvc
compiler.cppstd=23
compiler.runtime=dynamic
compiler.version=$msvcVer
os=Windows
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
"@.Trim() | Set-Content -Path $p -Encoding ascii
Write-Host "win-ci MSVC version=$msvcVer (generator=Ninja)"
Get-Content $p
ninja --version
- name: Resolve version
id: ver
shell: bash
run: |
set -euo pipefail
ver="$(grep -E 'CURRENT_VERSION_STRING' src/VersionConstants.hpp | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
short_sha="$(echo "${GITHUB_SHA}" | cut -c1-7)"
asset_ver="${ver}+${short_sha}"
zip_name="infinipaint-${asset_ver}-win-x86_64-portable.zip"
{
echo "version=${ver}"
echo "tag=windows-continuous"
echo "asset_ver=${asset_ver}"
echo "zip_name=${zip_name}"
} >> "$GITHUB_OUTPUT"
- name: Build Windows x86_64
shell: cmd
run: |
call .\conan\export_libs.bat
if errorlevel 1 exit /b 1
conan install . -of=build-x86_64 --build=missing -pr:h=win-ci -pr:b=default
if errorlevel 1 exit /b 1
if not exist build-x86_64\build mkdir build-x86_64\build
cd build-x86_64\build
call .\generators\conanbuild.bat
cmake ..\.. -G Ninja -DCMAKE_TOOLCHAIN_FILE="generators\conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release -DCONFIG_NEXT_TO_EXECUTABLE=OFF
if errorlevel 1 exit /b 1
cmake --build . --config Release
if errorlevel 1 exit /b 1
call .\generators\deactivate_conanbuild.bat
- name: Package portable zip
id: pkg
shell: pwsh
env:
ZIP_NAME: ${{ steps.ver.outputs.zip_name }}
run: |
$ErrorActionPreference = "Stop"
$exe = Get-ChildItem -Path . -Recurse -Filter infinipaint.exe |
Where-Object { $_.FullName -match 'build-x86_64|Release' } |
Select-Object -First 1
if (-not $exe) { throw "infinipaint.exe not found under build-x86_64/Release" }
$outDir = Join-Path (Get-Location) "dist\portable"
Remove-Item -Recurse -Force $outDir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
Copy-Item $exe.FullName (Join-Path $outDir "infinipaint.exe")
$dllDir = Join-Path (Get-Location) "windowsinstall\dlls-x86_64"
if (Test-Path $dllDir) {
Copy-Item (Join-Path $dllDir "*.dll") $outDir -ErrorAction SilentlyContinue
}
$dataSrc = Join-Path (Get-Location) "assets\data"
if (-not (Test-Path $dataSrc)) { $dataSrc = Join-Path (Get-Location) "data" }
Copy-Item -Recurse $dataSrc (Join-Path $outDir "data")
$rembgSrc = Join-Path (Get-Location) "third_party\rembg"
if (Test-Path $rembgSrc) {
Copy-Item -Recurse $rembgSrc (Join-Path $outDir "rembg")
}
$modelsSrc = Join-Path (Get-Location) "third_party\rembg-home"
if (Test-Path $modelsSrc) {
Copy-Item -Recurse $modelsSrc (Join-Path $outDir "rembg-home")
}
New-Item -ItemType Directory -Force -Path "dist" | Out-Null
$zipPath = Join-Path (Get-Location) "dist\$env:ZIP_NAME"
if (Test-Path $zipPath) { Remove-Item $zipPath }
Compress-Archive -Path (Join-Path $outDir "*") -DestinationPath $zipPath
"zip_path=$zipPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
Get-Item $zipPath | Format-List FullName, Length
# Optional private artifact on GitHub (repo is private builder-only).
- name: Publish private GitHub release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.tag }}
name: Windows continuous (${{ steps.ver.outputs.asset_ver }})
prerelease: true
body: |
Continuous Windows build from `main` (private GitHub builder).
**Public download:** Forgejo release `windows-continuous`
https://forgejo.fsociety00.cc/copilot/infinipaint/releases/tag/windows-continuous
- Version: **${{ steps.ver.outputs.version }}**
- Commit: `${{ github.sha }}`
- Asset: `${{ steps.ver.outputs.zip_name }}`
files: dist/${{ steps.ver.outputs.zip_name }}
fail_on_unmatched_files: true
- name: Publish Forgejo release (public)
shell: bash
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
VERSION_TAG: ${{ steps.ver.outputs.tag }}
ASSET_VER: ${{ steps.ver.outputs.asset_ver }}
VERSION: ${{ steps.ver.outputs.version }}
ZIP_NAME: ${{ steps.ver.outputs.zip_name }}
FORGEJO_API: https://forgejo.fsociety00.cc/api/v1
FORGEJO_REPO: copilot/infinipaint
run: |
set -euo pipefail
if [ -z "${FORGEJO_TOKEN:-}" ]; then
echo "FORGEJO_TOKEN secret missing — cannot publish public Forgejo release" >&2
exit 1
fi
zip_path="dist/${ZIP_NAME}"
test -f "$zip_path"
python3 - <<'PY'
import json, os, ssl, urllib.error, urllib.parse, urllib.request
api = os.environ["FORGEJO_API"].rstrip("/")
repo = os.environ["FORGEJO_REPO"]
token = os.environ["FORGEJO_TOKEN"]
tag = os.environ["VERSION_TAG"]
name = f"Windows continuous ({os.environ['ASSET_VER']})"
zip_name = os.environ["ZIP_NAME"]
zip_path = f"dist/{zip_name}"
body = "\n".join([
"Continuous Windows build from `main` (built on private GitHub Actions).",
"",
f"- Version: **{os.environ['VERSION']}**",
f"- Commit: `{os.environ['GITHUB_SHA']}`",
f"- Asset: `{zip_name}`",
"",
"Updated on every push to `main`.",
])
ctx = ssl.create_default_context()
if os.environ.get("FORGEJO_CURL_INSECURE", "").lower() in ("1", "true"):
ctx = ssl._create_unverified_context()
def req(method, url, data=None, headers=None, raw=False, allow_404=False):
h = {"Authorization": f"token {token}"}
if headers:
h.update(headers)
body_data = data
if data is not None and not raw:
body_data = json.dumps(data).encode()
h.setdefault("Content-Type", "application/json")
r = urllib.request.Request(url, data=body_data, headers=h, method=method)
try:
with urllib.request.urlopen(r, context=ctx) as resp:
raw_body = resp.read()
return resp.status, (json.loads(raw_body) if raw_body else {})
except urllib.error.HTTPError as e:
err = e.read().decode(errors="replace")
if allow_404 and e.code == 404:
return 404, {}
raise SystemExit(f"{method} {url} -> {e.code}: {err}") from e
base = f"{api}/repos/{repo}/releases"
status, release = req("GET", f"{base}/tags/{urllib.parse.quote(tag)}", allow_404=True)
payload = {
"tag_name": tag,
"name": name,
"body": body,
"draft": False,
"prerelease": True,
}
if status == 404:
_, release = req("POST", base, payload)
else:
release_id = release["id"]
_, release = req("PATCH", f"{base}/{release_id}", payload)
for asset in release.get("assets") or []:
if asset.get("name") == zip_name:
req("DELETE", f"{base}/{release_id}/assets/{asset['id']}")
release_id = release["id"]
with open(zip_path, "rb") as f:
data = f.read()
upload_url = f"{base}/{release_id}/assets?name={urllib.parse.quote(zip_name)}"
_, asset = req(
"POST", upload_url, data=data, raw=True,
headers={"Content-Type": "application/octet-stream"},
)
print(asset.get("browser_download_url") or asset.get("name") or asset)
PY