name: Build on: push: branches: [main] pull_request: branches: [main] types: [opened, synchronize, reopened] workflow_dispatch: inputs: pr: description: Pull request number to build required: true type: string permissions: contents: read concurrency: group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }} cancel-in-progress: ${{ github.event_name != 'push' }} env: CONFIGURE: >- -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache jobs: eligibility: if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read pull-requests: read outputs: sha: ${{ steps.gate.outputs.sha }} pr: ${{ steps.gate.outputs.pr }} head: ${{ steps.gate.outputs.head }} relinker: ${{ steps.gate.outputs.relinker }} register-context: ${{ steps.gate.outputs.register-context }} write-watch: ${{ steps.gate.outputs.write-watch }} full: ${{ steps.gate.outputs.full }} steps: - uses: actions/github-script@v8 id: gate env: PR_NUMBER: ${{ inputs.pr }} with: script: | if (context.eventName === 'push') { if (context.ref !== 'refs/heads/main') { throw new Error('Automatic full builds only run on main'); } core.setOutput('sha', context.sha); core.setOutput('pr', ''); core.setOutput('head', ''); core.setOutput('relinker', true); core.setOutput('register-context', true); core.setOutput('write-watch', true); core.setOutput('full', true); await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write(); return; } const { owner, repo } = context.repo; const manual = context.eventName === 'workflow_dispatch'; if (manual) { for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) { const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); if (!['write', 'maintain', 'admin'].includes(data.permission)) { throw new Error('Only collaborators with write access can start or rerun builds'); } } if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) { throw new Error('Enter a positive pull request number'); } } const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number; if (!Number.isSafeInteger(pull_number) || pull_number < 1) { throw new Error('Invalid pull request number'); } let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }); for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) { await new Promise((resolve) => setTimeout(resolve, 3000)); ({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number })); } if (pr.state !== 'open' || pr.base.ref !== 'main') { throw new Error('The pull request must be open and target main'); } if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) { throw new Error('The pull request has newer commits; use the latest run'); } if (pr.mergeable === false) { throw new Error('The pull request does not merge cleanly into main; rebase it first'); } let relinker = manual; if (!manual) { const { data: commits } = await github.rest.repos.listCommits({ owner, repo, sha: 'main', author: pr.user.login, per_page: 1 }); relinker = commits.length > 0; } const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 }); const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)); const touches = (...prefixes) => manual || files.length >= 3000 || paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix)); const workflow = '.github/workflows/build.yml'; const libc = 'core/libs/prx/libc/'; relinker = relinker && touches( 'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp', 'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp', 'tools/', 'cmake/', 'CMakeLists.txt', workflow ); const registerContext = touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow); const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow); const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' }); core.setOutput('sha', main.commit.sha); core.setOutput('pr', String(pull_number)); core.setOutput('head', pr.head.sha); core.setOutput('relinker', relinker); core.setOutput('register-context', registerContext); core.setOutput('write-watch', writeWatch); core.setOutput('full', manual); core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`); await core.summary.addRaw( `PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` + `relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` + `register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}` ).write(); relinker: needs: eligibility if: ${{ needs.eligibility.outputs.relinker == 'true' }} runs-on: ${{ matrix.os }} timeout-minutes: 15 strategy: fail-fast: false matrix: include: - os: ubuntu-latest compiler: g++ - os: windows-latest compiler: g++ - os: macos-latest compiler: clang++ env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip defaults: run: shell: bash steps: - uses: actions/checkout@v7 with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash env: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi - if: runner.os == 'Windows' id: toolchain uses: actions/cache@v6 with: path: C:\winlibs key: ${{ env.WINLIBS_ZIP }} - if: runner.os == 'Windows' && steps.toolchain.outputs.cache-hit != 'true' run: | curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP" 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null - if: runner.os == 'Windows' run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON - run: cmake --build build-relinker --parallel 3 - run: ctest --test-dir build-relinker --output-on-failure --timeout 120 register-context: needs: eligibility if: ${{ needs.eligibility.outputs.register-context == 'true' }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-15-intel] configuration: [Debug, Release] runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: - uses: actions/checkout@v7 - run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} - run: cmake --build build/register-context --parallel - run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error write-watch: needs: eligibility if: ${{ needs.eligibility.outputs.write-watch == 'true' }} strategy: fail-fast: false matrix: os: [macos-15-intel, macos-15] runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: - uses: actions/checkout@v7 - run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release - run: cmake --build build/write-watch --parallel - run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error linux: needs: eligibility if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v7 with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false submodules: true - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash env: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi git submodule update --init --recursive - uses: hendrikmuhs/ccache-action@v1.2.24 with: key: linux save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - name: Install lavapipe and the SDL audio and XRandR development files run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build - run: cmake --build build - run: cmake --build build --target libs - run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml env: ANYPS5_REQUIRE_VULKAN: 1 GALLIUM_OVERRIDE_CPU_CAPS: avx - run: python3 tools/check_skips.py build/ctest.xml - name: Report tests slower than 30 s if: ${{ !cancelled() }} shell: bash run: | [ -f ctest.log ] || exit 0 awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t } END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log windows: needs: eligibility if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }} runs-on: windows-latest timeout-minutes: 45 env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt defaults: run: shell: bash steps: - uses: actions/checkout@v7 with: ref: ${{ needs.eligibility.outputs.sha }} persist-credentials: false submodules: true - name: Merge the pull request into main if: ${{ needs.eligibility.outputs.head != '' }} shell: bash env: PR_NUMBER: ${{ needs.eligibility.outputs.pr }} PR_HEAD: ${{ needs.eligibility.outputs.head }} run: | git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head" if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi git submodule update --init --recursive - id: toolchain uses: actions/cache@v6 with: path: C:\winlibs key: ${{ env.WINLIBS_ZIP }} - if: steps.toolchain.outputs.cache-hit != 'true' run: | curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP" 7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null - run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH" - uses: hendrikmuhs/ccache-action@v1.2.24 with: key: windows save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - shell: cmd run: cmake -S . -B build %CONFIGURE% - name: Check that every test executable is registered with ctest shell: cmd run: python tools/check_test_registration.py build - shell: cmd run: cmake --build build - shell: cmd run: cmake --build build --target libs - shell: cmd run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml - run: python tools/check_skips.py build/ctest.xml --job windows - name: Report tests slower than 30 s if: ${{ !cancelled() }} shell: bash run: | [ -f ctest.log ] || exit 0 awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t } END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log