Build WinPython (cycle file) #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build WinPython (cycle file) | |
| # One dispatch builds a whole cycle. Everything cycle-specific comes from | |
| # cycles/<cycle>.toml, so a new cycle is a new TOML rather than a copy of this | |
| # workflow, and "all" builds every Python that cycle has lockfiles for. | |
| # | |
| # Leave publish on and each leg uploads its own output straight to a draft | |
| # release, instead of leaving gigabytes of artifacts to download and re-upload | |
| # by hand. The tag comes from the cycle file, so re-running one missing flavor | |
| # adds it to the release the others are already on. Turn publish off and the | |
| # output stays artifacts, as before. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| cycle: | |
| description: 'Build cycle (file name in cycles/, without .toml)' | |
| required: true | |
| default: '2026_04' | |
| type: string | |
| python_versionf: | |
| description: 'Python to build; "all" takes every one this cycle has lockfiles for' | |
| required: true | |
| default: 'all' | |
| type: choice | |
| options: | |
| - 'all' | |
| - '3.13' | |
| - '3.14' | |
| - '3.14F' | |
| - '3.15' | |
| - '3.15F' | |
| publish: | |
| description: 'Upload to the draft release the cycle file names; off keeps the output as artifacts' | |
| required: false | |
| default: true | |
| type: boolean | |
| overwrite_published: | |
| description: 'Tick ONLY to replace the files of an already-published release; normally a release_level bump is what you want' | |
| required: false | |
| default: false | |
| type: boolean | |
| permissions: {} | |
| env: | |
| WINPYARCH: "64" | |
| dotwheelhouse: "dotpython\\wheelhouse\\included.wheels" | |
| jobs: | |
| config: | |
| # reads the cycle file once, instead of once per matrix leg | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| pandoc_source: ${{ steps.cfg.outputs.pandoc_source }} | |
| pandoc_sha256: ${{ steps.cfg.outputs.pandoc_sha256 }} | |
| release_level: ${{ steps.cfg.outputs.release_level }} | |
| release_tag: ${{ steps.cfg.outputs.release_tag }} | |
| release_title: ${{ steps.cfg.outputs.release_title }} | |
| matrix: ${{ steps.cfg.outputs.matrix }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Read cycle configuration | |
| id: cfg | |
| env: | |
| CYCLE: ${{ inputs.cycle }} | |
| PYTHON_VERSIONF: ${{ inputs.python_versionf }} | |
| run: python .github/scripts/cycle_config.py "cycles/$CYCLE.toml" "$PYTHON_VERSIONF" | |
| release-draft: | |
| # one job opens the draft, so the build legs cannot race to create it | |
| needs: config | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Open the draft release | |
| if: ${{ inputs.publish }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.config.outputs.release_tag }} | |
| TITLE: ${{ needs.config.outputs.release_title }} | |
| CYCLE: ${{ inputs.cycle }} | |
| OVERWRITE: ${{ inputs.overwrite_published }} | |
| run: | | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| # Adding a missing flavor to a draft is the normal path. Landing on | |
| # a release that is already published is almost always a forgotten | |
| # release_level bump, and the upload replaces files people have | |
| # downloaded -- so stop here, before an hour of build time. | |
| published=$(gh release view "$TAG" --json isDraft --jq '.isDraft | not') | |
| if [ "$published" = "true" ] && [ "$OVERWRITE" != "true" ]; then | |
| echo "::error::$TAG is already published. Bump release_level in cycles/$CYCLE.toml, or re-dispatch with overwrite_published ticked to replace its files." | |
| exit 1 | |
| fi | |
| # say which release the files land on, and leave the title alone in | |
| # case it was edited by hand | |
| state=$(gh release view "$TAG" --json isDraft --jq 'if .isDraft then "still a draft" else "ALREADY PUBLISHED" end') | |
| echo "release $TAG exists ($state); this run adds its files to it" | |
| else | |
| gh release create "$TAG" --draft \ | |
| --title "$TITLE" \ | |
| --notes "Draft opened by the build workflow for cycle $CYCLE. Files land as each build finishes; publish once they are all here." | |
| echo "opened draft release $TAG" | |
| fi | |
| build-winpython: | |
| needs: [config, release-draft] | |
| name: ${{ matrix.leg.python_versionf }} ${{ matrix.leg.name }} | |
| runs-on: windows-latest | |
| permissions: | |
| contents: write # gh release upload; the checkout itself only reads | |
| strategy: | |
| fail-fast: false # a cycle is eight legs or more: one bad flavor must not bin the rest | |
| matrix: ${{ fromJSON(needs.config.outputs.matrix) }} | |
| env: | |
| WINPYFLAVOR: ${{ matrix.leg.name }} | |
| WINPYVER: ${{ matrix.leg.winpyver }} | |
| WINPYVER2: ${{ matrix.leg.ver2 }} | |
| build_location: ${{ matrix.leg.build_location }} | |
| destwheelhouse: ${{ matrix.leg.destwheelhouse }} | |
| WINPYLOCKFILE: ${{ matrix.leg.lockfile }} | |
| WINPYLOCKFILEwhl: ${{ matrix.leg.lockfile_wheels }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Download, verify and extract python standalone | |
| uses: ./.github/actions/python-setup | |
| with: | |
| python_source: ${{ matrix.leg.src }} | |
| python_sha256: ${{ matrix.leg.sha }} | |
| build_location: ${{ env.build_location }} | |
| - name: Download, checking hash and integrating pandoc binary | |
| if: matrix.leg.PANDOC == '1' | |
| uses: ./.github/actions/pandoc-setup | |
| with: | |
| pandoc_source: ${{ needs.config.outputs.pandoc_source }} | |
| pandoc_sha256: ${{ needs.config.outputs.pandoc_sha256 }} | |
| build_location: ${{ env.build_location }} | |
| - name: Upgrade pip and patch launchers | |
| shell: pwsh | |
| run: | | |
| & "$env:build_location\python\python.exe" -m pip install --upgrade --force-reinstall pip --no-warn-script-location | |
| & "$env:build_location\python\python.exe" -c "from wppm import wppm;dist=wppm.Distribution();dist.patch_standard_packages('pip', to_movable=True)" | |
| - name: Download all requirements | |
| shell: pwsh | |
| run: | | |
| $py = "$env:build_location\python\python.exe" | |
| & $py -m pip download --dest $env:dotwheelhouse --no-deps --require-hashes -r $env:WINPYLOCKFILE | |
| if ($env:WINPYLOCKFILEwhl -ne '') { | |
| & $py -m pip download --dest $env:destwheelhouse --no-deps --require-hashes -r $env:WINPYLOCKFILEwhl | |
| } | |
| - name: Install lockfile | |
| shell: pwsh | |
| run: | | |
| & "$env:build_location\python\python.exe" -m pip install --no-deps --require-hashes -r $env:WINPYLOCKFILE --no-warn-script-location | |
| - name: Generate Assets and Hashes | |
| uses: ./.github/actions/publish-winpython | |
| with: | |
| build_location: ${{ env.build_location }} | |
| winpy_flavor: ${{ env.WINPYFLAVOR }} | |
| winpy_arch: ${{ env.WINPYARCH }} | |
| winpy_ver: ${{ env.WINPYVER }} | |
| winpy_ver2: ${{ env.WINPYVER2 }} | |
| release_level: ${{ needs.config.outputs.release_level }} | |
| dotwheelhouse: ${{ env.dotwheelhouse }} | |
| winpy_requirements_whl: ${{ matrix.leg.requirements_wheels }} | |
| format_zip: ${{ matrix.leg.formats.zip }} | |
| format_7z: ${{ matrix.leg.formats['7z'] }} | |
| format_exe: ${{ matrix.leg.formats.exe }} | |
| - name: Upload to the draft release | |
| if: ${{ inputs.publish }} | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.config.outputs.release_tag }} | |
| run: | | |
| shopt -s nullglob | |
| # the metadata is worth little on its own: fail the leg if no binary came out | |
| binaries=(publish_output/*.exe publish_output/*.zip publish_output/*.7z) | |
| if [ ${#binaries[@]} -eq 0 ]; then | |
| echo "::error::no binary in publish_output for $WINPYVER" | |
| exit 1 | |
| fi | |
| files=(publish_output/*) | |
| # --clobber so re-running a leg replaces its files instead of erroring | |
| gh release upload "$TAG" "${files[@]}" --clobber | |
| - name: Upload metadata artifact | |
| # the binaries are on the release; this is what the changelog commit needs | |
| if: ${{ inputs.publish }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ${{ matrix.leg.artifact_name }} | |
| path: | | |
| publish_output/*.md | |
| publish_output/*.toml | |
| publish_output/*.txt | |
| retention-days: 66 # keeps artifact for 66 days | |
| - name: Upload artifacts | |
| # no release to upload to, so the binaries have nowhere else to go | |
| if: ${{ !inputs.publish }} | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: ${{ matrix.leg.artifact_name }} | |
| path: publish_output | |
| retention-days: 66 # keeps artifact for 66 days | |
| changelogs: | |
| # A cycle's changelogs, filed and offered as one reviewable pull request. | |
| # | |
| # Whole-cycle builds only. A re-run of a single leg would otherwise reduce | |
| # the branch to that leg's files, and it has nothing to add anyway: the | |
| # same lockfile produces the same package list, so a rebuilt leg cannot | |
| # change a changelog. | |
| needs: [config, build-winpython] | |
| if: ${{ inputs.publish && inputs.python_versionf == 'all' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| # credentials are kept here, unlike in the build legs: this job pushes | |
| uses: actions/checkout@v6 | |
| - name: Install Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: '3.13' | |
| - name: Install pinned dependencies | |
| # the same hash-pinned set the test suite uses; diff.py wants packaging | |
| run: python -m pip install --no-deps --require-hashes -r tests/requir.wppmtest.txt | |
| - name: Collect the metadata every leg produced | |
| uses: actions/download-artifact@v6 | |
| with: | |
| pattern: publish_* | |
| merge-multiple: true | |
| path: release_metadata | |
| - name: File the changelogs | |
| run: python .github/scripts/changelog_files.py release_metadata changelogs | |
| - name: Open the changelog pull request | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| TAG: ${{ needs.config.outputs.release_tag }} | |
| TITLE: ${{ needs.config.outputs.release_title }} | |
| BASE: ${{ github.event.repository.default_branch }} | |
| run: | | |
| branch="changelogs/$TAG" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git switch -c "$branch" | |
| git add changelogs | |
| if git diff --cached --quiet; then | |
| echo "changelogs/ already holds this cycle; nothing to open" | |
| exit 0 | |
| fi | |
| git commit -m "Changelogs for $TITLE" | |
| # the branch is generated wholly by this job, so a re-run replaces it | |
| git push --force origin "$branch" | |
| if gh pr view "$branch" --json number >/dev/null 2>&1; then | |
| echo "pull request for $branch is open; the push updated it" | |
| else | |
| gh pr create --base "$BASE" --head "$branch" \ | |
| --title "Changelogs for $TITLE" \ | |
| --body "Package indexes, lock files and requirements for every leg of \`$TAG\`, filed by the build that produced them rather than copied by hand. Compare any two of them with \`wppm -diff <a>.md <b>.md\`." | |
| fi |