diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index d2fdbcde..026795d2 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -10,10 +10,17 @@ on: - "wiki/**" - "scripts/generate_site.py" - "scripts/generate_readme.py" + - "scripts/validate_site.py" - "scripts/verify.py" - "scripts/common.py" + - "schemas/**" + - "tests/**" + - "docs_assets/**" + - "install/**" + - "install.py" - "database.json" - "mkdocs.yml" + - ".github/workflows/deploy-site.yml" workflow_dispatch: permissions: @@ -40,50 +47,23 @@ jobs: # mkdocs-material >= 9.7.5 caps mkdocs < 2; the upper bound keeps a major # theme release from changing the site without a deliberate bump here. - - run: pip install pyyaml "mkdocs-material>=9.7.5,<10" "pymdown-extensions>=10.14" + - run: pip install pyyaml jsonschema==4.23.0 "mkdocs-material>=9.7.5,<10" "pymdown-extensions>=10.14" - - name: Restore large files from release - run: | - mkdir -p .cache/large - gh release download large-files -D .cache/large/ 2>/dev/null || true - python3 -c " - import hashlib, json, os, shutil - db = json.load(open('database.json')) - with open('.gitignore') as f: - ignored = {l.strip() for l in f if l.strip().startswith('bios/')} - cache = '.cache/large' - if not os.path.isdir(cache): - exit(0) - idx = {} - for fn in os.listdir(cache): - fp = os.path.join(cache, fn) - if os.path.isfile(fp): - h = hashlib.sha1(open(fp, 'rb').read()).hexdigest() - idx[h] = fp - restored = 0 - for sha1, entry in db['files'].items(): - path = entry['path'] - if path in ignored and not os.path.exists(path): - src = idx.get(sha1) - if src: - os.makedirs(os.path.dirname(path), exist_ok=True) - shutil.copy2(src, path) - print(f'Restored: {path}') - restored += 1 - print(f'Total: {restored} files restored') - " - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Refresh data directories - run: python scripts/refresh_data_dirs.py - continue-on-error: true + - name: Validate data contracts + run: python scripts/validate_schemas.py - name: Generate site run: | python scripts/generate_site.py python scripts/generate_readme.py --db database.json --platforms-dir platforms mkdocs build --strict + python scripts/validate_site.py + + # write_if_changed() compares content with its timestamp line stripped, + # so a run that only moves the clock leaves the file untouched and this + # stays a real staleness check rather than a guaranteed failure. + - name: Require committed README and CONTRIBUTING to be current + run: git diff --exit-code -- README.md CONTRIBUTING.md - name: Setup Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 02b1ab36..96213bb0 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -6,6 +6,10 @@ on: - "bios/**" - "platforms/**" - "emulators/**" + - "schemas/**" + - "scripts/**" + - "tests/**" + - "install.py" permissions: contents: read @@ -36,23 +40,27 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - files=$(git diff --name-only "$BASE_SHA"..."$HEAD_SHA" | grep '^bios/' || true) - echo "files=$files" >> "$GITHUB_OUTPUT" - echo "$files" > /tmp/changed_files.txt + # 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 - name: Validate BIOS files id: validate run: | - if [ -s /tmp/changed_files.txt ]; then - xargs python scripts/validate_pr.py --markdown < /tmp/changed_files.txt > /tmp/report.md 2>&1 || true + if [ -s changed_files.zlist ]; then + xargs -0 python scripts/validate_pr.py --markdown -- \ + < changed_files.zlist > report.md 2>&1 || true else - echo "No BIOS files changed" > /tmp/report.md + echo "No BIOS files changed" > report.md fi + cat report.md - name: Post validation report if: always() run: | - gh pr comment "${{ github.event.pull_request.number }}" --body-file /tmp/report.md + gh pr comment "${{ github.event.pull_request.number }}" --body-file report.md env: GH_TOKEN: ${{ github.token }} @@ -68,60 +76,8 @@ jobs: - name: Install dependencies run: pip install pyyaml jsonschema==4.23.0 - - name: Validate platform configs - run: | - python -c " - import json, yaml, sys - from jsonschema import validate, ValidationError - from pathlib import Path - - with open('schemas/platform.schema.json') as f: - schema = json.load(f) - - errors = [] - for yml_file in Path('platforms').glob('*.yml'): - if yml_file.name.startswith('_'): - continue - with open(yml_file) as f: - config = yaml.safe_load(f) - try: - validate(config, schema) - print(f'OK {yml_file.name}') - except ValidationError as e: - errors.append(f'{yml_file.name}: {e.message}') - print(f'FAIL {yml_file.name}: {e.message}') - - if errors: - sys.exit(1) - " - - - name: Validate emulator profiles - run: | - python -c " - import json, yaml, sys - from jsonschema import validate, ValidationError - from pathlib import Path - - with open('schemas/emulator.schema.json') as f: - schema = json.load(f) - - errors = [] - for yml_file in sorted(Path('emulators').glob('*.yml')): - if yml_file.name.endswith('.old.yml'): - continue - with open(yml_file) as f: - profile = yaml.safe_load(f) - try: - validate(profile, schema) - except ValidationError as e: - path = '/'.join(str(p) for p in e.absolute_path) - errors.append(f'{yml_file.name}: {path}: {e.message}') - print(f'FAIL {yml_file.name}: {path}: {e.message}') - - print(f'{len(errors)} invalid profiles') - if errors: - sys.exit(1) - " + - name: Validate platform configs and emulator profiles + run: python scripts/validate_schemas.py --source-only run-tests: runs-on: ubuntu-latest @@ -133,10 +89,10 @@ jobs: python-version: "3.12" - name: Install dependencies - run: pip install pyyaml + run: pip install pyyaml jsonschema==4.23.0 - name: Run test suite - run: python -m unittest tests.test_e2e -v + run: python -m unittest discover tests -v label-pr: runs-on: ubuntu-latest