From 8cdfb58c88cf39b512bcfc9a6d447e82c35b757e Mon Sep 17 00:00:00 2001 From: Silke Hofstra Date: Wed, 6 Dec 2023 14:13:17 +0100 Subject: [PATCH] ci/package_checks: add check for frozen packages Add a simple configuration file for the CI checks, that includes dates for a package freeze. Packages updates for packages in `common/iso_packages.txt` result in either a notice outside of freeze periods, or a warning in freeze periods. --- common/CI/config.yaml | 4 ++ common/CI/package_checks.py | 77 +++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 common/CI/config.yaml diff --git a/common/CI/config.yaml b/common/CI/config.yaml new file mode 100644 index 000000000000..0ecd3c2cb0db --- /dev/null +++ b/common/CI/config.yaml @@ -0,0 +1,4 @@ +--- +freeze: + start: null + end: null diff --git a/common/CI/package_checks.py b/common/CI/package_checks.py index cf2ef542a352..159a6a36dcd1 100755 --- a/common/CI/package_checks.py +++ b/common/CI/package_checks.py @@ -6,6 +6,7 @@ import re import subprocess from dataclasses import dataclass +from datetime import datetime, timezone from enum import Enum from typing import Any, Dict, List, Optional, TextIO, Tuple, Union from urllib import request @@ -14,6 +15,32 @@ import yaml +@dataclass +class FreezeConfig: + start: Optional[datetime] + end: Optional[datetime] + + def active(self) -> bool: + now = datetime.now(tz=timezone.utc) + + if self.start is None: + return False + + return now > self.start and (self.end is None or now < self.end) + + +@dataclass +class Config: + freeze: FreezeConfig + + @staticmethod + def load(stream: Any) -> 'Config': + return Config(**yaml.safe_load(stream)) + + def __post_init__(self) -> None: + self.freeze = FreezeConfig(**self.freeze) # type: ignore + + class Git: def __init__(self, path: str): self.path = path @@ -66,6 +93,8 @@ def untracked_files(self) -> List[str]: class Level(str, Enum): __str__ = Enum.__str__ + DEBUG = 'debug' + NOTICE = 'notice' ERROR = 'error' WARNING = 'warning' @@ -109,6 +138,7 @@ def _property(self, key: str) -> str: class PullRequestCheck: _package_files = ['package.yml'] _two_letter_dirs = ['py'] + _config: Optional[Config] = None def __init__(self, git: Git, files: List[str], commits: List[str], base: Optional[str]): self.git = git @@ -119,6 +149,14 @@ def __init__(self, git: Git, files: List[str], commits: List[str], base: Optiona def run(self) -> List[Result]: raise NotImplementedError + @property + def config(self) -> Config: + if self._config is None: + with self._open(os.path.join('common', 'CI', 'config.yaml')) as f: + self._config = Config.load(f) + + return self._config + @property def package_files(self) -> List[str]: return self._filter_packages(self.files) @@ -160,6 +198,14 @@ def package_file(self, package: str, file: str) -> str: def package_dir(self, package: str) -> str: return os.path.join('packages', self._package_subdir(package), package) + @staticmethod + def package_for(path: str) -> str: + parts = path.split("/") + if len(parts) != 3 or parts[0] != "packages": + return "" + + return parts[2] + def _package_subdir(self, package: str) -> str: package = package.lower() @@ -189,6 +235,36 @@ def _check_commit(self, commit: str) -> List[Result]: return results +class FrozenPackage(PullRequestCheck): + __packages: Optional[List[str]] = None + __message_normal = ('This package is included in the ISO. ' + 'Consider validating the functionality in a newly built ISO.') + __message_freeze = ('This package is included in the ISO and is currently frozen. ' + 'It can only be updated to fix critical bugs, ' + 'in consultation with multiple Solus staff members.') + + def run(self) -> List[Result]: + return [self._make_result(f) + for f in self.package_files + if not self._is_frozen(f)] + + def _make_result(self, file: str) -> Result: + if self.config.freeze.active(): + return Result(message=self.__message_freeze, file=file, level=Level.WARNING) + + return Result(message=self.__message_normal, file=file, level=Level.NOTICE) + + def _is_frozen(self, file: str) -> bool: + return self.package_for(file) in self._packages() + + def _packages(self) -> List[str]: + if self.__packages is None: + with self._open(os.path.join('common', 'iso_packages.txt')) as file: + self.__packages = [line.strip() for line in file] + + return self.__packages + + class Homepage(PullRequestCheck): _error = '`homepage` is not set' _level = Level.ERROR @@ -481,6 +557,7 @@ def _commit_package_yaml(self, ref: str) -> Optional[Dict[str, Any]]: class Checker: checks = [ CommitMessage, + FrozenPackage, Homepage, PackageBumped, PackageDependenciesOrder,