Still, you allow random people to open MRs. On Tue, 2 Jun 2026, at 05:07, Romain Beauxis via ffmpeg-devel wrote: > That being said, the workflows don't run by default. > > Adding Timo to get some context. Timo, who are the people who have rights > to run CI workflows by default? > > Thanks, > -- Romain > > Le lun. 1 juin 2026 à 22:05, Romain Beauxis <[email protected]> a > écrit : > >> It's true. Do you think I should revert? I sanitize the type of file that >> we get in? >> >> Le lun. 1 juin 2026 à 12:25, Jean-Baptiste Kempf via ffmpeg-devel < >> [email protected]> a écrit : >> >>> This is particularly dangerous. >>> This makes the CI prone to injection to files from random people. >>> >>> On Mon, 1 Jun 2026, at 17:41, Romain Beauxis via ffmpeg-cvslog wrote: >>> > 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] >>> >>> -- >>> Jean-Baptiste Kempf - President >>> +33 672 704 734 >>> https://jbkempf.com/ >>> _______________________________________________ >>> ffmpeg-devel mailing list -- [email protected] >>> To unsubscribe send an email to [email protected] >>> >> > _______________________________________________ > ffmpeg-devel mailing list -- [email protected] > To unsubscribe send an email to [email protected]
-- Jean-Baptiste Kempf - President +33 672 704 734 https://jbkempf.com/ _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
