Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/actions/publish-winpython/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,15 @@ runs:
Compress-Archive -Path "${{ inputs.build_location }}" -DestinationPath "$baseName.zip" -Force
}
if ("${{ inputs.format_7z }}" -eq "true") {
7z a -mx7 "$baseName.7z" "${{ inputs.build_location }}"
7z a -mx9 "$baseName.7z" "${{ inputs.build_location }}"
}
if ("${{ inputs.format_exe }}" -eq "true") {
if ("${{ inputs.format_exe }}" -eq "true") {
$SFXModulePath = "C:\Program Files\7-Zip\7z.sfx"
7z a -mx7 -t7z -sfx"$SFXModulePath" "$baseName.exe" "${{ inputs.build_location }}"
7z a -mx9 -t7z -sfx"$SFXModulePath" "$baseName.exe" "${{ inputs.build_location }}"
}
# VirusTotal refuses uploads over 650 MB
Get-ChildItem "$baseName.*" | Where-Object Length -gt 650MB | ForEach-Object {
Write-Output "::warning::$($_.Name) is $([math]::Round($_.Length / 1MB)) MB, over the VirusTotal 650 MB limit"
}

- name: Generate Hashes
Expand Down
33 changes: 29 additions & 4 deletions .github/scripts/cycle_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

python .github/scripts/cycle_config.py cycles/2026_04.toml all
python .github/scripts/cycle_config.py cycles/2026_04.toml 3.14
python .github/scripts/cycle_config.py cycles/2026_04.toml all slim,slimf

Writes to $GITHUB_OUTPUT when set, otherwise stdout, so it can be run locally
to see exactly what a workflow run would get. tomllib is stdlib from 3.11, and
Expand Down Expand Up @@ -154,8 +155,28 @@ def release_title(cfg: dict, cycle_name: str, today: datetime.date | None = None
return f"WinPython {name} of {today:%B} {ordinal(today.day)} {today.year}"


def build_config(cfg: dict, requested: str, cycle_name: str) -> dict:
def wanted_flavors(cfg: dict, flavors: str) -> set[str] | None:
"""The flavor names a comma-separated input asks for; None means all.

A typo would otherwise filter everything out, or quietly build less than
asked, so a name the cycle does not declare is refused.
"""
if flavors.strip() in ("", "all"):
return None
names = {name.strip() for name in flavors.split(",") if name.strip()}
declared = {flavor["name"] for flavor in cfg["flavors"]}
unknown = names - declared
if unknown:
raise SystemExit(
f"unknown flavor {', '.join(sorted(unknown))}; this cycle declares "
f"{', '.join(sorted(declared))} (or 'all')"
)
return names


def build_config(cfg: dict, requested: str, cycle_name: str, flavors: str = "all") -> dict:
pythons = cfg["pythons"]
only = wanted_flavors(cfg, flavors)
if requested == "all":
wanted = list(pythons)
elif requested in pythons:
Expand All @@ -167,6 +188,10 @@ def build_config(cfg: dict, requested: str, cycle_name: str) -> dict:
)

legs = [leg for python in wanted for leg in legs_for(cfg, python)]
if only is not None:
legs = [leg for leg in legs if leg["name"] in only]
if not legs and only is not None:
raise SystemExit(f"{requested}: none of {flavors} can be built for this Python")
if not legs:
missing = ", ".join(
f"pylock.64-{pythons[p]['ver2'].replace('.', '_')}<flavor>"
Expand All @@ -191,15 +216,15 @@ def build_config(cfg: dict, requested: str, cycle_name: str) -> dict:


def main(argv: list[str]) -> None:
if len(argv) != 3:
raise SystemExit(f"usage: {Path(argv[0]).name} <cycle.toml> <python_version|all>")
if len(argv) not in (3, 4):
raise SystemExit(f"usage: {Path(argv[0]).name} <cycle.toml> <python_version|all> [flavors|all]")
cfg_path = Path(argv[1])
if not cfg_path.is_file():
raise SystemExit(f"no such cycle file: {cfg_path}")
with cfg_path.open("rb") as fh:
cfg = tomllib.load(fh)

config = build_config(cfg, argv[2], cfg_path.stem)
config = build_config(cfg, argv[2], cfg_path.stem, argv[3] if len(argv) == 4 else "all")
rendered = "".join(f"{k}={v}\n" for k, v in config.items())
out = os.environ.get("GITHUB_OUTPUT")
if out:
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/build_winpython_cycle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ on:
- '3.14F'
- '3.15'
- '3.15F'
flavors:
description: 'Flavors to build, comma-separated (e.g. slim,slimf); "all" builds every one'
required: true
default: 'all'
type: string
publish:
description: 'Upload to the draft release the cycle file names; off keeps the output as artifacts'
required: false
Expand Down Expand Up @@ -69,7 +74,8 @@ jobs:
env:
CYCLE: ${{ inputs.cycle }}
PYTHON_VERSIONF: ${{ inputs.python_versionf }}
run: python .github/scripts/cycle_config.py "cycles/$CYCLE.toml" "$PYTHON_VERSIONF"
FLAVORS: ${{ inputs.flavors }}
run: python .github/scripts/cycle_config.py "cycles/$CYCLE.toml" "$PYTHON_VERSIONF" "$FLAVORS"

release-draft:
# one job opens the draft, so the build legs cannot race to create it
Expand Down Expand Up @@ -232,7 +238,7 @@ jobs:
# 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' }}
if: ${{ inputs.publish && inputs.python_versionf == 'all' && inputs.flavors == 'all' }}
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
38 changes: 37 additions & 1 deletion tests/test_cycle_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,39 @@ def test_every_python_can_be_dispatched_on_its_own(self, cycle_file, workflow_te
assert pythons <= offered, f"{cycle_file.stem} offers {pythons - offered}, the workflow does not"


@needs_script
@needs_cycles
@pytest.mark.parametrize("cycle_file", cycle_files, ids=lambda p: p.stem)
class TestFlavorFilter:
"""Re-running only some flavors, e.g. slim after a size fix."""

def test_all_is_no_filter(self, cycle_config, at_repo_root, cycle_file):
everything = legs_of(config_for(cycle_config, cycle_file))
import tomllib

with cycle_file.open("rb") as fh:
cfg = tomllib.load(fh)
assert legs_of(cycle_config.build_config(cfg, "all", cycle_file.stem, "all")) == everything

def test_keeps_only_the_named_flavors(self, cycle_config, at_repo_root, cycle_file):
import tomllib

with cycle_file.open("rb") as fh:
cfg = tomllib.load(fh)
built = {leg["name"] for leg in legs_of(config_for(cycle_config, cycle_file))}
some = sorted(built)[:2]
legs = legs_of(cycle_config.build_config(cfg, "all", cycle_file.stem, ", ".join(some)))
assert legs and {leg["name"] for leg in legs} == set(some)

def test_an_unknown_flavor_is_refused(self, cycle_config, at_repo_root, cycle_file):
import tomllib

with cycle_file.open("rb") as fh:
cfg = tomllib.load(fh)
with pytest.raises(SystemExit):
cycle_config.build_config(cfg, "all", cycle_file.stem, "slimm")


@needs_script
class TestReleaseTag:
"""The tag is what the download URLs the website publishes are built on."""
Expand Down Expand Up @@ -314,7 +347,10 @@ def test_the_changelog_pr_is_whole_cycle_only(self, workflow_text):
It has nothing to add either way: the same lockfile builds the same
package list, so a rebuilt leg cannot change a changelog.
"""
assert "if: ${{ inputs.publish && inputs.python_versionf == 'all' }}" in workflow_text
assert (
"if: ${{ inputs.publish && inputs.python_versionf == 'all' && inputs.flavors == 'all' }}"
in workflow_text
)
assert ".github/scripts/changelog_files.py release_metadata changelogs" in workflow_text
assert (REPO / ".github/scripts/changelog_files.py").is_file()

Expand Down
Loading