# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license

# Daily Monte Carlo fuzzing of the yolo CLI on a 4-personality, 3-OS runner matrix
# Confirmed, deduplicated findings are auto-filed as GitHub issues (hard cap per run) by the Report job

name: Fuzz

permissions:
  contents: read

on:
  schedule:
    - cron: "0 2 * * *" # daily at 02:00 UTC, 6h before ci.yml's 08:00 heavy run
  workflow_dispatch:
    inputs:
      minutes:
        description: "Fuzzing budget per runner in minutes"
        default: "300"
        type: string
      repro_command:
        description: "Reproduce one exact command from a fuzz issue instead of fuzzing, e.g. 'train detect model=yolo26n.pt data=coco8.yaml epochs=abc'"
        default: ""
        type: string

concurrency:
  group: fuzz
  cancel-in-progress: false

env:
  PYTHONFAULTHANDLER: 1
  YOLO_AUTOINSTALL: "false"

jobs:
  Assets:
    if: github.repository == 'ultralytics/ultralytics' || github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: ultralytics/actions/setup-uv@main # outside the composite so it adds no post step to every consumer
        with:
          python-version: "3.13"
      - uses: ./.github/actions/ci-assets
        with:
          build: true

  Fuzz:
    needs: Assets
    if: github.repository == 'ultralytics/ultralytics' || github.event_name == 'workflow_dispatch'
    runs-on: ${{ matrix.os }}
    timeout-minutes: 345
    strategy:
      fail-fast: false
      matrix:
        include: # one OS per personality: exports are the most platform-sensitive, predict/val exercise macOS spawn multiprocessing
          - personality: train
            os: ubuntu-latest
            extras: export-base,export-openvino,solutions
          - personality: export
            os: windows-latest
            extras: export-base,export-openvino,solutions
          - personality: predict-val
            os: macos-26
            extras: export-base,export-openvino,export-coreml,solutions
          - personality: chaos
            os: ubuntu-latest
            extras: export-base,export-openvino,solutions
    defaults:
      run:
        shell: bash # multi-line run blocks and ${GITHUB_WORKSPACE} expansion on all three OSes
    steps:
      - uses: actions/checkout@v7
      - uses: ultralytics/actions/setup-uv@main
        with:
          python-version: "3.13"
      - name: Install requirements
        uses: ultralytics/actions/retry@main
        with:
          retries: 3
          retry_delay_seconds: 30
          run: uv pip install -e ".[${{ matrix.extras }}]" --torch-backend cpu
      - name: Check environment
        run: yolo checks
      - uses: ./.github/actions/ci-assets
      - name: Cache fuzz exploration history
        # Separate from the asset cache: this entry is rewritten every run while the multi-GB assets are not. Cache
        # entries are immutable, so the key carries run id AND attempt — a re-run reuses the run id and would
        # otherwise hit its own entry, skip the save, and discard everything it explored. restore-keys then picks
        # up the newest prior entry. Entries age out inside fuzz.py (HISTORY_DAYS), bounding the file.
        uses: actions/cache@v6
        with:
          path: fuzz-history
          key: ultralytics-fuzz-history-${{ matrix.personality }}-${{ github.run_id }}-${{ github.run_attempt }}
          restore-keys: |
            ultralytics-fuzz-history-${{ matrix.personality }}-
      - name: Precache fuzz assets
        if: github.event.inputs.repro_command == ''
        uses: ultralytics/actions/retry@main
        with:
          retries: 3
          retry_delay_seconds: 60
          run: python .github/scripts/fuzz.py fuzz --budget-minutes 0 --personality ${{ matrix.personality }} --out fuzz-precache # zero-budget run downloads all corpus assets and executes no trials; its empty findings stay out of fuzz-out
      - name: Fuzz
        if: github.event.inputs.repro_command == ''
        env:
          MINUTES: ${{ github.event.inputs.minutes || '300' }} # via env, never interpolated into shell
        run: |
          python .github/scripts/fuzz.py fuzz \
            --budget-minutes "$MINUTES" \
            --seed ${{ github.run_id }} \
            --personality ${{ matrix.personality }} \
            --history fuzz-history/${{ matrix.personality }}.txt \
            --out fuzz-out
      - name: Reproduce command
        if: github.event.inputs.repro_command != '' && matrix.personality == 'chaos'
        env:
          REPRO_COMMAND: ${{ github.event.inputs.repro_command }} # via env, never interpolated into shell
        run: python .github/scripts/fuzz.py repro "$REPRO_COMMAND"
      - name: Upload findings
        if: always() && github.event.inputs.repro_command == ''
        uses: actions/upload-artifact@v7
        with:
          name: fuzz-${{ matrix.personality }}
          path: fuzz-out/
      - name: Prune uv Cache
        run: uv cache prune --ci

  Report:
    needs: Fuzz
    if: always() && github.event.inputs.repro_command == ''
    runs-on: ubuntu-latest
    timeout-minutes: 15
    permissions:
      contents: read
      issues: write
    steps:
      - uses: actions/checkout@v7 # report mode is stdlib-only, no package install needed
      - uses: actions/download-artifact@v8
        with:
          pattern: fuzz-*
          path: fuzz-out
          merge-multiple: true
      - name: File issues for confirmed findings
        id: report
        if: github.repository == 'ultralytics/ultralytics'
        env:
          GH_TOKEN: ${{ secrets._GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
        run: python .github/scripts/fuzz.py report --in fuzz-out --max-issues 3
      - name: Notify on new issues
        if: steps.report.outputs.new_issues > 0 && github.event_name == 'schedule' && github.run_attempt == '1'
        uses: slackapi/slack-github-action@v4.0.0
        with:
          webhook-type: incoming-webhook
          webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
          payload: |
            text: "<!subteam^S082BPCRAJ3> *${{ github.workflow }}* 🐛 filed ${{ steps.report.outputs.new_issues }} new fuzz issue(s) in `${{ github.repository }}` <https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|*Run*>"
