Skip to content

Commit 0c8de03

Browse files
committed
debug: Apply grace period
Signed-off-by: Cagri Yonca <cagri@ibm.com>
1 parent 2b3dbed commit 0c8de03

2 files changed

Lines changed: 155 additions & 5 deletions

File tree

.circleci/config.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ commands:
4646
command: |
4747
. venv/bin/activate
4848
pip install -r <<parameters.requirements>>
49+
- run:
50+
name: Apply grace period to installed packages
51+
command: |
52+
. venv/bin/activate
53+
pip install --quiet requests packaging
54+
python scripts/pin_safe_versions.py <<parameters.requirements>>
4955
5056
run-tests-with-coverage-report:
5157
parameters:
@@ -335,8 +341,8 @@ workflows:
335341
matrix:
336342
parameters:
337343
py-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
338-
- py39gevent
339344
- py312cassandra
345+
- py39gevent
340346
- py312aws
341347
- py313kafka
342348
- autowrapt:
@@ -346,14 +352,11 @@ workflows:
346352
- final_job:
347353
requires:
348354
- python3x
349-
- py39gevent
350355
- py312cassandra
356+
- py39gevent
351357
- py312aws
352358
- py313kafka
353359
- autowrapt
354360
- update-currency-versions:
355361
requires:
356362
- final_job
357-
# filters:
358-
# branches:
359-
# only: main

scripts/pin_safe_versions.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
# (c) Copyright IBM Corp. 2026
3+
4+
"""
5+
Downgrades any installed packages that were released within the 5-day grace
6+
period to their latest safe version. Run after pip install so that CI tests
7+
only exercise versions that have cleared the supply-chain safety window.
8+
9+
Usage:
10+
python scripts/pin_safe_versions.py [requirements_file]
11+
12+
If a requirements file is given, only the packages listed there are checked.
13+
Otherwise every installed package is checked (slow).
14+
"""
15+
16+
import re
17+
import subprocess
18+
import sys
19+
from datetime import datetime, timedelta
20+
21+
import requests
22+
from packaging.version import Version
23+
24+
GRACE_PERIOD_DAYS = 5
25+
26+
27+
def _get_pypi_releases(package_name):
28+
try:
29+
r = requests.get(f"https://pypi.org/pypi/{package_name}/json", timeout=10)
30+
r.raise_for_status()
31+
data = r.json()
32+
except Exception:
33+
return []
34+
35+
result = []
36+
for ver, files in data["releases"].items():
37+
if not files or re.search(r"(a|b|rc|dev)\d*$", ver, re.I):
38+
continue
39+
try:
40+
Version(ver)
41+
except Exception:
42+
continue
43+
upload_time = files[-1].get("upload_time_iso_8601", "")
44+
match = re.search(r"([\d-]+)T", upload_time)
45+
if not match:
46+
continue
47+
date = datetime.strptime(match[1], "%Y-%m-%d").date()
48+
result.append((ver, date))
49+
result.sort(key=lambda x: x[1], reverse=True)
50+
return result
51+
52+
53+
def _get_safe_version(releases):
54+
today = datetime.today().date()
55+
grace_cutoff = today - timedelta(days=GRACE_PERIOD_DAYS)
56+
for i, (ver, date) in enumerate(releases):
57+
grace_end = date + timedelta(days=GRACE_PERIOD_DAYS)
58+
superseded = any(nd < grace_end for _, nd in releases[:i])
59+
if not superseded and date <= grace_cutoff:
60+
return ver, date
61+
return None, None
62+
63+
64+
def _installed_packages():
65+
result = subprocess.run(
66+
["pip", "freeze"], capture_output=True, text=True, check=True
67+
)
68+
packages = {}
69+
for line in result.stdout.strip().splitlines():
70+
if "==" in line:
71+
pkg, ver = line.split("==", 1)
72+
packages[pkg.lower()] = ver.strip()
73+
return packages
74+
75+
76+
def _parse_req_file(path):
77+
names = set()
78+
try:
79+
with open(path) as f:
80+
for line in f:
81+
line = line.strip()
82+
if not line or line.startswith("#"):
83+
continue
84+
if line.startswith("-r "):
85+
# Recurse into included requirement files (same directory)
86+
import os
87+
88+
included = os.path.join(os.path.dirname(path), line[3:].strip())
89+
names |= _parse_req_file(included)
90+
continue
91+
if line.startswith("-"):
92+
continue
93+
name = re.split(r"[><=!;[\s]", line)[0].strip().lower()
94+
if name:
95+
names.add(name)
96+
except FileNotFoundError:
97+
print(f"Warning: requirements file '{path}' not found.")
98+
return names
99+
100+
101+
def main():
102+
packages_to_check = None
103+
if len(sys.argv) > 1:
104+
packages_to_check = _parse_req_file(sys.argv[1])
105+
print(f"Checking {len(packages_to_check)} packages from {sys.argv[1]}")
106+
107+
installed = _installed_packages()
108+
today = datetime.today().date()
109+
grace_cutoff = today - timedelta(days=GRACE_PERIOD_DAYS)
110+
111+
to_pin = []
112+
for pkg, installed_ver in installed.items():
113+
if packages_to_check is not None and pkg not in packages_to_check:
114+
continue
115+
116+
releases = _get_pypi_releases(pkg)
117+
if not releases:
118+
continue
119+
120+
installed_date = next((d for v, d in releases if v == installed_ver), None)
121+
if installed_date is None or installed_date <= grace_cutoff:
122+
continue
123+
124+
safe_ver, safe_date = _get_safe_version(releases)
125+
if safe_ver is None:
126+
print(
127+
f"[grace-period] {pkg}=={installed_ver} (released {installed_date}) "
128+
f"is within grace period but no safe version exists — skipping"
129+
)
130+
continue
131+
132+
print(
133+
f"[grace-period] {pkg}: {installed_ver} (released {installed_date}) "
134+
f"→ pinning to {safe_ver} (released {safe_date})"
135+
)
136+
to_pin.append(f"{pkg}=={safe_ver}")
137+
138+
if to_pin:
139+
print(f"\nPinning {len(to_pin)} package(s) to grace-period-safe versions...")
140+
subprocess.run(["pip", "install"] + to_pin, check=True)
141+
print("Grace period enforcement complete.")
142+
else:
143+
print("All checked packages comply with the grace period.")
144+
145+
146+
if __name__ == "__main__":
147+
main()

0 commit comments

Comments
 (0)