Skip to content

Commit a8d294c

Browse files
authored
Merge pull request winpython#2102 from stonebig/release-title-keeps-its-shape
Title the release the way the previous ones were titled
2 parents c5a6e82 + c4704bb commit a8d294c

3 files changed

Lines changed: 85 additions & 2 deletions

File tree

‎.github/scripts/cycle_config.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
2828
Paths are relative to the working directory, which in CI is the checkout root.
2929
"""
30+
import datetime
3031
import json
3132
import os
3233
import sys
@@ -128,6 +129,31 @@ def release_tag(cfg: dict, cycle_name: str) -> str:
128129
return tag
129130

130131

132+
def ordinal(day: int) -> str:
133+
"""1st, 2nd, 3rd, 4th ... and the 11th/12th/13th exceptions."""
134+
if 11 <= day % 100 <= 13:
135+
return f"{day}th"
136+
return f"{day}{ {1: 'st', 2: 'nd', 3: 'rd'}.get(day % 10, 'th') }"
137+
138+
139+
def release_title(cfg: dict, cycle_name: str, today: datetime.date | None = None) -> str:
140+
""""WinPython 2026-04 b1 of September 6th 2026", the shape releases have used.
141+
142+
Kept deliberately close to the titles written by hand before this: cycle,
143+
then the level after a space -- the tag runs them together, the title does
144+
not -- then the date, no comma, which is how all but one of them read.
145+
146+
The date is the day the draft is opened, since that is the only day the
147+
build knows about. A cycle publishing much later than it was built can
148+
still have the title edited; the tag, which is what URLs are built on,
149+
does not move.
150+
"""
151+
today = today or datetime.date.today()
152+
level = cfg.get("release_level", "")
153+
name = f"{cycle_name.replace('_', '-')} {level}".strip()
154+
return f"WinPython {name} of {today:%B} {ordinal(today.day)} {today.year}"
155+
156+
131157
def build_config(cfg: dict, requested: str, cycle_name: str) -> dict:
132158
pythons = cfg["pythons"]
133159
if requested == "all":
@@ -156,6 +182,7 @@ def build_config(cfg: dict, requested: str, cycle_name: str) -> dict:
156182
"cycle_dir": cfg["cycle_dir"],
157183
"release_level": cfg.get("release_level", ""),
158184
"release_tag": release_tag(cfg, cycle_name),
185+
"release_title": release_title(cfg, cycle_name),
159186
"pandoc_source": cfg["pandoc"]["source"],
160187
"pandoc_sha256": cfg["pandoc"]["sha256"],
161188
# 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
@@ -52,6 +52,7 @@ jobs:
5252
pandoc_sha256: ${{ steps.cfg.outputs.pandoc_sha256 }}
5353
release_level: ${{ steps.cfg.outputs.release_level }}
5454
release_tag: ${{ steps.cfg.outputs.release_tag }}
55+
release_title: ${{ steps.cfg.outputs.release_title }}
5556
matrix: ${{ steps.cfg.outputs.matrix }}
5657
steps:
5758
- uses: actions/checkout@v6
@@ -78,15 +79,17 @@ jobs:
7879
GH_TOKEN: ${{ github.token }}
7980
GH_REPO: ${{ github.repository }}
8081
TAG: ${{ needs.config.outputs.release_tag }}
82+
TITLE: ${{ needs.config.outputs.release_title }}
8183
CYCLE: ${{ inputs.cycle }}
8284
run: |
8385
if gh release view "$TAG" >/dev/null 2>&1; then
84-
# re-running one missing flavor is normal; say which release its files land on
86+
# re-running one missing flavor is normal; say which release its files
87+
# land on, and leave the title alone in case it was edited by hand
8588
state=$(gh release view "$TAG" --json isDraft --jq 'if .isDraft then "still a draft" else "ALREADY PUBLISHED" end')
8689
echo "release $TAG exists ($state); this run adds its files to it"
8790
else
8891
gh release create "$TAG" --draft \
89-
--title "WinPython $TAG" \
92+
--title "$TITLE" \
9093
--notes "Draft opened by the build workflow for cycle $CYCLE. Files land as each build finishes; publish once they are all here."
9194
echo "opened draft release $TAG"
9295
fi

‎tests/test_cycle_config.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,54 @@ def test_every_cycle_has_a_usable_tag(self, cycle_config, at_repo_root, cycle_fi
192192
assert tag and not set(tag) & cycle_config.TAG_FORBIDDEN
193193

194194

195+
@needs_script
196+
class TestReleaseTitle:
197+
"""The title people actually read on the releases page.
198+
199+
Reproduced from the titles written by hand for years rather than invented:
200+
cycle, then the level after a space -- the tag runs them together, the
201+
title does not -- then the date with no comma, which is how all but one of
202+
the previous titles read.
203+
"""
204+
205+
@pytest.mark.parametrize("cycle,level,date,expected", [
206+
# the real titles of these releases, to the letter
207+
("2026_03", "", (2026, 8, 22), "WinPython 2026-03 of August 22nd 2026"),
208+
("2026_03", "b3", (2026, 8, 8), "WinPython 2026-03 b3 of August 8th 2026"),
209+
("2026_02", "b2", (2026, 5, 1), "WinPython 2026-02 b2 of May 1st 2026"),
210+
("2026_01", "final", (2026, 3, 10), "WinPython 2026-01 final of March 10th 2026"),
211+
("2025_05", "rc", (2025, 12, 22), "WinPython 2025-05 rc of December 22nd 2025"),
212+
("2026_01", "b3", (2026, 2, 24), "WinPython 2026-01 b3 of February 24th 2026"),
213+
])
214+
def test_matches_the_titles_used_before(self, cycle_config, cycle, level, date, expected):
215+
import datetime
216+
217+
cfg = {"release_level": level} if level else {}
218+
assert cycle_config.release_title(cfg, cycle, datetime.date(*date)) == expected
219+
220+
def test_no_double_space_when_there_is_no_level(self, cycle_config):
221+
"""Some older titles read "2026-02 of May 17th": a level that was empty."""
222+
import datetime
223+
224+
title = cycle_config.release_title({"release_level": ""}, "2026_04", datetime.date(2026, 5, 17))
225+
assert " " not in title
226+
227+
@pytest.mark.parametrize("day,expected", [
228+
(1, "1st"), (2, "2nd"), (3, "3rd"), (4, "4th"),
229+
(11, "11th"), (12, "12th"), (13, "13th"), # not 11st/12nd/13rd
230+
(21, "21st"), (22, "22nd"), (23, "23rd"), (30, "30th"), (31, "31st"),
231+
])
232+
def test_ordinals(self, cycle_config, day, expected):
233+
assert cycle_config.ordinal(day) == expected
234+
235+
@needs_cycles
236+
@pytest.mark.parametrize("cycle_file", cycle_files, ids=lambda p: p.stem)
237+
def test_every_cycle_produces_a_title(self, cycle_config, at_repo_root, cycle_file):
238+
title = config_for(cycle_config, cycle_file)["release_title"]
239+
assert title.startswith("WinPython ") and " of " in title
240+
assert "\n" not in title, "a GITHUB_OUTPUT value has to stay on one line"
241+
242+
195243
@needs_script
196244
@needs_workflow
197245
@needs_cycles
@@ -260,6 +308,11 @@ def test_publishing_is_gated_both_ways(self, workflow_text):
260308
assert "if: ${{ inputs.publish }}" in workflow_text
261309
assert "if: ${{ !inputs.publish }}" in workflow_text
262310

311+
def test_the_release_title_is_built_by_the_script(self, workflow_text):
312+
"""Ordinal dates are miserable in shell, and untestable there."""
313+
assert 'TITLE: ${{ needs.config.outputs.release_title }}' in workflow_text
314+
assert '--title "$TITLE"' in workflow_text
315+
263316
def test_the_changelog_name_carries_the_release_level(self, workflow_text):
264317
"""A beta and the final it becomes share a ver2.
265318

0 commit comments

Comments
 (0)