-
Notifications
You must be signed in to change notification settings - Fork 69
Stop gating releases on a dated changelog entry #480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,26 @@ | ||
| #!/usr/bin/env python3 | ||
| """Release gate: the pushed tag and the package's CHANGELOG must agree. | ||
| """Release gate: the pushed tag must be a shape the version derivation accepts. | ||
|
|
||
| The tag *is* the version now — each pyproject derives it via | ||
| uv-dynamic-versioning — so the old tag-vs-pyproject leg of this gate is gone | ||
| along with the drift it existed to catch. What a tag cannot vouch for is that | ||
| anyone wrote down what changed, so this runs first in the publish job on tag | ||
| pushes: the package's changelog must carry a dated entry for the tagged | ||
| version (an "unreleased" heading fails — date it as part of cutting the | ||
| release). | ||
| along with the drift it existed to catch. What remains is the one thing a tag | ||
| can still get wrong on its own: a shape the derivation cannot turn into the | ||
| version the artifacts are published under. This runs first in the publish job | ||
| on tag pushes. | ||
|
|
||
| The ``xy`` distribution uses bare `vX.Y.Z` tags and `CHANGELOG.md`. Its bundled | ||
| Reflex integration and `reflex` extra ship on the same version line. | ||
| Writing up the release in `CHANGELOG.md` is a checklist item | ||
| (`spec/process/production-readiness.md`), deliberately not a gate: a missing or | ||
| undated entry is fixed with a commit, and blocking the publish on it only ever | ||
| forced a tag to be deleted and re-cut for a documentation edit. | ||
|
|
||
| The ``xy`` distribution uses bare `vX.Y.Z` tags. Its bundled Reflex integration | ||
| and `reflex` extra ship on the same version line. | ||
|
|
||
| Tags accept an optional PEP 440 pre-release suffix in its canonical spelling | ||
| (`a1`/`b2`/`rc3`). PyPI accepts pre-releases but does not serve them to default | ||
| `pip install`. Only the canonical spelling passes: `-alpha1`-style tags would | ||
| be normalized by the derivation (`0.0.1a1`) and could never match their own | ||
| artifacts. A pre-release still needs its own dated changelog entry. | ||
| artifacts. | ||
|
|
||
| Docs-deploy CalVer tags (2026.WW.N) do not match the release shape. | ||
| Dev/post/local shapes stay rejected: dev versions are the between-tags marker | ||
|
|
@@ -29,19 +33,15 @@ | |
| import os | ||
| import re | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import NamedTuple, Optional | ||
|
|
||
| ROOT = Path(__file__).resolve().parents[1] | ||
|
|
||
|
|
||
| class _Package(NamedTuple): | ||
| # The release tag shape uv-dynamic-versioning derives this package's | ||
| # distribution version from: prefix + `v` + a PEP 440 release number, | ||
| # optionally a canonical pre-release suffix (aN/bN/rcN), nothing else. | ||
| tag_re: re.Pattern[str] | ||
| tag_shape: str | ||
| changelog: Path | ||
|
|
||
|
|
||
| # Canonical PEP 440 spellings only: the derivation would normalize `alpha1` | ||
|
|
@@ -53,45 +53,20 @@ class _Package(NamedTuple): | |
| "xy": _Package( | ||
| tag_re=re.compile(rf"^v(?P<version>{_RELEASE})$"), | ||
| tag_shape=_SHAPE, | ||
| changelog=ROOT / "CHANGELOG.md", | ||
| ), | ||
| } | ||
| DEFAULT_CHANGELOG = PACKAGES["xy"].changelog | ||
| # Backward-compatible alias for the pre-adapter, xy-only gate. | ||
| TAG_RE = PACKAGES["xy"].tag_re | ||
|
|
||
|
|
||
| def check_release(tag: str, changelog: Path, package: str = "xy") -> list[str]: | ||
| errors: list[str] = [] | ||
| def check_release(tag: str, package: str = "xy") -> list[str]: | ||
| spec = PACKAGES[package] | ||
| match = spec.tag_re.match(tag) | ||
| if match is None: | ||
| if spec.tag_re.match(tag) is None: | ||
| return [ | ||
| f"tag {tag!r} is not a release tag for {package} — expected " | ||
| f"{spec.tag_shape!r}, the shape the distribution version is derived from" | ||
| ] | ||
|
Comment on lines
+62
to
68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
python -m pip install --quiet --disable-pip-version-check packaging
python - <<'PY'
from packaging.version import Version
for raw, expected in {
"01.02.003": "1.2.3",
"1.2.3rc01": "1.2.3rc1",
}.items():
actual = str(Version(raw))
assert actual == expected, (raw, actual, expected)
print(f"{raw} -> {actual}")
PY
rg -n -C 3 'non_canonical|v01\.2\.3|rc01|check_release' \
tests/test_check_release_version.py scripts/check_release_version.pyRepository: reflex-dev/xy Length of output: 188 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "files:"
git ls-files | rg '(^|/)(scripts/check_release_version.py|tests/test_check_release_version.py|pyproject.toml|uv.lock|requirements|Pipfile|poetry.lock)$'
echo
echo "check_release_version outline:"
ast-grep outline scripts/check_release_version.py --view expanded
echo
echo "selected lines:"
cat -n scripts/check_release_version.py | sed -n '1,140p'
echo
echo "test file selected lines if present:"
if [ -f tests/test_check_release_version.py ]; then
cat -n tests/test_check_release_version.py | sed -n '1,220p'
fi
echo
echo "version-related search:"
rg -n "check_release|PACKAGES|RELEASE|tag_re|distribution_version|release_tag|package_version|Version|tag" scripts tests pyproject.toml 2>/dev/null || trueRepository: reflex-dev/xy Length of output: 50369 🌐 Web query:
💡 Result: Under PEP 440, all numeric version components must be represented as sequences of ASCII digits (specifically 0-9) [1][2][3]. The specification requires that these components be interpreted as non-negative integers [1][4]. Regarding normalization and leading zeros: Integer Normalization: All numeric segments are normalized by interpreting them as integers via the Python int built-in [1][5]. Consequently, any leading zeros are stripped during the normalization process [1][6]. For example, a version segment of 00 or 09000 will normalize to 0 and 9000, respectively [1][2]. Local Versions Exception: This normalization behavior does not apply to integers contained within alphanumeric segments of a local version (e.g., 1.0+foo0100), which remain in their already normalized form [1][4]. Because the canonical format requires these components to be represented as integers, the presence of leading zeros is technically redundant and they are removed to ensure a consistent, standardized representation [1][7]. The requirement for ASCII digits specifically excludes arbitrary Unicode [Nd] code points, a clarification added to the specification to ensure consistent behavior across parsing tools [8][3]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from scripts.check_release_version import check_release
for tag in [
"v01.02.003",
"v1.2.30",
"v1.2.3rc01",
"v1.2.3α1",
"v1.2.3a1",
"v0.2.0",
]:
print(f"{tag!r}: {check_release(tag)!r}")
number = r"\d+"
release = rf"(?:{number}\.){{2}}{number}(?:(?:a|b|rc){number})?"
print("released tags accepted by current regex:", re.compile(re.escape("v") + release + r"$").match("v01.02.003") is not None)
print("release prerelease digit regex accepts non-ASCII Nd alpha:", re.search(r"[^\u0030-\u0039]", "α") is not None)
PYRepository: reflex-dev/xy Length of output: 514 Reject normalized numeric release aliases.
🤖 Prompt for AI Agents |
||
| version = match.group("version") | ||
| try: | ||
| text = changelog.read_text(encoding="utf-8") | ||
| except OSError as exc: | ||
| errors.append(f"cannot read {changelog}: {exc}") | ||
| return errors | ||
| # A heading with a real date. The gate checks substance (this version has | ||
| # dated notes), not heading style: both spellings used in this repo pass — | ||
| # Keep-a-Changelog brackets (`## [0.0.2] — 2026-07-24`) and the plain | ||
| # v-prefixed form v0.0.2 was actually documented with | ||
| # (`## v0.0.2 - 2026-07-24`); em dash or hyphen either way. | ||
| dated = re.compile( | ||
| rf"^## (?:\[{re.escape(version)}\]|v?{re.escape(version)}) [—-] " | ||
| rf"\d{{4}}-\d{{2}}-\d{{2}}\s*$", | ||
| re.M, | ||
| ) | ||
| if not dated.search(text): | ||
| errors.append( | ||
| f"{changelog.name} has no dated '## [{version}]' entry — date the " | ||
| "release section before tagging" | ||
| ) | ||
| return errors | ||
| return [] | ||
|
|
||
|
|
||
| def main(argv: Optional[list[str]] = None) -> int: | ||
|
|
@@ -107,25 +82,18 @@ def main(argv: Optional[list[str]] = None) -> int: | |
| default="xy", | ||
| help="which release line the tag belongs to (default: xy)", | ||
| ) | ||
| parser.add_argument( | ||
| "--changelog", | ||
| type=Path, | ||
| default=None, | ||
| help="changelog to gate against (defaults to the package's own)", | ||
| ) | ||
| args = parser.parse_args(argv) | ||
|
|
||
| if not args.tag: | ||
| print("release version gate: no tag provided (--tag or GITHUB_REF_NAME)", file=sys.stderr) | ||
| return 1 | ||
| changelog = args.changelog if args.changelog is not None else PACKAGES[args.package].changelog | ||
| errors = check_release(args.tag, changelog, args.package) | ||
| errors = check_release(args.tag, args.package) | ||
| if errors: | ||
| print("release version gate failed:", file=sys.stderr) | ||
| for error in errors: | ||
| print(f"- {error}", file=sys.stderr) | ||
| return 1 | ||
| print(f"release version gate OK: {args.tag} has a dated {changelog.name} entry") | ||
| print(f"release version gate OK: {args.tag} is a release tag for {args.package}") | ||
| return 0 | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -346,9 +346,16 @@ artifacts, not another package or release. | |
|
|
||
| Before tagging a release: | ||
|
|
||
| - Tag as `vX.Y.Z`, optionally with a canonical PEP 440 pre-release suffix | ||
| (`v0.2.0a1`/`b1`/`rc1`). That shape is the whole release gate | ||
| (`scripts/check_release_version.py`), because it is the shape | ||
| uv-dynamic-versioning derives the published version from; `.postN`, `.devN`, | ||
| local (`+…`), and non-canonical (`-alpha1`) spellings are refused before | ||
| anything builds. | ||
| - Add a dated `## [X.Y.Z] — YYYY-MM-DD` heading to `CHANGELOG.md` for the | ||
| version being tagged. This is the one thing the tag cannot vouch for, and the | ||
| release gate blocks the publish without it. | ||
| version being tagged. Deliberately not gated: a missing or undated entry is | ||
| fixed with a follow-up commit, and blocking the publish on it only ever forced | ||
| a tag to be deleted and re-cut over a documentation edit. | ||
|
Comment on lines
355
to
+358
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Show the pre-release heading format. A tag such as 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| - Refresh benchmark reports or explicitly document why the previous report still | ||
| applies. | ||
| - Run `make check-full` locally or confirm the equivalent | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the pre-release sentence.
Line 49 says “a pre-release tags”. Replace it with “pre-release tags”.
🤖 Prompt for AI Agents