# Builds and tests packages on PRs. Publishing is an explicit manual # action: run this workflow on main with publish_release enabled to release accumulated PRs. # No game or console file is ever built in or uploaded. name: Build on: # Pull requests to main build both packages (artifacts only): the Release is left untouched. pull_request: branches: [main] workflow_dispatch: inputs: publish_release: description: 'Publicar release / Publish release (main only)' type: boolean required: true default: false beta: description: 'Beta: pre-release da branch escolhida / pre-release of the chosen branch (not main)' type: boolean required: false default: false concurrency: # Manual runs share a separate group: a new merge cannot cancel a publication. group: ${{ github.event_name == 'workflow_dispatch' && 'manual-release' || format('build-{0}', github.ref) }} cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' }} permissions: contents: read jobs: linux: if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.publish_release == true) runs-on: ubuntu-24.04 defaults: run: working-directory: port env: CC: gcc-14 CXX: g++-14 PKG_CONFIG_PATH: /opt/tlgdeps/lib/pkgconfig:/opt/tlgdeps/lib64/pkgconfig:/opt/tlgdeps/lib/x86_64-linux-gnu/pkgconfig CMAKE_PREFIX_PATH: /opt/tlgdeps LD_LIBRARY_PATH: /opt/tlgdeps/lib:/opt/tlgdeps/lib64:/opt/tlgdeps/lib/x86_64-linux-gnu steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Release note tests working-directory: . run: python3 .github/scripts/test_release_notes.py - name: System packages run: | sudo apt-get update -q sudo apt-get install -y -q --no-install-recommends gcc-14 g++-14 cmake ninja-build pkg-config nasm \ curl git python3-venv python3-tk libvulkan-dev glslang-tools libboost-dev \ libxxhash-dev zlib1g-dev libx11-dev libxcb1-dev libxext-dev libxrandr-dev libxcursor-dev \ libxi-dev libxss-dev libxtst-dev libxkbcommon-dev libwayland-dev wayland-protocols \ libdecor-0-dev libegl-dev libgl-dev libdrm-dev libgbm-dev libpulse-dev libasound2-dev \ libpipewire-0.3-dev libudev-dev libdbus-1-dev libusb-1.0-0-dev libfreetype-dev \ clang libssl-dev - name: Cache built libraries id: deps uses: actions/cache@v4 with: path: /opt/tlgdeps key: tlgdeps-ubuntu2404-${{ hashFiles('port/packaging/linux/build_deps.sh') }} - name: Build libraries (SDL3, Zydis, fmt, FFmpeg) if: steps.deps.outputs.cache-hit != 'true' run: | sudo mkdir -p /opt/tlgdeps && sudo chown "$USER" /opt/tlgdeps bash packaging/linux/build_deps.sh - name: Build id: build run: | # Keep compilation progress visible while build.sh captures Ninja's full log. (while sleep 30; do echo "Build running: $(date -u +%H:%M:%S) UTC" tail -n 2 out/gpu-build.log 2>/dev/null || true done) & progress_pid=$! trap 'kill "$progress_pid" 2>/dev/null || true' EXIT # TLG_RENDERER_V2=1: renderer v2 too (port/gpu_v2, out/tlg-probe-v2), shipped in the package. TLG_RENDERER_V2=1 TLG_LTO=ON TLG_PGO=off bash build.sh # The unit tests need no GPU, display or game file. Each test step runs even when an earlier # one failed (all failures show in one run); any failure fails the job before Package. - name: C tests # build.sh's test list, reusing out/ from Build (same TLG_LTO/TLG_PGO: nothing is rebuilt). if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: TLG_LTO=ON TLG_PGO=off bash build.sh --test-only - name: Python tests # The venv is the one packaging/linux/package.sh reuses: the pinned PySide6 and iced-x86. if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: | set -euo pipefail source packaging/port.env python3 -m venv out/pyenv out/pyenv/bin/python -m pip install -q --disable-pip-version-check iced-x86==1.21.0 "$PORT_QT_PACKAGE" QT_QPA_PLATFORM=offscreen out/pyenv/bin/python -m unittest discover -s tests - name: GPU library tests # Every *-test executable CMake defines in out/gpu for this platform (the Windows-only ones # are not defined here); a test that needs a Vulkan driver prints SKIP without one. The link # rule keeps out phony aliases of third-party targets. Each test runs in an empty directory. if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: | set -euo pipefail mapfile -t tests < <(ninja -C out/gpu -t targets all | grep -E '^[^/:]+-test: (C|CXX)_EXECUTABLE_LINKER' | cut -d: -f1 | sort) (( ${#tests[@]} )) || { echo 'no GPU test targets in out/gpu'; exit 1; } echo "GPU tests: ${tests[*]}" ninja -C out/gpu "${tests[@]}" gpu=$PWD/out/gpu failed=() for test in "${tests[@]}"; do echo "::group::$test" (cd "$(mktemp -d)" && timeout 600 "$gpu/$test") || failed+=("$test") echo "::endgroup::" done (( ${#failed[@]} == 0 )) || { echo "::error::GPU tests failed: ${failed[*]}"; exit 1; } - name: Package run: bash packaging/linux/package.sh - name: Smoke test (frozen Qt launcher) # Opens the packaged launcher offscreen and renders a frame: a package whose launcher cannot # start (missing Qt plugin, QML, font, translation) must not become a release. run: | set -euo pipefail smoke=$(mktemp -d) tar -C "$smoke" -xzf dist/infamous-linux-x86_64.tar.gz out=$(QT_QPA_PLATFORM=offscreen timeout 120 "$smoke/infamous-linux-x86_64/inFAMOUS Second Son" --selftest 2>&1) || { echo "$out"; exit 1; } echo "$out" grep -qx 'selftest ok' <<<"$out" - uses: actions/upload-artifact@v4 with: name: infamous-linux-x86_64 path: port/dist/infamous-linux-x86_64.tar.gz if-no-files-found: error retention-days: 3 windows: if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.publish_release == true) runs-on: windows-latest defaults: run: shell: msys2 {0} working-directory: port steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-python@v5 id: python with: python-version: '3.12' - uses: msys2/setup-msys2@v2 with: msystem: CLANG64 update: true install: >- git patch mingw-w64-clang-x86_64-toolchain mingw-w64-clang-x86_64-cmake mingw-w64-clang-x86_64-ninja mingw-w64-clang-x86_64-pkgconf mingw-w64-clang-x86_64-python mingw-w64-clang-x86_64-vulkan-headers mingw-w64-clang-x86_64-vulkan-loader mingw-w64-clang-x86_64-vulkan-memory-allocator mingw-w64-clang-x86_64-glslang mingw-w64-clang-x86_64-sdl3 mingw-w64-clang-x86_64-fmt mingw-w64-clang-x86_64-boost mingw-w64-clang-x86_64-robin-map mingw-w64-clang-x86_64-xxhash mingw-w64-clang-x86_64-ffmpeg mingw-w64-clang-x86_64-zydis mingw-w64-clang-x86_64-spirv-tools mingw-w64-clang-x86_64-spirv-headers mingw-w64-clang-x86_64-spirv-cross mingw-w64-clang-x86_64-freetype - name: Build env: MSYS2_ROOT: ${{ runner.temp }}/msys64 run: | export MSYS2_ROOT=$(cygpath -u "$MSYS2_ROOT") # Keep compilation progress visible while build.sh captures Ninja's full log. (while sleep 30; do echo "Build running: $(date -u +%H:%M:%S) UTC" tail -n 2 out/gpu-build.log 2>/dev/null || true done) & progress_pid=$! trap 'kill "$progress_pid" 2>/dev/null || true' EXIT # TLG_RENDERER_V2=1: renderer v2 too (port/gpu_v2, out/tlg-probe-v2.exe), shipped in the package. TLG_RENDERER_V2=1 TLG_LTO=ON bash build.sh - name: Package env: MSYS2_ROOT: ${{ runner.temp }}/msys64 WINPYTHON_WIN: ${{ steps.python.outputs.python-path }} run: | export MSYS2_ROOT=$(cygpath -u "$MSYS2_ROOT") export WINPYTHON=$(cygpath -u "$WINPYTHON_WIN") bash packaging/windows/package.sh - name: Smoke test (frozen Qt launcher) # Unzips the package and runs the frozen launcher with --selftest (Qt offscreen platform). # The windowed exe has no console, so it writes its result to a file that is checked here. shell: pwsh working-directory: port run: | $smoke = Join-Path $env:RUNNER_TEMP 'smoke' Remove-Item -Recurse -Force $smoke -ErrorAction SilentlyContinue Expand-Archive -Path dist/infamous-windows-x86_64.zip -DestinationPath $smoke $exe = Join-Path $smoke 'infamous-windows-x86_64/inFAMOUS Second Son.exe' $outFile = Join-Path $env:RUNNER_TEMP 'selftest.txt' Remove-Item $outFile -ErrorAction SilentlyContinue $env:QT_QPA_PLATFORM = 'offscreen' $env:ISS_SELFTEST_OUT = $outFile $p = Start-Process -FilePath $exe -ArgumentList '--selftest' -PassThru if (-not $p.WaitForExit(180000)) { $p.Kill(); Write-Error 'selftest timed out'; exit 1 } if (Test-Path $outFile) { Get-Content $outFile } if ($p.ExitCode -ne 0 -or -not (Test-Path $outFile) -or -not ((Get-Content $outFile -Raw) -match 'selftest ok')) { Write-Error "selftest failed (exit code $($p.ExitCode))" exit 1 } - uses: actions/upload-artifact@v4 with: name: infamous-windows-x86_64 path: port/dist/infamous-windows-x86_64.zip if-no-files-found: error retention-days: 3 beta: # A manual run with "beta" on a branch other than main: beta.yml builds, tests and publishes a # pre-release of that branch (never Latest; posted only to the Discord beta channels, never # #chat-geral). The jobs above stay skipped. if: github.event_name == 'workflow_dispatch' && inputs.beta == true && github.ref != 'refs/heads/main' permissions: contents: write pull-requests: read uses: ./.github/workflows/beta.yml with: branch: ${{ github.ref_name }} # The beta channels' Discord webhooks (DISCORD_BETA_WEBHOOK, DISCORD_BETA_CHAT_WEBHOOK). secrets: inherit release: needs: [linux, windows] # Approval, merge and tag pushes never publish. Both platform jobs must pass first. if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && inputs.publish_release == true runs-on: ubuntu-24.04 permissions: contents: write pull-requests: read env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} steps: - uses: actions/checkout@v4 with: sparse-checkout: | docs/ARQUIVOS_NECESSARIOS.md .github/scripts/release_notes.sh sparse-checkout-cone-mode: false - uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Publish id: publish run: | set -euo pipefail ls -la dist test -f dist/infamous-linux-x86_64.tar.gz && test -f dist/infamous-windows-x86_64.zip short=${GITHUB_SHA::7} # Put accumulated changes first, before package and setup instructions. bash .github/scripts/release_notes.sh "$GITHUB_SHA" > notes.md cat >> notes.md <> notes.md tag="v$(date -u +%Y.%m.%d)-${short}" # A re-run of the same commit finds its release already published: keep it. if gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null | grep -qx false; then echo "tag=$tag" >> "$GITHUB_OUTPUT"; exit 0 fi # SHA256SUMS: the launcher's updater checks the downloaded package against it. (cd dist && sha256sum infamous-linux-x86_64.tar.gz infamous-windows-x86_64.zip > SHA256SUMS) # Upload into a draft first, so a failed upload never leaves a half-published release. # Releases are kept forever as version history; nothing here deletes one. gh release create "$tag" dist/* --draft --target "$GITHUB_SHA" \ --title "$tag ($(date -u +%Y-%m-%d) · ${short})" --notes-file notes.md n=$(gh release view "$tag" --json assets --jq '.assets | length') [[ $n -ge 3 ]] || { echo "draft release has $n assets"; exit 1; } for try in 1 2 3; do gh release edit "$tag" --draft=false --latest && break sleep 10 done [[ $(gh release view "$tag" --json isDraft --jq .isDraft) == false ]] echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "published=true" >> "$GITHUB_OUTPUT" - name: Notify Discord # Posts the new release to #chat-geral. A Discord failure never fails the build. if: steps.publish.outputs.published == 'true' continue-on-error: true env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} GAME: InFamous Second Son run: | set -euo pipefail [[ -n "${DISCORD_WEBHOOK:-}" ]] || { echo "DISCORD_RELEASE_WEBHOOK not set"; exit 0; } tag=${{ steps.publish.outputs.tag }}; color=5763719 short=${GITHUB_SHA::7} url="$GITHUB_SERVER_URL/$GH_REPO/releases/tag/$tag" downloads=$(gh release view "$tag" --json assets \ --jq '[.assets[] | select(.name != "SHA256SUMS") | "• [\(.name)](\(.url))"] | join("\n")') # One card per release: only the "## Novidades" section of the release notes (the full # notes are one click away). Without that section, the commit message stands in. news=$(gh release view "$tag" --json body --jq .body | tr -d '\r' | awk '/^## Novidades/ { on = 1; next } /^## / { on = 0 } on' | sed -e '/./,$!d' | sed -e ':a' -e '/^\n*$/{$d;N;ba' -e '}') [[ -n $news ]] || news="- $(git log -1 --format=%s "$GITHUB_SHA" 2>/dev/null || echo "Build $short")" jq -n \ --arg title "🎮 $GAME: nova versão ($tag)" --arg url "$url" \ --arg news "$news" --arg dl "$downloads" --arg commit "Commit $short" \ --argjson color "$color" ' (if ($news | length) > 3500 then ($news[:3500] | sub("\n[^\n]*$"; "")) + "\n… [notas completas](\($url))" else $news end) as $d | {username: "Novas versões", embeds: [{title: $title, url: $url, description: $d, color: $color, fields: [{name: "Downloads", value: $dl}], footer: {text: $commit}}]}' | curl -fsS -H 'Content-Type: application/json' -d @- "$DISCORD_WEBHOOK" # Artifacts expire through retention-days; keep run history and PR checks intact.