1
0
Fork 0
scripts/obs/update_unstable.py
Luca Beltrame c47ce0dc06
Refactor the update script
This is mainly done to allow better notifications and simplify a few
things.
2022-01-15 10:52:30 +01:00

238 lines
6.9 KiB
Python
Executable file

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2021 Luca Beltrame <lbeltrame@kde.org>
# SPDX-License-Identifier: BSD-3-clause
import argparse
from datetime import date
from collections import defaultdict
from functools import lru_cache
import logging
import json
from pathlib import Path
from typing import Union, Dict, Any, List, Tuple
from urllib.parse import quote
import git
from git.repo import Repo
import requests
import sarge
API_URL = "https://invent.kde.org/api/v4/projects/"
OBS_URL = "https://api.opensuse.org/trigger/runservice"
MATRIX_COMMANDER = "/home/mocker/local-venv/bin/matrix-commander.py"
REPO_TEMPLATE = "https://invent.kde.org/{}"
MESSAGE_TEMPLATE = f"""
## OBS package update report
Updated at {date.today().strftime('%Y-%m-%d')}
"""
def project_exists(project: str) -> bool:
# We want / to get quoted, so put safe to ""
project_name = quote(project, safe="")
request = requests.get(API_URL + project_name)
if request:
return True
return False
def get_remote_hash(url: str, branch: str = "master") -> str:
gitcmd = git.cmd.Git()
revision = gitcmd.ls_remote(url, branch, quiet=True, refs=True)
git_hash, branch = revision.split("\t")
return git_hash
def trigger_update(repository: str, package_name: str,
token: str) -> Union[requests.Response, bool]:
header = {"Authorization": f"Token {token}"}
parameters = {"project": repository, "package": package_name}
logging.info("Updating package %s", package_name)
result = requests.post(OBS_URL, params=parameters, headers=header)
if not result:
logging.error(
"Error during service run, package %s, error code %s, url %s",
package_name, result.status_code, result.url)
return False
logging.debug("Package %s complete", package_name)
return result
class RepoUpdater:
def __init__(self, config: str, cache_file: str, token_file: str) -> None:
self.cache = cache_file
self.token = token_file
if not Path(cache_file).exists():
logging.debug("File cache not found, not loading")
self._data: Dict[str, Dict[str, str]] = dict()
else:
with open(cache_file) as handle:
self._data = json.load(handle)
with open(config, "r") as mapping:
repo_data = json.load(mapping)
self.config = repo_data
@property
def repositories(self) -> List[str]:
return self._data.keys()
def update_repository(self, repository) -> List[Tuple[str, str, str]]:
if self._data.get(repository) is None:
logging.debug("No prior data - initializing empty")
self._data[repository] = dict()
to_update = self.get_updated_remotes(repository)
if not to_update:
logging.debug(f"Nothing to update for {repository}")
return
logging.info(f"Found {len(to_update)} updated repositories")
updated = list()
logging.info("Updating packages for %s", repository)
for package in to_update:
if trigger_update(repository, package, self.token):
remote_hash = to_update[package]
remote_name = self.config[repository][package]["kde"]
self._data[repository][remote_name] = remote_hash
self.save_cache()
updated.append((package, remote_name, remote_hash))
else:
updated.append((package, remote_name, "error"))
return updated
@lru_cache(maxsize=200)
def get_updated_remotes(self, repository: str) -> Dict[str, str]:
to_update = dict()
repodata = self.config[repository]
for repo, data in repodata.items():
kde_name = data["kde"]
branch = data["branch"]
url = REPO_TEMPLATE.format(kde_name)
if not project_exists(kde_name):
logging.warning("Repository %s not found, skipping",
kde_name)
continue
local_hash = self._data[repository].get(kde_name, "")
remote_hash = get_remote_hash(url, branch)
if local_hash != remote_hash:
logging.debug("Hash doesn't match, marking as changed")
to_update[repo] = remote_hash
return to_update
def save_cache(self) -> None:
logging.debug("Saving JSON cache")
with open(self.cache, "w") as handle:
json.dump(self._data, handle, indent=4)
def notify_matrix(update_data: Dict[str, List[Tuple[str, str]]]) -> None:
structure = [MESSAGE_TEMPLATE]
structure.append("Updated packages:\n")
errors = list()
for repo, update in update_data.items():
heading = f"### {repo}\n"
structure.append(heading)
structure.append(f"Updated {len(update)} packages.")
for package, remote, state in update:
if state != "error":
row = (f"* {package} - [{state}]"
f"(https://commits.kde.org/{remote}/{state}")
structure.append(row)
else:
errors.append(package)
structure.append("#### Packages with errors")
for errored in errors:
structure.append(f"* {errored}")
message = "\n".join(structure)
cmd = [MATRIX_COMMANDER, "--markdown", "-m", message]
logging.debug("Sending Matrix notification")
sarge.run(cmd)
def commit_changes(cache_file: str, repo_home: str) -> None:
repo = Repo(repo_home)
repo.index.add([cache_file])
repo.index.commit("[OBS unstable update bot] Update caches")
origin = repo.remotes["origin"]
origin.pull(rebase=True)
origin.push()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--cache-file", help="Location of the cache file",
default=Path.home() / ".local/share/obs_repo_cache.json")
parser.add_argument(
"--repo-root", help="Location of the git repository root")
parser.add_argument("mapping_file", help="KDE:OBS repository mapping file")
parser.add_argument("token", help="Authorization token file")
parser.add_argument("--debug", help="Show debugging output",
action="store_true")
options = parser.parse_args()
level = logging.INFO if not options.debug else logging.DEBUG
logging.basicConfig(format='%(levelname)s - %(message)s',
level=level)
cache_file = options.cache_file
with open(options.token) as handle:
token = handle.read().strip()
updater = RepoUpdater(options.mapping_file, cache_file, token)
updated_data = dict()
for repo in updater.repositories:
updated = updater.update_repository(repo)
updated_data[repo] = updated
if options.repo_root is not None:
logging.info("Committing changes")
commit_changes(cache_file, options.repo_root)
notify_matrix(updated_data)
logging.info("Complete")
if __name__ == "__main__":
main()