# 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
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }} # use your PAT here
      - 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"
          cache: "pip" # caching pip dependencies
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip wheel
          pip install openai requests build twine toml
      - name: Check PyPI version
        shell: python
        run: |
          import os
          import requests
          import toml

          # Load version and package name from pyproject.toml
          pyproject = toml.load('pyproject.toml')
          package_name = pyproject['project']['name']
          local_version = pyproject['project'].get('version', 'dynamic')

          # If version is dynamic, extract it from the specified file
          if local_version == 'dynamic':
              version_attr = pyproject['tool']['setuptools']['dynamic']['version']['attr']
              module_path, attr_name = version_attr.rsplit('.', 1)
              with open(f"{module_path.replace('.', '/')}/__init__.py") as f:
                  local_version = next(line.split('=')[1].strip().strip("'\"") for line in f if line.startswith(attr_name))

          print(f"Local Version: {local_version}")

          # Get online version from PyPI
          response = requests.get(f"https://pypi.org/pypi/{package_name}/json")
          online_version = response.json()['info']['version'] if response.status_code == 200 else None
          print(f"Online Version: {online_version or 'Not Found'}")

          # Determine if a new version should be published
          publish = False
          if online_version:
              local_ver = tuple(map(int, local_version.split('.')))
              online_ver = tuple(map(int, online_version.split('.')))
              major_diff = local_ver[0] - online_ver[0]
              minor_diff = local_ver[1] - online_ver[1]
              patch_diff = local_ver[2] - online_ver[2]

              publish = (
                  (major_diff == 0 and minor_diff == 0 and 0 < patch_diff <= 2) or
                  (major_diff == 0 and minor_diff == 1 and local_ver[2] == 0) or
                  (major_diff == 1 and local_ver[1] == 0 and local_ver[2] == 0)
              )
          else:
              publish = True  # First release

          os.system(f'echo "increment={publish}" >> $GITHUB_OUTPUT')
          os.system(f'echo "version={local_version}" >> $GITHUB_OUTPUT')
          os.system(f'echo "previous_version={online_version or "N/A"}" >> $GITHUB_OUTPUT')

          if publish:
              print('Ready to publish new version to PyPI ✅.')
        id: check_pypi
      - name: Publish new tag
        if: (github.event_name == 'push' || github.event.inputs.pypi == 'true')  && steps.check_pypi.outputs.increment == 'True'
        run: |
          git tag -a "v${{ steps.check_pypi.outputs.version }}" -m "$(git log -1 --pretty=%B)"  # i.e. "v0.1.2 commit message"
          git push origin "v${{ steps.check_pypi.outputs.version }}"
      - name: Publish new release
        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.PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
          CURRENT_TAG: ${{ steps.check_pypi.outputs.version }}
          PREVIOUS_TAG: ${{ steps.check_pypi.outputs.previous_version }}
        shell: python
        run: |
          import openai
          import os
          import requests
          import json
          import subprocess

          # Retrieve environment variables
          OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
          GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
          CURRENT_TAG = os.getenv('CURRENT_TAG')
          PREVIOUS_TAG = os.getenv('PREVIOUS_TAG')

          # Check for required environment variables
          if not all([OPENAI_API_KEY, GITHUB_TOKEN, CURRENT_TAG, PREVIOUS_TAG]):
              raise ValueError("One or more required environment variables are missing.")

          latest_tag = f"v{CURRENT_TAG}"
          previous_tag = f"v{PREVIOUS_TAG}"
          repo = os.getenv('GITHUB_REPOSITORY')
          headers = {"Authorization": f"token {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3.diff"}

          # Get the diff between the tags
          url = f"https://api.github.com/repos/{repo}/compare/{previous_tag}...{latest_tag}"
          response = requests.get(url, headers=headers)
          diff = response.text if response.status_code == 200 else f"Failed to get diff: {response.content}"

          # Get summary
          messages = [
              {
                  "role": "system",
                  "content": "You are an Ultralytics AI assistant skilled in software development and technical communication. Your task is to summarize GitHub releases in a way that is detailed, accurate, and understandable to both expert developers and non-expert users. Focus on highlighting the key changes and their impact in simple and intuitive terms."
              },
              {
                  "role": "user",
                  "content": f"Summarize the updates made in the '{latest_tag}' tag, focusing on major changes, their purpose, and potential impact. Keep the summary clear and suitable for a broad audience. Add emojis to enliven the summary. Reply directly with a summary along these example guidelines, though feel free to adjust as appropriate:\n\n"
                             f"## 🌟 Summary (single-line synopsis)\n"
                             f"## 📊 Key Changes (bullet points highlighting any major changes)\n"
                             f"## 🎯 Purpose & Impact (bullet points explaining any benefits and potential impact to users)\n"
                             f"\n\nHere's the release diff:\n\n{diff[:300000]}",
              }
          ]
          client = openai.OpenAI(api_key=OPENAI_API_KEY)
          completion = client.chat.completions.create(model="gpt-4o-2024-08-06", messages=messages)
          summary = completion.choices[0].message.content.strip()

          # Get the latest commit message
          commit_message = subprocess.run(['git', 'log', '-1', '--pretty=%B'], check=True, text=True, capture_output=True).stdout.split("\n")[0].strip()

          # Prepare release data
          release = {
              'tag_name': latest_tag,
              'name': f"{latest_tag} - {commit_message}",
              'body': summary,
              'draft': False,
              'prerelease': False
          }

          # Create the release on GitHub
          release_url = f"https://api.github.com/repos/{repo}/releases"
          release_response = requests.post(release_url, headers=headers, data=json.dumps(release))
          if release_response.status_code == 201:
              print(f'Successfully created release {latest_tag}')
          else:
              print(f'Failed to create release {latest_tag}: {release_response.content}')
      - name: Publish to PyPI
        continue-on-error: true
        if: (github.event_name == 'push' || github.event.inputs.pypi == 'true')  && steps.check_pypi.outputs.increment == 'True'
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: |
          python -m build
          python -m twine upload dist/* -u __token__ -p $PYPI_TOKEN
      - name: Extract PR Details
        env:
          GH_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN || secrets.GITHUB_TOKEN }}
        run: |
          # Check if the event is a pull request or pull_request_target
          if [ "${{ github.event_name }}" = "pull_request" ] || [ "${{ github.event_name }}" = "pull_request_target" ]; then
            PR_NUMBER=${{ github.event.pull_request.number }}
            PR_TITLE=$(gh pr view $PR_NUMBER --json title --jq '.title')
          else
            # Use gh to find the PR associated with the commit
            COMMIT_SHA=${{ github.event.after }}
            PR_JSON=$(gh pr list --search "${COMMIT_SHA}" --state merged --json number,title --jq '.[0]')
            PR_NUMBER=$(echo $PR_JSON | jq -r '.number')
            PR_TITLE=$(echo $PR_JSON | jq -r '.title')
          fi
          echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV
          echo "PR_TITLE=$PR_TITLE" >> $GITHUB_ENV
      - name: Notify on Slack (Success)
        if: success() && github.event_name == 'push' && steps.check_pypi.outputs.increment == 'True'
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {"text": "<!channel> 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 }} v${{ steps.check_pypi.outputs.version }}' pip package published 😃\n*Job Status:* ${{ job.status }}\n*Pull Request:* <https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}> ${{ env.PR_TITLE }}\n"}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
      - name: Notify on Slack (Failure)
        if: failure()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          payload: |
            {"text": "<!channel> 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:* <https://github.com/${{ github.repository }}/pull/${{ env.PR_NUMBER }}> ${{ env.PR_TITLE }}\n"}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_YOLO }}
