name: Release
on:
  workflow_call:
    inputs:
      source:
        required: false
        default: ''
        type: string
      target:
        required: false
        default: ''
        type: string
      version:
        required: false
        default: ''
        type: string
      linux_armv7l:
        required: false
        default: false
        type: boolean
      prerelease:
        required: false
        default: true
        type: boolean
    secrets:
      ARCHIVE_REPO_TOKEN:
        required: false
      GPG_SIGNING_KEY:
        required: false
      RELEASE_KEY:
        required: false
  workflow_dispatch:
    inputs:
      source:
        description: |
          SOURCE of this release's updates:
          channel, repo, tag, or channel/repo@tag
          (default: <current_repo>)
        required: false
        default: ''
        type: string
      target:
        description: |
          TARGET to publish this release to:
          channel, tag, or channel@tag
          (default: <source> if writable else <current_repo>[@source_tag])
        required: false
        default: ''
        type: string
      version:
        description: |
          VERSION: yyyy.mm.dd[.rev] or rev
          (default: auto-generated)
        required: false
        default: ''
        type: string
      linux_armv7l:
        description: Include linux_armv7l
        default: true
        type: boolean
      prerelease:
        description: Pre-release
        default: false
        type: boolean

permissions: {}

env:
  GH_TELEMETRY: "false"

