Skip to content

Commit 09a0f56

Browse files
committed
Merge branch 'master' into ivana/toxgen-test-prereleases
2 parents 556f3e1 + 67f0491 commit 09a0f56

File tree

4 files changed

+129
-9
lines changed

4 files changed

+129
-9
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ jobs:
4444
with:
4545
python-version: 3.12
4646

47-
- run: |
47+
- name: Detect unexpected changes to tox.ini or CI
48+
run: |
49+
pip install -e .
50+
pip install -r scripts/populate_tox/requirements.txt
51+
python scripts/populate_tox/populate_tox.py --fail-on-changes
4852
pip install -r scripts/split_tox_gh_actions/requirements.txt
4953
python scripts/split_tox_gh_actions/split_tox_gh_actions.py --fail-on-changes
5054

scripts/populate_tox/populate_tox.py

Lines changed: 120 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
"""
44

55
import functools
6+
import hashlib
67
import os
78
import sys
89
import time
910
from bisect import bisect_left
1011
from collections import defaultdict
12+
from datetime import datetime, timezone
1113
from importlib.metadata import metadata
1214
from packaging.specifiers import SpecifierSet
1315
from packaging.version import Version
1416
from pathlib import Path
17+
from textwrap import dedent
1518
from typing import Optional, Union
1619

1720
# Adding the scripts directory to PATH. This is necessary in order to be able
@@ -107,7 +110,7 @@ def fetch_release(package: str, version: Version) -> dict:
107110

108111

109112
def _prefilter_releases(
110-
integration: str, releases: dict[str, dict]
113+
integration: str, releases: dict[str, dict], older_than: Optional[datetime] = None
111114
) -> tuple[list[Version], Optional[Version]]:
112115
"""
113116
Filter `releases`, removing releases that are for sure unsupported.
@@ -148,6 +151,10 @@ def _prefilter_releases(
148151
if meta["yanked"]:
149152
continue
150153

154+
if older_than is not None:
155+
if datetime.fromisoformat(meta["upload_time_iso_8601"]) > older_than:
156+
continue
157+
151158
version = Version(release)
152159

153160
if min_supported and version < min_supported:
@@ -188,7 +195,7 @@ def _prefilter_releases(
188195

189196

190197
def get_supported_releases(
191-
integration: str, pypi_data: dict
198+
integration: str, pypi_data: dict, older_than: Optional[datetime] = None
192199
) -> tuple[list[Version], Optional[Version]]:
193200
"""
194201
Get a list of releases that are currently supported by the SDK.
@@ -199,14 +206,17 @@ def get_supported_releases(
199206
We return the list of supported releases and optionally also the newest
200207
prerelease, if it should be tested (meaning it's for a version higher than
201208
the current stable version).
209+
210+
If an `older_than` timestamp is provided, no release newer than that will be
211+
considered.
202212
"""
203213
package = pypi_data["info"]["name"]
204214

205215
# Get a consolidated list without taking into account Python support yet
206216
# (because that might require an additional API call for some
207217
# of the releases)
208218
releases, latest_prerelease = _prefilter_releases(
209-
integration, pypi_data["releases"]
219+
integration, pypi_data["releases"], older_than
210220
)
211221

212222
# Determine Python support
@@ -424,7 +434,9 @@ def _render_dependencies(integration: str, releases: list[Version]) -> list[str]
424434
return rendered
425435

426436

427-
def write_tox_file(packages: dict) -> None:
437+
def write_tox_file(
438+
packages: dict, update_timestamp: bool, last_updated: datetime
439+
) -> None:
428440
template = ENV.get_template("tox.jinja")
429441

430442
context = {"groups": {}}
@@ -443,6 +455,11 @@ def write_tox_file(packages: dict) -> None:
443455
}
444456
)
445457

458+
if update_timestamp:
459+
context["updated"] = datetime.now(tz=timezone.utc).isoformat()
460+
else:
461+
context["updated"] = last_updated.isoformat()
462+
446463
rendered = template.render(context)
447464

