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]
