PR #23251 opened by toots URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23251 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23251.patch
Developers can attach sample files to a PR and list their target paths within the fate-suite in a fate-samples block in the PR description: ```fate-samples vorbis/tos.ogg mov/some-new-sample.mov ``` A new inject-pr-samples.py script fetches the PR metadata from the Forgejo API, resolves each listed path to its matching attachment by filename, and downloads the files into the fate-suite directory before FATE runs. The script validates that pr-number is an integer, that paths are relative, contain no '..', and are at most 3 components deep (matching the deepest paths in the existing fate-suite). Attachment URLs are restricted to the code.ffmpeg.org domain. The script can also be used locally: SAMPLES=/path/to/fate-suite .forgejo/inject-pr-samples.py <pr-number> Example run: https://code.ffmpeg.org/FFmpeg/FFmpeg/actions/runs/53924/jobs/2/attempt/1 / PR: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23016 >From dbea85ceba4fbf390aaa88ba8de4e605d610c845 Mon Sep 17 00:00:00 2001 From: Romain Beauxis <[email protected]> Date: Wed, 27 May 2026 08:09:12 -0500 Subject: [PATCH] .forgejo: add support for ephemeral FATE samples via PR attachments Developers can attach sample files to a PR and list their target paths within the fate-suite in a fate-samples block in the PR description: ```fate-samples vorbis/tos.ogg mov/some-new-sample.mov ``` A new inject-pr-samples.py script fetches the PR metadata from the Forgejo API, resolves each listed path to its matching attachment by filename, and downloads the files into the fate-suite directory before FATE runs. The script validates that pr-number is an integer, that paths are relative, contain no '..', and are at most 3 components deep (matching the deepest paths in the existing fate-suite). Attachment URLs are restricted to the code.ffmpeg.org domain. The script can also be used locally: SAMPLES=/path/to/fate-suite .forgejo/inject-pr-samples.py <pr-number> --- .forgejo/inject-pr-samples.py | 141 ++++++++++++++++++++++++++++++++++ .forgejo/workflows/test.yml | 6 ++ 2 files changed, 147 insertions(+) create mode 100755 .forgejo/inject-pr-samples.py diff --git a/.forgejo/inject-pr-samples.py b/.forgejo/inject-pr-samples.py new file mode 100755 index 0000000000..bd8eac0971 --- /dev/null +++ b/.forgejo/inject-pr-samples.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Inject PR attachment samples into the fate-suite directory. + +Usage: inject-pr-samples.py <pr-number> + +Reads SAMPLES from the environment (defaults to fate-suite). For each path +listed in a ```fate-samples``` block in the PR description, downloads the +matching PR attachment into $SAMPLES/<path>. + +The PR description should contain a block like: + + ```fate-samples + vorbis/tos.ogg + mov/some-new-sample.mov + ``` + +Each filename must match a file attached to the PR. +""" + +import hashlib +import json +import os +import re +import sys +import tempfile +import urllib.request +from pathlib import Path, PurePosixPath + +FORGEJO_API = "https://code.ffmpeg.org/api/v1/repos/ffmpeg/ffmpeg/issues" +ATTACHMENT_BASE = "https://code.ffmpeg.org/attachments/" + + +def fetch_json(url): + with urllib.request.urlopen(url) as r: + return json.load(r) + + +def parse_fate_samples(body): + paths = [] + in_block = False + for line in body.splitlines(): + if line == "```fate-samples": + in_block = True + elif line == "```" and in_block: + break + elif in_block: + parts = line.split() + if len(parts) == 1: + paths.append(parts[0]) + return paths + + +MAX_PATH_DEPTH = 3 + + +def validate_path(path): + p = PurePosixPath(path) + if p.is_absolute(): + raise ValueError(f"path must be relative: {path!r}") + if ".." in p.parts: + raise ValueError(f"path must not contain '..': {path!r}") + if not p.parts: + raise ValueError(f"empty path") + if len(p.parts) > MAX_PATH_DEPTH: + raise ValueError(f"path too deep (max {MAX_PATH_DEPTH} components): {path!r}") + + +def validate_url(url): + if not url.startswith(ATTACHMENT_BASE): + raise ValueError(f"unexpected attachment URL: {url!r}") + + +def digest(path): + h = hashlib.sha256() + with open(path, "rb") as f: + while chunk := f.read(1 << 16): + h.update(chunk) + return h.digest() + + +def download(url, dst): + dst.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(dir=dst.parent, delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + with urllib.request.urlopen(url) as r: + while chunk := r.read(1 << 16): + tmp.write(chunk) + if dst.exists() and digest(dst) != digest(tmp_path): + raise ValueError(f"already exists with different content: {dst}") + tmp_path.rename(dst) + except: + tmp_path.unlink(missing_ok=True) + raise + + +def main(): + if len(sys.argv) != 2 or not re.fullmatch(r"[0-9]+", sys.argv[1]): + print(f"Usage: {sys.argv[0]} <pr-number>", file=sys.stderr) + sys.exit(1) + + pr_number = sys.argv[1] + samples_dir = Path(os.environ.get("SAMPLES", "fate-suite")) + + pr = fetch_json(f"{FORGEJO_API}/{pr_number}") + assets = {a["name"]: a["browser_download_url"] for a in pr.get("assets", [])} + paths = parse_fate_samples(pr.get("body", "")) + + if not paths: + sys.exit(0) + + for path in paths: + try: + validate_path(path) + except ValueError as e: + print(f"fate-samples: {e}", file=sys.stderr) + sys.exit(1) + + name = PurePosixPath(path).name + url = assets.get(name) + if url is None: + print(f"fate-samples: no attachment named {name!r}", file=sys.stderr) + sys.exit(1) + + try: + validate_url(url) + except ValueError as e: + print(f"fate-samples: {e}", file=sys.stderr) + sys.exit(1) + + dst = samples_dir / path + try: + download(url, dst) + except ValueError as e: + print(f"fate-samples: {e}", file=sys.stderr) + sys.exit(1) + print(f"Injected: {path}") + + +if __name__ == "__main__": + main() diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 342120188e..8e3860b08f 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -58,6 +58,9 @@ jobs: with: path: fate-suite key: fate-suite-${{ steps.fate.outputs.hash }} + - name: Inject PR Samples + if: ${{ forge.event_name == 'pull_request' }} + run: SAMPLES=$PWD/fate-suite .forgejo/inject-pr-samples.py ${{ forge.event.pull_request.number }} - name: Run Fate run: | LD_LIBRARY_PATH="$(printf "%s:" "$PWD"/lib*)$PWD" make fate fate-build SAMPLES="$PWD/fate-suite" -j$(nproc) || FATERES=$? @@ -110,6 +113,9 @@ jobs: with: path: fate-suite key: fate-suite-${{ steps.fate.outputs.hash }} + - name: Inject PR Samples + if: ${{ forge.event_name == 'pull_request' }} + run: SAMPLES=$PWD/fate-suite ffmpeg/.forgejo/inject-pr-samples.py ${{ forge.event.pull_request.number }} - name: Run Fate run: | if [[ "${{ matrix.target_exec }}" == "wine" ]]; then -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