448465
with open(TOX_FILE, "w") as file:
@@ -496,7 +513,59 @@ def _add_python_versions_to_release(
496513
release.rendered_python_versions = _render_python_versions(release.python_versions)
497514

498515

499-
def main() -> None:
516+
def get_file_hash() -> str:
517+
"""Calculate a hash of the tox.ini file."""
518+
hasher = hashlib.md5()
519+
520+
with open(TOX_FILE, "rb") as f:
521+
buf = f.read()
522+
hasher.update(buf)
523+
524+
return hasher.hexdigest()
525+
526+
527+
def get_last_updated() -> Optional[datetime]:
528+
timestamp = None
529+
530+
with open(TOX_FILE, "r") as f:
531+
for line in f:
532+
if line.startswith("# Last generated:"):
533+
timestamp = datetime.fromisoformat(line.strip().split()[-1])
534+
break
535+
536+
if timestamp is None:
537+
print(
538+
"Failed to find out when tox.ini was last generated; the timestamp seems to be missing from the file."
539+
)
540+
541+
return timestamp
542+
543+
544+
def main(fail_on_changes: bool = False) -> None:
545+
"""
546+
Generate tox.ini from the tox.jinja template.
547+
548+
The script has two modes of operation:
549+
- fail on changes mode (if `fail_on_changes` is True)
550+
- normal mode (if `fail_on_changes` is False)
551+
552+
Fail on changes mode is run on every PR to make sure that `tox.ini`,
553+
`tox.jinja` and this script don't go out of sync because of manual changes
554+
in one place but not the other.
555+
556+
Normal mode is meant to be run as a cron job, regenerating tox.ini and
557+
proposing the changes via a PR.
558+
"""
559+
print(f"Running in {'fail_on_changes' if fail_on_changes else 'normal'} mode.")
560+
last_updated = get_last_updated()
561+
if fail_on_changes:
562+
# We need to make the script ignore any new releases after the `last_updated`
563+
# timestamp so that we don't fail CI on a PR just because a new package
564+
# version was released, leading to unrelated changes in tox.ini.
565+
print(
566+
f"Since we're in fail_on_changes mode, we're only considering releases before the last tox.ini update at {last_updated.isoformat()}."
567+
)
568+
500569
global MIN_PYTHON_VERSION, MAX_PYTHON_VERSION
501570
sdk_python_versions = _parse_python_versions_from_classifiers(
502571
metadata("sentry-sdk").get_all("Classifier")
@@ -523,7 +592,14 @@ def main() -> None:
523592
pypi_data = fetch_package(package)
524593

525594
# Get the list of all supported releases
526-
releases, latest_prerelease = get_supported_releases(integration, pypi_data)
595+
596+
# If in fail-on-changes mode, ignore releases newer than `last_updated`
597+
older_than = last_updated if fail_on_changes else None
598+
599+
releases, latest_prerelease = get_supported_releases(
600+
integration, pypi_data, older_than
601+
)
602+
527603
if not releases:
528604
print(" Found no supported releases.")
529605
continue
@@ -553,8 +629,44 @@ def main() -> None:
553629
}
554630
)
555631

556-
write_tox_file(packages)
632+
if fail_on_changes:
633+
old_file_hash = get_file_hash()
634+
635+
write_tox_file(
636+
packages, update_timestamp=not fail_on_changes, last_updated=last_updated
637+
)
638+
639+
if fail_on_changes:
640+
new_file_hash = get_file_hash()
641+
if old_file_hash != new_file_hash:
642+
raise RuntimeError(
643+
dedent(
644+
"""
645+
Detected that `tox.ini` is out of sync with
646+
`scripts/populate_tox/tox.jinja` and/or
647+
`scripts/populate_tox/populate_tox.py`. This might either mean
648+
that `tox.ini` was changed manually, or the `tox.jinja`
649+
template and/or the `populate_tox.py` script were changed without
650+
regenerating `tox.ini`.
651+
652+
Please don't make manual changes to `tox.ini`. Instead, make the
653+
changes to the `tox.jinja` template and/or the `populate_tox.py`
654+
script (as applicable) and regenerate the `tox.ini` file with:
655+
656+
python -m venv toxgen.env
657+
. toxgen.env/bin/activate
658+
pip install -r scripts/populate_tox/requirements.txt
659+
python scripts/populate_tox/populate_tox.py
660+
"""
661+
)
662+
)
663+
print("Done checking tox.ini. Looking good!")
664+
else:
665+
print(
666+
"Done generating tox.ini. Make sure to also update the CI YAML files to reflect the new test targets."
667+
)
557668

558669

559670
if __name__ == "__main__":
560-
main()
671+
fail_on_changes = len(sys.argv) == 2 and sys.argv[1] == "--fail-on-changes"
672+
main(fail_on_changes)

scripts/populate_tox/tox.jinja

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# or in the script (if you want to change the auto-generated part).
1010
# The file (and all resulting CI YAMLs) then need to be regenerated via
1111
# "scripts/generate-test-files.sh".
12+
#
13+
# Last generated: {{ updated }}
1214

1315
[tox]
1416
requires =

tox.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# or in the script (if you want to change the auto-generated part).
1010
# The file (and all resulting CI YAMLs) then need to be regenerated via
1111
# "scripts/generate-test-files.sh".
12+
#
13+
# Last generated: 2025-02-19T11:15:06.395241+00:00
1214

1315
[tox]
1416
requires =

0 commit comments

Comments
 (0)