diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..9d7031e9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,359 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# YOLO Continuous Integration (CI) GitHub Actions tests + +name: Ultralytics CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 8 * * *" # runs at 08:00 UTC every day + workflow_dispatch: + inputs: + hub: + description: "Run HUB" + default: false + type: boolean + benchmarks: + description: "Run Benchmarks" + default: false + type: boolean + tests: + description: "Run Tests" + default: false + type: boolean + gpu: + description: "Run GPU" + default: false + type: boolean + raspberrypi: + description: "Run Raspberry Pi" + default: false + type: boolean + conda: + description: "Run Conda" + default: false + type: boolean + +jobs: + HUB: + # if: github.repository == 'ultralytics/ultralytics' && (github.event_name == 'schedule' || github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.hub == 'true')) + if: github.repository == 'ultralytics/ultralytics' && 'workflow_dispatch' && github.event.inputs.hub == 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.11"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v4 + - name: Install requirements + shell: bash # for Windows compatibility + run: | + uv pip install --system . --extra-index-url https://download.pytorch.org/whl/cpu + - name: Check environment + run: | + yolo checks + uv pip list + - name: Test HUB training + shell: python + env: + API_KEY: ${{ secrets.ULTRALYTICS_HUB_API_KEY }} + MODEL_ID: ${{ secrets.ULTRALYTICS_HUB_MODEL_ID }} + run: | + import os + from ultralytics import YOLO, hub + api_key, model_id = os.environ['API_KEY'], os.environ['MODEL_ID'] + hub.login(api_key) + hub.reset_model(model_id) + model = YOLO('https://hub.ultralytics.com/models/' + model_id) + model.train() + - name: Test HUB inference API + shell: python + env: + API_KEY: ${{ secrets.ULTRALYTICS_HUB_API_KEY }} + MODEL_ID: ${{ secrets.ULTRALYTICS_HUB_MODEL_ID }} + run: | + import os + import requests + import json + api_key, model_id = os.environ['API_KEY'], os.environ['MODEL_ID'] + url = f"https://api.ultralytics.com/v1/predict/{model_id}" + headers = {"x-api-key": api_key} + data = {"size": 320, "confidence": 0.25, "iou": 0.45} + with open("ultralytics/assets/zidane.jpg", "rb") as f: + response = requests.post(url, headers=headers, data=data, files={"image": f}) + assert response.status_code == 200, f'Status code {response.status_code}, Reason {response.reason}' + print(json.dumps(response.json(), indent=2)) + + Benchmarks: + if: github.event_name != 'workflow_dispatch' || github.event.inputs.benchmarks == 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-14] + python-version: ["3.11"] + model: [yolo11n] + steps: + - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install requirements + shell: bash # for Windows compatibility + run: | + uv pip install --system -e ".[export]" "coverage[toml]" --extra-index-url https://download.pytorch.org/whl/cpu --index-strategy unsafe-first-match + - name: Check environment + run: | + yolo checks + uv pip list + - name: Benchmark DetectionModel + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/${{ matrix.model }}.pt' imgsz=160 verbose=0.309 + - name: Benchmark ClassificationModel + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/${{ matrix.model }}-cls.pt' imgsz=160 verbose=0.249 + - name: Benchmark YOLOWorld DetectionModel + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/yolov8s-worldv2.pt' imgsz=160 verbose=0.337 + - name: Benchmark SegmentationModel + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/${{ matrix.model }}-seg.pt' imgsz=160 verbose=0.195 + - name: Benchmark PoseModel + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/${{ matrix.model }}-pose.pt' imgsz=160 verbose=0.197 + - name: Benchmark OBBModel + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/${{ matrix.model }}-obb.pt' imgsz=160 verbose=0.597 + - name: Benchmark YOLOv10Model + shell: bash + run: coverage run -a --source=ultralytics -m ultralytics.cfg.__init__ benchmark model='path with spaces/yolov10n.pt' imgsz=160 verbose=0.205 + - name: Merge Coverage Reports + run: | + coverage xml -o coverage-benchmarks.xml + - name: Upload Coverage Reports to CodeCov + if: github.repository == 'ultralytics/ultralytics' + uses: codecov/codecov-action@v5 + with: + flags: Benchmarks + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Prune uv Cache + run: uv cache prune --ci + - name: Benchmark Summary + run: | + cat benchmarks.log + echo "$(cat benchmarks.log)" >> $GITHUB_STEP_SUMMARY + + Tests: + if: github.event_name != 'workflow_dispatch' || github.event.inputs.tests == 'true' + timeout-minutes: 360 + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14, windows-latest] + python-version: ["3.11"] + torch: [latest] + include: + - os: ubuntu-latest + python-version: "3.8" # torch 1.8.0 requires python >=3.6, <=3.8 + torch: "1.8.0" # min torch version CI https://pypi.org/project/torchvision/ + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - uses: astral-sh/setup-uv@v4 + - name: Install requirements + shell: bash # for Windows compatibility + run: | + # CoreML must be installed before export due to protobuf error from AutoInstall + slow="" + torch="" + if [ "${{ matrix.torch }}" == "1.8.0" ]; then + torch="torch==1.8.0 torchvision==0.9.0" + fi + if [[ "${{ github.event_name }}" =~ ^(schedule|workflow_dispatch)$ ]]; then + slow="pycocotools mlflow" + fi + uv pip install --system -e ".[export]" $torch $slow pytest-cov --extra-index-url https://download.pytorch.org/whl/cpu + - name: Check environment + run: | + yolo checks + uv pip list + - name: Pytest tests + shell: bash # for Windows compatibility + run: | + slow="" + if [[ "${{ github.event_name }}" =~ ^(schedule|workflow_dispatch)$ ]]; then + slow="--slow" + fi + pytest $slow --cov=ultralytics/ --cov-report xml tests/ + - name: Upload Coverage Reports to CodeCov + if: github.repository == 'ultralytics/ultralytics' # && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' + uses: codecov/codecov-action@v5 + with: + flags: Tests + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Prune uv Cache + run: uv cache prune --ci + + GPU: + if: github.repository == 'ultralytics/ultralytics' && (github.event_name != 'workflow_dispatch' || github.event.inputs.gpu == 'true') + timeout-minutes: 360 + runs-on: gpu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + - name: Install requirements + shell: bash # for Windows compatibility + run: uv pip install --system -e . pytest-cov + - name: Check environment + run: | + yolo checks + uv pip list + - name: Pytest tests + run: | + slow="" + if [[ "${{ github.event_name }}" =~ ^(schedule|workflow_dispatch)$ ]]; then + slow="--slow" + fi + pytest $slow --cov=ultralytics/ --cov-report xml tests/test_cuda.py + - name: Upload Coverage Reports to CodeCov + uses: codecov/codecov-action@v5 + with: + flags: GPU + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + RaspberryPi: + if: github.repository == 'ultralytics/ultralytics' && (github.event_name == 'schedule' || github.event.inputs.raspberrypi == 'true') + timeout-minutes: 120 + runs-on: raspberry-pi + steps: + - uses: actions/checkout@v4 + - name: Activate Virtual Environment + run: | + python3.11 -m venv env + source env/bin/activate + echo PATH=$PATH >> $GITHUB_ENV + - name: Install requirements + run: | + python -m pip install --upgrade pip wheel + pip install -e ".[export]" pytest mlflow pycocotools + - name: Check environment + run: | + yolo checks + pip list + - name: Pytest tests + run: pytest --slow tests/ + - name: Benchmark ClassificationModel + run: python -m ultralytics.cfg.__init__ benchmark model='yolo11n-cls.pt' imgsz=160 verbose=0.249 + - name: Benchmark YOLOWorld DetectionModel + run: python -m ultralytics.cfg.__init__ benchmark model='yolov8s-worldv2.pt' imgsz=160 verbose=0.337 + - name: Benchmark SegmentationModel + run: python -m ultralytics.cfg.__init__ benchmark model='yolo11n-seg.pt' imgsz=160 verbose=0.195 + - name: Benchmark PoseModel + run: python -m ultralytics.cfg.__init__ benchmark model='yolo11n-pose.pt' imgsz=160 verbose=0.197 + - name: Benchmark OBBModel + run: python -m ultralytics.cfg.__init__ benchmark model='yolo11n-obb.pt' imgsz=160 verbose=0.597 + - name: Benchmark YOLOv10Model + run: python -m ultralytics.cfg.__init__ benchmark model='yolov10n.pt' imgsz=160 verbose=0.205 + - name: Benchmark Summary + run: | + cat benchmarks.log + echo "$(cat benchmarks.log)" >> $GITHUB_STEP_SUMMARY + # The below is fixed in: https://github.com/ultralytics/ultralytics/pull/15987 + # - name: Reboot # run a reboot command in the background to free resources for next run and not crash main thread + # run: sudo bash -c "sleep 10; reboot" & + + Conda: + if: github.repository == 'ultralytics/ultralytics' && (github.event_name == 'schedule' || github.event.inputs.conda == 'true') + continue-on-error: true + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.11"] + defaults: + run: + shell: bash -el {0} + steps: + - uses: conda-incubator/setup-miniconda@v3 + with: + python-version: ${{ matrix.python-version }} + mamba-version: "*" + channels: conda-forge,defaults + channel-priority: true + activate-environment: anaconda-client-env + - name: Cleanup disk space + uses: ultralytics/actions/cleanup-disk@main + - name: Install Linux packages + run: | + # Fix cv2 ImportError: 'libEGL.so.1: cannot open shared object file: No such file or directory' + sudo apt-get update + sudo apt-get install -y libegl1 libopengl0 + - name: Install Libmamba + run: | + conda config --set solver libmamba + - name: Install Ultralytics package from conda-forge + run: | + conda install -c pytorch -c conda-forge pytorch torchvision ultralytics openvino + - name: Install pip packages + run: | + # CoreML must be installed before export due to protobuf error from AutoInstall + pip install pytest "coremltools>=7.0; platform_system != 'Windows' and python_version <= '3.11'" + - name: Check environment + run: | + conda list + - name: Test CLI + run: | + yolo predict model=yolo11n.pt imgsz=320 + yolo train model=yolo11n.pt data=coco8.yaml epochs=1 imgsz=32 + yolo val model=yolo11n.pt data=coco8.yaml imgsz=32 + yolo export model=yolo11n.pt format=torchscript imgsz=160 + yolo solutions + - name: Test Python + # Note this step must use the updated default bash environment, not a python environment + run: | + python -c " + from ultralytics import YOLO + model = YOLO('yolo11n.pt') + results = model.train(data='coco8.yaml', epochs=3, imgsz=160) + results = model.val(imgsz=160) + results = model.predict(imgsz=160) + results = model.export(format='onnx', imgsz=160) + " + - name: PyTest + run: | + VERSION=$(conda list ultralytics | grep ultralytics | awk '{print $2}') + echo "Ultralytics version: $VERSION" + git clone https://github.com/ultralytics/ultralytics.git + cd ultralytics + git checkout tags/v$VERSION + pytest tests + + Summary: + runs-on: ubuntu-latest + needs: [HUB, Benchmarks, Tests, GPU, RaspberryPi, Conda] + if: always() + steps: + - name: Check for failure and notify + if: (needs.HUB.result == 'failure' || needs.Benchmarks.result == 'failure' || needs.Tests.result == 'failure' || needs.GPU.result == 'failure' || needs.RaspberryPi.result == 'failure' || needs.Conda.result == 'failure' ) && github.repository == 'ultralytics/ultralytics' && (github.event_name == 'schedule' || github.event_name == 'push') && github.run_attempt == '1' + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }} + payload: | + text: " GitHub Actions error for ${{ github.workflow }} ❌\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* ${{ github.event_name }}\n" diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..7e0dadc8 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,44 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# Ultralytics Contributor License Agreement (CLA) action https://docs.ultralytics.com/help/CLA +# This workflow automatically requests Pull Requests (PR) authors to sign the Ultralytics CLA before PRs can be merged + +name: CLA Assistant +on: + issue_comment: + types: + - created + pull_request_target: + types: + - reopened + - opened + - synchronize + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLA: + if: github.repository == 'ultralytics/ultralytics' + runs-on: ubuntu-latest + steps: + - name: CLA Assistant + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Must be repository secret PAT + PERSONAL_ACCESS_TOKEN: ${{ secrets._GITHUB_TOKEN }} + with: + path-to-signatures: "signatures/version1/cla.json" + path-to-document: "https://docs.ultralytics.com/help/CLA" # CLA document + # Branch must not be protected + branch: cla-signatures + allowlist: dependabot[bot],github-actions,[pre-commit*,pre-commit*,bot* + + remote-organization-name: ultralytics + remote-repository-name: cla + custom-pr-sign-comment: "I have read the CLA Document and I sign the CLA" + custom-allsigned-prcomment: All Contributors have signed the CLA. ✅ diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 00000000..26846b0b --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,210 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# Builds ultralytics/ultralytics:latest images on DockerHub https://hub.docker.com/r/ultralytics + +name: Publish Docker Images + +on: + push: + branches: [main] + paths-ignore: + - "docs/**" + - "mkdocs.yml" + workflow_dispatch: + inputs: + Dockerfile: + type: boolean + description: Use Dockerfile + default: true + Dockerfile-cpu: + type: boolean + description: Use Dockerfile-cpu + default: true + Dockerfile-arm64: + type: boolean + description: Use Dockerfile-arm64 + default: true + Dockerfile-jetson-jetpack6: + type: boolean + description: Use Dockerfile-jetson-jetpack6 + default: true + Dockerfile-jetson-jetpack5: + type: boolean + description: Use Dockerfile-jetson-jetpack5 + default: true + Dockerfile-jetson-jetpack4: + type: boolean + description: Use Dockerfile-jetson-jetpack4 + default: true + Dockerfile-python: + type: boolean + description: Use Dockerfile-python + default: true + Dockerfile-conda: + type: boolean + description: Use Dockerfile-conda + default: true + push: + type: boolean + description: Publish all Images to Docker Hub + +jobs: + docker: + if: github.repository == 'ultralytics/ultralytics' + name: Push + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 10 + matrix: + include: + - dockerfile: "Dockerfile" + tags: "latest" + platforms: "linux/amd64" + - dockerfile: "Dockerfile-cpu" + tags: "latest-cpu" + platforms: "linux/amd64" + - dockerfile: "Dockerfile-arm64" + tags: "latest-arm64" + platforms: "linux/arm64" + - dockerfile: "Dockerfile-jetson-jetpack6" + tags: "latest-jetson-jetpack6" + platforms: "linux/arm64" + - dockerfile: "Dockerfile-jetson-jetpack5" + tags: "latest-jetson-jetpack5" + platforms: "linux/arm64" + - dockerfile: "Dockerfile-jetson-jetpack4" + tags: "latest-jetson-jetpack4" + platforms: "linux/arm64" + - dockerfile: "Dockerfile-python" + tags: "latest-python" + platforms: "linux/amd64" + # - dockerfile: "Dockerfile-conda" + # tags: "latest-conda" + # platforms: "linux/amd64" + outputs: + new_release: ${{ steps.check_tag.outputs.new_release }} + steps: + - name: Cleanup disk space + uses: ultralytics/actions/cleanup-disk@main + + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 # copy full .git directory to access full git history in Docker images + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Retrieve Ultralytics version + id: get_version + run: | + VERSION=$(grep "^__version__ =" ultralytics/__init__.py | awk -F'"' '{print $2}') + echo "Retrieved Ultralytics version: $VERSION" + echo "version=$VERSION" >> $GITHUB_OUTPUT + VERSION_TAG=$(echo "${{ matrix.tags }}" | sed "s/latest/${VERSION}/") + echo "Intended version tag: $VERSION_TAG" + echo "version_tag=$VERSION_TAG" >> $GITHUB_OUTPUT + + - name: Check if version tag exists on DockerHub + id: check_tag + run: | + RESPONSE=$(curl -s https://hub.docker.com/v2/repositories/ultralytics/ultralytics/tags/$VERSION_TAG) + MESSAGE=$(echo $RESPONSE | jq -r '.message') + if [[ "$MESSAGE" == "null" ]]; then + echo "Tag $VERSION_TAG already exists on DockerHub." + echo "new_release=false" >> $GITHUB_OUTPUT + elif [[ "$MESSAGE" == *"404"* ]]; then + echo "Tag $VERSION_TAG does not exist on DockerHub." + echo "new_release=true" >> $GITHUB_OUTPUT + else + echo "Unexpected response from DockerHub. Please check manually." + echo "new_release=false" >> $GITHUB_OUTPUT + fi + env: + VERSION_TAG: ${{ steps.get_version.outputs.version_tag }} + + - name: Build Image + if: github.event_name == 'push' || github.event.inputs[matrix.dockerfile] == 'true' + uses: ultralytics/actions/retry@main + with: + timeout_minutes: 120 + retry_delay_seconds: 60 + retries: 2 + run: | + docker build \ + --platform ${{ matrix.platforms }} \ + -f docker/${{ matrix.dockerfile }} \ + -t ultralytics/ultralytics:${{ matrix.tags }} \ + -t ultralytics/ultralytics:${{ steps.get_version.outputs.version_tag }} \ + . + + - name: Run Tests + if: (github.event_name == 'push' || github.event.inputs[matrix.dockerfile] == 'true') && matrix.platforms == 'linux/amd64' && matrix.dockerfile != 'Dockerfile-conda' # arm64 images not supported on GitHub CI runners + run: docker run ultralytics/ultralytics:${{ matrix.tags }} /bin/bash -c "pip install pytest && pytest tests" + + - name: Run Benchmarks + # WARNING: Dockerfile (GPU) error on TF.js export 'module 'numpy' has no attribute 'object'. + if: (github.event_name == 'push' || github.event.inputs[matrix.dockerfile] == 'true') && matrix.platforms == 'linux/amd64' && matrix.dockerfile != 'Dockerfile' && matrix.dockerfile != 'Dockerfile-conda' # arm64 images not supported on GitHub CI runners + run: docker run ultralytics/ultralytics:${{ matrix.tags }} yolo benchmark model=yolo11n.pt imgsz=160 verbose=0.309 + + - name: Push Docker Image with Ultralytics version tag + if: (github.event_name == 'push' || (github.event.inputs[matrix.dockerfile] == 'true' && github.event.inputs.push == 'true')) && steps.check_tag.outputs.new_release == 'true' && matrix.dockerfile != 'Dockerfile-conda' + run: | + docker push ultralytics/ultralytics:${{ steps.get_version.outputs.version_tag }} + + - name: Push Docker Image with latest tag + if: github.event_name == 'push' || (github.event.inputs[matrix.dockerfile] == 'true' && github.event.inputs.push == 'true') + run: | + docker push ultralytics/ultralytics:${{ matrix.tags }} + if [[ "${{ matrix.tags }}" == "latest" ]]; then + t=ultralytics/ultralytics:latest-runner + docker build -f docker/Dockerfile-runner -t $t . + docker push $t + fi + if [[ "${{ matrix.tags }}" == "latest-python" ]]; then + t=ultralytics/ultralytics:latest-jupyter + v=ultralytics/ultralytics:${{ steps.get_version.outputs.version }}-jupyter + docker build -f docker/Dockerfile-jupyter -t $t -t $v . + docker push $t + if [[ "${{ steps.check_tag.outputs.new_release }}" == "true" ]]; then + docker push $v + fi + fi + + trigger-actions: + runs-on: ubuntu-latest + needs: docker + # Only trigger actions on new Ultralytics releases + if: success() && github.repository == 'ultralytics/ultralytics' && github.event_name == 'push' && needs.docker.outputs.new_release == 'true' + steps: + - name: Trigger Additional GitHub Actions + env: + GH_TOKEN: ${{ secrets._GITHUB_TOKEN }} + run: | + sleep 60 + gh workflow run deploy_cloud_run.yml \ + --repo ultralytics/assistant \ + --ref main + + notify: + runs-on: ubuntu-latest + needs: [docker, trigger-actions] + if: always() + steps: + - name: Check for failure and notify + if: needs.docker.result == 'failure' && github.repository == 'ultralytics/ultralytics' && github.event_name == 'push' && github.run_attempt == '1' + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }} + payload: | + text: " GitHub Actions error for ${{ github.workflow }} ❌\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* ${{ github.event_name }}\n" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..40b7efc1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,105 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# Test and publish docs to https://docs.ultralytics.com +# Ignores the following Docs rules to match Google-style docstrings: +# D100: Missing docstring in public module +# D104: Missing docstring in public package +# D203: 1 blank line required before class docstring +# D205: 1 blank line required between summary line and description +# D212: Multi-line docstring summary should start at the first line +# D213: Multi-line docstring summary should start at the second line +# D401: First line of docstring should be in imperative mood +# D406: Section name should end with a newline +# D407: Missing dashed underline after section +# D413: Missing blank line after last section + +name: Publish Docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + inputs: + publish_docs: + description: "Publish live to https://docs.ultralytics.com" + default: true + type: boolean + +jobs: + Docs: + if: github.repository == 'ultralytics/ultralytics' + runs-on: ubuntu-latest + env: + GITHUB_REF: ${{ github.head_ref || github.ref }} + steps: + - name: Git config + run: | + git config --global user.name "UltralyticsAssistant" + git config --global user.email "web@ultralytics.com" + - name: Checkout Repository + uses: actions/checkout@v4 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + token: ${{ secrets._GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + ref: ${{ env.GITHUB_REF }} + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - uses: astral-sh/setup-uv@v4 + - name: Install Dependencies + run: uv pip install --system ruff black tqdm mkdocs-material "mkdocstrings[python]" mkdocs-jupyter mkdocs-redirects mkdocs-ultralytics-plugin mkdocs-macros-plugin + - name: Ruff fixes + continue-on-error: true + run: ruff check --fix --unsafe-fixes --select D --ignore=D100,D104,D203,D205,D212,D213,D401,D406,D407,D413 . + - name: Update Docs Reference Section and Push Changes + continue-on-error: true + run: | + python docs/build_reference.py + git pull origin "$GITHUB_REF" + git add . + git reset HEAD -- .github/workflows/ # workflow changes are not permitted with default token + if ! git diff --staged --quiet; then + git commit -m "Auto-update Ultralytics Docs Reference by https://ultralytics.com/actions" + git push + else + echo "No changes to commit" + fi + - name: Ruff checks + run: ruff check --select D --ignore=D100,D104,D203,D205,D212,D213,D401,D406,D407,D413 . + - name: Build Docs and Check for Warnings + run: | + export JUPYTER_PLATFORM_DIRS=1 + python docs/build_docs.py + - name: Commit and Push Docs changes + continue-on-error: true + if: always() + run: | + git pull origin "$GITHUB_REF" + git add --update # only add updated files + git reset HEAD -- .github/workflows/ # workflow changes are not permitted with default token + if ! git diff --staged --quiet; then + git commit -m "Auto-update Ultralytics Docs by https://ultralytics.com/actions" + git push + else + echo "No changes to commit" + fi + - name: Publish Docs to https://docs.ultralytics.com + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_docs == 'true') + run: | + git clone https://github.com/ultralytics/docs.git docs-repo + cd docs-repo + git checkout gh-pages || git checkout -b gh-pages + rm -rf * + cp -R ../site/* . + echo "${{ secrets.INDEXNOW_KEY_DOCS }}" > "${{ secrets.INDEXNOW_KEY_DOCS }}.txt" + git add . + if git diff --staged --quiet; then + echo "No changes to commit" + else + LATEST_HASH=$(git rev-parse --short=7 HEAD) + git commit -m "Update Docs for 'ultralytics ${{ steps.check_pypi.outputs.version }} - $LATEST_HASH'" + git push https://${{ secrets._GITHUB_TOKEN }}@github.com/ultralytics/docs.git gh-pages + fi diff --git a/.github/workflows/links.yml b/.github/workflows/links.yml new file mode 100644 index 00000000..b66a7d50 --- /dev/null +++ b/.github/workflows/links.yml @@ -0,0 +1,93 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# Continuous Integration (CI) GitHub Actions tests broken link checker using https://github.com/lycheeverse/lychee +# Ignores the following status codes to reduce false positives: +# - 401(Vimeo, 'unauthorized') +# - 403(OpenVINO, 'forbidden') +# - 429(Instagram, 'too many requests') +# - 500(Zenodo, 'cached') +# - 502(Zenodo, 'bad gateway') +# - 999(LinkedIn, 'unknown status code') + +name: Check Broken links + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" # runs at 00:00 UTC every day + +jobs: + Links: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download and install lychee + run: | + LYCHEE_URL=$(curl -s https://api.github.com/repos/lycheeverse/lychee/releases/latest | grep "browser_download_url" | grep "x86_64-unknown-linux-gnu.tar.gz" | cut -d '"' -f 4) + curl -L $LYCHEE_URL -o lychee.tar.gz + tar xzf lychee.tar.gz + sudo mv lychee /usr/local/bin + + - name: Test Markdown and HTML links with retry + uses: ultralytics/actions/retry@main + with: + timeout_minutes: 60 + retry_delay_seconds: 900 + retries: 2 + run: | + lychee \ + --scheme https \ + --timeout 60 \ + --insecure \ + --accept 401,403,429,500,502,999 \ + --exclude-all-private \ + --exclude 'https?://(www\.)?(linkedin\.com|twitter\.com|instagram\.com|kaggle\.com|fonts\.gstatic\.com|url\.com)' \ + --exclude-path docs/zh \ + --exclude-path docs/es \ + --exclude-path docs/ru \ + --exclude-path docs/pt \ + --exclude-path docs/fr \ + --exclude-path docs/de \ + --exclude-path docs/ja \ + --exclude-path docs/ko \ + --exclude-path docs/hi \ + --exclude-path docs/ar \ + --github-token ${{ secrets.GITHUB_TOKEN }} \ + --header "User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36" \ + './**/*.md' \ + './**/*.html' + + - name: Test Markdown, HTML, YAML, Python and Notebook links with retry + if: github.event_name == 'workflow_dispatch' + uses: ultralytics/actions/retry@main + with: + timeout_minutes: 60 + retry_delay_seconds: 900 + retries: 2 + run: | + lychee \ + --scheme https \ + --timeout 60 \ + --insecure \ + --accept 401,403,429,500,502,999 \ + --exclude-all-private \ + --exclude 'https?://(www\.)?(linkedin\.com|twitter\.com|instagram\.com|kaggle\.com|fonts\.gstatic\.com|url\.com)' \ + --exclude-path '**/ci.yaml' \ + --exclude-path docs/zh \ + --exclude-path docs/es \ + --exclude-path docs/ru \ + --exclude-path docs/pt \ + --exclude-path docs/fr \ + --exclude-path docs/de \ + --exclude-path docs/ja \ + --exclude-path docs/ko \ + --exclude-path docs/hi \ + --exclude-path docs/ar \ + --github-token ${{ secrets.GITHUB_TOKEN }} \ + --header "User-Agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.183 Safari/537.36" \ + './**/*.md' \ + './**/*.html' \ + './**/*.yml' \ + './**/*.yaml' \ + './**/*.py' \ + './**/*.ipynb' diff --git a/.github/workflows/merge-main-into-prs.yml b/.github/workflows/merge-main-into-prs.yml new file mode 100644 index 00000000..68ecf947 --- /dev/null +++ b/.github/workflows/merge-main-into-prs.yml @@ -0,0 +1,87 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# Automatically merges repository 'main' branch into all open PRs to keep them up-to-date +# Action runs on updates to main branch so when one PR merges to main all others update + +name: Merge main into PRs + +on: + workflow_dispatch: + # push: + # branches: + # - ${{ github.event.repository.default_branch }} + +jobs: + Merge: + if: github.repository == 'ultralytics/ultralytics' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + cache: "pip" + - name: Install requirements + run: | + pip install pygithub + - name: Merge default branch into PRs + shell: python + run: | + from github import Github + import os + import time + + g = Github("${{ secrets._GITHUB_TOKEN }}") + repo = g.get_repo("${{ github.repository }}") + + # Fetch the default branch name + default_branch_name = repo.default_branch + default_branch = repo.get_branch(default_branch_name) + + # Initialize counters + updated_branches = 0 + up_to_date_branches = 0 + errors = 0 + + for pr in repo.get_pulls(state='open', sort='created'): + try: + # Label PRs as popular for positive reactions + reactions = pr.as_issue().get_reactions() + if sum([(1 if r.content not in {"-1", "confused"} else 0) for r in reactions]) > 5: + pr.set_labels(*("popular",) + tuple(l.name for l in pr.get_labels())) + + # Get full names for repositories and branches + base_repo_name = repo.full_name + head_repo_name = pr.head.repo.full_name + base_branch_name = pr.base.ref + head_branch_name = pr.head.ref + + # Check if PR is behind the default branch + comparison = repo.compare(default_branch.commit.sha, pr.head.sha) + if comparison.behind_by > 0: + print(f"⚠️ PR #{pr.number} ({head_repo_name}:{head_branch_name} -> {base_repo_name}:{base_branch_name}) is behind {default_branch_name} by {comparison.behind_by} commit(s).") + + # Attempt to update the branch + try: + success = pr.update_branch() + assert success, "Branch update failed" + print(f"✅ Successfully merged '{default_branch_name}' into PR #{pr.number} ({head_repo_name}:{head_branch_name} -> {base_repo_name}:{base_branch_name}).") + updated_branches += 1 + time.sleep(10) # rate limit merges + except Exception as update_error: + print(f"❌ Could not update PR #{pr.number} ({head_repo_name}:{head_branch_name} -> {base_repo_name}:{base_branch_name}): {update_error}") + errors += 1 + else: + print(f"✅ PR #{pr.number} ({head_repo_name}:{head_branch_name} -> {base_repo_name}:{base_branch_name}) is already up to date with {default_branch_name}, no merge required.") + up_to_date_branches += 1 + except Exception as e: + print(f"❌ Could not process PR #{pr.number}: {e}") + errors += 1 + + # Print summary + print("\n\nSummary:") + print(f"Branches updated: {updated_branches}") + print(f"Branches already up-to-date: {up_to_date_branches}") + print(f"Total errors: {errors}") diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..c2711aa4 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,102 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license +# Publish pip package to PyPI https://pypi.org/project/ultralytics/ + +name: Publish to PyPI + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + pypi: + type: boolean + description: Publish to PyPI + +jobs: + publish: + if: github.repository == 'ultralytics/ultralytics' && github.actor == 'glenn-jocher' + name: Publish + runs-on: ubuntu-latest + environment: # for GitHub Deployments tab + name: Release - PyPI + url: https://pypi.org/p/ultralytics + permissions: + id-token: write # for PyPI trusted publishing + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets._GITHUB_TOKEN }} + - name: Git config + run: | + git config --global user.name "UltralyticsAssistant" + git config --global user.email "web@ultralytics.com" + - name: Set up Python environment + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - uses: astral-sh/setup-uv@v4 + - name: Install dependencies + run: uv pip install --system --no-cache ultralytics-actions build twine toml + - name: Check PyPI version + shell: python + run: | + import os + from actions.utils import check_pypi_version + local_version, online_version, publish = check_pypi_version() + os.system(f'echo "increment={publish}" >> $GITHUB_OUTPUT') + os.system(f'echo "current_tag=v{local_version}" >> $GITHUB_OUTPUT') + os.system(f'echo "previous_tag=v{online_version}" >> $GITHUB_OUTPUT') + if publish: + print('Ready to publish new version to PyPI ✅.') + id: check_pypi + - name: Build package + if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True' + run: python -m build + - name: Publish to PyPI + continue-on-error: true + if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True' + uses: pypa/gh-action-pypi-publish@release/v1 + - name: Publish new tag + continue-on-error: true + if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True' + run: | + git tag -a "${{ steps.check_pypi.outputs.current_tag }}" -m "$(git log -1 --pretty=%B)" # i.e. "v0.1.2 commit message" + git push origin "${{ steps.check_pypi.outputs.current_tag }}" + - name: Publish new release + continue-on-error: true + if: (github.event_name == 'push' || github.event.inputs.pypi == 'true') && steps.check_pypi.outputs.increment == 'True' + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} + CURRENT_TAG: ${{ steps.check_pypi.outputs.current_tag }} + PREVIOUS_TAG: ${{ steps.check_pypi.outputs.previous_tag }} + run: ultralytics-actions-summarize-release + shell: bash + - name: Extract PR Details + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_JSON=$(gh pr list --search "${GITHUB_SHA}" --state merged --json number,title --jq '.[0]') + PR_NUMBER=$(echo "${PR_JSON}" | jq -r '.number') + PR_TITLE=$(echo "${PR_JSON}" | jq -r '.title') + echo "PR_NUMBER=${PR_NUMBER}" >> "${GITHUB_ENV}" + echo "PR_TITLE=${PR_TITLE}" >> "${GITHUB_ENV}" + - name: Prune uv Cache + run: uv cache prune --ci + - name: Notify on Slack (Success) + if: success() && github.event_name == 'push' && steps.check_pypi.outputs.increment == 'True' + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }} + payload: | + text: " GitHub Actions success for ${{ github.workflow }} ✅\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* NEW `${{ github.repository }} ${{ steps.check_pypi.outputs.current_tag }}` pip package published 😃\n*Job Status:* ${{ job.status }}\n*Pull Request:* ${{ env.PR_TITLE }}\n" + - name: Notify on Slack (Failure) + if: failure() + uses: slackapi/slack-github-action@v2.0.0 + with: + webhook-type: incoming-webhook + webhook: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }} + payload: | + text: " GitHub Actions error for ${{ github.workflow }} ❌\n\n\n*Repository:* https://github.com/${{ github.repository }}\n*Action:* https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\n*Author:* ${{ github.actor }}\n*Event:* ${{ github.event_name }}\n*Job Status:* ${{ job.status }}\n*Pull Request:* ${{ env.PR_TITLE }}\n" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..cfbe31ae --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,51 @@ +# Ultralytics YOLO 🚀, AGPL-3.0 license + +name: Close stale issues +on: + schedule: + - cron: "0 0 * * *" # Runs at 00:00 UTC every day + +permissions: + pull-requests: write + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + stale-issue-message: | + 👋 Hello there! We wanted to give you a friendly reminder that this issue has not had any recent activity and may be closed soon, but don't worry - you can always reopen it if needed. If you still have any questions or concerns, please feel free to let us know how we can help. + + For additional resources and information, please see the links below: + + - **Docs**: https://docs.ultralytics.com + - **HUB**: https://hub.ultralytics.com + - **Community**: https://community.ultralytics.com + + Feel free to inform us of any other **issues** you discover or **feature requests** that come to mind in the future. Pull Requests (PRs) are also always welcomed! + + Thank you for your contributions to YOLO 🚀 and Vision AI ⭐ + + stale-pr-message: | + 👋 Hello there! We wanted to let you know that we've decided to close this pull request due to inactivity. We appreciate the effort you put into contributing to our project, but unfortunately, not all contributions are suitable or aligned with our product roadmap. + + We hope you understand our decision, and please don't let it discourage you from contributing to open source projects in the future. We value all of our community members and their contributions, and we encourage you to keep exploring new projects and ways to get involved. + + For additional resources and information, please see the links below: + + - **Docs**: https://docs.ultralytics.com + - **HUB**: https://hub.ultralytics.com + - **Community**: https://community.ultralytics.com + + Thank you for your contributions to YOLO 🚀 and Vision AI ⭐ + + days-before-issue-stale: 30 + days-before-issue-close: 10 + days-before-pr-stale: 90 + days-before-pr-close: 30 + exempt-issue-labels: "documentation,tutorial,TODO" + operations-per-run: 300 # The maximum number of operations per run, used to control rate limiting.