yt-dlp/yt-dlp

11 workflows · maturity 83% · 13 patterns · GitHub ↗

Security 33.33/100

Security dimensions

permissions
25
security scan
8.3
supply chain
0
secret handling
0
harden runner
0

Tools: github/codeql-action/analyze, github/codeql-action/init

Workflows (11)

build matrix perms .github/workflows/build.yml
Triggers
workflow_call, workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest, ${{ matrix.runner }}, macos-14, ${{ matrix.runner }}, ubuntu-latest
Jobs
process, unix, linux, macos, windows, meta_files
Matrix
include, include.arch, include.python_version, include.runner→ ${{ fromJSON(needs.process.outputs.linux_matrix) }}, 3.10, 3.13, arm64, windows-11-arm, windows-2025, x64, x86
Actions
docker/setup-qemu-action
Commands
  • import datetime as dt import json import os import re INPUTS = json.loads(os.environ['INPUTS']) timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f') version = INPUTS.get('version') if version and '.' not in version: # build.yml was dispatched with only a revision as the version input value version_parts = [*timestamp.split('.')[:3], version] elif not version: # build.yml was dispatched without any version input value, so include .HHMMSS revision version_parts = timestamp.split('.')[:4] else: # build.yml was called or dispatched with a complete version input value version_parts = version.split('.') assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric' outputs = { 'origin': INPUTS.get('origin') or os.environ['REPOSITORY'], 'timestamp': timestamp, 'version': '.'.join(version_parts), } print(json.dumps(outputs, indent=2)) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write('\n'.join(f'{key}={value}' for key, value in outputs.items()))
  • import json import os EXE_MAP = { 'linux': [{ 'os': 'linux', 'arch': 'x86_64', 'runner': 'ubuntu-24.04', }, { 'os': 'linux', 'arch': 'aarch64', 'runner': 'ubuntu-24.04-arm', }], 'linux_armv7l': [{ 'os': 'linux', 'arch': 'armv7l', 'runner': 'ubuntu-24.04-arm', 'qemu_platform': 'linux/arm/v7', 'onefile': False, 'update_to': 'yt-dlp/yt-dlp@2023.03.04', }], 'musllinux': [{ 'os': 'musllinux', 'arch': 'x86_64', 'runner': 'ubuntu-24.04', 'python_version': '3.14', }, { 'os': 'musllinux', 'arch': 'aarch64', 'runner': 'ubuntu-24.04-arm', 'python_version': '3.14', }], } INPUTS = json.loads(os.environ['INPUTS']) matrix = [exe for key, group in EXE_MAP.items() for exe in group if INPUTS.get(key)] if not matrix: # If we send an empty matrix when no linux inputs are given, the entire workflow fails matrix = [EXE_MAP['linux'][0]] for exe in matrix: exe['exe'] = '_'.join(filter(None, ( 'yt-dlp', exe['os'], exe['arch'] != 'x86_64' and exe['arch'], ))) exe.setdefault('qemu_platform', None) exe.setdefault('onefile', True) exe.setdefault('onedir', True) exe.setdefault('python_version', os.environ['PYTHON_VERSION']) exe.setdefault('update_to', os.environ['UPDATE_TO']) if not any(INPUTS.get(key) for key in EXE_MAP): print('skipping linux job') else: print(json.dumps(matrix, indent=2)) with open(os.environ['GITHUB_OUTPUT'], 'a') as f: f.write(f'matrix={json.dumps(matrix)}')
  • sudo apt -y install zip pandoc man sed
  • python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}" python devscripts/update_changelog.py -vv python devscripts/make_lazy_extractors.py
  • make all-extra tar
  • chmod +x ./yt-dlp cp ./yt-dlp ./yt-dlp_downgraded version="$(./yt-dlp --version)" ./yt-dlp_downgraded -v --update-to "${UPDATE_TO}" downgraded_version="$(./yt-dlp_downgraded --version)" [[ "${version}" != "${downgraded_version}" ]]
  • mkdir -p ./dist pushd bundle/docker docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}" popd if [[ -z "${SKIP_ONEFILE_BUILD}" ]]; then sudo chown "${USER}:docker" "./dist/${EXE_NAME}" fi
  • cd bundle/docker docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
View raw YAML
name: Build Artifacts
on:
  workflow_call:
    inputs:
      version:
        required: true
        type: string
      channel:
        required: false
        default: stable
        type: string
      origin:
        required: true
        type: string
      unix:
        default: true
        type: boolean
      linux:
        default: true
        type: boolean
      linux_armv7l:
        default: true
        type: boolean
      musllinux:
        default: true
        type: boolean
      macos:
        default: true
        type: boolean
      windows:
        default: true
        type: boolean
    secrets:
      GPG_SIGNING_KEY:
        required: false

  workflow_dispatch:
    inputs:
      version:
        description: |
          VERSION: yyyy.mm.dd[.rev] or rev
          (default: auto-generated)
        required: false
        default: ''
        type: string
      channel:
        description: |
          SOURCE of this build's updates: stable/nightly/master/<repo>
        required: true
        default: stable
        type: string
      unix:
        description: yt-dlp, yt-dlp.tar.gz
        default: true
        type: boolean
      linux:
        description: yt-dlp_linux, yt-dlp_linux.zip, yt-dlp_linux_aarch64, yt-dlp_linux_aarch64.zip
        default: true
        type: boolean
      linux_armv7l:
        description: yt-dlp_linux_armv7l.zip
        default: true
        type: boolean
      musllinux:
        description: yt-dlp_musllinux, yt-dlp_musllinux.zip, yt-dlp_musllinux_aarch64, yt-dlp_musllinux_aarch64.zip
        default: true
        type: boolean
      macos:
        description: yt-dlp_macos, yt-dlp_macos.zip
        default: true
        type: boolean
      windows:
        description: yt-dlp.exe, yt-dlp_win.zip, yt-dlp_x86.exe, yt-dlp_win_x86.zip, yt-dlp_arm64.exe, yt-dlp_win_arm64.zip
        default: true
        type: boolean

