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
15 changes: 14 additions & 1 deletion .github/scripts/cycle_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
says so with a release_tag key.
* a cycle with nothing to build fails here, loudly, instead of starting
runners whose every step is then skipped.
* a flavor's optional post suffix ("_post1"): a rebuild of the same
lockfile under new file names. It can join a release that is already
published, since nothing there gets replaced.

Paths are relative to the working directory, which in CI is the checkout root.
"""
Expand All @@ -40,6 +43,9 @@
# parts a hand-written tag realistically trips over.
TAG_FORBIDDEN = set(" ~^:?*[\\\x7f") | {chr(c) for c in range(32)}

# a post suffix lands in file names and URLs
POST_ALLOWED = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-")


def python_facts(requested: str, entry: dict) -> dict:
"""What every flavor of one Python shares.
Expand Down Expand Up @@ -86,6 +92,10 @@ def flavor_entry(cfg: dict, flavor: dict, python: dict) -> dict | None:
def optional(path: Path) -> str:
return path.as_posix() if path.is_file() else ""

post = flavor.get("post", "")
if set(post) - POST_ALLOWED:
raise SystemExit(f"{flavor['name']}: post {post!r} must be letters, digits, '.', '_' or '-'")

leg = {key: value for key, value in python.items() if key != "archdet"}
leg.update(
name=flavor["name"],
Expand All @@ -94,7 +104,8 @@ def optional(path: Path) -> str:
lockfile=lockfile.as_posix(),
lockfile_wheels=optional(cycle_dir / f"pylock.{stem}_wheels.toml"),
requirements_wheels=optional(cycle_dir / f"requir.{stem}_wheels.txt"),
winpyver=f"{python['ver2']}{flavor['name']}{level}",
post=post,
winpyver=f"{python['ver2']}{flavor['name']}{level}{post}",
artifact_name=f"publish_{python['python_version']}{flavor['name']}",
)
return leg
Expand Down Expand Up @@ -208,6 +219,8 @@ def build_config(cfg: dict, requested: str, cycle_name: str, flavors: str = "all
"release_level": cfg.get("release_level", ""),
"release_tag": release_tag(cfg, cycle_name),
"release_title": release_title(cfg, cycle_name),
# every leg renamed by a post suffix: none of its files can replace one
"adds_only": "true" if all(leg["post"] for leg in legs) else "false",
"pandoc_source": cfg["pandoc"]["source"],
"pandoc_sha256": cfg["pandoc"]["sha256"],
# consumed by the build job as strategy.matrix via fromJSON
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/build_winpython_cycle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
release_level: ${{ steps.cfg.outputs.release_level }}
release_tag: ${{ steps.cfg.outputs.release_tag }}
release_title: ${{ steps.cfg.outputs.release_title }}
adds_only: ${{ steps.cfg.outputs.adds_only }}
matrix: ${{ steps.cfg.outputs.matrix }}
steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -93,14 +94,16 @@ jobs:
TITLE: ${{ needs.config.outputs.release_title }}
CYCLE: ${{ inputs.cycle }}
OVERWRITE: ${{ inputs.overwrite_published }}
ADDS_ONLY: ${{ needs.config.outputs.adds_only }}
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
# a post respin only adds new file names, so it may join a published one
if [ "$published" = "true" ] && [ "$OVERWRITE" != "true" ] && [ "$ADDS_ONLY" != "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
Expand Down Expand Up @@ -183,7 +186,7 @@ jobs:
winpy_arch: ${{ env.WINPYARCH }}
winpy_ver: ${{ env.WINPYVER }}
winpy_ver2: ${{ env.WINPYVER2 }}
release_level: ${{ needs.config.outputs.release_level }}
release_level: ${{ needs.config.outputs.release_level }}${{ matrix.leg.post }}
dotwheelhouse: ${{ env.dotwheelhouse }}
winpy_requirements_whl: ${{ matrix.leg.requirements_wheels }}
format_zip: ${{ matrix.leg.formats.zip }}
Expand Down
1 change: 1 addition & 0 deletions cycles/2026_04.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ name = "slim"
PANDOC = "1"
WINPYARCHDET = "64"
formats = { zip = false, "7z" = true, exe = true }
post = "_post1" # -mx9 re-spin, to fit VirusTotal's 650 MB

[[flavors]]
name = "whl"
Expand Down
25 changes: 25 additions & 0 deletions tests/test_cycle_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,31 @@ def test_an_unknown_flavor_is_refused(self, cycle_config, at_repo_root, cycle_fi
cycle_config.build_config(cfg, "all", cycle_file.stem, "slimm")


@needs_script
class TestPostSuffix:
"""A re-spin of one flavor under new file names, the lockfile unchanged."""

CFG = {
"cycle_dir": "nowhere",
"pythons": {},
"flavors": [{"name": "slim", "PANDOC": "1", "formats": {}, "post": "_post1"}],
}
PYTHON = {"ver2": "3.14.10.0", "python_version": "3.14", "archdet": "64"}

def test_renames_the_output_not_the_lockfile(self, cycle_config, tmp_path):
(tmp_path / "pylock.64-3_14_10_0slim.toml").write_text("")
cfg = dict(self.CFG, cycle_dir=str(tmp_path))
leg = cycle_config.flavor_entry(cfg, cfg["flavors"][0], self.PYTHON)
assert leg["winpyver"] == "3.14.10.0slim_post1"
assert leg["lockfile"].endswith("pylock.64-3_14_10_0slim.toml")

def test_a_suffix_unfit_for_a_url_is_refused(self, cycle_config, tmp_path):
(tmp_path / "pylock.64-3_14_10_0slim.toml").write_text("")
flavor = dict(self.CFG["flavors"][0], post=" post/1")
with pytest.raises(SystemExit):
cycle_config.flavor_entry(dict(self.CFG, cycle_dir=str(tmp_path)), flavor, self.PYTHON)


@needs_script
class TestReleaseTag:
"""The tag is what the download URLs the website publishes are built on."""
Expand Down
Loading