Compare commits
2 Commits
5f8db2fa47
...
a2f33f1be2
Author | SHA1 | Date |
---|---|---|
|
a2f33f1be2 | |
|
8126e5de21 |
83
updatebot.py
83
updatebot.py
|
@ -5,6 +5,7 @@ import logging
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
import tomllib
|
import tomllib
|
||||||
|
@ -122,6 +123,10 @@ class BaseProject(abc.ABC, pydantic.BaseModel):
|
||||||
def apply_update(self, path: Path, version: str) -> None:
|
def apply_update(self, path: Path, version: str) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def diff(self, path: Path) -> str:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class KustomizeProject(BaseProject):
|
class KustomizeProject(BaseProject):
|
||||||
kind: Literal['kustomize']
|
kind: Literal['kustomize']
|
||||||
|
@ -153,6 +158,26 @@ class KustomizeProject(BaseProject):
|
||||||
with filepath.open('wb') as f:
|
with filepath.open('wb') as f:
|
||||||
yaml.dump(kustomization, f)
|
yaml.dump(kustomization, f)
|
||||||
|
|
||||||
|
def diff(self, path: Path) -> str:
|
||||||
|
try:
|
||||||
|
p = subprocess.run(
|
||||||
|
['kubectl', 'diff', '-k', '.'],
|
||||||
|
cwd=path,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
encoding='utf-8',
|
||||||
|
)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
log.warning('Cannot compute manifest diff: %s', e)
|
||||||
|
return ''
|
||||||
|
if not p.stdout and p.returncode != 0:
|
||||||
|
log.error('Error computing manifest diff: %s', p.stderr)
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
assert p.stdout is not None
|
||||||
|
return p.stdout
|
||||||
|
|
||||||
|
|
||||||
class DirectoryProject(BaseProject):
|
class DirectoryProject(BaseProject):
|
||||||
kind: Literal['dir'] | Literal['directory']
|
kind: Literal['dir'] | Literal['directory']
|
||||||
|
@ -166,7 +191,7 @@ Project = Union[
|
||||||
|
|
||||||
class RepoConfig(pydantic.BaseModel):
|
class RepoConfig(pydantic.BaseModel):
|
||||||
url: str
|
url: str
|
||||||
token_file: Path
|
token_file: Optional[Path] = None
|
||||||
branch: str = 'master'
|
branch: str = 'master'
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
|
@ -176,16 +201,18 @@ class RepoConfig(pydantic.BaseModel):
|
||||||
return urllib.parse.urlunsplit(urlparts)
|
return urllib.parse.urlunsplit(urlparts)
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def auth_token(self) -> str:
|
def auth_token(self) -> Optional[str]:
|
||||||
return self.token_file.read_text().strip()
|
if self.token_file:
|
||||||
|
return self.token_file.read_text().strip()
|
||||||
|
|
||||||
def get_git_url(self) -> str:
|
def get_git_url(self) -> str:
|
||||||
session = _get_session()
|
session = _get_session()
|
||||||
|
headers = {}
|
||||||
|
if token := self.auth_token:
|
||||||
|
headers['Authorization'] = f'Bearer {token}'
|
||||||
r = session.get(
|
r = session.get(
|
||||||
self.repo_api_url,
|
self.repo_api_url,
|
||||||
headers={
|
headers=headers,
|
||||||
'Authorization': f'token {self.auth_token}',
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
data = r.json()
|
data = r.json()
|
||||||
if ssh_url := data.get('ssh_url'):
|
if ssh_url := data.get('ssh_url'):
|
||||||
|
@ -193,7 +220,11 @@ class RepoConfig(pydantic.BaseModel):
|
||||||
return data['clone_url']
|
return data['clone_url']
|
||||||
|
|
||||||
def create_pr(
|
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:
|
) -> None:
|
||||||
session = _get_session()
|
session = _get_session()
|
||||||
r = session.post(
|
r = session.post(
|
||||||
|
@ -205,6 +236,7 @@ class RepoConfig(pydantic.BaseModel):
|
||||||
'title': title,
|
'title': title,
|
||||||
'base': target_branch,
|
'base': target_branch,
|
||||||
'head': source_branch,
|
'head': source_branch,
|
||||||
|
'body': body,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
log.log(TRACE, '%r', r.content)
|
log.log(TRACE, '%r', r.content)
|
||||||
|
@ -235,7 +267,7 @@ class Arguments:
|
||||||
|
|
||||||
def update_project(
|
def update_project(
|
||||||
repo: git.Repo, name: str, project: Project
|
repo: git.Repo, name: str, project: Project
|
||||||
) -> Optional[git.Commit]:
|
) -> tuple[Optional[git.Commit], Optional[str]]:
|
||||||
basedir = Path(repo.working_dir)
|
basedir = Path(repo.working_dir)
|
||||||
log.debug('Checking for latest version of %s', name)
|
log.debug('Checking for latest version of %s', name)
|
||||||
latest = project.source.get_latest_version()
|
latest = project.source.get_latest_version()
|
||||||
|
@ -248,10 +280,12 @@ def update_project(
|
||||||
repo.index.add(str(path))
|
repo.index.add(str(path))
|
||||||
c = repo.index.commit(f'{name}: Update to {latest}')
|
c = repo.index.commit(f'{name}: Update to {latest}')
|
||||||
log.info('Commited %s %s', str(c)[:7], c.summary)
|
log.info('Commited %s %s', str(c)[:7], c.summary)
|
||||||
return c
|
log.debug('Computing manifest diff')
|
||||||
|
diff = project.diff(path)
|
||||||
|
return (c, diff)
|
||||||
else:
|
else:
|
||||||
log.info('No changes to commit')
|
log.info('No changes to commit')
|
||||||
return None
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> Arguments:
|
def parse_args() -> Arguments:
|
||||||
|
@ -322,8 +356,11 @@ def main() -> None:
|
||||||
log.debug('Checking out new branch: %s', args.branch_name)
|
log.debug('Checking out new branch: %s', args.branch_name)
|
||||||
repo.heads[0].checkout(force=True, B=args.branch_name)
|
repo.heads[0].checkout(force=True, B=args.branch_name)
|
||||||
title = None
|
title = None
|
||||||
|
pr_desc = ''
|
||||||
for project in projects:
|
for project in projects:
|
||||||
commit = update_project(repo, project, config.projects[project])
|
commit, diff = update_project(
|
||||||
|
repo, project, config.projects[project]
|
||||||
|
)
|
||||||
if commit and not title:
|
if commit and not title:
|
||||||
if not isinstance(commit.summary, str):
|
if not isinstance(commit.summary, str):
|
||||||
title = bytes(commit.summary).decode(
|
title = bytes(commit.summary).decode(
|
||||||
|
@ -331,17 +368,29 @@ def main() -> None:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
title = commit.summary
|
title = commit.summary
|
||||||
|
if diff:
|
||||||
|
pr_desc += diff
|
||||||
if not title:
|
if not title:
|
||||||
log.info('No changes made')
|
log.info('No changes made')
|
||||||
return
|
return
|
||||||
repo.head.reference.set_tracking_branch(
|
|
||||||
git.RemoteReference(
|
|
||||||
repo, f'refs/remotes/origin/{args.branch_name}'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not args.dry_run:
|
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)
|
repo.remote().push(force=True)
|
||||||
config.repo.create_pr(title, args.branch_name, config.repo.branch)
|
if pr_desc:
|
||||||
|
pr_desc = (
|
||||||
|
'<details>\n<summary>Manifest diff</summary>\n\n'
|
||||||
|
f'```diff\n{pr_desc}```\n'
|
||||||
|
'</details>'
|
||||||
|
)
|
||||||
|
config.repo.create_pr(
|
||||||
|
title, args.branch_name, config.repo.branch, pr_desc
|
||||||
|
)
|
||||||
|
elif pr_desc:
|
||||||
|
print(pr_desc)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in New Issue