Files
libretro/.github/workflows/validate.yml
T
Abdessamad Derraz 087e5b4d73 feat: show measurements on the site
Each emulator with records gets a Tested block: claim, result, method, computer, core build and the permalink of its record at the pin. A Measurements page filters every test in the browser from the compact index, and the Data page lists measurements.sqlite with its tables. One SQLite file per emulator is linked from its page. wiki/measurements.md says how a test runs, held to the Start here budgets.

Both workflows fetch the pinned commit alone with git before the build, and the deploy fails without it.
2026-10-27 08:30:16 +01:00

341 lines
13 KiB
YAML

name: Validation
# The same checks on both roads into main. Work lands here by direct push as
# often as by pull request, and a test suite reachable only from a PR guards
# the road nobody takes.
on:
pull_request:
paths:
- "bios/**"
- "platforms/**"
- "emulators/**"
- "schemas/**"
- "scripts/**"
- "tests/**"
# The bootstraps pin install.py's SHA-256 and a test enforces the pin,
# so editing one without the other has to fail the PR.
- "install.py"
- "install.sh"
- "install.ps1"
# The handheld port bundles install.py and its tests read both.
- "ports/**"
# Every input of the site build, so a page that would fail the deploy
# fails here first (tests/test_workflow_paths.py).
- "wiki/**"
- "docs_assets/**"
- "provenance/**"
- "exports/**"
- "install/**"
- "database.json"
- "release.json"
- "derivable.json"
- "measurements-pin.json"
- "website/**"
# Spelled out twice because the workflow parser reads no YAML anchor. A test
# holds the two lists equal.
push:
branches: [main]
paths:
- "bios/**"
- "platforms/**"
- "emulators/**"
- "schemas/**"
- "scripts/**"
- "tests/**"
- "install.py"
- "install.sh"
- "install.ps1"
- "ports/**"
- "wiki/**"
- "docs_assets/**"
- "provenance/**"
- "exports/**"
- "install/**"
- "database.json"
- "release.json"
- "derivable.json"
- "measurements-pin.json"
- "website/**"
permissions:
contents: read
pull-requests: write
concurrency:
# A push series collapses to the tip: what has to stay verified is the head
# of main, not every commit that passed under it.
group: validate-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate-bios:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- name: Install dependencies
run: pip install pyyaml
- name: Get changed BIOS files
id: changed
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# NUL-separated and filtered to added/modified/renamed: a deleted
# file has no bytes left to hash, and a path with a space or a
# quote must survive the hand-off to validate_pr.py.
git diff --name-only --diff-filter=AMR -z "$BASE_SHA"..."$HEAD_SHA" \
-- bios/ > changed_files.zlist
# The report has to reach the pull request whatever the outcome, so the
# exit code is recorded here and acted on after the comment is posted.
# Swallowing it outright left the job green on a file that failed its
# hash check.
- name: Validate BIOS files
id: validate
run: |
if [ -s changed_files.zlist ]; then
set +e
xargs -0 python scripts/validate_pr.py --markdown -- \
< changed_files.zlist > report.md 2>&1
echo "rc=$?" >> "$GITHUB_OUTPUT"
set -e
else
echo "No BIOS files changed" > report.md
echo "rc=0" >> "$GITHUB_OUTPUT"
fi
cat report.md
# The job summary is the report every contributor sees, fork or not:
# it needs no write token.
- name: Write the report to the job summary
if: always()
run: |
if [ -f report.md ]; then
cat report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "The validation step stopped before writing a report." >> "$GITHUB_STEP_SUMMARY"
fi
# A pull request from a fork runs with a read-only GITHUB_TOKEN, so a
# comment would fail and turn a valid file red. The comment is posted
# only when the branch lives in this repository. pull_request_target is
# not an option: it hands a write token to a run that would read the
# contributor's files and scripts.
- name: Post validation report
if: always() && github.event.pull_request.head.repo.full_name == github.repository
run: |
gh pr comment "${{ github.event.pull_request.number }}" --body-file report.md
env:
GH_TOKEN: ${{ github.token }}
- name: Fail when a file did not validate
if: steps.validate.outputs.rc != '0'
run: |
echo "::error::validate_pr.py exited ${{ steps.validate.outputs.rc }}. The report is in the job summary of this run."
exit 1
validate-configs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- name: Install dependencies
run: pip install pyyaml "jsonschema[format-nongpl]==4.26.0"
- name: Validate platform configs and emulator profiles
run: python scripts/validate_schemas.py --source-only
# A profile written from a clone can cite lines that do not hold the value
# it declares, and schemas and tests pass it. Each profile a change adds is
# confronted with its upstream, both roads into main. An edited profile is
# not held to it: the backlog of older refs is reviewed by hand.
check-new-profiles:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: "3.12"
- name: Install dependencies
run: pip install pyyaml
- name: Confront added profiles with their upstream
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BEFORE: ${{ github.event.before }}
run: |
# A pull request is checked out as a merge commit whose first
# parent is the base, while a push names the commit it moved main from.
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
base=HEAD^1
elif [ -z "$BEFORE" ] || [ "$BEFORE" = "0000000000000000000000000000000000000000" ]; then
echo "first push of the branch: no base to diff against"
exit 0
else
base=$BEFORE
fi
python scripts/profile_sync.py --added --base "$base" --strict
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
# install.py stays on Python 3.8: the suite runs it end to end under a
# real 3.8 interpreter, which import-only and grammar checks miss. 3.9
# is the floor pipeline.py states for the build scripts: a real 3.9
# imports every module under scripts/, so it needs their dependencies.
# jsonschema 4.26 requires 3.10, 4.25.1 is the last release for 3.9.
# The last version listed is the default python.
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
python-version: |
3.8
3.9
3.12
- name: Install the build scripts' dependencies for Python 3.9
run: python3.9 -m pip install pyyaml "jsonschema[format-nongpl]==4.25.1"
# markdown: the site validation test renders a wiki line to prove the
# heading guard's premise, and skipped on every run without it. pyte
# reads the installer's full screen back in its end-to-end tests,
# hypothesis fuzzes its key decoder, and vermin holds install.py to 3.8.
- name: Install dependencies
run: pip install pyyaml "jsonschema[format-nongpl]==4.26.0" "markdown==3.10.2" "pyte==0.8.2" "hypothesis==6.170.8" "vermin==1.9.2"
# The full screen also runs under the 3.8 floor: the end-to-end tests
# start install.py with the interpreter that runs them.
- name: Install the screen tests' dependency for Python 3.8
run: python3.8 -m pip install "pyte==0.8.2"
- name: Hold install.py to Python 3.8
run: vermin --no-tips -t=3.8- --violations install.py
# The handheld launchers' test runs shellcheck and fails under CI
# without it. The Ubuntu image ships it: this names it if one stops.
- name: Check that shellcheck is installed
run: shellcheck --version
# The site is built as deploy-site.yml builds it, from the lockfile.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "24"
cache: npm
cache-dependency-path: website/package-lock.json
- run: npm ci --prefix website
# The exporter fidelity cases compare what we hand back against the
# file each platform publishes, so they need those files. They skip
# without them rather than fail, which is why an unreachable upstream
# must not stop the run: the rest of the suite still has to report.
- name: Fetch the platforms' own files
continue-on-error: true
run: python scripts/export_native.py --all --fetch --output-dir "$RUNNER_TEMP/native"
# A pack built without its data directories exits non-zero: it is not
# the pack a user gets. Tests that build packs need the same trees a
# release build has, as deploy-site.yml fetches them.
- name: Fetch the data directories
run: python scripts/refresh_data_dirs.py
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The measurement records at their pin, as deploy-site.yml reads them:
# the pages and the schema check read the same lines.
- name: Clone the measurements at their pin
run: |
repo=$(python -c "import json; print(json.load(open('measurements-pin.json'))['repo'])")
commit=$(python -c "import json; print(json.load(open('measurements-pin.json'))['commit'])")
git init -q .cache/measurements
git -C .cache/measurements fetch -q --depth 1 "$repo" "$commit"
git -C .cache/measurements checkout -q --detach "$commit"
# The pages are gitignored, and the tests that read them fail
# in CI without it. The suite runs whatever this step returned, so a
# generator that stops still leaves every other test reporting.
- name: Generate the site
run: python scripts/generate_site.py --require-measurements
- name: Run test suite
if: ${{ !cancelled() }}
# No test downloads a release asset: 252 MB files fetched into the
# shared cache filled the runner's disk and changed what later tests
# resolved. The download tests mock the network and clear it.
env:
RETROBIOS_OFFLINE: "1"
run: python -m unittest discover -s tests -t . -v
- name: Run the full-screen interface tests on Python 3.8
if: ${{ !cancelled() }}
env:
RETROBIOS_OFFLINE: "1"
run: python3.8 -m unittest tests.test_install_tui -v
# deploy-site.yml runs this after the merge, and a dead link or a stray
# heading failed it there with nothing in front of it. Run whatever the
# suite returned, so one red step does not hide the other. The release
# assets are not restored: they change counts, not links or headings.
- name: Build the site
if: ${{ !cancelled() }}
run: |
npm run build --prefix website
python scripts/validate_site.py --site-dir website/dist
label-pr:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
# Labels need a write token, which a pull request from a fork does not
# get. The step is skipped there instead of reporting a success that
# set nothing.
- name: Auto-label PR
if: github.event.pull_request.head.repo.full_name == github.repository
run: |
labels=""
files=$(gh pr diff "${{ github.event.pull_request.number }}" --name-only)
if echo "$files" | grep -q '^bios/'; then
labels="$labels bios"
for sys in $(echo "$files" | grep '^bios/' | cut -d/ -f2 | sort -u); do
labels="$labels system:$(echo "$sys" | tr '[:upper:]' '[:lower:]')"
done
fi
if echo "$files" | grep -q '^platforms/'; then
labels="$labels platform-config"
fi
if echo "$files" | grep -q '^scripts/'; then
labels="$labels automation"
fi
for label in $labels; do
gh pr edit "${{ github.event.pull_request.number }}" --add-label "$label" \
|| echo "::warning::label $label was not added"
done
env:
GH_TOKEN: ${{ github.token }}