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 rem Ninja + cmake_layout puts generators under build/Release/generators call build-x86_64\build\Release\generators\conanbuild.bat if errorlevel 1 exit /b 1 cmake --preset conan-release -DCONFIG_NEXT_TO_EXECUTABLE=OFF if errorlevel 1 exit /b 1 cmake --build --preset conan-release if errorlevel 1 exit /b 1 call build-x86_64\build\Release\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