1010import tomllib
1111import zipfile
1212from dataclasses import dataclass
13+ from datetime import date
1314from email .message import Message
1415from email .parser import Parser
1516from pathlib import Path
2324 r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
2425 r"(?:(?:a|b|rc)\d+)?$"
2526)
27+ _RELEASE_DATE_PATTERN : Final [re .Pattern [str ]] = re .compile (r"^\d{4}-\d{2}-\d{2}$" )
2628
2729
2830class ReleaseValidationError (RuntimeError ):
@@ -128,14 +130,34 @@ def _check_root_extras(root: Path, version: str) -> None:
128130 )
129131
130132
133+ def _check_release_tag (tag : str , version : str ) -> None :
134+ version_tag = f"v{ version } "
135+ if tag == version_tag :
136+ return
137+
138+ dated_prefix = f"{ version_tag } _"
139+ if tag .startswith (dated_prefix ):
140+ release_date = tag .removeprefix (dated_prefix )
141+ if _RELEASE_DATE_PATTERN .fullmatch (release_date ) is not None :
142+ try :
143+ date .fromisoformat (release_date )
144+ except ValueError :
145+ pass
146+ else :
147+ return
148+
149+ raise ReleaseValidationError (
150+ f"Release tag { tag !r} does not match workspace version { version !r} ; "
151+ f"expected { version_tag !r} or { version_tag + '_YYYY-MM-DD' !r} ."
152+ )
153+
154+
131155def check_release (* , root : Path , tag : str | None ) -> str :
132156 version = _workspace_version (root )
133157 _check_changelog (root , version )
134158 _check_root_extras (root , version )
135- if tag is not None and tag != f"v{ version } " :
136- raise ReleaseValidationError (
137- f"Release tag { tag !r} does not match workspace version { version !r} ."
138- )
159+ if tag is not None :
160+ _check_release_tag (tag , version )
139161 print (f"Release metadata valid for { version } ." )
140162 return version
141163
@@ -334,7 +356,10 @@ def _parser() -> argparse.ArgumentParser:
334356 subparsers = parser .add_subparsers (dest = "command" , required = True )
335357
336358 check_parser = subparsers .add_parser ("check" , help = "Validate release metadata." )
337- check_parser .add_argument ("--tag" , help = "Expected git tag, for example v1.0.0." )
359+ check_parser .add_argument (
360+ "--tag" ,
361+ help = "Expected git tag, for example v1.0.0 or v1.0.0_2026-07-04." ,
362+ )
338363
339364 build_parser = subparsers .add_parser ("build" , help = "Build and validate all artifacts." )
340365 build_parser .add_argument ("--output-dir" , type = Path , default = Path ("dist" ))
@@ -347,7 +372,11 @@ def _parser() -> argparse.ArgumentParser:
347372 prepare_parser = subparsers .add_parser (
348373 "prepare" , help = "Validate, build, and smoke test a tagged release."
349374 )
350- prepare_parser .add_argument ("--tag" , required = True )
375+ prepare_parser .add_argument (
376+ "--tag" ,
377+ required = True ,
378+ help = "Git tag, for example v1.0.0 or v1.0.0_2026-07-04." ,
379+ )
351380 prepare_parser .add_argument ("--output-dir" , type = Path , default = Path ("dist" ))
352381 return parser
353382
0 commit comments