diff --git a/updatebot.py b/updatebot.py index eacac8c..a94277d 100644 --- a/updatebot.py +++ b/updatebot.py @@ -5,6 +5,7 @@ import logging import functools import os import re +import subprocess import tempfile import threading import urllib.parse @@ -130,6 +131,10 @@ class BaseProject(abc.ABC, pydantic.BaseModel): def apply_updates(self, basedir: Path) -> Iterable[tuple[ImageDef, str]]: raise NotImplementedError + @abc.abstractmethod + def resource_diff(self, basedir: Path) -> Optional[str]: + raise NotImplementedError + class KustomizeProject(BaseProject): kind: Literal['kustomize'] @@ -169,6 +174,25 @@ class KustomizeProject(BaseProject): yaml.dump(kustomization, f) yield (image, version) + def resource_diff(self, basedir: Path) -> Optional[str]: + path = basedir / self.path + cmd = ['kubectl', 'diff', '-k', path] + try: + p = subprocess.run( + cmd, + stdin=subprocess.DEVNULL, + capture_output=True, + check=False, + encoding='utf-8', + ) + except FileNotFoundError as e: + log.warning('Cannot generate resource diff: %s', e) + return None + if p.returncode != 0 and not p.stdout: + log.error('Failed to generate resource diff: %s', p.stderr) + return None + return p.stdout + Project = KustomizeProject @@ -204,7 +228,11 @@ class RepoConfig(pydantic.BaseModel): return data['clone_url'] def create_pr( - self, title: str, source_branch: str, target_branch: str + self, + title: str, + source_branch: str, + target_branch: str, + body: Optional[str] = None, ) -> None: session = _get_session() r = session.post( @@ -216,6 +244,7 @@ class RepoConfig(pydantic.BaseModel): 'title': title, 'base': target_branch, 'head': source_branch, + 'body': body, }, ) log.log(TRACE, '%r', r.content) @@ -244,10 +273,12 @@ class Arguments: projects: list[str] -def update_project(repo: git.Repo, project: Project) -> Optional[str]: +def update_project( + repo: git.Repo, project: Project +) -> tuple[Optional[str], Optional[str]]: basedir = Path(repo.working_dir) title = None - for (image, version) in project.apply_updates(basedir): + for image, version in project.apply_updates(basedir): log.info('Updating %s to %s', image.name, version) if repo.index.diff(None): log.debug('Committing changes to %s', project.path) @@ -261,7 +292,8 @@ def update_project(repo: git.Repo, project: Project) -> Optional[str]: title = c.summary else: log.info('No changes to commit') - return title + diff = project.resource_diff(basedir) + return title, diff def parse_args() -> Arguments: @@ -322,29 +354,39 @@ def main() -> None: log.debug('Checking out new branch: %s', args.branch_name) repo.heads[0].checkout(force=True, B=args.branch_name) title = None + description = None if project.name not in projects: continue - title = update_project(repo, project) + title, diff = update_project(repo, project) if not title: log.info('No changes made') continue - repo.head.reference.set_tracking_branch( - git.RemoteReference( - repo, f'refs/remotes/origin/{args.branch_name}' + if diff: + description = ( + '
\nResource diff\n\n' + f'```diff\n{diff}```\n' + '
' ) - ) if not args.dry_run: + repo.head.reference.set_tracking_branch( + git.RemoteReference( + repo, f'refs/remotes/origin/{args.branch_name}' + ) + ) repo.remote().push(force=True) config.repo.create_pr( - title, args.branch_name, config.repo.branch + title, + args.branch_name, + config.repo.branch, + description, ) else: - log.info( - 'Would create PR %s → %s: %s', - config.repo.branch, - args.branch_name, + print( + 'Would create PR', + f'{args.branch_name} → {config.repo.branch}:', title, ) + print(description or '') if __name__ == '__main__':