From acbd53c69bb9d0a15b6341e18f55c110d2be0443 Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:34:05 +0200 Subject: [PATCH 1/9] ci: move required build gate to Linux --- .github/workflows/ci.yml | 111 +++++++++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6adab2efa1..dbf07fa264 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: pull-requests: read outputs: compile: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.compile }} + fast: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.fast }} + lua: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group') && 'true' || steps.filter.outputs.lua }} steps: - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -49,10 +51,43 @@ jobs: - "CMakeLists.txt" - "CMakePresets.json" - ".github/workflows/ci.yml" - - ".github/workflows/reusable-build-windows.yml" + - ".github/workflows/reusable-build-linux.yml" + fast: + - "src/**" + - "tests/**" + - "modules/**" + - "mods/**" + - "data/**" + - "cmake/**" + - "vc18/**" + - "vcpkg.json" + - "vcpkg-configuration.json" + - "CMakeLists.txt" + - "CMakePresets.json" + - ".github/**" + - ".yamllint.yaml" + - "**/*.lua" + - "**/*.xml" + lua: + - "src/**" + - "tests/**" + - "modules/**" + - "mods/**" + - "data/**" + - "cmake/**" + - "vc18/**" + - "vcpkg.json" + - "vcpkg-configuration.json" + - "CMakeLists.txt" + - "CMakePresets.json" + - ".github/workflows/ci.yml" + - ".github/workflows/reusable-build-linux.yml" + - ".github/workflows/reusable-tests-lua.yml" checks: name: Fast Checks + needs: changes + if: needs.changes.outputs.fast == 'true' permissions: contents: read checks: write @@ -61,18 +96,20 @@ jobs: tests-lua: name: Lua Syntax + needs: changes + if: needs.changes.outputs.lua == 'true' permissions: contents: read uses: ./.github/workflows/reusable-tests-lua.yml - build-windows: - name: Build - Windows + build-linux: + name: Build - Linux needs: [changes, checks, tests-lua] if: needs.changes.outputs.compile == 'true' && needs.checks.result == 'success' && needs.tests-lua.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) permissions: contents: read packages: read - uses: ./.github/workflows/reusable-build-windows.yml + uses: ./.github/workflows/reusable-build-linux.yml required: name: CI / Required @@ -81,7 +118,7 @@ jobs: - changes - checks - tests-lua - - build-windows + - build-linux runs-on: ubuntu-latest permissions: {} steps: @@ -100,31 +137,63 @@ jobs: jobs = json.loads(os.environ["NEEDS_JSON"]) rejected = {} - for name in ("changes", "checks", "tests-lua"): - result = jobs[name].get("result") - if result != "success": - rejected[name] = f"expected success, got {result}" + changes_result = jobs["changes"].get("result") + if changes_result != "success": + rejected["changes"] = f"expected success, got {changes_result}" - compile_scope = jobs["changes"].get("outputs", {}).get("compile") - if compile_scope not in {"true", "false"}: - rejected["scope:compile"] = ( - f"expected true or false, got {compile_scope!r}" - ) + outputs = jobs["changes"].get("outputs", {}) + scopes = { + "compile": outputs.get("compile"), + "fast": outputs.get("fast"), + "lua": outputs.get("lua"), + } + for scope, value in scopes.items(): + if value not in {"true", "false"}: + rejected[f"scope:{scope}"] = ( + f"expected true or false, got {value!r}" + ) + + for job_name, scope_name in (("checks", "fast"), ("tests-lua", "lua")): + result = jobs[job_name].get("result") + scope_value = scopes[scope_name] + if scope_value == "true" and result != "success": + rejected[job_name] = ( + f"scope {scope_name}=true requires success, got {result}" + ) + elif scope_value == "false" and result != "skipped": + rejected[job_name] = ( + f"scope {scope_name}=false requires skipped, got {result}" + ) + + compile_scope = scopes["compile"] + if compile_scope == "true": + for required_scope in ("fast", "lua"): + if scopes[required_scope] != "true": + rejected[f"scope:{required_scope}"] = ( + f"compile=true requires {required_scope}=true, got {scopes[required_scope]!r}" + ) is_draft = os.environ["IS_DRAFT"] == "true" - windows_result = jobs["build-windows"].get("result") - if windows_result not in {"success", "skipped"}: - rejected["build-windows"] = ( - f"unexpected conclusion {windows_result}" + linux_result = jobs["build-linux"].get("result") + if linux_result not in {"success", "skipped"}: + rejected["build-linux"] = ( + f"unexpected conclusion {linux_result}" + ) + elif not is_draft and compile_scope == "true" and linux_result != "success": + rejected["build-linux"] = ( + f"scope compile=true requires success, got {linux_result}" ) - elif not is_draft and compile_scope == "true" and windows_result != "success": - rejected["build-windows"] = ( - f"scope compile=true requires success, got {windows_result}" + elif (is_draft or compile_scope == "false") and linux_result != "skipped": + rejected["build-linux"] = ( + f"draft or compile=false requires skipped, got {linux_result}" ) print("Required job results:") for name in sorted(jobs): print(f"- {name}: {jobs[name].get('result')}") + print("Detected scopes:") + for name in sorted(scopes): + print(f"- {name}: {scopes[name]}") if rejected: for name, reason in sorted(rejected.items()): From 8aee61333e983c002f6c0ba0d0ee051ff14d3981 Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:34:21 +0200 Subject: [PATCH 2/9] ci: avoid retrying superseded runs --- .github/workflows/infrastructure-retry.yml | 30 +++++----------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/.github/workflows/infrastructure-retry.yml b/.github/workflows/infrastructure-retry.yml index b5f79b6d39..0182745d6c 100644 --- a/.github/workflows/infrastructure-retry.yml +++ b/.github/workflows/infrastructure-retry.yml @@ -11,12 +11,12 @@ permissions: {} jobs: retry-once: name: Retry infrastructure failure once - # GitHub aggregates an exact job-level startup_failure as workflow failure; - # the script below proves that classification before issuing a retry. + # Superseded runs are intentionally cancelled by CI concurrency and must not + # create more runner demand. Only a real timeout or a proven startup failure + # is eligible for one automatic retry. if: >- github.event.workflow_run.run_attempt == 1 && - (github.event.workflow_run.conclusion == 'cancelled' || - github.event.workflow_run.conclusion == 'timed_out' || + (github.event.workflow_run.conclusion == 'timed_out' || github.event.workflow_run.conclusion == 'failure') runs-on: ubuntu-latest timeout-minutes: 5 @@ -28,37 +28,19 @@ jobs: env: CONCLUSION: ${{ github.event.workflow_run.conclusion }} GH_TOKEN: ${{ github.token }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} REPOSITORY: ${{ github.repository }} RUN_ID: ${{ github.event.workflow_run.id }} - RUN_NUMBER: ${{ github.event.workflow_run.run_number }} - WORKFLOW_EVENT: ${{ github.event.workflow_run.event }} - WORKFLOW_ID: ${{ github.event.workflow_run.workflow_id }} shell: bash run: | set -euo pipefail - if [[ ! "${RUN_ID}" =~ ^[0-9]+$ || ! "${RUN_NUMBER}" =~ ^[0-9]+$ || ! "${WORKFLOW_ID}" =~ ^[0-9]+$ ]]; then + if [[ ! "${RUN_ID}" =~ ^[0-9]+$ ]]; then echo "::error title=Invalid workflow run metadata::Refusing to issue a retry request." exit 1 fi retry_reason="" - if [[ "${CONCLUSION}" == "cancelled" ]]; then - newer_runs="$( - gh api --method GET \ - "repos/${REPOSITORY}/actions/workflows/${WORKFLOW_ID}/runs" \ - -f branch="${HEAD_BRANCH}" \ - -f event="${WORKFLOW_EVENT}" \ - -F per_page=100 \ - --jq ".workflow_runs | map(select(.run_number > ${RUN_NUMBER})) | length" - )" - if (( newer_runs > 0 )); then - echo "::notice title=Stale cancellation is not retried::A newer CI run already exists for this branch and event." - exit 0 - fi - retry_reason="workflow was cancelled and has not been superseded" - elif [[ "${CONCLUSION}" == "timed_out" ]]; then + if [[ "${CONCLUSION}" == "timed_out" ]]; then # Repository policy intentionally permits one retry for the exact timed_out conclusion. retry_reason="workflow timed out" elif [[ "${CONCLUSION}" == "failure" ]]; then From de0136d05dca8f0042e25f54825dcfebc4dcbf1a Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:35:11 +0200 Subject: [PATCH 3/9] docs: record Linux CI hybrid task --- .../active/OTC-20260816-linux-ci-hybrid.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md diff --git a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md new file mode 100644 index 0000000000..7db2661619 --- /dev/null +++ b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md @@ -0,0 +1,72 @@ +--- +task_id: OTC-20260816-linux-ci-hybrid +status: active +owner: current-agent +branch: ci/OTC-20260816-linux-ci-hybrid +base_branch: main +related_pr: null +feature_scope: infrastructure +completion_claim: internal_only +ownership_released: false +owned_paths: + - .github/workflows/ci.yml + - .github/workflows/infrastructure-retry.yml + - .github/workflows/reusable-build-windows.yml + - docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md + - docs/agents/tasks/archive/OTC-20260816-linux-ci-hybrid.md +--- + +# OTC-20260816 Linux CI hybrid + +## Objective + +Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners while preserving Synology/self-hosted capacity exclusively for work that genuinely needs controlled runtime, LAN, display, or persistent-environment evidence. + +## Coordination + +- Owner explicitly authorized disabling Windows builds because this OTClient deployment uses Linux only. +- PR #328 is closed as superseded; its safe hosted-runner queue reductions are carried forward where applicable without its Windows gate. +- PR #280 remains a separate specialized Synology/runtime lane and its owned files are not modified by this task. + +## Implemented scope + +- Replace the required Windows compile job in `.github/workflows/ci.yml` with the existing GitHub-hosted Linux reusable build. +- Keep ordinary scope detection, fast checks, Lua checks, required aggregation, and Linux builds on GitHub-hosted Ubuntu runners. +- Avoid retrying intentionally cancelled superseded CI runs. +- Remove the reusable Windows build workflow after verifying that the ordinary CI caller is replaced and no active workflow file in the current workflow inventory names another Windows build entry point. +- Do not change the dedicated Synology/Track A runtime workflows. + +## Acceptance inventory + +- [ ] `CI` has no `windows-2025`, `build-windows`, or `reusable-build-windows.yml` dependency. +- [ ] Compile-relevant PRs require `Build - Linux` via `.github/workflows/reusable-build-linux.yml`. +- [ ] Documentation/task-only changes do not allocate unrelated fast/Lua/build runners. +- [ ] Generic CI jobs use GitHub-hosted Ubuntu runners. +- [ ] Dedicated Synology/runtime workflow files remain unchanged. +- [ ] Superseded `cancelled` CI runs are not automatically retried. +- [ ] Workflow validation/actionlint and exact-head required CI pass. +- [ ] Related PRs are terminal: #328 closed superseded; implementation PR merged when green; #280 intentionally remains separate if still active. + +## Validation + +1. Inspect the exact branch diff and workflow references. +2. Verify no Windows build dependency remains in general CI. +3. Open a PR and inspect exact-head Actions jobs/runner labels. +4. Require workflow syntax/actionlint and `CI / Required` success. +5. Merge only on the exact validated head. +6. Archive this task and release ownership after post-merge verification. + +## Runtime E2E + +`NOT_APPLICABLE_WITH_REASON`: this task changes CI routing rather than product runtime behavior. The real GitHub Actions run on the implementation head is the environment/outcome verification. The dedicated Synology runtime lane remains responsible for runtime/display/LAN evidence. + +## Context checkpoint + +```yaml +state: PROVEN +phase: implementation +base_head: a27b9f3383b0555142b31216672e9f0143d2cd3d +superseded_pr: 328 +specialized_runtime_pr: 280 +next_action: remove the Windows reusable build, inspect the exact diff, open the implementation PR, and validate exact-head Actions +``` From 73ec4b8ceb1d42906246f70536cc84b8cdb1d0de Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:35:25 +0200 Subject: [PATCH 4/9] ci: remove unused Windows build workflow --- .github/workflows/reusable-build-windows.yml | 208 ------------------- 1 file changed, 208 deletions(-) delete mode 100644 .github/workflows/reusable-build-windows.yml diff --git a/.github/workflows/reusable-build-windows.yml b/.github/workflows/reusable-build-windows.yml deleted file mode 100644 index bfb1912739..0000000000 --- a/.github/workflows/reusable-build-windows.yml +++ /dev/null @@ -1,208 +0,0 @@ -name: Reusable Windows Build - -on: - workflow_call: - -permissions: - contents: read - packages: read - -jobs: - build: - name: Compile (${{ matrix.name }}) - strategy: - fail-fast: false - matrix: - include: - - name: CMake Release - type: CMake - preset: windows-release - run_tests: false - upload: true - artifact: windows-cmake-release - - name: CMake Tests - type: CMake - preset: windows-tests - run_tests: true - upload: false - artifact: windows-cmake-tests - - name: Solution Debug - type: Solution - configuration: Debug - artifact: windows-solution-debug - - name: Solution OpenGL - type: Solution - configuration: OpenGL - artifact: windows-solution-opengl - - name: Solution DirectX - type: Solution - configuration: DirectX - artifact: windows-solution-directx - runs-on: windows-2025 - env: - VCPKG_BINARY_CACHE_ACCESS: read - VCPKG_BINARY_SOURCES: "clear;nuget,https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json,read;nugettimeout,600" - VCPKG_NUGET_REPOSITORY: https://github.com/${{ github.repository }}.git - VCPKG_NUGET_API_KEY: ${{ github.token }} - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - SCCACHE_DIR: ${{ github.workspace }}\.sccache - steps: - - name: Install VS 2026 Build Tools (v145 toolset) - if: matrix.type == 'Solution' - shell: pwsh - run: | - $acceptedExitCodes = @(0, 3010) - for ($attempt = 1; $attempt -le 3; $attempt++) { - choco install visualstudio2026-workload-vctools --yes --ignore-package-exit-codes=3010 --no-progress - if ($acceptedExitCodes -contains $LASTEXITCODE) { - exit 0 - } - - Write-Warning "Chocolatey install failed with exit code $LASTEXITCODE on attempt $attempt." - if ($attempt -lt 3) { - Start-Sleep -Seconds (30 * $attempt) - } - } - - Write-Warning "Chocolatey could not install the VS 2026 v145 workload. Continuing so the next step can use any runner-provided toolset." - - - name: Setup MSBuild.exe - if: matrix.type == 'Solution' - uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 - with: - vs-prerelease: true - vs-version: "latest" - - - name: Verify v145 platform toolset - if: matrix.type == 'Solution' - shell: pwsh - run: | - $programFilesX86 = [Environment]::GetFolderPath("ProgramFilesX86") - $vswhere = Join-Path $programFilesX86 "Microsoft Visual Studio\Installer\vswhere.exe" - if (-not (Test-Path $vswhere)) { - throw "vswhere.exe not found" - } - - $installPath = & $vswhere -latest -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath - if (-not $installPath) { - throw "Visual Studio with VC tools was not found" - } - - $vcMsbuildRoot = Join-Path $installPath "MSBuild\Microsoft\VC" - $toolset = Get-ChildItem -Path $vcMsbuildRoot -Directory -Recurse -Filter v145 -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match "\\PlatformToolsets\\v145$" } | - Select-Object -First 1 - if (-not $toolset) { - Get-ChildItem -Path $vcMsbuildRoot -Directory -Recurse -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match "\\PlatformToolsets\\v\d+$" } | - Sort-Object FullName | - ForEach-Object { Write-Host "Found platform toolset: $($_.FullName)" } - throw "v145 platform toolset was not found under $vcMsbuildRoot" - } - - Write-Host "Using v145 platform toolset at $($toolset.FullName)" - - - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Get vcpkg baseline - shell: pwsh - run: | - $json = Get-Content vcpkg.json -Raw | ConvertFrom-Json - $vcpkgCommitId = $json.'builtin-baseline' - "VCPKG_GIT_COMMIT_ID=$vcpkgCommitId" | Out-File -FilePath $env:GITHUB_ENV -Append - - - name: Remove Windows pre-installed MySQL - if: matrix.type == 'CMake' - shell: pwsh - run: Remove-Item -Recurse -Force C:\mysql* -ErrorAction SilentlyContinue - - - name: Prepare sccache directory - if: matrix.type == 'CMake' - shell: pwsh - run: New-Item -ItemType Directory -Force -Path $env:SCCACHE_DIR | Out-Null - - - name: CCache - if: matrix.type == 'CMake' - uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 - with: - max-size: "1G" - variant: "sccache" - key: ccache-windows-${{ matrix.preset }} - restore-keys: | - ccache-windows - - - name: Setup NuGet source for GitHub Packages - shell: pwsh - env: - NUGET_AUTH_TOKEN: ${{ env.VCPKG_NUGET_API_KEY }} - run: | - $feed = "https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json" - $nuget = (Get-Command nuget.exe -ErrorAction SilentlyContinue).Source - if (-not $nuget) { - throw "nuget.exe not found in PATH" - } - - & $nuget sources remove -Name "GitHubPackages" 2>&1 | Out-Null - & $nuget sources add ` - -Name "GitHubPackages" ` - -Source $feed ` - -UserName "${{ github.repository_owner }}" ` - -Password "$env:NUGET_AUTH_TOKEN" ` - -StorePasswordInClearText - - & $nuget setapikey "$env:NUGET_AUTH_TOKEN" -Source $feed - & $nuget sources list - - - name: Setup vcpkg - uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6 - with: - vcpkgDirectory: ${{ matrix.type == 'Solution' && format('{0}/vcpkg', github.workspace) || '' }} - vcpkgGitURL: "https://github.com/microsoft/vcpkg.git" - vcpkgGitCommitId: ${{ env.VCPKG_GIT_COMMIT_ID }} - - - name: Run CMake - if: matrix.type == 'CMake' - uses: lukka/run-cmake@5d55ea7949e25f69f0ecb516d8d572297e03a956 # v10.9 - with: - configurePreset: ${{ matrix.preset }} - buildPreset: ${{ matrix.preset }} - configurePresetAdditionalArgs: "['-DTOGGLE_BIN_FOLDER=ON', '-DOPTIONS_ENABLE_IPO=OFF']" - - - name: Run CTest - if: matrix.type == 'CMake' && matrix.run_tests - shell: pwsh - run: ctest --preset ${{ matrix.preset }} --parallel 2 - - - name: Upload artifacts (CMake) - if: matrix.type == 'CMake' && matrix.upload - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ matrix.artifact }} - path: build/${{ matrix.preset }}/bin/ - - - name: Build project (MSBuild solution) - if: matrix.type == 'Solution' - shell: pwsh - run: | - $vcpkgRoot = Join-Path (Get-Location) "vcpkg" - $outDir = Join-Path (Get-Location) "artifacts/${{ matrix.configuration }}" - New-Item -ItemType Directory -Force -Path $outDir | Out-Null - - msbuild.exe vc18/otclient.sln ` - /m ` - /p:Configuration=${{ matrix.configuration }} ` - /p:Platform=x64 ` - /p:VcpkgEnableManifest=true ` - /p:VcpkgRoot="$vcpkgRoot" ` - /p:OutDir="$outDir\\" ` - /p:GITHUB_WORKSPACE="${{ github.workspace }}" - - - name: Upload artifacts (Solution) - if: matrix.type == 'Solution' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ${{ matrix.artifact }} - path: artifacts/${{ matrix.configuration }}/ From db6e4357e1d51d112c306248fcdebd5ef1127397 Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:36:04 +0200 Subject: [PATCH 5/9] docs: bind Linux CI hybrid task to PR 331 --- .../active/OTC-20260816-linux-ci-hybrid.md | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md index 7db2661619..9cd796a1ad 100644 --- a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md +++ b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md @@ -4,7 +4,7 @@ status: active owner: current-agent branch: ci/OTC-20260816-linux-ci-hybrid base_branch: main -related_pr: null +related_pr: "331" feature_scope: infrastructure completion_claim: internal_only ownership_released: false @@ -38,20 +38,20 @@ Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners w ## Acceptance inventory -- [ ] `CI` has no `windows-2025`, `build-windows`, or `reusable-build-windows.yml` dependency. -- [ ] Compile-relevant PRs require `Build - Linux` via `.github/workflows/reusable-build-linux.yml`. -- [ ] Documentation/task-only changes do not allocate unrelated fast/Lua/build runners. -- [ ] Generic CI jobs use GitHub-hosted Ubuntu runners. -- [ ] Dedicated Synology/runtime workflow files remain unchanged. -- [ ] Superseded `cancelled` CI runs are not automatically retried. +- [x] `CI` has no `windows-2025`, `build-windows`, or `reusable-build-windows.yml` dependency on the implementation branch. +- [x] Compile-relevant PRs require `Build - Linux` via `.github/workflows/reusable-build-linux.yml`. +- [x] Documentation/task-only changes are scoped so unrelated fast/Lua/build jobs can be skipped. +- [ ] Generic CI jobs are observed on GitHub-hosted Ubuntu runners on the exact implementation head. +- [x] Dedicated Synology/runtime workflow files are outside this task's changed-file set. +- [x] Superseded `cancelled` CI runs are not automatically retried. - [ ] Workflow validation/actionlint and exact-head required CI pass. -- [ ] Related PRs are terminal: #328 closed superseded; implementation PR merged when green; #280 intentionally remains separate if still active. +- [ ] Related PRs are terminal: #328 closed superseded; PR #331 merged when green; #280 intentionally remains separate if still active. ## Validation 1. Inspect the exact branch diff and workflow references. 2. Verify no Windows build dependency remains in general CI. -3. Open a PR and inspect exact-head Actions jobs/runner labels. +3. Inspect PR #331 exact-head Actions jobs/runner labels. 4. Require workflow syntax/actionlint and `CI / Required` success. 5. Merge only on the exact validated head. 6. Archive this task and release ownership after post-merge verification. @@ -64,9 +64,15 @@ Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners w ```yaml state: PROVEN -phase: implementation +phase: validation base_head: a27b9f3383b0555142b31216672e9f0143d2cd3d +implementation_pr: 331 superseded_pr: 328 specialized_runtime_pr: 280 -next_action: remove the Windows reusable build, inspect the exact diff, open the implementation PR, and validate exact-head Actions +changed_paths: + - .github/workflows/ci.yml + - .github/workflows/infrastructure-retry.yml + - .github/workflows/reusable-build-windows.yml (removed) + - docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md +next_action: validate PR 331 on its exact head, audit the final diff, then merge and archive if green ``` From c0a00d454c24c8c17889c5a82531cc2cb9c05d23 Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:00:05 +0200 Subject: [PATCH 6/9] ci: add hosted Linux client startup smoke --- .github/workflows/ci.yml | 113 ++++++++++++++++++++++++++++++++++----- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbf07fa264..528be65c9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,6 +111,93 @@ jobs: packages: read uses: ./.github/workflows/reusable-build-linux.yml + smoke-linux: + name: Client Startup Smoke - Linux + needs: [changes, build-linux] + if: needs.changes.outputs.compile == 'true' && needs.build-linux.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + steps: + - name: Checkout repository resources + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + + - name: Download Linux release artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: linux-linux-release + path: build/linux-release/bin + + - name: Install headless runtime dependencies + shell: bash + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y \ + libgl1 \ + libgl1-mesa-dri \ + libglu1-mesa \ + libopenal1 \ + libx11-6 \ + libxcursor1 \ + libxi6 \ + libxinerama1 \ + libxrandr2 \ + xauth \ + xvfb + + - name: Verify client artifact dependencies + shell: bash + run: | + set -euo pipefail + client="build/linux-release/bin/otclient" + test -f "${client}" + chmod +x "${client}" + if ldd "${client}" | tee "${RUNNER_TEMP}/otclient-ldd.log" | grep -q "not found"; then + echo "::error title=Missing runtime dependency::Linux client artifact has unresolved shared libraries." + exit 1 + fi + + - name: Launch client under virtual display + env: + ALSOFT_DRIVERS: "null" + LIBGL_ALWAYS_SOFTWARE: "1" + shell: bash + run: | + set -euo pipefail + client="./build/linux-release/bin/otclient" + user_dir="${RUNNER_TEMP}/otclient-smoke-user" + log="${RUNNER_TEMP}/otclient-smoke.log" + mkdir -p "${user_dir}" + + set +e + timeout --signal=TERM --kill-after=5s 20s \ + xvfb-run -a -s "-screen 0 1280x720x24" \ + "${client}" --user-dir="${user_dir}" >"${log}" 2>&1 + status=$? + set -e + + cat "${log}" + if [[ "${status}" -ne 124 ]]; then + echo "::error title=Client startup smoke failed::Expected OTClient to stay alive until the 20s smoke timeout; exit status was ${status}." + exit 1 + fi + + echo "OTClient remained alive for the bounded 20s headless startup window." + + - name: Upload startup smoke evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: linux-client-startup-smoke + path: | + ${{ runner.temp }}/otclient-smoke.log + ${{ runner.temp }}/otclient-ldd.log + if-no-files-found: warn + required: name: CI / Required if: always() @@ -119,6 +206,7 @@ jobs: - checks - tests-lua - build-linux + - smoke-linux runs-on: ubuntu-latest permissions: {} steps: @@ -174,19 +262,18 @@ jobs: ) is_draft = os.environ["IS_DRAFT"] == "true" - linux_result = jobs["build-linux"].get("result") - if linux_result not in {"success", "skipped"}: - rejected["build-linux"] = ( - f"unexpected conclusion {linux_result}" - ) - elif not is_draft and compile_scope == "true" and linux_result != "success": - rejected["build-linux"] = ( - f"scope compile=true requires success, got {linux_result}" - ) - elif (is_draft or compile_scope == "false") and linux_result != "skipped": - rejected["build-linux"] = ( - f"draft or compile=false requires skipped, got {linux_result}" - ) + for job_name in ("build-linux", "smoke-linux"): + result = jobs[job_name].get("result") + if result not in {"success", "skipped"}: + rejected[job_name] = f"unexpected conclusion {result}" + elif not is_draft and compile_scope == "true" and result != "success": + rejected[job_name] = ( + f"scope compile=true requires success, got {result}" + ) + elif (is_draft or compile_scope == "false") and result != "skipped": + rejected[job_name] = ( + f"draft or compile=false requires skipped, got {result}" + ) print("Required job results:") for name in sorted(jobs): From 8b35383d84e878989d7e9b8c4027a80dd918ec29 Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:00:29 +0200 Subject: [PATCH 7/9] docs: extend hybrid CI acceptance with hosted startup smoke --- .../active/OTC-20260816-linux-ci-hybrid.md | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md index 9cd796a1ad..2d392733ed 100644 --- a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md +++ b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md @@ -20,29 +20,65 @@ owned_paths: ## Objective -Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners while preserving Synology/self-hosted capacity exclusively for work that genuinely needs controlled runtime, LAN, display, or persistent-environment evidence. +Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners, including a bounded headless startup smoke of the built Linux client, while preserving Synology/self-hosted capacity exclusively for work that genuinely needs controlled runtime, LAN, real display/input, persistent sessions, or physical gameplay evidence. ## Coordination - Owner explicitly authorized disabling Windows builds because this OTClient deployment uses Linux only. +- Owner explicitly accepted the runner boundary: deterministic/static/build/startup validation on GitHub-hosted runners; physical gameplay control and persistent runtime evidence on Synology. - PR #328 is closed as superseded; its safe hosted-runner queue reductions are carried forward where applicable without its Windows gate. - PR #280 remains a separate specialized Synology/runtime lane and its owned files are not modified by this task. +- Historical task `OTC-20260712-client-test-foundation` still lists `.github/workflows/reusable-build-linux.yml` as owned although its implementation PR #3 is already merged. This task does not modify that file; the startup smoke is deliberately implemented in `.github/workflows/ci.yml` after the existing Linux build artifact is produced. ## Implemented scope - Replace the required Windows compile job in `.github/workflows/ci.yml` with the existing GitHub-hosted Linux reusable build. - Keep ordinary scope detection, fast checks, Lua checks, required aggregation, and Linux builds on GitHub-hosted Ubuntu runners. +- Add a required GitHub-hosted Linux startup-smoke job for compile-relevant non-draft changes: + - download the `linux-linux-release` artifact from the same Actions run; + - check dynamic-library resolution with `ldd`; + - run the real `otclient` binary under `Xvfb` with software GL and null OpenAL output; + - isolate persisted state with `--user-dir` under `RUNNER_TEMP`; + - require the client to remain alive for a bounded 20-second startup window; + - upload startup and dependency logs as evidence. - Avoid retrying intentionally cancelled superseded CI runs. - Remove the reusable Windows build workflow after verifying that the ordinary CI caller is replaced and no active workflow file in the current workflow inventory names another Windows build entry point. - Do not change the dedicated Synology/Track A runtime workflows. +## Runner boundary + +### GitHub-hosted runners + +Responsible for deterministic and disposable validation that does not require a durable game session: + +- static analysis, workflow validation and Lua syntax; +- C++/Lua unit and bounded integration tests; +- Linux release/test compilation; +- Linux client artifact dependency validation; +- bounded headless client startup smoke under a virtual X display. + +### Synology/self-hosted runtime + +Responsible for evidence that depends on the real controlled environment: + +- persistent OTClient session and canonical runtime registration; +- real display/input ownership; +- login and physical gameplay control such as walking/clicking; +- LAN/runtime integration requiring the Synology environment; +- long-lived observations and direct runtime evidence. + +A GitHub headless startup smoke is not evidence of successful physical gameplay and must never replace Synology runtime E2E where that evidence is required. + ## Acceptance inventory - [x] `CI` has no `windows-2025`, `build-windows`, or `reusable-build-windows.yml` dependency on the implementation branch. - [x] Compile-relevant PRs require `Build - Linux` via `.github/workflows/reusable-build-linux.yml`. -- [x] Documentation/task-only changes are scoped so unrelated fast/Lua/build jobs can be skipped. +- [x] Documentation/task-only changes are scoped so unrelated fast/Lua/build/smoke jobs can be skipped. - [ ] Generic CI jobs are observed on GitHub-hosted Ubuntu runners on the exact implementation head. +- [ ] Exact-head Actions proves the real Linux release artifact starts under `Xvfb` and survives the bounded 20-second smoke window. +- [ ] Startup smoke evidence artifact contains dependency/startup logs. - [x] Dedicated Synology/runtime workflow files are outside this task's changed-file set. +- [x] Runner responsibility boundary is durably recorded and explicitly prevents hosted startup smoke from being treated as physical gameplay E2E. - [x] Superseded `cancelled` CI runs are not automatically retried. - [ ] Workflow validation/actionlint and exact-head required CI pass. - [ ] Related PRs are terminal: #328 closed superseded; PR #331 merged when green; #280 intentionally remains separate if still active. @@ -52,13 +88,18 @@ Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners w 1. Inspect the exact branch diff and workflow references. 2. Verify no Windows build dependency remains in general CI. 3. Inspect PR #331 exact-head Actions jobs/runner labels. -4. Require workflow syntax/actionlint and `CI / Required` success. -5. Merge only on the exact validated head. -6. Archive this task and release ownership after post-merge verification. +4. Require workflow syntax/actionlint, both Linux builds, hosted client startup smoke and `CI / Required` success. +5. Verify the smoke job uses the real release artifact, a virtual display, isolated user directory, bounded liveness and no Synology runner labels. +6. Merge only on the exact validated head. +7. Verify post-merge `main` and its Actions outcome. +8. Archive this task and release ownership after post-merge verification. ## Runtime E2E -`NOT_APPLICABLE_WITH_REASON`: this task changes CI routing rather than product runtime behavior. The real GitHub Actions run on the implementation head is the environment/outcome verification. The dedicated Synology runtime lane remains responsible for runtime/display/LAN evidence. +`SPLIT_BY_ENVIRONMENT`: + +- GitHub-hosted environment outcome required here: real release artifact headless startup smoke. +- Physical gameplay/runtime E2E remains intentionally outside this infrastructure task and belongs to the dedicated Synology/Track A runtime lane. That lane must provide its own direct display/PID/session/gameplay evidence when a task requires it. ## Context checkpoint @@ -69,10 +110,11 @@ base_head: a27b9f3383b0555142b31216672e9f0143d2cd3d implementation_pr: 331 superseded_pr: 328 specialized_runtime_pr: 280 +historical_merged_pr_with_stale_task_claim: 3 changed_paths: - .github/workflows/ci.yml - .github/workflows/infrastructure-retry.yml - .github/workflows/reusable-build-windows.yml (removed) - docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md -next_action: validate PR 331 on its exact head, audit the final diff, then merge and archive if green +next_action: validate the new exact PR head through hosted Linux builds plus real client startup smoke, audit the final diff, then merge and archive if green ``` From cc33d4c042c94d73f3b91472b70fe9eb85b036ec Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:03:33 +0200 Subject: [PATCH 8/9] ci: cancel superseded work when pull requests close --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 528be65c9a..287a2e7a92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: branches: [main] - types: [opened, synchronize, reopened, ready_for_review, converted_to_draft] + types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed] merge_group: branches: [main] types: [checks_requested] @@ -20,6 +20,7 @@ concurrency: jobs: changes: name: Detect Build Scope + if: github.event_name != 'pull_request' || github.event.action != 'closed' runs-on: ubuntu-latest permissions: contents: read @@ -200,7 +201,7 @@ jobs: required: name: CI / Required - if: always() + if: always() && (github.event_name != 'pull_request' || github.event.action != 'closed') needs: - changes - checks From 4c50f1d5843bfe067cca19519e25e4fa9dc7ccfe Mon Sep 17 00:00:00 2001 From: blakinio <75369544+blakinio@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:03:53 +0200 Subject: [PATCH 9/9] docs: record closed-PR CI cancellation invariant --- .../tasks/active/OTC-20260816-linux-ci-hybrid.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md index 2d392733ed..d6dcf38760 100644 --- a/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md +++ b/docs/agents/tasks/active/OTC-20260816-linux-ci-hybrid.md @@ -27,6 +27,7 @@ Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners, - Owner explicitly authorized disabling Windows builds because this OTClient deployment uses Linux only. - Owner explicitly accepted the runner boundary: deterministic/static/build/startup validation on GitHub-hosted runners; physical gameplay control and persistent runtime evidence on Synology. - PR #328 is closed as superseded; its safe hosted-runner queue reductions are carried forward where applicable without its Windows gate. +- Live validation exposed that PR #328's already-started Windows matrix kept consuming hosted runners after the PR was closed. General CI now listens for `pull_request.closed`; the new no-work run shares the PR concurrency group, so closing a PR cancels its older in-progress CI instead of leaving orphaned build demand. - PR #280 remains a separate specialized Synology/runtime lane and its owned files are not modified by this task. - Historical task `OTC-20260712-client-test-foundation` still lists `.github/workflows/reusable-build-linux.yml` as owned although its implementation PR #3 is already merged. This task does not modify that file; the startup smoke is deliberately implemented in `.github/workflows/ci.yml` after the existing Linux build artifact is produced. @@ -41,6 +42,7 @@ Make the ordinary OTClient build/test path Linux-only on GitHub-hosted runners, - isolate persisted state with `--user-dir` under `RUNNER_TEMP`; - require the client to remain alive for a bounded 20-second startup window; - upload startup and dependency logs as evidence. +- On PR close, create only a no-work CI run in the same concurrency group so any older build for that PR is cancelled without allocating new build/test runners. - Avoid retrying intentionally cancelled superseded CI runs. - Remove the reusable Windows build workflow after verifying that the ordinary CI caller is replaced and no active workflow file in the current workflow inventory names another Windows build entry point. - Do not change the dedicated Synology/Track A runtime workflows. @@ -74,6 +76,7 @@ A GitHub headless startup smoke is not evidence of successful physical gameplay - [x] `CI` has no `windows-2025`, `build-windows`, or `reusable-build-windows.yml` dependency on the implementation branch. - [x] Compile-relevant PRs require `Build - Linux` via `.github/workflows/reusable-build-linux.yml`. - [x] Documentation/task-only changes are scoped so unrelated fast/Lua/build/smoke jobs can be skipped. +- [x] Closed PRs have a concurrency-cancellation path that emits no normal build/test work. - [ ] Generic CI jobs are observed on GitHub-hosted Ubuntu runners on the exact implementation head. - [ ] Exact-head Actions proves the real Linux release artifact starts under `Xvfb` and survives the bounded 20-second smoke window. - [ ] Startup smoke evidence artifact contains dependency/startup logs. @@ -90,9 +93,10 @@ A GitHub headless startup smoke is not evidence of successful physical gameplay 3. Inspect PR #331 exact-head Actions jobs/runner labels. 4. Require workflow syntax/actionlint, both Linux builds, hosted client startup smoke and `CI / Required` success. 5. Verify the smoke job uses the real release artifact, a virtual display, isolated user directory, bounded liveness and no Synology runner labels. -6. Merge only on the exact validated head. -7. Verify post-merge `main` and its Actions outcome. -8. Archive this task and release ownership after post-merge verification. +6. Verify closed-PR events skip normal jobs while sharing the same concurrency key used by the PR's active run. +7. Merge only on the exact validated head. +8. Verify post-merge `main` and its Actions outcome. +9. Archive this task and release ownership after post-merge verification. ## Runtime E2E @@ -111,6 +115,7 @@ implementation_pr: 331 superseded_pr: 328 specialized_runtime_pr: 280 historical_merged_pr_with_stale_task_claim: 3 +observed_orphaned_run: 31934213173 changed_paths: - .github/workflows/ci.yml - .github/workflows/infrastructure-retry.yml