diff --git a/.forgejo/workflows/windows_release.yml b/.forgejo/workflows/windows_release.yml index 525c04d..8ab39d6 100644 --- a/.forgejo/workflows/windows_release.yml +++ b/.forgejo/workflows/windows_release.yml @@ -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}" \ - -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 - 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 + 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" \ + "${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_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 // .' + - 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 + echo "Timed out waiting for Forgejo asset ${ZIP_NAME}" >&2 + exit 1 diff --git a/docs/windows-runner.md b/docs/windows-runner.md index d071a10..1b8c97f 100644 --- a/docs/windows-runner.md +++ b/docs/windows-runner.md @@ -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<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**.