Files
2026-10-23 10:30:02 -03:00

331 lines
17 KiB
YAML

# 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
Publicação manual do commit ${short}. **Nenhum arquivo do jogo incluído**: veja o README-player.md dentro do pacote para o que copiar do seu PS4.
Manual release of commit ${short}. **No game files included**: see README-player.md inside the package for what to copy from your PS4.
☕ Apoie o desenvolvimento / Support development: [Buy me a coffee](https://buymeacoffee.com/hayatog)
- Linux x86-64 (glibc 2.39+): \`infamous-linux-x86_64.tar.gz\`
- Windows 10/11 64-bit: \`infamous-windows-x86_64.zip\`
**Sensor de movimento e touchpad** (grafite, absorver poderes, portas): no DualShock 4/DualSense funcionam direto. Em outros controles, um toque em **L3+R3** liga/desliga o modo sensor (ou Share no Xbox Series): ele é a lata de spray do grafite: o controle fica de lado, X/A sacode, R2 pinta e o **analógico esquerdo mira** o cursor (para os lados e para cima/baixo; ele fica onde você soltar). **View/Select** é o clique do touchpad (segurar para absorver fumaça, neon...), e **View/Select + analógico direito** desliza. Teclado: T/G viram de lado (as setas miram enquanto seguram), Y sacode, F pinta (R2), Tab/Backspace clicam, Tab + setas deslizam, Tab + Backspace apertam as duas pontas (View/Select + R1 no controle: levantar o carro de cima do Reggie). A zona morta dos analógicos fica no launcher.
**Motion sensor and touchpad** (graffiti, absorbing powers, doors): on a DualShock 4/DualSense they work as is. On other controllers, one tap on **L3+R3** toggles sensor mode (or Share on an Xbox Series): it is the graffiti's spray can: the controller is held sideways, cross/A shakes, R2 sprays and the **left stick aims** the cursor (left/right and up/down; it stays where you let go). **View/Select** is the touchpad click (hold it to absorb smoke, neon...), and **View/Select + right stick** swipes. Keyboard: T/G turn it sideways (the arrow keys aim while held), Y shakes, F sprays (R2), Tab/Backspace click, Tab + arrow keys swipe, Tab + Backspace press both ends (View/Select + R1 on a controller: lifting the car off Reggie). The stick dead zone is in the launcher.
NOTES
# The list of game files the player needs (docs/ARQUIVOS_NECESSARIOS.md), before the download.
sed '1,/^\[Português\]/d' docs/ARQUIVOS_NECESSARIOS.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.