mirror of https://github.com/sipwise/repoapi.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
376 lines
13 KiB
376 lines
13 KiB
# Copyright (C) 2017-2024 The Sipwise Team - http://sipwise.com
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the Free
|
|
# Software Foundation, either version 3 of the License, or (at your option)
|
|
# any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
# more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
import datetime
|
|
|
|
import structlog
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.forms.models import model_to_dict
|
|
from django.utils import timezone
|
|
|
|
from .conf import settings
|
|
from .exceptions import BuildReleaseUnique
|
|
from .exceptions import PreviousBuildNotDone
|
|
from .utils import get_simple_release
|
|
from .utils import ReleaseConfig
|
|
from .utils import remove_from_textlist
|
|
from repoapi.models import JenkinsBuildInfo
|
|
from repoapi.utils import regex_mrXX
|
|
from repoapi.utils import regex_mrXX_up
|
|
from repoapi.utils import regex_mrXXX
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
build_release_jobs = ",".join(settings.BUILD_RELEASE_JOBS)
|
|
release_jobs_len = len(build_release_jobs) + 1
|
|
|
|
|
|
class BuildReleaseManager(models.Manager):
|
|
_jbi = JenkinsBuildInfo.objects
|
|
|
|
def release(self, version, distribution):
|
|
qs = self.get_queryset()
|
|
return qs.filter(
|
|
Q(release=version, distribution=distribution)
|
|
| Q(release="{}-update".format(version), distribution=distribution)
|
|
)
|
|
|
|
def create_build_release(self, uuid, release, fake=False):
|
|
config = ReleaseConfig(release)
|
|
qs = self.get_queryset()
|
|
br = qs.filter(
|
|
release=config.release, distribution=config.debian_release
|
|
)
|
|
release_ok = config.release
|
|
if br.exists():
|
|
if regex_mrXXX.match(config.branch):
|
|
msg = f"release[mrX.Y.Z]:{release} has already a build"
|
|
raise BuildReleaseUnique(msg)
|
|
elif regex_mrXX.match(config.branch):
|
|
release_ok = f"{config.release}-update"
|
|
msg = (
|
|
f"release[mrX.Y]:{config.branch} has already a build, "
|
|
f"set {release_ok} as release"
|
|
)
|
|
logger.info(msg)
|
|
if not br.last().done:
|
|
logger.info(f"release:{release} is already building")
|
|
raise PreviousBuildNotDone(msg)
|
|
projects = ",".join(config.projects)
|
|
if fake:
|
|
start_date = timezone.make_aware(datetime.datetime(1977, 1, 1))
|
|
built_projects = ",".join(
|
|
list(settings.BUILD_RELEASE_JOBS) + config.projects
|
|
)
|
|
else:
|
|
start_date = timezone.now()
|
|
built_projects = None
|
|
return self.create(
|
|
start_date=start_date,
|
|
uuid=uuid,
|
|
tag=config.tag,
|
|
branch=config.branch,
|
|
release=release_ok,
|
|
distribution=config.debian_release,
|
|
projects=projects,
|
|
built_projects=built_projects,
|
|
)
|
|
|
|
def jbi(self, release_uuid):
|
|
qs = self._jbi.get_queryset()
|
|
return qs.filter(param_release_uuid=release_uuid)
|
|
|
|
def release_jobs(self, release_uuid, flat=True):
|
|
qs = self._jbi.get_queryset()
|
|
res = qs.filter(
|
|
jobname__in=settings.BUILD_RELEASE_JOBS,
|
|
param_release_uuid=release_uuid,
|
|
).distinct()
|
|
if res.exists():
|
|
if flat:
|
|
return res.values_list("jobname", flat=True)
|
|
else:
|
|
return res.values("jobname")
|
|
|
|
def release_jobs_full(self, release_uuid):
|
|
res = dict()
|
|
jobs = self.release_jobs(release_uuid)
|
|
if jobs is None:
|
|
return res
|
|
for job in jobs:
|
|
uuids = self.release_jobs_uuids(release_uuid, job)
|
|
res[job] = [model_to_dict(x) for x in uuids]
|
|
return res
|
|
|
|
def release_jobs_uuids(self, release_uuid, job):
|
|
qs = self._jbi.get_queryset()
|
|
params = {
|
|
"param_release_uuid": release_uuid,
|
|
"jobname": job,
|
|
"tag__isnull": False,
|
|
}
|
|
res = qs.filter(**params).distinct()
|
|
if res.exists():
|
|
return res.order_by("-date")
|
|
|
|
def releases_with_builds(self):
|
|
qs = self.get_queryset()
|
|
res = set()
|
|
for br in qs.all():
|
|
res.add(get_simple_release(br.release))
|
|
return res
|
|
|
|
|
|
class BuildRelease(models.Model):
|
|
uuid = models.CharField(max_length=64, unique=True, null=False)
|
|
start_date = models.DateTimeField(
|
|
blank=True, editable=False, default=timezone.now
|
|
)
|
|
tag = models.CharField(max_length=50, null=True, blank=True)
|
|
branch = models.CharField(max_length=50, null=False)
|
|
release = models.CharField(max_length=50, null=False, db_index=True)
|
|
distribution = models.CharField(max_length=50, null=False, editable=False)
|
|
projects = models.TextField(null=False, editable=False)
|
|
built_projects = models.TextField(null=True, editable=False)
|
|
triggered_projects = models.TextField(null=True, editable=False)
|
|
failed_projects = models.TextField(null=True, editable=False)
|
|
pool_size = models.SmallIntegerField(default=0, editable=False)
|
|
triggered_jobs = models.TextField(null=True, editable=False)
|
|
objects = BuildReleaseManager()
|
|
|
|
class Meta:
|
|
permissions = [
|
|
("can_trigger", "can trigger build releases"),
|
|
("can_trigger_hotfix", "can trigger hotfix builds"),
|
|
]
|
|
|
|
def __str__(self):
|
|
return "%s[%s]" % (self.release, self.uuid)
|
|
|
|
def refresh_projects(self):
|
|
old_projects = set(self.projects_list)
|
|
self.projects = ",".join(self.config.projects)
|
|
new_list = set(self.projects_list)
|
|
append_list = [item for item in new_list if item not in old_projects]
|
|
removed_list = [item for item in old_projects if item not in new_list]
|
|
for project in removed_list:
|
|
remove_from_textlist(self, "built_projects", project)
|
|
remove_from_textlist(self, "triggered_projects", project)
|
|
remove_from_textlist(self, "failed_projects", project)
|
|
JenkinsBuildInfo.objects.filter(
|
|
projectname__in=removed_list,
|
|
param_release_uuid=self.uuid,
|
|
).delete()
|
|
self.save()
|
|
return (append_list, removed_list)
|
|
|
|
def resume(self):
|
|
if not self.done:
|
|
from .tasks import build_resume
|
|
|
|
build_resume.delay(self.id)
|
|
|
|
@property
|
|
def last_update(self):
|
|
job = BuildRelease.objects.jbi(self.uuid).order_by("-date").first()
|
|
if job:
|
|
return job.date
|
|
|
|
@property
|
|
def is_update(self):
|
|
return regex_mrXX_up.match(self.release) is not None
|
|
|
|
@property
|
|
def done(self):
|
|
if self.built_projects is None:
|
|
return False
|
|
built_len = len(self.built_projects)
|
|
if self.is_update:
|
|
return built_len == len(self.projects)
|
|
else:
|
|
return built_len == release_jobs_len + len(self.projects)
|
|
|
|
@property
|
|
def projects_list(self):
|
|
return [x.strip() for x in self.projects.split(",")]
|
|
|
|
@property
|
|
def built_projects_list(self):
|
|
if self.built_projects is not None:
|
|
return [x.strip() for x in self.built_projects.split(",")]
|
|
return []
|
|
|
|
@property
|
|
def queued_projects_list(self):
|
|
return [
|
|
x for x in self.projects_list if x not in self.built_projects_list
|
|
]
|
|
|
|
@property
|
|
def failed_projects_list(self):
|
|
if self.failed_projects is not None:
|
|
return [x.strip() for x in self.failed_projects.split(",")]
|
|
return []
|
|
|
|
@property
|
|
def triggered_projects_list(self):
|
|
if self.triggered_projects is not None:
|
|
return [x.strip() for x in self.triggered_projects.split(",")]
|
|
return []
|
|
|
|
@property
|
|
def triggered_jobs_list(self):
|
|
if self.triggered_jobs is not None:
|
|
return [x.strip() for x in self.triggered_jobs.split(",")]
|
|
return []
|
|
|
|
def append_triggered_job(self, value):
|
|
if value in self.triggered_jobs_list:
|
|
return False
|
|
if self.triggered_jobs is None:
|
|
self.triggered_jobs = value
|
|
else:
|
|
self.triggered_jobs += ",{}".format(value)
|
|
self.save()
|
|
return True
|
|
|
|
def append_triggered(self, value):
|
|
if value in self.triggered_projects_list:
|
|
return False
|
|
if self.triggered_projects is None:
|
|
self.triggered_projects = value
|
|
else:
|
|
self.triggered_projects += ",{}".format(value)
|
|
self.pool_size += 1
|
|
self.save()
|
|
return True
|
|
|
|
def _append_failed(self, value):
|
|
if value in self.failed_projects_list:
|
|
return False
|
|
if self.failed_projects is None:
|
|
self.failed_projects = value
|
|
else:
|
|
self.failed_projects += ",{}".format(value)
|
|
if self.pool_size > 0:
|
|
self.pool_size -= 1
|
|
self.save()
|
|
return True
|
|
|
|
def _append_built(self, value):
|
|
if value in self.built_projects_list:
|
|
return False
|
|
if self.built_projects is None:
|
|
self.built_projects = value
|
|
else:
|
|
self.built_projects += ",{}".format(value)
|
|
failed_projects = self.failed_projects_list
|
|
if value in failed_projects:
|
|
failed_projects.remove(value)
|
|
fp = ",".join(failed_projects)
|
|
if len(fp) > 0:
|
|
self.failed_projects = fp
|
|
else:
|
|
self.failed_projects = None
|
|
if self.pool_size > 0:
|
|
self.pool_size -= 1
|
|
self.save()
|
|
return True
|
|
|
|
def remove_triggered(self, jbi):
|
|
remove_from_textlist(self, "triggered_projects", jbi.projectname)
|
|
|
|
def append_built(self, jbi):
|
|
jobname = jbi.jobname
|
|
if jbi.result == "FAILURE":
|
|
if jobname.endswith("-piuparts"):
|
|
return False
|
|
return self._append_failed(jbi.projectname)
|
|
is_repos = jobname.endswith("-repos")
|
|
is_rj = jobname in settings.BUILD_RELEASE_JOBS
|
|
if is_repos or is_rj:
|
|
if jbi.result in ["SUCCESS", "UNSTABLE"]:
|
|
return self._append_built(jbi.projectname)
|
|
return False
|
|
|
|
@property
|
|
def branch_or_tag(self):
|
|
if self.tag:
|
|
return "tag/{}".format(self.tag)
|
|
return "branch/{}".format(self.branch)
|
|
|
|
def _next(self, exclude=[]):
|
|
structlog.contextvars.bind_contextvars(release=f"{self}")
|
|
if self.built_projects is None:
|
|
return self.build_deps[0][0]
|
|
if self.done:
|
|
return
|
|
t_list = self.triggered_projects_list
|
|
built_list = self.built_projects_list
|
|
deps_missing = []
|
|
for grp in self.build_deps:
|
|
for prj in grp:
|
|
if prj not in built_list:
|
|
if prj not in t_list:
|
|
return prj
|
|
else:
|
|
deps_missing.append(prj)
|
|
else:
|
|
if len(deps_missing) > 0:
|
|
logger.info(
|
|
"release has build_deps missing",
|
|
deps_missing=deps_missing,
|
|
)
|
|
return None
|
|
for prj in self.projects_list:
|
|
if prj in exclude:
|
|
continue
|
|
if prj not in built_list and prj not in t_list:
|
|
return prj
|
|
|
|
@property
|
|
def next(self):
|
|
failed_projects = self.failed_projects_list
|
|
structlog.contextvars.bind_contextvars(release=f"{self}")
|
|
if any(job in failed_projects for job in settings.BUILD_RELEASE_JOBS):
|
|
logger.info(
|
|
"release has failed release_jobs, stop sending jobs",
|
|
failed_projects=failed_projects,
|
|
)
|
|
return
|
|
prj = self._next(exclude=failed_projects)
|
|
if prj in failed_projects and self.config.is_build_dep(prj):
|
|
return None
|
|
return prj
|
|
|
|
@property
|
|
def build_release(self) -> str:
|
|
if self.release in ["trunk", "trunk-next"]:
|
|
return "trunk"
|
|
return self.release
|
|
|
|
@property
|
|
def build_deps(self) -> list:
|
|
return self.config.levels_build_deps
|
|
|
|
@property
|
|
def config(self):
|
|
if getattr(self, "_config", None) is None:
|
|
self._config = ReleaseConfig(
|
|
self.release, distribution=self.distribution
|
|
)
|
|
return self._config
|