release.yml 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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
  50. permissions:
  51. contents: read
  52. jobs:
  53. prepare:
  54. permissions:
  55. contents: write
  56. runs-on: ubuntu-latest
  57. outputs:
  58. channel: ${{ steps.setup_variables.outputs.channel }}
  59. version: ${{ steps.setup_variables.outputs.version }}
  60. target_repo: ${{ steps.setup_variables.outputs.target_repo }}
  61. target_repo_token: ${{ steps.setup_variables.outputs.target_repo_token }}
  62. target_tag: ${{ steps.setup_variables.outputs.target_tag }}
  63. pypi_project: ${{ steps.setup_variables.outputs.pypi_project }}
  64. pypi_suffix: ${{ steps.setup_variables.outputs.pypi_suffix }}
  65. pypi_token: ${{ steps.setup_variables.outputs.pypi_token }}
  66. head_sha: ${{ steps.get_target.outputs.head_sha }}
  67. steps:
  68. - uses: actions/checkout@v4
  69. with:
  70. fetch-depth: 0
  71. - uses: actions/setup-python@v4
  72. with:
  73. python-version: "3.10"
  74. - name: Process inputs
  75. id: process_inputs
  76. run: |
  77. cat << EOF
  78. ::group::Inputs
  79. prerelease=${{ inputs.prerelease }}
  80. source=${{ inputs.source }}
  81. target=${{ inputs.target }}
  82. version=${{ inputs.version }}
  83. ::endgroup::
  84. EOF
  85. IFS='@' read -r source_repo source_tag <<<"${{ inputs.source }}"
  86. IFS='@' read -r target_repo target_tag <<<"${{ inputs.target }}"
  87. cat << EOF >> "$GITHUB_OUTPUT"
  88. source_repo=${source_repo}
  89. source_tag=${source_tag}
  90. target_repo=${target_repo}
  91. target_tag=${target_tag}
  92. EOF
  93. - name: Setup variables
  94. id: setup_variables
  95. env:
  96. source_repo: ${{ steps.process_inputs.outputs.source_repo }}
  97. source_tag: ${{ steps.process_inputs.outputs.source_tag }}
  98. target_repo: ${{ steps.process_inputs.outputs.target_repo }}
  99. target_tag: ${{ steps.process_inputs.outputs.target_tag }}
  100. run: |
  101. # unholy bash monstrosity (sincere apologies)
  102. fallback_token () {
  103. if ${{ !secrets.ARCHIVE_REPO_TOKEN }}; then
  104. echo "::error::Repository access secret ${target_repo_token^^} not found"
  105. exit 1
  106. fi
  107. target_repo_token=ARCHIVE_REPO_TOKEN
  108. return 0
  109. }
  110. source_is_channel=0
  111. [[ "${source_repo}" == 'stable' ]] && source_repo='yt-dlp/yt-dlp'
  112. if [[ -z "${source_repo}" ]]; then
  113. source_repo='${{ github.repository }}'
  114. elif [[ '${{ vars[format('{0}_archive_repo', env.source_repo)] }}' ]]; then
  115. source_is_channel=1
  116. source_channel='${{ vars[format('{0}_archive_repo', env.source_repo)] }}'
  117. elif [[ -z "${source_tag}" && "${source_repo}" != */* ]]; then
  118. source_tag="${source_repo}"
  119. source_repo='${{ github.repository }}'
  120. fi
  121. resolved_source="${source_repo}"
  122. if [[ "${source_tag}" ]]; then
  123. resolved_source="${resolved_source}@${source_tag}"
  124. elif [[ "${source_repo}" == 'yt-dlp/yt-dlp' ]]; then
  125. resolved_source='stable'
  126. fi
  127. revision="${{ (inputs.prerelease || !vars.PUSH_VERSION_COMMIT) && '$(date -u +"%H%M%S")' || '' }}"
  128. version="$(
  129. python devscripts/update-version.py \
  130. -c "${resolved_source}" -r "${{ github.repository }}" ${{ inputs.version || '$revision' }} | \
  131. grep -Po "version=\K\d+\.\d+\.\d+(\.\d+)?")"
  132. if [[ "${target_repo}" ]]; then
  133. if [[ -z "${target_tag}" ]]; then
  134. if [[ '${{ vars[format('{0}_archive_repo', env.target_repo)] }}' ]]; then
  135. target_tag="${source_tag:-${version}}"
  136. else
  137. target_tag="${target_repo}"
  138. target_repo='${{ github.repository }}'
  139. fi
  140. fi
  141. if [[ "${target_repo}" != '${{ github.repository}}' ]]; then
  142. target_repo='${{ vars[format('{0}_archive_repo', env.target_repo)] }}'
  143. target_repo_token='${{ env.target_repo }}_archive_repo_token'
  144. ${{ !!secrets[format('{0}_archive_repo_token', env.target_repo)] }} || fallback_token
  145. pypi_project='${{ vars[format('{0}_pypi_project', env.target_repo)] }}'
  146. pypi_suffix='${{ vars[format('{0}_pypi_suffix', env.target_repo)] }}'
  147. ${{ !secrets[format('{0}_pypi_token', env.target_repo)] }} || pypi_token='${{ env.target_repo }}_pypi_token'
  148. fi
  149. else
  150. target_tag="${source_tag:-${version}}"
  151. if ((source_is_channel)); then
  152. target_repo="${source_channel}"
  153. target_repo_token='${{ env.source_repo }}_archive_repo_token'
  154. ${{ !!secrets[format('{0}_archive_repo_token', env.source_repo)] }} || fallback_token
  155. pypi_project='${{ vars[format('{0}_pypi_project', env.source_repo)] }}'
  156. pypi_suffix='${{ vars[format('{0}_pypi_suffix', env.source_repo)] }}'
  157. ${{ !secrets[format('{0}_pypi_token', env.source_repo)] }} || pypi_token='${{ env.source_repo }}_pypi_token'
  158. else
  159. target_repo='${{ github.repository }}'
  160. fi
  161. fi
  162. if [[ "${target_repo}" == '${{ github.repository }}' ]] && ${{ !inputs.prerelease }}; then
  163. pypi_project='${{ vars.PYPI_PROJECT }}'
  164. fi
  165. if [[ -z "${pypi_token}" && "${pypi_project}" ]]; then
  166. if ${{ !secrets.PYPI_TOKEN }}; then
  167. pypi_token=OIDC
  168. else
  169. pypi_token=PYPI_TOKEN
  170. fi
  171. fi
  172. echo "::group::Output variables"
  173. cat << EOF | tee -a "$GITHUB_OUTPUT"
  174. channel=${resolved_source}
  175. version=${version}
  176. target_repo=${target_repo}
  177. target_repo_token=${target_repo_token}
  178. target_tag=${target_tag}
  179. pypi_project=${pypi_project}
  180. pypi_suffix=${pypi_suffix}
  181. pypi_token=${pypi_token}
  182. EOF
  183. echo "::endgroup::"
  184. - name: Update documentation
  185. env:
  186. version: ${{ steps.setup_variables.outputs.version }}
  187. target_repo: ${{ steps.setup_variables.outputs.target_repo }}
  188. if: |
  189. !inputs.prerelease && env.target_repo == github.repository
  190. run: |
  191. make doc
  192. sed '/### /Q' Changelog.md >> ./CHANGELOG
  193. echo '### ${{ env.version }}' >> ./CHANGELOG
  194. python ./devscripts/make_changelog.py -vv -c >> ./CHANGELOG
  195. echo >> ./CHANGELOG
  196. grep -Poz '(?s)### \d+\.\d+\.\d+.+' 'Changelog.md' | head -n -1 >> ./CHANGELOG
  197. cat ./CHANGELOG > Changelog.md
  198. - name: Push to release
  199. id: push_release
  200. env:
  201. version: ${{ steps.setup_variables.outputs.version }}
  202. target_repo: ${{ steps.setup_variables.outputs.target_repo }}
  203. if: |
  204. !inputs.prerelease && env.target_repo == github.repository
  205. run: |
  206. git config --global user.name github-actions
  207. git config --global user.email github-actions@github.com
  208. git add -u
  209. git commit -m "Release ${{ env.version }}" \
  210. -m "Created by: ${{ github.event.sender.login }}" -m ":ci skip all :ci run dl"
  211. git push origin --force ${{ github.event.ref }}:release
  212. - name: Get target commitish
  213. id: get_target
  214. run: |
  215. echo "head_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
  216. - name: Update master
  217. env:
  218. target_repo: ${{ steps.setup_variables.outputs.target_repo }}
  219. if: |
  220. vars.PUSH_VERSION_COMMIT != '' && !inputs.prerelease && env.target_repo == github.repository
  221. run: git push origin ${{ github.event.ref }}
  222. build:
  223. needs: prepare
  224. uses: ./.github/workflows/build.yml
  225. with:
  226. version: ${{ needs.prepare.outputs.version }}
  227. channel: ${{ needs.prepare.outputs.channel }}
  228. origin: ${{ needs.prepare.outputs.target_repo }}
  229. permissions:
  230. contents: read
  231. packages: write # For package cache
  232. secrets:
  233. GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
  234. publish_pypi:
  235. needs: [prepare, build]
  236. if: ${{ needs.prepare.outputs.pypi_project }}
  237. runs-on: ubuntu-latest
  238. permissions:
  239. id-token: write # mandatory for trusted publishing
  240. steps:
  241. - uses: actions/checkout@v4
  242. - uses: actions/setup-python@v4
  243. with:
  244. python-version: "3.10"
  245. - name: Install Requirements
  246. run: |
  247. sudo apt -y install pandoc man
  248. python -m pip install -U pip setuptools wheel twine
  249. python -m pip install -U -r requirements.txt
  250. - name: Prepare
  251. env:
  252. version: ${{ needs.prepare.outputs.version }}
  253. suffix: ${{ needs.prepare.outputs.pypi_suffix }}
  254. channel: ${{ needs.prepare.outputs.channel }}
  255. target_repo: ${{ needs.prepare.outputs.target_repo }}
  256. pypi_project: ${{ needs.prepare.outputs.pypi_project }}
  257. run: |
  258. python devscripts/update-version.py -c "${{ env.channel }}" -r "${{ env.target_repo }}" -s "${{ env.suffix }}" "${{ env.version }}"
  259. python devscripts/make_lazy_extractors.py
  260. sed -i -E "s/(name=')[^']+(', # package name)/\1${{ env.pypi_project }}\2/" setup.py
  261. - name: Build
  262. run: |
  263. rm -rf dist/*
  264. make pypi-files
  265. python devscripts/set-variant.py pip -M "You installed yt-dlp with pip or using the wheel from PyPi; Use that to update"
  266. python setup.py sdist bdist_wheel
  267. - name: Publish to PyPI via token
  268. env:
  269. TWINE_USERNAME: __token__
  270. TWINE_PASSWORD: ${{ secrets[needs.prepare.outputs.pypi_token] }}
  271. if: |
  272. needs.prepare.outputs.pypi_token != 'OIDC' && env.TWINE_PASSWORD
  273. run: |
  274. twine upload dist/*
  275. - name: Publish to PyPI via trusted publishing
  276. if: |
  277. needs.prepare.outputs.pypi_token == 'OIDC'
  278. uses: pypa/gh-action-pypi-publish@release/v1
  279. with:
  280. verbose: true
  281. publish:
  282. needs: [prepare, build]
  283. permissions:
  284. contents: write
  285. runs-on: ubuntu-latest
  286. steps:
  287. - uses: actions/checkout@v4
  288. with:
  289. fetch-depth: 0
  290. - uses: actions/download-artifact@v3
  291. - uses: actions/setup-python@v4
  292. with:
  293. python-version: "3.10"
  294. - name: Generate release notes
  295. env:
  296. head_sha: ${{ needs.prepare.outputs.head_sha }}
  297. target_repo: ${{ needs.prepare.outputs.target_repo }}
  298. target_tag: ${{ needs.prepare.outputs.target_tag }}
  299. run: |
  300. printf '%s' \
  301. '[![Installation](https://img.shields.io/badge/-Which%20file%20should%20I%20download%3F-white.svg?style=for-the-badge)]' \
  302. '(https://github.com/${{ github.repository }}#installation "Installation instructions") ' \
  303. '[![Documentation](https://img.shields.io/badge/-Docs-brightgreen.svg?style=for-the-badge&logo=GitBook&labelColor=555555)]' \
  304. '(https://github.com/${{ github.repository }}' \
  305. '${{ env.target_repo == github.repository && format('/tree/{0}', env.target_tag) || '' }}#readme "Documentation") ' \
  306. '[![Donate](https://img.shields.io/badge/_-Donate-red.svg?logo=githubsponsors&labelColor=555555&style=for-the-badge)]' \
  307. '(https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators "Donate") ' \
  308. '[![Discord](https://img.shields.io/discord/807245652072857610?color=blue&labelColor=555555&label=&logo=discord&style=for-the-badge)]' \
  309. '(https://discord.gg/H5MNcFW63r "Discord") ' \
  310. ${{ env.target_repo == 'yt-dlp/yt-dlp' && '\
  311. "[![Nightly](https://img.shields.io/badge/Get%20nightly%20builds-purple.svg?style=for-the-badge)]" \
  312. "(https://github.com/yt-dlp/yt-dlp-nightly-builds/releases/latest \"Nightly builds\") " \
  313. "[![Master](https://img.shields.io/badge/Get%20master%20builds-lightblue.svg?style=for-the-badge)]" \
  314. "(https://github.com/yt-dlp/yt-dlp-master-builds/releases/latest \"Master builds\")"' || '' }} > ./RELEASE_NOTES
  315. printf '\n\n' >> ./RELEASE_NOTES
  316. cat >> ./RELEASE_NOTES << EOF
  317. #### A description of the various files are in the [README](https://github.com/${{ github.repository }}#release-files)
  318. ---
  319. $(python ./devscripts/make_changelog.py -vv --collapsible)
  320. EOF
  321. printf '%s\n\n' '**This is a pre-release build**' >> ./PRERELEASE_NOTES
  322. cat ./RELEASE_NOTES >> ./PRERELEASE_NOTES
  323. printf '%s\n\n' 'Generated from: https://github.com/${{ github.repository }}/commit/${{ env.head_sha }}' >> ./ARCHIVE_NOTES
  324. cat ./RELEASE_NOTES >> ./ARCHIVE_NOTES
  325. - name: Publish to archive repo
  326. env:
  327. GH_TOKEN: ${{ secrets[needs.prepare.outputs.target_repo_token] }}
  328. GH_REPO: ${{ needs.prepare.outputs.target_repo }}
  329. version: ${{ needs.prepare.outputs.version }}
  330. channel: ${{ needs.prepare.outputs.channel }}
  331. if: |
  332. inputs.prerelease && env.GH_TOKEN != '' && env.GH_REPO != '' && env.GH_REPO != github.repository
  333. run: |
  334. title="${{ startswith(env.GH_REPO, 'yt-dlp/') && 'yt-dlp ' || '' }}${{ env.channel }}"
  335. gh release create \
  336. --notes-file ARCHIVE_NOTES \
  337. --title "${title} ${{ env.version }}" \
  338. ${{ env.version }} \
  339. artifact/*
  340. - name: Prune old release
  341. env:
  342. GH_TOKEN: ${{ github.token }}
  343. version: ${{ needs.prepare.outputs.version }}
  344. target_repo: ${{ needs.prepare.outputs.target_repo }}
  345. target_tag: ${{ needs.prepare.outputs.target_tag }}
  346. if: |
  347. env.target_repo == github.repository && env.target_tag != env.version
  348. run: |
  349. gh release delete --yes --cleanup-tag "${{ env.target_tag }}" || true
  350. git tag --delete "${{ env.target_tag }}" || true
  351. sleep 5 # Enough time to cover deletion race condition
  352. - name: Publish 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. head_sha: ${{ needs.prepare.outputs.head_sha }}
  359. if: |
  360. env.target_repo == github.repository
  361. run: |
  362. title="${{ github.repository == 'yt-dlp/yt-dlp' && 'yt-dlp ' || '' }}"
  363. title+="${{ env.target_tag != env.version && format('{0} ', env.target_tag) || '' }}"
  364. gh release create \
  365. --notes-file ${{ inputs.prerelease && 'PRERELEASE_NOTES' || 'RELEASE_NOTES' }} \
  366. --target ${{ env.head_sha }} \
  367. --title "${title}${{ env.version }}" \
  368. ${{ inputs.prerelease && '--prerelease' || '' }} \
  369. ${{ env.target_tag }} \
  370. artifact/*