This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

commit 78fff004f021fc9b5a3467317eaab7deb446c955
Author:     Romain Beauxis <[email protected]>
AuthorDate: Wed May 27 08:09:12 2026 -0500
Commit:     Romain Beauxis <[email protected]>
CommitDate: Mon Jun 1 10:40:57 2026 -0500

    .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 exports a new_samples=true/false output via $FORGEJO_OUTPUT.
    After FATE completes, a final workflow step fails the run if any new
    sample was injected, reminding contributors to add their samples to the
    official fate-suite before the PR can be merged.
    
    The script can also be used locally:
      SAMPLES=/path/to/fate-suite .forgejo/inject-pr-samples.py <pr-number>
---
 .forgejo/inject-pr-samples.py | 174 ++++++++++++++++++++++++++++++++++++++++++
 .forgejo/workflows/test.yml   |  18 +++++
 2 files changed, 192 insertions(+)

diff --git a/.forgejo/inject-pr-samples.py b/.forgejo/inject-pr-samples.py
new file mode 100755
index 0000000000..3f50067751
--- /dev/null
+++ b/.forgejo/inject-pr-samples.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+# Copyright (c) 2026 Romain Beauxis <[email protected]>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+#    this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+#    this list of conditions and the following disclaimer in the documentation
+#    and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""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)
+
+    new_samples = False
+
+    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
+        is_new = not dst.exists()
+        try:
+            download(url, dst)
+        except ValueError as e:
+            print(f"fate-samples: {e}", file=sys.stderr)
+            sys.exit(1)
+        if is_new:
+            new_samples = True
+        print(f"Injected: {path}")
+
+    output_file = os.environ.get("FORGEJO_OUTPUT")
+    if output_file:
+        with open(output_file, "a") as f:
+            print(f"new_samples={'true' if new_samples else 'false'}", file=f)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml
index 342120188e..3af1522b88 100644
--- a/.forgejo/workflows/test.yml
+++ b/.forgejo/workflows/test.yml
@@ -58,11 +58,20 @@ jobs:
         with:
           path: fate-suite
           key: fate-suite-${{ steps.fate.outputs.hash }}
+      - name: Inject PR Samples
+        id: inject
+        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=$?
           find . -name "*.err" -exec printf '::group::%s\n' {} \; -exec cat {} 
\; -exec printf '::endgroup::\n' \;
           exit ${FATERES:-0}
+      - name: Fail if new samples were injected
+        if: ${{ steps.inject.outputs.new_samples == 'true' }}
+        run: |
+          echo "New FATE samples were injected from PR attachments. Please add 
them to the official fate-suite before merging."
+          exit 1
   run_fate_full:
     name: Fate (Full, ${{ matrix.target_exec }})
     strategy:
@@ -110,6 +119,10 @@ jobs:
         with:
           path: fate-suite
           key: fate-suite-${{ steps.fate.outputs.hash }}
+      - name: Inject PR Samples
+        id: inject
+        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
@@ -119,3 +132,8 @@ jobs:
           LD_LIBRARY_PATH="$(printf "%s:" "$PWD"/lib*)$PWD" make -C build fate 
fate-build SAMPLES="$PWD/fate-suite" -j$(nproc) || FATERES=$?
           find . -name "*.err" -exec printf '::group::%s\n' {} \; -exec cat {} 
\; -exec printf '::endgroup::\n' \;
           exit ${FATERES:-0}
+      - name: Fail if new samples were injected
+        if: ${{ steps.inject.outputs.new_samples == 'true' }}
+        run: |
+          echo "New FATE samples were injected from PR attachments. Please add 
them to the official fate-suite before merging."
+          exit 1

_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to