Skip to content

Commit c6e7a35

Browse files
committed
ci: draft a cycle-file build workflow
Sits beside the per-cycle workflows for comparison; nothing existing is changed. Cycle data moves to cycles/2026_03.toml, so a new cycle is a new file instead of a copy of the workflow. Verified the TOML reproduces github_workflows_build-2026_03.yml exactly: ver2, src and sha for all five versions, and the five flavors with their formats.
1 parent 2436864 commit c6e7a35

3 files changed

Lines changed: 343 additions & 0 deletions

File tree

.github/scripts/cycle_config.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Read a cycle TOML and emit its build configuration as GITHUB_OUTPUT lines.
2+
3+
python .github/scripts/cycle_config.py cycles/2026_03.toml 3.14
4+
5+
Writes to $GITHUB_OUTPUT when set, otherwise stdout, so it can be run locally
6+
to see exactly what a workflow run would get. tomllib is stdlib from 3.11, and
7+
GitHub runners are newer than that, so this needs no dependency.
8+
"""
9+
import json
10+
import os
11+
import sys
12+
import tomllib
13+
from pathlib import Path
14+
15+
16+
def build_config(cfg: dict, requested: str) -> dict:
17+
pythons = cfg["pythons"]
18+
if requested not in pythons:
19+
raise SystemExit(
20+
f"no entry for python {requested!r}; this cycle offers {', '.join(sorted(pythons))}"
21+
)
22+
entry = pythons[requested]
23+
24+
# the check the PowerShell version did: ver2's first 3 parts must appear in
25+
# the tarball URL, so a copy-paste slip between the two cannot go unnoticed
26+
short = ".".join(entry["ver2"].split(".")[:3])
27+
if short not in entry["src"]:
28+
raise SystemExit(f"{requested}: '{short}' not found in src {entry['src']}")
29+
30+
return {
31+
"ver2": entry["ver2"],
32+
"src": entry["src"],
33+
"sha": entry["sha"],
34+
"cycle_dir": cfg["cycle_dir"],
35+
"release_level": cfg.get("release_level", ""),
36+
"pandoc_source": cfg["pandoc"]["source"],
37+
"pandoc_sha256": cfg["pandoc"]["sha256"],
38+
# consumed by the build job as strategy.matrix via fromJSON
39+
"matrix": json.dumps({"flavor": cfg["flavors"]}, separators=(",", ":")),
40+
}
41+
42+
43+
def main(argv: list[str]) -> None:
44+
if len(argv) != 3:
45+
raise SystemExit(f"usage: {Path(argv[0]).name} <cycle.toml> <python_version>")
46+
cfg_path = Path(argv[1])
47+
if not cfg_path.is_file():
48+
raise SystemExit(f"no such cycle file: {cfg_path}")
49+
with cfg_path.open("rb") as fh:
50+
cfg = tomllib.load(fh)
51+
52+
rendered = "".join(f"{k}={v}\n" for k, v in build_config(cfg, argv[2]).items())
53+
out = os.environ.get("GITHUB_OUTPUT")
54+
if out:
55+
with open(out, "a", encoding="utf-8") as fh:
56+
fh.write(rendered)
57+
sys.stdout.write(rendered)
58+
59+
60+
if __name__ == "__main__":
61+
main(sys.argv)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
name: Build WinPython (cycle file)
2+
# Draft, sitting beside github_workflows_build-YYYY_NN.yml for comparison.
3+
#
4+
# Same build as those, but everything cycle-specific comes from cycles/<cycle>.toml
5+
# instead of being copied into a new workflow each time. A new cycle is a new TOML.
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
cycle:
11+
description: 'Build cycle (file name in cycles/, without .toml)'
12+
required: true
13+
default: '2026_03'
14+
type: string
15+
python_versionf:
16+
description: 'Python version to build; must exist in that cycle file'
17+
required: true
18+
default: '3.14'
19+
type: choice
20+
options:
21+
- '3.13'
22+
- '3.14'
23+
- '3.14F'
24+
- '3.15'
25+
- '3.15F'
26+
27+
env:
28+
WINPYARCH: "64"
29+
dotwheelhouse: "dotpython\\wheelhouse\\included.wheels"
30+
31+
jobs:
32+
config:
33+
# reads the cycle file once, instead of once per matrix leg
34+
runs-on: ubuntu-latest
35+
outputs:
36+
ver2: ${{ steps.cfg.outputs.ver2 }}
37+
src: ${{ steps.cfg.outputs.src }}
38+
sha: ${{ steps.cfg.outputs.sha }}
39+
cycle_dir: ${{ steps.cfg.outputs.cycle_dir }}
40+
release_level: ${{ steps.cfg.outputs.release_level }}
41+
pandoc_source: ${{ steps.cfg.outputs.pandoc_source }}
42+
pandoc_sha256: ${{ steps.cfg.outputs.pandoc_sha256 }}
43+
matrix: ${{ steps.cfg.outputs.matrix }}
44+
steps:
45+
- uses: actions/checkout@v6
46+
with:
47+
persist-credentials: false
48+
49+
- name: Read cycle configuration
50+
id: cfg
51+
run: python .github/scripts/cycle_config.py "cycles/${{ inputs.cycle }}.toml" "${{ inputs.python_versionf }}"
52+
53+
build-winpython:
54+
needs: config
55+
runs-on: windows-latest
56+
strategy:
57+
fail-fast: true
58+
matrix: ${{ fromJSON(needs.config.outputs.matrix) }}
59+
60+
env:
61+
PYTHON_VERSIONF: ${{ inputs.python_versionf }}
62+
WINPYFLAVOR: ${{ matrix.flavor.name }}
63+
PANDOC: ${{ matrix.flavor.PANDOC }}
64+
WINPYARCHDET: ${{ matrix.flavor.WINPYARCHDET }}
65+
my_cycle: ${{ needs.config.outputs.cycle_dir }}
66+
my_release_level: ${{ needs.config.outputs.release_level }}
67+
68+
steps:
69+
- name: Checkout repository
70+
uses: actions/checkout@v6
71+
72+
- name: Set static and matrix variables based on selected Python version
73+
shell: pwsh
74+
env:
75+
PYTHON_VERSIONF: ${{ env.PYTHON_VERSIONF }}
76+
WINPYARCHDET: ${{ env.WINPYARCHDET }}
77+
WINPYVER2: ${{ needs.config.outputs.ver2 }}
78+
my_cycle: ${{ env.my_cycle }}
79+
my_release_level: ${{ env.my_release_level }}
80+
FLAVOR_NAME: ${{ matrix.flavor.name }}
81+
run: |
82+
# Normalize PYTHON_VERSION by removing trailing 'F' if present
83+
$PYTHON_VERSION = $env:PYTHON_VERSIONF -replace 'F$',''
84+
Add-Content -Path $env:GITHUB_ENV -Value "PYTHON_VERSION=$PYTHON_VERSION"
85+
86+
# Detect architecture (64 or 64F)
87+
$detected_arch = if ($env:PYTHON_VERSIONF -like '*F') { '64F' } else { '64' }
88+
89+
$WINPYVER2 = $env:WINPYVER2
90+
$BUILD_LOCATION = "WPy64-" + ($WINPYVER2 -replace '[.]', "")
91+
Add-Content -Path $env:GITHUB_ENV -Value "build_location=$BUILD_LOCATION"
92+
93+
# ver2-vs-source consistency is checked in cycle_config.py
94+
95+
$WINPYREQUIREMENTS = ''
96+
$WINPYREQUIREMENTSwhl = ''
97+
98+
# Generate requirement files expected names dynamically
99+
$V_TAG = $env:WINPYVER2 -replace '\.', '_'
100+
$testreq = "$($env:my_cycle)/requir.64-$($V_TAG)$($env:FLAVOR_NAME)$($env:my_release_level).txt"
101+
$testwhl = "$($env:my_cycle)/requir.64-$($V_TAG)$($env:FLAVOR_NAME)$($env:my_release_level)_wheels.txt"
102+
103+
Write-Host "Checking for requirements files: $testreq and $testwhl (expected arch $detected_arch)"
104+
105+
if ($env:WINPYARCHDET -eq $detected_arch -and (Test-Path $testreq)) {
106+
$WINPYREQUIREMENTS = $testreq
107+
Write-Host "Found $WINPYREQUIREMENTS"
108+
if (Test-Path $testwhl) {
109+
$WINPYREQUIREMENTSwhl = $testwhl
110+
Write-Host "Found also $WINPYREQUIREMENTSwhl"
111+
}
112+
}
113+
114+
Add-Content -Path $env:GITHUB_ENV -Value "WINPYREQUIREMENTS=$WINPYREQUIREMENTS"
115+
Add-Content -Path $env:GITHUB_ENV -Value "WINPYREQUIREMENTSwhl=$WINPYREQUIREMENTSwhl"
116+
117+
$WINPYLOCKFILE = ''
118+
$WINPYLOCKFILEwhl = ''
119+
120+
# Generate pylock files expected names dynamically
121+
$testreq = "$($env:my_cycle)/pylock.64-$($V_TAG)$($env:FLAVOR_NAME)$($env:my_release_level).toml"
122+
$testwhl = "$($env:my_cycle)/pylock.64-$($V_TAG)$($env:FLAVOR_NAME)$($env:my_release_level)_wheels.toml"
123+
124+
Write-Host "Checking for pylock files: $testreq and $testwhl (expected arch $detected_arch)"
125+
126+
if ($env:WINPYARCHDET -eq $detected_arch -and (Test-Path $testreq)) {
127+
$WINPYLOCKFILE = $testreq
128+
Write-Host "Found $WINPYLOCKFILE"
129+
if (Test-Path $testwhl) {
130+
$WINPYLOCKFILEwhl = $testwhl
131+
Write-Host "Found also $WINPYLOCKFILEwhl"
132+
}
133+
}
134+
135+
Add-Content -Path $env:GITHUB_ENV -Value "WINPYLOCKFILE=$WINPYLOCKFILE"
136+
Add-Content -Path $env:GITHUB_ENV -Value "WINPYLOCKFILEwhl=$WINPYLOCKFILEwhl"
137+
138+
$ARTIFACT_NAME = "publish_${PYTHON_VERSION}$($env:FLAVOR_NAME)"
139+
Add-Content -Path $env:GITHUB_ENV -Value "ARTIFACT_NAME=$ARTIFACT_NAME"
140+
141+
$destwheelhouse = "$BUILD_LOCATION\wheelhouse\included.wheels"
142+
Add-Content -Path $env:GITHUB_ENV -Value "destwheelhouse=$destwheelhouse"
143+
144+
$WINPYVER = "${WINPYVER2}$($env:FLAVOR_NAME)$($env:my_release_level)"
145+
Add-Content -Path $env:GITHUB_ENV -Value "WINPYVER=$WINPYVER"
146+
147+
# Store WINPYVER2 in env for later steps
148+
Add-Content -Path $env:GITHUB_ENV -Value "WINPYVER2=$WINPYVER2"
149+
150+
- name: Download, verify and extract python standalone
151+
if: env.WINPYLOCKFILE != ''
152+
uses: ./.github/actions/python-setup
153+
with:
154+
python_source: ${{ needs.config.outputs.src }}
155+
python_sha256: ${{ needs.config.outputs.sha }}
156+
build_location: ${{ env.build_location }}
157+
158+
- name: Download, checking hash and integrating pandoc binary
159+
if: env.WINPYLOCKFILE != '' && env.PANDOC == '1'
160+
uses: ./.github/actions/pandoc-setup
161+
with:
162+
pandoc_source: ${{ needs.config.outputs.pandoc_source }}
163+
pandoc_sha256: ${{ needs.config.outputs.pandoc_sha256 }}
164+
build_location: ${{ env.build_location }}
165+
166+
- name: Upgrade pip and patch launchers
167+
if: env.WINPYLOCKFILE != ''
168+
shell: pwsh
169+
run: |
170+
& "$env:build_location\python\python.exe" -m pip install --upgrade --force-reinstall pip --no-warn-script-location
171+
& "$env:build_location\python\python.exe" -c "from wppm import wppm;dist=wppm.Distribution();dist.patch_standard_packages('pip', to_movable=True)"
172+
173+
- name: Download all requirements
174+
if: ${{ env.WINPYLOCKFILE != '' }}
175+
shell: pwsh
176+
run: |
177+
$py = "$env:build_location\python\python.exe"
178+
& $py -m pip download --dest $env:dotwheelhouse --no-deps --require-hashes -r $env:WINPYLOCKFILE
179+
if ($env:WINPYLOCKFILEwhl -ne '') {
180+
& $py -m pip download --dest $env:destwheelhouse --no-deps --require-hashes -r $env:WINPYLOCKFILEwhl
181+
}
182+
183+
- name: Install lockfile
184+
if: env.WINPYLOCKFILE != ''
185+
shell: pwsh
186+
run: |
187+
& "$env:build_location\python\python.exe" -m pip install --no-deps --require-hashes -r $env:WINPYLOCKFILE --no-warn-script-location
188+
189+
- name: Generate Assets and Hashes
190+
if: env.WINPYLOCKFILE != ''
191+
uses: ./.github/actions/publish-winpython
192+
with:
193+
build_location: ${{ env.build_location }}
194+
winpy_flavor: ${{ env.WINPYFLAVOR }}
195+
winpy_arch: ${{ env.WINPYARCH }}
196+
winpy_ver: ${{ env.WINPYVER }}
197+
winpy_ver2: ${{ env.WINPYVER2 }}
198+
dotwheelhouse: ${{ env.dotwheelhouse }}
199+
winpy_requirements_whl: ${{ env.WINPYREQUIREMENTSwhl }}
200+
format_zip: ${{ matrix.flavor.formats.zip }}
201+
format_7z: ${{ matrix.flavor.formats['7z'] }}
202+
format_exe: ${{ matrix.flavor.formats.exe }}
203+
204+
- name: Upload artifacts
205+
if: env.WINPYLOCKFILE != ''
206+
uses: actions/upload-artifact@v6
207+
with:
208+
name: ${{ env.ARTIFACT_NAME }}
209+
path: publish_output
210+
retention-days: 66 # keeps artifact for 66 days

cycles/2026_03.toml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# WinPython 2026-03 build cycle.
2+
#
3+
# Everything that changes from one cycle to the next lives here, so a new cycle
4+
# is a new file rather than a copy of the workflow.
5+
6+
cycle_dir = "winpython/portable/cycle_2026_03"
7+
release_level = "b3"
8+
9+
[pandoc]
10+
source = "https://github.com/jgm/pandoc/releases/download/3.1.9/pandoc-3.1.9-windows-x86_64.zip"
11+
sha256 = "11eb6dbe5286c9e5edb0cca4412e7d99ec6578ec04158b0b7fe11f7fd96688e5"
12+
13+
# The python-build-standalone tarball for each version offered by the workflow.
14+
# ver2 is the WinPython 4-part version; its first 3 parts must appear in src.
15+
16+
[pythons."3.13"]
17+
ver2 = "3.13.15.0"
18+
src = "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.13.15+20260807-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
19+
sha = "44bf9ae71f4b45e3ba3104ae331c6eff3f7002593c26fd12453eb9310c4f259a"
20+
21+
[pythons."3.14"]
22+
ver2 = "3.14.7.0"
23+
src = "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.14.7+20260807-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
24+
sha = "840e368318d1630bd8efbd3f7de8fab32cb4bbd735baef733f9034f2676c6a92"
25+
26+
[pythons."3.14F"]
27+
ver2 = "3.14.7.0"
28+
src = "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.14.7+20260807-x86_64-pc-windows-msvc-freethreaded-install_only_stripped.tar.gz"
29+
sha = "c8323293c9e76e8b244a0e533894512f067dac2a6aa2dfcb3494cbe74b3af58e"
30+
31+
[pythons."3.15"]
32+
ver2 = "3.15.0.4"
33+
src = "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.15.0rc1+20260807-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
34+
sha = "bc0d0f3c7b88873688f30bc78da8387a6653a5493424951d3a84c658c973198e"
35+
36+
[pythons."3.15F"]
37+
ver2 = "3.15.0.4"
38+
src = "https://github.com/astral-sh/python-build-standalone/releases/download/20260807/cpython-3.15.0rc1+20260807-x86_64-pc-windows-msvc-freethreaded-install_only_stripped.tar.gz"
39+
sha = "e5998e014f16074ac9f6d61eb2a31ff8ac8bc331b61da2c5acf7d15abf4a2650"
40+
41+
# Flavors become the job matrix. 2026-02 renamed "free" to "dotf", so this
42+
# varies per cycle too and belongs in the cycle file.
43+
44+
[[flavors]]
45+
name = "dot"
46+
PANDOC = "0"
47+
WINPYARCHDET = "64"
48+
formats = { zip = true, "7z" = false, exe = true }
49+
50+
[[flavors]]
51+
name = "slim"
52+
PANDOC = "1"
53+
WINPYARCHDET = "64"
54+
formats = { zip = false, "7z" = true, exe = true }
55+
56+
[[flavors]]
57+
name = "whl"
58+
PANDOC = "0"
59+
WINPYARCHDET = "64"
60+
formats = { zip = false, "7z" = true, exe = false }
61+
62+
[[flavors]]
63+
name = "dotf"
64+
PANDOC = "0"
65+
WINPYARCHDET = "64F"
66+
formats = { zip = true, "7z" = false, exe = true }
67+
68+
[[flavors]]
69+
name = "slimf"
70+
PANDOC = "1"
71+
WINPYARCHDET = "64F"
72+
formats = { zip = false, "7z" = true, exe = true }

0 commit comments

Comments
 (0)