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]

Reply via email to