[ci] Bump `actions/setup-python` to v5
[yt-dlp3.git] / .github / workflows / release.yml
blobfac096be7d335c667e3b694d766284ec56a91898
1 name: Release
2 on:
3   workflow_call:
4     inputs:
5       prerelease:
6         required: false
7         default: true
8         type: boolean
9       source:
10         required: false
11         default: ''
12         type: string
13       target:
14         required: false
15         default: ''
16         type: string
17       version:
18         required: false
19         default: ''
20         type: string
21   workflow_dispatch:
22     inputs:
23       source:
24         description: |
25           SOURCE of this release's updates:
26           channel, repo, tag, or channel/repo@tag
27           (default: <current_repo>)
28         required: false
29         default: ''
30         type: string
31       target:
32         description: |
33           TARGET to publish this release to:
34           channel, tag, or channel@tag
35           (default: <source> if writable else <current_repo>[@source_tag])
36         required: false
37         default: ''
38         type: string
39       version:
40         description: |
41           VERSION: yyyy.mm.dd[.rev] or rev
42           (default: auto-generated)
43         required: false
44         default: ''
45         type: string
46       prerelease:
47         description: Pre-release
48         default: false
49         type: boolean
51 permissions:
52   contents: read
54 jobs:
55   prepare:
56     permissions:
57       contents: write
58     runs-on: ubuntu-latest
59     outputs:
60       channel: ${{ steps.setup_variables.outputs.channel }}
61       version: ${{ steps.setup_variables.outputs.version }}
62       target_repo: ${{ steps.setup_variables.outputs.target_repo }}
63       target_repo_token: ${{ steps.setup_variables.outputs.target_repo_token }}
64       target_tag: ${{ steps.setup_variables.outputs.target_tag }}
65       pypi_project: ${{ steps.setup_variables.outputs.pypi_project }}
66       pypi_suffix: ${{ steps.setup_variables.outputs.pypi_suffix }}
67       head_sha: ${{ steps.get_target.outputs.head_sha }}
69     steps:
70       - uses: actions/checkout@v4
71         with:
72           fetch-depth: 0
74       - uses: actions/setup-python@v5
75         with:
76           python-version: "3.10"
78       - name: Process inputs
79         id: process_inputs
80         run: |
81           cat << EOF
82           ::group::Inputs
83           prerelease=${{ inputs.prerelease }}
84           source=${{ inputs.source }}
85           target=${{ inputs.target }}
86           version=${{ inputs.version }}
87           ::endgroup::
88           EOF
89           IFS='@' read -r source_repo source_tag <<<"${{ inputs.source }}"
90           IFS='@' read -r target_repo target_tag <<<"${{ inputs.target }}"
91           cat << EOF >> "$GITHUB_OUTPUT"
92           source_repo=${source_repo}
93           source_tag=${source_tag}
94           target_repo=${target_repo}
95           target_tag=${target_tag}
96           EOF
98       - name: Setup variables
99         id: setup_variables
100         env:
101           source_repo: ${{ steps.process_inputs.outputs.source_repo }}
102           source_tag: ${{ steps.process_inputs.outputs.source_tag }}
103           target_repo: ${{ steps.process_inputs.outputs.target_repo }}
104           target_tag: ${{ steps.process_inputs.outputs.target_tag }}
105         run: |
106           # unholy bash monstrosity (sincere apologies)
107           fallback_token () {
108             if ${{ !secrets.ARCHIVE_REPO_TOKEN }}; then
109               echo "::error::Repository access secret ${target_repo_token^^} not found"
110               exit 1
111             fi
112             target_repo_token=ARCHIVE_REPO_TOKEN
113             return 0
114           }
116           source_is_channel=0
117           [[ "${source_repo}" == 'stable' ]] && source_repo='yt-dlp/yt-dlp'
118           if [[ -z "${source_repo}" ]]; then
119             source_repo='${{ github.repository }}'
120           elif [[ '${{ vars[format('{0}_archive_repo', env.source_repo)] }}' ]]; then
121             source_is_channel=1
122             source_channel='${{ vars[format('{0}_archive_repo', env.source_repo)] }}'
123           elif [[ -z "${source_tag}" && "${source_repo}" != */* ]]; then
124             source_tag="${source_repo}"
125             source_repo='${{ github.repository }}'
126           fi
127           resolved_source="${source_repo}"
128           if [[ "${source_tag}" ]]; then
129             resolved_source="${resolved_source}@${source_tag}"
130           elif [[ "${source_repo}" == 'yt-dlp/yt-dlp' ]]; then
131             resolved_source='stable'
132           fi
134           revision="${{ (inputs.prerelease || !vars.PUSH_VERSION_COMMIT) && '$(date -u +"%H%M%S")' || '' }}"
135           version="$(
136             python devscripts/update-version.py \
137             -c "${resolved_source}" -r "${{ github.repository }}" ${{ inputs.version || '$revision' }} | \
138             grep -Po "version=\K\d+\.\d+\.\d+(\.\d+)?")"
140           if [[ "${target_repo}" ]]; then
141             if [[ -z "${target_tag}" ]]; then
142               if [[ '${{ vars[format('{0}_archive_repo', env.target_repo)] }}' ]]; then
143                 target_tag="${source_tag:-${version}}"
144               else
145                 target_tag="${target_repo}"
146                 target_repo='${{ github.repository }}'
147               fi
148             fi
149             if [[ "${target_repo}" != '${{ github.repository}}' ]]; then
150               target_repo='${{ vars[format('{0}_archive_repo', env.target_repo)] }}'
151               target_repo_token='${{ env.target_repo }}_archive_repo_token'
152               ${{ !!secrets[format('{0}_archive_repo_token', env.target_repo)] }} || fallback_token
153               pypi_project='${{ vars[format('{0}_pypi_project', env.target_repo)] }}'
154               pypi_suffix='${{ vars[format('{0}_pypi_suffix', env.target_repo)] }}'
155             fi
156           else
157             target_tag="${source_tag:-${version}}"
158             if ((source_is_channel)); then
159               target_repo="${source_channel}"
160               target_repo_token='${{ env.source_repo }}_archive_repo_token'
161               ${{ !!secrets[format('{0}_archive_repo_token', env.source_repo)] }} || fallback_token
162               pypi_project='${{ vars[format('{0}_pypi_project', env.source_repo)] }}'
163               pypi_suffix='${{ vars[format('{0}_pypi_suffix', env.source_repo)] }}'
164             else
165               target_repo='${{ github.repository }}'
166             fi
167           fi
169           if [[ "${target_repo}" == '${{ github.repository }}' ]] && ${{ !inputs.prerelease }}; then
170             pypi_project='${{ vars.PYPI_PROJECT }}'
171           fi
173           echo "::group::Output variables"
174           cat << EOF | tee -a "$GITHUB_OUTPUT"
175           channel=${resolved_source}
176           version=${version}
177           target_repo=${target_repo}
178           target_repo_token=${target_repo_token}
179           target_tag=${target_tag}
180           pypi_project=${pypi_project}
181           pypi_suffix=${pypi_suffix}
182           EOF
183           echo "::endgroup::"
185       - name: Update documentation
186         env:
187           version: ${{ steps.setup_variables.outputs.version }}
188           target_repo: ${{ steps.setup_variables.outputs.target_repo }}
189         if: |
190           !inputs.prerelease && env.target_repo == github.repository
191         run: |
192           make doc
193           sed '/### /Q' Changelog.md >> ./CHANGELOG
194           echo '### ${{ env.version }}' >> ./CHANGELOG
195           python ./devscripts/make_changelog.py -vv -c >> ./CHANGELOG
196           echo >> ./CHANGELOG
197           grep -Poz '(?s)### \d+\.\d+\.\d+.+' 'Changelog.md' | head -n -1 >> ./CHANGELOG
198           cat ./CHANGELOG > Changelog.md
200       - name: Push to release
201         id: push_release
202         env:
203           version: ${{ steps.setup_variables.outputs.version }}
204           target_repo: ${{ steps.setup_variables.outputs.target_repo }}
205         if: |
206           !inputs.prerelease && env.target_repo == github.repository
207         run: |
208           git config --global user.name "github-actions[bot]"
209           git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
210           git add -u
211           git commit -m "Release ${{ env.version }}" \
212             -m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all :ci run dl"
213           git push origin --force ${{ github.event.ref }}:release
215       - name: Get target commitish
216         id: get_target
217         run: |
218           echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
220       - name: Update master
221         env:
222           target_repo: ${{ steps.setup_variables.outputs.target_repo }}
223         if: |
224           vars.PUSH_VERSION_COMMIT != '' && !inputs.prerelease && env.target_repo == github.repository
225         run: git push origin ${{ github.event.ref }}
227   build:
228     needs: prepare
229     uses: ./.github/workflows/build.yml
230     with:
231       version: ${{ needs.prepare.outputs.version }}
232       channel: ${{ needs.prepare.outputs.channel }}
233       origin: ${{ needs.prepare.outputs.target_repo }}
234     permissions:
235       contents: read
236       packages: write # For package cache
237     secrets:
238       GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
240   publish_pypi:
241     needs: [prepare, build]
242     if: ${{ needs.prepare.outputs.pypi_project }}
243     runs-on: ubuntu-latest
244     permissions:
245       id-token: write  # mandatory for trusted publishing
247     steps:
248       - uses: actions/checkout@v4
249         with:
250           fetch-depth: 0
251       - uses: actions/setup-python@v5
252         with:
253           python-version: "3.10"
255       - name: Install Requirements
256         run: |
257           sudo apt -y install pandoc man
258           python devscripts/install_deps.py -o --include build
260       - name: Prepare
261         env:
262           version: ${{ needs.prepare.outputs.version }}
263           suffix: ${{ needs.prepare.outputs.pypi_suffix }}
264           channel: ${{ needs.prepare.outputs.channel }}
265           target_repo: ${{ needs.prepare.outputs.target_repo }}
266           pypi_project: ${{ needs.prepare.outputs.pypi_project }}
267         run: |
268           python devscripts/update-version.py -c "${{ env.channel }}" -r "${{ env.target_repo }}" -s "${{ env.suffix }}" "${{ env.version }}"
269           python devscripts/make_lazy_extractors.py
270           sed -i -E '0,/(name = ")[^"]+(")/s//\1${{ env.pypi_project }}\2/' pyproject.toml
272       - name: Build
273         run: |
274           rm -rf dist/*
275           make pypi-files
276           printf '%s\n\n' \
277             'Official repository: <https://github.com/yt-dlp/yt-dlp>' \
278             '**PS**: Some links in this document will not work since this is a copy of the README.md from Github' > ./README.md.new
279           cat ./README.md >> ./README.md.new && mv -f ./README.md.new ./README.md
280           python devscripts/set-variant.py pip -M "You installed yt-dlp with pip or using the wheel from PyPi; Use that to update"
281           make clean-cache
282           python -m build --no-isolation .
284       - name: Publish to PyPI
285         uses: pypa/gh-action-pypi-publish@release/v1
286         with:
287           verbose: true
289   publish:
290     needs: [prepare, build]
291     permissions:
292       contents: write
293     runs-on: ubuntu-latest
295     steps:
296       - uses: actions/checkout@v4
297         with:
298           fetch-depth: 0
299       - uses: actions/download-artifact@v3
300       - uses: actions/setup-python@v5
301         with:
302           python-version: "3.10"
304       - name: Generate release notes
305         env:
306           head_sha: ${{ needs.prepare.outputs.head_sha }}
307           target_repo: ${{ needs.prepare.outputs.target_repo }}
308           target_tag: ${{ needs.prepare.outputs.target_tag }}
309         run: |
310           printf '%s' \
311             '[![Installation](https://img.shields.io/badge/-Which%20file%20should%20I%20download%3F-white.svg?style=for-the-badge)]' \
312               '(https://github.com/${{ github.repository }}#installation "Installation instructions") ' \
313             '[![Documentation](https://img.shields.io/badge/-Docs-brightgreen.svg?style=for-the-badge&logo=GitBook&labelColor=555555)]' \
314               '(https://github.com/${{ github.repository }}' \
315               '${{ env.target_repo == github.repository && format('/tree/{0}', env.target_tag) || '' }}#readme "Documentation") ' \
316             '[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)]' \
317               '(https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators "Donate") ' \
318             '[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&labelColor=555555&label=&logo=discord&style=for-the-badge)]' \
319               '(https://discord.gg/H5MNcFW63r "Discord") ' \
320             ${{ env.target_repo == 'yt-dlp/yt-dlp' && '\
321               "[![Nightly](https://img.shields.io/badge/Get%20nightly%20builds-purple.svg?style=for-the-badge)]" \
322               "(https://github.com/yt-dlp/yt-dlp-nightly-builds/releases/latest \"Nightly builds\") " \
323               "[![Master](https://img.shields.io/badge/Get%20master%20builds-lightblue.svg?style=for-the-badge)]" \
324               "(https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest \"Master builds\")"' || '' }} > ./RELEASE_NOTES
325           printf '\n\n' >> ./RELEASE_NOTES
326           cat >> ./RELEASE_NOTES << EOF
327           #### A description of the various files are in the [README](https://github.com/${{ github.repository }}#release-files)
328           ---
329           $(python ./devscripts/make_changelog.py -vv --collapsible)
330           EOF
331           printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES
332           cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
333           printf '%s\n\n' 'Generated from: https://github.com/${{ github.repository }}/commit/${{ env.head_sha }}' >> ./ARCHIVE_NOTES
334           cat ./RELEASE_NOTES >> ./ARCHIVE_NOTES
336       - name: Publish to archive repo
337         env:
338           GH_TOKEN: ${{ secrets[needs.prepare.outputs.target_repo_token] }}
339           GH_REPO: ${{ needs.prepare.outputs.target_repo }}
340           version: ${{ needs.prepare.outputs.version }}
341           channel: ${{ needs.prepare.outputs.channel }}
342         if: |
343           inputs.prerelease && env.GH_TOKEN != '' && env.GH_REPO != '' && env.GH_REPO != github.repository
344         run: |
345           title="${{ startswith(env.GH_REPO, 'yt-dlp/') && 'yt-dlp ' || '' }}${{ env.channel }}"
346           gh release create \
347             --notes-file ARCHIVE_NOTES \
348             --title "${title} ${{ env.version }}" \
349             ${{ env.version }} \
350             artifact/*
352       - name: Prune old release
353         env:
354           GH_TOKEN: ${{ github.token }}
355           version: ${{ needs.prepare.outputs.version }}
356           target_repo: ${{ needs.prepare.outputs.target_repo }}
357           target_tag: ${{ needs.prepare.outputs.target_tag }}
358         if: |
359           env.target_repo == github.repository && env.target_tag != env.version
360         run: |
361           gh release delete --yes --cleanup-tag "${{ env.target_tag }}" || true
362           git tag --delete "${{ env.target_tag }}" || true
363           sleep 5  # Enough time to cover deletion race condition
365       - name: Publish release
366         env:
367           GH_TOKEN: ${{ github.token }}
368           version: ${{ needs.prepare.outputs.version }}
369           target_repo: ${{ needs.prepare.outputs.target_repo }}
370           target_tag: ${{ needs.prepare.outputs.target_tag }}
371           head_sha: ${{ needs.prepare.outputs.head_sha }}
372         if: |
373           env.target_repo == github.repository
374         run: |
375           title="${{ github.repository == 'yt-dlp/yt-dlp' && 'yt-dlp ' || '' }}"
376           title+="${{ env.target_tag != env.version && format('{0} ', env.target_tag) || '' }}"
377           gh release create \
378             --notes-file ${{ inputs.prerelease && 'PRERELEASE_NOTES' || 'RELEASE_NOTES' }} \
379             --target ${{ env.head_sha }} \
380             --title "${title}${{ env.version }}" \
381             ${{ inputs.prerelease && '--prerelease' || '' }} \
382             ${{ env.target_tag }} \
383             artifact/*