Run Windows release orchestration on Forgejo desktop-release
Some checks failed
Windows Release / windows-x86_64 (push) Has been cancelled

Tower runner dispatches/waits on the private GitHub MSVC builder and
verifies the public windows-continuous Forgejo release. Needs
GH_BUILDER_TOKEN on the Forgejo repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
copilot 2026-11-07 15:50:33 +01:00
parent 3140f9bb55
commit 166778d451
2 changed files with 111 additions and 164 deletions

View file

@ -1,7 +1,8 @@
name: Windows Release
# Continuous Windows portable build on every push to main.
# Needs self-hosted runner label `windows-release` (MSVC 2022).
# Forgejo-orchestrated Windows portable zip.
# Tower `desktop-release` triggers the private GitHub builder (MSVC), then
# verifies the public Forgejo release tag `windows-continuous`.
# See docs/windows-runner.md
on:
@ -18,30 +19,18 @@ concurrency:
jobs:
windows-x86_64:
runs-on: windows-release
timeout-minutes: 240
runs-on: desktop-release
timeout-minutes: 360
steps:
- name: Checkout
shell: bash
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
rm -rf .git
git init .
origin_url="${GITHUB_SERVER_URL#https://}"
origin_url="${origin_url#http://}"
git remote add origin "https://oauth2:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git"
git -c http.sslVerify=false fetch --depth=1 origin "${GITHUB_SHA}"
git checkout --force FETCH_HEAD
git submodule update --init --recursive
- name: Resolve version
- name: Resolve expected asset
id: ver
shell: bash
run: |
set -euo pipefail
ver="$(grep -E 'CURRENT_VERSION_STRING' src/VersionConstants.hpp | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
# Shallow metadata from the triggering commit (no full Windows checkout).
ver="$(curl -fsSL -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/contents/src/VersionConstants.hpp?ref=${GITHUB_SHA}" \
| python3 -c "import sys,json,base64,re; d=json.load(sys.stdin); t=base64.b64decode(d['content']).decode(); print(re.search(r'CURRENT_VERSION_STRING\s*=\s*\"([^\"]+)\"', t).group(1))")"
short_sha="$(echo "${GITHUB_SHA}" | cut -c1-7)"
asset_ver="${ver}+${short_sha}"
zip_name="infinipaint-${asset_ver}-win-x86_64-portable.zip"
@ -50,129 +39,87 @@ jobs:
echo "tag=windows-continuous"
echo "asset_ver=${asset_ver}"
echo "zip_name=${zip_name}"
echo "short_sha=${short_sha}"
} >> "$GITHUB_OUTPUT"
echo "Expect Forgejo asset: ${zip_name}"
- name: Ensure Conan
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
if (-not (Get-Command conan -ErrorAction SilentlyContinue)) {
python -m pip install --user "conan>=2,<3"
Get-ChildItem "$env:USERPROFILE\AppData\Roaming\Python" -Recurse -Filter conan.exe -ErrorAction SilentlyContinue |
ForEach-Object { $env:Path = "$($_.DirectoryName);$env:Path" }
}
conan --version
- name: Build Windows x86_64
shell: cmd
run: |
call windowsinstall\conan_init_x86_64.bat
if errorlevel 1 exit /b 1
call windowsinstall\build_x86_64.bat
if errorlevel 1 exit /b 1
- 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 'Release' } |
Select-Object -First 1
if (-not $exe) { throw "infinipaint.exe not found under 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")
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
- name: Publish Forgejo release
- name: Dispatch or reuse GitHub Windows build
id: gh
shell: bash
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_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_CURL_INSECURE: "true"
GH_BUILDER_TOKEN: ${{ secrets.GH_BUILDER_TOKEN }}
WANT_SHA: ${{ github.sha }}
run: |
set -euo pipefail
zip_path="dist/${ZIP_NAME}"
test -f "$zip_path"
command -v jq >/dev/null
command -v curl >/dev/null
curl_tls=()
if [ "${FORGEJO_CURL_INSECURE}" = "true" ]; then
curl_tls+=(--insecure)
if [ -z "${GH_BUILDER_TOKEN:-}" ]; then
echo "GH_BUILDER_TOKEN secret missing on Forgejo repo" >&2
exit 1
fi
api_base="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases"
body=$(printf '%s\n' \
"Continuous Windows build from \`main\`." \
"" \
"- Version: **${VERSION}**" \
"- Commit: \`${GITHUB_SHA}\`" \
"- Asset: \`${ZIP_NAME}\`" \
"" \
"Updated on every push to \`main\`. Requires runner label \`windows-release\` (see docs/windows-runner.md).")
payload=$(jq -n \
--arg tag "$VERSION_TAG" \
--arg name "Windows continuous (${ASSET_VER})" \
--arg body "$body" \
'{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:true}')
tag_enc=$(jq -nr --arg t "$VERSION_TAG" '$t|@uri')
resp_file=$(mktemp)
status=$(curl "${curl_tls[@]}" -sS -o "$resp_file" -w '%{http_code}' \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"${api_base}/tags/${tag_enc}")
release_id=""
if [ "$status" = "200" ]; then
release_id=$(jq -r '.id' < "$resp_file")
curl "${curl_tls[@]}" -sS -X PATCH \
-H "Authorization: token ${FORGEJO_TOKEN}" \
api="https://api.github.com/repos/littlecodedragon/infinipaint"
auth=(-H "Authorization: Bearer ${GH_BUILDER_TOKEN}" -H "Accept: application/vnd.github+json")
gh_sha=$(curl -fsSL "${auth[@]}" "${api}/commits/main" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha'])")
if [ "$gh_sha" != "$WANT_SHA" ]; then
echo "WARN: GitHub main=${gh_sha:0:7} != Forgejo ${WANT_SHA:0:7} — keep remotes synced (push both)." >&2
fi
runs=$(curl -fsSL "${auth[@]}" "${api}/actions/workflows/windows_release.yml/runs?branch=main&per_page=15")
run_id=$(echo "$runs" | python3 -c "
import sys, json, os
want = os.environ['WANT_SHA']
runs = json.load(sys.stdin).get('workflow_runs', [])
# Prefer live or successful run for this SHA.
for r in runs:
if r.get('head_sha') != want:
continue
st, conc = r.get('status'), r.get('conclusion')
if st in ('queued', 'in_progress') or (st == 'completed' and conc == 'success'):
print(r['id'])
break
")
if [ -z "${run_id}" ]; then
echo "No usable GitHub run for ${WANT_SHA:0:7} — dispatching workflow_dispatch"
code=$(curl -sS -o /tmp/gh-dispatch.json -w '%{http_code}' -X POST "${auth[@]}" \
-H "Content-Type: application/json" \
-d "$payload" \
"${api_base}/${release_id}" >/dev/null
jq -r --arg n "$ZIP_NAME" '.assets[]? | select(.name==$n) | .id' < "$resp_file" | while read -r aid; do
[ -n "$aid" ] || continue
curl "${curl_tls[@]}" -sS -X DELETE \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"${api_base}/${release_id}/assets/${aid}" >/dev/null || true
"${api}/actions/workflows/windows_release.yml/dispatches" \
-d '{"ref":"main"}')
[ "$code" = "204" ] || { cat /tmp/gh-dispatch.json; exit 1; }
sleep 8
runs=$(curl -fsSL "${auth[@]}" "${api}/actions/workflows/windows_release.yml/runs?branch=main&per_page=5")
run_id=$(echo "$runs" | python3 -c "import sys,json; print(json.load(sys.stdin)['workflow_runs'][0]['id'])")
fi
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
echo "Tracking GitHub run ${run_id}"
- name: Wait for GitHub build + Forgejo public release
shell: bash
env:
GH_BUILDER_TOKEN: ${{ secrets.GH_BUILDER_TOKEN }}
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ steps.gh.outputs.run_id }}
ZIP_NAME: ${{ steps.ver.outputs.zip_name }}
ASSET_VER: ${{ steps.ver.outputs.asset_ver }}
SHORT_SHA: ${{ steps.ver.outputs.short_sha }}
run: |
set -euo pipefail
api="https://api.github.com/repos/littlecodedragon/infinipaint"
auth=(-H "Authorization: Bearer ${GH_BUILDER_TOKEN}" -H "Accept: application/vnd.github+json")
fj="https://forgejo.fsociety00.cc/api/v1/repos/copilot/infinipaint/releases/tags/windows-continuous"
for i in $(seq 1 240); do
g=$(curl -fsSL "${auth[@]}" "${api}/actions/runs/${RUN_ID}")
status=$(echo "$g" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('status'), d.get('conclusion') or '-')")
rel=$(curl -fsSL -H "Authorization: token ${FORGEJO_TOKEN}" "$fj" || echo '{}')
rname=$(echo "$rel" | python3 -c "import sys,json;print(json.load(sys.stdin).get('name',''))" 2>/dev/null || true)
assets=$(echo "$rel" | python3 -c "import sys,json;print([a.get('name') for a in json.load(sys.stdin).get('assets',[])])" 2>/dev/null || true)
echo "$(date -u +%H:%M:%S) github=${status} forgejo=${rname} assets=${assets}"
if echo "$assets" | grep -Fq "${ZIP_NAME}" || echo "$rname" | grep -Fq "${SHORT_SHA}"; then
echo "Public Forgejo release is up to date."
exit 0
fi
if echo "$status" | grep -q '^completed failure\|^completed cancelled\|^completed timed_out'; then
echo "GitHub Windows build failed: ${status}" >&2
echo "$g" | python3 -c "import sys,json;print(json.load(sys.stdin).get('html_url',''))"
exit 1
fi
sleep 60
done
elif [ "$status" = "404" ]; then
status=$(curl "${curl_tls[@]}" -sS -o "$resp_file" -w '%{http_code}' -X POST \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" \
"$api_base")
[ "$status" = "200" ] || [ "$status" = "201" ] || { cat "$resp_file"; exit 1; }
release_id=$(jq -r '.id' < "$resp_file")
else
cat "$resp_file"; exit 1
fi
name_enc=$(jq -nr --arg n "$ZIP_NAME" '$n|@uri')
curl "${curl_tls[@]}" -sS -X POST \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${zip_path}" \
"${api_base}/${release_id}/assets?name=${name_enc}" \
| jq -r '.browser_download_url // .name // .'
echo "Timed out waiting for Forgejo asset ${ZIP_NAME}" >&2
exit 1

View file

@ -1,10 +1,15 @@
# Windows continuous release (GitHub Actions as private builder)
# Windows continuous release (Forgejo → private GitHub builder)
Native InfiniPaint Windows builds need **MSVC** (see `docs/BUILDING.md` +
`windowsinstall/`). There is no Linux cross-build path.
Policy: **Forgejo is the public fork**; GitHub is only a **private** mirror used
to run `windows-latest` CI. Friends download the zip from Forgejo — not GitHub.
Policy:
- **Forgejo** (`copilot/infinipaint`) is the **public** fork and download site.
- **GitHub** (`littlecodedragon/infinipaint`) is a **private** builder mirror only
(`windows-latest` MSVC). Must stay private.
- Tower **`desktop-release`** runs the Forgejo workflow: it dispatches/waits on
the private GitHub build, then checks the public Forgejo release.
## Public download (friends)
@ -12,36 +17,31 @@ https://forgejo.fsociety00.cc/copilot/infinipaint/releases/tag/windows-continuou
Portable zip → extract → run `infinipaint.exe`.
## Workflow
## Workflows
File: `.github/workflows/windows_release.yml`
| File | Runner | Role |
|------|--------|------|
| `.forgejo/workflows/windows_release.yml` | `desktop-release` (Tower) | Orchestrate + verify Forgejo release |
| `.github/workflows/windows_release.yml` | `windows-latest` (private) | Compile + upload Forgejo asset |
| | |
|---|---|
| Trigger | every push to `main` on the private GitHub mirror, plus `workflow_dispatch` |
| Runner | `windows-latest` (GitHub-hosted, MSVC ~195 / VS 18) |
| Build | Conan profile `win-ci` + **Ninja** (avoids CMake&lt;4 vs `Visual Studio 18 2026`) |
| Output | public Forgejo prerelease **`windows-continuous`** (+ private GitHub copy) |
Forgejo repo secret: **`GH_BUILDER_TOKEN`** (GitHub PAT with `actions:write` /
`contents:read` on the private mirror).
Required GitHub Actions secret: `FORGEJO_TOKEN` (Forgejo PAT with release write on
GitHub Actions secret: **`FORGEJO_TOKEN`** (Forgejo PAT with release write on
`copilot/infinipaint`).
Local Tower Windows can keep using `windowsinstall/conan_init_x86_64.bat`
(`conan/profiles/win-x86_64`, MSVC 194). CI generates a `win-ci` profile for
whatever MSVC `windows-latest` ships.
## Tray
## GitHub remote (private builder)
Tower CI job tray (`ci-runner-job-tray.py`) tracks:
- `Forgejo desktop-release` — host unit (orchestrator + APK jobs)
- `Forgejo InfiniPaint Windows` — Forgejo Actions run for this workflow
## Sync remotes
```bash
git remote add github https://github.com/littlecodedragon/infinipaint.git
git push -u github main
git push origin main # Forgejo (public)
git push github main # private builder
```
Keep the repo **private** (Settings → Change visibility, or
`gh repo edit littlecodedragon/infinipaint --visibility private`).
## Forgejo note
`.forgejo/workflows/windows_release.yml` still targets a self-hosted
`windows-release` runner (optional). Primary public zips come from the GitHub
builder → Forgejo release upload above.
Keep GitHub **private**.