1
0
Fork 0
ynh-apps_tools/update_app_levels/update_app_levels.py

240 lines
7.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Update app catalog: commit, and create a merge request
"""
import argparse
import logging
import tempfile
import textwrap
import time
2023-01-20 17:14:30 +01:00
from collections import OrderedDict
2024-02-09 21:41:26 +01:00
from typing import Any, Optional
from pathlib import Path
import jinja2
import requests
import toml
from git import Repo
2024-02-08 23:08:46 +01:00
APPS_REPO = "YunoHost/apps"
CI_RESULTS_URL = "https://ci-apps.yunohost.org/ci/api/results"
REPO_APPS_ROOT = Path(Repo(__file__, search_parent_directories=True).working_dir)
2024-02-09 21:41:26 +01:00
def github_token() -> Optional[str]:
github_token_path = REPO_APPS_ROOT / ".github_token"
if github_token_path.exists():
return github_token_path.open("r", encoding="utf-8").read().strip()
return None
def get_ci_results() -> dict[str, dict[str, Any]]:
return requests.get(CI_RESULTS_URL, timeout=10).json()
def ci_result_is_outdated(result) -> bool:
# 3600 * 24 * 60 = ~2 months
return (int(time.time()) - result.get("timestamp", 0)) > 3600 * 24 * 60
def update_catalog(catalog, ci_results) -> dict:
"""
Actually change the catalog data
"""
# Re-sort the catalog keys / subkeys
for app, infos in catalog.items():
catalog[app] = OrderedDict(sorted(infos.items()))
catalog = OrderedDict(sorted(catalog.items()))
def app_level(app):
if app not in ci_results:
return 0
if ci_result_is_outdated(ci_results[app]):
return 0
return ci_results[app]["level"]
for app, info in catalog.items():
info["level"] = app_level(app)
return catalog
def list_changes(catalog, ci_results) -> dict[str, list[tuple[str, int, int]]]:
"""
Lists changes for a pull request
"""
changes = {
"major_regressions": [],
"minor_regressions": [],
"improvements": [],
"outdated": [],
"missing": [],
}
for app, infos in catalog.items():
if infos.get("state") != "working":
continue
if app not in ci_results:
changes["missing"].append(app)
continue
if ci_result_is_outdated(ci_results[app]):
changes["outdated"].append(app)
continue
ci_level = ci_results[app]["level"]
current_level = infos.get("level")
if ci_level == current_level:
continue
if current_level is None or ci_level > current_level:
changes["improvements"].append((app, current_level, ci_level))
continue
if ci_level < current_level:
if ci_level <= 4 < current_level:
changes["major_regressions"].append((app, current_level, ci_level))
else:
changes["minor_regressions"].append((app, current_level, ci_level))
return changes
def pretty_changes(changes: dict[str, list[tuple[str, int, int]]]) -> str:
2024-03-11 17:34:33 +01:00
pr_body_template = textwrap.dedent(
"""
{%- if changes["major_regressions"] %}
2024-02-07 17:30:45 +01:00
### Major regressions 😭
{% for app in changes["major_regressions"] %}
2024-02-07 17:30:45 +01:00
- [ ] [{{app.0}}: {{app.1}} {{app.2}}](https://ci-apps.yunohost.org/ci/apps/{{app.0}}/latestjob)
{%- endfor %}
{% endif %}
{%- if changes["minor_regressions"] %}
2024-02-07 17:30:45 +01:00
### Minor regressions 😬
{% for app in changes["minor_regressions"] %}
2024-02-07 17:30:45 +01:00
- [ ] [{{app.0}}: {{app.1}} {{app.2}}](https://ci-apps.yunohost.org/ci/apps/{{app.0}}/latestjob)
{%- endfor %}
{% endif %}
{%- if changes["improvements"] %}
2024-02-07 17:30:45 +01:00
### Improvements 🥳
{% for app in changes["improvements"] %}
2024-02-07 17:30:45 +01:00
- [{{app.0}}: {{app.1}} {{app.2}}](https://ci-apps.yunohost.org/ci/apps/{{app.0}}/latestjob)
{%- endfor %}
{% endif %}
{%- if changes["missing"] %}
2024-02-07 17:30:45 +01:00
### Missing 🫠
{% for app in changes["missing"] %}
2024-02-09 21:41:26 +01:00
- [{{app}} (See latest job if it exists)](https://ci-apps.yunohost.org/ci/apps/{{app}}/latestjob)
{%- endfor %}
{% endif %}
{%- if changes["outdated"] %}
2024-02-07 17:30:45 +01:00
### Outdated ⏰
{% for app in changes["outdated"] %}
2024-02-09 21:41:26 +01:00
- [ ] [{{app}} (See latest job if it exists)](https://ci-apps.yunohost.org/ci/apps/{{app}}/latestjob)
{%- endfor %}
{% endif %}
2024-03-11 17:34:33 +01:00
"""
)
return jinja2.Environment().from_string(pr_body_template).render(changes=changes)
def make_pull_request(pr_body: str) -> None:
pr_data = {
"title": "Update app levels according to CI results",
"body": pr_body,
"head": "update_app_levels",
2024-03-11 17:34:33 +01:00
"base": "master",
}
with requests.Session() as s:
s.headers.update({"Authorization": f"token {github_token()}"})
2024-03-11 17:34:33 +01:00
response = s.post(
f"https://api.github.com/repos/{APPS_REPO}/pulls", json=pr_data
)
if response.status_code == 422:
2024-03-11 17:34:33 +01:00
response = s.get(
f"https://api.github.com/repos/{APPS_REPO}/pulls",
data={"head": "update_app_levels"},
)
2024-02-07 17:25:16 +01:00
response.raise_for_status()
pr_number = response.json()[0]["number"]
# head can't be updated
del pr_data["head"]
2024-03-11 17:34:33 +01:00
response = s.patch(
f"https://api.github.com/repos/{APPS_REPO}/pulls/{pr_number}",
json=pr_data,
)
2024-02-07 17:25:16 +01:00
response.raise_for_status()
existing_url = response.json()["html_url"]
2024-03-11 17:34:33 +01:00
logging.warning(
f"An existing Pull Request has been updated at {existing_url} !"
)
else:
2024-02-07 17:25:16 +01:00
response.raise_for_status()
new_url = response.json()["html_url"]
logging.info(f"Opened a Pull Request at {new_url} !")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--commit", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--pr", action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-v", "--verbose", action=argparse.BooleanOptionalAction)
args = parser.parse_args()
logging.getLogger().setLevel(logging.INFO)
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
with tempfile.TemporaryDirectory(prefix="update_app_levels_") as tmpdir:
logging.info("Cloning the repository...")
apps_repo = Repo.clone_from(f"git@github.com:{APPS_REPO}", to_path=tmpdir)
2024-02-08 23:09:11 +01:00
assert apps_repo.working_tree_dir is not None
apps_toml_path = Path(apps_repo.working_tree_dir) / "apps.toml"
# Load the app catalog and filter out the non-working ones
2024-02-08 23:09:11 +01:00
catalog = toml.load(apps_toml_path.open("r", encoding="utf-8"))
new_branch = apps_repo.create_head("update_app_levels", apps_repo.refs.master)
apps_repo.head.reference = new_branch
logging.info("Retrieving the CI results...")
ci_results = get_ci_results()
# Now compute changes, then update the catalog
changes = list_changes(catalog, ci_results)
pr_body = pretty_changes(changes)
catalog = update_catalog(catalog, ci_results)
# Save the new catalog
updated_catalog = toml.dumps(catalog)
updated_catalog = updated_catalog.replace(",]", " ]")
2024-02-08 23:09:11 +01:00
apps_toml_path.open("w", encoding="utf-8").write(updated_catalog)
if args.commit:
logging.info("Committing and pushing the new catalog...")
apps_repo.index.add("apps.toml")
apps_repo.index.commit("Update app levels according to CI results")
2024-02-09 21:41:26 +01:00
apps_repo.git.push("--set-upstream", "origin", new_branch)
2024-02-07 17:33:01 +01:00
if args.verbose:
print(pr_body)
if args.pr:
logging.info("Opening a pull request...")
make_pull_request(pr_body)
if __name__ == "__main__":
main()