Skip to content

Commit 17179aa

Browse files
authored
Merge pull request #2129 from stonebig/master
cycle: per-flavor post suffix for a re-spin; slim_post1 for 2026-04
2 parents ea8c51f + 1803379 commit 17179aa

4 files changed

Lines changed: 45 additions & 3 deletions

File tree

‎.github/scripts/cycle_config.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
says so with a release_tag key.
2626
* a cycle with nothing to build fails here, loudly, instead of starting
2727
runners whose every step is then skipped.
28+
* a flavor's optional post suffix ("_post1"): a rebuild of the same
29+
lockfile under new file names. It can join a release that is already
30+
published, since nothing there gets replaced.
2831
2932
Paths are relative to the working directory, which in CI is the checkout root.
3033
"""
@@ -40,6 +43,9 @@
4043
# parts a hand-written tag realistically trips over.
4144
TAG_FORBIDDEN = set(" ~^:?*[\\\x7f") | {chr(c) for c in range(32)}
4245

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

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

95+
post = flavor.get("post", "")
96+
if set(post) - POST_ALLOWED:
97+
raise SystemExit(f"{flavor['name']}: post {post!r} must be letters, digits, '.', '_' or '-'")
98+
8999
leg = {key: value for key, value in python.items() if key != "archdet"}
90100
leg.update(
91101
name=flavor["name"],
@@ -94,7 +104,8 @@ def optional(path: Path) -> str:
94104
lockfile=lockfile.as_posix(),
95105
lockfile_wheels=optional(cycle_dir / f"pylock.{stem}_wheels.toml"),
96106
requirements_wheels=optional(cycle_dir / f"requir.{stem}_wheels.txt"),
97-
winpyver=f"{python['ver2']}{flavor['name']}{level}",
107+
post=post,
108+
winpyver=f"{python['ver2']}{flavor['name']}{level}{post}",
98109
artifact_name=f"publish_{python['python_version']}{flavor['name']}",
99110
)
100111
return leg
@@ -208,6 +219,8 @@ def build_config(cfg: dict, requested: str, cycle_name: str, flavors: str = "all
208219
"release_level": cfg.get("release_level", ""),
209220
"release_tag": release_tag(cfg, cycle_name),
210221
"release_title": release_title(cfg, cycle_name),
222+
# every leg renamed by a post suffix: none of its files can replace one
223+
"adds_only": "true" if all(leg["post"] for leg in legs) else "false",
211224
"pandoc_source": cfg["pandoc"]["source"],
212225
"pandoc_sha256": cfg["pandoc"]["sha256"],
213226
# consumed by the build job as strategy.matrix via fromJSON

‎.github/workflows/build_winpython_cycle.yml‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ jobs:
6363
release_level: ${{ steps.cfg.outputs.release_level }}
6464
release_tag: ${{ steps.cfg.outputs.release_tag }}
6565
release_title: ${{ steps.cfg.outputs.release_title }}
66+
adds_only: ${{ steps.cfg.outputs.adds_only }}
6667
matrix: ${{ steps.cfg.outputs.matrix }}
6768
steps:
6869
- uses: actions/checkout@v6
@@ -93,14 +94,16 @@ jobs:
9394
TITLE: ${{ needs.config.outputs.release_title }}
9495
CYCLE: ${{ inputs.cycle }}
9596
OVERWRITE: ${{ inputs.overwrite_published }}
97+
ADDS_ONLY: ${{ needs.config.outputs.adds_only }}
9698
run: |
9799
if gh release view "$TAG" >/dev/null 2>&1; then
98100
# Adding a missing flavor to a draft is the normal path. Landing on
99101
# a release that is already published is almost always a forgotten
100102
# release_level bump, and the upload replaces files people have
101103
# downloaded -- so stop here, before an hour of build time.
102104
published=$(gh release view "$TAG" --json isDraft --jq '.isDraft | not')
103-
if [ "$published" = "true" ] && [ "$OVERWRITE" != "true" ]; then
105+
# a post respin only adds new file names, so it may join a published one
106+
if [ "$published" = "true" ] && [ "$OVERWRITE" != "true" ] && [ "$ADDS_ONLY" != "true" ]; then
104107
echo "::error::$TAG is already published. Bump release_level in cycles/$CYCLE.toml, or re-dispatch with overwrite_published ticked to replace its files."
105108
exit 1
106109
fi
@@ -183,7 +186,7 @@ jobs:
183186
winpy_arch: ${{ env.WINPYARCH }}
184187
winpy_ver: ${{ env.WINPYVER }}
185188
winpy_ver2: ${{ env.WINPYVER2 }}
186-
release_level: ${{ needs.config.outputs.release_level }}
189+
release_level: ${{ needs.config.outputs.release_level }}${{ matrix.leg.post }}
187190
dotwheelhouse: ${{ env.dotwheelhouse }}
188191
winpy_requirements_whl: ${{ matrix.leg.requirements_wheels }}
189192
format_zip: ${{ matrix.leg.formats.zip }}

‎cycles/2026_04.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ name = "slim"
5656
PANDOC = "1"
5757
WINPYARCHDET = "64"
5858
formats = { zip = false, "7z" = true, exe = true }
59+
post = "_post1" # -mx9 re-spin, to fit VirusTotal's 650 MB
5960

6061
[[flavors]]
6162
name = "whl"

‎tests/test_cycle_config.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,31 @@ def test_an_unknown_flavor_is_refused(self, cycle_config, at_repo_root, cycle_fi
185185
cycle_config.build_config(cfg, "all", cycle_file.stem, "slimm")
186186

187187

188+
@needs_script
189+
class TestPostSuffix:
190+
"""A re-spin of one flavor under new file names, the lockfile unchanged."""
191+
192+
CFG = {
193+
"cycle_dir": "nowhere",
194+
"pythons": {},
195+
"flavors": [{"name": "slim", "PANDOC": "1", "formats": {}, "post": "_post1"}],
196+
}
197+
PYTHON = {"ver2": "3.14.10.0", "python_version": "3.14", "archdet": "64"}
198+
199+
def test_renames_the_output_not_the_lockfile(self, cycle_config, tmp_path):
200+
(tmp_path / "pylock.64-3_14_10_0slim.toml").write_text("")
201+
cfg = dict(self.CFG, cycle_dir=str(tmp_path))
202+
leg = cycle_config.flavor_entry(cfg, cfg["flavors"][0], self.PYTHON)
203+
assert leg["winpyver"] == "3.14.10.0slim_post1"
204+
assert leg["lockfile"].endswith("pylock.64-3_14_10_0slim.toml")
205+
206+
def test_a_suffix_unfit_for_a_url_is_refused(self, cycle_config, tmp_path):
207+
(tmp_path / "pylock.64-3_14_10_0slim.toml").write_text("")
208+
flavor = dict(self.CFG["flavors"][0], post=" post/1")
209+
with pytest.raises(SystemExit):
210+
cycle_config.flavor_entry(dict(self.CFG, cycle_dir=str(tmp_path)), flavor, self.PYTHON)
211+
212+
188213
@needs_script
189214
class TestReleaseTag:
190215
"""The tag is what the download URLs the website publishes are built on."""

0 commit comments

Comments
 (0)