Skip to content

Build WinPython (cycle file) #15

Build WinPython (cycle file)

Build WinPython (cycle file) #15

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