permissions: {}

jobs:
  process:
    name: Process
    runs-on: ubuntu-latest
    outputs:
      origin: ${{ steps.process_inputs.outputs.origin }}
      timestamp: ${{ steps.process_inputs.outputs.timestamp }}
      version: ${{ steps.process_inputs.outputs.version }}
      linux_matrix: ${{ steps.linux_matrix.outputs.matrix }}

    steps:
      - name: Process inputs
        id: process_inputs
        env:
          INPUTS: ${{ toJSON(inputs) }}
          REPOSITORY: ${{ github.repository }}
        shell: python
        run: |
          import datetime as dt
          import json
          import os
          import re
          INPUTS = json.loads(os.environ['INPUTS'])
          timestamp = dt.datetime.now(tz=dt.timezone.utc).strftime('%Y.%m.%d.%H%M%S.%f')
          version = INPUTS.get('version')
          if version and '.' not in version:
              # build.yml was dispatched with only a revision as the version input value
              version_parts = [*timestamp.split('.')[:3], version]
          elif not version:
              # build.yml was dispatched without any version input value, so include .HHMMSS revision
              version_parts = timestamp.split('.')[:4]
          else:
              # build.yml was called or dispatched with a complete version input value
              version_parts = version.split('.')
          assert all(re.fullmatch(r'[0-9]+', part) for part in version_parts), 'Version must be numeric'
          outputs = {
              'origin': INPUTS.get('origin') or os.environ['REPOSITORY'],
              'timestamp': timestamp,
              'version': '.'.join(version_parts),
          }
          print(json.dumps(outputs, indent=2))
          with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
              f.write('\n'.join(f'{key}={value}' for key, value in outputs.items()))

      - name: Build Linux matrix
        id: linux_matrix
        env:
          INPUTS: ${{ toJSON(inputs) }}
          PYTHON_VERSION: '3.13'
          UPDATE_TO: yt-dlp/yt-dlp@2025.09.05
        shell: python
        run: |
          import json
          import os
          EXE_MAP = {
              'linux': [{
                  'os': 'linux',
                  'arch': 'x86_64',
                  'runner': 'ubuntu-24.04',
              }, {
                  'os': 'linux',
                  'arch': 'aarch64',
                  'runner': 'ubuntu-24.04-arm',
              }],
              'linux_armv7l': [{
                  'os': 'linux',
                  'arch': 'armv7l',
                  'runner': 'ubuntu-24.04-arm',
                  'qemu_platform': 'linux/arm/v7',
                  'onefile': False,
                  'update_to': 'yt-dlp/yt-dlp@2023.03.04',
              }],
              'musllinux': [{
                  'os': 'musllinux',
                  'arch': 'x86_64',
                  'runner': 'ubuntu-24.04',
                  'python_version': '3.14',
              }, {
                  'os': 'musllinux',
                  'arch': 'aarch64',
                  'runner': 'ubuntu-24.04-arm',
                  'python_version': '3.14',
              }],
          }
          INPUTS = json.loads(os.environ['INPUTS'])
          matrix = [exe for key, group in EXE_MAP.items() for exe in group if INPUTS.get(key)]
          if not matrix:
              # If we send an empty matrix when no linux inputs are given, the entire workflow fails
              matrix = [EXE_MAP['linux'][0]]
          for exe in matrix:
              exe['exe'] = '_'.join(filter(None, (
                  'yt-dlp',
                  exe['os'],
                  exe['arch'] != 'x86_64' and exe['arch'],
              )))
              exe.setdefault('qemu_platform', None)
              exe.setdefault('onefile', True)
              exe.setdefault('onedir', True)
              exe.setdefault('python_version', os.environ['PYTHON_VERSION'])
              exe.setdefault('update_to', os.environ['UPDATE_TO'])
          if not any(INPUTS.get(key) for key in EXE_MAP):
              print('skipping linux job')
          else:
              print(json.dumps(matrix, indent=2))
          with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
              f.write(f'matrix={json.dumps(matrix)}')

  unix:
    name: unix
    needs: [process]
    if: inputs.unix
    permissions:
      contents: read
    runs-on: ubuntu-latest
    env:
      CHANNEL: ${{ inputs.channel }}
      ORIGIN: ${{ needs.process.outputs.origin }}
      VERSION: ${{ needs.process.outputs.version }}
      UPDATE_TO: yt-dlp/yt-dlp@2025.09.05

    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.10"

      - name: Install Requirements
        run: |
          sudo apt -y install zip pandoc man sed

      - name: Prepare
        run: |
          python devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}"
          python devscripts/update_changelog.py -vv
          python devscripts/make_lazy_extractors.py

      - name: Build Unix platform-independent binary
        run: |
          make all-extra tar

      - name: Verify --update-to
        if: vars.UPDATE_TO_VERIFICATION
        run: |
          chmod +x ./yt-dlp
          cp ./yt-dlp ./yt-dlp_downgraded
          version="$(./yt-dlp --version)"
          ./yt-dlp_downgraded -v --update-to "${UPDATE_TO}"
          downgraded_version="$(./yt-dlp_downgraded --version)"
          [[ "${version}" != "${downgraded_version}" ]]

      - name: Upload artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: build-bin-${{ github.job }}
          path: |
            yt-dlp
            yt-dlp.tar.gz
          compression-level: 0

  linux:
    name: ${{ matrix.os }} (${{ matrix.arch }})
    needs: [process]
    if: inputs.linux || inputs.linux_armv7l || inputs.musllinux
    permissions:
      contents: read
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include: ${{ fromJSON(needs.process.outputs.linux_matrix) }}
    env:
      CHANNEL: ${{ inputs.channel }}
      ORIGIN: ${{ needs.process.outputs.origin }}
      VERSION: ${{ needs.process.outputs.version }}
      EXE_NAME: ${{ matrix.exe }}
      PYTHON_VERSION: ${{ matrix.python_version }}
      UPDATE_TO: ${{ (vars.UPDATE_TO_VERIFICATION && matrix.update_to) || '' }}
      SKIP_ONEDIR_BUILD: ${{ (!matrix.onedir && '1') || '' }}
      SKIP_ONEFILE_BUILD: ${{ (!matrix.onefile && '1') || '' }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false

      - name: Set up QEMU
        if: matrix.qemu_platform
        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a  # v4.0.0
        with:
          image: tonistiigi/binfmt:qemu-v10.0.4-56@sha256:30cc9a4d03765acac9be2ed0afc23af1ad018aed2c28ea4be8c2eb9afe03fbd1
          cache-image: false
          platforms: ${{ matrix.qemu_platform }}

      - name: Build executable
        env:
          SERVICE: ${{ matrix.os }}_${{ matrix.arch }}
        run: |
          mkdir -p ./dist
          pushd bundle/docker
          docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"
          popd
          if [[ -z "${SKIP_ONEFILE_BUILD}" ]]; then
            sudo chown "${USER}:docker" "./dist/${EXE_NAME}"
          fi

      - name: Verify executable in container
        env:
          SERVICE: ${{ matrix.os }}_${{ matrix.arch }}_verify
        run: |
          cd bundle/docker
          docker compose up --build --exit-code-from "${SERVICE}" "${SERVICE}"

      - name: Upload artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: build-bin-${{ matrix.os }}_${{ matrix.arch }}
          path: |
            dist/${{ matrix.exe }}*
          compression-level: 0

  macos:
    name: macos
    needs: [process]
    if: inputs.macos
    permissions:
      contents: read
    runs-on: macos-14
    env:
      CHANNEL: ${{ inputs.channel }}
      ORIGIN: ${{ needs.process.outputs.origin }}
      VERSION: ${{ needs.process.outputs.version }}
      UPDATE_TO: yt-dlp/yt-dlp@2025.09.05

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false

      # NB: Building universal2 does not work with python from actions/setup-python

      - name: Install Requirements
        run: |
          brew install coreutils
          # We need to use system Python in order to roll our own universal2 curl_cffi wheel
          brew uninstall --ignore-dependencies python3
          python3 -m venv ~/yt-dlp-build-venv
          source ~/yt-dlp-build-venv/bin/activate
          python3 -m pip install -U --require-hashes -r "bundle/requirements/requirements-pip.txt"
          rm -rf build
          # Only directly install wheels for "macosx_10_15_universal2" and "any" platforms
          mkdir -p build/wheels
          python3 -m pip download \
            --only-binary=:all: \
            --platform=macosx_10_15_universal2 \
            --platform=any \
            -d build/wheels \
            --require-hashes \
            -r "bundle/requirements/requirements-macos.txt"
          python3 -m pip install --force-reinstall --no-deps -U build/wheels/*.whl
          rm -rf build/wheels/*
          # We need to fuse our own universal2 wheels for curl_cffi and cffi
          mkdir -p build/universal2
          for platform in "macosx_11_0_arm64" "macosx_11_0_x86_64"; do
            python3 -m pip download \
              --no-deps \
              --only-binary=:all: \
              --platform "${platform}" \
              -d build/wheels \
               --require-hashes \
              -r "bundle/requirements/requirements-macos-curl_cffi.txt"
          done
          # Overwrite x86_64-only libs with fat/universal2 libs or else PyInstaller will do the opposite
          # See https://github.com/yt-dlp/yt-dlp/pull/10069
          pushd build/wheels
          mkdir -p curl_cffi/.dylibs
          python_libdir=$(python3 -c 'import sys; from pathlib import Path; print(Path(sys.path[1]).parent)')
          for dylib in lib{ssl,crypto}.3.dylib; do
            cp "${python_libdir}/${dylib}" "curl_cffi/.dylibs/${dylib}"
            for wheel in curl_cffi*macos*x86_64.whl; do
              zip "${wheel}" "curl_cffi/.dylibs/${dylib}"
            done
          done
          popd
          python3 -m delocate.cmd.delocate_fuse build/wheels/curl_cffi*.whl -w build/universal2
          python3 -m delocate.cmd.delocate_fuse build/wheels/cffi*.whl -w build/universal2
          for wheel in build/universal2/*cffi*.whl; do
            mv -n -- "${wheel}" "${wheel/x86_64/universal2}"
          done
          python3 -m pip install --force-reinstall --no-deps -U build/universal2/*cffi*.whl
          rm -rf build

      - name: Prepare
        run: |
          python3 devscripts/update-version.py -c "${CHANNEL}" -r "${ORIGIN}" "${VERSION}"
          python3 devscripts/make_lazy_extractors.py
      - name: Build
        run: |
          source ~/yt-dlp-build-venv/bin/activate
          python3 -m bundle.pyinstaller --target-architecture universal2 --onedir
          (cd ./dist/yt-dlp_macos && zip -r ../yt-dlp_macos.zip .)
          python3 -m bundle.pyinstaller --target-architecture universal2

      - name: Verify --update-to
        if: vars.UPDATE_TO_VERIFICATION
        run: |
          chmod +x ./dist/yt-dlp_macos
          cp ./dist/yt-dlp_macos ./dist/yt-dlp_macos_downgraded
          version="$(./dist/yt-dlp_macos --version)"
          ./dist/yt-dlp_macos_downgraded -v --update-to "${UPDATE_TO}"
          downgraded_version="$(./dist/yt-dlp_macos_downgraded --version)"
          [[ "$version" != "$downgraded_version" ]]

      - name: Upload artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: build-bin-${{ github.job }}
          path: |
            dist/yt-dlp_macos
            dist/yt-dlp_macos.zip
          compression-level: 0

  windows:
    name: windows (${{ matrix.arch }})
    needs: [process]
    if: inputs.windows
    permissions:
      contents: read
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
        - arch: 'x64'
          runner: windows-2025
          python_version: '3.10'
        - arch: 'x86'
          runner: windows-2025
          python_version: '3.10'
        - arch: 'arm64'
          runner: windows-11-arm
          python_version: '3.13'  # arm64 only has Python >= 3.11 available
    env:
      CHANNEL: ${{ inputs.channel }}
      ORIGIN: ${{ needs.process.outputs.origin }}
      VERSION: ${{ needs.process.outputs.version }}
      SUFFIX: ${{ (matrix.arch != 'x64' && format('_{0}', matrix.arch)) || '' }}
      UPDATE_TO: yt-dlp/yt-dlp@2025.09.05

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: ${{ matrix.python_version }}
          architecture: ${{ matrix.arch }}

      - name: Install Requirements
        env:
          ARCH: ${{ matrix.arch }}
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          python -m venv /yt-dlp-build-venv
          /yt-dlp-build-venv/Scripts/Activate.ps1
          python -m pip install -U --require-hashes -r "bundle/requirements/requirements-pip.txt"
          python -m pip install -U --require-hashes -r "bundle/requirements/requirements-win-${Env:ARCH}-pyinstaller.txt"
          python -m pip install -U --require-hashes -r "bundle/requirements/requirements-win-${Env:ARCH}.txt"

      - name: Prepare
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          python devscripts/update-version.py -c "${Env:CHANNEL}" -r "${Env:ORIGIN}" "${Env:VERSION}"
          python devscripts/make_lazy_extractors.py

      - name: Build
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          /yt-dlp-build-venv/Scripts/Activate.ps1
          python -m bundle.pyinstaller
          python -m bundle.pyinstaller --onedir
          Compress-Archive -Path ./dist/yt-dlp${Env:SUFFIX}/* -DestinationPath ./dist/yt-dlp_win${Env:SUFFIX}.zip

      - name: Verify --update-to
        if: vars.UPDATE_TO_VERIFICATION
        shell: pwsh
        run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          $name = "yt-dlp${Env:SUFFIX}"
          Copy-Item "./dist/${name}.exe" "./dist/${name}_downgraded.exe"
          $version = & "./dist/${name}.exe" --version
          & "./dist/${name}_downgraded.exe" -v --update-to "${Env:UPDATE_TO}"
          $downgraded_version = & "./dist/${name}_downgraded.exe" --version
          if ($version -eq $downgraded_version) {
            exit 1
          }

      - name: Upload artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: build-bin-${{ github.job }}-${{ matrix.arch }}
          path: |
            dist/yt-dlp${{ env.SUFFIX }}.exe
            dist/yt-dlp_win${{ env.SUFFIX }}.zip
          compression-level: 0

  meta_files:
    name: Metadata files
    needs:
      - process
      - unix
      - linux
      - macos
      - windows
    if: always() && !failure() && !cancelled()
    runs-on: ubuntu-latest
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          path: artifact
          pattern: build-bin-*
          merge-multiple: true

      - name: Make SHA2-SUMS files
        shell: bash
        run: |
          cd ./artifact/
          # make sure SHA sums are also printed to stdout
          sha256sum -- * | tee ../SHA2-256SUMS
          sha512sum -- * | tee ../SHA2-512SUMS
          # also print as permanent annotations to the summary page
          while read -r shasum; do
            echo "::notice title=${shasum##* }::sha256: ${shasum% *}"
          done < ../SHA2-256SUMS

      - name: Make Update spec
        run: |
          cat >> _update_spec << EOF
          # This file is used for regulating self-update
          lock 2022.08.18.36 .+ Python 3\.6
          lock 2023.11.16 zip Python 3\.7
          lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
          lock 2024.10.22 py2exe .+
          lock 2024.10.22 zip Python 3\.8
          lock 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
          lock 2025.08.11 darwin_legacy_exe .+
          lock 2025.08.27 linux_armv7l_exe .+
          lock 2025.10.14 zip Python 3\.9
          lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
          lockV2 yt-dlp/yt-dlp 2023.11.16 zip Python 3\.7
          lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
          lockV2 yt-dlp/yt-dlp 2024.10.22 py2exe .+
          lockV2 yt-dlp/yt-dlp 2024.10.22 zip Python 3\.8
          lockV2 yt-dlp/yt-dlp 2024.10.22 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
          lockV2 yt-dlp/yt-dlp 2025.08.11 darwin_legacy_exe .+
          lockV2 yt-dlp/yt-dlp 2025.08.27 linux_armv7l_exe .+
          lockV2 yt-dlp/yt-dlp 2025.10.14 zip Python 3\.9
          lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 zip Python 3\.7
          lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server)
          lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 py2exe .+
          lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 zip Python 3\.8
          lockV2 yt-dlp/yt-dlp-nightly-builds 2024.10.22.051025 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
          lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.12.233030 darwin_legacy_exe .+
          lockV2 yt-dlp/yt-dlp-nightly-builds 2025.08.30.232839 linux_armv7l_exe .+
          lockV2 yt-dlp/yt-dlp-nightly-builds 2025.10.14.232845 zip Python 3\.9
          lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 zip Python 3\.7
          lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server)
          lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.045052 py2exe .+
          lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 zip Python 3\.8
          lockV2 yt-dlp/yt-dlp-master-builds 2024.10.22.060347 win(?:_x86)?_exe Python 3\.[78].+ Windows-(?:7-|2008ServerR2)
          lockV2 yt-dlp/yt-dlp-master-builds 2025.08.12.232447 darwin_legacy_exe .+
          lockV2 yt-dlp/yt-dlp-master-builds 2025.09.05.212910 linux_armv7l_exe .+
          lockV2 yt-dlp/yt-dlp-master-builds 2025.10.14.232330 zip Python 3\.9
          EOF

      - name: Sign checksum files
        env:
          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
        if: env.GPG_SIGNING_KEY
        run: |
          gpg --batch --import <<< "${GPG_SIGNING_KEY}"
          for signfile in ./SHA*SUMS; do
            gpg --batch --detach-sign "$signfile"
          done

      - name: Upload artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v7.0.0
        with:
          name: build-${{ github.job }}
          path: |
            _update_spec
            SHA*SUMS*
          compression-level: 0
          overwrite: true
challenge-tests matrix perms .github/workflows/challenge-tests.yml
Triggers
push, pull_request
Runs on
${{ matrix.os }}
Jobs
tests
Matrix
os, python-version→ 3.10, 3.11, 3.12, 3.13, 3.14, pypy-3.11, ubuntu-latest, windows-latest
Actions
denoland/setup-deno, oven-sh/setup-bun
Commands
  • wget "https://bellard.org/quickjs/binary_releases/quickjs-linux-x86_64-${QJS_VERSION}.zip" -O quickjs.zip unzip quickjs.zip qjs sudo install qjs /usr/local/bin/qjs
  • $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $true Invoke-WebRequest "https://bellard.org/quickjs/binary_releases/quickjs-win-x86_64-${Env:QJS_VERSION}.zip" -OutFile quickjs.zip unzip quickjs.zip
  • python ./devscripts/install_deps.py --print --omit-default --include-group test > requirements.txt python ./devscripts/install_deps.py --print -c certifi -c requests -c urllib3 -c yt-dlp-ejs >> requirements.txt python -m pip install -U -r requirements.txt
  • python -m yt_dlp -v --js-runtimes node --js-runtimes bun --js-runtimes quickjs || true python ./devscripts/run_tests.py test/test_jsc -k download
View raw YAML
name: Challenge Tests
on:
  push:
    paths:
      - .github/workflows/challenge-tests.yml
      - test/test_jsc/*.py
      - yt_dlp/extractor/youtube/jsc/**.js
      - yt_dlp/extractor/youtube/jsc/**.py
      - yt_dlp/extractor/youtube/pot/**.py
      - yt_dlp/utils/_jsruntime.py
  pull_request:
    paths:
      - .github/workflows/challenge-tests.yml
      - test/test_jsc/*.py
      - yt_dlp/extractor/youtube/jsc/**.js
      - yt_dlp/extractor/youtube/jsc/**.py
      - yt_dlp/extractor/youtube/pot/**.py
      - yt_dlp/utils/_jsruntime.py

permissions: {}

concurrency:
  group: challenge-tests-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  tests:
    name: Challenge Tests
    if: ${{ !contains(github.event.head_commit.message, ':ci skip') }}
    permissions:
      contents: read
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest]
        python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', pypy-3.11]
    env:
      QJS_VERSION: '2025-04-26'  # Earliest version with rope strings
    steps:
    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      with:
        persist-credentials: false
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install Deno
      uses: denoland/setup-deno@e95548e56dfa95d4e1a28d6f422fafe75c4c26fb  # v2.0.3
      with:
        deno-version: '2.0.0'  # minimum supported version
    - name: Install Bun
      uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6  # v2.2.0
      with:
        # minimum supported version is 1.0.31 but earliest available Windows version is 1.1.0
        bun-version: ${{ (matrix.os == 'windows-latest' && '1.1.0') || '1.0.31' }}
        no-cache: true
    - name: Install Node
      uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f  # v6.3.0
      with:
        node-version: '20.0'  # minimum supported version
    - name: Install QuickJS (Linux)
      if: matrix.os == 'ubuntu-latest'
      shell: bash
      run: |
          wget "https://bellard.org/quickjs/binary_releases/quickjs-linux-x86_64-${QJS_VERSION}.zip" -O quickjs.zip
          unzip quickjs.zip qjs
          sudo install qjs /usr/local/bin/qjs
    - name: Install QuickJS (Windows)
      if: matrix.os == 'windows-latest'
      shell: pwsh
      run: |
          $ErrorActionPreference = "Stop"
          $PSNativeCommandUseErrorActionPreference = $true
          Invoke-WebRequest "https://bellard.org/quickjs/binary_releases/quickjs-win-x86_64-${Env:QJS_VERSION}.zip" -OutFile quickjs.zip
          unzip quickjs.zip
    - name: Install test requirements
      shell: bash
      run: |
        python ./devscripts/install_deps.py --print --omit-default --include-group test > requirements.txt
        python ./devscripts/install_deps.py --print -c certifi -c requests -c urllib3 -c yt-dlp-ejs >> requirements.txt
        python -m pip install -U -r requirements.txt
    - name: Run tests
      timeout-minutes: 15
      shell: bash
      run: |
        python -m yt_dlp -v --js-runtimes node --js-runtimes bun --js-runtimes quickjs || true
        python ./devscripts/run_tests.py test/test_jsc -k download
codeql matrix perms security .github/workflows/codeql.yml
Triggers
push, pull_request, schedule
Runs on
ubuntu-latest
Jobs
analyze
Matrix
language→ actions, javascript-typescript, python
Actions
github/codeql-action/init, github/codeql-action/analyze
View raw YAML
name: "CodeQL"

on:
  push:
    branches: [ 'master' ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ 'master' ]
  schedule:
    - cron: '59 11 * * 5'

permissions: {}

concurrency:
  group: codeql-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    runs-on: ubuntu-latest
    permissions:
      actions: read  # Needed by github/codeql-action if repository is private
      contents: read
      security-events: write  # Needed to use github/codeql-action with Github Advanced Security

    strategy:
      fail-fast: false
      matrix:
        language: [ 'actions', 'javascript-typescript', 'python' ]

    steps:
    - name: Checkout repository
      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      with:
        persist-credentials: false

    - name: Initialize CodeQL
      uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8  # v4.33.0
      with:
        languages: ${{ matrix.language }}
        build-mode: none

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8  # v4.33.0
      with:
        category: "/language:${{matrix.language}}"
core matrix perms .github/workflows/core.yml
Triggers
push, pull_request
Runs on
${{ matrix.os }}
Jobs
tests
Matrix
include, include.os, include.python-version, os, python-version→ 3.10, 3.11, 3.12, 3.13, 3.14, pypy-3.11, ubuntu-latest, windows-latest
Commands
  • python ./devscripts/install_deps.py --include-group test --include-extra curl-cffi
  • flags=() # Check if a networking file is involved patterns="\ ^yt_dlp/networking/ ^yt_dlp/utils/networking\.py$ ^test/test_http_proxy\.py$ ^test/test_networking\.py$ ^test/test_networking_utils\.py$ ^test/test_socks\.py$ ^test/test_websockets\.py$ ^pyproject\.toml$ " if git diff --name-only "${source}" "${target}" | grep -Ef <(printf '%s' "${patterns}"); then flags+=(--flaky) fi python3 -m yt_dlp -v || true # Print debug head python3 -m devscripts.run_tests "${flags[@]}" --pytest-args '--reruns 2 --reruns-delay 3.0' core
View raw YAML
name: Core Tests
on:
  push:
    paths:
      - .github/**
      - devscripts/**
      - test/**
      - yt_dlp/**.py
      - '!yt_dlp/extractor/**.py'
      - yt_dlp/extractor/youtube/**.py
      - yt_dlp/extractor/__init__.py
      - yt_dlp/extractor/common.py
      - yt_dlp/extractor/extractors.py
  pull_request:
    paths:
      - .github/**
      - devscripts/**
      - test/**
      - yt_dlp/**.py
      - '!yt_dlp/extractor/**.py'
      - yt_dlp/extractor/youtube/**.py
      - yt_dlp/extractor/__init__.py
      - yt_dlp/extractor/common.py
      - yt_dlp/extractor/extractors.py

permissions: {}

concurrency:
  group: core-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  tests:
    name: Core Tests
    if: ${{ !contains(github.event.head_commit.message, ':ci skip') }}
    permissions:
      contents: read
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest]
        # CPython 3.10 is in quick-test
        python-version: ['3.11', '3.12', '3.13', '3.14', pypy-3.11]
        include:
        # atleast one of each CPython/PyPy tests must be in windows
        - os: windows-latest
          python-version: '3.10'
        - os: windows-latest
          python-version: '3.11'
        - os: windows-latest
          python-version: '3.12'
        - os: windows-latest
          python-version: '3.13'
        - os: windows-latest
          python-version: '3.14'
        - os: windows-latest
          python-version: pypy-3.11
    steps:
    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      with:
        fetch-depth: 0
        persist-credentials: false
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install test requirements
      run: python ./devscripts/install_deps.py --include-group test --include-extra curl-cffi
    - name: Run tests
      timeout-minutes: 15
      continue-on-error: False
      env:
        source: ${{ (github.event_name == 'push' && github.event.before) || 'origin/master' }}
        target: ${{ (github.event_name == 'push' && github.event.after) || 'HEAD' }}
      shell: bash
      run: |
        flags=()
        # Check if a networking file is involved
        patterns="\
        ^yt_dlp/networking/
        ^yt_dlp/utils/networking\.py$
        ^test/test_http_proxy\.py$
        ^test/test_networking\.py$
        ^test/test_networking_utils\.py$
        ^test/test_socks\.py$
        ^test/test_websockets\.py$
        ^pyproject\.toml$
        "
        if git diff --name-only "${source}" "${target}" | grep -Ef <(printf '%s' "${patterns}"); then
          flags+=(--flaky)
        fi
        python3 -m yt_dlp -v || true  # Print debug head
        python3 -m devscripts.run_tests "${flags[@]}" --pytest-args '--reruns 2 --reruns-delay 3.0' core
issue-lockdown perms .github/workflows/issue-lockdown.yml
Triggers
issues
Runs on
ubuntu-latest
Jobs
lockdown
Commands
  • gh issue lock "${ISSUE_NUMBER}" -R "${REPOSITORY}"
View raw YAML
name: Issue Lockdown
on:
  issues:
    types: [opened]

permissions: {}

jobs:
  lockdown:
    name: Issue Lockdown
    if: vars.ISSUE_LOCKDOWN
    permissions:
      issues: write  # Needed to lock issues
    runs-on: ubuntu-latest
    steps:
      - name: "Lock new issue"
        env:
          GH_TOKEN: ${{ github.token }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          REPOSITORY: ${{ github.repository }}
        run: |
          gh issue lock "${ISSUE_NUMBER}" -R "${REPOSITORY}"
quick-test perms .github/workflows/quick-test.yml
Triggers
push, pull_request
Runs on
ubuntu-latest, ubuntu-latest
Jobs
tests, check
Commands
  • python ./devscripts/install_deps.py --omit-default --include-group test
  • python3 -m yt_dlp -v || true python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
  • python ./devscripts/install_deps.py --omit-default --include-group static-analysis
  • python ./devscripts/make_lazy_extractors.py
  • ruff check --output-format github .
  • autopep8 --diff .
  • git ls-files --format="%(objectmode) %(path)" yt_dlp/ | ( ! grep -v "^100644" )
View raw YAML
name: Quick Test
on: [push, pull_request]

permissions: {}

concurrency:
  group: quick-test-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  tests:
    name: Core Test
    if: ${{ !contains(github.event.head_commit.message, ':ci skip all') }}
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      with:
        persist-credentials: false
    - name: Set up Python 3.10
      uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
      with:
        python-version: '3.10'
    - name: Install test requirements
      shell: bash
      run: python ./devscripts/install_deps.py --omit-default --include-group test
    - name: Run tests
      timeout-minutes: 15
      shell: bash
      run: |
        python3 -m yt_dlp -v || true
        python3 ./devscripts/run_tests.py --pytest-args '--reruns 2 --reruns-delay 3.0' core
  check:
    name: Code check
    if: ${{ !contains(github.event.head_commit.message, ':ci skip all') }}
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
      with:
        persist-credentials: false
    - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
      with:
        python-version: '3.10'
    - name: Install dev dependencies
      run: python ./devscripts/install_deps.py --omit-default --include-group static-analysis
    - name: Make lazy extractors
      run: python ./devscripts/make_lazy_extractors.py
    - name: Run ruff
      run: ruff check --output-format github .
    - name: Run autopep8
      run: autopep8 --diff .
    - name: Check file mode
      shell: bash
      run: git ls-files --format="%(objectmode) %(path)" yt_dlp/ | ( ! grep -v "^100644" )
release perms .github/workflows/release.yml
Triggers
workflow_call, workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
prepare, build, publish_pypi, publish
Actions
pypa/gh-action-pypi-publish
Commands
  • python -m devscripts.setup_variables process_inputs
  • python -m devscripts.setup_variables
  • python devscripts/update-version.py -c "${CHANNEL}" -r "${REPOSITORY}" "${VERSION}" python devscripts/update_changelog.py -vv make doc
  • 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 --force "${GITHUB_EVENT_REF}:release"
  • echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
  • git push origin "${GITHUB_EVENT_REF}"
  • sudo apt -y install pandoc man python -m pip install -U --require-hashes -r "bundle/requirements/requirements-pypi-build.txt"
  • 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
View raw YAML
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
  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: {}

jobs:
  prepare:
    name: Prepare
    permissions:
      contents: write  # Needed to git-push the release commit
    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

      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: "3.10"  # 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 }}
          PUSH_VERSION_COMMIT: ${{ vars.PUSH_VERSION_COMMIT }}
          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 }}
        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 to release
        id: push_release
        env:
          VERSION: ${{ steps.setup_variables.outputs.version }}
          GITHUB_EVENT_SENDER_LOGIN: ${{ github.event.sender.login }}
          GITHUB_EVENT_REF: ${{ github.event.ref }}
        if: steps.setup_variables.outputs.target_repo == github.repository && !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 --force "${GITHUB_EVENT_REF}:release"

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

      - name: Update master
        env:
          GITHUB_EVENT_REF: ${{ github.event.ref }}
        if: vars.PUSH_VERSION_COMMIT && !inputs.prerelease && steps.setup_variables.outputs.target_repo == github.repository
        run: git push origin "${GITHUB_EVENT_REF}"

  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.10"

      - name: Install Requirements
        run: |
          sudo apt -y install pandoc man
          python -m pip install -U --require-hashes -r "bundle/requirements/requirements-pypi-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: false
      - 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.10"

      - 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: 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}"
            --target "${HEAD_SHA}"
            --title "${TITLE_PREFIX}${TITLE}${VERSION}"
          )
          if ((PRERELEASE)); then
            gh_options+=(--prerelease)
          fi
          gh release create "${gh_options[@]}" "${TARGET_TAG}" artifact/*
release-master perms .github/workflows/release-master.yml
Triggers
push
Runs on
ubuntu-latest
Jobs
release, publish_pypi
Actions
pypa/gh-action-pypi-publish
View raw YAML
name: Release (master)
on:
  push:
    branches:
      - master
    paths:
      - "yt_dlp/**.py"
      - "!yt_dlp/version.py"
      - "bundle/**"
      - "pyproject.toml"
      - "Makefile"
      - ".github/workflows/build.yml"
      - ".github/workflows/release.yml"
      - ".github/workflows/release-master.yml"
concurrency:
  group: release-master

permissions: {}

jobs:
  release:
    name: Publish Github release
    if: vars.BUILD_MASTER
    permissions:
      contents: write  # May be needed to publish release
      id-token: write  # Needed for trusted publishing
    uses: ./.github/workflows/release.yml
    with:
      prerelease: true
      source: ${{ (github.repository != 'yt-dlp/yt-dlp' && vars.MASTER_ARCHIVE_REPO) || 'master' }}
      target: 'master'
    secrets:
      ARCHIVE_REPO_TOKEN: ${{ secrets.ARCHIVE_REPO_TOKEN }}
      GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}

  publish_pypi:
    name: Publish to PyPI
    needs: [release]
    if: vars.MASTER_PYPI_PROJECT
    permissions:
      id-token: write  # Needed for trusted publishing
    runs-on: ubuntu-latest
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          path: dist
          name: build-pypi
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e  # v1.13.0
        with:
          verbose: true
release-nightly perms .github/workflows/release-nightly.yml
Triggers
schedule, workflow_dispatch
Runs on
ubuntu-latest, ubuntu-latest
Jobs
check_nightly, release, publish_pypi
Actions
pypa/gh-action-pypi-publish
Commands
  • echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}"
  • relevant_files=( "yt_dlp/*.py" ':!yt_dlp/version.py' "bundle/*.py" "bundle/docker/compose.yml" "bundle/docker/linux/*" "pyproject.toml" "Makefile" ".github/workflows/build.yml" ".github/workflows/release.yml" ".github/workflows/release-nightly.yml" ) if [[ -f .nightly_commit_hash ]]; then limit_args=( "$(cat .nightly_commit_hash)..HEAD" ) else limit_args=( --since="24 hours ago" ) fi echo "commit=$(git log --format=%H -1 "${limit_args[@]}" -- "${relevant_files[@]}")" | tee -a "${GITHUB_OUTPUT}"
  • echo "${HEAD}" | tee .nightly_commit_hash
View raw YAML
name: Release (nightly)
on:
  schedule:
    - cron: '23 23 * * *'
  workflow_dispatch:

permissions: {}

jobs:
  check_nightly:
    name: Check for new commits
    if: github.event_name == 'workflow_dispatch' || vars.BUILD_NIGHTLY
    permissions:
      contents: read
    runs-on: ubuntu-latest
    outputs:
      commit: ${{ steps.check_for_new_commits.outputs.commit }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          fetch-depth: 0
          persist-credentials: false

      - name: Retrieve HEAD commit hash
        id: head
        shell: bash
        run: echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}"

      - name: Cache nightly commit hash
        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306  # v5.0.3
        env:
          SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
        with:
          path: .nightly_commit_hash
          key: release-nightly-${{ steps.head.outputs.head }}
          restore-keys: |
            release-nightly-

      - name: Check for new commits
        id: check_for_new_commits
        shell: bash
        run: |
          relevant_files=(
            "yt_dlp/*.py"
            ':!yt_dlp/version.py'
            "bundle/*.py"
            "bundle/docker/compose.yml"
            "bundle/docker/linux/*"
            "pyproject.toml"
            "Makefile"
            ".github/workflows/build.yml"
            ".github/workflows/release.yml"
            ".github/workflows/release-nightly.yml"
          )
          if [[ -f .nightly_commit_hash ]]; then
            limit_args=(
              "$(cat .nightly_commit_hash)..HEAD"
            )
          else
            limit_args=(
              --since="24 hours ago"
            )
          fi
          echo "commit=$(git log --format=%H -1 "${limit_args[@]}" -- "${relevant_files[@]}")" | tee -a "${GITHUB_OUTPUT}"

      - name: Record new nightly commit hash
        env:
          HEAD: ${{ steps.head.outputs.head }}
        shell: bash
        run: echo "${HEAD}" | tee .nightly_commit_hash

  release:
    name: Publish Github release
    needs: [check_nightly]
    if: needs.check_nightly.outputs.commit
    permissions:
      contents: write  # May be needed to publish release
      id-token: write  # Needed for trusted publishing
    uses: ./.github/workflows/release.yml
    with:
      prerelease: true
      source: ${{ (github.repository != 'yt-dlp/yt-dlp' && vars.NIGHTLY_ARCHIVE_REPO) || 'nightly' }}
      target: 'nightly'
    secrets:
      ARCHIVE_REPO_TOKEN: ${{ secrets.ARCHIVE_REPO_TOKEN }}
      GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}

  publish_pypi:
    name: Publish to PyPI
    needs: [release]
    if: vars.NIGHTLY_PYPI_PROJECT
    permissions:
      id-token: write  # Needed for trusted publishing
    runs-on: ubuntu-latest
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c  # v8.0.1
        with:
          path: dist
          name: build-pypi

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e  # v1.13.0
        with:
          verbose: true
sanitize-comment perms .github/workflows/sanitize-comment.yml
Triggers
issue_comment
Runs on
ubuntu-latest
Jobs
sanitize-comment
Actions
yt-dlp/sanitize-comment
View raw YAML
name: Sanitize comment

on:
  issue_comment:
    types: [created, edited]

permissions: {}

jobs:
  sanitize-comment:
    name: Sanitize comment
    if: vars.SANITIZE_COMMENT && !github.event.issue.pull_request
    permissions:
      issues: write  # Needed by yt-dlp/sanitize-comment to edit comments
    runs-on: ubuntu-latest
    steps:
      - name: Sanitize comment
        uses: yt-dlp/sanitize-comment@4536c691101b89f5373d50fe8a7980cae146346b  # v1.0.0
test-workflows perms .github/workflows/test-workflows.yml
Triggers
push, pull_request
Runs on
ubuntu-latest, ubuntu-latest
Jobs
check, zizmor
Actions
zizmorcore/zizmor-action
Commands
  • python -m devscripts.install_deps --omit-default --include-group test sudo apt -y install shellcheck python -m pip install -U pyflakes curl -LO "${ACTIONLINT_REPO}/releases/download/v${ACTIONLINT_VERSION}/${ACTIONLINT_TARBALL}" printf '%s %s' "${ACTIONLINT_SHA256SUM}" "${ACTIONLINT_TARBALL}" | sha256sum -c - tar xvzf "${ACTIONLINT_TARBALL}" actionlint chmod +x actionlint
  • ./actionlint -color
  • shellcheck bundle/docker/linux/*.sh
  • pytest -Werror --tb=short --color=yes devscripts/setup_variables_tests.py
View raw YAML
name: Test and lint workflows
on:
  push:
    branches: [master]
    paths:
      - .github/*.yml
      - .github/workflows/*
      - bundle/docker/linux/*.sh
      - devscripts/setup_variables.py
      - devscripts/setup_variables_tests.py
      - devscripts/utils.py
  pull_request:
    branches: [master]
    paths:
      - .github/*.yml
      - .github/workflows/*
      - bundle/docker/linux/*.sh
      - devscripts/setup_variables.py
      - devscripts/setup_variables_tests.py
      - devscripts/utils.py

permissions: {}

concurrency:
  group: test-workflows-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
  ACTIONLINT_VERSION: "1.7.11"
  ACTIONLINT_SHA256SUM: 900919a84f2229bac68ca9cd4103ea297abc35e9689ebb842c6e34a3d1b01b0a
  ACTIONLINT_REPO: https://github.com/rhysd/actionlint

jobs:
  check:
    name: Check workflows
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false
      - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405  # v6.2.0
        with:
          python-version: "3.10"  # Keep this in sync with release.yml's prepare job
      - name: Install requirements
        env:
          ACTIONLINT_TARBALL: ${{ format('actionlint_{0}_linux_amd64.tar.gz', env.ACTIONLINT_VERSION) }}
        shell: bash
        run: |
          python -m devscripts.install_deps --omit-default --include-group test
          sudo apt -y install shellcheck
          python -m pip install -U pyflakes
          curl -LO "${ACTIONLINT_REPO}/releases/download/v${ACTIONLINT_VERSION}/${ACTIONLINT_TARBALL}"
          printf '%s  %s' "${ACTIONLINT_SHA256SUM}" "${ACTIONLINT_TARBALL}" | sha256sum -c -
          tar xvzf "${ACTIONLINT_TARBALL}" actionlint
          chmod +x actionlint
      - name: Run actionlint
        run: |
          ./actionlint -color
      - name: Check Docker shell scripts
        run: |
          shellcheck bundle/docker/linux/*.sh
      - name: Test GHA devscripts
        run: |
          pytest -Werror --tb=short --color=yes devscripts/setup_variables_tests.py

  zizmor:
    name: Run zizmor
    permissions:
      contents: read
      actions: read  # Needed by zizmorcore/zizmor-action if repository is private
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2
        with:
          persist-credentials: false
      - name: Run zizmor
        uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8  # v0.5.2
        with:
          advanced-security: false
          persona: pedantic
          version: v1.23.1