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

404 lines
15 KiB
Python
Raw Normal View History

import time
import hashlib
import re
import sys
import requests
import toml
import os
import glob
STRATEGIES = ["latest_github_release", "latest_github_tag", "latest_github_commit"]
if len(sys.argv) >= 2:
dry_run = True
else:
dry_run = False
GITHUB_LOGIN = open(os.path.dirname(__file__) + "/../../.github_login").read().strip()
GITHUB_TOKEN = open(os.path.dirname(__file__) + "/../../.github_token").read().strip()
GITHUB_EMAIL = open(os.path.dirname(__file__) + "/../../.github_email").read().strip()
from github import Github, InputGitAuthor
github = Github(GITHUB_TOKEN)
author = InputGitAuthor(GITHUB_LOGIN, GITHUB_EMAIL)
def apps_to_run_auto_update_for():
catalog = toml.load(open(os.path.dirname(__file__) + "/../../apps.toml"))
2023-03-28 00:42:18 +02:00
apps_flagged_as_working_and_on_yunohost_apps_org = [
app
for app, infos in catalog.items()
if infos["state"] == "working"
and "/github.com/yunohost-apps" in infos["url"].lower()
]
2023-03-28 00:42:18 +02:00
manifest_tomls = glob.glob(
os.path.dirname(__file__) + "/../../.apps_cache/*/manifest.toml"
)
apps_with_manifest_toml = [path.split("/")[-2] for path in manifest_tomls]
2023-03-28 00:42:18 +02:00
relevant_apps = list(
sorted(
set(apps_flagged_as_working_and_on_yunohost_apps_org)
& set(apps_with_manifest_toml)
)
)
out = []
for app in relevant_apps:
2023-03-28 00:42:18 +02:00
manifest = toml.load(
os.path.dirname(__file__) + f"/../../.apps_cache/{app}/manifest.toml"
)
sources = manifest.get("resources", {}).get("sources", {})
if any("autoupdate" in source for source in sources.values()):
out.append(app)
return out
def filter_and_get_latest_tag(tags, app_id):
filter_keywords = ["start", "rc", "beta", "alpha"]
tags = [t for t in tags if not any(keyword in t for keyword in filter_keywords)]
tag_dict = {}
for t in tags:
t_to_check = t
if t.startswith(app_id + "-"):
t_to_check = t.split("-", 1)[-1]
if not re.match(r"^v?[\d\.]*\d$", t_to_check):
print(f"Ignoring tag {t_to_check}, doesn't look like a version number")
else:
tag_dict[t] = tag_to_int_tuple(t_to_check)
tags = sorted(list(tag_dict.keys()), key=tag_dict.get)
return tags[-1], '.'.join([str(i) for i in tag_dict[tags[-1]]])
def tag_to_int_tuple(tag):
tag = tag.strip("v")
int_tuple = tag.split(".")
assert all(i.isdigit() for i in int_tuple), f"Cant convert {tag} to int tuple :/"
return tuple(int(i) for i in int_tuple)
def sha256_of_remote_file(url):
print(f"Computing sha256sum for {url} ...")
try:
r = requests.get(url, stream=True)
m = hashlib.sha256()
for data in r.iter_content(8192):
m.update(data)
return m.hexdigest()
except Exception as e:
print(f"Failed to compute sha256 for {url} : {e}")
return None
2023-03-28 00:42:18 +02:00
class AppAutoUpdater:
def __init__(self, app_id):
if dry_run:
if not os.path.exists(app_id + "/manifest.toml"):
raise Exception("manifest.toml doesnt exists?")
# app_id is in fact a path
manifest = toml.load(open(app_id + "/manifest.toml"))
else:
# We actually want to look at the manifest on the "testing" (or default) branch
self.repo = github.get_repo(f"Yunohost-Apps/{app_id}_ynh")
# Determine base branch, either `testing` or default branch
try:
self.base_branch = self.repo.get_branch("testing").name
except:
self.base_branch = self.repo.default_branch
contents = self.repo.get_contents("manifest.toml", ref=self.base_branch)
self.manifest_raw = contents.decoded_content.decode()
self.manifest_raw_sha = contents.sha
manifest = toml.loads(self.manifest_raw)
self.app_id = manifest["id"]
self.current_version = manifest["version"].split("~")[0]
self.sources = manifest.get("resources", {}).get("sources")
if not self.sources:
raise Exception("There's no resources.sources in manifest.toml ?")
self.main_upstream = manifest.get("upstream", {}).get("code")
def run(self):
todos = {}
for source, infos in self.sources.items():
if "autoupdate" not in infos:
continue
strategy = infos.get("autoupdate", {}).get("strategy")
if strategy not in STRATEGIES:
2023-03-28 00:42:18 +02:00
raise Exception(
f"Unknown strategy to autoupdate {source}, expected one of {STRATEGIES}, got {strategy}"
)
asset = infos.get("autoupdate", {}).get("asset", "tarball")
print(f"Checking {source} ...")
2023-03-28 00:42:18 +02:00
new_version, new_asset_urls = self.get_latest_version_and_asset(
strategy, asset, infos, source
2023-03-28 00:42:18 +02:00
)
if source == "main":
print(f"Current version in manifest: {self.current_version}")
print(f"Newest version on upstream: {new_version}")
if self.current_version == new_version:
2023-03-28 00:42:18 +02:00
print(
f"Version is still {new_version}, no update required for {source}"
)
continue
else:
print(f"Update needed for {source}")
2023-03-28 00:42:18 +02:00
todos[source] = {
"new_asset_urls": new_asset_urls,
"old_assets": infos,
"new_version": new_version,
}
else:
if isinstance(new_asset_urls, str) and infos["url"] == new_asset_urls:
print(f"URL is still up to date for asset {source}")
continue
2023-03-28 00:42:18 +02:00
elif isinstance(new_asset_urls, dict) and new_asset_urls == {
k: infos[k]["url"] for k in new_asset_urls.keys()
}:
print(f"URLs are still up to date for asset {source}")
continue
else:
print(f"Update needed for {source}")
2023-03-28 00:42:18 +02:00
todos[source] = {
"new_asset_urls": new_asset_urls,
"old_assets": infos,
}
if dry_run or not todos:
return
if "main" in todos:
new_version = todos["main"]["new_version"]
message = f"Upgrade to v{new_version}"
new_branch = f"ci-auto-update-{new_version}"
else:
message = "Upgrade sources"
new_branch = "ci-auto-update-sources"
try:
# Get the commit base for the new branch, and create it
commit_sha = self.repo.get_branch(self.base_branch).commit.sha
self.repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=commit_sha)
except:
pass
manifest_new = self.manifest_raw
for source, infos in todos.items():
2023-03-28 00:42:18 +02:00
manifest_new = self.replace_version_and_asset_in_manifest(
manifest_new,
infos.get("new_version"),
infos["new_asset_urls"],
infos["old_assets"],
is_main=source == "main",
)
self.repo.update_file(
"manifest.toml",
message=message,
content=manifest_new,
sha=self.manifest_raw_sha,
branch=new_branch,
author=author,
)
# Wait a bit to preserve the API rate limit
time.sleep(1.5)
# Open the PR
2023-03-28 00:42:18 +02:00
pr = self.repo.create_pull(
title=message, body=message, head=new_branch, base=self.base_branch
)
print("Created PR " + self.repo.full_name + " updated with PR #" + str(pr.id))
def get_latest_version_and_asset(self, strategy, asset, infos, source):
upstream = infos.get("autoupdate", {}).get("upstream", self.main_upstream)
if "github" in strategy:
assert upstream and upstream.startswith(
2023-03-28 00:42:18 +02:00
"https://github.com/"
), f"When using strategy {strategy}, having a defined upstream code repo on github.com is required"
upstream_repo = upstream.replace("https://github.com/", "").strip(
2023-03-28 00:42:18 +02:00
"/"
)
assert (
len(upstream_repo.split("/")) == 2
), f"'{upstream}' doesn't seem to be a github repository ?"
if strategy == "latest_github_release":
releases = self.github_api(f"repos/{upstream_repo}/releases")
2023-03-28 00:42:18 +02:00
tags = [
release["tag_name"]
for release in releases
if not release["draft"] and not release["prerelease"]
]
latest_version_orig, latest_version = filter_and_get_latest_tag(tags, self.app_id)
if asset == "tarball":
2023-03-28 00:42:18 +02:00
latest_tarball = (
f"{upstream}/archive/refs/tags/{latest_version_orig}.tar.gz"
2023-03-28 00:42:18 +02:00
)
return latest_version, latest_tarball
# FIXME
else:
2023-03-28 00:42:18 +02:00
latest_release = [
release
for release in releases
if release["tag_name"] == latest_version_orig
2023-03-28 00:42:18 +02:00
][0]
latest_assets = {
a["name"]: a["browser_download_url"]
for a in latest_release["assets"]
if not a["name"].endswith(".md5")
}
if isinstance(asset, str):
2023-03-28 00:42:18 +02:00
matching_assets_urls = [
url
for name, url in latest_assets.items()
if re.match(asset, name)
]
if not matching_assets_urls:
2023-03-28 00:42:18 +02:00
raise Exception(
f"No assets matching regex '{asset}' for release {latest_version} among {list(latest_assets.keys())}"
)
elif len(matching_assets_urls) > 1:
2023-03-28 00:42:18 +02:00
raise Exception(
f"Too many assets matching regex '{asset}' for release {latest_version} : {matching_assets_urls}"
)
return latest_version, matching_assets_urls[0]
elif isinstance(asset, dict):
matching_assets_dicts = {}
for asset_name, asset_regex in asset.items():
2023-03-28 00:42:18 +02:00
matching_assets_urls = [
url
for name, url in latest_assets.items()
if re.match(asset_regex, name)
]
if not matching_assets_urls:
2023-03-28 00:42:18 +02:00
raise Exception(
f"No assets matching regex '{asset}' for release {latest_version} among {list(latest_assets.keys())}"
)
elif len(matching_assets_urls) > 1:
2023-03-28 00:42:18 +02:00
raise Exception(
f"Too many assets matching regex '{asset}' for release {latest_version} : {matching_assets_urls}"
)
matching_assets_dicts[asset_name] = matching_assets_urls[0]
return latest_version.strip("v"), matching_assets_dicts
elif strategy == "latest_github_tag":
if asset != "tarball":
2023-03-28 00:42:18 +02:00
raise Exception(
"For the latest_github_tag strategy, only asset = 'tarball' is supported"
)
tags = self.github_api(f"repos/{upstream_repo}/tags")
latest_version_orig, latest_version = filter_and_get_latest_tag([t["name"] for t in tags], self.app_id)
2023-03-28 00:42:18 +02:00
latest_tarball = (
f"{upstream}/archive/refs/tags/{latest_version}.tar.gz"
2023-03-28 00:42:18 +02:00
)
return latest_version, latest_tarball
elif strategy == "latest_github_commit":
if asset != "tarball":
raise Exception(
"For the latest_github_release strategy, only asset = 'tarball' is supported"
)
commits = self.github_api(f"repos/{upstream_repo}/commits")
latest_commit = commits[0]
latest_tarball = f"https://github.com/{upstream_repo}/archive/{latest_commit['sha']}.tar.gz"
# Let's have the version as something like "2023.01.23"
latest_version = latest_commit["commit"]["author"]["date"][:10].replace("-", ".")
return latest_version, latest_tarball
def github_api(self, uri):
if dry_run:
auth = None
else:
auth = (GITHUB_LOGIN, GITHUB_TOKEN)
2023-03-28 00:42:18 +02:00
r = requests.get(
f"https://api.github.com/{uri}", auth=None
2023-03-28 00:42:18 +02:00
)
assert r.status_code == 200, r
return r.json()
2023-03-28 00:42:18 +02:00
def replace_version_and_asset_in_manifest(
self, content, new_version, new_assets_urls, current_assets, is_main
):
if isinstance(new_assets_urls, str):
sha256 = sha256_of_remote_file(new_assets_urls)
elif isinstance(new_assets_urls, dict):
2023-03-28 00:42:18 +02:00
sha256 = {
url: sha256_of_remote_file(url) for url in new_assets_urls.values()
}
if is_main:
2023-03-28 00:42:18 +02:00
def repl(m):
return m.group(1) + new_version + m.group(3)
2023-03-28 00:42:18 +02:00
content = re.sub(
r"(\s*version\s*=\s*[\"\'])([\d\.]+)(\~ynh\d+[\"\'])", repl, content
)
if isinstance(new_assets_urls, str):
content = content.replace(current_assets["url"], new_assets_urls)
content = content.replace(current_assets["sha256"], sha256)
elif isinstance(new_assets_urls, dict):
for key, url in new_assets_urls.items():
content = content.replace(current_assets[key]["url"], url)
content = content.replace(current_assets[key]["sha256"], sha256[url])
return content
# Progress bar helper, stolen from https://stackoverflow.com/a/34482761
def progressbar(it, prefix="", size=60, file=sys.stdout):
it = list(it)
count = len(it)
2023-03-28 00:42:18 +02:00
def show(j, name=""):
name += " "
2023-03-28 00:42:18 +02:00
x = int(size * j / count)
file.write(
"%s[%s%s] %i/%i %s\r" % (prefix, "#" * x, "." * (size - x), j, count, name)
)
file.flush()
2023-03-28 00:42:18 +02:00
show(0)
for i, item in enumerate(it):
yield item
2023-03-28 00:42:18 +02:00
show(i + 1, item)
file.write("\n")
file.flush()
if __name__ == "__main__":
if len(sys.argv) >= 2:
AppAutoUpdater(sys.argv[1]).run()
else:
for app in progressbar(apps_to_run_auto_update_for(), "Checking: ", 40):
AppAutoUpdater(app).run()