jobs:
  prepare:
    name: Prepare
    permissions:
      contents: read  # Push via SSH; HTTPS write token is not needed
    runs-on: ubuntu-latest
    outputs:
      channel: ${{ steps.setup_variables.outputs.channel }}
      version: ${{ steps.setup_variables.outputs.version }}
      target_repo: ${{ steps.setup_variables.outputs.target_repo }}
      target_tag: ${{ steps.setup_variables.outputs.target_tag }}
      pypi_project: ${{ steps.setup_variables.outputs.pypi_project }}
      pypi_suffix: ${{ steps.setup_variables.outputs.pypi_suffix }}
      head_sha: ${{ steps.get_target.outputs.head_sha }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: true  # Needed to git-push the release commit
          ssh-key: ${{ secrets.RELEASE_KEY }}

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: "3.13"  # Keep this in sync with test-workflows.yml

      - name: Process inputs
        id: process_inputs
        env:
          INPUTS: ${{ toJSON(inputs) }}
        run: |
          python -m devscripts.setup_variables process_inputs

      - name: Setup variables
        id: setup_variables
        env:
          INPUTS: ${{ toJSON(inputs) }}
          PROCESSED: ${{ toJSON(steps.process_inputs.outputs) }}
          REPOSITORY: ${{ github.repository }}
          PYPI_PROJECT: ${{ vars.PYPI_PROJECT }}
          SOURCE_PYPI_PROJECT: ${{ vars[format('{0}_pypi_project', steps.process_inputs.outputs.source_repo)] }}
          SOURCE_PYPI_SUFFIX: ${{ vars[format('{0}_pypi_suffix', steps.process_inputs.outputs.source_repo)] }}
          TARGET_PYPI_PROJECT: ${{ vars[format('{0}_pypi_project', steps.process_inputs.outputs.target_repo)] }}
          TARGET_PYPI_SUFFIX: ${{ vars[format('{0}_pypi_suffix', steps.process_inputs.outputs.target_repo)] }}
          SOURCE_ARCHIVE_REPO: ${{ vars[format('{0}_archive_repo', steps.process_inputs.outputs.source_repo)] }}
          TARGET_ARCHIVE_REPO: ${{ vars[format('{0}_archive_repo', steps.process_inputs.outputs.target_repo)] }}
          HAS_ARCHIVE_REPO_TOKEN: ${{ !!secrets.ARCHIVE_REPO_TOKEN }}
          HAS_RELEASE_KEY: ${{ !!secrets.RELEASE_KEY }}
        run: |
          python -m devscripts.setup_variables

      - name: Update version & documentation
        env:
          CHANNEL: ${{ steps.setup_variables.outputs.channel }}
          # Use base repo since this could be committed; build jobs will call this again with true origin
          REPOSITORY: ${{ github.repository }}
          VERSION: ${{ steps.setup_variables.outputs.version }}
        run: |
          python devscripts/update-version.py -c "${CHANNEL}" -r "${REPOSITORY}" "${VERSION}"
          python devscripts/update_changelog.py -vv
          make doc

      - name: Push release commit
        env:
          VERSION: ${{ steps.setup_variables.outputs.version }}
          GITHUB_EVENT_SENDER_LOGIN: ${{ github.event.sender.login }}
          GITHUB_EVENT_REF: ${{ github.event.ref }}
          RELEASE_KEY: ${{ secrets.RELEASE_KEY }}
        if: steps.setup_variables.outputs.target_repo == github.repository && env.RELEASE_KEY && !inputs.prerelease
        run: |
          git config --global user.name "github-actions[bot]"
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add -u
          git commit -m "Release ${VERSION}" \
            -m "Created by: ${GITHUB_EVENT_SENDER_LOGIN}" -m ":ci skip all"
          git push origin "${GITHUB_EVENT_REF}"

      - name: Get target commitish
        id: get_target
        run: |
          echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

  build:
    name: Build
    needs: [prepare]
    permissions:
      contents: read
    uses: ./.github/workflows/build.yml
    with:
      version: ${{ needs.prepare.outputs.version }}
      channel: ${{ needs.prepare.outputs.channel }}
      origin: ${{ needs.prepare.outputs.target_repo }}
      linux_armv7l: ${{ inputs.linux_armv7l }}
    secrets:
      GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}

  publish_pypi:
    name: Publish to PyPI
    needs: [prepare, build]
    if: needs.prepare.outputs.pypi_project
    permissions:
      contents: read
      id-token: write  # Needed for trusted publishing
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          fetch-depth: 0  # Needed for changelog
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: "3.13"

      - name: Install Requirements
        run: |
          sudo apt -y install pandoc man
          python -m pip install -U --require-hashes -r "bundle/requirements/requirements-pip.txt"
          python -m pip install -U --require-hashes -r "bundle/requirements/requirements-build.txt"

      - name: Prepare
        env:
          VERSION: ${{ needs.prepare.outputs.version }}
          SUFFIX: ${{ needs.prepare.outputs.pypi_suffix }}
          CHANNEL: ${{ needs.prepare.outputs.channel }}
          TARGET_REPO: ${{ needs.prepare.outputs.target_repo }}
          PYPI_PROJECT: ${{ needs.prepare.outputs.pypi_project }}
        run: |
          python devscripts/update-version.py -c "${CHANNEL}" -r "${TARGET_REPO}" -s "${SUFFIX}" "${VERSION}"
          python devscripts/update_changelog.py -vv
          python devscripts/make_lazy_extractors.py
          sed -i -E '0,/(name = ")[^"]+(")/s//\1'"${PYPI_PROJECT}"'\2/' pyproject.toml

      - name: Build
        run: |
          rm -rf dist/*
          make pypi-files
          printf '%s\n\n' \
            'Official repository: <https://github.com/yt-dlp/yt-dlp>' \
            '**PS**: Some links in this document will not work since this is a copy of the README.md from Github' > ./README.md.new
          cat ./README.md >> ./README.md.new && mv -f ./README.md.new ./README.md
          python devscripts/set-variant.py pip -M "You installed yt-dlp with pip or using the wheel from PyPi; Use that to update"
          make clean-cache
          python -m build --no-isolation .

      - name: Upload artifacts
        if: github.event.workflow != '.github/workflows/release.yml'  # Reusable workflow_call
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: build-pypi
          path: |
            dist/*
          compression-level: 0

      - name: Publish to PyPI
        if: github.event.workflow == '.github/workflows/release.yml'  # Direct workflow_dispatch
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e  # v1.13.0
        with:
          verbose: true

  publish:
    name: Publish GitHub release
    needs: [prepare, build]
    permissions:
      contents: write  # Needed by gh to publish release to Github
    runs-on: ubuntu-latest
    env:
      TARGET_REPO: ${{ needs.prepare.outputs.target_repo }}
      TARGET_TAG: ${{ needs.prepare.outputs.target_tag }}
      VERSION: ${{ needs.prepare.outputs.version }}
      HEAD_SHA: ${{ needs.prepare.outputs.head_sha }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: true  # Needed to git-push the release tag
          ssh-key: ${{ secrets.RELEASE_KEY }}
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          path: artifact
          pattern: build-*
          merge-multiple: true
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: "3.13"

      - name: Generate release notes
        env:
          REPOSITORY: ${{ github.repository }}
          BASE_REPO: yt-dlp/yt-dlp
          NIGHTLY_REPO: yt-dlp/yt-dlp-nightly-builds
          MASTER_REPO: yt-dlp/yt-dlp-master-builds
          DOCS_PATH: ${{ env.TARGET_REPO == github.repository && format('/tree/{0}', env.TARGET_TAG) || '' }}
        run: |
          printf '%s' \
            "[![Installation](https://img.shields.io/badge/-Which%20file%20to%20download%3F-white.svg?style=for-the-badge)]" \
              "(https://github.com/${REPOSITORY}#installation \"Installation instructions\") " \
            "[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&labelColor=555555&label=&logo=discord&style=for-the-badge)]" \
              "(https://discord.gg/H5MNcFW63r \"Discord\") " \
            "[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)]" \
              "(https://github.com/${BASE_REPO}/blob/master/Maintainers.md#maintainers \"Donate\") " \
            "[![Documentation](https://img.shields.io/badge/-Docs-brightgreen.svg?style=for-the-badge&logo=GitBook&labelColor=555555)]" \
              "(https://github.com/${REPOSITORY}${DOCS_PATH}#readme \"Documentation\") " > ./RELEASE_NOTES
          if [[ "${TARGET_REPO}" == "${BASE_REPO}" ]]; then
            printf '%s' \
              "[![Nightly](https://img.shields.io/badge/Nightly%20builds-purple.svg?style=for-the-badge)]" \
                "(https://github.com/${NIGHTLY_REPO}/releases/latest \"Nightly builds\") " \
              "[![Master](https://img.shields.io/badge/Master%20builds-lightblue.svg?style=for-the-badge)]" \
                "(https://github.com/${MASTER_REPO}/releases/latest \"Master builds\")" >> ./RELEASE_NOTES
          fi
          printf '\n\n%s\n\n%s%s%s\n\n---\n' \
            "#### A description of the various files is in the [README](https://github.com/${REPOSITORY}#release-files)" \
            "The zipimport Unix executable contains code licensed under ISC and MIT. " \
            "The PyInstaller-bundled executables are subject to these and other licenses, all of which are compiled in " \
            "[THIRD_PARTY_LICENSES.txt](https://github.com/${BASE_REPO}/blob/${HEAD_SHA}/THIRD_PARTY_LICENSES.txt)" >> ./RELEASE_NOTES
          python ./devscripts/make_changelog.py -vv --collapsible >> ./RELEASE_NOTES
          printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES
          cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
          printf '%s\n\n' "Generated from: https://github.com/${REPOSITORY}/commit/${HEAD_SHA}" >> ./ARCHIVE_NOTES
          cat ./RELEASE_NOTES >> ./ARCHIVE_NOTES

      - name: Publish to archive repo
        env:
          GH_TOKEN: ${{ secrets.ARCHIVE_REPO_TOKEN }}
          GH_REPO: ${{ needs.prepare.outputs.target_repo }}
          TITLE_PREFIX: ${{ startswith(env.TARGET_REPO, 'yt-dlp/') && 'yt-dlp ' || '' }}
          TITLE: ${{ inputs.target != env.TARGET_REPO && inputs.target || needs.prepare.outputs.channel }}
        if: inputs.prerelease && env.GH_TOKEN && env.GH_REPO && env.GH_REPO != github.repository
        run: |
          gh release create \
            --notes-file ARCHIVE_NOTES \
            --title "${TITLE_PREFIX}${TITLE} ${VERSION}" \
            "${VERSION}" \
            artifact/*

      - name: Prune old release
        env:
          GH_TOKEN: ${{ github.token }}
        if: env.TARGET_REPO == github.repository && env.TARGET_TAG != env.VERSION
        run: |
          gh release delete --yes --cleanup-tag "${TARGET_TAG}" || true
          git tag --delete "${TARGET_TAG}" || true
          sleep 5  # Enough time to cover deletion race condition

      - name: Push tag
        if: env.TARGET_REPO == github.repository
        run: |
          git tag "${TARGET_TAG}" "${HEAD_SHA}"
          git push origin "${TARGET_TAG}"
          sleep 5  # Enough time to cover git-push vs gh-release-create race condition

      - name: Publish release
        env:
          GH_TOKEN: ${{ github.token }}
          NOTES_FILE: ${{ inputs.prerelease && 'PRERELEASE_NOTES' || 'RELEASE_NOTES' }}
          TITLE_PREFIX: ${{ github.repository == 'yt-dlp/yt-dlp' && 'yt-dlp ' || '' }}
          TITLE: ${{ env.TARGET_TAG != env.VERSION && format('{0} ', env.TARGET_TAG) || '' }}
          PRERELEASE: ${{ inputs.prerelease && '1' || '0' }}
        if: env.TARGET_REPO == github.repository
        run: |
          gh_options=(
            --notes-file "${NOTES_FILE}"
            --verify-tag
            --title "${TITLE_PREFIX}${TITLE}${VERSION}"
          )
          if ((PRERELEASE)); then
            gh_options+=(--prerelease)
          fi
          gh release create "${gh_options[@]}" "${TARGET_TAG}" artifact/*
