diff --git a/src/_remove_invalid_releases.py b/src/_remove_invalid_releases.py index 4d8a679f..5b71b129 100644 --- a/src/_remove_invalid_releases.py +++ b/src/_remove_invalid_releases.py @@ -4,7 +4,7 @@ from common import dates, releasedata """Remove empty releases or releases which are released in the future.""" -TODAY = dates.today() +TODAY = dates.today_at_midnight() frontmatter, _ = releasedata.parse_argv(ignore_auto_config=True) with releasedata.ProductData(frontmatter.name) as product_data: diff --git a/src/common/dates.py b/src/common/dates.py index dcc08a9a..29f2b907 100644 --- a/src/common/dates.py +++ b/src/common/dates.py @@ -1,5 +1,5 @@ import calendar -from datetime import datetime, timezone +import datetime def parse_date(text: str, formats: list[str] = frozenset([ @@ -15,7 +15,7 @@ def parse_date(text: str, formats: list[str] = frozenset([ "%Y/%m/%d", # 2020/01/25 "%A %d %B %Y", # Wednesday 1 January 2020 "%A %d %b %Y", # Wednesday 1 Jan 2020 -])) -> datetime: +])) -> datetime.datetime: """Parse a given text representing a date using a list of formats. """ return parse_datetime(text, formats, to_utc=False) @@ -28,7 +28,7 @@ def parse_month_year_date(text: str, formats: list[str] = frozenset([ "%Y/%m", # 2020/01 "%m-%Y", # 01-2020 "%m/%Y", # 01/2020 -])) -> datetime: +])) -> datetime.datetime: """Parse a given text representing a partial date using a list of formats, adjusting it to the last day of the month. """ @@ -37,7 +37,7 @@ def parse_month_year_date(text: str, formats: list[str] = frozenset([ return date.replace(day=last_day) -def parse_date_or_month_year_date(text: str) -> datetime: +def parse_date_or_month_year_date(text: str) -> datetime.datetime: """Parse a given text representing a date or a partial date using the default list of formats. """ try: @@ -60,7 +60,7 @@ def parse_datetime(text: str, formats: list[str] = frozenset([ "%a %b %d %H:%M:%S %z %Y", # Wed Jan 01 00:00:00 -0400 2020 "%b %d %Y %I:%M %p", # Jan 1 2020 0:00 pm "%Y%m%d%H%M%S", # 20230501083234 -]), to_utc: bool = True) -> datetime: +]), to_utc: bool = True) -> datetime.datetime: """Parse a given text representing a datetime using a list of formats, optionally converting it to UTC. """ @@ -78,12 +78,12 @@ def parse_datetime(text: str, formats: list[str] = frozenset([ ) for fmt in formats: try: - dt = datetime.strptime(text, fmt) # NOQA: DTZ007, timezone is handled below + dt = datetime.datetime.strptime(text, fmt) # NOQA: DTZ007, timezone is handled below if to_utc: - dt = dt.astimezone(timezone.utc) + dt = dt.astimezone(datetime.timezone.utc) elif dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) + dt = dt.replace(tzinfo=datetime.timezone.utc) return dt except ValueError: @@ -93,11 +93,15 @@ def parse_datetime(text: str, formats: list[str] = frozenset([ raise ValueError(msg) -def date(year: int, month: int, day: int) -> datetime: +def date(year: int, month: int, day: int) -> datetime.datetime: """Create a datetime object with the given year, month and day, at midnight.""" - return datetime(year, month, day, tzinfo=timezone.utc) + return datetime.datetime(year, month, day, tzinfo=datetime.timezone.utc) -def today() -> datetime: +def today_at_midnight() -> datetime.datetime: """Create a datetime object with today's date, at midnight.""" - return datetime.now(tz=timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + return datetime.datetime.now(tz=datetime.timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + +def today() -> datetime.date: + """Create a date object with today's date.""" + return datetime.datetime.now(tz=datetime.timezone.utc).date() diff --git a/src/graalvm.py b/src/graalvm.py index 23ec572b..eee30384 100644 --- a/src/graalvm.py +++ b/src/graalvm.py @@ -23,7 +23,7 @@ with ProductData(config.product) as product_data: date_text = cells[date_index].get_text().strip() date = dates.parse_date(date_text) - if date > dates.today(): + if date > dates.today_at_midnight(): logging.info(f"Skipping future version {cells}") continue diff --git a/src/samsung-security.py b/src/samsung-security.py index 4348c0cc..4d46feac 100644 --- a/src/samsung-security.py +++ b/src/samsung-security.py @@ -11,7 +11,7 @@ This script works cumulatively: when a model is not listed anymore on https://se it retains the date and use it as the model's EOL date. """ -TODAY = dates.today() +TODAY = dates.today_at_midnight() frontmatter, config = parse_argv() with ProductData(config.product) as product_data: diff --git a/update-product-data.py b/update-product-data.py index 33168507..48a868b4 100644 --- a/update-product-data.py +++ b/update-product-data.py @@ -11,6 +11,7 @@ from ruamel.yaml import YAML, StringIO from ruamel.yaml.representer import RoundTripRepresenter from ruamel.yaml.resolver import Resolver +from src.common.dates import today_at_midnight from src.common.endoflife import list_products from src.common.gha import GitHubOutput from src.common.releasedata import DATA_DIR @@ -74,9 +75,18 @@ class ReleaseCycle: self.data["latestReleaseDate"] = date self.updated = True + def release_date(self) -> datetime.date | None: + return self.__as_date(self.data.get("releaseDate", None)) + + def eol(self) -> datetime.date | bool | None: + return self.__as_date(self.data.get("eol", None)) + def latest(self) -> str | None: return self.data.get("latest", None) + def latest_release_date(self) -> datetime.date | None: + return self.__as_date(self.data.get("latestReleaseDate", None)) + def includes(self, version: str) -> bool: """matches releases that are exact (such as 4.1 being the first release for the 4.1 release cycle) or releases that include a dot just after the release cycle (4.1.*) @@ -93,6 +103,19 @@ class ReleaseCycle: def __str__(self) -> str: return self.product + '#' + self.name + @staticmethod + def __as_date(o: str | bool | datetime.datetime | datetime.date | None) -> datetime.date | bool | None: + if isinstance(o, datetime.date): + return o + if isinstance(o, datetime.datetime): + return o.date() + if isinstance(o, str): + return datetime.date.fromisoformat(o) + if isinstance(o, bool): + return o + + return None + class Product: def __init__(self, name: str, product_dir: Path, versions_dir: Path) -> None: @@ -196,6 +219,7 @@ def update_product(name: str, product_dir: Path, releases_dir: Path, output: Git today = datetime.datetime.now(tz=datetime.timezone.utc).date() __raise_alert_for_unmatched_versions(name, output, product, today, 30) __raise_alert_for_unmatched_releases(name, output, product) + __raise_alert_for_stale_releases(name, output, product) def __raise_alert_for_unmatched_versions(name: str, output: GitHubOutput, product: Product, today: datetime.date, @@ -235,6 +259,36 @@ def __raise_alert_for_unmatched_releases(name: str, output: GitHubOutput, produc __print_unmatched_releases_as_yaml(product) +def __raise_alert_for_stale_releases(name: str, output: GitHubOutput, product: Product) -> None: + threshold = product.data.get("staleReleaseThresholdYears", 1) * 365 + + for release in product.releases: + logging.debug(f"checking staleness of {name}:{release.name}") + eol = release.eol() + + if isinstance(eol, datetime.date): + continue # explicitly set, skip + + if eol: + continue # explicitly EOL, skip + + latest_release_date = release.latest_release_date() + if latest_release_date: + days_since_latest = (today_at_midnight().date() - latest_release_date).days + if days_since_latest > threshold: + message = f"{name}:{release.name} is not EOL and has not had a release in {days_since_latest} days" + logging.warning(message) + output.println(message) + continue + + release_date = release.release_date() + days_since_release = (today_at_midnight().date() - release_date).days + if days_since_release > threshold: + message = f"{name}:{release.name} was released {days_since_release} days ago and has no EOL date" + logging.warning(message) + output.println(message) + + if __name__ == "__main__": parser = argparse.ArgumentParser(description='Update product releases.') parser.add_argument('product', nargs='?', help='restrict update to the given product')