mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
[IMP] runbot: runbot 5.0
Runbot initial architechture was working for a single odoo repo, and was adapted to build enterprise. Addition of upgrade repo and test began to make result less intuitive revealing more weakness of the system. Adding to the oddities of duplicate detection and branch matching, there was some room for improvement in the runbot models. This (small) commit introduce the runbot v5.0, designed for a closer match of odoo's development flows, and hopefully improving devs experience and making runbot configuration more flexible. **Remotes:** remote intoduction helps to detect duplicate between odoo and odoo-dev repos: a commit is now on a repo, a repo having multiple remote. If a hash is in odoo-dev, we consider that it is the same in odoo. Note: github seems to manage commit kind of the same way. It is possible to send a status on a commit on odoo when the commit only exists in odoo-dev. This change also allows to remove some repo duplicate configuration between a repo and his dev corresponding repo. (modules, server files, manifests, ...) **Trigger:** before v5.0, only one build per repo was created, making it difficult to tweak what test to execute in what case. The example use case was for upgrade. We want to test upgrade to master when pushing on odoo. But we also want to test upgrade the same way when pushing on upgrade. We introduce a build that should be ran on pushing on either repo when each repo already have specific tests. The trigger allows to specify a build to create with a specific config. The trigger is executed when any repo of the trigger repo is pushed. The trigger can define depedencies: only build enterprise when pushing enterprise, but enterprise needs odoo. Test upgrade to master when pushing either odoo or upgrade. Trigger will also allows to extract some build like cla that where executed on both enterprise and odoo, and hidden in a subbuild. **Bundle:** Cross repo branches/pr branches matching was hidden in build creation and can be confusing. A build can be detected as a duplicate of a pr, but not always if naming is wrong or traget is invalid/changes. This was mainly because of how a community ref will be found. This was making ci on pr undeterministic if duplicate matching fails. This was also creating two build, with one pointing to the other when duplicate detection was working, but the visual result can be confusing. Associtaions of remotes and bundles fix this by adding all pr and related branches from all repo in a bundle. First of all this helps to visualise what the runbot consider has branch matching and that should be considered as part of the same task, giving a place where to warn devs of some possible inconsistencies. Associate whith repo/remote, we can consider branches in the same repo in a bundle as expected to have the same head. Only one build is created since trigger considers repo, not remotes. **Batch:** A batch is a group of build, a batch on a bundle can be compared to a build on a branch in previous version. When a branch is pushed, the corresponding bundle creates a new batch, and wait for new commit. Once no new update are detected in the batch for 60 seconds, All the trigger are executed if elligible. The created build are added to the batch in a batch_slot. It is also possible that an corresponding build exists (duplicate) and is added to the slot instead of creating a new build. Co-authored-by d-fence <moc@odoo.com>
This commit is contained in:
parent
7ab7eed27e
commit
45721cdf6c
194
README.md
194
README.md
@ -1,15 +1,191 @@
|
||||
Odoo Runbot Repository
|
||||
=======================
|
||||
# Odoo Runbot Repository
|
||||
|
||||
[](http://runbot.odoo.com/runbot)
|
||||
|
||||
This repository contains the source code of Odoo testing bot [runbot.odoo.com](http://runbot.odoo.com/runbot) and related addons.
|
||||
|
||||
Runbot
|
||||
------
|
||||
|
||||
The `runbot/` directory holds the main runbot Odoo addon.
|
||||
|
||||
Runbot CLA addon
|
||||
------------------
|
||||
|
||||
The `runbot_cla/` directory contains an Odoo addon that checks CLA.
|
||||
## Warnings
|
||||
|
||||
**Runbot will delete folders/ drop database to free some space during usage.** Even if only elements create by runbot are concerned, don't use runbot on a server with sensitive data.
|
||||
|
||||
**Runbot changes some default odoo behaviours** Runbot database may work with other modules, but without any garantee. Avoid to use runbot on an existing database/install other modules than runbot.
|
||||
|
||||
## Glossary/models
|
||||
|
||||
Runbot v5 use a set of concept in order to cover all the use cases we need
|
||||
|
||||
- **Project**: regroups a set of repositories that works togeter. Ususally one project is enough and a default *R&D* project exists.
|
||||
- **Repository**: A repository name regrouping repo and forks Ex: odoo, enterprise
|
||||
- **Remote**: A remote for a repository. Example: odoo/odoo, odoo-dev/odoo
|
||||
- **Build**: A test instance, using a set of commit and parameter to run some code and produce a result.
|
||||
- **Trigger**: Indicates that a build should be created when a new commit is pushed on a repo. A trigger has both trigger repos, and dependency repo. Ex: new commit on runbot-> build with runbot and a dependency with odoo.
|
||||
- **Bundle**: A set or branches that work together: all the branches with the same name and all linked pr in the same project.
|
||||
- **Batch**: A container for builds and commit of a bundle. When new commit is pushed on a branch, if a trigger exists for the repo of that branch, a new batch is created with this commit. After 60 seconds, if no other commit is added to the batch, a build is created by trigger having a new commit in this batch.
|
||||
|
||||
## HOW TO
|
||||
|
||||
This section give the basic steps to follow to configure the runbot v5.0. The configuration may differ from one use to another, this one will describe how to test addons for odoo, needing to fetch odoo core but without testing vanilla odoo. As an exemple, runbot will be used as a tested addons.
|
||||
|
||||
### Setup
|
||||
|
||||
Runbot is an addon for odoo, meaning that both odoo and runbot code are needed to run. Some tips to configure odoo are available in [odoo setup documentation](https://www.odoo.com/documentation/13.0/setup/install.html#setup-install-source) (requirements, postgres, ...) This page will mainly focus on runbot specificities.
|
||||
|
||||
Chose a workspace and clone both repository.
|
||||
|
||||
```
|
||||
git clone https://github.com/odoo/odoo.git
|
||||
git clone https://github.com/odoo/runbot.git
|
||||
```
|
||||
|
||||
Runbot depends on some odoo version, runbot v5.0 is currently based on odoo 13.0 (Runbot 13.0.5.0). Both runbot and odoo 13.0 branch should be chekouted. *This logic follow the convention imposed by runbot to run code from different repository, the branch name must be the same or be prefixed by a main branch name.*
|
||||
|
||||
```
|
||||
git -C odoo checkout 13.0
|
||||
git -C runbot checkout 13.0
|
||||
```
|
||||
|
||||
### Specific requirements
|
||||
|
||||
You will also need to install docker on your system. The user that will be used to operate the runbot must also have access to the Docker commands. On Debian like system's , it's only a matter of adding the user to the `docker` group.
|
||||
|
||||
```
|
||||
sudo adduser $USER docker
|
||||
```
|
||||
|
||||
The only specific python requirement is the `matplotlib` library.
|
||||
|
||||
```
|
||||
sudo apt install python3-matplotlib
|
||||
```
|
||||
|
||||
### Install and start runbot
|
||||
|
||||
Runbot being an odoo addon, you need to start odoo giving runbot in the addons path. Install runbot by giving the -i instruction.
|
||||
|
||||
```
|
||||
python3 odoo/odoo-bin -d runbot_database --addons-path odoo/addons,runbot -i runbot --stop-after-init --without-demo=1
|
||||
```
|
||||
|
||||
Then, launch runbot
|
||||
|
||||
```
|
||||
python3 odoo/odoo-bin -d runbot_database --addons-path odoo/addons,runbot --limit-memory-soft 4294967296 --limit-memory-hard 4311744512 --limit-time-real-cron=1800
|
||||
```
|
||||
|
||||
Note:
|
||||
- --limit-time-real-cron is important to ensure that cron have enough time to build docker images and clone repos the first time. It may be reduced to a lower value later (600 is adviced).
|
||||
- --limit-memory-* is not mandatory, but fetching odoo on multiple remote with only 2Gib may result in a failure of the fetch command. If git fails to create async thread or run out of memory, increasing memory limit may be a good idea. *cf. odoo-bin --help for more info.*
|
||||
|
||||
You may want to configure a service or launch odoo in a screen depending on your preferences.
|
||||
|
||||
### Configuration
|
||||
|
||||
*Note: Runbot is optimized to run commit discovery and build sheduling on different host to allow load share on different machine. This basic configuration will show how to run runbot on a single machine, a less-tested use case*
|
||||
|
||||
#### Bootstrap
|
||||
Once launched, the cron should start to do basic work. The commit discovery and build sheduling is disabled by default, but runbot bootstrap will start to setup some directories in static.
|
||||
>Starting job `Runbot`.
|
||||
```
|
||||
ls runbot/runbot/static
|
||||
```
|
||||
>build docker nginx repo sources src
|
||||
|
||||
- **repo** contains the bare repositories
|
||||
- **sources** contains the exported sources needed for each build
|
||||
- **build** contains the different workspaces for dockers, containing logs/ filestore, ...
|
||||
- **docker** contains DockerFile and docker build logs
|
||||
- **nginx** contaings the nginx config used to access running instances
|
||||
All of them are emply for now.
|
||||
|
||||
A database defined by *runbot.runbot_db_template* icp will be created. By default, runbot use template1. This database will be used as template for testing builds. You can change this database for more customisation.
|
||||
|
||||
Other cron operation are still disabled for now.
|
||||
|
||||
#### Access backend
|
||||
Access odoo "backend" *127.0.0.1:8069/web*
|
||||
|
||||
If not connected yet, connect as admin (default password: admin). You may want to check that.Check odoo documentation for other needed configuration as master password. This is mainly needed for production purpose, a local instance will work as it is.
|
||||
If you create another Odoo user to manage the runbot, you may add the group *Runbot administrator* to this user
|
||||
|
||||
#### Add remotes and repositories
|
||||
Access runbot app and go to the Repos->Repositories menu
|
||||
|
||||
Create a new repo for odoo
|
||||

|
||||
|
||||
- A single remote is added, the base odoo repo. Only branches will be fetch to limit disc usage and branch created in backend. It is possible to add multiple remotes for forks.
|
||||
|
||||
- The repo is in poll mode since github won't hook your runbot instance. Poll mode is limited to one update every 5 minutes.
|
||||
|
||||
- The modules to install pattern is -* in order to disable default module to test for this repo. This will speed up install. To install and test all module, leave this space empty or use \*. Some module may be blacklisted individually, by using *-module,-other_module, l10n_\*.
|
||||
|
||||
- Server files will allow runbot to know the possible file to use to launch odoo. odoo-bin is the one to use for the last version, but you may want to add other server files for older versions (comma separeted list). The same logic is used for manifest files.
|
||||
|
||||
- Addons path are the place where addons directories are located. This will be used for addon-path parameter but also for modules discovery.
|
||||
|
||||
Create a repo for you custom addons repo
|
||||

|
||||
- For your custom repo, it is adviced to configure the repo in hook mode if possible.
|
||||
- No server files should be given since it is an addons repo.
|
||||
- No addons_path given to use repo root as default.
|
||||
- we only want to test runbot and runbot_cla on runbot, `-*,runbot,runbot_cla` will blacklist all except this ones
|
||||
- The remote has PR option checked to fetch pull request too. This is optional.
|
||||
|
||||
#### Tweak runbot parameters and enable features
|
||||
|
||||
Acces the runbot settings and tweak the default parameters.
|
||||
- The *number of worker* is the default number of parallel testing builds per machine. It is adviced to keep one physical core per worker on a dedicated machine. On a local machine,keep it low, **2** is a good start (using 8 on runbot.odoo.com).
|
||||
|
||||
- The *number of running build* is the number of parallel running builds. Runbot will start to kill running build once this limit is reached. This number can be pumped up on a server (using 60 on runbot.odoo.com).
|
||||
- *Runbot domain* will mainly be used for nginx to access running build.
|
||||
- Max commit age is the limit after what a branch head will be ignorred in processing. This will reduce the processing of old non deleted branch. Keep in mind that pushing an old commit on a branch will also be ignored by runbot.
|
||||
|
||||
- **Discover new commits** is disabled by default but is needed to fetch repositories and create new commits/batches/builds. **Check** this option.
|
||||
|
||||
- **Discover new commits** is needed to fetch repositories and create new commits/batches/builds. **Check** this option.
|
||||
|
||||
- **Schedule builds** is needed to process pending/testing. **Check** this option. To use a dedicated host to schedule builds, leave this option unchecked and use the dedicated tool in runbot/builder.
|
||||
|
||||
Save the parameter. The next cron execution should do a lot of setup.
|
||||
NOTE: The default limit_time_real-cron should be ideally set to at least 1800 for this operation.
|
||||
- If schedule builds is check, the first time consuming operation will be to build the docker image. You can check the current running dockers with `docker ps -a`. One of them should be up for a few minutes. If the build is not finished at the end of the cron timeout, docker build will either resolve its progress and continue the next step, but could also fail on the same step each time and stay stuck. Ensure to have limit-time-real-cron high enough, depending on your brandwidth and power this value could be 600-1800 (or more). Let's wait and make a coffee. You can also check progress by tailing runbot/static/docker/docker_build.txt
|
||||
|
||||
- The next git update will init the repositories, a config file with your remotes should be created for each repo. You can check the content in /runbot/static/repo/(runbot|odoo)/config. The repo will be fetched, this operation may take some time too.
|
||||
|
||||
Those two operation will be faster on next executions.
|
||||
|
||||
Finally, the first new branches/batches should be created. You can list them in Bundle > Bundles.
|
||||
|
||||
#### Bundles configuration
|
||||
|
||||
We need to define which bundle are base versions (master should already be marked as a base). In runbot case we only need 13.0 but all saas- and numerical branches should be marked as base in a general way. A base will be used to fill missing commits in a batch if a bundle doesn't have a branch in each repo, and will trigger the creation of a version. Versions may be use for upgrade test.
|
||||
|
||||
Bundles can also be marked as `no_build`, so that new commit won't create batch creation and bundle won't be displayed on main page.
|
||||
|
||||
#### Triggers
|
||||
At this point, runbot will discover new branches, new commits, create bundle, but no build will be created.
|
||||
|
||||
When a new commit is discovered, the branch is update with a new commit. Then this commit is added in a batch, a container for new builds when they arrive, but only if a trigger corresponding to this repo exists. After one minute without new commit update in the batch, the different triggers will create one build each.
|
||||
In this example, we want to create a new build when a new commit is pushed on runbot, and this builds needs a commit in odoo as a dependency.
|
||||
|
||||

|
||||
|
||||
Note that the used config is default. It is adviced to customize this config. In our example, adding */runbot,/runbot_cla* test-tags on config step *all* may be a good idea to speed up testing by skipping tests from dependencies.
|
||||
|
||||
When a branch is pushed, a new batch will be created, and after one minute the new build will be created if no other change is detected. The build remains in pending state for now. Check the result on 127.0.0.1:8069/runbot
|
||||
|
||||
#### Hosts
|
||||
Runbot is able to share pending builds accross multiple hosts. In the present case, there is only one. A new host will never assign pending build to himself by default.
|
||||
Go in the Build Hosts menu and chose yours. Uncheck *Only accept assigned build*. You can also tweak the number of parallel builds for this host.
|
||||
|
||||
### Modules filters
|
||||
Modules to install can be filtered by repo, and by config step. The first filter to be applied is the repo one, creating the default list for a config step.
|
||||
Addong -module on a repo will remove the module from the default, it is adviced to reflect the default case on repo. To test only a custom module, adding *-\** on odoo repo will disable all odoo addons. Only dependencies of custom modules will be installed. Some specific modules can also be filtered using *-module1,-module1* or somme specific modules can be kept using *-\*,module1,module2*
|
||||
Module can also be filtered on a config step with the same logic as repo filter, exept that all modules can be unblacklisted from repo by starting the list with *\** (all available modules)
|
||||
It is also possible to add test-tags to config step to allow more module to be installed but only testing some specific one. Test tags: */module1,/module2*
|
||||
|
||||
### db template
|
||||
Db creation will use template0 by default. It is possible to specify a specific template to use in runbot config *Postgresql template*. It is mainly use to add extension by default.
|
||||
|
||||
|
@ -2,39 +2,53 @@
|
||||
{
|
||||
'name': "runbot",
|
||||
'summary': "Runbot",
|
||||
'description': "Runbot for Odoo 11.0",
|
||||
'description': "Runbot for Odoo 13.0",
|
||||
'author': "Odoo SA",
|
||||
'website': "http://runbot.odoo.com",
|
||||
'category': 'Website',
|
||||
'version': '4.10',
|
||||
'depends': ['website', 'base'],
|
||||
'version': '5.0',
|
||||
'depends': ['base', 'base_automation', 'website'],
|
||||
'data': [
|
||||
'data/build_parse.xml',
|
||||
'data/error_link.xml',
|
||||
'data/runbot_build_config_data.xml',
|
||||
'data/runbot_data.xml',
|
||||
'data/runbot_error_regex_data.xml',
|
||||
'data/website_data.xml',
|
||||
|
||||
'security/runbot_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/ir.rule.csv',
|
||||
'views/assets.xml',
|
||||
'views/repo_views.xml',
|
||||
|
||||
'templates/assets.xml',
|
||||
'templates/badge.xml',
|
||||
'templates/batch.xml',
|
||||
'templates/branch.xml',
|
||||
'templates/build.xml',
|
||||
'templates/bundle.xml',
|
||||
'templates/commit.xml',
|
||||
'templates/dashboard.xml',
|
||||
'templates/frontend.xml',
|
||||
'templates/git.xml',
|
||||
'templates/nginx.xml',
|
||||
'templates/utils.xml',
|
||||
'templates/build_error.xml',
|
||||
|
||||
'views/branch_views.xml',
|
||||
'views/build_views.xml',
|
||||
'views/host_views.xml',
|
||||
'views/build_error_views.xml',
|
||||
'views/error_log_views.xml',
|
||||
'views/build_views.xml',
|
||||
'views/bundle_views.xml',
|
||||
'views/commit_views.xml',
|
||||
'views/config_views.xml',
|
||||
'views/error_log_views.xml',
|
||||
'views/host_views.xml',
|
||||
'views/repo_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/stat_views.xml',
|
||||
'views/upgrade.xml',
|
||||
'views/warning_views.xml',
|
||||
|
||||
'wizards/mutli_build_wizard_views.xml',
|
||||
'wizards/stat_regex_wizard_views.xml',
|
||||
'templates/frontend.xml',
|
||||
'templates/build.xml',
|
||||
'templates/assets.xml',
|
||||
'templates/dashboard.xml',
|
||||
'templates/nginx.xml',
|
||||
'templates/badge.xml',
|
||||
'templates/branch.xml',
|
||||
'data/runbot_build_config_data.xml',
|
||||
'data/build_parse.xml',
|
||||
'data/runbot_error_regex_data.xml',
|
||||
'data/error_link.xml',
|
||||
'data/website_data.xml',
|
||||
],
|
||||
}
|
||||
|
@ -3,11 +3,11 @@
|
||||
import contextlib
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import psycopg2
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import os
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
@ -19,29 +19,11 @@ from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
dest_reg = re.compile(r'^\d{5,}-.{1,32}-[\da-f]{6}(.*)*$')
|
||||
dest_reg = re.compile(r'^\d{5,}-.+$')
|
||||
|
||||
class Commit():
|
||||
def __init__(self, repo, sha):
|
||||
self.repo = repo
|
||||
self.sha = sha
|
||||
|
||||
def _source_path(self, *path):
|
||||
return self.repo._source_path(self.sha, *path)
|
||||
|
||||
def export(self):
|
||||
return self.repo._git_export(self.sha)
|
||||
|
||||
def read_source(self, file, mode='r'):
|
||||
file_path = self._source_path(file)
|
||||
try:
|
||||
with open(file_path, mode) as f:
|
||||
return f.read()
|
||||
except:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return '%s:%s' % (self.repo.short_name, self.sha)
|
||||
class RunbotException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def fqdn():
|
||||
@ -89,15 +71,29 @@ def rfind(filename, pattern):
|
||||
return False
|
||||
|
||||
|
||||
def time_delta(time):
|
||||
if isinstance(time, timedelta):
|
||||
return time
|
||||
return timedelta(seconds=-time)
|
||||
|
||||
|
||||
def s2human(time):
|
||||
"""Convert a time in second into an human readable string"""
|
||||
return format_timedelta(
|
||||
timedelta(seconds=time),
|
||||
time_delta(time),
|
||||
format="narrow",
|
||||
threshold=2.1,
|
||||
)
|
||||
|
||||
|
||||
def s2human_long(time):
|
||||
return format_timedelta(
|
||||
time_delta(time),
|
||||
threshold=2.1,
|
||||
add_direction=True, locale='en'
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def local_pgadmin_cursor():
|
||||
cnx = None
|
||||
|
@ -35,13 +35,15 @@ ENV COVERAGE_FILE /data/build/.coverage
|
||||
|
||||
|
||||
class Command():
|
||||
def __init__(self, pres, cmd, posts, finals=None, config_tuples=None):
|
||||
|
||||
def __init__(self, pres, cmd, posts, finals=None, config_tuples=None, cmd_checker=None):
|
||||
""" Command object that represent commands to run in Docker container
|
||||
:param pres: list of pre-commands
|
||||
:param cmd: list of main command only run if the pres commands succeed (&&)
|
||||
:param posts: list of post commands posts only run if the cmd command succedd (&&)
|
||||
:param finals: list of finals commands always executed
|
||||
:param config_tuples: list of key,value tuples to write in config file
|
||||
:param cmd_checker: a checker object that must have a `_cmd_check` method that will be called at build
|
||||
returns a string of the full command line to run
|
||||
"""
|
||||
self.pres = pres or []
|
||||
@ -49,6 +51,7 @@ class Command():
|
||||
self.posts = posts or []
|
||||
self.finals = finals or []
|
||||
self.config_tuples = config_tuples or []
|
||||
self.cmd_checker = cmd_checker
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.cmd, name)
|
||||
@ -57,7 +60,7 @@ class Command():
|
||||
return self.cmd[key]
|
||||
|
||||
def __add__(self, l):
|
||||
return Command(self.pres, self.cmd + l, self.posts, self.finals, self.config_tuples)
|
||||
return Command(self.pres, self.cmd + l, self.posts, self.finals, self.config_tuples, self.cmd_checker)
|
||||
|
||||
def __str__(self):
|
||||
return ' '.join(self)
|
||||
@ -66,6 +69,8 @@ class Command():
|
||||
return self.build().replace('&& ', '&&\n').replace('|| ', '||\n\t').replace(';', ';\n')
|
||||
|
||||
def build(self):
|
||||
if self.cmd_checker:
|
||||
self.cmd_checker._cmd_check(self)
|
||||
cmd_chain = []
|
||||
cmd_chain += [' '.join(pre) for pre in self.pres if pre]
|
||||
cmd_chain.append(' '.join(self))
|
||||
@ -95,6 +100,10 @@ class Command():
|
||||
|
||||
|
||||
def docker_build(log_path, build_dir):
|
||||
return _docker_build(log_path, build_dir)
|
||||
|
||||
|
||||
def _docker_build(log_path, build_dir):
|
||||
"""Build the docker image
|
||||
:param log_path: path to the logfile that will contain odoo stdout and stderr
|
||||
:param build_dir: the build directory that contains the Odoo sources to build.
|
||||
@ -111,7 +120,11 @@ def docker_build(log_path, build_dir):
|
||||
dbuild.wait()
|
||||
|
||||
|
||||
def docker_run(run_cmd, log_path, build_dir, container_name, exposed_ports=None, cpu_limit=None, preexec_fn=None, ro_volumes=None, env_variables=None):
|
||||
def docker_run(*args, **kwargs):
|
||||
return _docker_run(*args, **kwargs)
|
||||
|
||||
|
||||
def _docker_run(run_cmd, log_path, build_dir, container_name, exposed_ports=None, cpu_limit=None, preexec_fn=None, ro_volumes=None, env_variables=None):
|
||||
"""Run tests in a docker container
|
||||
:param run_cmd: command string to run in container
|
||||
:param log_path: path to the logfile that will contain odoo stdout and stderr
|
||||
@ -166,11 +179,16 @@ def docker_run(run_cmd, log_path, build_dir, container_name, exposed_ports=None,
|
||||
if cpu_limit:
|
||||
docker_command.extend(['--ulimit', 'cpu=%s' % int(cpu_limit)])
|
||||
docker_command.extend(['odoo:runbot_tests', '/bin/bash', '-c', "%s" % run_cmd])
|
||||
docker_run = subprocess.Popen(docker_command, stdout=logs, stderr=logs, preexec_fn=preexec_fn, close_fds=False, cwd=build_dir)
|
||||
subprocess.Popen(docker_command, stdout=logs, stderr=logs, preexec_fn=preexec_fn, close_fds=False, cwd=build_dir)
|
||||
_logger.info('Started Docker container %s', container_name)
|
||||
return
|
||||
|
||||
|
||||
def docker_stop(container_name, build_dir=None):
|
||||
return _docker_stop(container_name, build_dir)
|
||||
|
||||
|
||||
def _docker_stop(container_name, build_dir):
|
||||
"""Stops the container named container_name"""
|
||||
container_name = sanitize_container_name(container_name)
|
||||
_logger.info('Stopping container %s', container_name)
|
||||
@ -181,11 +199,13 @@ def docker_stop(container_name, build_dir=None):
|
||||
_logger.info('Stopping docker without defined build_dir')
|
||||
subprocess.run(['docker', 'stop', container_name])
|
||||
|
||||
|
||||
def docker_is_running(container_name):
|
||||
container_name = sanitize_container_name(container_name)
|
||||
dinspect = subprocess.run(['docker', 'container', 'inspect', container_name], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
||||
return True if dinspect.returncode == 0 else False
|
||||
|
||||
|
||||
def docker_state(container_name, build_dir):
|
||||
container_name = sanitize_container_name(container_name)
|
||||
started = os.path.exists(os.path.join(build_dir, 'start-%s' % container_name))
|
||||
@ -201,6 +221,7 @@ def docker_state(container_name, build_dir):
|
||||
|
||||
return 'UNKNOWN'
|
||||
|
||||
|
||||
def docker_clear_state(container_name, build_dir):
|
||||
"""Return True if container is still running"""
|
||||
container_name = sanitize_container_name(container_name)
|
||||
@ -209,6 +230,7 @@ def docker_clear_state(container_name, build_dir):
|
||||
if os.path.exists(os.path.join(build_dir, 'end-%s' % container_name)):
|
||||
os.remove(os.path.join(build_dir, 'end-%s' % container_name))
|
||||
|
||||
|
||||
def docker_get_gateway_ip():
|
||||
"""Return the host ip of the docker default bridge gateway"""
|
||||
docker_net_inspect = subprocess.run(['docker', 'network', 'inspect', 'bridge'], stdout=subprocess.PIPE)
|
||||
@ -220,7 +242,12 @@ def docker_get_gateway_ip():
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
|
||||
def docker_ps():
|
||||
return _docker_ps()
|
||||
|
||||
|
||||
def _docker_ps():
|
||||
"""Return a list of running containers names"""
|
||||
try:
|
||||
docker_ps = subprocess.run(['docker', 'ps', '--format', '{{.Names}}'], stderr=subprocess.DEVNULL, stdout=subprocess.PIPE)
|
||||
@ -229,7 +256,11 @@ def docker_ps():
|
||||
return []
|
||||
if docker_ps.returncode != 0:
|
||||
return []
|
||||
return docker_ps.stdout.decode().strip().split('\n')
|
||||
output = docker_ps.stdout.decode()
|
||||
if not output:
|
||||
return []
|
||||
return output.strip().split('\n')
|
||||
|
||||
|
||||
def build(args):
|
||||
"""Build container from CLI"""
|
||||
@ -345,6 +376,29 @@ def tests(args):
|
||||
docker_run(cmd.build(), logfile, args.build_dir, container_name, exposed_ports=[args.odoo_port, args.odoo_port + 1], cpu_limit=300)
|
||||
|
||||
|
||||
##############################################################################
|
||||
# Ugly monkey patch to set runbot in set runbot in testing mode
|
||||
# No Docker will be started, instead a fake docker_run function will be used
|
||||
##############################################################################
|
||||
|
||||
if os.environ.get('RUNBOT_MODE') == 'test':
|
||||
_logger.warning('Using Fake Docker')
|
||||
|
||||
def fake_docker_run(run_cmd, log_path, build_dir, container_name, exposed_ports=None, cpu_limit=None, preexec_fn=None, ro_volumes=None, env_variables=None, *args, **kwargs):
|
||||
_logger.info('Docker Fake Run: %s', run_cmd)
|
||||
open(os.path.join(build_dir, 'start-%s' % container_name), 'w').write('fake start\n')
|
||||
open(os.path.join(build_dir, 'end-%s' % container_name), 'w').write('fake end')
|
||||
with open(log_path, 'w') as log_file:
|
||||
log_file.write('Fake docker_run started\n')
|
||||
log_file.write('run_cmd: %s\n' % run_cmd)
|
||||
log_file.write('build_dir: %s\n' % container_name)
|
||||
log_file.write('container_name: %s\n' % container_name)
|
||||
log_file.write('.modules.loading: Modules loaded.\n')
|
||||
log_file.write('Initiating shutdown\n')
|
||||
|
||||
docker_run = fake_docker_run
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s: %(message)s')
|
||||
parser = argparse.ArgumentParser()
|
||||
|
@ -11,51 +11,47 @@ from odoo.http import request, route, Controller
|
||||
class RunbotBadge(Controller):
|
||||
|
||||
@route([
|
||||
'/runbot/badge/<int:repo_id>/<branch>.svg',
|
||||
'/runbot/badge/<any(default,flat):theme>/<int:repo_id>/<branch>.svg',
|
||||
'/runbot/badge/<int:repo_id>/<name>.svg',
|
||||
'/runbot/badge/trigger/<int:trigger_id>/<name>.svg',
|
||||
'/runbot/badge/<any(default,flat):theme>/<int:repo_id>/<name>.svg',
|
||||
'/runbot/badge/trigger/<any(default,flat):theme>/<int:trigger_id>/<name>.svg',
|
||||
], type="http", auth="public", methods=['GET', 'HEAD'])
|
||||
def badge(self, repo_id, branch, theme='default'):
|
||||
|
||||
domain = [('repo_id', '=', repo_id),
|
||||
('branch_id.branch_name', '=', branch),
|
||||
('branch_id.sticky', '=', True),
|
||||
('hidden', '=', False),
|
||||
('parent_id', '=', False),
|
||||
('global_state', 'in', ['testing', 'running', 'done']),
|
||||
('global_result', 'not in', ['skipped', 'manually_killed']),
|
||||
]
|
||||
|
||||
last_update = '__last_update'
|
||||
builds = request.env['runbot.build'].sudo().search_read(
|
||||
domain, ['global_state', 'global_result', 'build_age', last_update],
|
||||
order='id desc', limit=1)
|
||||
|
||||
if not builds:
|
||||
return request.not_found()
|
||||
|
||||
build = builds[0]
|
||||
etag = request.httprequest.headers.get('If-None-Match')
|
||||
retag = hashlib.md5(str(build[last_update]).encode()).hexdigest()
|
||||
|
||||
if etag == retag:
|
||||
return werkzeug.wrappers.Response(status=304)
|
||||
|
||||
if build['global_state'] in ('testing', 'waiting'):
|
||||
state = build['global_state']
|
||||
cache_factor = 1
|
||||
def badge(self, name, repo_id=False, trigger_id=False, theme='default'):
|
||||
if trigger_id:
|
||||
triggers = request.env['runbot.trigger'].browse(trigger_id)
|
||||
else:
|
||||
cache_factor = 2
|
||||
if build['global_result'] == 'ok':
|
||||
triggers = request.env['runbot.trigger'].search([('repo_ids', 'in', repo_id)])
|
||||
# -> hack to use repo. Would be better to change logic and use a trigger_id in params
|
||||
bundle = request.env['runbot.bundle'].search([('name', '=', name),
|
||||
('project_id', '=', request.env.ref('runbot.main_project').id)]) # WARNING no filter on project
|
||||
if not bundle or not triggers:
|
||||
return request.not_found()
|
||||
batch = request.env['runbot.batch'].search([
|
||||
('bundle_id', '=', bundle.id),
|
||||
('state', '=', 'done'),
|
||||
('category_id', '=', request.env.ref('runbot.default_category').id)
|
||||
], order='id desc', limit=1)
|
||||
|
||||
builds = batch.slot_ids.filtered(lambda s: s.trigger_id in triggers).mapped('build_id')
|
||||
if not builds:
|
||||
state = 'testing'
|
||||
else:
|
||||
result = builds.result_multi()
|
||||
if result == 'ok':
|
||||
state = 'success'
|
||||
elif build['global_result'] == 'warn':
|
||||
elif result == 'warn':
|
||||
state = 'warning'
|
||||
else:
|
||||
state = 'failed'
|
||||
|
||||
etag = request.httprequest.headers.get('If-None-Match')
|
||||
retag = hashlib.md5(state.encode()).hexdigest()
|
||||
if etag == retag:
|
||||
return werkzeug.wrappers.Response(status=304)
|
||||
|
||||
# from https://github.com/badges/shields/blob/master/colorscheme.json
|
||||
color = {
|
||||
'testing': "#dfb317",
|
||||
'waiting': "#dfb317",
|
||||
'success': "#4c1",
|
||||
'failed': "#e05d44",
|
||||
'warning': "#fe7d37",
|
||||
@ -75,13 +71,12 @@ class RunbotBadge(Controller):
|
||||
self.width = text_width(text) + 10
|
||||
|
||||
data = {
|
||||
'left': Text(branch, '#555'),
|
||||
'left': Text(name, '#555'),
|
||||
'right': Text(state, color),
|
||||
}
|
||||
five_minutes = 5 * 60
|
||||
headers = [
|
||||
('Content-Type', 'image/svg+xml'),
|
||||
('Cache-Control', 'max-age=%d' % (five_minutes * cache_factor,)),
|
||||
('Cache-Control', 'max-age=%d' % (10*60,)),
|
||||
('ETag', retag),
|
||||
]
|
||||
return request.render("runbot.badge_" + theme, data, headers=headers)
|
||||
|
@ -1,19 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import operator
|
||||
import datetime
|
||||
import werkzeug
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import functools
|
||||
|
||||
import werkzeug.utils
|
||||
import werkzeug.urls
|
||||
|
||||
from werkzeug.exceptions import NotFound, Forbidden
|
||||
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.addons.website.controllers.main import QueryURL
|
||||
|
||||
from odoo.http import Controller, request, route
|
||||
from ..common import uniq_list, flatten, fqdn
|
||||
from odoo.http import Controller, Response, request, route as o_route
|
||||
from odoo.osv import expression
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def route(routes, **kw):
|
||||
def decorator(f):
|
||||
@o_route(routes, **kw)
|
||||
@functools.wraps(f)
|
||||
def response_wrap(*args, **kwargs):
|
||||
projects = request.env['runbot.project'].search([])
|
||||
more = request.httprequest.cookies.get('more', False) == '1'
|
||||
filter_mode = request.httprequest.cookies.get('filter_mode', 'all')
|
||||
keep_search = request.httprequest.cookies.get('keep_search', False) == '1'
|
||||
cookie_search = request.httprequest.cookies.get('search', '')
|
||||
refresh = kwargs.get('refresh', False)
|
||||
nb_build_errors = request.env['runbot.build.error'].search_count([('random', '=', True), ('parent_id', '=', False)])
|
||||
nb_assigned_errors = request.env['runbot.build.error'].search_count([('responsible', '=', request.env.user.id)])
|
||||
kwargs['more'] = more
|
||||
kwargs['projects'] = projects
|
||||
|
||||
response = f(*args, **kwargs)
|
||||
if isinstance(response, Response):
|
||||
if keep_search and cookie_search and 'search' not in kwargs:
|
||||
search = cookie_search
|
||||
else:
|
||||
search = kwargs.get('search', '')
|
||||
if keep_search and cookie_search != search:
|
||||
response.set_cookie('search', search)
|
||||
|
||||
project = response.qcontext.get('project') or projects[0]
|
||||
|
||||
response.qcontext['projects'] = projects
|
||||
response.qcontext['more'] = more
|
||||
response.qcontext['keep_search'] = keep_search
|
||||
response.qcontext['search'] = search
|
||||
response.qcontext['current_path'] = request.httprequest.full_path
|
||||
response.qcontext['refresh'] = refresh
|
||||
response.qcontext['filter_mode'] = filter_mode
|
||||
response.qcontext['qu'] = QueryURL('/runbot/%s' % (slug(project)), path_args=['search'], search=search, refresh=refresh)
|
||||
if 'title' not in response.qcontext:
|
||||
response.qcontext['title'] = 'Runbot %s' % project.name or ''
|
||||
response.qcontext['nb_build_errors'] = nb_build_errors
|
||||
response.qcontext['nb_assigned_errors'] = nb_assigned_errors
|
||||
|
||||
return response
|
||||
return response_wrap
|
||||
return decorator
|
||||
|
||||
|
||||
class Runbot(Controller):
|
||||
|
||||
@ -26,354 +74,293 @@ class Runbot(Controller):
|
||||
level = ['info', 'warning', 'danger'][int(pending_count > warn) + int(pending_count > crit)]
|
||||
return pending_count, level, scheduled_count
|
||||
|
||||
@route(['/runbot', '/runbot/repo/<model("runbot.repo"):repo>'], website=True, auth='public', type='http')
|
||||
def repo(self, repo=None, search='', refresh='', **kwargs):
|
||||
@o_route([
|
||||
'/runbot/submit'
|
||||
], type='http', auth="public", methods=['GET', 'POST'], csrf=False)
|
||||
def submit(self, more=False, redirect='/', keep_search=False, category=False, filter_mode=False, update_triggers=False, **kwargs):
|
||||
response = werkzeug.utils.redirect(redirect)
|
||||
response.set_cookie('more', '1' if more else '0')
|
||||
response.set_cookie('keep_search', '1' if keep_search else '0')
|
||||
response.set_cookie('filter_mode', filter_mode or 'all')
|
||||
response.set_cookie('category', category or '0')
|
||||
if update_triggers:
|
||||
enabled_triggers = []
|
||||
project_id = int(update_triggers)
|
||||
for key in kwargs.keys():
|
||||
if key.startswith('trigger_'):
|
||||
enabled_triggers.append(key.replace('trigger_', ''))
|
||||
|
||||
key = 'trigger_display_%s' % project_id
|
||||
if len(request.env['runbot.trigger'].search([('project_id', '=', project_id)])) == len(enabled_triggers):
|
||||
response.delete_cookie(key)
|
||||
else:
|
||||
response.set_cookie(key, '-'.join(enabled_triggers))
|
||||
return response
|
||||
|
||||
@route(['/',
|
||||
'/runbot',
|
||||
'/runbot/<model("runbot.project"):project>',
|
||||
'/runbot/<model("runbot.project"):project>/search/<search>'], website=True, auth='public', type='http')
|
||||
def bundles(self, project=None, search='', projects=False, refresh=False, **kwargs):
|
||||
search = search if len(search) < 60 else search[:60]
|
||||
branch_obj = request.env['runbot.branch']
|
||||
build_obj = request.env['runbot.build']
|
||||
repo_obj = request.env['runbot.repo']
|
||||
env = request.env
|
||||
categories = env['runbot.category'].search([])
|
||||
if not project and projects:
|
||||
project = projects[0]
|
||||
|
||||
repo_ids = repo_obj.search([])
|
||||
repos = repo_obj.browse(repo_ids)
|
||||
if not repo and repos:
|
||||
repo = repos[0].id
|
||||
|
||||
pending = self._pending()
|
||||
pending_count, level, scheduled_count = self._pending()
|
||||
context = {
|
||||
'repos': repos.ids,
|
||||
'repo': repo,
|
||||
'host_stats': [],
|
||||
'pending_total': pending[0],
|
||||
'pending_level': pending[1],
|
||||
'scheduled_count': pending[2],
|
||||
'hosts_data': request.env['runbot.host'].search([]),
|
||||
'categories': categories,
|
||||
'search': search,
|
||||
'refresh': refresh,
|
||||
'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message'),
|
||||
'pending_total': pending_count,
|
||||
'pending_level': level,
|
||||
'scheduled_count': scheduled_count,
|
||||
'hosts_data': request.env['runbot.host'].search([]),
|
||||
}
|
||||
if project:
|
||||
domain = [('last_batch', '!=', False), ('project_id', '=', project.id), ('no_build', '=', False)]
|
||||
|
||||
filter_mode = request.httprequest.cookies.get('filter_mode', False)
|
||||
if filter_mode == 'sticky':
|
||||
domain.append(('sticky', '=', True))
|
||||
elif filter_mode == 'nosticky':
|
||||
domain.append(('sticky', '=', False))
|
||||
|
||||
build_ids = []
|
||||
if repo:
|
||||
domain = [('repo_id', '=', repo.id)]
|
||||
if search:
|
||||
search_domain = []
|
||||
for to_search in search.split("|"):
|
||||
search_domain = ['|', '|', '|'] + search_domain
|
||||
search_domain += [('dest', 'ilike', to_search), ('subject', 'ilike', to_search), ('branch_id.branch_name', 'ilike', to_search)]
|
||||
domain += search_domain[1:]
|
||||
domain = expression.AND([domain, [('hidden', '=', False)]]) # don't display children builds on repo view
|
||||
build_ids = build_obj.search(domain, limit=100)
|
||||
branch_ids, build_by_branch_ids = [], {}
|
||||
search_domains = []
|
||||
pr_numbers = []
|
||||
for search_elem in search.split("|"):
|
||||
if search_elem.isnumeric():
|
||||
pr_numbers.append(int(search_elem))
|
||||
else:
|
||||
search_domains.append([('name', 'like', search_elem)])
|
||||
if pr_numbers:
|
||||
res = request.env['runbot.branch'].search([('name', 'in', pr_numbers)])
|
||||
if res:
|
||||
search_domains.append([('id', 'in', res.mapped('bundle_id').ids)])
|
||||
search_domain = expression.OR(search_domains)
|
||||
print(search_domain)
|
||||
domain = expression.AND([domain, search_domain])
|
||||
|
||||
if build_ids:
|
||||
branch_query = """
|
||||
SELECT br.id FROM runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id WHERE bu.id in %s
|
||||
ORDER BY bu.sequence DESC
|
||||
"""
|
||||
sticky_dom = [('repo_id', '=', repo.id), ('sticky', '=', True)]
|
||||
sticky_branch_ids = [] if search else branch_obj.search(sticky_dom).sorted(key=lambda b: (b.branch_name == 'master', b.id), reverse=True).ids
|
||||
request._cr.execute(branch_query, (tuple(build_ids.ids),))
|
||||
branch_ids = uniq_list(sticky_branch_ids + [br[0] for br in request._cr.fetchall()])
|
||||
e = expression.expression(domain, request.env['runbot.bundle'])
|
||||
where_clause, where_params = e.to_sql()
|
||||
|
||||
build_query = """
|
||||
SELECT
|
||||
branch_id,
|
||||
max(case when br_bu.row = 1 then br_bu.build_id end),
|
||||
max(case when br_bu.row = 2 then br_bu.build_id end),
|
||||
max(case when br_bu.row = 3 then br_bu.build_id end),
|
||||
max(case when br_bu.row = 4 then br_bu.build_id end)
|
||||
FROM (
|
||||
SELECT
|
||||
br.id AS branch_id,
|
||||
bu.id AS build_id,
|
||||
row_number() OVER (PARTITION BY branch_id) AS row
|
||||
FROM
|
||||
runbot_branch br INNER JOIN runbot_build bu ON br.id=bu.branch_id
|
||||
WHERE
|
||||
br.id in %s AND (bu.hidden = 'f' OR bu.hidden IS NULL)
|
||||
GROUP BY br.id, bu.id
|
||||
ORDER BY br.id, bu.id DESC
|
||||
) AS br_bu
|
||||
WHERE
|
||||
row <= 4
|
||||
GROUP BY br_bu.branch_id;
|
||||
"""
|
||||
request._cr.execute(build_query, (tuple(branch_ids),))
|
||||
build_by_branch_ids = {
|
||||
rec[0]: [r for r in rec[1:] if r is not None] for rec in request._cr.fetchall()
|
||||
}
|
||||
env.cr.execute("""
|
||||
SELECT id FROM runbot_bundle
|
||||
WHERE {where_clause}
|
||||
ORDER BY
|
||||
(case when sticky then 1 when sticky is null then 2 else 2 end),
|
||||
case when sticky then version_number end collate "C" desc,
|
||||
last_batch desc
|
||||
LIMIT 40""".format(where_clause=where_clause), where_params)
|
||||
bundles = env['runbot.bundle'].browse([r[0] for r in env.cr.fetchall()])
|
||||
|
||||
branches = branch_obj.browse(branch_ids)
|
||||
build_ids = flatten(build_by_branch_ids.values())
|
||||
build_dict = {build.id: build for build in build_obj.browse(build_ids)}
|
||||
category_id = int(request.httprequest.cookies.get('category') or 0) or request.env['ir.model.data'].xmlid_to_res_id('runbot.default_category')
|
||||
|
||||
def branch_info(branch):
|
||||
return {
|
||||
'branch': branch,
|
||||
'builds': [build_dict[build_id] for build_id in build_by_branch_ids.get(branch.id) or []]
|
||||
}
|
||||
trigger_display = request.httprequest.cookies.get('trigger_display_%s' % project.id, None)
|
||||
if trigger_display is not None:
|
||||
trigger_display = [int(td) for td in trigger_display.split('-') if td]
|
||||
bundles = bundles.with_context(category_id=category_id)
|
||||
|
||||
triggers = env['runbot.trigger'].search([('project_id', '=', project.id)])
|
||||
context.update({
|
||||
'branches': [branch_info(b) for b in branches],
|
||||
'qu': QueryURL('/runbot/repo/' + slug(repo), search=search, refresh=refresh),
|
||||
'fqdn': fqdn(),
|
||||
'active_category_id': category_id,
|
||||
'bundles': bundles,
|
||||
'project': project,
|
||||
'triggers': triggers,
|
||||
'trigger_display': trigger_display,
|
||||
})
|
||||
|
||||
# consider host gone if no build in last 100
|
||||
build_threshold = max(build_ids or [0]) - 100
|
||||
|
||||
context.update({'message': request.env['ir.config_parameter'].sudo().get_param('runbot.runbot_message')})
|
||||
return request.render('runbot.repo', context)
|
||||
res = request.render('runbot.bundles', context)
|
||||
return res
|
||||
|
||||
@route([
|
||||
'/runbot/bundle/<model("runbot.bundle"):bundle>',
|
||||
'/runbot/bundle/<model("runbot.bundle"):bundle>/page/<int:page>'
|
||||
], website=True, auth='public', type='http')
|
||||
def bundle(self, bundle=None, page=1, limit=50, **kwargs):
|
||||
domain = [('bundle_id', '=', bundle.id), ('hidden', '=', False)]
|
||||
batch_count = request.env['runbot.batch'].search_count(domain)
|
||||
pager = request.website.pager(
|
||||
url='/runbot/bundle/%s' % bundle.id,
|
||||
total=batch_count,
|
||||
page=page,
|
||||
step=50,
|
||||
)
|
||||
batchs = request.env['runbot.batch'].search(domain, limit=limit, offset=pager.get('offset', 0), order='id desc')
|
||||
|
||||
context = {
|
||||
'bundle': bundle,
|
||||
'batchs': batchs,
|
||||
'pager': pager,
|
||||
'project': bundle.project_id,
|
||||
'title': 'Bundle %s' % bundle.name
|
||||
}
|
||||
|
||||
return request.render('runbot.bundle', context)
|
||||
|
||||
@o_route([
|
||||
'/runbot/bundle/<model("runbot.bundle"):bundle>/force',
|
||||
'/runbot/bundle/<model("runbot.bundle"):bundle>/force/<int:auto_rebase>',
|
||||
], type='http', auth="user", methods=['GET', 'POST'], csrf=False)
|
||||
def force_bundle(self, bundle, auto_rebase=False, **post):
|
||||
_logger.info('user %s forcing bundle %s', request.env.user.name, bundle.name) # user must be able to read bundle
|
||||
batch = bundle.sudo()._force(auto_rebase=auto_rebase)
|
||||
return werkzeug.utils.redirect('/runbot/batch/%s' % batch.id)
|
||||
|
||||
@route(['/runbot/batch/<int:batch_id>'], website=True, auth='public', type='http')
|
||||
def batch(self, batch_id=None, **kwargs):
|
||||
batch = request.env['runbot.batch'].browse(batch_id)
|
||||
context = {
|
||||
'batch': batch,
|
||||
'project': batch.bundle_id.project_id,
|
||||
'title': 'Batch %s (%s)' % (batch.id, batch.bundle_id.name)
|
||||
}
|
||||
return request.render('runbot.batch', context)
|
||||
|
||||
@o_route(['/runbot/batch/slot/<model("runbot.batch.slot"):slot>/build'], auth='user', type='http')
|
||||
def slot_create_build(self, slot=None, **kwargs):
|
||||
build = slot.sudo()._create_missing_build()
|
||||
return werkzeug.utils.redirect('/runbot/build/%s' % build.id)
|
||||
|
||||
@route(['/runbot/commit/<model("runbot.commit"):commit>'], website=True, auth='public', type='http')
|
||||
def commit(self, commit=None, **kwargs):
|
||||
status_list = request.env['runbot.commit.status'].search([('commit_id', '=', commit.id)], order='id desc')
|
||||
last_status_by_context = dict()
|
||||
for status in status_list:
|
||||
if status.context in last_status_by_context:
|
||||
continue
|
||||
last_status_by_context[status.context] = status
|
||||
context = {
|
||||
'commit': commit,
|
||||
'project': commit.repo_id.project_id,
|
||||
'reflogs': request.env['runbot.ref.log'].search([('commit_id', '=', commit.id)]),
|
||||
'status_list': status_list,
|
||||
'last_status_by_context': last_status_by_context,
|
||||
'title': 'Commit %s' % commit.name[:8]
|
||||
}
|
||||
return request.render('runbot.commit', context)
|
||||
|
||||
@o_route(['/runbot/commit/resend/<int:status_id>'], website=True, auth='user', type='http')
|
||||
def resend_status(self, status_id=None, **kwargs):
|
||||
CommitStatus = request.env['runbot.commit.status']
|
||||
status = CommitStatus.browse(status_id)
|
||||
if not status.exists():
|
||||
raise NotFound()
|
||||
last_status = CommitStatus.search([('commit_id', '=', status.commit_id.id), ('context', '=', status.context)], order='id desc', limit=1)
|
||||
if status != last_status:
|
||||
raise Forbidden("Only the last status can be resent")
|
||||
if last_status.sent_date and (datetime.datetime.now() - last_status.sent_date).seconds > 60: # ensure at least 60sec between two resend
|
||||
new_status = status.sudo().copy()
|
||||
new_status.description = 'Status resent by %s' % request.env.user.name
|
||||
new_status._send()
|
||||
_logger.info('github status %s resent by %s', status_id, request.env.user.name)
|
||||
return werkzeug.utils.redirect('/runbot/commit/%s' % status.commit_id.id)
|
||||
|
||||
@o_route([
|
||||
'/runbot/build/<int:build_id>/<operation>',
|
||||
'/runbot/build/<int:build_id>/<operation>/<int:exact>',
|
||||
], type='http', auth="public", methods=['POST'], csrf=False)
|
||||
def build_force(self, build_id, operation, exact=0, search=None, **post):
|
||||
def build_operations(self, build_id, operation, **post):
|
||||
build = request.env['runbot.build'].sudo().browse(build_id)
|
||||
if operation == 'force':
|
||||
build = build._force(exact=bool(exact))
|
||||
if operation == 'rebuild':
|
||||
build = build._rebuild()
|
||||
elif operation == 'kill':
|
||||
build._ask_kill()
|
||||
elif operation == 'wakeup':
|
||||
build._wake_up()
|
||||
|
||||
qs = ''
|
||||
if search:
|
||||
qs = '?' + werkzeug.urls.url_encode({'search': search})
|
||||
return werkzeug.utils.redirect(build.build_url + qs)
|
||||
return werkzeug.utils.redirect(build.build_url)
|
||||
|
||||
@route(['/runbot/build/<int:build_id>'], type='http', auth="public", website=True)
|
||||
def build(self, build_id, search=None, **post):
|
||||
"""Events/Logs"""
|
||||
|
||||
Build = request.env['runbot.build']
|
||||
Logging = request.env['ir.logging']
|
||||
|
||||
build = Build.browse([build_id])[0]
|
||||
if not build.exists():
|
||||
return request.not_found()
|
||||
|
||||
show_rebuild_button = Build.search([('branch_id', '=', build.branch_id.id), ('parent_id', '=', False)], limit=1) == build
|
||||
|
||||
context = {
|
||||
'repo': build.repo_id,
|
||||
'build': build,
|
||||
'fqdn': fqdn(),
|
||||
'br': {'branch': build.branch_id},
|
||||
'show_rebuild_button': show_rebuild_button,
|
||||
'default_category': request.env['ir.model.data'].xmlid_to_res_id('runbot.default_category'),
|
||||
'project': build.params_id.trigger_id.project_id,
|
||||
'title': 'Build %s' % build.id
|
||||
}
|
||||
return request.render("runbot.build", context)
|
||||
|
||||
@route(['/runbot/quick_connect/<model("runbot.branch"):branch>'], type='http', auth="public", website=True)
|
||||
def fast_launch(self, branch, **post):
|
||||
"""Connect to the running Odoo instance"""
|
||||
Build = request.env['runbot.build']
|
||||
domain = [('branch_id', '=', branch.id), ('config_id', '=', branch.config_id.id)]
|
||||
|
||||
# Take the 10 lasts builds to find at least 1 running... Else no luck
|
||||
builds = Build.search(domain, order='sequence desc', limit=10)
|
||||
|
||||
if builds:
|
||||
last_build = False
|
||||
for build in builds:
|
||||
if build.real_build.local_state == 'running':
|
||||
last_build = build.real_build
|
||||
break
|
||||
|
||||
if not last_build:
|
||||
# Find the last build regardless the state to propose a rebuild
|
||||
last_build = builds[0]
|
||||
|
||||
if last_build.local_state != 'running':
|
||||
url = "/runbot/build/%s?ask_rebuild=1" % last_build.id
|
||||
else:
|
||||
url = "http://%s/web/login?db=%s-all&login=admin&redirect=/web?debug=1" % (last_build.domain, last_build.dest)
|
||||
else:
|
||||
return request.not_found()
|
||||
return werkzeug.utils.redirect(url)
|
||||
|
||||
@route(['/runbot/dashboard'], type='http', auth="public", website=True)
|
||||
def dashboard(self, refresh=None):
|
||||
cr = request.cr
|
||||
RB = request.env['runbot.build']
|
||||
repos = request.env['runbot.repo'].search([]) # respect record rules
|
||||
|
||||
cr.execute("""SELECT bu.id
|
||||
FROM runbot_branch br
|
||||
JOIN LATERAL (SELECT *
|
||||
FROM runbot_build bu
|
||||
WHERE bu.branch_id = br.id
|
||||
ORDER BY id DESC
|
||||
LIMIT 3
|
||||
) bu ON (true)
|
||||
JOIN runbot_repo r ON (r.id = br.repo_id)
|
||||
WHERE br.sticky
|
||||
AND br.repo_id in %s
|
||||
ORDER BY r.sequence, r.name, br.branch_name, bu.id DESC
|
||||
""", [tuple(repos._ids)])
|
||||
|
||||
builds = RB.browse(map(operator.itemgetter(0), cr.fetchall()))
|
||||
|
||||
count = RB.search_count
|
||||
pending = self._pending()
|
||||
qctx = {
|
||||
'refresh': refresh,
|
||||
'host_stats': [],
|
||||
'pending_total': pending[0],
|
||||
'pending_level': pending[1],
|
||||
@route([
|
||||
'/runbot/branch/<model("runbot.branch"):branch>',
|
||||
], website=True, auth='public', type='http')
|
||||
def branch(self, branch=None, **kwargs):
|
||||
pr_branch = branch.bundle_id.branch_ids.filtered(lambda rec: not rec.is_pr and rec.id != branch.id and rec.remote_id.repo_id == branch.remote_id.repo_id)[:1]
|
||||
branch_pr = branch.bundle_id.branch_ids.filtered(lambda rec: rec.is_pr and rec.id != branch.id and rec.remote_id.repo_id == branch.remote_id.repo_id)[:1]
|
||||
context = {
|
||||
'branch': branch,
|
||||
'project': branch.remote_id.repo_id.project_id,
|
||||
'title': 'Branch %s' % branch.name,
|
||||
'pr_branch': pr_branch,
|
||||
'branch_pr': branch_pr
|
||||
}
|
||||
|
||||
repos_values = qctx['repo_dict'] = OrderedDict()
|
||||
for build in builds:
|
||||
repo = build.repo_id
|
||||
branch = build.branch_id
|
||||
r = repos_values.setdefault(repo.id, {'branches': OrderedDict()})
|
||||
if 'name' not in r:
|
||||
r.update({
|
||||
'name': repo.name,
|
||||
'base': repo.base,
|
||||
'testing': count([('repo_id', '=', repo.id), ('local_state', '=', 'testing')]),
|
||||
'running': count([('repo_id', '=', repo.id), ('local_state', '=', 'running')]),
|
||||
'pending': count([('repo_id', '=', repo.id), ('local_state', '=', 'pending')]),
|
||||
})
|
||||
b = r['branches'].setdefault(branch.id, {'name': branch.branch_name, 'builds': list()})
|
||||
b['builds'].append(build)
|
||||
return request.render('runbot.branch', context)
|
||||
|
||||
# consider host gone if no build in last 100
|
||||
build_threshold = max(builds.ids or [0]) - 100
|
||||
for result in RB.read_group([('id', '>', build_threshold)], ['host'], ['host']):
|
||||
if result['host']:
|
||||
qctx['host_stats'].append({
|
||||
'fqdn': fqdn(),
|
||||
'host': result['host'],
|
||||
'testing': count([('local_state', '=', 'testing'), ('host', '=', result['host'])]),
|
||||
'running': count([('local_state', '=', 'running'), ('host', '=', result['host'])]),
|
||||
})
|
||||
|
||||
return request.render("runbot.sticky-dashboard", qctx)
|
||||
|
||||
def _glances_ctx(self):
|
||||
repos = request.env['runbot.repo'].search([]) # respect record rules
|
||||
default_config_id = request.env.ref('runbot.runbot_build_config_default').id
|
||||
query = """
|
||||
SELECT split_part(r.name, ':', 2),
|
||||
br.branch_name,
|
||||
(array_agg(bu.global_result order by bu.id desc))[1]
|
||||
FROM runbot_build bu
|
||||
JOIN runbot_branch br on (br.id = bu.branch_id)
|
||||
JOIN runbot_repo r on (r.id = br.repo_id)
|
||||
WHERE br.sticky
|
||||
AND br.repo_id in %s
|
||||
AND (bu.hidden = 'f' OR bu.hidden IS NULL)
|
||||
AND (
|
||||
bu.global_state in ('running', 'done')
|
||||
)
|
||||
AND bu.global_result not in ('skipped', 'manually_killed')
|
||||
AND (bu.config_id = r.repo_config_id
|
||||
OR bu.config_id = br.branch_config_id
|
||||
OR bu.config_id = %s)
|
||||
GROUP BY 1,2,r.sequence,br.id
|
||||
ORDER BY r.sequence, (br.branch_name='master'), br.id
|
||||
"""
|
||||
cr = request.env.cr
|
||||
cr.execute(query, (tuple(repos.ids), default_config_id))
|
||||
ctx = OrderedDict()
|
||||
for row in cr.fetchall():
|
||||
ctx.setdefault(row[0], []).append(row[1:])
|
||||
return ctx
|
||||
|
||||
@route('/runbot/glances', type='http', auth='public', website=True)
|
||||
def glances(self, refresh=None):
|
||||
glances_ctx = self._glances_ctx()
|
||||
@route([
|
||||
'/runbot/glances',
|
||||
'/runbot/glances/<int:project_id>'
|
||||
], type='http', auth='public', website=True)
|
||||
def glances(self, project_id=None, **kwargs):
|
||||
project_ids = [project_id] if project_id else request.env['runbot.project'].search([]).ids # search for access rights
|
||||
bundles = request.env['runbot.bundle'].search([('sticky', '=', True), ('project_id', 'in', project_ids)])
|
||||
pending = self._pending()
|
||||
qctx = {
|
||||
'refresh': refresh,
|
||||
'pending_total': pending[0],
|
||||
'pending_level': pending[1],
|
||||
'glances_data': glances_ctx,
|
||||
'bundles': bundles,
|
||||
'title': 'Glances'
|
||||
}
|
||||
return request.render("runbot.glances", qctx)
|
||||
|
||||
@route(['/runbot/monitoring',
|
||||
'/runbot/monitoring/<int:config_id>',
|
||||
'/runbot/monitoring/<int:config_id>/<int:view_id>'], type='http', auth='user', website=True)
|
||||
def monitoring(self, config_id=None, view_id=None, refresh=None, **kwargs):
|
||||
glances_ctx = self._glances_ctx()
|
||||
'/runbot/monitoring/<int:category_id>',
|
||||
'/runbot/monitoring/<int:category_id>/<int:view_id>'], type='http', auth='user', website=True)
|
||||
def monitoring(self, category_id=None, view_id=None, **kwargs):
|
||||
pending = self._pending()
|
||||
hosts_data = request.env['runbot.host'].search([])
|
||||
|
||||
last_monitored = None
|
||||
|
||||
monitored_config_id = config_id or int(request.env['ir.config_parameter'].sudo().get_param('runbot.monitored_config_id', 1))
|
||||
request.env.cr.execute("""SELECT DISTINCT ON (branch_id) branch_id, id FROM runbot_build
|
||||
WHERE config_id = %s
|
||||
AND global_state in ('running', 'done')
|
||||
AND branch_id in (SELECT id FROM runbot_branch where sticky='t')
|
||||
AND local_state != 'duplicate'
|
||||
AND hidden = false
|
||||
ORDER BY branch_id ASC, id DESC""", [int(monitored_config_id)])
|
||||
last_monitored = request.env['runbot.build'].browse([r[1] for r in request.env.cr.fetchall()])
|
||||
|
||||
config = request.env['runbot.build.config'].browse(monitored_config_id)
|
||||
if category_id:
|
||||
category = request.env['runbot.category'].browse(category_id)
|
||||
assert category.exists()
|
||||
else:
|
||||
category = request.env.ref('runbot.nightly_category')
|
||||
category_id = category.id
|
||||
bundles = request.env['runbot.bundle'].search([('sticky', '=', True)]) # NOTE we dont filter on project
|
||||
qctx = {
|
||||
'config': config,
|
||||
'refresh': refresh,
|
||||
'category': category,
|
||||
'pending_total': pending[0],
|
||||
'pending_level': pending[1],
|
||||
'scheduled_count': pending[2],
|
||||
'glances_data': glances_ctx,
|
||||
'bundles': bundles,
|
||||
'hosts_data': hosts_data,
|
||||
'last_monitored': last_monitored, # nightly
|
||||
'auto_tags': request.env['runbot.build.error'].disabling_tags(),
|
||||
'build_errors': request.env['runbot.build.error'].search([('random', '=', True)]),
|
||||
'kwargs': kwargs
|
||||
'kwargs': kwargs,
|
||||
'title': 'monitoring'
|
||||
}
|
||||
return request.render(view_id if view_id else config.monitoring_view_id.id or "runbot.monitoring", qctx)
|
||||
return request.render(view_id if view_id else "runbot.monitoring", qctx)
|
||||
|
||||
@route(['/runbot/config/<int:config_id>',
|
||||
'/runbot/config/<config_name>'], type='http', auth="public", website=True)
|
||||
def config(self, config_id=None, config_name=None, refresh=None, **kwargs):
|
||||
@route(['/runbot/errors',
|
||||
'/runbot/errors/<int:error_id>'], type='http', auth='user', website=True)
|
||||
def build_errors(self, error_id=None, **kwargs):
|
||||
build_errors = request.env['runbot.build.error'].search([('random', '=', True), ('parent_id', '=', False), ('responsible', '!=', request.env.user.id)]).filtered(lambda rec: len(rec.children_build_ids) > 1)
|
||||
assigned_errors = request.env['runbot.build.error'].search([('responsible', '=', request.env.user.id)])
|
||||
build_errors = build_errors.sorted(lambda rec: (rec.last_seen_date.date(), rec.build_count), reverse=True)
|
||||
assigned_errors = assigned_errors.sorted(lambda rec: (rec.last_seen_date.date(), rec.build_count), reverse=True)
|
||||
build_errors = assigned_errors + build_errors
|
||||
|
||||
if config_id:
|
||||
monitored_config_id = config_id
|
||||
else:
|
||||
config = request.env['runbot.build.config'].search([('name', '=', config_name)], limit=1)
|
||||
if config:
|
||||
monitored_config_id = config.id
|
||||
else:
|
||||
raise UserError('Config name not found')
|
||||
|
||||
readable_repos = request.env['runbot.repo'].search([])
|
||||
request.env.cr.execute("""SELECT DISTINCT ON (branch_id) branch_id, id FROM runbot_build
|
||||
WHERE config_id = %s
|
||||
AND global_state in ('running', 'done')
|
||||
AND branch_id in (SELECT id FROM runbot_branch where sticky='t' and repo_id in %s)
|
||||
AND local_state != 'duplicate'
|
||||
AND hidden = false
|
||||
ORDER BY branch_id ASC, id DESC""", [int(monitored_config_id), tuple(readable_repos.ids)])
|
||||
last_monitored = request.env['runbot.build'].browse([r[1] for r in request.env.cr.fetchall()])
|
||||
|
||||
config = request.env['runbot.build.config'].browse(monitored_config_id)
|
||||
qctx = {
|
||||
'config': config,
|
||||
'refresh': refresh,
|
||||
'last_monitored': last_monitored, # nightly
|
||||
'kwargs': kwargs
|
||||
'build_errors': build_errors,
|
||||
'title': 'Build Errors'
|
||||
}
|
||||
return request.render(config.monitoring_view_id.id or "runbot.config_monitoring", qctx)
|
||||
|
||||
@route(['/runbot/branch/<int:branch_id>', '/runbot/branch/<int:branch_id>/page/<int:page>'], website=True, auth='public', type='http')
|
||||
def branch_builds(self, branch_id=None, search='', page=1, limit=50, refresh='', **kwargs):
|
||||
""" list builds of a runbot branch """
|
||||
domain =[('branch_id','=',branch_id), ('hidden', '=', False)]
|
||||
builds_count = request.env['runbot.build'].search_count(domain)
|
||||
pager = request.website.pager(
|
||||
url='/runbot/branch/%s' % branch_id,
|
||||
total=builds_count,
|
||||
page=page,
|
||||
step=50,
|
||||
)
|
||||
builds = request.env['runbot.build'].search(domain, limit=limit, offset=pager.get('offset',0))
|
||||
|
||||
context = {'pager': pager, 'builds': builds, 'repo': request.env['runbot.branch'].browse(branch_id).repo_id}
|
||||
return request.render("runbot.branch", context)
|
||||
return request.render('runbot.build_error', qctx)
|
||||
|
@ -4,39 +4,63 @@ import time
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import http, tools
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class RunbotHook(http.Controller):
|
||||
|
||||
@http.route(['/runbot/hook/<int:repo_id>', '/runbot/hook/org'], type='http', auth="public", website=True, csrf=False)
|
||||
def hook(self, repo_id=None, **post):
|
||||
class Hook(http.Controller):
|
||||
|
||||
@http.route(['/runbot/hook/<int:remote_id>', '/runbot/hook/org'], type='http', auth="public", website=True, csrf=False)
|
||||
def hook(self, remote_id=None, **post):
|
||||
event = request.httprequest.headers.get("X-Github-Event")
|
||||
payload = json.loads(request.params.get('payload', '{}'))
|
||||
if repo_id is None:
|
||||
if remote_id is None:
|
||||
repo_data = payload.get('repository')
|
||||
if repo_data and event in ['push', 'pull_request']:
|
||||
repo_domain = [
|
||||
remote_domain = [
|
||||
'|', '|', ('name', '=', repo_data['ssh_url']),
|
||||
('name', '=', repo_data['clone_url']),
|
||||
('name', '=', repo_data['clone_url'].rstrip('.git')),
|
||||
]
|
||||
repo = request.env['runbot.repo'].sudo().search(
|
||||
repo_domain, limit=1)
|
||||
repo_id = repo.id
|
||||
remote = request.env['runbot.remote'].sudo().search(
|
||||
remote_domain, limit=1)
|
||||
remote_id = remote.id
|
||||
|
||||
repo = request.env['runbot.repo'].sudo().browse([repo_id])
|
||||
remote = request.env['runbot.remote'].sudo().browse([remote_id])
|
||||
|
||||
# force update of dependencies to in case a hook is lost
|
||||
# force update of dependencies too in case a hook is lost
|
||||
if not payload or event == 'push' or (event == 'pull_request' and payload.get('action') in ('synchronize', 'opened', 'reopened')):
|
||||
(repo | repo.dependency_ids).set_hook_time(time.time())
|
||||
elif event == 'pull_request' and payload and payload.get('action', '') == 'edited' and 'base' in payload.get('changes'):
|
||||
# handle PR that have been re-targeted
|
||||
remote.repo_id.set_hook_time(time.time())
|
||||
elif event == 'pull_request':
|
||||
pr_number = payload.get('pull_request', {}).get('number', '')
|
||||
branch = request.env['runbot.branch'].sudo().search([('repo_id', '=', repo.id), ('name', '=', 'refs/pull/%s' % pr_number)])
|
||||
branch._get_branch_infos(payload.get('pull_request', {}))
|
||||
branch = request.env['runbot.branch'].sudo().search([('remote_id', '=', remote.id), ('name', '=', pr_number)])
|
||||
if payload and payload.get('action', '') == 'edited' and 'base' in payload.get('changes'):
|
||||
# handle PR that have been re-targeted
|
||||
branch._compute_branch_infos(payload.get('pull_request', {}))
|
||||
_logger.info('retargeting %s to %s', branch.name, branch.target_branch_name)
|
||||
base = request.env['runbot.bundle'].search([
|
||||
('name', '=', branch.target_branch_name),
|
||||
('is_base', '=', True),
|
||||
('project_id', '=', branch.remote_id.repo_id.project_id.id)
|
||||
])
|
||||
if base:
|
||||
_logger.info('Changing base of bundle %s to %s(%s)', branch.bundle_id, base.name, base.id)
|
||||
branch.bundle_id.defined_base_id = base.id
|
||||
# TODO remove all ci
|
||||
|
||||
elif payload.get('action') in ('deleted', 'closed'):
|
||||
_logger.info('Closing pr %s', branch.name)
|
||||
branch.alive = False
|
||||
else:
|
||||
_logger.debug('Ignoring unsupported pull request operation %s %s', event, payload.get('action', ''))
|
||||
elif event == 'delete':
|
||||
if payload.get('ref_type') == 'branch':
|
||||
branch_ref = payload.get('ref')
|
||||
_logger.info('Hook for branch deletion %s in repo %s', branch_ref, remote.repo_id.name)
|
||||
branch = request.env['runbot.branch'].sudo().search([('remote_id', '=', remote.id), ('name', '=', branch_ref)])
|
||||
branch.alive = False
|
||||
else:
|
||||
_logger.debug('Ignoring unsupported hook %s %s', event, payload.get('action', ''))
|
||||
return ""
|
||||
|
@ -3,7 +3,7 @@
|
||||
<data noupdate="1">
|
||||
<record id="runbot_build_config_step_test_base" model="runbot.build.config.step">
|
||||
<field name="name">base</field>
|
||||
<field name="install_modules">base</field>
|
||||
<field name="install_modules">-*,base</field>
|
||||
<field name="cpu_limit">600</field>
|
||||
<field name="test_enable" eval="False"/>
|
||||
<field name="protected" eval="True"/>
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
<record id="runbot_build_config_step_test_all" model="runbot.build.config.step">
|
||||
<field name="name">all</field>
|
||||
<field name="install_modules">*</field>
|
||||
<field name="install_modules"></field>
|
||||
<field name="test_enable" eval="True"/>
|
||||
<field name="protected" eval="True"/>
|
||||
<field name="default_sequence">20</field>
|
||||
@ -27,7 +27,6 @@
|
||||
|
||||
<record id="runbot_build_config_default" model="runbot.build.config">
|
||||
<field name="name">Default</field>
|
||||
<field name="update_github_state" eval="True"/>
|
||||
<field name="step_order_ids" eval="[(5,0,0),
|
||||
(0, 0, {'step_id': ref('runbot_build_config_step_test_base')}),
|
||||
(0, 0, {'step_id': ref('runbot_build_config_step_test_all')}),
|
||||
@ -37,7 +36,6 @@
|
||||
|
||||
<record id="runbot_build_config_default_no_run" model="runbot.build.config">
|
||||
<field name="name">Default no run</field>
|
||||
<field name="update_github_state" eval="True"/>
|
||||
<field name="step_order_ids" eval="[(5,0,0),
|
||||
(0, 0, {'step_id': ref('runbot_build_config_step_test_base')}),
|
||||
(0, 0, {'step_id': ref('runbot_build_config_step_test_all')})]"/>
|
||||
@ -54,7 +52,7 @@
|
||||
<!-- Coverage-->
|
||||
<record id="runbot_build_config_step_test_coverage" model="runbot.build.config.step">
|
||||
<field name="name">coverage</field>
|
||||
<field name="install_modules">*</field>
|
||||
<field name="install_modules"></field>
|
||||
<field name="cpu_limit">7000</field>
|
||||
<field name="test_enable" eval="True"/>
|
||||
<field name="coverage" eval="True"/>
|
||||
@ -75,7 +73,6 @@
|
||||
<field name="create_config_ids" eval="[(4, ref('runbot_build_config_light_test'))]"/>
|
||||
<field name="number_builds">20</field>
|
||||
<field name="protected" eval="True"/>
|
||||
<field name="force_build" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="runbot_build_config_multibuild" model="runbot.build.config">
|
||||
@ -84,10 +81,10 @@
|
||||
<field name="step_order_ids" eval="[(5,0,0), (0, 0, {'step_id': ref('runbot_build_config_step_create_light_multi')})]"/>
|
||||
<field name="protected" eval="True"/>
|
||||
</record>
|
||||
<!-- l10n todo check-->
|
||||
<!-- l10n -->
|
||||
<record id="runbot_build_config_step_test_l10n" model="runbot.build.config.step">
|
||||
<field name="name">l10n</field>
|
||||
<field name="install_modules">*</field>
|
||||
<field name="install_modules"></field>
|
||||
<field name="test_enable" eval="True"/>
|
||||
<field name="protected" eval="True"/>
|
||||
<field name="default_sequence">30</field>
|
||||
@ -104,7 +101,7 @@
|
||||
<!-- Click all-->
|
||||
<record id="runbot_build_config_step_test_click_all" model="runbot.build.config.step">
|
||||
<field name="name">clickall</field>
|
||||
<field name="install_modules">*</field>
|
||||
<field name="install_modules"></field>
|
||||
<field name="cpu_limit">5400</field>
|
||||
<field name="test_enable" eval="True"/>
|
||||
<field name="protected" eval="True"/>
|
||||
|
94
runbot/data/runbot_data.xml
Normal file
94
runbot/data/runbot_data.xml
Normal file
@ -0,0 +1,94 @@
|
||||
<odoo>
|
||||
<record model="runbot.category" id="runbot.default_category">
|
||||
<field name="name">Default</field>
|
||||
<field name="icon">gear</field>
|
||||
</record>
|
||||
<record model="runbot.category" id="runbot.nightly_category">
|
||||
<field name="name">Nightly</field>
|
||||
<field name="icon">moon-o</field>
|
||||
</record>
|
||||
<record model="runbot.category" id="runbot.weekly_category">
|
||||
<field name="name">Weekly</field>
|
||||
<field name="icon">tasks</field>
|
||||
</record>
|
||||
|
||||
<record model="runbot.project" id="runbot.main_project">
|
||||
<field name="name">R&D</field>
|
||||
</record>
|
||||
|
||||
<record model="runbot.bundle" id="runbot.bundle_master">
|
||||
<field name="name">master</field>
|
||||
<field name="is_base">True</field>
|
||||
<field name="project_id" ref="runbot.main_project"/>
|
||||
</record>
|
||||
<record model="runbot.bundle" id="runbot.bundle_dummy">
|
||||
<field name="name">Dummy</field>
|
||||
<field name="no_build">True</field>
|
||||
<field name="project_id" ref="runbot.main_project"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.config_parameter" id="runbot.runbot_is_base_regex">
|
||||
<field name="key">runbot.runbot_is_base_regex</field>
|
||||
<field name="value">^((master)|(saas-)?\d+\.\d+)$</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.server" id="action_toggle_is_base">
|
||||
<field name="name">Mark is base</field>
|
||||
<field name="model_id" ref="runbot.model_runbot_bundle" />
|
||||
<field name="binding_model_id" ref="runbot.model_runbot_bundle" />
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
records.write({'is_base': True})
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.server" id="action_mark_no_build">
|
||||
<field name="name">Mark no build</field>
|
||||
<field name="model_id" ref="runbot.model_runbot_bundle" />
|
||||
<field name="binding_model_id" ref="runbot.model_runbot_bundle" />
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
records.write({'no_build': True})
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.server" id="action_mark_build">
|
||||
<field name="name">Mark build</field>
|
||||
<field name="model_id" ref="runbot.model_runbot_bundle" />
|
||||
<field name="binding_model_id" ref="runbot.model_runbot_bundle" />
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
records.write({'no_build': False})
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_runbot" model="ir.cron">
|
||||
<field name="name">Runbot</field>
|
||||
<field name="interval_number">10</field>
|
||||
<field name="interval_type">seconds</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="model_id" ref="model_runbot_runbot"/>
|
||||
<field name="code">model._cron()</field>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="bundle_create" model="base.automation">
|
||||
<field name="name">Base, staging and tmp management</field>
|
||||
<field name="model_id" ref="runbot.model_runbot_bundle"/>
|
||||
<field name="trigger">on_create</field>
|
||||
<field name="active" eval="True"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if record.name.startswith('tmp.'):
|
||||
record['no_build'] = True
|
||||
elif record.name.startswith('staging.'):
|
||||
name = record.name.replace('staging.', '')
|
||||
base = record.env['runbot.bundle'].search([('name', '=', name), ('project_id', '=', record.project_id.id), ('is_base', '=', True)], limit=1)
|
||||
if base:
|
||||
record['defined_base_id'] = base
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
BIN
runbot/documentation/images/repo_odoo.png
Normal file
BIN
runbot/documentation/images/repo_odoo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
runbot/documentation/images/repo_runbot.png
Normal file
BIN
runbot/documentation/images/repo_runbot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
runbot/documentation/images/trigger.png
Normal file
BIN
runbot/documentation/images/trigger.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -1,5 +1,4 @@
|
||||
from odoo.fields import Field, pycompat
|
||||
import json
|
||||
from odoo.fields import Field
|
||||
from collections import MutableMapping
|
||||
from psycopg2.extras import Json
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
def migrate(cr, version):
|
||||
|
||||
repo_modules = '-auth_ldap,-document_ftp,-base_gengo,-website_gengo,-website_instantclick,-pad,-pad_project,-note_pad,-pos_cache,-pos_blackbox_be,-hw_*,-theme_*,-l10n_*,'
|
||||
repo_modules = '-auth_ldap,-document_ftp,-base_gengo,-website_gengo,-website_instantclick,-pad,-pad_bundle,-note_pad,-pos_cache,-pos_blackbox_be,-hw_*,-theme_*,-l10n_*,'
|
||||
cr.execute("UPDATE runbot_repo SET modules = CONCAT(%s, modules) WHERE modules_auto = 'all' or modules_auto = 'repo';", (repo_modules,))
|
||||
|
||||
# ceux qui n'ont pas d'étoile on prefix par '-*,'
|
||||
|
499
runbot/migrations/13.0.5.0/post-migration.py
Normal file
499
runbot/migrations/13.0.5.0/post-migration.py
Normal file
@ -0,0 +1,499 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.api import Environment
|
||||
from odoo import SUPERUSER_ID
|
||||
import logging
|
||||
import progressbar
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
|
||||
def _bar(total):
|
||||
b = progressbar.ProgressBar(maxval=total, \
|
||||
widgets=[progressbar.Bar('=', '[', ']'), ' ', progressbar.Percentage()])
|
||||
b.start()
|
||||
return b
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunbotMigrationException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
env = Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# monkey patch github to raise an exception during migration to avoid problems
|
||||
def _github():
|
||||
raise RunbotMigrationException('_github call')
|
||||
|
||||
env['runbot.remote']._github = _github
|
||||
|
||||
# some checks:
|
||||
for keyword in ('real_build', 'duplicate_id', '_get_all_commit', '_get_repo', '_copy_dependency_ids', 'Commit', '_get_repo_available_modules'):
|
||||
matches = env['runbot.build.config.step'].search([('python_code', 'like', keyword)])
|
||||
if matches:
|
||||
_logger.warning('Some python steps found with %s ref: %s', keyword, matches)
|
||||
|
||||
cr.execute('SELECT id FROM runbot_repo WHERE nginx = true')
|
||||
if cr.fetchone():
|
||||
cr.execute("""INSERT INTO ir_config_parameter (KEY, value) VALUES ('runbot_nginx', 'True')""")
|
||||
|
||||
########################
|
||||
# Repo, remotes, triggers and projects
|
||||
########################
|
||||
|
||||
visited = set()
|
||||
owner_to_remote = {}
|
||||
repos_infos = {}
|
||||
triggers = {}
|
||||
triggers_by_project = defaultdict(list)
|
||||
deleted_repos_ids = set()
|
||||
|
||||
RD_project = env.ref('runbot.main_project')
|
||||
security_project = env['runbot.project'].create({
|
||||
'name': 'Security'
|
||||
})
|
||||
|
||||
# create a master bundle for security (master was discovered after saas-14)
|
||||
env['runbot.bundle'].create({
|
||||
'name': 'master',
|
||||
'is_base': True,
|
||||
'project_id': security_project.id
|
||||
})
|
||||
|
||||
project_matching = { # some hardcoded info
|
||||
'odoo': RD_project,
|
||||
'enterprise': RD_project,
|
||||
'upgrade': RD_project,
|
||||
'design-themes': RD_project,
|
||||
'odoo-security': security_project,
|
||||
'enterprise-security': security_project,
|
||||
}
|
||||
|
||||
# remove the SET NULL on runbot_build.repo_id when the repo is deleted.
|
||||
# Otherwise we will not be able to create commits later
|
||||
cr.execute('ALTER TABLE runbot_build DROP CONSTRAINT IF EXISTS runbot_build_repo_id_fkey1;')
|
||||
|
||||
cr.execute("""
|
||||
SELECT
|
||||
id, name, duplicate_id, modules, modules_auto, server_files, manifest_files, addons_paths, mode, token, repo_config_id
|
||||
FROM runbot_repo order by id
|
||||
""")
|
||||
for id, name, duplicate_id, modules, modules_auto, server_files, manifest_files, addons_paths, mode, token, repo_config_id in cr.fetchall():
|
||||
cr.execute(""" SELECT res_groups_id FROM res_groups_runbot_repo_rel WHERE runbot_repo_id = %s""", (id,))
|
||||
group_ids = [r[0] for r in cr.fetchall()]
|
||||
repo_name = name.split('/')[-1].replace('.git', '')
|
||||
owner = name.split(':')[-1].split('/')[0]
|
||||
|
||||
if duplicate_id in visited:
|
||||
repo = env['runbot.repo'].browse(duplicate_id)
|
||||
else:
|
||||
repo = env['runbot.repo'].browse(id)
|
||||
|
||||
cr.execute('ALTER SEQUENCE runbot_remote_id_seq RESTART WITH %s', (id, ))
|
||||
remote = env['runbot.remote'].create({
|
||||
'name': name,
|
||||
'repo_id': repo.id if duplicate_id not in visited else duplicate_id,
|
||||
'sequence': repo.sequence,
|
||||
'fetch_pull': mode != 'disabled',
|
||||
'fetch_heads': mode != 'disabled',
|
||||
'token': token,
|
||||
})
|
||||
assert remote.id == id
|
||||
|
||||
# Move repo_id to remote_id
|
||||
# Implies to ensure that remote id's will be the same as the old repo
|
||||
# repo_id is set to null to avoid the cascading delete when the repo will be removed
|
||||
cr.execute('UPDATE runbot_branch SET remote_id=repo_id, repo_id=NULL WHERE repo_id = %s', (id,))
|
||||
|
||||
owner_to_remote[(owner, repo.id)] = remote.id
|
||||
|
||||
repo_infos = {
|
||||
'name': repo_name,
|
||||
'modules': modules,
|
||||
'group_ids': group_ids,
|
||||
'server_files': server_files,
|
||||
'manifest_files': manifest_files,
|
||||
'addons_paths': addons_paths,
|
||||
}
|
||||
|
||||
if duplicate_id in visited:
|
||||
cr.execute('DELETE FROM runbot_repo WHERE id = %s', (id, ))
|
||||
deleted_repos_ids.add(id)
|
||||
if repos_infos[duplicate_id] != repo_infos:
|
||||
_logger.warning('deleting duplicate with different values:\nexisting->%s\ndeleted->%s', repos_infos[duplicate_id], repo_infos)
|
||||
else:
|
||||
visited.add(id)
|
||||
repos_infos[id] = repo_infos
|
||||
repo.name = repo_name
|
||||
repo.main_remote = remote
|
||||
# if not, we need to give information on how to group repos: odoo+enterprise+upgarde+design-theme/se/runbot
|
||||
# this mean that we will need to group build too. Could be nice but maybe a little difficult.
|
||||
if repo_name in project_matching:
|
||||
project = project_matching[repo_name]
|
||||
else:
|
||||
project = env['runbot.project'].create({
|
||||
'name': repo_name,
|
||||
})
|
||||
# also create a master budle, just in case
|
||||
env['runbot.bundle'].create({
|
||||
'name': 'master',
|
||||
'is_base': True,
|
||||
'project_id': project.id
|
||||
})
|
||||
repo.project_id = project.id
|
||||
|
||||
cr.execute(""" SELECT dependency_id FROM runbot_repo_dep_rel WHERE dependant_id = %s""", (id,))
|
||||
dependency_ids = [r[0] for r in cr.fetchall()]
|
||||
|
||||
trigger = env['runbot.trigger'].create({
|
||||
'name': repo_name,
|
||||
'project_id': project.id,
|
||||
'repo_ids': [(4, id)],
|
||||
'dependency_ids': [(4, dependency_id) for dependency_id in dependency_ids],
|
||||
'config_id': repo_config_id if repo_config_id else env.ref('runbot.runbot_build_config_default').id,
|
||||
})
|
||||
triggers[id] = trigger
|
||||
triggers_by_project[project.id].append(trigger)
|
||||
|
||||
#######################
|
||||
# Branches
|
||||
#######################
|
||||
cr.execute('UPDATE runbot_branch SET name=branch_name')
|
||||
|
||||
# no build, config, ...
|
||||
dummy_bundle = env.ref('runbot.bundle_dummy')
|
||||
########################
|
||||
# Bundles
|
||||
########################
|
||||
_logger.info('Creating bundles')
|
||||
|
||||
branches = env['runbot.branch'].search([], order='id')
|
||||
|
||||
branches._compute_reference_name()
|
||||
|
||||
bundles = {('master', RD_project.id): env.ref('runbot.bundle_master')}
|
||||
branch_to_bundle = {}
|
||||
branch_to_version = {}
|
||||
progress = _bar(len(branches))
|
||||
env.cr.execute("""SELECT id FROM runbot_branch WHERE sticky='t'""")
|
||||
sticky_ids = [rec[0] for rec in env.cr.fetchall()]
|
||||
|
||||
for i, branch in enumerate(branches):
|
||||
progress.update(i)
|
||||
repo = branch.remote_id.repo_id
|
||||
if branch.target_branch_name and branch.pull_head_name:
|
||||
# 1. update source_repo: do not call github and use a naive approach:
|
||||
# pull_head_name contains odoo-dev and a repo in group starts with odoo-dev -> this is a known repo.
|
||||
owner = branch.pull_head_name.split(':')[0]
|
||||
pull_head_remote_id = owner_to_remote.get((owner, repo.id))
|
||||
if pull_head_remote_id:
|
||||
branch.pull_head_remote_id = pull_head_remote_id
|
||||
project_id = repo.project_id.id
|
||||
name = branch.reference_name
|
||||
|
||||
key = (name, project_id)
|
||||
if key not in bundles:
|
||||
bundle = env['runbot.bundle'].create({
|
||||
'name': name,
|
||||
'project_id': project_id,
|
||||
'sticky': branch.id in sticky_ids,
|
||||
'is_base': branch.id in sticky_ids,
|
||||
})
|
||||
bundles[key] = bundle
|
||||
bundle = bundles[key]
|
||||
|
||||
if branch.is_pr:
|
||||
if bundle.is_base:
|
||||
_logger.warning('Trying to add pr %s (%s) to base bundle (%s)', branch.name, branch.id, bundle.name)
|
||||
bundle = dummy_bundle
|
||||
elif ':' in name:
|
||||
# handle external PR's
|
||||
base_name = name.split(':')[1].split('-')[0]
|
||||
defined_base_key = (base_name, project_id)
|
||||
if defined_base_key in bundles:
|
||||
bundle.defined_base_id = bundles[defined_base_key]
|
||||
|
||||
branch.bundle_id = bundle
|
||||
branch_to_bundle[branch.id] = bundle
|
||||
branch_to_version[branch.id] = bundle.version_id.id
|
||||
|
||||
branches.flush()
|
||||
env['runbot.bundle'].flush()
|
||||
progress.finish()
|
||||
|
||||
batch_size = 100000
|
||||
|
||||
sha_commits = {}
|
||||
sha_repo_commits = {}
|
||||
branch_heads = {}
|
||||
commit_link_ids = defaultdict(dict)
|
||||
cr.execute("SELECT count(*) FROM runbot_build")
|
||||
nb_build = cr.fetchone()[0]
|
||||
|
||||
########################
|
||||
# BUILDS
|
||||
########################
|
||||
_logger.info('Creating main commits')
|
||||
counter = 0
|
||||
progress = _bar(nb_build)
|
||||
cross_project_duplicate_ids = []
|
||||
for offset in range(0, nb_build, batch_size):
|
||||
cr.execute("""
|
||||
SELECT id,
|
||||
repo_id, name, author, author_email, committer, committer_email, subject, date, duplicate_id, branch_id
|
||||
FROM runbot_build ORDER BY id asc LIMIT %s OFFSET %s""", (batch_size, offset))
|
||||
|
||||
for id, repo_id, name, author, author_email, committer, committer_email, subject, date, duplicate_id, branch_id in cr.fetchall():
|
||||
progress.update(counter)
|
||||
remote_id = env['runbot.remote'].browse(repo_id)
|
||||
#assert remote_id.exists()
|
||||
if not repo_id:
|
||||
_logger.warning('No repo_id for build %s, skipping', id)
|
||||
continue
|
||||
key = (name, remote_id.repo_id.id)
|
||||
if key in sha_repo_commits:
|
||||
commit = sha_repo_commits[key]
|
||||
else:
|
||||
if duplicate_id and remote_id.repo_id.project_id.id != RD_project.id:
|
||||
cross_project_duplicate_ids.append(id)
|
||||
elif duplicate_id:
|
||||
_logger.warning('Problem: duplicate: %s,%s', id, duplicate_id)
|
||||
|
||||
commit = env['runbot.commit'].create({
|
||||
'name': name,
|
||||
'repo_id': remote_id.repo_id.id, # now that the repo_id on the build correspond to a remote_id
|
||||
'author': author,
|
||||
'author_email': author_email,
|
||||
'committer': committer,
|
||||
'committer_email': committer_email,
|
||||
'subject': subject,
|
||||
'date': date
|
||||
})
|
||||
sha_repo_commits[key] = commit
|
||||
sha_commits[name] = commit
|
||||
branch_heads[branch_id] = commit.id
|
||||
counter += 1
|
||||
|
||||
commit_link_ids[id][commit.repo_id.id] = commit.id
|
||||
|
||||
|
||||
progress.finish()
|
||||
|
||||
if cross_project_duplicate_ids:
|
||||
_logger.info('Cleaning cross project duplicates')
|
||||
cr.execute("UPDATE runbot_build SET local_state='done', duplicate_id=NULL WHERE id IN %s", (tuple(cross_project_duplicate_ids), ))
|
||||
|
||||
_logger.info('Creating params')
|
||||
counter = 0
|
||||
|
||||
cr.execute("SELECT count(*) FROM runbot_build WHERE duplicate_id IS NULL")
|
||||
nb_real_build = cr.fetchone()[0]
|
||||
progress = _bar(nb_real_build)
|
||||
|
||||
# monkey patch to avoid search
|
||||
original = env['runbot.build.params']._find_existing
|
||||
existing = {}
|
||||
|
||||
def _find_existing(fingerprint):
|
||||
return existing.get(fingerprint, env['runbot.build.params'])
|
||||
|
||||
param = env['runbot.build.params']
|
||||
param._find_existing = _find_existing
|
||||
|
||||
builds_deps = defaultdict(list)
|
||||
def get_deps(bid):
|
||||
if bid < get_deps.start or bid > get_deps.stop:
|
||||
builds_deps.clear()
|
||||
get_deps.start = bid
|
||||
get_deps.stop = bid+batch_size
|
||||
cr.execute('SELECT build_id, dependency_hash, dependecy_repo_id, closest_branch_id, match_type FROM runbot_build_dependency WHERE build_id>=%s and build_id<=%s', (get_deps.start, get_deps.stop))
|
||||
for build_id, dependency_hash, dependecy_repo_id, closest_branch_id, match_type in cr.fetchall():
|
||||
builds_deps[build_id].append((dependency_hash, dependecy_repo_id, closest_branch_id, match_type))
|
||||
return builds_deps[bid]
|
||||
get_deps.start = 0
|
||||
get_deps.stop = 0
|
||||
|
||||
def update_build_params(params_id, id):
|
||||
cr.execute('UPDATE runbot_build SET params_id=%s WHERE id=%s OR duplicate_id = %s', (params_id, id, id))
|
||||
|
||||
build_ids_to_recompute = []
|
||||
for offset in range(0, nb_real_build, batch_size):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
id, branch_id, repo_id, extra_params, config_id, config_data
|
||||
FROM runbot_build WHERE duplicate_id IS NULL ORDER BY id asc LIMIT %s OFFSET %s""", (batch_size, offset))
|
||||
|
||||
for id, branch_id, repo_id, extra_params, config_id, config_data in cr.fetchall():
|
||||
progress.update(counter)
|
||||
counter += 1
|
||||
build_ids_to_recompute.append(id)
|
||||
|
||||
remote_id = env['runbot.remote'].browse(repo_id)
|
||||
commit_link_ids_create_values = [
|
||||
{'commit_id': commit_link_ids[id][remote_id.repo_id.id], 'match_type':'base_head'}]
|
||||
|
||||
for dependency_hash, dependecy_repo_id, closest_branch_id, match_type in get_deps(id):
|
||||
dependency_remote_id = env['runbot.remote'].browse(dependecy_repo_id)
|
||||
key = (dependency_hash, dependency_remote_id.id)
|
||||
commit = sha_repo_commits.get(key) or sha_commits.get(dependency_hash)
|
||||
if not commit:
|
||||
# -> most of the time, commit in exists but with wrong repo. Info can be found on other commit.
|
||||
_logger.warning('Missing commit %s created', dependency_hash)
|
||||
commit = env['runbot.commit'].create({
|
||||
'name': dependency_hash,
|
||||
'repo_id': dependency_remote_id.repo_id.id,
|
||||
})
|
||||
sha_repo_commits[key] = commit
|
||||
sha_commits[dependency_hash] = commit
|
||||
commit_link_ids[id][dependency_remote_id.id] = commit.id
|
||||
match_type = 'base_head' if match_type in ('pr_target', 'prefix', 'default') else 'head'
|
||||
commit_link_ids_create_values.append({'commit_id': commit.id, 'match_type':match_type, 'branch_id': closest_branch_id})
|
||||
|
||||
params = param.create({
|
||||
'version_id': branch_to_version[branch_id],
|
||||
'extra_params': extra_params,
|
||||
'config_id': config_id,
|
||||
'project_id': env['runbot.repo'].browse(remote_id.repo_id.id).project_id,
|
||||
'trigger_id': triggers[remote_id.repo_id.id].id,
|
||||
'config_data': config_data,
|
||||
'commit_link_ids': [(0, 0, values) for values in commit_link_ids_create_values]
|
||||
})
|
||||
existing[params.fingerprint] = params
|
||||
update_build_params(params.id, id)
|
||||
env.cache.invalidate()
|
||||
progress.finish()
|
||||
|
||||
env['runbot.build.params']._find_existing = original
|
||||
|
||||
######################
|
||||
# update dest
|
||||
######################
|
||||
_logger.info('Updating build dests')
|
||||
counter = 0
|
||||
progress = _bar(nb_real_build)
|
||||
for offset in range(0, len(build_ids_to_recompute), batch_size):
|
||||
builds = env['runbot.build'].browse(build_ids_to_recompute[offset:offset+batch_size])
|
||||
builds._compute_dest()
|
||||
progress.update(batch_size)
|
||||
progress.finish()
|
||||
|
||||
for branch, head in branch_heads.items():
|
||||
cr.execute('UPDATE runbot_branch SET head=%s WHERE id=%s', (head, branch))
|
||||
del branch_heads
|
||||
# adapt build commits
|
||||
|
||||
|
||||
_logger.info('Creating batchs')
|
||||
###################
|
||||
# Bundle batch
|
||||
####################
|
||||
cr.execute("SELECT count(*) FROM runbot_build WHERE parent_id IS NOT NULL")
|
||||
nb_root_build = cr.fetchone()[0]
|
||||
counter = 0
|
||||
progress = _bar(nb_root_build)
|
||||
previous_batch = {}
|
||||
for offset in range(0, nb_root_build, batch_size):
|
||||
cr.execute("""
|
||||
SELECT
|
||||
id, duplicate_id, repo_id, branch_id, create_date, build_type, config_id, params_id
|
||||
FROM runbot_build WHERE parent_id IS NULL order by id asc
|
||||
LIMIT %s OFFSET %s""", (batch_size, offset))
|
||||
for id, duplicate_id, repo_id, branch_id, create_date, build_type, config_id, params_id in cr.fetchall():
|
||||
progress.update(counter)
|
||||
counter += 1
|
||||
if repo_id is None:
|
||||
_logger.warning('Skipping %s: no repo', id)
|
||||
continue
|
||||
bundle = branch_to_bundle[branch_id]
|
||||
# try to merge build in same batch
|
||||
# not temporal notion in this case, only hash consistency
|
||||
batch = False
|
||||
build_id = duplicate_id or id
|
||||
build_commits = commit_link_ids[build_id]
|
||||
batch_repos_ids = []
|
||||
|
||||
# check if this build can be added to last_batch
|
||||
if bundle.last_batch:
|
||||
if create_date - bundle.last_batch.last_update < datetime.timedelta(minutes=5):
|
||||
if duplicate_id and build_id in bundle.last_batch.slot_ids.mapped('build_id').ids:
|
||||
continue
|
||||
|
||||
# to fix: nightly will be in the same batch of the previous normal one. If config_id is diffrent, create batch?
|
||||
# possible fix: max create_date diff
|
||||
batch = bundle.last_batch
|
||||
batch_commits = batch.commit_ids
|
||||
batch_repos_ids = batch_commits.mapped('repo_id').ids
|
||||
for commit in batch_commits:
|
||||
if commit.repo_id.id in build_commits:
|
||||
if commit.id != build_commits[commit.repo_id.id]:
|
||||
batch = False
|
||||
batch_repos_ids = []
|
||||
break
|
||||
|
||||
missing_commits = [commit_id for repo_id, commit_id in build_commits.items() if repo_id not in batch_repos_ids]
|
||||
|
||||
if not batch:
|
||||
batch = env['runbot.batch'].create({
|
||||
'create_date': create_date,
|
||||
'last_update': create_date,
|
||||
'state': 'ready',
|
||||
'bundle_id': bundle.id
|
||||
})
|
||||
#if bundle.last_batch:
|
||||
# previous = previous_batch.get(bundle.last_batch.id)
|
||||
# if previous:
|
||||
# previous_build_by_trigger = {slot.trigger_id.id: slot.build_id.id for slot in previous.slot_ids}
|
||||
# else:
|
||||
# previous_build_by_trigger = {}
|
||||
# batch_slot_triggers = bundle.last_batch.slot_ids.mapped('trigger_id').ids
|
||||
# missing_trigger_ids = [trigger for trigger in triggers_by_project[bundle.project_id.id] if trigger.id not in batch_slot_triggers]
|
||||
# for trigger in missing_trigger_ids:
|
||||
# env['runbot.batch.slot'].create({
|
||||
# 'trigger_id': trigger.id,
|
||||
# 'batch_id': bundle.last_batch.id,
|
||||
# 'build_id': previous_build_by_trigger.get(trigger.id), # may be None, if we want to create empty slots. Else, iter on slot instead
|
||||
# 'link_type': 'matched',
|
||||
# 'active': True,
|
||||
# })
|
||||
|
||||
previous_batch[batch.id] = bundle.last_batch
|
||||
bundle.last_batch = batch
|
||||
else:
|
||||
batch.last_update = create_date
|
||||
|
||||
real_repo_id = env['runbot.remote'].browse(repo_id).repo_id.id
|
||||
env['runbot.batch.slot'].create({
|
||||
'params_id': params_id,
|
||||
'trigger_id': triggers[real_repo_id].id,
|
||||
'batch_id': batch.id,
|
||||
'build_id': build_id,
|
||||
'link_type': 'rebuild' if build_type == 'rebuild' else 'matched' if duplicate_id else 'created',
|
||||
'active': True,
|
||||
})
|
||||
commit_links_values = []
|
||||
for missing_commit in missing_commits:
|
||||
commit_links_values.append({
|
||||
'commit_id': missing_commit,
|
||||
'match_type': 'new',
|
||||
})
|
||||
batch.commit_link_ids = [(0, 0, values) for values in commit_links_values]
|
||||
if batch.state == 'ready' and all(slot.build_id.global_state in (False, 'running', 'done') for slot in batch.slot_ids):
|
||||
batch.state = 'done'
|
||||
|
||||
env.cache.invalidate()
|
||||
progress.finish()
|
||||
|
||||
#Build of type rebuild may point to same params as rebbuild?
|
||||
|
||||
###################
|
||||
# Cleaning (performances)
|
||||
###################
|
||||
# 1. avoid UPDATE "runbot_build" SET "commit_path_mode"=NULL WHERE "commit_path_mode"='soft'
|
||||
|
||||
_logger.info('Pre-cleaning')
|
||||
cr.execute('alter table runbot_build alter column commit_path_mode drop not null')
|
||||
cr.execute('ANALYZE')
|
||||
cr.execute("delete from runbot_build where local_state='duplicate'") # what about duplicate childrens?
|
||||
_logger.info('End')
|
51
runbot/migrations/13.0.5.0/pre-migration.py
Normal file
51
runbot/migrations/13.0.5.0/pre-migration.py
Normal file
@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
# dependency is not correct since it will be all commits. This also free the name for a build dependant on another build params
|
||||
|
||||
# those indexes are improving the branches deletion
|
||||
cr.execute('CREATE INDEX ON runbot_branch (defined_sticky);')
|
||||
cr.execute('CREATE INDEX ON runbot_build_dependency (closest_branch_id);')
|
||||
|
||||
# Fix duplicate problems
|
||||
cr.execute("UPDATE runbot_build SET duplicate_id = null WHERE duplicate_id > id")
|
||||
cr.execute("UPDATE runbot_build SET local_state='done' WHERE duplicate_id IS NULL AND local_state = 'duplicate';")
|
||||
# Remove builds without a repo
|
||||
cr.execute("DELETE FROM runbot_build WHERE repo_id IS NULL")
|
||||
|
||||
cr.execute("DELETE FROM ir_ui_view WHERE id IN (SELECT res_id FROM ir_model_data WHERE name = 'inherits_branch_in_menu' AND module = 'runbot')")
|
||||
|
||||
# Fix branches
|
||||
cr.execute("""DELETE FROM runbot_branch WHERE name SIMILAR TO 'refs/heads/\d+' RETURNING id,name;""") # Remove old bad branches named like PR
|
||||
for branch_id, name in cr.fetchall():
|
||||
_logger.warning('Deleting branch id %s with name "%s"', branch_id, name)
|
||||
|
||||
cr.execute("""SELECT branch_name,repo_id, count(*) AS nb FROM runbot_branch GROUP BY branch_name,repo_id HAVING count(*) > 1;""") # Branches with duplicate branch_name in same repo
|
||||
for branch_name, repo_id, nb in cr.fetchall():
|
||||
cr.execute("""DELETE FROM runbot_branch WHERE (sticky='f' OR sticky IS NULL) AND branch_name=%s and repo_id=%s and name ~ 'refs/heads/.+/.+' RETURNING id,branch_name;""", (branch_name, repo_id))
|
||||
for branch_id, branch_name in cr.fetchall():
|
||||
_logger.warning('Deleting branch id %s with branch_name "%s"', branch_id, branch_name)
|
||||
|
||||
# Raise in case of buggy PR's
|
||||
cr.execute("SELECT id,name FROM runbot_branch WHERE name LIKE 'refs/pull/%' AND pull_head_name is null")
|
||||
bad_prs = cr.fetchall()
|
||||
if bad_prs:
|
||||
for pr in bad_prs:
|
||||
_logger.warning('PR with NULL pull_head_name found: %s (%s)', pr[1], pr[0])
|
||||
raise RuntimeError("Migration error", "Found %s PR's without pull_head_name" % len(bad_prs))
|
||||
|
||||
# avoid recompute of branch._comput_bundle_id otherwise, it cannot find xml data
|
||||
cr.execute('ALTER TABLE runbot_branch ADD COLUMN bundle_id INTEGER;')
|
||||
|
||||
# avoid recompute of pull_head_name wich is emptied during the recompute
|
||||
cr.execute('ALTER TABLE runbot_branch ADD COLUMN pull_head_remote_id INTEGER;')
|
||||
|
||||
cr.execute('ALTER TABLE runbot_branch ADD COLUMN is_pr BOOLEAN;')
|
||||
cr.execute("""UPDATE runbot_branch SET is_pr = CASE WHEN name like 'refs/pull/%' THEN true ELSE false END;""")
|
||||
|
||||
# delete runbot.repo inehrited views
|
||||
cr.execute("DELETE FROM ir_ui_view WHERE inherit_id IN (SELECT id from ir_ui_view WHERE name = 'runbot.repo');")
|
||||
return
|
@ -1,14 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import repo
|
||||
from . import batch
|
||||
from . import branch
|
||||
from . import build
|
||||
from . import event
|
||||
from . import build_dependency
|
||||
from . import build_config
|
||||
from . import ir_cron
|
||||
from . import host
|
||||
from . import build_error
|
||||
from . import bundle
|
||||
from . import commit
|
||||
from . import database
|
||||
from . import event
|
||||
from . import host
|
||||
from . import ir_cron
|
||||
from . import ir_ui_view
|
||||
from . import project
|
||||
from . import repo
|
||||
from . import res_config_settings
|
||||
from . import runbot
|
||||
from . import upgrade
|
||||
from . import user
|
||||
from . import version
|
||||
|
||||
from . import build_stat
|
||||
from . import build_stat_regex
|
||||
from . import res_config_settings
|
||||
|
408
runbot/models/batch.py
Normal file
408
runbot/models/batch.py
Normal file
@ -0,0 +1,408 @@
|
||||
import time
|
||||
import logging
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
from odoo import models, fields, api
|
||||
from ..common import dt2time, s2human_long, pseudo_markdown
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Batch(models.Model):
|
||||
_name = 'runbot.batch'
|
||||
_description = "Bundle batch"
|
||||
|
||||
last_update = fields.Datetime('Last ref update')
|
||||
bundle_id = fields.Many2one('runbot.bundle', required=True, index=True, ondelete='cascade')
|
||||
commit_link_ids = fields.Many2many('runbot.commit.link')
|
||||
commit_ids = fields.Many2many('runbot.commit', compute='_compute_commit_ids')
|
||||
slot_ids = fields.One2many('runbot.batch.slot', 'batch_id')
|
||||
state = fields.Selection([('preparing', 'Preparing'), ('ready', 'Ready'), ('done', 'Done'), ('skipped', 'Skipped')])
|
||||
hidden = fields.Boolean('Hidden', default=False)
|
||||
age = fields.Integer(compute='_compute_age', string='Build age')
|
||||
category_id = fields.Many2one('runbot.category', default=lambda self: self.env.ref('runbot.default_category', raise_if_not_found=False))
|
||||
log_ids = fields.One2many('runbot.batch.log', 'batch_id')
|
||||
has_warning = fields.Boolean("Has warning")
|
||||
|
||||
@api.depends('commit_link_ids')
|
||||
def _compute_commit_ids(self):
|
||||
for batch in self:
|
||||
batch.commit_ids = batch.commit_link_ids.commit_id
|
||||
|
||||
@api.depends('create_date')
|
||||
def _compute_age(self):
|
||||
"""Return the time between job start and now"""
|
||||
for batch in self:
|
||||
if batch.create_date:
|
||||
batch.age = int(time.time() - dt2time(batch.create_date))
|
||||
else:
|
||||
batch.buildage_age = 0
|
||||
|
||||
def get_formated_age(self):
|
||||
return s2human_long(self.age)
|
||||
|
||||
def _url(self):
|
||||
self.ensure_one()
|
||||
runbot_domain = self.env['runbot.runbot']._domain()
|
||||
return "http://%s/runbot/batch/%s" % (runbot_domain, self.id)
|
||||
|
||||
def _new_commit(self, branch, match_type='new'):
|
||||
# if not the same hash for repo:
|
||||
commit = branch.head
|
||||
self.last_update = fields.Datetime.now()
|
||||
for commit_link in self.commit_link_ids:
|
||||
# case 1: a commit already exists for the repo (pr+branch, or fast push)
|
||||
if commit_link.commit_id.repo_id == commit.repo_id:
|
||||
if commit_link.commit_id.id != commit.id:
|
||||
self._log('New head on branch %s during throttle phase: Replacing commit %s with %s', branch.name, commit_link.commit_id.name, commit.name)
|
||||
commit_link.write({'commit_id': commit.id, 'branch_id': branch.id})
|
||||
elif not commit_link.branch_id.is_pr and branch.is_pr:
|
||||
commit_link.branch_id = branch # Try to have a pr instead of branch on commit if possible ?
|
||||
break
|
||||
else:
|
||||
self.write({'commit_link_ids': [(0, 0, {
|
||||
'commit_id': commit.id,
|
||||
'match_type': match_type,
|
||||
'branch_id': branch.id
|
||||
})]})
|
||||
|
||||
def _skip(self):
|
||||
for batch in self:
|
||||
if batch.bundle_id.is_base or batch.state == 'done':
|
||||
continue
|
||||
batch.state = 'skipped' # done?
|
||||
batch._log('Skipping batch')
|
||||
for slot in batch.slot_ids:
|
||||
slot.skipped = True
|
||||
build = slot.build_id
|
||||
testing_slots = build.slot_ids.filtered(lambda s: not s.skipped)
|
||||
if not testing_slots:
|
||||
if build.global_state == 'pending':
|
||||
build._skip('Newer build found')
|
||||
elif build.global_state in ('waiting', 'testing'):
|
||||
build.killable = True
|
||||
elif slot.link_type == 'created':
|
||||
batches = testing_slots.mapped('batch_id')
|
||||
_logger.info('Cannot skip build %s build is still in use in batches %s', build.id, batches.ids)
|
||||
bundles = batches.mapped('bundle_id') - batch.bundle_id
|
||||
if bundles:
|
||||
batch._log('Cannot kill or skip build %s, build is used in another bundle: %s', build.id, bundles.mapped('name'))
|
||||
|
||||
def _process(self):
|
||||
for batch in self:
|
||||
if batch.state == 'preparing' and batch.last_update < fields.Datetime.now() - datetime.timedelta(seconds=60):
|
||||
batch._prepare()
|
||||
elif batch.state == 'ready' and all(slot.build_id.global_state in (False, 'running', 'done') for slot in batch.slot_ids):
|
||||
batch._log('Batch done')
|
||||
batch.state = 'done'
|
||||
|
||||
def _create_build(self, params):
|
||||
"""
|
||||
Create a build with given params_id if it does not already exists.
|
||||
In the case that a very same build already exists that build is returned
|
||||
"""
|
||||
build = self.env['runbot.build'].search([('params_id', '=', params.id), ('parent_id', '=', False)], limit=1, order='id desc')
|
||||
link_type = 'matched'
|
||||
if build:
|
||||
build.killable = False
|
||||
else:
|
||||
description = params.trigger_id.description if params.trigger_id.description else False
|
||||
link_type = 'created'
|
||||
build = self.env['runbot.build'].create({
|
||||
'params_id': params.id,
|
||||
'description': description,
|
||||
'build_type': 'normal' if self.category_id == self.env.ref('runbot.default_category') else 'scheduled',
|
||||
'no_auto_run': self.bundle_id.no_auto_run,
|
||||
})
|
||||
build._github_status(post_commit=False)
|
||||
return link_type, build
|
||||
|
||||
def _prepare(self, auto_rebase=False):
|
||||
for level, message in self.bundle_id.consistency_warning():
|
||||
if level == "warning":
|
||||
self.warning("Bundle warning: %s" % message)
|
||||
|
||||
self.state = 'ready'
|
||||
_logger.info('Preparing batch %s', self.id)
|
||||
|
||||
bundle = self.bundle_id
|
||||
project = bundle.project_id
|
||||
if not bundle.version_id:
|
||||
_logger.error('No version found on bundle %s in project %s', bundle.name, project.name)
|
||||
triggers = self.env['runbot.trigger'].search([ # could be optimised for multiple batches. Ormcached method?
|
||||
('project_id', '=', project.id),
|
||||
('category_id', '=', self.category_id.id)
|
||||
]).filtered(
|
||||
lambda t: not t.version_domain or \
|
||||
self.bundle_id.version_id.filtered_domain(t.get_version_domain())
|
||||
)
|
||||
|
||||
pushed_repo = self.commit_link_ids.mapped('commit_id.repo_id')
|
||||
dependency_repos = triggers.mapped('dependency_ids')
|
||||
all_repos = triggers.mapped('repo_ids') | dependency_repos
|
||||
missing_repos = all_repos - pushed_repo
|
||||
|
||||
######################################
|
||||
# Find missing commits
|
||||
######################################
|
||||
def fill_missing(branch_commits, match_type):
|
||||
if branch_commits:
|
||||
for branch, commit in branch_commits.items(): # branch first in case pr is closed.
|
||||
nonlocal missing_repos
|
||||
if commit.repo_id in missing_repos:
|
||||
if not branch.alive:
|
||||
self._log("Skipping dead branch %s" % branch.name)
|
||||
continue
|
||||
values = {
|
||||
'commit_id': commit.id,
|
||||
'match_type': match_type,
|
||||
'branch_id': branch.id,
|
||||
}
|
||||
if match_type.startswith('base'):
|
||||
values['base_commit_id'] = commit.id
|
||||
values['merge_base_commit_id'] = commit.id
|
||||
self.write({'commit_link_ids': [(0, 0, values)]})
|
||||
missing_repos -= commit.repo_id
|
||||
|
||||
# CHECK branch heads consistency
|
||||
branch_per_repo = {}
|
||||
for branch in bundle.branch_ids.sorted(lambda b: (b.head.id, b.is_pr), reverse=True):
|
||||
if branch.alive:
|
||||
commit = branch.head
|
||||
repo = commit.repo_id
|
||||
if repo not in branch_per_repo:
|
||||
branch_per_repo[repo] = branch
|
||||
elif branch_per_repo[repo].head != branch.head and branch.alive:
|
||||
obranch = branch_per_repo[repo]
|
||||
self._log("Branch %s and branch %s in repo %s don't have the same head: %s ≠ %s", branch.dname, obranch.dname, repo.name, branch.head.name, obranch.head.name)
|
||||
|
||||
# 1.1 FIND missing commit in bundle heads
|
||||
if missing_repos:
|
||||
fill_missing({branch: branch.head for branch in bundle.branch_ids.sorted(lambda b: (b.head.id, b.is_pr), reverse=True)}, 'head')
|
||||
|
||||
# 1.2 FIND merge_base info for those commits
|
||||
# use last not preparing batch to define previous repos_heads instead of branches heads:
|
||||
# Will allow to have a diff info on base bundle, compare with previous bundle
|
||||
last_base_batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.base_id.id), ('state', '!=', 'preparing'), ('category_id', '=', self.category_id.id), ('id', '!=', self.id)], order='id desc', limit=1)
|
||||
base_head_per_repo = {commit.repo_id.id: commit for commit in last_base_batch.commit_ids}
|
||||
self._update_commits_infos(base_head_per_repo) # set base_commit, diff infos, ...
|
||||
|
||||
# 2. FIND missing commit in a compatible base bundle
|
||||
if missing_repos and not bundle.is_base:
|
||||
merge_base_commits = self.commit_link_ids.mapped('merge_base_commit_id')
|
||||
if auto_rebase:
|
||||
batch = last_base_batch
|
||||
self._log('Using last done batch %s to define missing commits (automatic rebase)', batch.id)
|
||||
else:
|
||||
batch = False
|
||||
link_commit = self.env['runbot.commit.link'].search([
|
||||
('commit_id', 'in', merge_base_commits.ids),
|
||||
('match_type', 'in', ('new', 'head'))
|
||||
])
|
||||
batches = self.env['runbot.batch'].search([
|
||||
('bundle_id', '=', bundle.base_id.id),
|
||||
('commit_link_ids', 'in', link_commit.ids),
|
||||
('state', '!=', 'preparing'),
|
||||
('category_id', '=', self.category_id.id)
|
||||
]).sorted(lambda b: (len(b.commit_ids & merge_base_commits), b.id), reverse=True)
|
||||
if batches:
|
||||
batch = batches[0]
|
||||
self._log('Using batch %s to define missing commits', batch.id)
|
||||
batch_exiting_commit = batch.commit_ids.filtered(lambda c: c.repo_id in merge_base_commits.repo_id)
|
||||
not_matching = (batch_exiting_commit - merge_base_commits)
|
||||
if not_matching:
|
||||
message = 'Only %s out of %s merge base matched. You may want to rebase your branches to ensure compatibility' % (len(merge_base_commits)-len(not_matching), len(merge_base_commits))
|
||||
suggestions = [('Tip: rebase %s to %s' % (commit.repo_id.name, commit.name)) for commit in not_matching]
|
||||
self.warning('%s\n%s' % (message, '\n'.join(suggestions)))
|
||||
if batch:
|
||||
fill_missing({link.branch_id: link.commit_id for link in batch.commit_link_ids}, 'base_match')
|
||||
|
||||
# 3.1 FIND missing commit in base heads
|
||||
if missing_repos:
|
||||
if not bundle.is_base:
|
||||
self._log('Not all commit found in bundle branches and base batch. Fallback on base branches heads.')
|
||||
fill_missing({branch: branch.head for branch in self.bundle_id.base_id.branch_ids}, 'base_head')
|
||||
|
||||
# 3.2 FIND missing commit in master base heads
|
||||
if missing_repos: # this is to get an upgrade branch.
|
||||
if not bundle.is_base:
|
||||
self._log('Not all commit found in current version. Fallback on master branches heads.')
|
||||
master_bundle = self.env['runbot.version']._get('master').with_context(project_id=self.bundle_id.project_id.id).base_bundle_id
|
||||
fill_missing({branch: branch.head for branch in master_bundle.branch_ids}, 'base_head')
|
||||
|
||||
# 4. FIND missing commit in foreign project
|
||||
if missing_repos:
|
||||
foreign_projects = dependency_repos.mapped('project_id') - project
|
||||
if foreign_projects:
|
||||
self._log('Not all commit found. Fallback on foreign base branches heads.')
|
||||
foreign_bundles = bundle.search([('name', '=', bundle.name), ('project_id', 'in', foreign_projects.ids)])
|
||||
fill_missing({branch: branch.head for branch in foreign_bundles.mapped('branch_ids').sorted('is_pr', reverse=True)}, 'head')
|
||||
if missing_repos:
|
||||
foreign_bundles = bundle.search([('name', '=', bundle.base_id.name), ('project_id', 'in', foreign_projects.ids)])
|
||||
fill_missing({branch: branch.head for branch in foreign_bundles.mapped('branch_ids')}, 'base_head')
|
||||
|
||||
# CHECK missing commit
|
||||
if missing_repos:
|
||||
_logger.warning('Missing repo %s for batch %s', missing_repos.mapped('name'), self.id)
|
||||
|
||||
######################################
|
||||
# Generate build params
|
||||
######################################
|
||||
if auto_rebase:
|
||||
for commit_link in self.commit_link_ids:
|
||||
commit_link.commit_id = commit_link.commit_id._rebase_on(commit_link.base_commit_id)
|
||||
commit_link_by_repos = {commit_link.commit_id.repo_id.id: commit_link for commit_link in self.commit_link_ids}
|
||||
bundle_repos = bundle.branch_ids.mapped('remote_id.repo_id')
|
||||
version_id = self.bundle_id.version_id.id
|
||||
project_id = self.bundle_id.project_id.id
|
||||
config_by_trigger = {}
|
||||
for trigger_custom in self.bundle_id.trigger_custom_ids:
|
||||
config_by_trigger[trigger_custom.trigger_id.id] = trigger_custom.config_id
|
||||
for trigger in triggers:
|
||||
trigger_repos = trigger.repo_ids | trigger.dependency_ids
|
||||
if trigger_repos & missing_repos:
|
||||
self.warning('Missing commit for repo %s for trigger %s', (trigger_repos & missing_repos).mapped('name'), trigger.name)
|
||||
continue
|
||||
# in any case, search for an existing build
|
||||
config = config_by_trigger.get(trigger.id, trigger.config_id)
|
||||
|
||||
params_value = {
|
||||
'version_id': version_id,
|
||||
'extra_params': '',
|
||||
'config_id': config.id,
|
||||
'project_id': project_id,
|
||||
'trigger_id': trigger.id, # for future reference and access rights
|
||||
'config_data': {},
|
||||
'commit_link_ids': [(6, 0, [commit_link_by_repos[repo.id].id for repo in trigger_repos])],
|
||||
'modules': bundle.modules
|
||||
}
|
||||
params_value['builds_reference_ids'] = trigger._reference_builds(bundle)
|
||||
|
||||
params = self.env['runbot.build.params'].create(params_value)
|
||||
|
||||
build = self.env['runbot.build']
|
||||
link_type = 'created'
|
||||
if ((trigger.repo_ids & bundle_repos) or bundle.build_all or bundle.sticky) and not trigger.manual: # only auto link build if bundle has a branch for this trigger
|
||||
link_type, build = self._create_build(params)
|
||||
self.env['runbot.batch.slot'].create({
|
||||
'batch_id': self.id,
|
||||
'trigger_id': trigger.id,
|
||||
'build_id': build.id,
|
||||
'params_id': params.id,
|
||||
'link_type': link_type,
|
||||
})
|
||||
|
||||
######################################
|
||||
# SKIP older batches
|
||||
######################################
|
||||
default_category = self.env.ref('runbot.default_category')
|
||||
if not bundle.sticky and self.category_id == default_category:
|
||||
skippable = self.env['runbot.batch'].search([
|
||||
('bundle_id', '=', bundle.id),
|
||||
('state', '!=', 'done'),
|
||||
('id', '<', self.id),
|
||||
('category_id', '=', default_category.id)
|
||||
])
|
||||
skippable._skip()
|
||||
|
||||
def _update_commits_infos(self, base_head_per_repo):
|
||||
for link_commit in self.commit_link_ids:
|
||||
commit = link_commit.commit_id
|
||||
base_head = base_head_per_repo.get(commit.repo_id.id)
|
||||
if not base_head:
|
||||
self.warning('No base head found for repo %s', commit.repo_id.name)
|
||||
continue
|
||||
link_commit.base_commit_id = base_head
|
||||
merge_base_sha = False
|
||||
try:
|
||||
link_commit.base_ahead = link_commit.base_behind = 0
|
||||
link_commit.file_changed = link_commit.diff_add = link_commit.diff_remove = 0
|
||||
link_commit.merge_base_commit_id = commit.id
|
||||
if commit.name == base_head.name:
|
||||
continue
|
||||
merge_base_sha = commit.repo_id._git(['merge-base', commit.name, base_head.name]).strip()
|
||||
merge_base_commit = self.env['runbot.commit']._get(merge_base_sha, commit.repo_id.id)
|
||||
link_commit.merge_base_commit_id = merge_base_commit.id
|
||||
|
||||
ahead, behind = commit.repo_id._git(['rev-list', '--left-right', '--count', '%s...%s' % (commit.name, base_head.name)]).strip().split('\t')
|
||||
|
||||
link_commit.base_ahead = int(ahead)
|
||||
link_commit.base_behind = int(behind)
|
||||
|
||||
if merge_base_sha == commit.name:
|
||||
continue
|
||||
|
||||
# diff. Iter on --numstat, easier to parse than --shortstat summary
|
||||
diff = commit.repo_id._git(['diff', '--numstat', merge_base_sha, commit.name]).strip()
|
||||
if diff:
|
||||
for line in diff.split('\n'):
|
||||
link_commit.file_changed += 1
|
||||
add, remove, _ = line.split(None, 2)
|
||||
try:
|
||||
link_commit.diff_add += int(add)
|
||||
link_commit.diff_remove += int(remove)
|
||||
except ValueError: # binary files
|
||||
pass
|
||||
except subprocess.CalledProcessError:
|
||||
self.warning('Commit info failed between %s and %s', commit.name, base_head.name)
|
||||
|
||||
def warning(self, message, *args):
|
||||
self.has_warning = True
|
||||
_logger.warning('batch %s: ' + message, self.id, *args)
|
||||
self._log(message, *args, level='WARNING')
|
||||
|
||||
def _log(self, message, *args, level='INFO'):
|
||||
self.env['runbot.batch.log'].create({
|
||||
'batch_id': self.id,
|
||||
'message': message % args if args else message,
|
||||
'level': level,
|
||||
})
|
||||
|
||||
|
||||
class BatchLog(models.Model):
|
||||
_name = 'runbot.batch.log'
|
||||
_description = 'Batch log'
|
||||
|
||||
batch_id = fields.Many2one('runbot.batch', index=True)
|
||||
message = fields.Text('Message')
|
||||
level = fields.Char()
|
||||
|
||||
|
||||
def _markdown(self):
|
||||
""" Apply pseudo markdown parser for message.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return pseudo_markdown(self.message)
|
||||
|
||||
|
||||
|
||||
class BatchSlot(models.Model):
|
||||
_name = 'runbot.batch.slot'
|
||||
_description = 'Link between a bundle batch and a build'
|
||||
_order = 'trigger_id,id'
|
||||
|
||||
_fa_link_type = {'created': 'hashtag', 'matched': 'link', 'rebuild': 'refresh'}
|
||||
|
||||
batch_id = fields.Many2one('runbot.batch', index=True)
|
||||
trigger_id = fields.Many2one('runbot.trigger', index=True)
|
||||
build_id = fields.Many2one('runbot.build', index=True)
|
||||
params_id = fields.Many2one('runbot.build.params', index=True, required=True)
|
||||
link_type = fields.Selection([('created', 'Build created'), ('matched', 'Existing build matched'), ('rebuild', 'Rebuild')], required=True) # rebuild type?
|
||||
active = fields.Boolean('Attached', default=True)
|
||||
skipped = fields.Boolean('Skipped', default=False)
|
||||
# rebuild, what to do: since build can be in multiple batch:
|
||||
# - replace for all batch?
|
||||
# - only available on batch and replace for batch only?
|
||||
# - create a new bundle batch will new linked build?
|
||||
|
||||
def fa_link_type(self):
|
||||
return self._fa_link_type.get(self.link_type, 'exclamation-triangle')
|
||||
|
||||
def _create_missing_build(self):
|
||||
"""Create a build when the slot does not have one"""
|
||||
self.ensure_one()
|
||||
if self.build_id:
|
||||
return self.build_id
|
||||
self.link_type, self.build_id = self.batch_id._create_build(self.params_id)
|
||||
return self.build_id
|
@ -1,354 +1,221 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from subprocess import CalledProcessError
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo import models, fields, api
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class runbot_branch(models.Model):
|
||||
|
||||
_name = "runbot.branch"
|
||||
class Branch(models.Model):
|
||||
_name = 'runbot.branch'
|
||||
_description = "Branch"
|
||||
_order = 'name'
|
||||
_sql_constraints = [('branch_repo_uniq', 'unique (name,repo_id)', 'The branch must be unique per repository !')]
|
||||
_sql_constraints = [('branch_repo_uniq', 'unique (name,remote_id)', 'The branch must be unique per repository !')]
|
||||
|
||||
repo_id = fields.Many2one('runbot.repo', 'Repository', required=True, ondelete='cascade')
|
||||
duplicate_repo_id = fields.Many2one('runbot.repo', 'Duplicate Repository', related='repo_id.duplicate_id',)
|
||||
name = fields.Char('Ref Name', required=True)
|
||||
branch_name = fields.Char(compute='_get_branch_infos', string='Branch', readonly=1, store=True)
|
||||
branch_url = fields.Char(compute='_get_branch_url', string='Branch url', readonly=1)
|
||||
pull_head_name = fields.Char(compute='_get_branch_infos', string='PR HEAD name', readonly=1, store=True)
|
||||
target_branch_name = fields.Char(compute='_get_branch_infos', string='PR target branch', store=True)
|
||||
pull_branch_name = fields.Char(compute='_compute_pull_branch_name', string='Branch display name')
|
||||
sticky = fields.Boolean('Sticky')
|
||||
closest_sticky = fields.Many2one('runbot.branch', compute='_compute_closest_sticky', string='Closest sticky')
|
||||
defined_sticky = fields.Many2one('runbot.branch', string='Force sticky')
|
||||
previous_version = fields.Many2one('runbot.branch', compute='_compute_previous_version', string='Previous version branch')
|
||||
intermediate_stickies = fields.Many2many('runbot.branch', compute='_compute_intermediate_stickies', string='Intermediates stickies')
|
||||
coverage_result = fields.Float(compute='_compute_coverage_result', type='Float', string='Last coverage', store=False) # non optimal search in loop, could we store this result ? or optimise
|
||||
state = fields.Char('Status')
|
||||
modules = fields.Char("Modules to Install", help="Comma-separated list of modules to install and test.")
|
||||
priority = fields.Boolean('Build priority', default=False)
|
||||
no_build = fields.Boolean("Forbid creation of build on this branch", default=False)
|
||||
no_auto_build = fields.Boolean("Don't automatically build commit on this branch", default=False)
|
||||
rebuild_requested = fields.Boolean("Request a rebuild", help="Rebuild the latest commit even when no_auto_build is set.", default=False)
|
||||
name = fields.Char('Name', required=True)
|
||||
remote_id = fields.Many2one('runbot.remote', 'Remote', required=True, ondelete='cascade')
|
||||
|
||||
branch_config_id = fields.Many2one('runbot.build.config', 'Branch Config')
|
||||
config_id = fields.Many2one('runbot.build.config', 'Run Config', compute='_compute_config_id', inverse='_inverse_config_id')
|
||||
head = fields.Many2one('runbot.commit', 'Head Commit', index=True)
|
||||
head_name = fields.Char('Head name', related='head.name', store=True)
|
||||
|
||||
make_stats = fields.Boolean('Extract stats from logs', compute='_compute_make_stats', store=True)
|
||||
reference_name = fields.Char(compute='_compute_reference_name', string='Bundle name', store=True)
|
||||
bundle_id = fields.Many2one('runbot.bundle', 'Bundle', compute='_compute_bundle_id', store=True, ondelete='cascade', index=True)
|
||||
|
||||
@api.depends('sticky', 'defined_sticky', 'target_branch_name', 'name')
|
||||
# won't be recompute if a new branch is marked as sticky or sticky is removed, but should be ok if not stored
|
||||
def _compute_closest_sticky(self):
|
||||
is_pr = fields.Boolean('IS a pr', required=True)
|
||||
pull_head_name = fields.Char(compute='_compute_branch_infos', string='PR HEAD name', readonly=1, store=True)
|
||||
pull_head_remote_id = fields.Many2one('runbot.remote', 'Pull head repository', compute='_compute_branch_infos', store=True, index=True)
|
||||
target_branch_name = fields.Char(compute='_compute_branch_infos', string='PR target branch', store=True)
|
||||
|
||||
reflog_ids = fields.One2many('runbot.ref.log', 'branch_id')
|
||||
|
||||
branch_url = fields.Char(compute='_compute_branch_url', string='Branch url', readonly=1)
|
||||
dname = fields.Char('Display name', compute='_compute_dname', search='_search_dname')
|
||||
|
||||
alive = fields.Boolean('Alive', default=True)
|
||||
|
||||
@api.depends('name', 'remote_id.short_name')
|
||||
def _compute_dname(self):
|
||||
for branch in self:
|
||||
if branch.sticky:
|
||||
branch.closest_sticky = branch
|
||||
elif branch.defined_sticky:
|
||||
branch.closest_sticky = branch.defined_sticky # be carefull with loop
|
||||
elif branch.target_branch_name:
|
||||
corresponding_branch = self.search([('branch_name', '=', branch.target_branch_name), ('repo_id', '=', branch.repo_id.id)])
|
||||
branch.closest_sticky = corresponding_branch.closest_sticky
|
||||
branch.dname = '%s:%s' % (branch.remote_id.short_name, branch.name)
|
||||
|
||||
def _search_dname(self, operator, value):
|
||||
if ':' not in value:
|
||||
return [('name', operator, 'value')]
|
||||
repo_short_name, branch_name = value.split(':')
|
||||
owner, repo_name = repo_short_name.split('/')
|
||||
return ['&', ('remote_id', '=', self.env['runbot.remote'].search([('owner', '=', owner), ('repo_name', '=', repo_name)]).id), ('name', operator, branch_name)]
|
||||
|
||||
@api.depends('name', 'is_pr', 'target_branch_name', 'pull_head_name', 'pull_head_remote_id')
|
||||
def _compute_reference_name(self):
|
||||
"""
|
||||
Unique reference for a branch inside a bundle.
|
||||
- branch_name for branches
|
||||
- branch name part of pull_head_name for pr if remote is known
|
||||
- pull_head_name (organisation:branch_name) for external pr
|
||||
"""
|
||||
for branch in self:
|
||||
if branch.is_pr:
|
||||
_, name = branch.pull_head_name.split(':')
|
||||
if branch.pull_head_remote_id:
|
||||
branch.reference_name = name
|
||||
else:
|
||||
repo_ids = (branch.repo_id | branch.repo_id.duplicate_id).ids
|
||||
self.env.cr.execute("select id from runbot_branch where sticky = 't' and repo_id = any(%s) and %s like name||'%%'", (repo_ids, branch.name or ''))
|
||||
branch.closest_sticky = self.browse(self.env.cr.fetchone())
|
||||
|
||||
@api.depends('closest_sticky') #, 'closest_sticky.previous_version')
|
||||
def _compute_previous_version(self):
|
||||
for branch in self.sorted(key='sticky', reverse=True):
|
||||
# orm does not support non_searchable.non_stored dependency.
|
||||
# thus, the closest_sticky.previous_version dependency will log an error
|
||||
# when previous_version is written.
|
||||
# this dependency is usefull to make the compute recursive, avoiding to have
|
||||
# both record and record.closest_sticky in self, in that order, making the record.previous_version
|
||||
# empty in all cases.
|
||||
# Sorting self on sticky will mitigate the problem. but it is still posible to
|
||||
# have computation errors if defined_sticky is not sticky. (which is not a normal use case)
|
||||
if branch.closest_sticky == branch:
|
||||
repo_ids = (branch.repo_id | branch.repo_id.duplicate_id).ids
|
||||
domain = [('branch_name', 'like', '%.0'), ('sticky', '=', True), ('branch_name', '!=', 'master'), ('repo_id', 'in', repo_ids)]
|
||||
if branch.branch_name != 'master' and branch.id:
|
||||
domain += [('id', '<', branch.id)]
|
||||
branch.previous_version = self.search(domain, limit=1, order='id desc')
|
||||
branch.reference_name = branch.pull_head_name # repo is not known, not in repo list must be an external pr, so use complete label
|
||||
#if ':patch-' in branch.pull_head_name:
|
||||
# branch.reference_name = '%s~%s' % (branch.pull_head_name, branch.name)
|
||||
else:
|
||||
branch.previous_version = branch.closest_sticky.previous_version
|
||||
|
||||
@api.depends('previous_version', 'closest_sticky')
|
||||
def _compute_intermediate_stickies(self):
|
||||
for branch in self.sorted(key='sticky', reverse=True):
|
||||
if branch.closest_sticky == branch:
|
||||
if not branch.previous_version:
|
||||
branch.intermediate_stickies = [(5, 0, 0)]
|
||||
continue
|
||||
repo_ids = (branch.repo_id | branch.repo_id.duplicate_id).ids
|
||||
domain = [('id', '>', branch.previous_version.id), ('sticky', '=', True), ('branch_name', '!=', 'master'), ('repo_id', 'in', repo_ids)]
|
||||
if branch.closest_sticky.branch_name != 'master' and branch.closest_sticky.id:
|
||||
domain += [('id', '<', branch.closest_sticky.id)]
|
||||
branch.intermediate_stickies = [(6, 0, self.search(domain, order='id desc').ids)]
|
||||
else:
|
||||
branch.intermediate_stickies = [(6, 0, branch.closest_sticky.intermediate_stickies.ids)]
|
||||
|
||||
def _compute_config_id(self):
|
||||
for branch in self:
|
||||
if branch.branch_config_id:
|
||||
branch.config_id = branch.branch_config_id
|
||||
else:
|
||||
branch.config_id = branch.repo_id.config_id
|
||||
|
||||
def _inverse_config_id(self):
|
||||
for branch in self:
|
||||
branch.branch_config_id = branch.config_id
|
||||
|
||||
def _compute_pull_branch_name(self):
|
||||
for branch in self:
|
||||
branch.pull_branch_name = branch.pull_head_name.split(':')[-1] if branch.pull_head_name else branch.branch_name
|
||||
|
||||
@api.depends('sticky')
|
||||
def _compute_make_stats(self):
|
||||
for branch in self:
|
||||
branch.make_stats = branch.sticky
|
||||
branch.reference_name = branch.name
|
||||
|
||||
@api.depends('name')
|
||||
def _get_branch_infos(self, pull_info=None):
|
||||
"""compute branch_name, branch_url, pull_head_name and target_branch_name based on name"""
|
||||
def _compute_branch_infos(self, pull_info=None):
|
||||
"""compute branch_url, pull_head_name and target_branch_name based on name"""
|
||||
name_to_remote = {}
|
||||
prs = self.filtered(lambda branch: branch.is_pr)
|
||||
pull_info_dict = {}
|
||||
if not pull_info and len(prs) > 30: # this is arbitrary, we should store # page on remote
|
||||
pr_per_remote = defaultdict(list)
|
||||
for pr in prs:
|
||||
pr_per_remote[pr.remote_id].append(pr)
|
||||
for remote, prs in pr_per_remote.items():
|
||||
_logger.info('Getting info in %s for %s pr using page scan', remote.name, len(prs))
|
||||
pr_names = set([pr.name for pr in prs])
|
||||
count = 0
|
||||
for result in remote._github('/repos/:owner/:repo/pulls?state=all&sort=updated&direction=desc', ignore_errors=True, recursive=True):
|
||||
for info in result:
|
||||
number = str(info.get('number'))
|
||||
pr_names.discard(number)
|
||||
pull_info_dict[(remote, number)] = info
|
||||
count += 1
|
||||
if not pr_names:
|
||||
break
|
||||
if count > 100:
|
||||
_logger.info('Not all pr found after 100 pages: remaining: %s', pr_names)
|
||||
break
|
||||
|
||||
for branch in self:
|
||||
branch.target_branch_name = False
|
||||
branch.pull_head_name = False
|
||||
branch.pull_head_remote_id = False
|
||||
if branch.name:
|
||||
branch.branch_name = branch.name.split('/')[-1]
|
||||
pi = pull_info or branch._get_pull_info()
|
||||
pi = branch.is_pr and (pull_info or pull_info_dict.get((branch.remote_id, branch.name)) or branch._get_pull_info())
|
||||
if pi:
|
||||
try:
|
||||
branch.target_branch_name = pi['base']['ref']
|
||||
branch.pull_head_name = pi['head']['label']
|
||||
else:
|
||||
branch.branch_name = ''
|
||||
pull_head_repo_name = False
|
||||
if pi['head'].get('repo'):
|
||||
pull_head_repo_name = pi['head']['repo'].get('full_name')
|
||||
if pull_head_repo_name not in name_to_remote:
|
||||
owner, repo_name = pull_head_repo_name.split('/')
|
||||
name_to_remote[pull_head_repo_name] = self.env['runbot.remote'].search([('owner', '=', owner), ('repo_name', '=', repo_name)], limit=1)
|
||||
branch.pull_head_remote_id = name_to_remote[pull_head_repo_name]
|
||||
except (TypeError, AttributeError):
|
||||
_logger.exception('Error for pr %s using pull_info %s', branch.name, pi)
|
||||
raise
|
||||
|
||||
def recompute_infos(self):
|
||||
""" public method to recompute infos on demand """
|
||||
self._get_branch_infos()
|
||||
|
||||
@api.depends('branch_name')
|
||||
def _get_branch_url(self):
|
||||
"""compute the branch url based on branch_name"""
|
||||
@api.depends('name', 'remote_id.base_url', 'is_pr')
|
||||
def _compute_branch_url(self):
|
||||
"""compute the branch url based on name"""
|
||||
for branch in self:
|
||||
if branch.name:
|
||||
if re.match('^[0-9]+$', branch.branch_name):
|
||||
branch.branch_url = "https://%s/pull/%s" % (branch.repo_id.base, branch.branch_name)
|
||||
if branch.is_pr:
|
||||
branch.branch_url = "https://%s/pull/%s" % (branch.remote_id.base_url, branch.name)
|
||||
else:
|
||||
branch.branch_url = "https://%s/tree/%s" % (branch.repo_id.base, branch.branch_name)
|
||||
branch.branch_url = "https://%s/tree/%s" % (branch.remote_id.base_url, branch.name)
|
||||
else:
|
||||
branch.branch_url = ''
|
||||
|
||||
@api.depends('reference_name', 'remote_id.repo_id.project_id')
|
||||
def _compute_bundle_id(self):
|
||||
dummy = self.env.ref('runbot.bundle_dummy')
|
||||
for branch in self:
|
||||
if branch.bundle_id == dummy:
|
||||
continue
|
||||
name = branch.reference_name
|
||||
project = branch.remote_id.repo_id.project_id or self.env.ref('runbot.main_project')
|
||||
project.ensure_one()
|
||||
bundle = self.env['runbot.bundle'].search([('name', '=', name), ('project_id', '=', project.id)])
|
||||
need_new_base = not bundle and branch.match_is_base(name)
|
||||
if (bundle.is_base or need_new_base) and branch.remote_id != branch.remote_id.repo_id.main_remote_id:
|
||||
_logger.warning('Trying to add a dev branch to base bundle, falling back on dummy bundle')
|
||||
bundle = dummy
|
||||
elif name and branch.remote_id and branch.remote_id.repo_id._is_branch_forbidden(name):
|
||||
_logger.warning('Trying to add a forbidden branch, falling back on dummy bundle')
|
||||
bundle = dummy
|
||||
elif bundle.is_base and branch.is_pr:
|
||||
_logger.warning('Trying to add pr to base bundle, falling back on dummy bundle')
|
||||
bundle = dummy
|
||||
elif not bundle:
|
||||
values = {
|
||||
'name': name,
|
||||
'project_id': project.id,
|
||||
}
|
||||
if need_new_base:
|
||||
values['is_base'] = True
|
||||
|
||||
if branch.is_pr and branch.target_branch_name: # most likely external_pr, use target as version
|
||||
base = self.env['runbot.bundle'].search([
|
||||
('name', '=', branch.target_branch_name),
|
||||
('is_base', '=', True),
|
||||
('project_id', '=', project.id)
|
||||
])
|
||||
if base:
|
||||
values['defined_base_id'] = base.id
|
||||
if name:
|
||||
bundle = self.env['runbot.bundle'].create(values) # this prevent creating a branch in UI
|
||||
branch.bundle_id = bundle
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, value_list):
|
||||
branches = super().create(value_list)
|
||||
for branch in branches:
|
||||
if branch.head:
|
||||
self.env['runbot.ref.log'].create({'commit_id': branch.head.id, 'branch_id': branch.id})
|
||||
return branches
|
||||
|
||||
def write(self, values):
|
||||
if 'head' in values:
|
||||
head = self.head
|
||||
super().write(values)
|
||||
if 'head' in values and head != self.head:
|
||||
self.env['runbot.ref.log'].create({'commit_id': self.head.id, 'branch_id': self.id})
|
||||
|
||||
def _get_pull_info(self):
|
||||
self.ensure_one()
|
||||
repo = self.repo_id
|
||||
if repo.token and self.name.startswith('refs/pull/'):
|
||||
pull_number = self.name[len('refs/pull/'):]
|
||||
return repo._github('/repos/:owner/:repo/pulls/%s' % pull_number, ignore_errors=True) or {}
|
||||
remote = self.remote_id
|
||||
if self.is_pr:
|
||||
_logger.info('Getting info for %s', self.name)
|
||||
return remote._github('/repos/:owner/:repo/pulls/%s' % self.name, ignore_errors=False) or {} # TODO catch and send a managable exception
|
||||
return {}
|
||||
|
||||
def _is_on_remote(self):
|
||||
# check that a branch still exists on remote
|
||||
self.ensure_one()
|
||||
branch = self
|
||||
repo = branch.repo_id
|
||||
try:
|
||||
repo._git(['ls-remote', '-q', '--exit-code', repo.name, branch.name])
|
||||
except CalledProcessError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_last_branch_name_builds(self):
|
||||
# naive way to find corresponding build, only matching branch name or pr pull_head_name and target_branch_name.
|
||||
self.ensure_one()
|
||||
domain = []
|
||||
if self.pull_head_name:
|
||||
domain = [('pull_head_name', 'like', '%%:%s' % self.pull_head_name.split(':')[-1]), ('target_branch_name', '=', self.target_branch_name)] # pr matching pull head name
|
||||
else:
|
||||
domain = [('name', '=', self.name)]
|
||||
#domain += [('id', '!=', self.branch_id.id)]
|
||||
|
||||
e = expression.expression(domain, self)
|
||||
where_clause, where_params = e.to_sql()
|
||||
|
||||
repo_ids = tuple(self.env['runbot.repo'].search([]).ids) # access rights
|
||||
query = """
|
||||
SELECT max(b.id)
|
||||
FROM runbot_build b
|
||||
JOIN runbot_branch br ON br.id = b.branch_id
|
||||
|
||||
WHERE b.branch_id IN (
|
||||
SELECT id from runbot_branch WHERE %s
|
||||
)
|
||||
AND b.build_type IN ('normal', 'rebuild')
|
||||
AND b.repo_id in %%s
|
||||
AND (b.hidden = false OR b.hidden IS NULL)
|
||||
AND b.parent_id IS NULL
|
||||
AND (br.no_build = false OR br.no_build IS NULL)
|
||||
GROUP BY b.repo_id
|
||||
""" % where_clause
|
||||
|
||||
self.env.cr.execute(query, where_params + [repo_ids])
|
||||
results = [r[0] for r in self.env.cr.fetchall()]
|
||||
return self.env['runbot.build'].browse(results)
|
||||
|
||||
@api.model_create_single
|
||||
def create(self, vals):
|
||||
if not vals.get('config_id') and ('use-coverage' in (vals.get('name') or '')):
|
||||
coverage_config = self.env.ref('runbot.runbot_build_config_test_coverage', raise_if_not_found=False)
|
||||
if coverage_config:
|
||||
vals['config_id'] = coverage_config
|
||||
|
||||
return super(runbot_branch, self).create(vals)
|
||||
|
||||
def _get_last_coverage_build(self):
|
||||
""" Return the last build with a coverage value > 0"""
|
||||
self.ensure_one()
|
||||
return self.env['runbot.build'].search([
|
||||
('branch_id.id', '=', self.id),
|
||||
('local_state', 'in', ['done', 'running']),
|
||||
('coverage_result', '>=', 0.0),
|
||||
], order='sequence desc', limit=1)
|
||||
|
||||
def _compute_coverage_result(self):
|
||||
""" Compute the coverage result of the last build in branch """
|
||||
for branch in self:
|
||||
last_build = branch._get_last_coverage_build()
|
||||
branch.coverage_result = last_build.coverage_result or 0.0
|
||||
|
||||
def _get_closest_branch(self, target_repo_id):
|
||||
"""
|
||||
Return branch id of the closest branch based on name or pr informations.
|
||||
"""
|
||||
self.ensure_one()
|
||||
Branch = self.env['runbot.branch']
|
||||
|
||||
repo = self.repo_id
|
||||
name = self.pull_head_name or self.branch_name
|
||||
|
||||
target_repo = self.env['runbot.repo'].browse(target_repo_id)
|
||||
|
||||
target_repo_ids = [target_repo.id]
|
||||
r = target_repo.duplicate_id
|
||||
while r:
|
||||
if r.id in target_repo_ids:
|
||||
break
|
||||
target_repo_ids.append(r.id)
|
||||
r = r.duplicate_id
|
||||
|
||||
_logger.debug('Search closest of %s (%s) in repos %r', name, repo.name, target_repo_ids)
|
||||
|
||||
def sort_by_repo(branch):
|
||||
return (
|
||||
not branch.sticky, # sticky first
|
||||
target_repo_ids.index(branch.repo_id[0].id),
|
||||
-1 * len(branch.branch_name), # little change of logic here, was only sorted on branch_name in prefix matching case before
|
||||
-1 * branch.id
|
||||
def ref(self):
|
||||
return 'refs/%s/%s/%s' % (
|
||||
self.remote_id.remote_name,
|
||||
'pull' if self.is_pr else 'heads',
|
||||
self.name
|
||||
)
|
||||
|
||||
# 1. same name, not a PR
|
||||
if not self.pull_head_name: # not a pr
|
||||
domain = [
|
||||
('repo_id', 'in', target_repo_ids),
|
||||
('branch_name', '=', self.branch_name),
|
||||
('name', '=like', 'refs/heads/%'),
|
||||
]
|
||||
targets = Branch.search(domain, order='id DESC')
|
||||
targets = sorted(targets, key=sort_by_repo)
|
||||
if targets and targets[0]._is_on_remote():
|
||||
return (targets[0], 'exact')
|
||||
def recompute_infos(self):
|
||||
""" public method to recompute infos on demand """
|
||||
self._compute_branch_infos()
|
||||
|
||||
# 2. PR with head name equals
|
||||
if self.pull_head_name:
|
||||
domain = [
|
||||
('repo_id', 'in', target_repo_ids),
|
||||
('pull_head_name', '=', self.pull_head_name),
|
||||
('name', '=like', 'refs/pull/%'),
|
||||
]
|
||||
pulls = Branch.search(domain, order='id DESC')
|
||||
pulls = sorted(pulls, key=sort_by_repo)
|
||||
for pull in Branch.browse([pu['id'] for pu in pulls]):
|
||||
pi = pull._get_pull_info()
|
||||
if pi.get('state') == 'open':
|
||||
if ':' in self.pull_head_name:
|
||||
(repo_name, pr_branch_name) = self.pull_head_name.split(':')
|
||||
repo = self.env['runbot.repo'].browse(target_repo_ids).filtered(lambda r: ':%s/' % repo_name in r.name)
|
||||
# most of the time repo will be pull.repo_id.duplicate_id, but it is still possible to have a pr pointing the same repo
|
||||
if repo:
|
||||
pr_branch_ref = 'refs/heads/%s' % pr_branch_name
|
||||
pr_branch = self._get_or_create_branch(repo.id, pr_branch_ref)
|
||||
# use _get_or_create_branch in case a pr is scanned before pull_head_name branch.
|
||||
return (pr_branch, 'exact PR')
|
||||
return (pull, 'exact PR')
|
||||
|
||||
# 4.Match a PR in enterprise without community PR
|
||||
# Moved before 3 because it makes more sense
|
||||
if self.pull_head_name:
|
||||
if self.name.startswith('refs/pull'):
|
||||
if ':' in self.pull_head_name:
|
||||
(repo_name, pr_branch_name) = self.pull_head_name.split(':')
|
||||
repos = self.env['runbot.repo'].browse(target_repo_ids).filtered(lambda r: ':%s/' % repo_name in r.name)
|
||||
else:
|
||||
pr_branch_name = self.pull_head_name
|
||||
repos = target_repo
|
||||
if repos:
|
||||
duplicate_branch_name = 'refs/heads/%s' % pr_branch_name
|
||||
domain = [
|
||||
('repo_id', 'in', tuple(repos.ids)),
|
||||
('branch_name', '=', pr_branch_name),
|
||||
('pull_head_name', '=', False),
|
||||
]
|
||||
targets = Branch.search(domain, order='id DESC')
|
||||
targets = sorted(targets, key=sort_by_repo)
|
||||
if targets and targets[0]._is_on_remote():
|
||||
return (targets[0], 'no PR')
|
||||
|
||||
# 3. Match a branch which is the dashed-prefix of current branch name
|
||||
if not self.pull_head_name:
|
||||
if '-' in self.branch_name:
|
||||
name_start = 'refs/heads/%s' % self.branch_name.split('-')[0]
|
||||
domain = [('repo_id', 'in', target_repo_ids), ('name', '=like', '%s%%' % name_start)]
|
||||
branches = Branch.search(domain, order='id DESC')
|
||||
branches = sorted(branches, key=sort_by_repo)
|
||||
for branch in branches:
|
||||
if self.branch_name.startswith('%s-' % branch.branch_name) and branch._is_on_remote():
|
||||
return (branch, 'prefix')
|
||||
|
||||
# 5. last-resort value
|
||||
if self.target_branch_name:
|
||||
default_target_ref = 'refs/heads/%s' % self.target_branch_name
|
||||
default_branch = self.search([('repo_id', 'in', target_repo_ids), ('name', '=', default_target_ref)], limit=1)
|
||||
if default_branch:
|
||||
return (default_branch, 'pr_target')
|
||||
|
||||
default_target_ref = 'refs/heads/master'
|
||||
default_branch = self.search([('repo_id', 'in', target_repo_ids), ('name', '=', default_target_ref)], limit=1)
|
||||
# we assume that master will always exists
|
||||
return (default_branch, 'default')
|
||||
|
||||
def _branch_exists(self, branch_id):
|
||||
Branch = self.env['runbot.branch']
|
||||
branch = Branch.search([('id', '=', branch_id)])
|
||||
if branch and branch[0]._is_on_remote():
|
||||
return True
|
||||
@api.model
|
||||
def match_is_base(self, name):
|
||||
"""match against is_base_regex ir.config_parameter"""
|
||||
if not name:
|
||||
return False
|
||||
icp = self.env['ir.config_parameter'].sudo()
|
||||
regex = icp.get_param('runbot.runbot_is_base_regex', False)
|
||||
if regex:
|
||||
return re.match(regex, name)
|
||||
|
||||
def _get_or_create_branch(self, repo_id, name):
|
||||
res = self.search([('repo_id', '=', repo_id), ('name', '=', name)], limit=1)
|
||||
if res:
|
||||
return res
|
||||
_logger.warning('creating missing branch %s', name)
|
||||
Branch = self.env['runbot.branch']
|
||||
branch = Branch.create({'repo_id': repo_id, 'name': name})
|
||||
return branch
|
||||
|
||||
def toggle_request_branch_rebuild(self):
|
||||
for branch in self:
|
||||
if not branch.rebuild_requested:
|
||||
branch.rebuild_requested = True
|
||||
branch.repo_id.sudo().set_hook_time(time.time())
|
||||
else:
|
||||
branch.rebuild_requested = False
|
||||
class RefLog(models.Model):
|
||||
_name = 'runbot.ref.log'
|
||||
_description = 'Ref log'
|
||||
_log_access = False
|
||||
|
||||
commit_id = fields.Many2one('runbot.commit', index=True)
|
||||
branch_id = fields.Many2one('runbot.branch', index=True)
|
||||
date = fields.Datetime(default=fields.Datetime.now)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +1,15 @@
|
||||
import base64
|
||||
import glob
|
||||
import logging
|
||||
import fnmatch
|
||||
import re
|
||||
import shlex
|
||||
import time
|
||||
from ..common import now, grep, time2str, rfind, Commit, s2human, os
|
||||
from ..common import now, grep, time2str, rfind, s2human, os, RunbotException
|
||||
from ..container import docker_run, docker_get_gateway_ip, Command
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval, test_python_expr
|
||||
from odoo.addons.runbot.models.repo import RunbotException
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@ -20,18 +20,17 @@ PYTHON_DEFAULT = "# type python code here\n\n\n\n\n\n"
|
||||
|
||||
|
||||
class Config(models.Model):
|
||||
_name = "runbot.build.config"
|
||||
_name = 'runbot.build.config'
|
||||
_description = "Build config"
|
||||
_inherit = "mail.thread"
|
||||
|
||||
name = fields.Char('Config name', required=True, unique=True, track_visibility='onchange', help="Unique name for config please use trigram as postfix for custom configs")
|
||||
name = fields.Char('Config name', required=True, tracking=True, help="Unique name for config please use trigram as postfix for custom configs")
|
||||
|
||||
description = fields.Char('Config description')
|
||||
step_order_ids = fields.One2many('runbot.build.config.step.order', 'config_id', copy=True)
|
||||
update_github_state = fields.Boolean('Notify build state to github', default=False, track_visibility='onchange')
|
||||
protected = fields.Boolean('Protected', default=False, track_visibility='onchange')
|
||||
protected = fields.Boolean('Protected', default=False, tracking=True)
|
||||
group = fields.Many2one('runbot.build.config', 'Configuration group', help="Group of config's and config steps")
|
||||
group_name = fields.Char('Group name', related='group.name')
|
||||
monitoring_view_id = fields.Many2one('ir.ui.view', 'Monitoring view')
|
||||
|
||||
@api.model_create_single
|
||||
def create(self, values):
|
||||
@ -70,7 +69,7 @@ class Config(models.Model):
|
||||
raise UserError('Jobs of type run_odoo should be preceded by a job of type install_odoo')
|
||||
self._check_recustion()
|
||||
|
||||
def _check_recustion(self, visited=None): # todo test
|
||||
def _check_recustion(self, visited=None):
|
||||
visited = visited or []
|
||||
recursion = False
|
||||
if self in visited:
|
||||
@ -84,52 +83,88 @@ class Config(models.Model):
|
||||
create_config._check_recustion(visited[:])
|
||||
|
||||
|
||||
class ConfigStepUpgradeDb(models.Model):
|
||||
_name = 'runbot.config.step.upgrade.db'
|
||||
_description = "Config Step Upgrade Db"
|
||||
|
||||
step_id = fields.Many2one('runbot.build.config.step', 'Step')
|
||||
config_id = fields.Many2one('runbot.build.config', 'Config')
|
||||
db_pattern = fields.Char('Db suffix pattern')
|
||||
min_target_version_id = fields.Many2one('runbot.version', "Minimal target version_id")
|
||||
|
||||
|
||||
class ConfigStep(models.Model):
|
||||
_name = 'runbot.build.config.step'
|
||||
_description = "Config step"
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
# general info
|
||||
name = fields.Char('Step name', required=True, unique=True, track_visibility='onchange', help="Unique name for step please use trigram as postfix for custom step_ids")
|
||||
name = fields.Char('Step name', required=True, unique=True, tracking=True, help="Unique name for step please use trigram as postfix for custom step_ids")
|
||||
domain_filter = fields.Char('Domain filter', tracking=True)
|
||||
job_type = fields.Selection([
|
||||
('install_odoo', 'Test odoo'),
|
||||
('run_odoo', 'Run odoo'),
|
||||
('python', 'Python code'),
|
||||
('create_build', 'Create build'),
|
||||
], default='install_odoo', required=True, track_visibility='onchange')
|
||||
protected = fields.Boolean('Protected', default=False, track_visibility='onchange')
|
||||
default_sequence = fields.Integer('Sequence', default=100, track_visibility='onchange') # or run after? # or in many2many rel?
|
||||
('configure_upgrade', 'Configure Upgrade'),
|
||||
('configure_upgrade_complement', 'Configure Upgrade Complement'),
|
||||
('test_upgrade', 'Test Upgrade'),
|
||||
('restore', 'Restore')
|
||||
], default='install_odoo', required=True, tracking=True)
|
||||
protected = fields.Boolean('Protected', default=False, tracking=True)
|
||||
default_sequence = fields.Integer('Sequence', default=100, tracking=True) # or run after? # or in many2many rel?
|
||||
step_order_ids = fields.One2many('runbot.build.config.step.order', 'step_id')
|
||||
group = fields.Many2one('runbot.build.config', 'Configuration group', help="Group of config's and config steps")
|
||||
group_name = fields.Char('Group name', related='group.name')
|
||||
make_stats = fields.Boolean('Make stats', default=False)
|
||||
build_stat_regex_ids = fields.Many2many('runbot.build.stat.regex', string='Stats Regexes')
|
||||
# install_odoo
|
||||
create_db = fields.Boolean('Create Db', default=True, track_visibility='onchange') # future
|
||||
custom_db_name = fields.Char('Custom Db Name', track_visibility='onchange') # future
|
||||
create_db = fields.Boolean('Create Db', default=True, tracking=True) # future
|
||||
custom_db_name = fields.Char('Custom Db Name', tracking=True) # future
|
||||
install_modules = fields.Char('Modules to install', help="List of module patterns to install, use * to install all available modules, prefix the pattern with dash to remove the module.", default='')
|
||||
db_name = fields.Char('Db Name', compute='_compute_db_name', inverse='_inverse_db_name', track_visibility='onchange')
|
||||
cpu_limit = fields.Integer('Cpu limit', default=3600, track_visibility='onchange')
|
||||
coverage = fields.Boolean('Coverage', default=False, track_visibility='onchange')
|
||||
flamegraph = fields.Boolean('Allow Flamegraph', default=False, track_visibility='onchange')
|
||||
test_enable = fields.Boolean('Test enable', default=True, track_visibility='onchange')
|
||||
test_tags = fields.Char('Test tags', help="comma separated list of test tags", track_visibility='onchange')
|
||||
enable_auto_tags = fields.Boolean('Allow auto tag', default=False, track_visibility='onchange')
|
||||
sub_command = fields.Char('Subcommand', track_visibility='onchange')
|
||||
extra_params = fields.Char('Extra cmd args', track_visibility='onchange')
|
||||
additionnal_env = fields.Char('Extra env', help='Example: foo="bar",bar="foo". Cannot contains \' ', track_visibility='onchange')
|
||||
db_name = fields.Char('Db Name', compute='_compute_db_name', inverse='_inverse_db_name', tracking=True)
|
||||
cpu_limit = fields.Integer('Cpu limit', default=3600, tracking=True)
|
||||
coverage = fields.Boolean('Coverage', default=False, tracking=True)
|
||||
flamegraph = fields.Boolean('Allow Flamegraph', default=False, tracking=True)
|
||||
test_enable = fields.Boolean('Test enable', default=True, tracking=True)
|
||||
test_tags = fields.Char('Test tags', help="comma separated list of test tags", tracking=True)
|
||||
enable_auto_tags = fields.Boolean('Allow auto tag', default=False, tracking=True)
|
||||
sub_command = fields.Char('Subcommand', tracking=True)
|
||||
extra_params = fields.Char('Extra cmd args', tracking=True)
|
||||
additionnal_env = fields.Char('Extra env', help='Example: foo="bar",bar="foo". Cannot contains \' ', tracking=True)
|
||||
# python
|
||||
python_code = fields.Text('Python code', track_visibility='onchange', default=PYTHON_DEFAULT)
|
||||
python_result_code = fields.Text('Python code for result', track_visibility='onchange', default=PYTHON_DEFAULT)
|
||||
ignore_triggered_result = fields.Boolean('Ignore error triggered in logs', track_visibility='onchange', default=False)
|
||||
python_code = fields.Text('Python code', tracking=True, default=PYTHON_DEFAULT)
|
||||
python_result_code = fields.Text('Python code for result', tracking=True, default=PYTHON_DEFAULT)
|
||||
ignore_triggered_result = fields.Boolean('Ignore error triggered in logs', tracking=True, default=False)
|
||||
running_job = fields.Boolean('Job final state is running', default=False, help="Docker won't be killed if checked")
|
||||
# create_build
|
||||
create_config_ids = fields.Many2many('runbot.build.config', 'runbot_build_config_step_ids_create_config_ids_rel', string='New Build Configs', track_visibility='onchange', index=True)
|
||||
number_builds = fields.Integer('Number of build to create', default=1, track_visibility='onchange')
|
||||
hide_build = fields.Boolean('Hide created build in frontend', default=True, track_visibility='onchange')
|
||||
force_build = fields.Boolean("As a forced rebuild, don't use duplicate detection", default=False, track_visibility='onchange')
|
||||
force_host = fields.Boolean('Use same host as parent for children', default=False, track_visibility='onchange') # future
|
||||
make_orphan = fields.Boolean('No effect on the parent result', help='Created build result will not affect parent build result', default=False, track_visibility='onchange')
|
||||
create_config_ids = fields.Many2many('runbot.build.config', 'runbot_build_config_step_ids_create_config_ids_rel', string='New Build Configs', tracking=True, index=True)
|
||||
number_builds = fields.Integer('Number of build to create', default=1, tracking=True)
|
||||
|
||||
force_host = fields.Boolean('Use same host as parent for children', default=False, tracking=True) # future
|
||||
make_orphan = fields.Boolean('No effect on the parent result', help='Created build result will not affect parent build result', default=False, tracking=True)
|
||||
|
||||
# upgrade
|
||||
# 1. define target
|
||||
upgrade_to_master = fields.Boolean() # upgrade niglty + (future migration? no, need last master, not nightly master)
|
||||
upgrade_to_current = fields.Boolean(help="If checked, only upgrade to current will be used, other options will be ignored")
|
||||
upgrade_to_major_versions = fields.Boolean() # upgrade (no master)
|
||||
upgrade_to_all_versions = fields.Boolean() # upgrade niglty (no master)
|
||||
upgrade_to_version_ids = fields.Many2many('runbot.version', relation='runbot_upgrade_to_version_ids', string='Forced version to use as target')
|
||||
# 2. define source from target
|
||||
#upgrade_from_current = fields.Boolean() #usefull for future migration (13.0-dev/13.3-dev -> master) AVOID TO USE THAT
|
||||
upgrade_from_previous_major_version = fields.Boolean() # 13.0
|
||||
upgrade_from_last_intermediate_version = fields.Boolean() # 13.3
|
||||
upgrade_from_all_intermediate_version = fields.Boolean() # 13.2 # 13.1
|
||||
upgrade_from_version_ids = fields.Many2many('runbot.version', relation='runbot_upgrade_from_version_ids', string='Forced version to use as source (cartesian with target)')
|
||||
|
||||
upgrade_flat = fields.Boolean("Flat", help="Take all decisions in on build")
|
||||
|
||||
upgrade_config_id = fields.Many2one('runbot.build.config',string='Upgrade Config', tracking=True, index=True)
|
||||
upgrade_dbs = fields.One2many('runbot.config.step.upgrade.db', 'step_id', tracking=True)
|
||||
|
||||
restore_download_db_suffix = fields.Char('Download db suffix')
|
||||
restore_rename_db_suffix = fields.Char('Rename db suffix')
|
||||
|
||||
@api.constrains('python_code')
|
||||
def _check_python_code(self):
|
||||
@ -145,13 +180,6 @@ class ConfigStep(models.Model):
|
||||
if msg:
|
||||
raise ValidationError(msg)
|
||||
|
||||
@api.onchange('number_builds')
|
||||
def _onchange_number_builds(self):
|
||||
if self.number_builds > 1:
|
||||
self.force_build = True
|
||||
else:
|
||||
self.force_build = False
|
||||
|
||||
@api.onchange('sub_command')
|
||||
def _onchange_number_builds(self):
|
||||
if self.sub_command:
|
||||
@ -207,25 +235,15 @@ class ConfigStep(models.Model):
|
||||
def _run(self, build):
|
||||
log_path = build._path('logs', '%s.txt' % self.name)
|
||||
build.write({'job_start': now(), 'job_end': False}) # state, ...
|
||||
build._log('run', 'Starting step **%s** from config **%s**' % (self.name, build.config_id.name), log_type='markdown', level='SEPARATOR')
|
||||
build._log('run', 'Starting step **%s** from config **%s**' % (self.name, build.params_id.config_id.name), log_type='markdown', level='SEPARATOR')
|
||||
return self._run_step(build, log_path)
|
||||
|
||||
def _run_step(self, build, log_path):
|
||||
build.log_counter = self.env['ir.config_parameter'].sudo().get_param('runbot.runbot_maxlogs', 100)
|
||||
if self.job_type == 'run_odoo':
|
||||
return self._run_odoo_run(build, log_path)
|
||||
if self.job_type == 'install_odoo':
|
||||
return self._run_odoo_install(build, log_path)
|
||||
elif self.job_type == 'python':
|
||||
return self._run_python(build, log_path)
|
||||
elif self.job_type == 'create_build':
|
||||
return self._create_build(build, log_path)
|
||||
|
||||
def _create_build(self, build, log_path):
|
||||
Build = self.env['runbot.build']
|
||||
if self.force_build:
|
||||
Build = Build.with_context(force_rebuild=True)
|
||||
run_method = getattr(self, '_run_%s' % self.job_type)
|
||||
return run_method(build, log_path)
|
||||
|
||||
def _run_create_build(self, build, log_path):
|
||||
count = 0
|
||||
for create_config in self.create_config_ids:
|
||||
for _ in range(self.number_builds):
|
||||
@ -233,23 +251,8 @@ class ConfigStep(models.Model):
|
||||
if count > 200:
|
||||
build._logger('Too much build created')
|
||||
break
|
||||
children = Build.create({
|
||||
'dependency_ids': build._copy_dependency_ids(),
|
||||
'config_id': create_config.id,
|
||||
'parent_id': build.id,
|
||||
'branch_id': build.branch_id.id,
|
||||
'name': build.name,
|
||||
'build_type': build.build_type,
|
||||
'date': build.date,
|
||||
'author': build.author,
|
||||
'author_email': build.author_email,
|
||||
'committer': build.committer,
|
||||
'committer_email': build.committer_email,
|
||||
'subject': build.subject,
|
||||
'hidden': self.hide_build,
|
||||
'orphan_result': self.make_orphan,
|
||||
})
|
||||
build._log('create_build', 'created with config %s' % create_config.name, log_type='subbuild', path=str(children.id))
|
||||
child = build._add_child({'config_id': create_config.id}, orphan=self.make_orphan)
|
||||
build._log('create_build', 'created with config %s' % create_config.name, log_type='subbuild', path=str(child.id))
|
||||
|
||||
def make_python_ctx(self, build):
|
||||
return {
|
||||
@ -262,14 +265,14 @@ class ConfigStep(models.Model):
|
||||
'log_path': build._path('logs', '%s.txt' % self.name),
|
||||
'glob': glob.glob,
|
||||
'Command': Command,
|
||||
'Commit': Commit,
|
||||
'base64': base64,
|
||||
're': re,
|
||||
'time': time,
|
||||
'grep': grep,
|
||||
'rfind': rfind,
|
||||
}
|
||||
def _run_python(self, build, log_path): # TODO rework log_path after checking python steps, compute on build
|
||||
|
||||
def _run_python(self, build, log_path):
|
||||
eval_ctx = self.make_python_ctx(build)
|
||||
try:
|
||||
safe_eval(self.python_code.strip(), eval_ctx, mode="exec", nocopy=True)
|
||||
@ -283,14 +286,21 @@ class ConfigStep(models.Model):
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def _is_docker_step(self):
|
||||
if not self:
|
||||
return False
|
||||
self.ensure_one()
|
||||
return self.job_type in ('install_odoo', 'run_odoo') or (self.job_type == 'python' and 'docker_run(' in self.python_code)
|
||||
return self.job_type in ('install_odoo', 'run_odoo', 'restore', 'test_upgrade') or (self.job_type == 'python' and ('docker_run(' in self.python_code or '_run_install_odoo(' in self.python_code))
|
||||
|
||||
def _run_run_odoo(self, build, log_path, force=False):
|
||||
if not force:
|
||||
if build.parent_id:
|
||||
build._log('_run_run_odoo', 'build has a parent, skip run')
|
||||
return
|
||||
if build.no_auto_run:
|
||||
build._log('_run_run_odoo', 'build auto run is disabled, skip run')
|
||||
return
|
||||
|
||||
def _run_odoo_run(self, build, log_path):
|
||||
exports = build._checkout()
|
||||
# update job_start AFTER checkout to avoid build being killed too soon if checkout took some time and docker take some time to start
|
||||
build.job_start = now()
|
||||
@ -307,15 +317,17 @@ class ConfigStep(models.Model):
|
||||
# not sure, to avoid old server to check other dbs
|
||||
cmd += ["--max-cron-threads", "0"]
|
||||
|
||||
db_name = build.config_data.get('db_name') or [step.db_name for step in build.config_id.step_ids() if step.job_type == 'install_odoo'][-1]
|
||||
db_name = build.params_id.config_data.get('db_name') or [step.db_name for step in build.params_id.config_id.step_ids() if step.job_type == 'install_odoo'][-1]
|
||||
# we need to have at least one job of type install_odoo to run odoo, take the last one for db_name.
|
||||
cmd += ['-d', '%s-%s' % (build.dest, db_name)]
|
||||
|
||||
if grep(build._server("tools/config.py"), "proxy-mode") and build.repo_id.nginx:
|
||||
icp = self.env['ir.config_parameter'].sudo()
|
||||
nginx = icp.get_param('runbot.runbot_nginx', True)
|
||||
if grep(build._server("tools/config.py"), "proxy-mode") and nginx:
|
||||
cmd += ["--proxy-mode"]
|
||||
|
||||
if grep(build._server("tools/config.py"), "db-filter"):
|
||||
if build.repo_id.nginx:
|
||||
if nginx:
|
||||
cmd += ['--db-filter', '%d.*$']
|
||||
else:
|
||||
cmd += ['--db-filter', '%s.*$' % build.dest]
|
||||
@ -329,10 +341,10 @@ class ConfigStep(models.Model):
|
||||
self.env.cr.commit() # commit before docker run to be 100% sure that db state is consistent with dockers
|
||||
self.invalidate_cache()
|
||||
res = docker_run(cmd, log_path, build_path, docker_name, exposed_ports=[build_port, build_port + 1], ro_volumes=exports)
|
||||
build.repo_id._reload_nginx()
|
||||
self.env['runbot.runbot']._reload_nginx()
|
||||
return res
|
||||
|
||||
def _run_odoo_install(self, build, log_path):
|
||||
def _run_install_odoo(self, build, log_path):
|
||||
exports = build._checkout()
|
||||
# update job_start AFTER checkout to avoid build being killed too soon if checkout took some time and docker take some time to start
|
||||
build.job_start = now()
|
||||
@ -349,13 +361,13 @@ class ConfigStep(models.Model):
|
||||
python_params = ['-m', 'flamegraph', '-o', self._perfs_data_path()]
|
||||
cmd = build._cmd(python_params, py_version, sub_command=self.sub_command)
|
||||
# create db if needed
|
||||
db_suffix = build.config_data.get('db_name') or self.db_name
|
||||
db_name = "%s-%s" % (build.dest, db_suffix)
|
||||
db_suffix = build.params_id.config_data.get('db_name') or (build.params_id.dump_db.db_suffix if not self.create_db else False) or self.db_name
|
||||
db_name = '%s-%s' % (build.dest, db_suffix)
|
||||
if self.create_db:
|
||||
build._local_pg_createdb(db_name)
|
||||
cmd += ['-d', db_name]
|
||||
# list module to install
|
||||
extra_params = build.extra_params or self.extra_params or ''
|
||||
extra_params = build.params_id.extra_params or self.extra_params or ''
|
||||
if mods and '-i' not in extra_params:
|
||||
cmd += ['-i', mods]
|
||||
config_path = build._server("tools/config.py")
|
||||
@ -402,7 +414,7 @@ class ConfigStep(models.Model):
|
||||
cmd.finals.append(['pg_dump', db_name, '>', sql_dest])
|
||||
cmd.finals.append(['cp', '-r', filestore_path, filestore_dest])
|
||||
cmd.finals.append(['cd', dump_dir, '&&', 'zip', '-rmq9', zip_path, '*'])
|
||||
infos = '{\n "db_name": "%s",\n "build_id": %s,\n "shas": [%s]\n}' % (db_name, build.id, ', '.join(['"%s"' % commit for commit in build._get_all_commit()]))
|
||||
infos = '{\n "db_name": "%s",\n "build_id": %s,\n "shas": [%s]\n}' % (db_name, build.id, ', '.join(['"%s"' % build_commit.commit_id.dname for build_commit in build.params_id.commit_link_ids]))
|
||||
build.write_file('logs/%s/info.json' % db_name, infos)
|
||||
|
||||
if self.flamegraph:
|
||||
@ -410,9 +422,357 @@ class ConfigStep(models.Model):
|
||||
cmd.finals.append(['gzip', '-f', self._perfs_data_path()]) # keep data but gz them to save disc space
|
||||
max_timeout = int(self.env['ir.config_parameter'].get_param('runbot.runbot_timeout', default=10000))
|
||||
timeout = min(self.cpu_limit, max_timeout)
|
||||
env_variables = self.additionnal_env.split(',') if self.additionnal_env else []
|
||||
env_variables = self.additionnal_env.split(';') if self.additionnal_env else []
|
||||
return docker_run(cmd, log_path, build._path(), build._get_docker_name(), cpu_limit=timeout, ro_volumes=exports, env_variables=env_variables)
|
||||
|
||||
def _upgrade_create_childs(self):
|
||||
pass
|
||||
|
||||
def _run_configure_upgrade_complement(self, build, *args):
|
||||
"""
|
||||
Parameters:
|
||||
- upgrade_dumps_trigger_id: a configure_upgradestep
|
||||
|
||||
A complement aims to test the exact oposite of an upgrade trigger.
|
||||
Ignore configs an categories: only focus on versions.
|
||||
"""
|
||||
param = build.params_id
|
||||
version = param.version_id
|
||||
builds_references = param.builds_reference_ids
|
||||
builds_references_by_version_id = {b.params_id.version_id.id: b for b in builds_references}
|
||||
upgrade_complement_step = build.params_id.trigger_id.upgrade_dumps_trigger_id.upgrade_step_id
|
||||
version_domain = build.params_id.trigger_id.upgrade_dumps_trigger_id.get_version_domain()
|
||||
valid_targets = build.browse()
|
||||
next_versions = version.next_major_version_id | version.next_intermediate_version_ids
|
||||
if version_domain: # filter only on version where trigger is enabled
|
||||
next_versions = next_versions.filtered_domain(version_domain)
|
||||
if next_versions:
|
||||
for next_version in next_versions:
|
||||
if version in upgrade_complement_step._get_upgrade_source_versions(next_version):
|
||||
valid_targets |= (builds_references_by_version_id.get(next_version.id) or build.browse())
|
||||
|
||||
for target in valid_targets:
|
||||
build._log('', 'Checking upgrade to [%s](%s)' % (target.params_id.version_id.name, target.build_url), log_type='markdown')
|
||||
for upgrade_db in upgrade_complement_step.upgrade_dbs:
|
||||
if not upgrade_db.min_target_version_id or upgrade_db.min_target_version_id.number <= target.params_id.version_id.number:
|
||||
# note: here we don't consider the upgrade_db config here
|
||||
dbs = build.database_ids.sorted('db_suffix')
|
||||
for db in self._filter_upgrade_database(dbs, upgrade_db.db_pattern):
|
||||
child = build._add_child({
|
||||
'upgrade_to_build_id': target.id,
|
||||
'upgrade_from_build_id': build, # always current build
|
||||
'dump_db': db.id,
|
||||
'config_id': upgrade_complement_step.upgrade_config_id
|
||||
})
|
||||
child.description = 'Testing migration from %s to %s using parent db %s' % (
|
||||
version.name,
|
||||
target.params_id.version_id.name,
|
||||
db.name,
|
||||
)
|
||||
child._log('', 'This build tests change of schema in stable version testing upgrade to %s' % target.params_id.version_id.name)
|
||||
|
||||
def _run_configure_upgrade(self, build, log_path):
|
||||
"""
|
||||
Source/target parameters:
|
||||
- upgrade_to_current | (upgrade_to_master + (upgrade_to_major_versions | upgrade_to_all_versions))
|
||||
- upgrade_from_previous_major_version + (upgrade_from_all_intermediate_version | upgrade_from_last_intermediate_version)
|
||||
- upgrade_dbs
|
||||
- upgrade_to_version_ids (use instead of upgrade_to flags)
|
||||
- upgrade_from_version_ids (use instead of upgrade_from flags)
|
||||
|
||||
Other parameters
|
||||
- upgrade_flat
|
||||
- upgrade_config_id
|
||||
|
||||
Create subbuilds with parameters defined for a step of type test_upgrade:
|
||||
- upgrade_to_build_id
|
||||
- upgrade_from_build_id
|
||||
- dump_db
|
||||
- config_id (upgrade_config_id)
|
||||
|
||||
If upgrade_flat is False, a level of child will be create for target, source and dbs
|
||||
(if there is multiple choices).
|
||||
If upgrade_flat is True, all combination will be computed locally and only one level of children will be added to caller build.
|
||||
|
||||
Note:
|
||||
- This step should be alone in a config since this config is recursive
|
||||
- A typical upgrade_config_id should have a restore step and a test_upgrade step.
|
||||
"""
|
||||
assert len(build.parent_path.split('/')) < 6 # small security to avoid recursion loop, 6 is arbitrary
|
||||
param = build.params_id
|
||||
end = False
|
||||
target_builds = False
|
||||
source_builds_by_target = {}
|
||||
builds_references = param.builds_reference_ids
|
||||
builds_references_by_version_id = {b.params_id.version_id.id: b for b in builds_references}
|
||||
if param.upgrade_to_build_id:
|
||||
target_builds = param.upgrade_to_build_id
|
||||
else:
|
||||
if self.upgrade_to_current:
|
||||
target_builds = build
|
||||
else:
|
||||
target_builds = build.browse()
|
||||
if self.upgrade_to_version_ids:
|
||||
for version in self.upgrade_to_version_ids:
|
||||
target_builds |= builds_references_by_version_id.get(version.id) or build.browse()
|
||||
else:
|
||||
master_build = builds_references.filtered(lambda b: b.params_id.version_id.name == 'master')
|
||||
base_builds = (builds_references - master_build)
|
||||
if self.upgrade_to_master:
|
||||
target_builds = master_build
|
||||
if self.upgrade_to_major_versions:
|
||||
target_builds |= base_builds.filtered(lambda b: b.params_id.version_id.is_major)
|
||||
elif self.upgrade_to_all_versions:
|
||||
target_builds |= base_builds
|
||||
target_builds = target_builds.sorted(lambda b: b.params_id.version_id.number)
|
||||
if target_builds:
|
||||
build._log('', 'Testing upgrade targeting %s' % ', '.join(target_builds.mapped('params_id.version_id.name')))
|
||||
if not target_builds:
|
||||
build._log('_run_configure_upgrade', 'No reference build found with correct target in availables references, skipping. %s' % builds_references.mapped('params_id.version_id.name'), level='ERROR')
|
||||
end = True
|
||||
elif len(target_builds) > 1 and not self.upgrade_flat:
|
||||
for target_build in target_builds:
|
||||
build._add_child({'upgrade_to_build_id': target_build.id})
|
||||
end = True
|
||||
if end:
|
||||
return # replace this by a python job friendly solution
|
||||
|
||||
for target_build in target_builds:
|
||||
if param.upgrade_from_build_id:
|
||||
source_builds_by_target[target_build] = param.upgrade_from_build_id
|
||||
else:
|
||||
target_version = target_build.params_id.version_id
|
||||
from_builds = self._get_upgrade_source_builds(target_version, builds_references_by_version_id)
|
||||
source_builds_by_target[target_build] = from_builds
|
||||
if from_builds:
|
||||
build._log('', 'Defining source version(s) for %s: %s' % (target_build.params_id.version_id.name, ', '.join(source_builds_by_target[target_build].mapped('params_id.version_id.name'))))
|
||||
if not from_builds:
|
||||
build._log('_run_configure_upgrade', 'No source version found for %s, skipping' % target_version.name, level='INFO')
|
||||
elif not self.upgrade_flat:
|
||||
for from_build in from_builds:
|
||||
build._add_child({'upgrade_to_build_id': target_build.id, 'upgrade_from_build_id': from_build.id})
|
||||
end = True
|
||||
|
||||
if end:
|
||||
return # replace this by a python job friendly solution
|
||||
|
||||
assert not param.dump_db
|
||||
if not self.upgrade_dbs:
|
||||
build._log('configure_upgrade', 'No upgrade dbs defined in step %s' % self.name, level='WARN')
|
||||
for target, sources in source_builds_by_target.items():
|
||||
for source in sources:
|
||||
for upgrade_db in self.upgrade_dbs:
|
||||
if not upgrade_db.min_target_version_id or upgrade_db.min_target_version_id.number <= target.params_id.version_id.number:
|
||||
config_id = upgrade_db.config_id
|
||||
dump_builds = build.search([('id', 'child_of', source.id), ('params_id.config_id', '=', config_id.id), ('orphan_result', '=', False)])
|
||||
# this search is not optimal
|
||||
if not dump_builds:
|
||||
build._log('_run_configure_upgrade', 'No child build found with config %s in %s' % (config_id.name, source.id), level='ERROR')
|
||||
dbs = dump_builds.database_ids.sorted('db_suffix')
|
||||
valid_databases = list(self._filter_upgrade_database(dbs, upgrade_db.db_pattern))
|
||||
if not valid_databases:
|
||||
build._log('_run_configure_upgrade', 'No datase found for pattern %s' % (upgrade_db.db_pattern), level='ERROR')
|
||||
for db in valid_databases:
|
||||
#commit_ids = build.params_id.commit_ids
|
||||
#if commit_ids != target.params_id.commit_ids:
|
||||
# repo_ids = commit_ids.mapped('repo_id')
|
||||
# for commit_link in target.params_id.commit_link_ids:
|
||||
# if commit_link.commit_id.repo_id not in repo_ids:
|
||||
# additionnal_commit_links |= commit_link
|
||||
# build._log('', 'Adding sources from build [%s](%s)' % (target.id, target.build_url), log_type='markdown')
|
||||
|
||||
child = build._add_child({
|
||||
'upgrade_to_build_id': target.id,
|
||||
'upgrade_from_build_id': source,
|
||||
'dump_db': db.id,
|
||||
'config_id': self.upgrade_config_id
|
||||
})
|
||||
|
||||
child.description = 'Testing migration from %s to %s using db %s (%s)' % (
|
||||
source.params_id.version_id.name,
|
||||
target.params_id.version_id.name,
|
||||
db.name,
|
||||
config_id.name
|
||||
)
|
||||
# TODO log somewhere if no db at all is found for a db_suffix
|
||||
|
||||
def _get_upgrade_source_versions(self, target_version):
|
||||
if self.upgrade_from_version_ids:
|
||||
return self.upgrade_from_version_ids
|
||||
else:
|
||||
versions = self.env['runbot.version'].browse()
|
||||
if self.upgrade_from_previous_major_version:
|
||||
versions |= target_version.previous_major_version_id
|
||||
if self.upgrade_from_all_intermediate_version:
|
||||
versions |= target_version.intermediate_version_ids
|
||||
elif self.upgrade_from_last_intermediate_version:
|
||||
if target_version.intermediate_version_ids:
|
||||
versions |= target_version.intermediate_version_ids[-1]
|
||||
return versions
|
||||
|
||||
def _get_upgrade_source_builds(self, target_version, builds_references_by_version_id):
|
||||
versions = self._get_upgrade_source_versions(target_version)
|
||||
from_builds = self.env['runbot.build'].browse()
|
||||
for version in versions:
|
||||
from_builds |= builds_references_by_version_id.get(version.id) or self.env['runbot.build'].browse()
|
||||
return from_builds.sorted(lambda b: b.params_id.version_id.number)
|
||||
|
||||
def _filter_upgrade_database(self, dbs, pattern):
|
||||
pat_list = pattern.split(',') if pattern else []
|
||||
for db in dbs:
|
||||
if any(fnmatch.fnmatch(db.db_suffix, pat) for pat in pat_list):
|
||||
yield db
|
||||
|
||||
def _run_test_upgrade(self, build, log_path):
|
||||
target = build.params_id.upgrade_to_build_id
|
||||
commit_ids = build.params_id.commit_ids
|
||||
target_commit_ids = target.params_id.commit_ids
|
||||
if commit_ids != target_commit_ids:
|
||||
target_repo_ids = target_commit_ids.mapped('repo_id')
|
||||
for commit in commit_ids:
|
||||
if commit.repo_id not in target_repo_ids:
|
||||
target_commit_ids |= commit
|
||||
build._log('', 'Adding sources from build [%s](%s)' % (target.id, target.build_url), log_type='markdown')
|
||||
build = build.with_context(defined_commit_ids=target_commit_ids)
|
||||
exports = build._checkout()
|
||||
|
||||
dump_db = build.params_id.dump_db
|
||||
|
||||
migrate_db_name = '%s-%s' % (build.dest, dump_db.db_suffix) # only ok if restore does not force db_suffix
|
||||
|
||||
migrate_cmd = build._cmd()
|
||||
migrate_cmd += ['-u all']
|
||||
migrate_cmd += ['-d', migrate_db_name]
|
||||
migrate_cmd += ['--stop-after-init']
|
||||
migrate_cmd += ['--max-cron-threads=0']
|
||||
# migrate_cmd += ['--upgrades-paths', '/%s' % migration_scripts] upgrades-paths is broken, ln is created automatically in sources
|
||||
|
||||
build._log('run', 'Start migration build %s' % build.dest)
|
||||
timeout = self.cpu_limit
|
||||
|
||||
migrate_cmd.finals.append(['psql', migrate_db_name, '-c', '"SELECT id, name, state FROM ir_module_module WHERE state NOT IN (\'installed\', \'uninstalled\', \'uninstallable\') AND name NOT LIKE \'test_%\' "', '>', '/data/build/logs/modules_states.txt'])
|
||||
|
||||
env_variables = self.additionnal_env.split(';') if self.additionnal_env else []
|
||||
exception_env = self.env['runbot.upgrade.exception']._generate()
|
||||
if exception_env:
|
||||
env_variables.append(exception_env)
|
||||
docker_run(migrate_cmd, log_path, build._path(), build._get_docker_name(), cpu_limit=timeout, ro_volumes=exports, env_variables=env_variables)
|
||||
|
||||
def _run_restore(self, build, log_path):
|
||||
# exports = build._checkout()
|
||||
params = build.params_id
|
||||
|
||||
if 'dump_url' in params.config_data:
|
||||
dump_url = params.config_data['dump_url']
|
||||
zip_name = dump_url.split('/')[-1]
|
||||
build._log('test-migration', 'Restoring db [%s](%s)' % (zip_name, dump_url), log_type='markdown')
|
||||
else:
|
||||
download_db_suffix = params.dump_db.db_suffix or self.restore_download_db_suffix
|
||||
dump_build = params.dump_db.build_id or build.parent_id
|
||||
assert download_db_suffix and dump_build
|
||||
download_db_name = '%s-%s' % (dump_build.dest, download_db_suffix)
|
||||
zip_name = '%s.zip' % download_db_name
|
||||
dump_url = '%s%s' % (dump_build.http_log_url(), zip_name)
|
||||
build._log('test-migration', 'Restoring dump [%s](%s) from build [%s](%s)' % (zip_name, dump_url, dump_build.id, dump_build.build_url), log_type='markdown')
|
||||
restore_suffix = self.restore_rename_db_suffix or params.dump_db.db_suffix
|
||||
assert restore_suffix
|
||||
restore_db_name = '%s-%s' % (build.dest, restore_suffix)
|
||||
|
||||
build._local_pg_createdb(restore_db_name)
|
||||
cmd = ' && '.join([
|
||||
'mkdir /data/build/restore',
|
||||
'cd /data/build/restore',
|
||||
'wget %s' % dump_url,
|
||||
'unzip -q %s' % zip_name,
|
||||
'echo "### restoring filestore"',
|
||||
'mkdir -p /data/build/datadir/filestore/%s' % restore_db_name,
|
||||
'mv filestore/* /data/build/datadir/filestore/%s' % restore_db_name,
|
||||
'echo "###restoring db"',
|
||||
'psql -q %s < dump.sql' % (restore_db_name),
|
||||
'cd /data/build',
|
||||
'echo "### cleaning"',
|
||||
'rm -r restore',
|
||||
'echo "### listing modules"',
|
||||
"""psql %s -c "select name from ir_module_module where state = 'installed'" -t -A > /data/build/logs/restore_modules_installed.txt""" % restore_db_name,
|
||||
|
||||
])
|
||||
|
||||
docker_run(cmd, log_path, build._path(), build._get_docker_name(), cpu_limit=self.cpu_limit)
|
||||
|
||||
def _reference_builds(self, bundle, trigger):
|
||||
upgrade_dumps_trigger_id = trigger.upgrade_dumps_trigger_id
|
||||
refs_batches = self._reference_batches(bundle, trigger)
|
||||
refs_builds = refs_batches.mapped('slot_ids').filtered(
|
||||
lambda slot: slot.trigger_id == upgrade_dumps_trigger_id
|
||||
).mapped('build_id')
|
||||
# should we filter on active? implicit. On match type? on skipped ?
|
||||
# is last_"done"_batch enough?
|
||||
# TODO active test false and take last done/running build limit 1 -> in case of rebuild
|
||||
return refs_builds
|
||||
|
||||
def _is_upgrade_step(self):
|
||||
return self.job_type in ('configure_upgrade', 'configure_upgrade_complement')
|
||||
|
||||
def _reference_batches(self, bundle, trigger):
|
||||
if self.job_type == 'configure_upgrade_complement':
|
||||
return self._reference_batches_complement(bundle, trigger)
|
||||
else:
|
||||
return self._reference_batches_upgrade(bundle, trigger.upgrade_dumps_trigger_id.category_id.id)
|
||||
|
||||
def _reference_batches_complement(self, bundle, trigger):
|
||||
category_id = trigger.upgrade_dumps_trigger_id.category_id.id
|
||||
version = bundle.version_id
|
||||
next_versions = version.next_major_version_id | version.next_intermediate_version_ids # TODO filter on trigger version
|
||||
target_versions = version.browse()
|
||||
|
||||
upgrade_complement_step = trigger.upgrade_dumps_trigger_id.upgrade_step_id
|
||||
|
||||
if next_versions:
|
||||
for next_version in next_versions:
|
||||
if bundle.version_id in upgrade_complement_step._get_upgrade_source_versions(next_version):
|
||||
target_versions |= next_version
|
||||
return target_versions.with_context(
|
||||
category_id=category_id, project_id=bundle.project_id.id
|
||||
).mapped('base_bundle_id.last_done_batch')
|
||||
|
||||
def _reference_batches_upgrade(self, bundle, category_id):
|
||||
target_refs_bundles = self.env['runbot.bundle']
|
||||
sticky_domain = [('sticky', '=', True), ('project_id', '=', bundle.project_id.id)]
|
||||
if self.upgrade_to_version_ids:
|
||||
target_refs_bundles |= self.env['runbot.bundle'].search(sticky_domain + [('version_id', 'in', self.upgrade_to_version_ids.ids)])
|
||||
else:
|
||||
if self.upgrade_to_master:
|
||||
target_refs_bundles |= self.env['runbot.bundle'].search(sticky_domain + [('name', '=', 'master')])
|
||||
if self.upgrade_to_all_versions:
|
||||
target_refs_bundles |= self.env['runbot.bundle'].search(sticky_domain + [('name', '!=', 'master')])
|
||||
elif self.upgrade_to_major_versions:
|
||||
target_refs_bundles |= self.env['runbot.bundle'].search(sticky_domain + [('name', '!=', 'master'), ('version_id.is_major', '=', True)])
|
||||
|
||||
source_refs_bundles = self.env['runbot.bundle']
|
||||
|
||||
def from_versions(f_bundle):
|
||||
nonlocal source_refs_bundles
|
||||
if self.upgrade_from_previous_major_version:
|
||||
source_refs_bundles |= f_bundle.previous_major_version_base_id
|
||||
if self.upgrade_from_all_intermediate_version:
|
||||
source_refs_bundles |= f_bundle.intermediate_version_base_ids
|
||||
elif self.upgrade_from_last_intermediate_version:
|
||||
if f_bundle.intermediate_version_base_ids:
|
||||
source_refs_bundles |= f_bundle.intermediate_version_base_ids[-1]
|
||||
|
||||
if self.upgrade_from_version_ids:
|
||||
source_refs_bundles |= self.env['runbot.bundle'].search(sticky_domain + [('version_id', 'in', self.upgrade_from_version_ids.ids)])
|
||||
# this is subject to discussion. should this be smart and filter 'from_versions' or should it be flexible and do all possibilities
|
||||
else:
|
||||
if self.upgrade_to_current:
|
||||
from_versions(bundle)
|
||||
for f_bundle in target_refs_bundles:
|
||||
from_versions(f_bundle)
|
||||
|
||||
return (target_refs_bundles | source_refs_bundles).with_context(
|
||||
category_id=category_id
|
||||
).mapped('last_done_batch')
|
||||
|
||||
def log_end(self, build):
|
||||
if self.job_type == 'create_build':
|
||||
build._logger('Step %s finished in %s' % (self.name, s2human(build.job_time)))
|
||||
@ -421,7 +781,7 @@ class ConfigStep(models.Model):
|
||||
kwargs = dict(message='Step %s finished in %s' % (self.name, s2human(build.job_time)))
|
||||
if self.job_type == 'install_odoo':
|
||||
kwargs['message'] += ' $$fa-download$$'
|
||||
db_suffix = build.config_data.get('db_name') or self.db_name
|
||||
db_suffix = build.params_id.config_data.get('db_name') or self.db_name
|
||||
kwargs['path'] = '%s%s-%s.zip' % (build.http_log_url(), build.dest, db_suffix)
|
||||
kwargs['log_type'] = 'link'
|
||||
build._log('', **kwargs)
|
||||
@ -461,11 +821,11 @@ class ConfigStep(models.Model):
|
||||
|
||||
def _coverage_params(self, build, modules_to_install):
|
||||
pattern_to_omit = set()
|
||||
for commit in build._get_all_commit():
|
||||
for commit in build.params_id.commit_ids:
|
||||
docker_source_folder = build._docker_source_folder(commit)
|
||||
for manifest_file in commit.repo.manifest_files.split(','):
|
||||
for manifest_file in commit.repo_id.manifest_files.split(','):
|
||||
pattern_to_omit.add('*%s' % manifest_file)
|
||||
for (addons_path, module, _) in build._get_available_modules(commit):
|
||||
for (addons_path, module, _) in commit._get_available_modules():
|
||||
if module not in modules_to_install:
|
||||
# we want to omit docker_source_folder/[addons/path/]module/*
|
||||
module_path_in_docker = os.path.join(docker_source_folder, addons_path, module)
|
||||
@ -484,6 +844,8 @@ class ConfigStep(models.Model):
|
||||
build_values.update(self._make_coverage_results(build))
|
||||
if self.test_enable or self.test_tags:
|
||||
build_values.update(self._make_tests_results(build))
|
||||
elif self.job_type == 'test_upgrade':
|
||||
build_values.update(self._make_upgrade_results(build))
|
||||
return build_values
|
||||
|
||||
def _make_python_results(self, build):
|
||||
@ -511,6 +873,35 @@ class ConfigStep(models.Model):
|
||||
build._log('coverage_result', 'Coverage file not found', level='WARNING')
|
||||
return build_values
|
||||
|
||||
def _make_upgrade_results(self, build):
|
||||
build_values = {}
|
||||
build._log('upgrade', 'Getting results for build %s' % build.dest)
|
||||
|
||||
if build.local_result != 'ko':
|
||||
checkers = [
|
||||
self._check_log,
|
||||
self._check_module_loaded,
|
||||
self._check_error,
|
||||
self._check_module_states,
|
||||
self._check_build_ended,
|
||||
self._check_warning,
|
||||
]
|
||||
local_result = self._get_checkers_result(build, checkers)
|
||||
build_values['local_result'] = build._get_worst_result([build.local_result, local_result])
|
||||
|
||||
return build_values
|
||||
|
||||
def _check_module_states(self, build):
|
||||
if not build.is_file('logs/modules_states.txt'):
|
||||
build._log('', '"logs/modules_states.txt" file not found.', level='ERROR')
|
||||
return 'ko'
|
||||
|
||||
content = build.read_file('logs/modules_states.txt') or ''
|
||||
if '(0 rows)' not in content:
|
||||
build._log('', 'Some modules are not in installed/uninstalled/uninstallable state after migration. \n %s' % content)
|
||||
return 'ko'
|
||||
return 'ok'
|
||||
|
||||
def _check_log(self, build):
|
||||
log_path = build._path('logs', '%s.txt' % self.name)
|
||||
if not os.path.isfile(log_path):
|
||||
@ -577,7 +968,7 @@ class ConfigStep(models.Model):
|
||||
return build_values
|
||||
|
||||
def _make_stats(self, build):
|
||||
if not ((build.branch_id.make_stats or build.config_data.get('make_stats')) and self.make_stats):
|
||||
if not self.make_stats: # TODO garbage collect non sticky stat
|
||||
return
|
||||
build._log('make_stats', 'Getting stats from log file')
|
||||
log_path = build._path('logs', '%s.txt' % self.name)
|
||||
@ -592,6 +983,7 @@ class ConfigStep(models.Model):
|
||||
self.env['runbot.build.stat']._write_key_values(build, self, key_values)
|
||||
except Exception as e:
|
||||
message = '**An error occured while computing statistics of %s:**\n`%s`' % (build.job, str(e).replace('\\n', '\n').replace("\\'", "'"))
|
||||
_logger.exception(message)
|
||||
build._log('make_stats', message, level='INFO', log_type='markdown')
|
||||
|
||||
def _step_state(self):
|
||||
|
@ -1,16 +0,0 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class RunbotBuildDependency(models.Model):
|
||||
_name = "runbot.build.dependency"
|
||||
_description = "Build dependency"
|
||||
|
||||
build_id = fields.Many2one('runbot.build', 'Build', required=True, ondelete='cascade', index=True)
|
||||
dependecy_repo_id = fields.Many2one('runbot.repo', 'Dependency repo', required=True, ondelete='cascade')
|
||||
dependency_hash = fields.Char('Name of commit', index=True)
|
||||
closest_branch_id = fields.Many2one('runbot.branch', 'Branch', ondelete='cascade')
|
||||
match_type = fields.Char('Match Type')
|
||||
|
||||
def _get_repo(self):
|
||||
return self.closest_branch_id.repo_id or self.dependecy_repo_id
|
||||
|
@ -10,7 +10,7 @@ from odoo.exceptions import ValidationError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunbotBuildError(models.Model):
|
||||
class BuildError(models.Model):
|
||||
|
||||
_name = "runbot.build.error"
|
||||
_description = "Build error"
|
||||
@ -24,16 +24,16 @@ class RunbotBuildError(models.Model):
|
||||
module_name = fields.Char('Module name') # name in ir_logging
|
||||
function = fields.Char('Function name') # func name in ir logging
|
||||
fingerprint = fields.Char('Error fingerprint', index=True)
|
||||
random = fields.Boolean('underterministic error', track_visibility='onchange')
|
||||
responsible = fields.Many2one('res.users', 'Assigned fixer', track_visibility='onchange')
|
||||
fixing_commit = fields.Char('Fixing commit', track_visibility='onchange')
|
||||
random = fields.Boolean('underterministic error', tracking=True)
|
||||
responsible = fields.Many2one('res.users', 'Assigned fixer', tracking=True)
|
||||
fixing_commit = fields.Char('Fixing commit', tracking=True)
|
||||
build_ids = fields.Many2many('runbot.build', 'runbot_build_error_ids_runbot_build_rel', string='Affected builds')
|
||||
branch_ids = fields.Many2many('runbot.branch', compute='_compute_branch_ids')
|
||||
repo_ids = fields.Many2many('runbot.repo', compute='_compute_repo_ids')
|
||||
active = fields.Boolean('Error is not fixed', default=True, track_visibility='onchange')
|
||||
bundle_ids = fields.One2many('runbot.bundle', compute='_compute_bundle_ids')
|
||||
trigger_ids = fields.Many2many('runbot.trigger', compute='_compute_trigger_ids')
|
||||
active = fields.Boolean('Error is not fixed', default=True, tracking=True)
|
||||
tag_ids = fields.Many2many('runbot.build.error.tag', string='Tags')
|
||||
build_count = fields.Integer(compute='_compute_build_counts', string='Nb seen', stored=True)
|
||||
parent_id = fields.Many2one('runbot.build.error', 'Linked to')
|
||||
build_count = fields.Integer(compute='_compute_build_counts', string='Nb seen')
|
||||
parent_id = fields.Many2one('runbot.build.error', 'Linked to', index=True)
|
||||
child_ids = fields.One2many('runbot.build.error', 'parent_id', string='Child Errors', context={'active_test': False})
|
||||
children_build_ids = fields.Many2many('runbot.build', compute='_compute_children_build_ids', string='Children builds')
|
||||
error_history_ids = fields.Many2many('runbot.build.error', compute='_compute_error_history_ids', string='Old errors', context={'active_test': False})
|
||||
@ -63,7 +63,7 @@ class RunbotBuildError(models.Model):
|
||||
if 'active' in vals:
|
||||
for build_error in self:
|
||||
(build_error.child_ids - self).write({'active': vals['active']})
|
||||
return super(RunbotBuildError, self).write(vals)
|
||||
return super(BuildError, self).write(vals)
|
||||
|
||||
@api.depends('build_ids')
|
||||
def _compute_build_counts(self):
|
||||
@ -71,14 +71,15 @@ class RunbotBuildError(models.Model):
|
||||
build_error.build_count = len(build_error.children_build_ids)
|
||||
|
||||
@api.depends('build_ids')
|
||||
def _compute_branch_ids(self):
|
||||
def _compute_bundle_ids(self):
|
||||
for build_error in self:
|
||||
build_error.branch_ids = build_error.mapped('build_ids.branch_id')
|
||||
top_parent_builds = build_error.build_ids.mapped(lambda rec: rec and rec._get_top_parent())
|
||||
build_error.bundle_ids = top_parent_builds.mapped('slot_ids').mapped('batch_id.bundle_id')
|
||||
|
||||
@api.depends('build_ids')
|
||||
def _compute_repo_ids(self):
|
||||
def _compute_trigger_ids(self):
|
||||
for build_error in self:
|
||||
build_error.repo_ids = build_error.mapped('build_ids.repo_id')
|
||||
build_error.trigger_ids = build_error.mapped('build_ids.params_id.trigger_id')
|
||||
|
||||
@api.depends('content')
|
||||
def _compute_summary(self):
|
||||
@ -134,7 +135,6 @@ class RunbotBuildError(models.Model):
|
||||
build.build_error_ids += build_error
|
||||
del hash_dict[build_error.fingerprint]
|
||||
|
||||
fixed_errors_dict = {rec.fingerprint: rec for rec in self.env['runbot.build.error'].search([('fingerprint', 'in', list(hash_dict.keys())), ('active', '=', False)])}
|
||||
# create an error for the remaining entries
|
||||
for fingerprint, logs in hash_dict.items():
|
||||
build_error = self.env['runbot.build.error'].create({
|
||||
@ -161,7 +161,7 @@ class RunbotBuildError(models.Model):
|
||||
|
||||
@api.model
|
||||
def test_tags_list(self):
|
||||
active_errors = self.search([('test_tags', '!=', False), ('random', '=', True)])
|
||||
active_errors = self.search([('test_tags', '!=', False)])
|
||||
test_tag_list = active_errors.mapped('test_tags')
|
||||
return [test_tag for error_tags in test_tag_list for test_tag in (error_tags).split(',')]
|
||||
|
||||
@ -170,7 +170,7 @@ class RunbotBuildError(models.Model):
|
||||
return ['-%s' % tag for tag in self.test_tags_list()]
|
||||
|
||||
|
||||
class RunbotBuildErrorTag(models.Model):
|
||||
class BuildErrorTag(models.Model):
|
||||
|
||||
_name = "runbot.build.error.tag"
|
||||
_description = "Build error tag"
|
||||
@ -179,7 +179,7 @@ class RunbotBuildErrorTag(models.Model):
|
||||
error_ids = fields.Many2many('runbot.build.error', string='Errors')
|
||||
|
||||
|
||||
class RunbotErrorRegex(models.Model):
|
||||
class ErrorRegex(models.Model):
|
||||
|
||||
_name = "runbot.error.regex"
|
||||
_description = "Build error regex"
|
||||
|
@ -5,7 +5,7 @@ from odoo import models, fields, api, tools
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunbotBuildStat(models.Model):
|
||||
class BuildStat(models.Model):
|
||||
_name = "runbot.build.stat"
|
||||
_description = "Statistics"
|
||||
_sql_constraints = [
|
||||
@ -45,54 +45,64 @@ class RunbotBuildStatSql(models.Model):
|
||||
_description = "Build stat sql view"
|
||||
_auto = False
|
||||
|
||||
id = fields.Many2one("runbot.build.stat", readonly=True)
|
||||
bundle_id = fields.Many2one("runbot.bundle", string="Bundle", readonly=True)
|
||||
bundle_name = fields.Char(string="Bundle name", readonly=True)
|
||||
bundle_sticky = fields.Boolean(string="Sticky", readonly=True)
|
||||
batch_id = fields.Many2one("runbot.bundle", string="Batch", readonly=True)
|
||||
trigger_id = fields.Many2one("runbot.trigger", string="Trigger", readonly=True)
|
||||
trigger_name = fields.Char(string="Trigger name", readonly=True)
|
||||
|
||||
stat_id = fields.Many2one("runbot.build.stat", string="Stat", readonly=True)
|
||||
key = fields.Char("Key", readonly=True)
|
||||
value = fields.Float("Value", readonly=True)
|
||||
|
||||
config_step_id = fields.Many2one(
|
||||
"runbot.build.config.step", string="Config Step", readonly=True
|
||||
)
|
||||
config_step_name = fields.Char(String="Config Step name", readonly=True)
|
||||
|
||||
build_id = fields.Many2one("runbot.build", string="Build", readonly=True)
|
||||
build_config_id = fields.Many2one("runbot.build.config", string="Config", readonly=True)
|
||||
build_name = fields.Char(String="Build name", readonly=True)
|
||||
build_parent_path = fields.Char('Build Parent path')
|
||||
build_host = fields.Char(string="Host", readonly=True)
|
||||
branch_id = fields.Many2one("runbot.branch", string="Branch", readonly=True)
|
||||
branch_name = fields.Char(string="Branch name", readonly=True)
|
||||
branch_sticky = fields.Boolean(string="Sticky", readonly=True)
|
||||
repo_id = fields.Many2one("runbot.repo", string="Repo", readonly=True)
|
||||
repo_name = fields.Char(string="Repo name", readonly=True)
|
||||
|
||||
def init(self):
|
||||
""" Create SQL view for build stat """
|
||||
tools.drop_view_if_exists(self._cr, "runbot_build_stat_sql")
|
||||
self._cr.execute(
|
||||
""" CREATE VIEW runbot_build_stat_sql AS (
|
||||
""" CREATE OR REPLACE VIEW runbot_build_stat_sql AS (
|
||||
SELECT
|
||||
stat.id AS id,
|
||||
(stat.id::bigint*(2^32)+bun.id::bigint) AS id,
|
||||
stat.id AS stat_id,
|
||||
stat.key AS key,
|
||||
stat.value AS value,
|
||||
step.id AS config_step_id,
|
||||
step.name AS config_step_name,
|
||||
bu.id AS build_id,
|
||||
bu.config_id AS build_config_id,
|
||||
bp.config_id AS build_config_id,
|
||||
bu.parent_path AS build_parent_path,
|
||||
bu.name AS build_name,
|
||||
bu.host AS build_host,
|
||||
br.id AS branch_id,
|
||||
br.branch_name AS branch_name,
|
||||
br.sticky AS branch_sticky,
|
||||
repo.id AS repo_id,
|
||||
repo.name AS repo_name
|
||||
bun.id AS bundle_id,
|
||||
bun.name AS bundle_name,
|
||||
bun.sticky AS bundle_sticky,
|
||||
ba.id AS batch_id,
|
||||
tr.id AS trigger_id,
|
||||
tr.name AS trigger_name
|
||||
FROM
|
||||
runbot_build_stat AS stat
|
||||
JOIN
|
||||
runbot_build_config_step step ON stat.config_step_id = step.id
|
||||
JOIN
|
||||
runbot_build bu ON stat.build_id = bu.id
|
||||
runbot_build bu ON bu.id = stat.build_id
|
||||
JOIN
|
||||
runbot_branch br ON br.id = bu.branch_id
|
||||
runbot_build_params bp ON bp.id =bu.params_id
|
||||
JOIN
|
||||
runbot_repo repo ON br.repo_id = repo.id
|
||||
runbot_batch_slot bas ON bas.build_id = stat.build_id
|
||||
JOIN
|
||||
runbot_trigger tr ON tr.id = bas.trigger_id
|
||||
JOIN
|
||||
runbot_batch ba ON ba.id = bas.batch_id
|
||||
JOIN
|
||||
runbot_bundle bun ON bun.id = ba.bundle_id
|
||||
)"""
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..common import os
|
||||
import re
|
||||
|
||||
from odoo import models, fields, api
|
||||
@ -11,7 +12,7 @@ VALUE_PATTERN = r"\(\?P\<value\>.+\)" # used to verify value group pattern
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunbotBuildStatRegex(models.Model):
|
||||
class BuildStatRegex(models.Model):
|
||||
""" A regular expression to extract a float/int value from a log file
|
||||
The regulare should contain a named group like '(?P<value>.+)'.
|
||||
The result will be a key/value like {name: value}
|
||||
@ -59,8 +60,8 @@ class RunbotBuildStatRegex(models.Model):
|
||||
value = float(group_dict.get("value"))
|
||||
except ValueError:
|
||||
_logger.warning(
|
||||
'The matched value (%s) of "%s" cannot be converted into float'
|
||||
% (group_dict.get("value"), build_stat_regex.regex)
|
||||
'The matched value (%s) of "%s" cannot be converted into float',
|
||||
group_dict.get("value"), build_stat_regex.regex
|
||||
)
|
||||
continue
|
||||
key = (
|
||||
|
222
runbot/models/bundle.py
Normal file
222
runbot/models/bundle.py
Normal file
@ -0,0 +1,222 @@
|
||||
import time
|
||||
import logging
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
from collections import defaultdict
|
||||
from odoo import models, fields, api, tools
|
||||
from ..common import dt2time, s2human_long
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Bundle(models.Model):
|
||||
_name = 'runbot.bundle'
|
||||
_description = "Bundle"
|
||||
|
||||
name = fields.Char('Bundle name', required=True, help="Name of the base branch")
|
||||
project_id = fields.Many2one('runbot.project', required=True, index=True)
|
||||
branch_ids = fields.One2many('runbot.branch', 'bundle_id')
|
||||
|
||||
# custom behaviour
|
||||
no_build = fields.Boolean('No build')
|
||||
no_auto_run = fields.Boolean('No run')
|
||||
build_all = fields.Boolean('Force all triggers')
|
||||
modules = fields.Char("Modules to install", help="Comma-separated list of modules to install and test.")
|
||||
|
||||
batch_ids = fields.One2many('runbot.batch', 'bundle_id')
|
||||
last_batch = fields.Many2one('runbot.batch', index=True, domain=lambda self: [('category_id', '=', self.env.ref('runbot.default_category').id)])
|
||||
last_batchs = fields.Many2many('runbot.batch', 'Last batchs', compute='_compute_last_batchs')
|
||||
last_done_batch = fields.Many2many('runbot.batch', 'Last batchs', compute='_compute_last_done_batch')
|
||||
|
||||
sticky = fields.Boolean('Sticky', compute='_compute_sticky', store=True, index=True)
|
||||
is_base = fields.Boolean('Is base', index=True)
|
||||
defined_base_id = fields.Many2one('runbot.bundle', 'Forced base bundle', domain="[('project_id', '=', project_id), ('is_base', '=', True)]")
|
||||
base_id = fields.Many2one('runbot.bundle', 'Base bundle', compute='_compute_base_id', store=True)
|
||||
|
||||
version_id = fields.Many2one('runbot.version', 'Version', compute='_compute_version_id', store=True)
|
||||
version_number = fields.Char(related='version_id.number', store=True, index=True)
|
||||
|
||||
previous_major_version_base_id = fields.Many2one('runbot.bundle', 'Previous base bundle', compute='_compute_relations_base_id')
|
||||
intermediate_version_base_ids = fields.Many2many('runbot.bundle', 'Intermediate base bundles', compute='_compute_relations_base_id')
|
||||
|
||||
priority = fields.Boolean('Build priority', default=False)
|
||||
|
||||
trigger_custom_ids = fields.One2many('runbot.bundle.trigger.custom', 'bundle_id')
|
||||
auto_rebase = fields.Boolean('Auto rebase', default=False)
|
||||
|
||||
@api.depends('sticky')
|
||||
def _compute_make_stats(self):
|
||||
for bundle in self:
|
||||
bundle.make_stats = bundle.sticky
|
||||
|
||||
@api.depends('is_base')
|
||||
def _compute_sticky(self):
|
||||
for bundle in self:
|
||||
bundle.sticky = bundle.is_base
|
||||
|
||||
@api.depends('name', 'is_base', 'defined_base_id', 'base_id.is_base', 'project_id')
|
||||
def _compute_base_id(self):
|
||||
for bundle in self:
|
||||
if bundle.is_base:
|
||||
bundle.base_id = bundle
|
||||
continue
|
||||
if bundle.defined_base_id:
|
||||
bundle.base_id = bundle.defined_base_id
|
||||
continue
|
||||
project_id = bundle.project_id.id
|
||||
master_base = False
|
||||
for bid, bname in self._get_base_ids(project_id):
|
||||
if bundle.name.startswith('%s-' % bname):
|
||||
bundle.base_id = self.browse(bid)
|
||||
break
|
||||
elif bname == 'master':
|
||||
master_base = self.browse(bid)
|
||||
else:
|
||||
bundle.base_id = master_base
|
||||
|
||||
@tools.ormcache('project_id')
|
||||
def _get_base_ids(self, project_id):
|
||||
return [(b.id, b.name) for b in self.search([('is_base', '=', True), ('project_id', '=', project_id)])]
|
||||
|
||||
@api.depends('is_base', 'base_id.version_id')
|
||||
def _compute_version_id(self):
|
||||
for bundle in self.sorted(key='is_base', reverse=True):
|
||||
if not bundle.is_base:
|
||||
bundle.version_id = bundle.base_id.version_id
|
||||
continue
|
||||
bundle.version_id = self.env['runbot.version']._get(bundle.name)
|
||||
|
||||
@api.depends('version_id')
|
||||
def _compute_relations_base_id(self):
|
||||
for bundle in self:
|
||||
bundle = bundle.with_context(project_id=bundle.project_id.id)
|
||||
bundle.previous_major_version_base_id = bundle.version_id.previous_major_version_id.base_bundle_id
|
||||
bundle.intermediate_version_base_ids = bundle.version_id.intermediate_version_ids.mapped('base_bundle_id')
|
||||
|
||||
@api.depends_context('category_id')
|
||||
def _compute_last_batchs(self):
|
||||
if self:
|
||||
batch_ids = defaultdict(list)
|
||||
category_id = self.env.context.get('category_id', self.env['ir.model.data'].xmlid_to_res_id('runbot.default_category'))
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
id
|
||||
FROM (
|
||||
SELECT
|
||||
batch.id AS id,
|
||||
row_number() OVER (PARTITION BY batch.bundle_id order by batch.id desc) AS row
|
||||
FROM
|
||||
runbot_bundle bundle INNER JOIN runbot_batch batch ON bundle.id=batch.bundle_id
|
||||
WHERE
|
||||
bundle.id in %s
|
||||
AND batch.category_id = %s
|
||||
) AS bundle_batch
|
||||
WHERE
|
||||
row <= 4
|
||||
ORDER BY row, id desc
|
||||
""", [tuple(self.ids), category_id]
|
||||
)
|
||||
batchs = self.env['runbot.batch'].browse([r[0] for r in self.env.cr.fetchall()])
|
||||
for batch in batchs:
|
||||
batch_ids[batch.bundle_id.id].append(batch.id)
|
||||
|
||||
for bundle in self:
|
||||
bundle.last_batchs = [(6, 0, batch_ids[bundle.id])]
|
||||
|
||||
@api.depends_context('category_id')
|
||||
def _compute_last_done_batch(self):
|
||||
if self:
|
||||
# self.env['runbot.batch'].flush()
|
||||
for bundle in self:
|
||||
bundle.last_done_batch = False
|
||||
category_id = self.env.context.get('category_id', self.env['ir.model.data'].xmlid_to_res_id('runbot.default_category'))
|
||||
self.env.cr.execute("""
|
||||
SELECT
|
||||
id
|
||||
FROM (
|
||||
SELECT
|
||||
batch.id AS id,
|
||||
row_number() OVER (PARTITION BY batch.bundle_id order by batch.id desc) AS row
|
||||
FROM
|
||||
runbot_bundle bundle INNER JOIN runbot_batch batch ON bundle.id=batch.bundle_id
|
||||
WHERE
|
||||
bundle.id in %s
|
||||
AND batch.state = 'done'
|
||||
AND batch.category_id = %s
|
||||
) AS bundle_batch
|
||||
WHERE
|
||||
row = 1
|
||||
ORDER BY row, id desc
|
||||
""", [tuple(self.ids), category_id]
|
||||
)
|
||||
batchs = self.env['runbot.batch'].browse([r[0] for r in self.env.cr.fetchall()])
|
||||
for batch in batchs:
|
||||
batch.bundle_id.last_done_batch = batch
|
||||
|
||||
def create(self, values_list):
|
||||
res = super().create(values_list)
|
||||
if res.is_base:
|
||||
model = self.browse()
|
||||
model._get_base_ids.clear_cache(model)
|
||||
return res
|
||||
|
||||
def write(self, values):
|
||||
super().write(values)
|
||||
if 'is_base' in values:
|
||||
model = self.browse()
|
||||
model._get_base_ids.clear_cache(model)
|
||||
|
||||
def _force(self, category_id=None, auto_rebase=False):
|
||||
self.ensure_one()
|
||||
if self.last_batch.state == 'preparing':
|
||||
return
|
||||
values = {
|
||||
'last_update': fields.Datetime.now(),
|
||||
'bundle_id': self.id,
|
||||
'state': 'preparing',
|
||||
}
|
||||
if category_id:
|
||||
values['category_id'] = category_id
|
||||
new = self.env['runbot.batch'].create(values)
|
||||
self.last_batch = new
|
||||
new.sudo()._prepare(auto_rebase or self.auto_rebase)
|
||||
return new
|
||||
|
||||
def consistency_warning(self):
|
||||
if self.defined_base_id:
|
||||
return [('info', 'This bundle has a forced base: %s' % self.defined_base_id.name)]
|
||||
warnings = []
|
||||
for branch in self.branch_ids:
|
||||
if branch.is_pr and branch.target_branch_name != self.base_id.name:
|
||||
if branch.target_branch_name.startswith(self.base_id.name):
|
||||
warnings.append(('info', 'PR %s targeting a non base branch: %s' % (branch.dname, branch.target_branch_name)))
|
||||
else:
|
||||
warnings.append(('warning' if branch.alive else 'info', 'PR %s targeting wrong version: %s (expecting %s)' % (branch.dname, branch.target_branch_name, self.base_id.name)))
|
||||
elif not branch.is_pr and not branch.name.startswith(self.base_id.name) and not self.defined_base_id:
|
||||
warnings.append(('warning', 'Branch %s not starting with version name (%s)' % (branch.dname, self.base_id.name)))
|
||||
return warnings
|
||||
|
||||
def branch_groups(self):
|
||||
self.branch_ids.sorted(key=lambda b: (b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr))
|
||||
branch_groups = {repo: [] for repo in self.branch_ids.mapped('remote_id.repo_id').sorted('sequence')}
|
||||
for branch in self.branch_ids.sorted(key=lambda b: (b.is_pr)):
|
||||
branch_groups[branch.remote_id.repo_id].append(branch)
|
||||
return branch_groups
|
||||
|
||||
|
||||
class BundleTriggerCustomisation(models.Model):
|
||||
_name = 'runbot.bundle.trigger.custom'
|
||||
_description = 'Custom trigger'
|
||||
|
||||
trigger_id = fields.Many2one('runbot.trigger', domain="[('project_id', '=', bundle_id.project_id)]")
|
||||
bundle_id = fields.Many2one('runbot.bundle')
|
||||
config_id = fields.Many2one('runbot.build.config')
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"bundle_custom_trigger_unique",
|
||||
"unique (bundle_id, trigger_id)",
|
||||
"Only one custom trigger per trigger per bundle is allowed",
|
||||
)
|
||||
]
|
226
runbot/models/commit.py
Normal file
226
runbot/models/commit.py
Normal file
@ -0,0 +1,226 @@
|
||||
|
||||
import subprocess
|
||||
|
||||
from ..common import os, RunbotException
|
||||
import glob
|
||||
import shutil
|
||||
|
||||
from odoo import models, fields, api, registry
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Commit(models.Model):
|
||||
_name = 'runbot.commit'
|
||||
_description = "Commit"
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"commit_unique",
|
||||
"unique (name, repo_id, rebase_on_id)",
|
||||
"Commit must be unique to ensure correct duplicate matching",
|
||||
)
|
||||
]
|
||||
name = fields.Char('SHA')
|
||||
repo_id = fields.Many2one('runbot.repo', string='Repo group')
|
||||
date = fields.Datetime('Commit date')
|
||||
author = fields.Char('Author')
|
||||
author_email = fields.Char('Author Email')
|
||||
committer = fields.Char('Committer')
|
||||
committer_email = fields.Char('Committer Email')
|
||||
subject = fields.Text('Subject')
|
||||
dname = fields.Char('Display name', compute='_compute_dname')
|
||||
rebase_on_id = fields.Many2one('runbot.commit', 'Rebase on commit')
|
||||
|
||||
def _get(self, name, repo_id, vals=None, rebase_on_id=False):
|
||||
commit = self.search([('name', '=', name), ('repo_id', '=', repo_id), ('rebase_on_id', '=', rebase_on_id)])
|
||||
if not commit:
|
||||
commit = self.env['runbot.commit'].create({**(vals or {}), 'name': name, 'repo_id': repo_id, 'rebase_on_id': rebase_on_id})
|
||||
return commit
|
||||
|
||||
def _rebase_on(self, commit):
|
||||
if self == commit:
|
||||
return self
|
||||
return self._get(self.name, self.repo_id.id, self.read()[0], commit.id)
|
||||
|
||||
def _get_available_modules(self):
|
||||
for manifest_file_name in self.repo_id.manifest_files.split(','): # '__manifest__.py' '__openerp__.py'
|
||||
for addons_path in (self.repo_id.addons_paths or '').split(','): # '' 'addons' 'odoo/addons'
|
||||
sep = os.path.join(addons_path, '*')
|
||||
for manifest_path in glob.glob(self._source_path(sep, manifest_file_name)):
|
||||
module = os.path.basename(os.path.dirname(manifest_path))
|
||||
yield (addons_path, module, manifest_file_name)
|
||||
|
||||
def export(self):
|
||||
"""Export a git repo into a sources"""
|
||||
# TODO add automated tests
|
||||
self.ensure_one()
|
||||
|
||||
export_path = self._source_path()
|
||||
|
||||
if os.path.isdir(export_path):
|
||||
_logger.info('git export: exporting to %s (already exists)', export_path)
|
||||
return export_path
|
||||
|
||||
|
||||
_logger.info('git export: exporting to %s (new)', export_path)
|
||||
os.makedirs(export_path)
|
||||
|
||||
self.repo_id._fetch(self.name)
|
||||
export_sha = self.name
|
||||
if self.rebase_on_id:
|
||||
export_sha = self.rebase_on_id.name
|
||||
self.rebase_on_id.repo_id._fetch(export_sha)
|
||||
|
||||
p1 = subprocess.Popen(['git', '--git-dir=%s' % self.repo_id.path, 'archive', export_sha], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
p2 = subprocess.Popen(['tar', '-xmC', export_path], stdin=p1.stdout, stdout=subprocess.PIPE)
|
||||
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
|
||||
(_, err) = p2.communicate()
|
||||
p1.poll() # fill the returncode
|
||||
if p1.returncode:
|
||||
raise RunbotException("Git archive failed for %s with error code %s. (%s)" % (self.name, p1.returncode, p1.stderr.read().decode()))
|
||||
if err:
|
||||
raise RunbotException("Export for %s failed. (%s)" % (self.name, err))
|
||||
|
||||
if self.rebase_on_id:
|
||||
# we could be smart here and detect if merge_base == commit, in witch case checkouting base_commit is enough. Since we don't have this info
|
||||
# and we are exporting in a custom folder anyway, lets
|
||||
_logger.info('Applying patch for %s', self.name)
|
||||
p1 = subprocess.Popen(['git', '--git-dir=%s' % self.repo_id.path, 'diff', '%s...%s' % (export_sha, self.name)], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||
p2 = subprocess.Popen(['patch', '-p0', '-d', export_path], stdin=p1.stdout, stdout=subprocess.PIPE)
|
||||
p1.stdout.close()
|
||||
(message, err) = p2.communicate()
|
||||
p1.poll()
|
||||
if err:
|
||||
shutil.rmtree(export_path)
|
||||
raise RunbotException("Apply patch failed for %s...%s. (%s)" % (export_sha, self.name, err))
|
||||
if p1.returncode or p2.returncode:
|
||||
shutil.rmtree(export_path)
|
||||
raise RunbotException("Apply patch failed for %s...%s with error code %s+%s. (%s)" % (export_sha, self.name, p1.returncode, p2.returncode, message))
|
||||
|
||||
# migration scripts link if necessary
|
||||
icp = self.env['ir.config_parameter']
|
||||
ln_param = icp.get_param('runbot_migration_ln', default='')
|
||||
migration_repo_id = int(icp.get_param('runbot_migration_repo_id', default=0))
|
||||
if ln_param and migration_repo_id and self.repo_id.server_files:
|
||||
scripts_dir = self.env['runbot.repo'].browse(migration_repo_id).name
|
||||
try:
|
||||
os.symlink('/data/build/%s' % scripts_dir, self._source_path(ln_param))
|
||||
except FileNotFoundError:
|
||||
_logger.warning('Impossible to create migration symlink')
|
||||
|
||||
return export_path
|
||||
|
||||
def read_source(self, file, mode='r'):
|
||||
file_path = self._source_path(file)
|
||||
try:
|
||||
with open(file_path, mode) as f:
|
||||
return f.read()
|
||||
except:
|
||||
return False
|
||||
|
||||
def _source_path(self, *path):
|
||||
export_name = self.name
|
||||
if self.rebase_on_id:
|
||||
export_name = '%s_%s' % (self.name, self.rebase_on_id.name)
|
||||
return os.path.join(self.env['runbot.runbot']._root(), 'sources', self.repo_id.name, export_name, *path)
|
||||
|
||||
@api.depends('name', 'repo_id.name')
|
||||
def _compute_dname(self):
|
||||
for commit in self:
|
||||
commit.dname = '%s:%s' % (commit.repo_id.name, commit.name[:8])
|
||||
|
||||
def _github_status(self, build, context, state, target_url, description=None, post_commit=True):
|
||||
self.ensure_one()
|
||||
Status = self.env['runbot.commit.status']
|
||||
last_status = Status.search([('commit_id', '=', self.id), ('context', '=', context)], order='id desc', limit=1)
|
||||
if last_status and last_status.state == state:
|
||||
_logger.info('Skipping already sent status %s:%s for %s', context, state, self.name)
|
||||
return
|
||||
last_status = Status.create({
|
||||
'build_id': build.id if build else False,
|
||||
'commit_id': self.id,
|
||||
'context': context,
|
||||
'state': state,
|
||||
'target_url': target_url,
|
||||
'description': description or context,
|
||||
})
|
||||
last_status._send(post_commit)
|
||||
|
||||
|
||||
class CommitLink(models.Model):
|
||||
_name = 'runbot.commit.link'
|
||||
_description = "Build commit"
|
||||
|
||||
commit_id = fields.Many2one('runbot.commit', 'Commit', required=True, index=True)
|
||||
# Link info
|
||||
match_type = fields.Selection([('new', 'New head of branch'), ('head', 'Head of branch'), ('base_head', 'Found on base branch'), ('base_match', 'Found on base branch')]) # HEAD, DEFAULT
|
||||
branch_id = fields.Many2one('runbot.branch', string='Found in branch') # Shouldn't be use for anything else than display
|
||||
|
||||
base_commit_id = fields.Many2one('runbot.commit', 'Base head commit', index=True)
|
||||
merge_base_commit_id = fields.Many2one('runbot.commit', 'Merge Base commit', index=True)
|
||||
base_behind = fields.Integer('# commits behind base')
|
||||
base_ahead = fields.Integer('# commits ahead base')
|
||||
file_changed = fields.Integer('# file changed')
|
||||
diff_add = fields.Integer('# line added')
|
||||
diff_remove = fields.Integer('# line removed')
|
||||
|
||||
|
||||
class CommitStatus(models.Model):
|
||||
_name = 'runbot.commit.status'
|
||||
_description = 'Commit status'
|
||||
_order = 'id desc'
|
||||
|
||||
commit_id = fields.Many2one('runbot.commit', string='Commit', required=True, index=True)
|
||||
context = fields.Char('Context', required=True)
|
||||
state = fields.Char('State', required=True)
|
||||
build_id = fields.Many2one('runbot.build', string='Build', index=True)
|
||||
target_url = fields.Char('Url')
|
||||
description = fields.Char('Description')
|
||||
sent_date = fields.Datetime('Sent Date')
|
||||
|
||||
def _send(self, post_commit=True):
|
||||
user_id = self.env.user.id
|
||||
_dbname = self.env.cr.dbname
|
||||
_context = self.env.context
|
||||
|
||||
status_id = self.id
|
||||
commit = self.commit_id
|
||||
all_remote = commit.repo_id.remote_ids
|
||||
remotes = all_remote.filtered(lambda remote: remote.token)
|
||||
no_token_remote = all_remote-remotes
|
||||
if no_token_remote:
|
||||
_logger.warning('No token on remote %s, skipping status', no_token_remote.mapped("name"))
|
||||
remote_ids = remotes.ids
|
||||
commit_name = commit.name
|
||||
|
||||
status = {
|
||||
'context': self.context,
|
||||
'state': self.state,
|
||||
'target_url': self.target_url,
|
||||
'description': self.description,
|
||||
}
|
||||
if remote_ids:
|
||||
|
||||
def send_github_status(env):
|
||||
for remote in env['runbot.remote'].browse(remote_ids):
|
||||
_logger.debug(
|
||||
"github updating %s status %s to %s in repo %s",
|
||||
status['context'], commit_name, status['state'], remote.name)
|
||||
remote._github('/repos/:owner/:repo/statuses/%s' % commit_name, status, ignore_errors=True)
|
||||
env['runbot.commit.status'].browse(status_id).sent_date = fields.Datetime.now()
|
||||
|
||||
def send_github_status_async():
|
||||
try:
|
||||
db_registry = registry(_dbname)
|
||||
with api.Environment.manage(), db_registry.cursor() as cr:
|
||||
env = api.Environment(cr, user_id, _context)
|
||||
send_github_status(env)
|
||||
except:
|
||||
_logger.exception('Something went wrong sending notification for %s', commit_name)
|
||||
|
||||
if post_commit:
|
||||
self._cr.after('commit', send_github_status_async)
|
||||
else:
|
||||
send_github_status(self.env)
|
23
runbot/models/database.py
Normal file
23
runbot/models/database.py
Normal file
@ -0,0 +1,23 @@
|
||||
import logging
|
||||
from odoo import models, fields, api
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Database(models.Model):
|
||||
_name = 'runbot.database'
|
||||
_description = "Database"
|
||||
|
||||
name = fields.Char('Host name', required=True, unique=True)
|
||||
build_id = fields.Many2one('runbot.build', index=True, required=True)
|
||||
db_suffix = fields.Char(compute='_compute_db_suffix')
|
||||
|
||||
def _compute_db_suffix(self):
|
||||
for record in self:
|
||||
record.db_suffix = record.name.replace('%s-' % record.build_id.dest, '')
|
||||
|
||||
@api.model_create_single
|
||||
def create(self, values):
|
||||
res = self.search([('name', '=', values['name']), ('build_id', '=', values['build_id'])])
|
||||
if res:
|
||||
return res
|
||||
return super().create(values)
|
@ -80,7 +80,7 @@ FOR EACH ROW EXECUTE PROCEDURE runbot_set_logging_build();
|
||||
|
||||
|
||||
class RunbotErrorLog(models.Model):
|
||||
_name = "runbot.error.log"
|
||||
_name = 'runbot.error.log'
|
||||
_description = "Error log"
|
||||
_auto = False
|
||||
_order = 'id desc'
|
||||
@ -95,30 +95,23 @@ class RunbotErrorLog(models.Model):
|
||||
path = fields.Char(string='Path', readonly=True)
|
||||
line = fields.Char(string='Line', readonly=True)
|
||||
build_id = fields.Many2one('runbot.build', string='Build', readonly=True)
|
||||
bu_name = fields.Char(String='Build name', readonly=True)
|
||||
#bu_name = fields.Char(String='Build name', readonly=True) as aggregate
|
||||
dest = fields.Char(String='Build dest', readonly=True)
|
||||
local_state = fields.Char(string='Local state', readonly=True)
|
||||
local_result = fields.Char(string='Local result', readonly=True)
|
||||
global_state = fields.Char(string='Global state', readonly=True)
|
||||
global_result = fields.Char(string='Global result', readonly=True)
|
||||
bu_create_date = fields.Datetime(string='Build create date', readonly=True)
|
||||
committer = fields.Char(string='committer', readonly=True)
|
||||
author = fields.Char(string='Author', readonly=True)
|
||||
host = fields.Char(string='Host', readonly=True)
|
||||
config_id = fields.Many2one('runbot.build.config', string='Config', readonly=True)
|
||||
parent_id = fields.Many2one('runbot.build', string='Parent build', readonly=True)
|
||||
hidden = fields.Boolean(string='Hidden', readonly=True)
|
||||
branch_id = fields.Many2one('runbot.branch', string='Branch', readonly=True)
|
||||
branch_name = fields.Char(string='Branch name', readonly=True)
|
||||
branch_sticky = fields.Boolean(string='Sticky', readonly=True)
|
||||
repo_id = fields.Many2one('runbot.repo', string='Repo', readonly=True)
|
||||
repo_name = fields.Char(string='Repo name', readonly=True)
|
||||
repo_short_name = fields.Char(compute='_compute_repo_short_name', readonly=True)
|
||||
#bundle_id = fields.Many2one('runbot.bundle', string='Bundle', readonly=True)
|
||||
#bundle_name = fields.Char(string='Bundle name', readonly=True)
|
||||
#bundle_sticky = fields.Boolean(string='Sticky', readonly=True)
|
||||
build_url = fields.Char(compute='_compute_build_url', readonly=True)
|
||||
|
||||
def _compute_repo_short_name(self):
|
||||
for l in self:
|
||||
l.repo_short_name = '/'.join(l.repo_id.base.split('/')[-2:])
|
||||
l.repo_short_name = '%s/%s' % (l.repo_id.owner, l.repo_id.repo_name)
|
||||
|
||||
def _compute_build_url(self):
|
||||
for l in self:
|
||||
@ -152,32 +145,18 @@ class RunbotErrorLog(models.Model):
|
||||
l.path AS path,
|
||||
l.line AS line,
|
||||
bu.id AS build_id,
|
||||
bu.name AS bu_name,
|
||||
bu.dest AS dest,
|
||||
bu.local_state AS local_state,
|
||||
bu.local_result AS local_result,
|
||||
bu.global_state AS global_state,
|
||||
bu.global_result AS global_result,
|
||||
bu.create_date AS bu_create_date,
|
||||
bu.committer AS committer,
|
||||
bu.author AS author,
|
||||
bu.host AS host,
|
||||
bu.config_id AS config_id,
|
||||
bu.parent_id AS parent_id,
|
||||
bu.hidden AS hidden,
|
||||
br.id AS branch_id,
|
||||
br.branch_name AS branch_name,
|
||||
br.sticky AS branch_sticky,
|
||||
re.id AS repo_id,
|
||||
re.name AS repo_name
|
||||
bu.parent_id AS parent_id
|
||||
FROM
|
||||
ir_logging AS l
|
||||
JOIN
|
||||
runbot_build bu ON l.build_id = bu.id
|
||||
JOIN
|
||||
runbot_branch br ON br.id = bu.branch_id
|
||||
JOIN
|
||||
runbot_repo re ON br.repo_id = re.id
|
||||
WHERE
|
||||
l.level = 'ERROR'
|
||||
)""")
|
||||
|
@ -1,26 +1,28 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import models, fields, api
|
||||
from ..common import fqdn, local_pgadmin_cursor
|
||||
from ..common import fqdn, local_pgadmin_cursor, os
|
||||
from ..container import docker_build
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunboHost(models.Model):
|
||||
_name = "runbot.host"
|
||||
class Host(models.Model):
|
||||
_name = 'runbot.host'
|
||||
_description = "Host"
|
||||
_order = 'id'
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
name = fields.Char('Host name', required=True, unique=True)
|
||||
disp_name = fields.Char('Display name')
|
||||
active = fields.Boolean('Active', default=True)
|
||||
active = fields.Boolean('Active', default=True, tracking=True)
|
||||
last_start_loop = fields.Datetime('Last start')
|
||||
last_end_loop = fields.Datetime('Last end')
|
||||
last_success = fields.Datetime('Last success')
|
||||
assigned_only = fields.Boolean('Only accept assigned build', default=False)
|
||||
nb_worker = fields.Integer('Number of max paralel build', help="0 to use icp value", default=0)
|
||||
assigned_only = fields.Boolean('Only accept assigned build', default=False, tracking=True)
|
||||
nb_worker = fields.Integer(
|
||||
'Number of max paralel build',
|
||||
default=lambda self: self.env['ir.config_parameter'].sudo().get_param('runbot.runbot_workers', default=2),
|
||||
tracking=True
|
||||
)
|
||||
nb_testing = fields.Integer(compute='_compute_nb')
|
||||
nb_running = fields.Integer(compute='_compute_nb')
|
||||
last_exception = fields.Char('Last exception')
|
||||
@ -43,20 +45,20 @@ class RunboHost(models.Model):
|
||||
|
||||
@api.model_create_single
|
||||
def create(self, values):
|
||||
if not 'disp_name' in values:
|
||||
if 'disp_name' not in values:
|
||||
values['disp_name'] = values['name']
|
||||
return super().create(values)
|
||||
|
||||
def _bootstrap_db_template(self):
|
||||
""" boostrap template database if needed """
|
||||
icp = self.env['ir.config_parameter']
|
||||
db_template = icp.get_param('runbot.runbot_db_template', default='template1')
|
||||
if db_template and db_template != 'template1':
|
||||
db_template = icp.get_param('runbot.runbot_db_template', default='template0')
|
||||
if db_template and db_template != 'template0':
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
local_cr.execute("""SELECT datname FROM pg_catalog.pg_database WHERE datname = '%s';""" % db_template)
|
||||
res = local_cr.fetchone()
|
||||
if not res:
|
||||
local_cr.execute("""CREATE DATABASE "%s" TEMPLATE template1 LC_COLLATE 'C' ENCODING 'unicode'""" % db_template)
|
||||
local_cr.execute("""CREATE DATABASE "%s" TEMPLATE template0 LC_COLLATE 'C' ENCODING 'unicode'""" % db_template)
|
||||
# TODO UPDATE pg_database set datallowconn = false, datistemplate = true (but not enough privileges)
|
||||
|
||||
def _bootstrap(self):
|
||||
@ -78,17 +80,13 @@ class RunboHost(models.Model):
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '../static'))
|
||||
|
||||
@api.model
|
||||
def _get_current(self):
|
||||
name = fqdn()
|
||||
def _get_current(self, suffix=''):
|
||||
name = '%s%s' % (fqdn(), suffix)
|
||||
return self.search([('name', '=', name)]) or self.create({'name': name})
|
||||
|
||||
def get_nb_worker(self):
|
||||
icp = self.env['ir.config_parameter']
|
||||
return self.nb_worker or int(icp.sudo().get_param('runbot.runbot_workers', default=6))
|
||||
|
||||
def get_running_max(self):
|
||||
icp = self.env['ir.config_parameter']
|
||||
return int(icp.get_param('runbot.runbot_running_max', default=75))
|
||||
return int(icp.get_param('runbot.runbot_running_max', default=5))
|
||||
|
||||
def set_psql_conn_count(self):
|
||||
_logger.debug('Updating psql connection count...')
|
||||
@ -102,7 +100,7 @@ class RunboHost(models.Model):
|
||||
return sum(host.nb_testing for host in self)
|
||||
|
||||
def _total_workers(self):
|
||||
return sum(host.get_nb_worker() for host in self)
|
||||
return sum(host.nb_worker for host in self)
|
||||
|
||||
def disable(self):
|
||||
""" Reserve host if possible """
|
||||
|
@ -6,6 +6,7 @@ from odoo import models, fields
|
||||
odoo.service.server.SLEEP_INTERVAL = 5
|
||||
odoo.addons.base.models.ir_cron._intervalTypes['seconds'] = lambda interval: relativedelta(seconds=interval)
|
||||
|
||||
|
||||
class ir_cron(models.Model):
|
||||
_inherit = "ir.cron"
|
||||
|
||||
|
15
runbot/models/ir_ui_view.py
Normal file
15
runbot/models/ir_ui_view.py
Normal file
@ -0,0 +1,15 @@
|
||||
from ..common import s2human, s2human_long
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class IrUiView(models.Model):
|
||||
_inherit = ["ir.ui.view"]
|
||||
|
||||
def _prepare_qcontext(self):
|
||||
qcontext = super(IrUiView, self)._prepare_qcontext()
|
||||
|
||||
if request and getattr(request, 'is_frontend', False):
|
||||
qcontext['s2human'] = s2human
|
||||
qcontext['s2human_long'] = s2human_long
|
||||
return qcontext
|
20
runbot/models/project.py
Normal file
20
runbot/models/project.py
Normal file
@ -0,0 +1,20 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_name = 'runbot.project'
|
||||
_description = 'Project'
|
||||
|
||||
name = fields.Char('Project name', required=True, unique=True)
|
||||
group_ids = fields.Many2many('res.groups', string='Required groups')
|
||||
|
||||
trigger_ids = fields.One2many('runbot.trigger', 'project_id', string='Triggers')
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
_name = 'runbot.category'
|
||||
_description = 'Trigger category'
|
||||
|
||||
name = fields.Char("Name")
|
||||
icon = fields.Char("Font awesome icon")
|
||||
view_id = fields.Many2one('ir.ui.view', "Link template")
|
File diff suppressed because it is too large
Load Diff
@ -1,29 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
|
||||
from .. import common
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
runbot_workers = fields.Integer('Total number of workers')
|
||||
runbot_workers = fields.Integer('Default number of workers')
|
||||
runbot_running_max = fields.Integer('Maximum number of running builds')
|
||||
runbot_timeout = fields.Integer('Max allowed step timeout (in seconds)')
|
||||
runbot_starting_port = fields.Integer('Starting port for running builds')
|
||||
runbot_domain = fields.Char('Runbot domain')
|
||||
runbot_max_age = fields.Integer('Max branch age (in days)')
|
||||
runbot_max_age = fields.Integer('Max commit age (in days)')
|
||||
runbot_logdb_uri = fields.Char('Runbot URI for build logs')
|
||||
runbot_update_frequency = fields.Integer('Update frequency (in seconds)')
|
||||
runbot_template = fields.Char('Postgresql template', help="Postgresql template to use when creating DB's")
|
||||
runbot_message = fields.Text('Frontend warning message')
|
||||
runbot_do_fetch = fields.Boolean('Discover new commits')
|
||||
runbot_do_schedule = fields.Boolean('Schedule builds')
|
||||
runbot_is_base_regex = fields.Char('Regex is_base')
|
||||
|
||||
runbot_db_gc_days = fields.Integer('Days before gc', default=30, config_parameter='runbot.db_gc_days')
|
||||
runbot_db_gc_days_child = fields.Integer('Days before gc of child', default=15, config_parameter='runbot.db_gc_days_child')
|
||||
|
||||
runbot_pending_warning = fields.Integer('Pending warning limit', default=5, config_parameter='runbot.pending.warning')
|
||||
runbot_pending_critical = fields.Integer('Pending critical limit', default=5, config_parameter='runbot.pending.critical')
|
||||
|
||||
# TODO other icp
|
||||
# runbot.runbot_maxlogs 100
|
||||
# runbot.runbot_nginx True
|
||||
# migration db
|
||||
# ln path
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||
res.update(runbot_workers=int(get_param('runbot.runbot_workers', default=6)),
|
||||
runbot_running_max=int(get_param('runbot.runbot_running_max', default=75)),
|
||||
res.update(runbot_workers=int(get_param('runbot.runbot_workers', default=2)),
|
||||
runbot_running_max=int(get_param('runbot.runbot_running_max', default=5)),
|
||||
runbot_timeout=int(get_param('runbot.runbot_timeout', default=10000)),
|
||||
runbot_starting_port=int(get_param('runbot.runbot_starting_port', default=2000)),
|
||||
runbot_domain=get_param('runbot.runbot_domain', default=common.fqdn()),
|
||||
@ -32,6 +49,9 @@ class ResConfigSettings(models.TransientModel):
|
||||
runbot_update_frequency=int(get_param('runbot.runbot_update_frequency', default=10)),
|
||||
runbot_template=get_param('runbot.runbot_db_template'),
|
||||
runbot_message=get_param('runbot.runbot_message', default=''),
|
||||
runbot_do_fetch=get_param('runbot.runbot_do_fetch', default=False),
|
||||
runbot_do_schedule=get_param('runbot.runbot_do_schedule', default=False),
|
||||
runbot_is_base_regex=get_param('runbot.runbot_is_base_regex', default='')
|
||||
)
|
||||
return res
|
||||
|
||||
@ -48,3 +68,16 @@ class ResConfigSettings(models.TransientModel):
|
||||
set_param('runbot.runbot_update_frequency', self.runbot_update_frequency)
|
||||
set_param('runbot.runbot_db_template', self.runbot_template)
|
||||
set_param('runbot.runbot_message', self.runbot_message)
|
||||
set_param('runbot.runbot_do_fetch', self.runbot_do_fetch)
|
||||
set_param('runbot.runbot_do_schedule', self.runbot_do_schedule)
|
||||
set_param('runbot.runbot_is_base_regex', self.runbot_is_base_regex)
|
||||
|
||||
@api.onchange('runbot_is_base_regex')
|
||||
def _on_change_is_base_regex(self):
|
||||
""" verify that the base_regex is valid
|
||||
"""
|
||||
if self.runbot_is_base_regex:
|
||||
try:
|
||||
re.compile(self.runbot_is_base_regex)
|
||||
except re.error:
|
||||
raise UserError("The regex is invalid")
|
||||
|
350
runbot/models/runbot.py
Normal file
350
runbot/models/runbot.py
Normal file
@ -0,0 +1,350 @@
|
||||
import time
|
||||
import logging
|
||||
import glob
|
||||
import random
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
from ..common import fqdn, dest_reg, os
|
||||
from ..container import docker_ps, docker_stop
|
||||
|
||||
from odoo import models, fields
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import config
|
||||
from odoo.modules.module import get_module_resource
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# after this point, not realy a repo buisness
|
||||
class Runbot(models.AbstractModel):
|
||||
_name = 'runbot.runbot'
|
||||
_description = 'Base runbot model'
|
||||
|
||||
def _commit(self):
|
||||
self.env.cr.commit()
|
||||
self.env.cache.invalidate()
|
||||
self.env.clear()
|
||||
|
||||
def _root(self):
|
||||
"""Return root directory of repository"""
|
||||
default = os.path.join(os.path.dirname(__file__), '../static')
|
||||
return os.path.abspath(default)
|
||||
|
||||
def _scheduler(self, host):
|
||||
|
||||
self._gc_testing(host)
|
||||
self._commit()
|
||||
for build in self._get_builds_with_requested_actions(host):
|
||||
build._process_requested_actions()
|
||||
self._commit()
|
||||
for build in self._get_builds_to_schedule(host):
|
||||
build._schedule()
|
||||
self._commit()
|
||||
self._assign_pending_builds(host, host.nb_worker, [('build_type', '!=', 'scheduled')])
|
||||
self._commit()
|
||||
self._assign_pending_builds(host, host.nb_worker-1 or host.nb_worker)
|
||||
self._commit()
|
||||
for build in self._get_builds_to_init(host):
|
||||
build._init_pendings(host)
|
||||
self._commit()
|
||||
self._gc_running(host)
|
||||
self._commit()
|
||||
self._reload_nginx()
|
||||
|
||||
def build_domain_host(self, host, domain=None):
|
||||
domain = domain or []
|
||||
return [('host', '=', host.name)] + domain
|
||||
|
||||
def _get_builds_with_requested_actions(self, host):
|
||||
return self.env['runbot.build'].search(self.build_domain_host(host, [('requested_action', 'in', ['wake_up', 'deathrow'])]))
|
||||
|
||||
def _get_builds_to_schedule(self, host):
|
||||
return self.env['runbot.build'].search(self.build_domain_host(host, [('local_state', 'in', ['testing', 'running'])]))
|
||||
|
||||
def _assign_pending_builds(self, host, nb_worker, domain=None):
|
||||
if host.assigned_only or nb_worker <= 0:
|
||||
return
|
||||
domain_host = self.build_domain_host(host)
|
||||
reserved_slots = self.env['runbot.build'].search_count(domain_host + [('local_state', 'in', ('testing', 'pending'))])
|
||||
assignable_slots = (nb_worker - reserved_slots)
|
||||
if assignable_slots > 0:
|
||||
allocated = self._allocate_builds(host, assignable_slots, domain)
|
||||
if allocated:
|
||||
|
||||
_logger.info('Builds %s where allocated to runbot', allocated)
|
||||
|
||||
def _get_builds_to_init(self, host):
|
||||
domain_host = self.build_domain_host(host)
|
||||
used_slots = self.env['runbot.build'].search_count(domain_host + [('local_state', '=', 'testing')])
|
||||
available_slots = host.nb_worker - used_slots
|
||||
if available_slots <= 0:
|
||||
return self.env['runbot.build']
|
||||
return self.env['runbot.build'].search(domain_host + [('local_state', '=', 'pending')], limit=available_slots)
|
||||
|
||||
def _gc_running(self, host):
|
||||
running_max = host.get_running_max()
|
||||
domain_host = self.build_domain_host(host)
|
||||
Build = self.env['runbot.build']
|
||||
cannot_be_killed_ids = Build.search(domain_host + [('keep_running', '!=', True)]).ids
|
||||
sticky_bundles = self.env['runbot.bundle'].search([('sticky', '=', True)])
|
||||
cannot_be_killed_ids = [
|
||||
build.id
|
||||
for build in sticky_bundles.mapped('last_batchs.slot_ids.build_id')
|
||||
if build.host == host.name
|
||||
][:running_max]
|
||||
build_ids = Build.search(domain_host + [('local_state', '=', 'running'), ('id', 'not in', cannot_be_killed_ids)], order='job_start desc').ids
|
||||
Build.browse(build_ids)[running_max:]._kill()
|
||||
|
||||
def _gc_testing(self, host):
|
||||
"""garbage collect builds that could be killed"""
|
||||
# decide if we need room
|
||||
Build = self.env['runbot.build']
|
||||
domain_host = self.build_domain_host(host)
|
||||
testing_builds = Build.search(domain_host + [('local_state', 'in', ['testing', 'pending']), ('requested_action', '!=', 'deathrow')])
|
||||
used_slots = len(testing_builds)
|
||||
available_slots = host.nb_worker - used_slots
|
||||
nb_pending = Build.search_count([('local_state', '=', 'pending'), ('host', '=', False)])
|
||||
if available_slots > 0 or nb_pending == 0:
|
||||
return
|
||||
|
||||
for build in testing_builds:
|
||||
top_parent = build._get_top_parent()
|
||||
if build.killable:
|
||||
top_parent._ask_kill(message='Build automatically killed, new build found.')
|
||||
|
||||
def _allocate_builds(self, host, nb_slots, domain=None):
|
||||
if nb_slots <= 0:
|
||||
return []
|
||||
non_allocated_domain = [('local_state', '=', 'pending'), ('host', '=', False)]
|
||||
if domain:
|
||||
non_allocated_domain = expression.AND([non_allocated_domain, domain])
|
||||
e = expression.expression(non_allocated_domain, self.env['runbot.build'])
|
||||
assert e.get_tables() == ['"runbot_build"']
|
||||
where_clause, where_params = e.to_sql()
|
||||
|
||||
# self-assign to be sure that another runbot batch cannot self assign the same builds
|
||||
query = """UPDATE
|
||||
runbot_build
|
||||
SET
|
||||
host = %%s
|
||||
WHERE
|
||||
runbot_build.id IN (
|
||||
SELECT runbot_build.id
|
||||
FROM runbot_build
|
||||
WHERE
|
||||
%s
|
||||
ORDER BY
|
||||
array_position(array['normal','rebuild','indirect','scheduled']::varchar[], runbot_build.build_type) ASC
|
||||
FOR UPDATE OF runbot_build SKIP LOCKED
|
||||
LIMIT %%s
|
||||
)
|
||||
RETURNING id""" % where_clause
|
||||
self.env.cr.execute(query, [host.name] + where_params + [nb_slots])
|
||||
return self.env.cr.fetchall()
|
||||
|
||||
def _domain(self):
|
||||
return self.env.get('ir.config_parameter').get_param('runbot.runbot_domain', fqdn())
|
||||
|
||||
def _reload_nginx(self):
|
||||
env = self.env
|
||||
settings = {}
|
||||
settings['port'] = config.get('http_port')
|
||||
settings['runbot_static'] = os.path.join(get_module_resource('runbot', 'static'), '')
|
||||
nginx_dir = os.path.join(self._root(), 'nginx')
|
||||
settings['nginx_dir'] = nginx_dir
|
||||
settings['re_escape'] = re.escape
|
||||
settings['fqdn'] = fqdn()
|
||||
|
||||
icp = env['ir.config_parameter'].sudo()
|
||||
nginx = icp.get_param('runbot.runbot_nginx', True) # or just force nginx?
|
||||
|
||||
if nginx:
|
||||
settings['builds'] = env['runbot.build'].search([('local_state', '=', 'running'), ('host', '=', fqdn())])
|
||||
|
||||
nginx_config = env['ir.ui.view'].render_template("runbot.nginx_config", settings)
|
||||
os.makedirs(nginx_dir, exist_ok=True)
|
||||
content = None
|
||||
nginx_conf_path = os.path.join(nginx_dir, 'nginx.conf')
|
||||
content = ''
|
||||
if os.path.isfile(nginx_conf_path):
|
||||
with open(nginx_conf_path, 'rb') as f:
|
||||
content = f.read()
|
||||
if content != nginx_config:
|
||||
_logger.debug('reload nginx')
|
||||
with open(nginx_conf_path, 'wb') as f:
|
||||
f.write(nginx_config)
|
||||
try:
|
||||
pid = int(open(os.path.join(nginx_dir, 'nginx.pid')).read().strip(' \n'))
|
||||
os.kill(pid, signal.SIGHUP)
|
||||
except Exception:
|
||||
_logger.debug('start nginx')
|
||||
if subprocess.call(['/usr/sbin/nginx', '-p', nginx_dir, '-c', 'nginx.conf']):
|
||||
# obscure nginx bug leaving orphan worker listening on nginx port
|
||||
if not subprocess.call(['pkill', '-f', '-P1', 'nginx: worker']):
|
||||
_logger.debug('failed to start nginx - orphan worker killed, retrying')
|
||||
subprocess.call(['/usr/sbin/nginx', '-p', nginx_dir, '-c', 'nginx.conf'])
|
||||
else:
|
||||
_logger.debug('failed to start nginx - failed to kill orphan worker - oh well')
|
||||
|
||||
def _get_cron_period(self):
|
||||
""" Compute a randomized cron period with a 2 min margin below
|
||||
real cron timeout from config.
|
||||
"""
|
||||
cron_limit = config.get('limit_time_real_cron')
|
||||
req_limit = config.get('limit_time_real')
|
||||
cron_timeout = cron_limit if cron_limit > -1 else req_limit
|
||||
return cron_timeout / 2
|
||||
|
||||
def _cron(self):
|
||||
"""
|
||||
This method is the default cron for new commit discovery and build sheduling.
|
||||
The cron runs for a long time to avoid spamming logs
|
||||
"""
|
||||
start_time = time.time()
|
||||
timeout = self._get_cron_period()
|
||||
get_param = self.env['ir.config_parameter'].get_param
|
||||
update_frequency = int(get_param('runbot.runbot_update_frequency', default=10))
|
||||
runbot_do_fetch = get_param('runbot.runbot_do_fetch')
|
||||
runbot_do_schedule = get_param('runbot.runbot_do_schedule')
|
||||
host = self.env['runbot.host']._get_current()
|
||||
host.set_psql_conn_count()
|
||||
host.last_start_loop = fields.Datetime.now()
|
||||
self._commit()
|
||||
# Bootstrap
|
||||
host._bootstrap()
|
||||
if runbot_do_schedule:
|
||||
host._docker_build()
|
||||
self._source_cleanup()
|
||||
self.env['runbot.build']._local_cleanup()
|
||||
self._docker_cleanup()
|
||||
_logger.info('Starting loop')
|
||||
while time.time() - start_time < timeout:
|
||||
repos = self.env['runbot.repo'].search([('mode', '!=', 'disabled')])
|
||||
|
||||
processing_batch = self.env['runbot.batch'].search([('state', 'in', ('preparing', 'ready'))], order='id asc')
|
||||
preparing_batch = processing_batch.filtered(lambda b: b.state == 'preparing')
|
||||
self._commit()
|
||||
if runbot_do_fetch:
|
||||
for repo in repos:
|
||||
repo._update_batches(bool(preparing_batch))
|
||||
self._commit()
|
||||
if processing_batch:
|
||||
_logger.info('starting processing of %s batches', len(processing_batch))
|
||||
for batch in processing_batch:
|
||||
batch._process()
|
||||
self._commit()
|
||||
_logger.info('end processing')
|
||||
self._commit()
|
||||
if runbot_do_schedule:
|
||||
sleep_time = self._scheduler_loop_turn(host, update_frequency)
|
||||
self.sleep(sleep_time)
|
||||
else:
|
||||
self.sleep(update_frequency)
|
||||
self._commit()
|
||||
|
||||
host.last_end_loop = fields.Datetime.now()
|
||||
|
||||
def sleep(self, t):
|
||||
time.sleep(t)
|
||||
|
||||
def _scheduler_loop_turn(self, host, default_sleep=1):
|
||||
try:
|
||||
self._scheduler(host)
|
||||
host.last_success = fields.Datetime.now()
|
||||
self._commit()
|
||||
except Exception as e:
|
||||
self.env.cr.rollback()
|
||||
self.env.clear()
|
||||
_logger.exception(e)
|
||||
message = str(e)
|
||||
if host.last_exception == message:
|
||||
host.exception_count += 1
|
||||
else:
|
||||
host.last_exception = str(e)
|
||||
host.exception_count = 1
|
||||
self._commit()
|
||||
return random.uniform(0, 3)
|
||||
else:
|
||||
if host.last_exception:
|
||||
host.last_exception = ""
|
||||
host.exception_count = 0
|
||||
return default_sleep
|
||||
|
||||
def _source_cleanup(self):
|
||||
try:
|
||||
if self.pool._init:
|
||||
return
|
||||
_logger.info('Source cleaning')
|
||||
# we can remove a source only if no build are using them as name or rependency_ids aka as commit
|
||||
cannot_be_deleted_builds = self.env['runbot.build'].search([('host', '=', fqdn()), ('local_state', '!=', 'done')])
|
||||
cannot_be_deleted_builds |= cannot_be_deleted_builds.mapped('params_id.builds_reference_ids')
|
||||
cannot_be_deleted_path = set()
|
||||
for build in cannot_be_deleted_builds:
|
||||
for build_commit in build.params_id.commit_link_ids:
|
||||
cannot_be_deleted_path.add(build_commit.commit_id._source_path())
|
||||
|
||||
to_delete = set()
|
||||
to_keep = set()
|
||||
repos = self.env['runbot.repo'].search([('mode', '!=', 'disabled')])
|
||||
for repo in repos:
|
||||
repo_source = os.path.join(self._root(), 'sources', repo.name, '*')
|
||||
for source_dir in glob.glob(repo_source):
|
||||
if source_dir not in cannot_be_deleted_path:
|
||||
to_delete.add(source_dir)
|
||||
else:
|
||||
to_keep.add(source_dir)
|
||||
|
||||
# we are comparing cannot_be_deleted_path with to keep to sensure that the algorithm is working, we want to avoid to erase file by mistake
|
||||
# note: it is possible that a parent_build is in testing without checkouting sources, but it should be exceptions
|
||||
if to_delete:
|
||||
if cannot_be_deleted_path != to_keep:
|
||||
_logger.warning('Inconsistency between sources and database: \n%s \n%s' % (cannot_be_deleted_path-to_keep, to_keep-cannot_be_deleted_path))
|
||||
to_delete = list(to_delete)
|
||||
to_keep = list(to_keep)
|
||||
cannot_be_deleted_path = list(cannot_be_deleted_path)
|
||||
for source_dir in to_delete:
|
||||
_logger.info('Deleting source: %s' % source_dir)
|
||||
assert 'static' in source_dir
|
||||
shutil.rmtree(source_dir)
|
||||
_logger.info('%s/%s source folder where deleted (%s kept)' % (len(to_delete), len(to_delete+to_keep), len(to_keep)))
|
||||
except:
|
||||
_logger.exception('An exception occured while cleaning sources')
|
||||
pass
|
||||
|
||||
def _docker_cleanup(self):
|
||||
_logger.info('Docker cleaning')
|
||||
docker_ps_result = docker_ps()
|
||||
|
||||
containers = {}
|
||||
ignored = []
|
||||
for dc in docker_ps_result:
|
||||
build = self.env['runbot.build']._build_from_dest(dc)
|
||||
if build:
|
||||
containers[build.id] = dc
|
||||
if containers:
|
||||
candidates = self.env['runbot.build'].search([('id', 'in', list(containers.keys())), ('local_state', '=', 'done')])
|
||||
for c in candidates:
|
||||
_logger.info('container %s found running with build state done', containers[c.id])
|
||||
docker_stop(containers[c.id], c._path())
|
||||
ignored = {dc for dc in docker_ps_result if not dest_reg.match(dc)}
|
||||
if ignored:
|
||||
_logger.debug('docker (%s) not deleted because not dest format', list(ignored))
|
||||
|
||||
def warning(self, message, *args):
|
||||
if args:
|
||||
message = message % args
|
||||
return self.env['runbot.warning'].create({'message': message})
|
||||
|
||||
|
||||
class RunbotWarning(models.Model):
|
||||
"""
|
||||
Generic Warnings for runbot
|
||||
"""
|
||||
|
||||
_name = 'runbot.warning'
|
||||
_description = 'Generic Runbot Warning'
|
||||
|
||||
message = fields.Char("Warning", index=True)
|
63
runbot/models/upgrade.py
Normal file
63
runbot/models/upgrade.py
Normal file
@ -0,0 +1,63 @@
|
||||
import re
|
||||
from odoo import models, fields
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class UpgradeExceptions(models.Model):
|
||||
_name = 'runbot.upgrade.exception'
|
||||
_description = 'Upgrade exception'
|
||||
|
||||
active = fields.Boolean('Active', default=True)
|
||||
elements = fields.Text('Elements')
|
||||
bundle_id = fields.Many2one('runbot.bundle', index=True)
|
||||
info = fields.Text('Info')
|
||||
|
||||
def _generate(self):
|
||||
exceptions = self.search([])
|
||||
if exceptions:
|
||||
return 'suppress_upgrade_warnings=%s' % (','.join(exceptions.mapped('elements'))).replace(' ', '').replace('\n', ',')
|
||||
return False
|
||||
|
||||
|
||||
class UpgradeRegex(models.Model):
|
||||
_name = 'runbot.upgrade.regex'
|
||||
_description = 'Upgrade regex'
|
||||
|
||||
active = fields.Boolean('Active', default=True)
|
||||
prefix = fields.Char('Type')
|
||||
regex = fields.Char('Regex')
|
||||
|
||||
|
||||
class BuildResult(models.Model):
|
||||
_inherit = 'runbot.build'
|
||||
|
||||
def _parse_upgrade_errors(self):
|
||||
ir_logs = self.env['ir.logging'].search([('level', 'in', ('ERROR', 'WARNING', 'CRITICAL')), ('type', '=', 'server'), ('build_id', 'in', self.ids)])
|
||||
|
||||
upgrade_regexes = self.env['runbot.upgrade.regex'].search([])
|
||||
exception = []
|
||||
for log in ir_logs:
|
||||
for upgrade_regex in upgrade_regexes:
|
||||
m = re.search(upgrade_regex.regex, log.message)
|
||||
if m:
|
||||
exception.append('%s:%s' % (upgrade_regex.prefix, m.groups()[0]))
|
||||
|
||||
if exception:
|
||||
bundle = False
|
||||
batches = self._get_top_parent().slot_ids.mapped('batch_id')
|
||||
if batches:
|
||||
bundle = batches[0].bundle_id.id
|
||||
res = {
|
||||
'name': 'Upgrade Exception',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'runbot.upgrade.exception',
|
||||
'view_mode': 'form',
|
||||
'context': {
|
||||
'default_elements': '\n'.join(exception),
|
||||
'default_bundle_id': bundle,
|
||||
'default_info': 'Automatically generated from build %s' % self.id
|
||||
}
|
||||
}
|
||||
return res
|
||||
else:
|
||||
raise UserError('Nothing found here')
|
10
runbot/models/user.py
Normal file
10
runbot/models/user.py
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
# Add default action_id
|
||||
action_id = fields.Many2one('ir.actions.actions',
|
||||
default=lambda self: self.env.ref('runbot.runbot_menu_warning_root', raise_if_not_found=False))
|
102
runbot/models/version.py
Normal file
102
runbot/models/version.py
Normal file
@ -0,0 +1,102 @@
|
||||
import logging
|
||||
import re
|
||||
from odoo import models, fields, api, tools
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Version(models.Model):
|
||||
_name = 'runbot.version'
|
||||
_description = "Version"
|
||||
_order = 'sequence desc, number desc,id'
|
||||
|
||||
name = fields.Char('Version name')
|
||||
number = fields.Char('Version number', compute='_compute_version_number', store=True, help="Usefull to sort by version")
|
||||
sequence = fields.Integer('sequence')
|
||||
is_major = fields.Char('Is major version', compute='_compute_version_number', store=True)
|
||||
|
||||
base_bundle_id = fields.Many2one('runbot.bundle', compute='_compute_base_bundle_id')
|
||||
|
||||
previous_major_version_id = fields.Many2one('runbot.version', compute='_compute_version_relations')
|
||||
intermediate_version_ids = fields.Many2many('runbot.version', compute='_compute_version_relations')
|
||||
next_major_version_id = fields.Many2one('runbot.version', compute='_compute_version_relations')
|
||||
next_intermediate_version_ids = fields.Many2many('runbot.version', compute='_compute_version_relations')
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_version_number(self):
|
||||
for version in self:
|
||||
if version.name == 'master':
|
||||
version.number = '~'
|
||||
version.is_major = False
|
||||
else:
|
||||
# max version number with this format: 99.99
|
||||
version.number = '.'.join([elem.zfill(2) for elem in re.sub(r'[^0-9\.]', '', version.name).split('.')])
|
||||
version.is_major = all(elem == '00' for elem in version.number.split('.')[1:])
|
||||
|
||||
def create(self, values):
|
||||
model = self.browse()
|
||||
model._get_id.clear_cache(model)
|
||||
return super().create(values)
|
||||
|
||||
def _get(self, name):
|
||||
return self.browse(self._get_id(name))
|
||||
|
||||
@tools.ormcache('name')
|
||||
def _get_id(self, name):
|
||||
version = self.search([('name', '=', name)])
|
||||
if not version:
|
||||
version = self.create({
|
||||
'name': name,
|
||||
})
|
||||
return version.id
|
||||
|
||||
@api.depends('is_major', 'number')
|
||||
def _compute_version_relations(self):
|
||||
all_versions = self.search([], order='sequence, number')
|
||||
for version in self:
|
||||
version.previous_major_version_id = next(
|
||||
(
|
||||
v
|
||||
for v in reversed(all_versions)
|
||||
if v.is_major and v.number < version.number and v.sequence <= version.sequence # TODO FIXME, make version comparable?
|
||||
), self.browse())
|
||||
if version.previous_major_version_id:
|
||||
version.intermediate_version_ids = all_versions.filtered(
|
||||
lambda v, current=version: v.number > current.previous_major_version_id.number and v.number < current.number and v.sequence <= current.sequence and v.sequence >= current.previous_major_version_id.sequence
|
||||
)
|
||||
else:
|
||||
version.intermediate_version_ids = all_versions.filtered(
|
||||
lambda v, current=version: v.number < current.number and v.sequence <= current.sequence
|
||||
)
|
||||
version.next_major_version_id = next(
|
||||
(
|
||||
v
|
||||
for v in all_versions
|
||||
if (v.is_major or v.name == 'master') and v.number > version.number and v.sequence >= version.sequence
|
||||
), self.browse())
|
||||
if version.next_major_version_id:
|
||||
version.next_intermediate_version_ids = all_versions.filtered(
|
||||
lambda v, current=version: v.number < current.next_major_version_id.number and v.number > current.number and v.sequence <= current.next_major_version_id.sequence and v.sequence >= current.sequence
|
||||
)
|
||||
else:
|
||||
version.next_intermediate_version_ids = all_versions.filtered(
|
||||
lambda v, current=version: v.number > current.number and v.sequence >= current.sequence
|
||||
)
|
||||
|
||||
# @api.depends('base_bundle_id.is_base', 'base_bundle_id.version_id', 'base_bundle_id.project_id')
|
||||
@api.depends_context('project_id')
|
||||
def _compute_base_bundle_id(self):
|
||||
project_id = self.env.context.get('project_id')
|
||||
if not project_id:
|
||||
_logger.warning("_compute_base_bundle_id: no project_id in context")
|
||||
project_id = self.env.ref('runbot.main_project').id
|
||||
|
||||
bundles = self.env['runbot.bundle'].search([
|
||||
('version_id', 'in', self.ids),
|
||||
('is_base', '=', True),
|
||||
('project_id', '=', project_id)
|
||||
])
|
||||
bundle_by_version = {bundle.version_id.id: bundle for bundle in bundles}
|
||||
for version in self:
|
||||
version.base_bundle_id = bundle_by_version.get(version.id)
|
@ -1,12 +1,10 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_runbot_repo,runbot_repo,runbot.model_runbot_repo,group_user,1,0,0,0
|
||||
access_runbot_remote,runbot_remote,runbot.model_runbot_remote,group_user,1,0,0,0
|
||||
access_runbot_branch,runbot_branch,runbot.model_runbot_branch,group_user,1,0,0,0
|
||||
access_runbot_build,runbot_build,runbot.model_runbot_build,group_user,1,0,0,0
|
||||
access_runbot_build_dependency,runbot_build_dependency,runbot.model_runbot_build_dependency,group_user,1,0,0,0
|
||||
access_runbot_repo_admin,runbot_repo_admin,runbot.model_runbot_repo,runbot.group_runbot_admin,1,1,1,1
|
||||
access_runbot_remote_admin,runbot_remote_admin,runbot.model_runbot_remote,runbot.group_runbot_admin,1,1,1,1
|
||||
access_runbot_branch_admin,runbot_branch_admin,runbot.model_runbot_branch,runbot.group_runbot_admin,1,1,1,1
|
||||
access_runbot_build_admin,runbot_build_admin,runbot.model_runbot_build,runbot.group_runbot_admin,1,1,1,1
|
||||
access_runbot_build_dependency_admin,runbot_build_dependency_admin,runbot.model_runbot_build_dependency,runbot.group_runbot_admin,1,1,1,1
|
||||
access_irlogging,log by runbot users,base.model_ir_logging,group_user,0,0,1,0
|
||||
|
||||
access_runbot_build_config_step_user,runbot_build_config_step_user,runbot.model_runbot_build_config_step,group_user,1,0,0,0
|
||||
@ -18,6 +16,9 @@ access_runbot_build_config_manager,runbot_build_config_manager,runbot.model_runb
|
||||
access_runbot_build_config_step_order_user,runbot_build_config_step_order_user,runbot.model_runbot_build_config_step_order,group_user,1,0,0,0
|
||||
access_runbot_build_config_step_order_manager,runbot_build_config_step_order_manager,runbot.model_runbot_build_config_step_order,runbot.group_build_config_user,1,1,1,1
|
||||
|
||||
access_runbot_config_step_upgrade_db_user,runbot_config_step_upgrade_db_user,runbot.model_runbot_config_step_upgrade_db,group_user,1,0,0,0
|
||||
access_runbot_config_step_upgrade_db_manager,runbot_config_step_upgrade_db_manager,runbot.model_runbot_config_step_upgrade_db,runbot.group_build_config_user,1,1,1,1
|
||||
|
||||
access_runbot_build_error_user,runbot_build_error_user,runbot.model_runbot_build_error,group_user,1,0,0,0
|
||||
access_runbot_build_error_manager,runbot_build_error_manager,runbot.model_runbot_build_error,runbot.group_runbot_admin,1,1,1,1
|
||||
access_runbot_build_error_tag_user,runbot_build_error_tag_user,runbot.model_runbot_build_error_tag,group_user,1,0,0,0
|
||||
@ -33,7 +34,7 @@ access_runbot_error_log_user,runbot_error_log_user,runbot.model_runbot_error_log
|
||||
access_runbot_error_log_manager,runbot_error_log_manager,runbot.model_runbot_error_log,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_repo_hooktime,runbot_repo_hooktime,runbot.model_runbot_repo_hooktime,group_user,1,0,0,0
|
||||
access_runbot_repo_reftime,runbot_repo_reftime,runbot.model_runbot_repo_reftime,group_user,1,0,0,0
|
||||
access_runbot_repo_referencetime,runbot_repo_referencetime,runbot.model_runbot_repo_reftime,group_user,1,0,0,0
|
||||
|
||||
access_runbot_build_stat_user,runbot_build_stat_user,runbot.model_runbot_build_stat,group_user,1,0,0,0
|
||||
access_runbot_build_stat_admin,runbot_build_stat_admin,runbot.model_runbot_build_stat,runbot.group_runbot_admin,1,1,1,1
|
||||
@ -41,5 +42,62 @@ access_runbot_build_stat_admin,runbot_build_stat_admin,runbot.model_runbot_build
|
||||
access_runbot_build_stat_sql_user,runbot_build_stat_sql_user,runbot.model_runbot_build_stat_sql,group_user,1,0,0,0
|
||||
access_runbot_build_stat_sql_admin,runbot_build_stat_sql_admin,runbot.model_runbot_build_stat_sql,runbot.group_runbot_admin,1,0,0,0
|
||||
|
||||
access_runbot_build_stat_regex_user,access_runbot_build_stat_regex_user,model_runbot_build_stat_regex,runbot.group_user,1,0,0,0
|
||||
access_runbot_build_stat_regex_admin,access_runbot_build_stat_regex_admin,model_runbot_build_stat_regex,runbot.group_runbot_admin,1,1,1,1
|
||||
access_runbot_build_stat_regex_user,access_runbot_build_stat_regex_user,runbot.model_runbot_build_stat_regex,runbot.group_user,1,0,0,0
|
||||
access_runbot_build_stat_regex_admin,access_runbot_build_stat_regex_admin,runbot.model_runbot_build_stat_regex,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
|
||||
access_runbot_trigger_user,access_runbot_trigger_user,runbot.model_runbot_trigger,runbot.group_user,1,0,0,0
|
||||
access_runbot_trigger_runbot_admin,access_runbot_trigger_runbot_admin,runbot.model_runbot_trigger,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_repo_user,access_runbot_repo_user,runbot.model_runbot_repo,runbot.group_user,1,0,0,0
|
||||
access_runbot_repo_runbot_admin,access_runbot_repo_runbot_admin,runbot.model_runbot_repo,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_commit_user,access_runbot_commit_user,runbot.model_runbot_commit,runbot.group_user,1,0,0,0
|
||||
|
||||
access_runbot_build_params_user,access_runbot_build_params_user,runbot.model_runbot_build_params,runbot.group_user,1,0,0,0
|
||||
access_runbot_build_params_runbot_admin,access_runbot_build_params_runbot_admin,runbot.model_runbot_build_params,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_commit_link_user,access_runbot_commit_link_user,runbot.model_runbot_commit_link,runbot.group_user,1,0,0,0
|
||||
access_runbot_commit_link_runbot_admin,access_runbot_commit_link_runbot_admin,runbot.model_runbot_commit_link,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_version_user,access_runbot_version_user,runbot.model_runbot_version,runbot.group_user,1,0,0,0
|
||||
access_runbot_version_runbot_admin,access_runbot_version_runbot_admin,runbot.model_runbot_version,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_project_user,access_runbot_project_user,runbot.model_runbot_project,runbot.group_user,1,0,0,0
|
||||
access_runbot_project_runbot_admin,access_runbot_project_runbot_admin,runbot.model_runbot_project,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_bundle_user,access_runbot_bundle_user,runbot.model_runbot_bundle,runbot.group_user,1,0,0,0
|
||||
access_runbot_bundle_runbot_admin,access_runbot_bundle_runbot_admin,runbot.model_runbot_bundle,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_batch_user,access_runbot_batch_user,runbot.model_runbot_batch,runbot.group_user,1,0,0,0
|
||||
access_runbot_batch_runbot_admin,access_runbot_batch_runbot_admin,runbot.model_runbot_batch,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_batch_slot_user,access_runbot_batch_slot_user,runbot.model_runbot_batch_slot,runbot.group_user,1,0,0,0
|
||||
access_runbot_batch_slot_runbot_admin,access_runbot_batch_slot_runbot_admin,runbot.model_runbot_batch_slot,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_ref_log_runbot_user,access_runbot_ref_log_runbot_user,runbot.model_runbot_ref_log,runbot.group_user,1,0,0,0
|
||||
access_runbot_ref_log_runbot_admin,access_runbot_ref_log_runbot_admin,runbot.model_runbot_ref_log,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_commit_status_runbot_user,access_runbot_commit_status_runbot_user,runbot.model_runbot_commit_status,runbot.group_user,1,0,0,0
|
||||
access_runbot_commit_status_runbot_admin,access_runbot_commit_status_runbot_admin,runbot.model_runbot_commit_status,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_bundle_trigger_custom_runbot_user,access_runbot_bundle_trigger_custom_runbot_user,runbot.model_runbot_bundle_trigger_custom,runbot.group_user,1,0,0,0
|
||||
access_runbot_bundle_trigger_custom_runbot_admin,access_runbot_bundle_trigger_custom_runbot_admin,runbot.model_runbot_bundle_trigger_custom,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_category_runbot_user,access_runbot_category_runbot_user,runbot.model_runbot_category,runbot.group_user,1,0,0,0
|
||||
access_runbot_category_runbot_admin,access_runbot_category_runbot_admin,runbot.model_runbot_category,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_batch_log_runbot_user,access_runbot_batch_log_runbot_user,runbot.model_runbot_batch_log,runbot.group_user,1,0,0,0
|
||||
|
||||
access_runbot_warning_user,access_runbot_warning_user,runbot.model_runbot_warning,runbot.group_user,1,0,0,0
|
||||
access_runbot_warning_admin,access_runbot_warning_admin,runbot.model_runbot_warning,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_database_user,access_runbot_database_user,runbot.model_runbot_database,runbot.group_user,1,0,0,0
|
||||
access_runbot_database_admin,access_runbot_database_admin,runbot.model_runbot_database,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_upgrade_regex_user,access_runbot_upgrade_regex_user,runbot.model_runbot_upgrade_regex,runbot.group_user,1,0,0,0
|
||||
access_runbot_upgrade_regex_admin,access_runbot_upgrade_regex_admin,runbot.model_runbot_upgrade_regex,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
access_runbot_upgrade_exception_user,access_runbot_upgrade_exception_user,runbot.model_runbot_upgrade_exception,runbot.group_user,1,0,0,0
|
||||
access_runbot_upgrade_exception_admin,access_runbot_upgrade_exception_admin,runbot.model_runbot_upgrade_exception,runbot.group_runbot_admin,1,1,1,1
|
||||
|
||||
|
|
@ -1,7 +1,14 @@
|
||||
id,name,model_id/id,groups/id,domain_force,perm_read,perm_create,perm_write,perm_unlink
|
||||
rule_repo,"limited to groups",model_runbot_repo,group_user,"['|', ('group_ids', '=', False), ('group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
|
||||
|
||||
rule_project,"limited to groups",model_runbot_project,group_user,"['|', ('group_ids', '=', False), ('group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_project_mgmt,"manager can see all",model_runbot_project,group_runbot_admin,"[(1, '=', 1)]",1,1,1,1
|
||||
|
||||
rule_repo,"limited to groups",model_runbot_repo,group_user,"['|', ('project_id.group_ids', '=', False), ('project_id.group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_repo_mgmt,"manager can see all",model_runbot_repo,group_runbot_admin,"[(1, '=', 1)]",1,1,1,1
|
||||
rule_branch,"limited to groups",model_runbot_branch,group_user,"['|', ('repo_id.group_ids', '=', False), ('repo_id.group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_branch,"limited to groups",model_runbot_branch,group_user,"['|', ('remote_id.repo_id.project_id.group_ids', '=', False), ('remote_id.repo_id.project_id.group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_branch_mgmt,"manager can see all",model_runbot_branch,group_runbot_admin,"[(1, '=', 1)]",1,1,1,1
|
||||
rule_build,"limited to groups",model_runbot_build,group_user,"['|', ('repo_id.group_ids', '=', False), ('repo_id.group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_commit,"limited to groups",model_runbot_commit,group_user,"['|', ('repo_id.project_id.group_ids', '=', False), ('repo_id.project_id.group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_commit_mgmt,"manager can see all",model_runbot_commit,group_runbot_admin,"[(1, '=', 1)]",1,1,1,1
|
||||
rule_build,"limited to groups",model_runbot_build,group_user,"['|', ('params_id.project_id.group_ids', '=', False), ('params_id.project_id.group_ids', 'in', [g.id for g in user.groups_id])]",1,1,1,1
|
||||
rule_build_mgmt,"manager can see all",model_runbot_build,group_runbot_admin,"[(1, '=', 1)]",1,1,1,1
|
||||
|
|
@ -1,13 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.module.category" id="module_category">
|
||||
<record model="ir.module.category" id="module_project">
|
||||
<field name="name">Runbot</field>
|
||||
</record>
|
||||
|
||||
<record id="group_user" model="res.groups">
|
||||
<field name="name">User</field>
|
||||
<field name="category_id" ref="module_category"/>
|
||||
<field name="category_id" ref="module_project"/>
|
||||
<!-- as public user is inactive, it wont be automatically added
|
||||
to this group via implied groups. add it manually -->
|
||||
<field name="users" eval="[(4, ref('base.public_user'))]"/>
|
||||
@ -17,39 +17,43 @@
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.group_user" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.group_portal" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_runbot_admin" model="res.groups">
|
||||
<field name="name">Manager</field>
|
||||
<field name="category_id" ref="module_category"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.module.category" id="build_config_category">
|
||||
<record model="ir.module.category" id="build_config_project">
|
||||
<field name="name">Build Config</field>
|
||||
</record>
|
||||
|
||||
<record id="group_build_config_user" model="res.groups">
|
||||
<field name="name">Build config user</field>
|
||||
<field name="category_id" ref="build_config_category"/>
|
||||
<field name="category_id" ref="build_config_project"/>
|
||||
</record>
|
||||
|
||||
<record id="group_build_config_manager" model="res.groups">
|
||||
<field name="name">Build config manager</field>
|
||||
<field name="category_id" ref="build_config_category"/>
|
||||
<field name="category_id" ref="build_config_project"/>
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_build_config_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_build_config_administrator" model="res.groups">
|
||||
<field name="name">Build config administrator</field>
|
||||
<field name="category_id" ref="build_config_category"/>
|
||||
<field name="category_id" ref="build_config_project"/>
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_build_config_manager'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_runbot_admin" model="res.groups">
|
||||
<field name="name">Runbot administrator</field>
|
||||
<field name="category_id" ref="module_project"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
<field name="implied_ids" eval="[(4, ref('runbot.group_user')), (4, ref('runbot.group_build_config_administrator'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- config access rules-->
|
||||
<record id="runbot_build_config_access_administrator" model="ir.rule">
|
||||
<field name="name">All config can be edited by config admin</field>
|
||||
|
@ -1,87 +0,0 @@
|
||||
.separator {
|
||||
border-top: 2px solid #666;
|
||||
}
|
||||
|
||||
[data-toggle="collapse"] .fa:before {
|
||||
content: "\f139";
|
||||
}
|
||||
|
||||
[data-toggle="collapse"].collapsed .fa:before {
|
||||
content: "\f13a";
|
||||
}
|
||||
|
||||
body, .table{
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color:#444;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #fff;
|
||||
color: #444;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
background-color: #ccc;
|
||||
color: #444;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-sm, .btn-group-sm > .btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.89rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
.btn-ssm, .btn-group-ssm > .btn {
|
||||
padding: 0.22rem 0.4rem;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.killed, .bg-killed, .bg-killed-light {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.dropdown-toggle:after { content: none }
|
||||
|
||||
.branch_name {
|
||||
max-width: 250px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.branch_time {
|
||||
float:right;
|
||||
margin-left:10px;
|
||||
}
|
||||
|
||||
.bg-success-light {
|
||||
background-color: #dff0d8;
|
||||
}
|
||||
.bg-danger-light {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
.bg-info-light {
|
||||
background-color: #d9edf7;
|
||||
}
|
||||
|
||||
.text-info{
|
||||
color: #096b72 !important;
|
||||
}
|
||||
.build_subject_buttons {
|
||||
display: flex;
|
||||
}
|
||||
.build_buttons {
|
||||
margin-left: auto
|
||||
}
|
||||
|
||||
.bg-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.label-killed {
|
||||
background-color: #aaa;
|
||||
}
|
202
runbot/static/src/css/runbot.scss
Normal file
202
runbot/static/src/css/runbot.scss
Normal file
@ -0,0 +1,202 @@
|
||||
.separator {
|
||||
border-top: 2px solid #666;
|
||||
}
|
||||
|
||||
[data-toggle="collapse"] .fa:before {
|
||||
content: "\f139";
|
||||
}
|
||||
|
||||
[data-toggle="collapse"].collapsed .fa:before {
|
||||
content: "\f13a";
|
||||
}
|
||||
|
||||
body, .table{
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color:#444;
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
background-color: #fff;
|
||||
color: #444;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
background-color: #ccc;
|
||||
color: #444;
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
.btn-sm, .btn-group-sm > .btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.89rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
.btn-ssm, .btn-group-ssm > .btn {
|
||||
padding: 0.22rem 0.4rem;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.killed, .bg-killed, .bg-killed-light {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.dropdown-toggle:after { content: none }
|
||||
|
||||
.one_line {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.batch_tile {
|
||||
padding: 6px;
|
||||
}
|
||||
.branch_time {
|
||||
float:right;
|
||||
margin-left:10px;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--info-light: #d9edf7;
|
||||
}
|
||||
|
||||
|
||||
.bg-success-light {
|
||||
background-color: #dff0d8;
|
||||
}
|
||||
.bg-danger-light {
|
||||
background-color: #f2dede;
|
||||
}
|
||||
.bg-info-light {
|
||||
background-color: var(--info-light);
|
||||
}
|
||||
|
||||
.text-info{
|
||||
color: #096b72 !important;
|
||||
}
|
||||
.build_subject_buttons {
|
||||
display: flex;
|
||||
}
|
||||
.build_buttons {
|
||||
margin-left: auto
|
||||
}
|
||||
|
||||
.bg-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.badge-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.table-condensed td {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.badge-light{
|
||||
border: 1px solid #AAA;
|
||||
}
|
||||
.arrow{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge-light:hover .arrow{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.slot_button_group {
|
||||
display: flex;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.slot_button_group .btn {
|
||||
flex: 0 0 25px;
|
||||
}
|
||||
|
||||
.slot_button_group .btn.slot_name {
|
||||
width: 40px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.batch_header {
|
||||
padding: 6px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.batch_slots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.batch_commits {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.batch_commits {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.match_type_new {
|
||||
background-color: var(--info-light);
|
||||
}
|
||||
.batch_row {
|
||||
.slot_container{
|
||||
flex: 1 0 200px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.slot_filler {
|
||||
width: 100px;
|
||||
height: 0px;
|
||||
flex: 1 0 200px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
.bundle_row {
|
||||
border-bottom: 1px solid var(--gray);
|
||||
.batch_commits {
|
||||
font-size: 80%;
|
||||
}
|
||||
.slot_container{
|
||||
flex:1 0 50%;
|
||||
}
|
||||
.slot_filler {
|
||||
flex:1 0 50%;
|
||||
}
|
||||
.more {
|
||||
.batch_commits {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.nomore {
|
||||
.batch_commits {
|
||||
display: none;
|
||||
padding:8px;
|
||||
}
|
||||
}
|
||||
.nomore.batch_tile:hover {
|
||||
.batch_commits {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
transform: translateY(100%);
|
||||
z-index: 100;
|
||||
border: 1px solid rgba(0, 0, 0, 0.125);
|
||||
border-radius: 0.2rem;
|
||||
box-sizing: border-box;
|
||||
margin-left:-1px;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,7 @@
|
||||
"use strict";
|
||||
|
||||
var OPMAP = {
|
||||
'rebuild': {operation: 'force', then: 'redirect'},
|
||||
'rebuild-exact': {operation: 'force/1', then: 'redirect'},
|
||||
'rebuild': {operation: 'rebuild', then: 'redirect'},
|
||||
'kill': {operation: 'kill', then: 'reload'},
|
||||
'wakeup': {operation: 'wakeup', then: 'reload'}
|
||||
};
|
||||
|
@ -2,7 +2,7 @@
|
||||
<data>
|
||||
<template id="assets_frontend" inherit_id="website.assets_frontend" name="runbot.assets.frontend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/octicons/2.0.2/octicons.css"/>
|
||||
<link rel="stylesheet" href="/runbot/static/src/css/runbot.scss"/>
|
||||
<script type="text/javascript" src="/runbot/static/src/js/runbot.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
147
runbot/templates/batch.xml
Normal file
147
runbot/templates/batch.xml
Normal file
@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.batch">
|
||||
<t t-call='website.layout'>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-stripped">
|
||||
<tr>
|
||||
<td>Bundle</td>
|
||||
<td>
|
||||
<a t-esc="batch.bundle_id.name" t-attf-href="/runbot/bundle/{{batch.bundle_id.id}}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="batch.category_id.id != default_category">
|
||||
<td>Category</td>
|
||||
<td t-esc="batch.category_id.name">
|
||||
<i t-attf-class="fa fa-{{batch.category_id.name}}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td t-esc="batch.slot_ids[0].params_id.version_id.name if batch.slot_ids else batch.bundle_id.version_id.name"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>State</td>
|
||||
<td t-esc="batch.state"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create date</td>
|
||||
<td t-esc="batch.create_date"/>
|
||||
</tr>
|
||||
<tr t-if="more">
|
||||
<td>Last update</td>
|
||||
<td>
|
||||
<t t-esc="batch.last_update"/>
|
||||
<span class="badge badge-info" t-esc="s2human(batch.last_update - batch.create_date)"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-att-class="'bg-info-light' if batch.state=='preparing' else 'bg-success-light' if not any(log.level != 'INFO' for log in batch.log_ids) else 'bg-warning-light'">
|
||||
<td>Commits</td>
|
||||
<td>
|
||||
<div t-foreach="batch.commit_link_ids.sorted(key=lambda lc: (lc.commit_id.repo_id.sequence, lc.commit_id.repo_id.id))" t-as="commit_link">
|
||||
<t t-set="commit" t-value="commit_link.commit_id"/>
|
||||
<span/>
|
||||
<a t-attf-href="/runbot/commit/#{commit.id}">
|
||||
<i class="fa fa-fw fa-hashtag" t-if="commit_link.match_type == 'new'" title="This commit is a new head"/>
|
||||
<i class="fa fa-fw fa-link" t-if="commit_link.match_type == 'head'" title="This commit is an existing head from bundle branches"/>
|
||||
<i class="fa fa-fw fa-code-fork" t-if="commit_link.match_type == 'base_match'" title="This commit is matched from a base batch with matching merge_base"/>
|
||||
<i class="fa fa-fw fa-clock-o" t-if="commit_link.match_type == 'base_head'" title="This commit is the head of a base branch"/>
|
||||
<span class="label" t-esc="commit.dname"/>
|
||||
</a>
|
||||
<a t-att-href="'https://%s/commit/%s' % (commit_link.branch_id.remote_id.base_url, commit_link.commit_id.name)" class="badge badge-light" title="View Commit on Github"><i class="fa fa-github"/></a>
|
||||
<small t-if="commit_link.match_type and commit_link.match_type.startswith('base')">
|
||||
from base:
|
||||
<span t-esc="commit_link.branch_id.name"/>
|
||||
<br/>
|
||||
</small>
|
||||
<small t-else="">
|
||||
found in branch
|
||||
<span t-esc="commit_link.branch_id.name"/>
|
||||
<t t-if="batch.state != 'preparing'">
|
||||
<span t-esc="'+%s' % commit_link.diff_add" class="text-success"/>
|
||||
<span t-esc="'-%s' % commit_link.diff_remove" class="text-danger"/>
|
||||
<span class="text-info">
|
||||
(
|
||||
<span t-esc="commit_link.file_changed"/>
|
||||
<i class="fa fa-file"/>
|
||||
)
|
||||
</span>
|
||||
</t>
|
||||
<br/>
|
||||
<t t-if="more">
|
||||
Base head:
|
||||
<span t-esc="commit_link.base_commit_id.name"/>
|
||||
<br/>
|
||||
Merge base:
|
||||
<span t-esc="commit_link.merge_base_commit_id.name"/>
|
||||
(
|
||||
<span t-esc="'%s ahead' % commit_link.base_ahead" class="text-success"/>
|
||||
,
|
||||
<span t-esc="'%s behind' % commit_link.base_behind" class="text-danger"/>
|
||||
)
|
||||
<br/>
|
||||
</t>
|
||||
</small>
|
||||
<b t-if="commit.rebase_on_id">Automatic rebase on <t t-esc="commit.rebase_on_id.name"/><br/></b>
|
||||
<t t-if="more or not (commit_link.match_type and commit_link.match_type.startswith('base'))">
|
||||
Subject:
|
||||
<span t-esc="commit.subject"/>
|
||||
<br/>
|
||||
Author:
|
||||
<span t-esc="commit.author"/>
|
||||
(
|
||||
<span t-esc="commit.author_email"/>
|
||||
)
|
||||
<br/>
|
||||
<t t-if="commit.author != commit.committer">
|
||||
Committer:
|
||||
<span t-esc="commit.committer"/>
|
||||
(
|
||||
<span t-esc="commit.committer_email"/>
|
||||
)
|
||||
<br/>
|
||||
</t>
|
||||
Commit date:
|
||||
<span t-esc="commit.date"/>
|
||||
<br/>
|
||||
</t>
|
||||
<hr/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Builds</td>
|
||||
<td>
|
||||
<t t-foreach="batch.slot_ids" t-as="slot">
|
||||
<t t-call="runbot.slot_button"/>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="more">
|
||||
<td>Old builds</td>
|
||||
<td>
|
||||
<t t-foreach="batch.with_context(active_test=False).slot_ids.filtered(lambda s: not s.active)" t-as="slot">
|
||||
<s>
|
||||
<t t-call="runbot.slot_button"/>
|
||||
</s>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<t t-foreach="batch.log_ids" t-as="log">
|
||||
<t t-set="logclass" t-value="dict(ERROR='danger', WARNING='warning', INFO='info').get(log.level, 'warning')"/>
|
||||
<div t-attf-class="alert alert-{{logclass}}">
|
||||
<b t-esc="log.level"/>
|
||||
--
|
||||
<t t-foreach="log._markdown().split('\n')" t-as="line">
|
||||
<span t-esc="line"/>
|
||||
<br t-if="not line_last"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
@ -7,49 +7,71 @@
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<div class="navbar navbar-default">
|
||||
<span class="text-center" style="font-size: 18px;">Builds for branch: <span id="branchclp"><t t-esc="builds[0].branch_id.branch_name" /></span>
|
||||
<a href="#" class="clipbtn octicon octicon-clippy" data-clipboard-target="#branchclp" title="Copy branch name to clipboard"/><br/>
|
||||
</span>
|
||||
<span class="pull-right"><t t-call="website.pager" /></span>
|
||||
<h3>
|
||||
<span class="text-muted"><t t-esc="branch.remote_id.short_name"/>:</span><t t-esc="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/>
|
||||
<div class="btn-group" role="group">
|
||||
<a t-att-href="branch.branch_url" class="btn btn-sm text-left" title="View Branch on Github"><i class="fa fa-github"/></a>
|
||||
<a groups="runbot.group_runbot_admin" class="btn btn-sm fa fa-list text-left" t-attf-href="/web/#id={{branch.id}}&view_type=form&model=runbot.branch" target="new" title="View Branch in Backend"/>
|
||||
</div>
|
||||
<table class="table table-condensed table-stripped" style="table-layout: initial;">
|
||||
<thead>
|
||||
</h3>
|
||||
</div>
|
||||
<table class="table table-condensed table-responsive table-stripped">
|
||||
<tr>
|
||||
<th>Create date</th>
|
||||
<th>Dest</th>
|
||||
<th>Subject</th>
|
||||
<th>result</th>
|
||||
<th>state</th>
|
||||
<th>host</th>
|
||||
<th>Build duration</th>
|
||||
<th>type</th>
|
||||
<td>Remote:</td>
|
||||
<td t-esc="branch.remote_id.name"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="builds" t-as="build">
|
||||
<t t-if="build.global_state in ['running','done']">
|
||||
<t t-if="build.global_result == 'ko'"><t t-set="rowclass">danger</t></t>
|
||||
<t t-if="build.global_result == 'warn'"><t t-set="rowclass">warning</t></t>
|
||||
<t t-if="build.global_result == 'ok'"><t t-set="rowclass">success</t></t>
|
||||
<t t-if="build.global_result == 'skipped'"><t t-set="rowclass">default</t></t>
|
||||
<t t-if="build.global_result in ['killed', 'manually_killed']"><t t-set="rowclass">killed</t></t>
|
||||
</t>
|
||||
<tr t-attf-class="bg-{{rowclass}}-light">
|
||||
<td><t t-esc="build.create_date" /></td>
|
||||
<td><a t-attf-href="/runbot/build/{{build['id']}}" title="Build details" aria-label="Build details"><t t-esc="build.dest" /></a></td>
|
||||
<tr>
|
||||
<td>Head:</td>
|
||||
<td t-esc="branch.head_name"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bundle:</td>
|
||||
<td>
|
||||
<t t-if="build.config_id != build.branch_id.config_id">
|
||||
<b t-esc="build.config_id.name"/>
|
||||
</t>
|
||||
<t t-esc="build.subject" />
|
||||
<small>
|
||||
<div class="btn-toolbar mb-1" role="toolbar">
|
||||
<div class="btn-group btn-group-ssm w-100" role="group">
|
||||
<a t-attf-href="/runbot/bundle/{{branch.bundle_id.id}}" t-esc="branch.bundle_id.name" class="btn btn-default text-left" title="View Bundle Details"/>
|
||||
</div>
|
||||
</div>
|
||||
</small>
|
||||
</td>
|
||||
<td><t t-esc="build.global_result" /></td>
|
||||
<td><t t-esc="build.global_state" /></td>
|
||||
<td><t t-esc="build.real_build.host" /></td>
|
||||
<td><t t-esc="build.build_time" /></td>
|
||||
<td><t t-esc="build.build_type" /></td>
|
||||
</tr>
|
||||
<t t-if="branch.is_pr">
|
||||
<tr t-if="pr_branch">
|
||||
<td>Pull Head Name</td>
|
||||
<td><a t-attf-href="/runbot/branch/{{pr_branch.id}}" t-esc="branch.pull_head_name" title="View PR Details"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Target Branch</td>
|
||||
<td t-esc="branch.target_branch_name"></td>
|
||||
</tr>
|
||||
</t>
|
||||
<t t-elif="branch_pr">
|
||||
<tr>
|
||||
<td>Pull Request:</td>
|
||||
<td><a t-attf-href="/runbot/branch/{{branch_pr.id}}" t-esc="branch_pr.name" title="View Branch Details"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
<table t-if="branch.reflog_ids" class="table table-condensed table-stripped" style="table-layout: initial;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ref Date</th>
|
||||
<th>SHA</th>
|
||||
<th>Commit Date</th>
|
||||
<th>Author</th>
|
||||
<th>Subject</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr t-foreach='branch.reflog_ids' t-as='reflog'>
|
||||
<td t-esc="reflog.date"/>
|
||||
<td><a t-attf-href="/runbot/commit/{{reflog.commit_id.id}}" t-esc="reflog.commit_id.name"/></td>
|
||||
<td t-esc="reflog.commit_id.date"/>
|
||||
<td t-esc="reflog.commit_id.author"/>
|
||||
<td t-esc="reflog.commit_id.subject"/>
|
||||
</tr>
|
||||
</table>
|
||||
<h4 t-else="">No Reflogs Found</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,101 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.build_name">
|
||||
<t t-if="bu.real_build.requested_action=='deathrow'"><i class="text-info fa fa-crosshairs"/> killing</t>
|
||||
<t t-if="bu.real_build.requested_action=='wake_up'"><i class="text-info fa fa-coffee"/> waking up</t>
|
||||
<t t-if="not bu.requested_action">
|
||||
<t t-if="bu.global_state=='pending'"><i class="text-default fa fa-pause"/> pending</t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')">
|
||||
<t t-set="textklass" t-value="dict(ko='danger', warn='warning').get(bu.global_result, 'info')"/>
|
||||
<t t-if="bu.global_state == 'waiting'">
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small t-if="not hide_time">time: <t t-esc="bu.get_formated_build_time()"/></small>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-attf-class="text-{{textklass}}"><i class="fa fa-spinner fa-spin"/> <t t-esc="bu.global_state"/></span> <small> step <t t-esc="bu['job']"/>: </small><small t-if="not hide_time"><t t-esc="bu.get_formated_job_time()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="bu.global_state in ('running', 'done')">
|
||||
<t t-if="bu.global_result=='ok'"><i class="text-success fa fa-thumbs-up" title="Success" aria-label="Success"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='ko'"><i class="text-danger fa fa-thumbs-down" title="Failed" aria-label="Failed"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='warn'"><i class="text-warning fa fa-warning" title="Some warnings" aria-label="Some warnings"/><small t-if="not hide_time"> age: <t t-esc="bu.get_formated_build_age()"/> -- time: <t t-esc="bu.get_formated_build_time()"/></small></t>
|
||||
<t t-if="bu.global_result=='skipped'"><i class="text-danger fa fa-ban"/> skipped</t>
|
||||
<t t-if="bu.global_result=='killed'"><i class="text-danger fa fa-times"/> killed</t>
|
||||
<t t-if="bu.global_result=='manually_killed'"><i class="text-danger fa fa-times"/> manually killed</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template id="runbot.build_button">
|
||||
<div t-attf-class="pull-right">
|
||||
<div t-attf-class="btn-group {{klass}}">
|
||||
<a t-if="bu.real_build.local_state=='running'" t-attf-href="http://{{bu['domain']}}/?db={{bu.real_build.dest}}-all" class="btn btn-primary" title="Sign in on this build" aria-label="Sign in on this build"><i class="fa fa-sign-in"/></a>
|
||||
<a t-if="bu.real_build.local_state=='done' and bu.real_build.requested_action != 'wake_up'" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu.real_build.id" class="btn btn-default" title="Wake up this build" aria-label="Wake up this build"><i class="fa fa-coffee"/></a>
|
||||
<a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default" title="Build details" aria-label="Build details"><i class="fa fa-file-text-o"/></a>
|
||||
<a t-if="show_commit_button" t-attf-href="https://#{repo.base}/commit/#{bu['name']}" class="btn btn-default" title="Open commit on GitHub" aria-label="Open commit on GitHub"><i class="fa fa-github"/></a>
|
||||
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build options" aria-label="Build options" aria-expanded="false"><i class="fa fa-cog"/><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<li t-if="bu.global_result=='skipped'" groups="runbot.group_runbot_admin">
|
||||
<a href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']">Force Build <i class="fa fa-level-up"></i></a>
|
||||
</li>
|
||||
<t t-if="bu.real_build.local_state=='running'">
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_build'].dest}}-all">Connect all <i class="fa fa-sign-in"></i></a></li>
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/?db={{bu['real_build'].dest}}-base">Connect base <i class="fa fa-sign-in"></i></a></li>
|
||||
<li><a t-attf-href="http://{{bu['domain']}}/">Connect <i class="fa fa-sign-in"></i></a></li>
|
||||
</t>
|
||||
<li t-if="bu.global_state in ['done','running'] or requested_action == 'deathrow'" groups="base.group_user">
|
||||
<t t-if="show_rebuild_button">
|
||||
<a href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']"
|
||||
title="Create a new build keeping build commit head, but will recompute all other info (config, dependencies, extra_params)">
|
||||
Rebuild <i class="fa fa-refresh"/>
|
||||
</a>
|
||||
</t>
|
||||
<a href="#" data-runbot="rebuild-exact" t-att-data-runbot-build="bu['id']"
|
||||
title="Create a new build keeping all build info (config, dependencies, extra_params)">
|
||||
Exact Rebuild <i class="fa fa-refresh"/>
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="bu.global_state != 'done'" groups="base.group_user">
|
||||
<a t-if="bu.real_build.requested_action != 'deathrow'" href="#" data-runbot="kill" t-att-data-runbot-build="bu['id']">Kill <i class="fa fa-crosshairs"/></a>
|
||||
<span t-else="" data-runbot="kill" > Killing <i class="fa fa-spinner fa-spin"/> <i class="fa fa-crosshairs"/></span>
|
||||
</li>
|
||||
<li t-if="bu.global_state == 'done'" groups="base.group_user">
|
||||
<a t-if="bu.real_build.requested_action != 'wake_up'" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu['id']">Wake up <i class="fa fa-coffee"/></a>
|
||||
<span t-else="" data-runbot="wakeup" > Waking up <i class="fa fa-spinner fa-spin"/> <i class="fa fa-crosshairs"/></span>
|
||||
</li>
|
||||
|
||||
<li t-if="bu.global_state not in ('testing', 'waiting', 'pending')" class="divider"></li>
|
||||
<li><a t-attf-href="/runbot/build/{{bu['id']}}">Logs <i class="fa fa-file-text-o"/></a></li>
|
||||
<t t-set="log_url" t-value="'http://%s' % bu.real_build.host if bu.real_build.host != fqdn else ''"/>
|
||||
<t t-if="bu.real_build.host" t-foreach="(bu.log_list or '').split(',')" t-as="log_name" >
|
||||
<li><a t-attf-href="{{log_url}}/runbot/static/build/#{bu['real_build'].dest}/logs/#{log_name}.txt">Full <t t-esc="log_name"/> logs <i class="fa fa-file-text-o"/></a></li>
|
||||
</t>
|
||||
<li t-if="bu.coverage and bu.real_build.host"><a t-attf-href="http://{{bu.real_build.host}}/runbot/static/build/#{bu['real_build'].dest}/coverage/index.html">Coverage <i class="fa fa-file-text-o"/></a></li>
|
||||
<li t-if="bu.global_state!='pending'" class="divider"></li>
|
||||
<li><a t-attf-href="{{br['branch'].branch_url}}"><t t-esc="'Branch ' if not br['branch'].pull_head_name else 'Pull '"/><i class="fa fa-github"/></a></li>
|
||||
<li><a t-attf-href="https://{{repo.base}}/commit/{{bu['name']}}">Commit <i class="fa fa-github"/></a></li>
|
||||
<li><a t-attf-href="https://{{repo.base}}/compare/{{br['branch'].branch_name}}">Compare <i class="fa fa-github"/></a></li>
|
||||
<!-- TODO branch.pull from -->
|
||||
<li class="divider"></li>
|
||||
<li groups="runbot.group_runbot_admin"><a t-attf-href="/web/#id={{bu['id']}}&view_type=form&model=runbot.build" target="new">View in backend</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Event / Logs page -->
|
||||
<template id="runbot.build_class">
|
||||
<t t-set="rowclass">info</t>
|
||||
<t t-if="build.global_state in ['running','done']">
|
||||
<t t-if="build.global_result == 'ok'"><t t-set="rowclass">success</t></t>
|
||||
<t t-if="build.global_result == 'skipped'"><t t-set="rowclass">default</t></t>
|
||||
<t t-if="build.global_result in ['killed', 'manually_killed']"><t t-set="rowclass">killed</t></t>
|
||||
</t>
|
||||
<t t-if="build.global_result == 'ko'"><t t-set="rowclass">danger</t></t>
|
||||
<t t-if="build.global_result == 'warn'"><t t-set="rowclass">warning</t></t>
|
||||
<t t-esc="rowclass"/>
|
||||
</template>
|
||||
<template id="runbot.build">
|
||||
<t t-call='website.layout'>
|
||||
|
||||
<t t-set="nav_form">
|
||||
<form class="form-inline">
|
||||
<div class="btn-group">
|
||||
@ -107,71 +14,172 @@
|
||||
</div>
|
||||
</form>
|
||||
<form class="form-inline" t-attf-action="/runbot/build/#{build['id']}/force" method='POST' t-if="request.params.get('ask_rebuild')" groups="runbot.group_user">
|
||||
<a href='#' class="btn btn-danger" data-runbot="rebuild" t-attf-data-runbot-build="#{build['id']}" > <i class='fa fa-refresh'/> Force Rebuild</a>
|
||||
<a href='#' class="btn btn-danger" data-runbot="rebuild" t-attf-data-runbot-build="#{build['id']}">
|
||||
<i class='fa fa-refresh'/>
|
||||
Force Rebuild
|
||||
</a>
|
||||
</form>
|
||||
</t>
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<div class="col-md-12">
|
||||
<t t-set="batches" t-value="build.slot_ids.mapped('batch_id')"/>
|
||||
<t t-set="bundles" t-value="batches.mapped('bundle_id')"/>
|
||||
<t t-if="batches">
|
||||
<t t-if="len(bundles) == 1">
|
||||
<t t-if="len(batches) == 1">
|
||||
<b>Batch:</b>
|
||||
<a t-esc="bundles.name" t-attf-href="/runbot/batch/{{batches[0].id}}"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<b>Bundle:</b>
|
||||
<t t-esc="bundles.name" t-attf-href="/runbot/bundle/{{bundle.id}}"/>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
This build is referenced in
|
||||
<t t-esc="len(bundles)"/>
|
||||
bundles
|
||||
<t t-if="more">
|
||||
:
|
||||
<a t-foreach="bundles" class="badge badge-light" t-as="bundle" t-esc="bundle.name" t-attf-href="/runbot/bundle/{{bundle.id}}"/>
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-if="len(batches) > 1">
|
||||
First apparition:
|
||||
<a t-esc="batches[0].bundle_id.name" t-attf-href="/runbot/batch/{{batches[0].id}}"/>
|
||||
<br/>
|
||||
Last apparition:
|
||||
<a t-esc="batches[-1].bundle_id.name" t-attf-href="/runbot/batch/{{batches[-1].id}}"/>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<table class="table table-condensed tabel-bordered">
|
||||
<tr>
|
||||
<t t-set="rowclass"><t t-call="runbot.build_class"><t t-set="build" t-value="build"/></t></t>
|
||||
<t t-set="rowclass">
|
||||
<t t-call="runbot.build_class">
|
||||
<t t-set="build" t-value="build"/>
|
||||
</t>
|
||||
</t>
|
||||
<td t-attf-class="bg-{{rowclass.strip()}}-light">
|
||||
<t t-if="build.description">
|
||||
<b>Description:</b> <t t-raw="build.md_description"/><br/>
|
||||
</t>
|
||||
<b>Subject:</b> <t t-esc="build.subject"/><br/>
|
||||
<b>Author:</b> <t t-esc="build.author"/><br/>
|
||||
<b>Committer:</b> <t t-esc="build.committer"/><br/>
|
||||
<b>Commit:</b> <a title="Go to github commit page" t-attf-href="https://{{build.repo_id.base}}/commit/{{build.name}}"><t t-esc="build.name"/></a><br/>
|
||||
<t t-foreach="build.sudo().dependency_ids" t-as="dep">
|
||||
<b>Dep:</b> <t t-esc="dep.dependecy_repo_id.short_name"/>:<a t-attf-href="https://{{dep.dependecy_repo_id.base}}/commit/{{dep.dependency_hash}}"><t t-esc="dep.dependency_hash"/></a>
|
||||
<t t-if='dep.closest_branch_id'> from branch <t t-esc="dep.closest_branch_id.name"/></t>
|
||||
<b>Description:</b>
|
||||
<t t-raw="build.md_description"/>
|
||||
<br/>
|
||||
</t>
|
||||
<b>Branch:</b> <span id="branchclp"><a title="Go to branch build list" t-attf-href="/runbot/branch/{{build.branch_id.id}}" t-esc="build.branch_id.branch_name"/></span>
|
||||
<t t-if="build.branch_id.pull_head_name" t-esc="'(%s)' % build.branch_id.pull_branch_name"/>
|
||||
<!--<a href="#" class="clipbtn octicon octicon-clippy" data-clipboard-target="#branchclp" title="Copy branch name to clipboard"/>--><br/>
|
||||
<b>Host:</b> <t t-esc="build.real_build.host"/><br/>
|
||||
<b>Dest:</b> <t t-esc="build.dest"/><br/>
|
||||
<b>Total time:</b> <t t-esc="build.get_formated_build_time()"/><br/>
|
||||
|
||||
<t t-foreach="build.params_id.sudo().commit_link_ids" t-as="build_commit">
|
||||
<b>Commit:</b>
|
||||
<a t-attf-href="/runbot/commit/{{build_commit.commit_id.id}}">
|
||||
<t t-esc="build_commit.commit_id.dname"/>
|
||||
</a>
|
||||
<a t-att-href="'https://%s/commit/%s' % (build_commit.branch_id.remote_id.base_url, build_commit.commit_id.name)" class="btn btn-sm text-left" title="View Commit on Github"><i class="fa fa-github"/></a>
|
||||
<t t-if="build_commit.match_type in ('default', 'pr_target', 'prefix') ">
|
||||
from base branch
|
||||
<br/>
|
||||
<t t-set="branch_name_builds" t-value="build.branch_id._get_last_branch_name_builds()"/>
|
||||
<t t-if="branch_name_builds">
|
||||
<b>Latest branch builds:</b>
|
||||
<t t-foreach="branch_name_builds" t-as="cbu">
|
||||
<t t-set="klass">info</t>
|
||||
|
||||
<t t-if="cbu.global_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="cbu.global_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="cbu.global_result in ('killed', 'manually_killed')"><t t-set="klass">killed</t></t>
|
||||
<t t-if="cbu.global_result == 'ok' and cbu.global_state in ('running','done')"><t t-set="klass">success</t></t>
|
||||
<a t-attf-href='/runbot/build/{{cbu.id}}'><span t-attf-class="label label-{{klass}}"><t t-esc="cbu.repo_id._get_repo_name_part()"/></span></a>
|
||||
</t><br/>
|
||||
</t>
|
||||
<div t-else="" class="ml-3">
|
||||
<b>Subject:</b>
|
||||
<t t-esc="build_commit.commit_id.subject"/>
|
||||
<br/>
|
||||
<b>Author:</b>
|
||||
<t t-esc="build_commit.commit_id.author"/>
|
||||
<br/>
|
||||
<b>Committer:</b>
|
||||
<t t-esc="build_commit.commit_id.committer"/>
|
||||
<br/>
|
||||
</div>
|
||||
</t>
|
||||
<b>Version:</b>
|
||||
<t t-esc="build.params_id.version_id.name"/>
|
||||
<br/>
|
||||
<b>Config:</b>
|
||||
<t t-esc="build.params_id.config_id.name"/>
|
||||
<br/>
|
||||
<t t-if='more'>
|
||||
<b>Trigger:</b>
|
||||
<t t-esc="build.params_id.trigger_id.name"/>
|
||||
<br/>
|
||||
<b>Config data:</b>
|
||||
<t t-esc="build.params_id.config_data.dict"/>
|
||||
<br/>
|
||||
<b>Modules:</b>
|
||||
<t t-esc="build.params_id.modules"/>
|
||||
<br/>
|
||||
<b>Extra params:</b>
|
||||
<t t-esc="build.params_id.extra_params"/>
|
||||
<br/>
|
||||
|
||||
<t t-if="len(build.params_id.builds_reference_ids) > 1">
|
||||
<b>Reference batch:</b>
|
||||
<t t-foreach="build.params_id.builds_reference_ids" t-as="reference">
|
||||
<span t-esc="reference.id"/>
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
|
||||
<t t-if="len(build.params_id.build_ids) > 1">
|
||||
<b>Similar builds:</b>
|
||||
<t t-foreach="build.params_id.build_ids" t-as="simbuild">
|
||||
<a t-if="simbuild.id != build.id" t-attf-href="/runbot/build/#{simbuild.id}">
|
||||
<span
|
||||
t-attf-class="label label-{{simbuild.get_color_class()}}"
|
||||
t-esc="simbuild.id"/>
|
||||
</a>
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
<b>Host:</b>
|
||||
<t t-esc="build.host"/>
|
||||
<br/>
|
||||
</t>
|
||||
<b>Total time:</b>
|
||||
<t t-esc="build.get_formated_build_time()"/>
|
||||
<br/>
|
||||
<b>Trigger:</b>
|
||||
<t t-esc="build.params_id.trigger_id.name"/>
|
||||
<br/>
|
||||
<br/>
|
||||
</td>
|
||||
<td t-if="build.real_build.children_ids">
|
||||
<td t-if="build.children_ids">
|
||||
Children:
|
||||
<table class="table table-condensed">
|
||||
<t t-foreach="build.real_build.children_ids.sorted('id')" t-as="child">
|
||||
<t t-set="rowclass"><t t-call="runbot.build_class"><t t-set="build" t-value="child"/></t></t>
|
||||
<tr t-attf-class="bg-{{rowclass.strip()}}-light"><td>
|
||||
<a t-attf-href="/runbot/build/{{child.id}}" >Build <t t-esc="child.id"/></a>
|
||||
<t t-foreach="build.children_ids.sorted('id')" t-as="child">
|
||||
<t t-set="rowclass">
|
||||
<t t-call="runbot.build_class">
|
||||
<t t-set="build" t-value="child"/>
|
||||
</t>
|
||||
</t>
|
||||
<tr t-attf-class="bg-{{rowclass.strip()}}-light{{' line-through' if child.orphan_result else ''}}">
|
||||
<td>
|
||||
<a t-attf-href="/runbot/build/{{child.id}}">
|
||||
Build
|
||||
<t t-esc="child.id"/>
|
||||
</a>
|
||||
<t t-if="child.description">
|
||||
<t t-raw="child.md_description" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
with config <t t-esc="child.config_id.name"/>
|
||||
with config
|
||||
<t t-esc="child.params_id.config_id.name"/>
|
||||
</t>
|
||||
<a groups="runbot.group_build_config_user" t-attf-href="/web#id={{child.params_id.config_id.id}}&view_type=form&model=runbot.build.config">...</a>
|
||||
<t t-if="child.orphan_result">
|
||||
<i class="fa fa-chain-broken" title="Build result ignored for parent" />
|
||||
</t>
|
||||
<t t-if="child.job">
|
||||
Running step:
|
||||
<t t-esc="child.job"/>
|
||||
</t>
|
||||
<a groups="runbot.group_build_config_user" t-attf-href="/web#id={{child.config_id.id}}&view_type=form&model=runbot.build.config">...</a>
|
||||
<t t-if="child.orphan_result"><i class="fa fa-chain-broken" title="Build result ignored for parent" /></t>
|
||||
<t t-if="child.job"> Running step: <t t-esc="child.job"/></t>
|
||||
<t t-if="child.global_state in ['testing', 'waiting']">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
<t t-esc="child.global_state"/>
|
||||
</t>
|
||||
</td>
|
||||
<td> <span t-attf-class="label label-info" t-esc="child.get_formated_build_time()"/>
|
||||
<td>
|
||||
<span t-attf-class="label label-info" t-esc="child.get_formated_build_time()"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-call="runbot.build_button">
|
||||
@ -179,15 +187,24 @@
|
||||
<t t-set="klass" t-value="'btn-group-ssm'"/>
|
||||
</t>
|
||||
|
||||
</td></tr>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p t-if="build.parent_id">Child of <a t-attf-href="/runbot/build/#{build.parent_id.id}"><t t-esc="build.parent_id.dest"/></a>
|
||||
<t t-if="build.orphan_result">&nbsp;<i class="fa fa-chain-broken" title="Build result ignored for parent" />&nbsp;Orphaned build, the result does not affect parent build result</t></p>
|
||||
<p t-if="build.duplicate_id">Duplicate of <a t-attf-href="/runbot/build/#{build.duplicate_id.id}"><t t-esc="build.duplicate_id.dest"/></a></p>
|
||||
<p t-if="build.parent_id">
|
||||
Child of
|
||||
<a t-attf-href="/runbot/build/#{build.parent_id.id}">
|
||||
<t t-esc="build.parent_id.dest"/>
|
||||
</a>
|
||||
<t t-if="build.orphan_result">
|
||||
&nbsp;
|
||||
<i class="fa fa-chain-broken" title="Build result ignored for parent" />
|
||||
&nbsp;Orphaned build, the result does not affect parent build result
|
||||
</t>
|
||||
</p>
|
||||
<table class="table table-condensed">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
@ -195,47 +212,87 @@
|
||||
<th>Type</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
<t t-foreach="build.real_build.sudo().log_ids" t-as="l">
|
||||
<t t-set="subbuild" t-value="(([child for child in build.real_build.children_ids if child.id == int(l.path)] if l.type == 'subbuild' else False) or [build.browse()])[0]"/>
|
||||
|
||||
<t t-set="commit_link_per_name" t-value="{commit_link.commit_id.repo_id.name:commit_link for commit_link in build.params_id.commit_link_ids}"/>
|
||||
<t t-foreach="build.sudo().log_ids" t-as="l">
|
||||
<t t-set="subbuild" t-value="(([child for child in build.children_ids if child.id == int(l.path)] if l.type == 'subbuild' else False) or [build.browse()])[0]"/>
|
||||
<t t-set="logclass" t-value="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(l.level)"/>
|
||||
<tr t-attf-class="'bg-%s-light' % {{logclass}} if {{logclass}} != 'separator' else {{logclass}}">
|
||||
<td style="white-space: nowrap; width:1%;"><t t-esc="l.create_date.strftime('%Y-%m-%d %H:%M:%S')"/></td>
|
||||
<td style="white-space: nowrap; width:1%;"><b t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-esc="l.level"/></td>
|
||||
<td style="white-space: nowrap; width:1%;"><t t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-esc="l.type"/></td>
|
||||
<td style="white-space: nowrap; width:1%;">
|
||||
<t t-esc="l.create_date.strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
</td>
|
||||
<td style="white-space: nowrap; width:1%;">
|
||||
<b t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-esc="l.level"/>
|
||||
</td>
|
||||
<td style="white-space: nowrap; width:1%;">
|
||||
<t t-if="l.level != 'SEPARATOR' and l.type not in ['link', 'markdown']" t-esc="l.type"/>
|
||||
</td>
|
||||
<t t-set="message_class" t-value="''"/>
|
||||
<t t-if="subbuild" t-set="message_class"><t t-call="runbot.build_class"><t t-set="build" t-value="subbuild"/></t></t>
|
||||
<t t-if="subbuild" t-set="message_class">
|
||||
<t t-call="runbot.build_class">
|
||||
<t t-set="build" t-value="subbuild"/>
|
||||
</t>
|
||||
</t>
|
||||
<td t-attf-class="bg-{{message_class.strip() or logclass}}-light">
|
||||
<t t-if="l.type not in ('runbot', 'link', 'markdown')">
|
||||
<t t-if="l.type == 'subbuild'">
|
||||
<a t-attf-href="/runbot/build/{{l.path}}">Build #<t t-esc="l.path"/></a>
|
||||
<a t-attf-href="/runbot/build/{{l.path}}">
|
||||
Build #
|
||||
<t t-esc="l.path"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="repo_name" t-value="l.path.replace('/data/build/', '').split('/')[0] "/>
|
||||
<t t-set="href" t-value=""/>
|
||||
<t t-if="repo_name in commit_link_per_name">
|
||||
<t t-set="repo_base_url" t-value="commit_link_per_name[repo_name].branch_id.remote_id.base_url if repo_name in commit_link_per_name else ''"/>
|
||||
<t t-set="commit_hash" t-value="commit_link_per_name[repo_name].commit_id.name if repo_name in commit_link_per_name else ''"/>
|
||||
<t t-set="path" t-value="l.path.replace('/data/build/%s/' % repo_name, '')"/>
|
||||
<t t-set="href" t-value="'https://%s/blob/%s/%s#L%s' % (repo_base_url, commit_hash, path, l.line)"/>
|
||||
</t>
|
||||
<a t-att-href="href" t-att-title="l.func"><t t-esc="l.name"/>:<t t-esc="l.line"/></a>
|
||||
</t>
|
||||
<a t-else="" t-attf-href="https://{{repo.base}}/blob/{{build['name']}}/{{l.path}}#L{{l.line}}"><t t-esc="l.name"/>:<t t-esc="l.line"/></a> <t t-esc="l.func"/>
|
||||
</t>
|
||||
<t t-if="l.type == 'link' and len(l.message.split('$$')) == 3">
|
||||
<t t-set="message" t-value="l.message.split('$$')"/>
|
||||
<t t-if="message[1].startswith('fa-')">
|
||||
<t t-esc="message[0]"/><a t-attf-href="{{l.path}}"><i t-attf-class="fa {{message[1]}}"/></a><t t-esc="message[2]"/>
|
||||
<t t-esc="message[0]"/>
|
||||
<a t-attf-href="{{l.path}}">
|
||||
<i t-attf-class="fa {{message[1]}}"/>
|
||||
</a>
|
||||
<t t-esc="message[2]"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="message[0]"/><a t-attf-href="{{l.path}}"><t t-esc="message[1]"/></a><t t-esc="message[2]"/>
|
||||
<t t-esc="message[0]"/>
|
||||
<a t-attf-href="{{l.path}}">
|
||||
<t t-esc="message[1]"/>
|
||||
</a>
|
||||
<t t-esc="message[2]"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="l.type == 'markdown'" t-raw="l._markdown()"/>
|
||||
<t t-else="">
|
||||
<t t-if="'\n' not in l.message" t-esc="l.message"/>
|
||||
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;"><t t-esc="l.message"/></pre>
|
||||
<pre t-if="'\n' in l.message" style="margin:0;padding:0; border: none;">
|
||||
<t t-esc="l.message"/>
|
||||
</pre>
|
||||
<t t-if="l.type == 'subbuild' and subbuild.sudo().error_log_ids">
|
||||
<a class="show" data-toggle="collapse" t-attf-data-target="#subbuild-{{subbuild.id}}"><i class="fa"></i></a>
|
||||
<a class="show" data-toggle="collapse" t-attf-data-target="#subbuild-{{subbuild.id}}">
|
||||
<i class="fa"/>
|
||||
</a>
|
||||
<div t-attf-id="subbuild-{{subbuild.id}}" class="collapse in">
|
||||
<table class="table table-condensed" style="margin-bottom:0;">
|
||||
<t t-foreach="subbuild.sudo().error_log_ids" t-as="sl">
|
||||
<tr>
|
||||
<td t-att-class="dict(CRITICAL='danger', ERROR='danger', WARNING='warning', OK='success', SEPARATOR='separator').get(sl.level)">
|
||||
<t t-if="sl.type == 'server'">
|
||||
<a t-attf-href="https://{{repo.base}}/blob/{{build['name']}}/{{sl.path}}#L{{sl.line}}"><t t-esc="sl.name"/>:<t t-esc="sl.line"/></a> <t t-esc="sl.func"/>
|
||||
<!--t-attf-href="https://{{repo.base_url}}/blob/{{build['name']}}/{{sl.path}}#L{{sl.line}}"-->
|
||||
<a t-att-title="sl.func"><t t-esc="sl.name"/>:<t t-esc="sl.line"/></a>
|
||||
</t>
|
||||
<t t-if="'\n' not in sl.message" t-esc="sl.message"/>
|
||||
<pre t-if="'\n' in sl.message" style="margin:0;padding:0; border: none;"><t t-esc="sl.message"/></pre>
|
||||
<pre t-if="'\n' in sl.message" style="margin:0;padding:0; border: none;">
|
||||
<t t-esc="sl.message"/>
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
66
runbot/templates/build_error.xml
Normal file
66
runbot/templates/build_error.xml
Normal file
@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.build_error">
|
||||
<t t-call='website.layout'>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<h1>Current Random Bugs on Runbot Builds</h1>
|
||||
<div class="accordion" id="errorAccordion">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col">Last seen date</div>
|
||||
<div class="col col-md-3">Module</div>
|
||||
<div class="col col-md-3">Summary</div>
|
||||
<div class="col">Nb Seen</div>
|
||||
<div class="col">Assigned to</div>
|
||||
<div class="col">&nbsp;</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-foreach="build_errors" t-as="build_error">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col"><t t-esc="build_error.last_seen_date" t-options='{"widget": "datetime"}'/></div>
|
||||
<div class="col col-md-3"><t t-esc="build_error.module_name"/></div>
|
||||
<div class="col col-md-3">
|
||||
<button class="btn btn-link" type="button" data-toggle="collapse" t-attf-data-target="#collapse{{build_error.id}}" aria-expanded="true" aria-controls="collapseOne">
|
||||
<i class="fa fa-minus"/>
|
||||
</button>
|
||||
<code><t t-esc="build_error.summary"/></code>
|
||||
</div>
|
||||
<div class="col">
|
||||
<t t-esc="build_error.build_count"/>
|
||||
</div>
|
||||
<div class="col"><t t-esc="build_error.responsible.name"/></div>
|
||||
<div class="col">
|
||||
<a groups="runbot.group_user" t-attf-href="/web/#id={{build_error.id}}&view_type=form&model=runbot.build.error" target="new" title="View in Backend">
|
||||
<i class="fa fa-list"/>
|
||||
</a>
|
||||
<a t-att-href="build_error.last_seen_build_id.build_url" t-attf-title="View last affected build ({{build_error.last_seen_build_id.id}})"><i class="fa fa-external-link"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-attf-id="collapse{{build_error.id}}" class="collapse" aria-labelledby="headingOne" data-parent="#errorAccordion">
|
||||
<div class="card-body">
|
||||
<pre class="pre-scrollable">
|
||||
<code><t t-esc="build_error.content.strip()" /></code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
81
runbot/templates/bundle.xml
Normal file
81
runbot/templates/bundle.xml
Normal file
@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.bundle">
|
||||
<t t-call='website.layout'>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<div class="navbar navbar-default">
|
||||
<span class="text-center" style="font-size: 18px;">
|
||||
<t t-esc="bundle.name"/>
|
||||
<i t-if="bundle.sticky" class="fa fa-star" style="color: #f0ad4e" />
|
||||
<a groups="runbot.group_runbot_admin" t-attf-href="/web/#id={{bundle.id}}&view_type=form&model=runbot.bundle" class="btn btn-default btn-sm" target="new" title="View in Backend">
|
||||
<i class="fa fa-list"/>
|
||||
</a>
|
||||
<a groups="runbot.group_user" t-attf-href="/runbot/bundle/{{bundle.id}}/force" title="Force A New Batch">
|
||||
<i class="fa fa-refresh"/>
|
||||
</a>
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
<t t-call="website.pager" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<table class="table table-condensed table-responsive table-stripped">
|
||||
<tr>
|
||||
<td>Version</td>
|
||||
<td>
|
||||
<t t-esc="bundle.version_id.name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Branches</td>
|
||||
<td>
|
||||
<t t-foreach="bundle.branch_groups().items()" t-as="group">
|
||||
<t t-foreach="group[1]" t-as="branch">
|
||||
<small>
|
||||
<div class="btn-toolbar mb-1" role="toolbar">
|
||||
<div class="btn-group btn-group-ssm" role="group">
|
||||
<a t-att-href="branch.branch_url" class="btn btn-default text-left" title="View Branch on Github"><i class="fa fa-github"/></a>
|
||||
<a groups="runbot.group_runbot_admin" class="btn btn-default fa fa-list text-left" t-attf-href="/web/#id={{branch.id}}&view_type=form&model=runbot.branch" target="new" title="View Branch in Backend"/>
|
||||
<a href="#" t-esc="branch.remote_id.short_name" class="btn btn-default disabled text-left"/>
|
||||
<a t-attf-href="/runbot/branch/{{branch.id}}" class="btn btn-default text-left" title="View Branch Details"><t t-esc="branch.name"/> <i t-if="not branch.alive" title="deleted/closed" class="fa fa-ban text-danger"/></a>
|
||||
<t t-if="len(group[1]) == 1 and not branch.is_pr">
|
||||
<a t-attf-href="https://{{group[0].main_remote_id.base_url}}/compare/{{bundle.version_id.name}}...{{branch.remote_id.owner}}:{{branch.name}}?expand=1" class="btn btn-default text-left" title="Create pr"><i class="fa fa-code-fork"/> Create pr</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</small>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr t-if="more">
|
||||
<td>Project</td>
|
||||
<td t-esc="bundle.project_id.name"/>
|
||||
</tr>
|
||||
<tr t-if="more">
|
||||
<td>New build enabled</td>
|
||||
<td>
|
||||
<i t-attf-class="fa fa-{{'times' if bundle.no_build else 'check'}}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="more">
|
||||
<td>Modules</td>
|
||||
<td t-esc="bundle.modules or '/'"/>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div t-foreach="bundle.consistency_warning()" t-as="warning" t-esc="warning[1]" t-attf-class="alert alert-{{warning[0]}}"/>
|
||||
<div class="batch_row" t-foreach="batchs" t-as="batch">
|
||||
<t t-call="runbot.batch_tile"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
126
runbot/templates/commit.xml
Normal file
126
runbot/templates/commit.xml
Normal file
@ -0,0 +1,126 @@
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.commit_status_state_td">
|
||||
<!-- Must be called with a `state` variable !-->
|
||||
<td t-if="state=='pending'">
|
||||
<i class="fa fa-circle text-warning"/>
|
||||
&nbsp;
|
||||
<t t-esc="state"/>
|
||||
</td>
|
||||
<td t-if="state=='success'">
|
||||
<i class="fa fa-check text-success"/>
|
||||
&nbsp;
|
||||
<t t-esc="state"/>
|
||||
</td>
|
||||
<td t-if="state in ('failure', 'error')">
|
||||
<i class="fa fa-times text-danger"/>
|
||||
&nbsp;
|
||||
<t t-esc="state"/>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<template id="runbot.commit">
|
||||
<t t-call='website.layout'>
|
||||
<div class="row">
|
||||
<!-- Commit base informations -->
|
||||
<div class="col-md-6">
|
||||
<table class="table table-stripped">
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<t t-esc="commit.name"/>
|
||||
<div class="btn-group" role="group">
|
||||
<a t-att-href="'' if not reflogs else 'https://%s/commit/%s' % (reflogs[0].branch_id.remote_id.base_url, commit.name)" class="btn btn-sm text-left" title="View Commit on Github"><i class="fa fa-github"/></a>
|
||||
<a groups="runbot.group_runbot_admin" class="btn btn-sm fa fa-list text-left" t-attf-href="/web/#id={{commit.id}}&view_type=form&model=runbot.commit" target="new" title="View Commit in Backend"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Repo</td>
|
||||
<td t-esc="commit.repo_id.name"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Subject</td>
|
||||
<td t-esc="commit.subject"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td t-esc="commit.date"/>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Author</td>
|
||||
<td>
|
||||
<t t-esc="commit.author"/>
|
||||
<small t-esc="commit.author_email"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="commit.author != commit.committer">
|
||||
<td>Commiter</td>
|
||||
<td>
|
||||
<t t-esc="commit.committer"/>
|
||||
<small t-esc="commit.committer_email"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Status -->
|
||||
<div class="col-md-4">
|
||||
<h3>Last Status</h3>
|
||||
<table class="table table-sm table-borderless">
|
||||
<tr t-foreach='last_status_by_context' t-as='context'>
|
||||
<t t-set="status" t-value="last_status_by_context[context]"/>
|
||||
<td t-esc="status.sent_date and status.sent_date.strftime('%Y-%m-%d %H:%M:%S') or '—'"/>
|
||||
<td t-esc="context"/>
|
||||
<t t-call="runbot.commit_status_state_td">
|
||||
<t t-set="state" t-value="status.state"/>
|
||||
</t>
|
||||
<td>
|
||||
<a t-att-href="status.target_url">
|
||||
build
|
||||
<t t-if="status.target_url" t-esc="status.target_url.split('/')[-1]" />
|
||||
</a>
|
||||
</td>
|
||||
<td groups="runbot.group_user">
|
||||
<a t-attf-href="/runbot/commit/resend/{{status.id}}" title="Resend github status">
|
||||
<i class="fa fa-repeat"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3>Branch presence history</h3>
|
||||
<table class="table table-stripped">
|
||||
<tr t-foreach='reflogs' t-as='reflog'>
|
||||
<td t-esc="reflog.date"/>
|
||||
<td t-esc="reflog.branch_id.remote_id.short_name"/>
|
||||
<td><a t-attf-href="/runbot/branch/{{reflog.branch_id.id}}" t-esc="reflog.branch_id.name" title="View Branch Details"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3>Status history</h3>
|
||||
<table class="table table-stripped">
|
||||
<tr t-foreach='status_list' t-as='status'>
|
||||
<td t-esc="status.sent_date and status.sent_date.strftime('%Y-%m-%d %H:%M:%S') or '—'"/>
|
||||
<td t-esc="status.context"/>
|
||||
<t t-call="runbot.commit_status_state_td">
|
||||
<t t-set="state" t-value="status.state"/>
|
||||
</t>
|
||||
<td>
|
||||
<a t-attf-href="/runbot/build/{{status.build_id.id}}">
|
||||
build
|
||||
<t t-esc="status.build_id.id" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
@ -1,112 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.sticky-dashboard">
|
||||
<t t-call='website.layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
<style>
|
||||
.bg-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
h4 {
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
.r-mb02 { margin-bottom: 0.2em; }
|
||||
</style>
|
||||
</t>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<div class="container-fluid">
|
||||
<p class="text-center">
|
||||
<t t-foreach="host_stats" t-as="hs">
|
||||
<span class="label label-default">
|
||||
<t t-esc="hs['host']"/>: <t t-esc="hs['testing']"/> testing, <t t-esc="hs['running']"/> running
|
||||
</span>&nbsp;
|
||||
</t>
|
||||
<span t-attf-class="label label-{{pending_level}}">Pending: <t t-esc="pending_total"/></span>
|
||||
</p>
|
||||
</div>
|
||||
<t t-foreach="repo_dict.values()" t-as="repo">
|
||||
<h4><span><t t-esc="repo['name']"/></span>
|
||||
<small class="pull-right">
|
||||
<t t-esc="repo['testing']"/> testing,
|
||||
<t t-esc="repo['running']"/> running,
|
||||
<t t-esc="repo['pending']"/> pending.
|
||||
</small></h4>
|
||||
<div t-foreach="repo['branches'].values()" t-as="br">
|
||||
<div class="col-md-1">
|
||||
<b t-esc="br['name']"/><br/>
|
||||
<small><t t-esc="br['builds'][0].get_formated_build_time()"/></small>
|
||||
</div>
|
||||
<div class="col-md-11 r-mb02">
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu.global_state=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result == 'skipped'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ['running','done'] and bu.global_result in ['killed', 'manually_killed']"><t t-set="klass">killed</t></t>
|
||||
<div t-attf-class="bg-{{klass}} col-md-4">
|
||||
<i class="fa fa-at"></i>
|
||||
<t t-esc="bu['author']"/>
|
||||
<t t-if="bu['committer'] and bu['author'] != bu['committer']" t-id="bu['committer']">
|
||||
(<i class="fa fa-sign-out"></i>&nbsp;<t t-esc="bu['committer']"/>)
|
||||
</t>
|
||||
<br/>
|
||||
<i class="fa fa-envelope-o"></i>
|
||||
<t t-if="bu['build_type']=='scheduled'"><i class="fa fa-moon-o" t-att-title="bu.build_type_label()" t-att-aria-label="bu.build_type_label()"/></t>
|
||||
<t t-if="bu['build_type'] in ('rebuild', 'redirect')"><i class="fa fa-recycle" t-att-title="bu.build_type_label()" t-att-aria-label="bu.build_type_label()"/></t>
|
||||
<a t-attf-href="https://#{repo['base']}/commit/#{bu['name']}"><t t-esc="bu['subject'][:32] + ('...' if bu['subject'][32:] else '') " t-att-title="bu['subject']"/></a>
|
||||
<br/>
|
||||
<t t-call="runbot.build_name"/> — <small><a t-attf-href="/runbot/build/{{bu['id']}}"><t t-esc="bu['dest']"/></a> on <t t-esc="bu.real_build.host"/> <a t-if="bu.local_state == 'running'" t-attf-href="http://{{bu['domain']}}/?db={{bu['dest']}}-all"><i class="fa fa-sign-in"></i></a></small>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="runbot.glances">
|
||||
<t t-call='portal.frontend_layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
<style>
|
||||
.label-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
h4 {
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
.r-mb02 { margin-bottom: 0.2em; }
|
||||
</style>
|
||||
</t>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<div>
|
||||
<span t-attf-class="label label-{{pending_level}}">Pending: <t t-esc="pending_total"/></span>
|
||||
<span t-attf-class="badge badge-{{pending_level}}">
|
||||
Pending:
|
||||
<t t-esc="pending_total"/>
|
||||
</span>
|
||||
</div>
|
||||
<t t-foreach="glances_data.keys()" t-as="repo">
|
||||
<h4><t t-esc="repo"/>
|
||||
<t t-set="project_id"/>
|
||||
<t t-set="nb_project" t-value="len(bundles.mapped('project_id'))"/>
|
||||
<t t-foreach="bundles.sorted(lambda b: (-b.project_id.id, b.version_id.number), reverse=True)" t-as="bundle">
|
||||
<h3 t-if="nb_project > 1 and project_id != bundle.project_id.id" t-esc="bundle.project_id.name"/>
|
||||
<t t-set="project_id" t-value="bundle.project_id.id"/>
|
||||
<h4>
|
||||
<t t-esc="bundle.name"/>
|
||||
</h4>
|
||||
<t t-foreach="glances_data[repo]" t-as="br">
|
||||
<t t-if="br[1] == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="br[1] == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="br[1] == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="br[1] == 'killed'"><t t-set="klass">killed</t></t>
|
||||
<span t-attf-class="label label-{{klass}}"><t t-esc="br[0]"/></span>
|
||||
<t t-foreach="bundle.last_done_batch.slot_ids" t-as="slot">
|
||||
<span t-attf-class="badge badge-{{slot.build_id.get_color_class()}}">
|
||||
<t t-esc="slot.trigger_id.name"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
@ -127,31 +49,6 @@
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
</t>
|
||||
<table>
|
||||
<tr t-foreach="last_monitored" t-as="build">
|
||||
<t t-set="build" t-value="build.real_build"/>
|
||||
<td>
|
||||
<t t-esc="build.repo_id.short_name"/>/<t t-esc="build.branch_id.branch_name"/>
|
||||
</td>
|
||||
<t t-if="build.local_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="build.local_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="build.local_result == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="build.local_result == 'killed'"><t t-set="klass">killed</t></t>
|
||||
<td>
|
||||
<a t-attf-href='/runbot/build/{{build.id}}'><span t-attf-class="label label-{{klass}}"><t t-esc="build.config_id.name"/></span></a>
|
||||
</td>
|
||||
<td>
|
||||
<span t-foreach="build.children_ids.sorted(key=lambda c:c.config_id.name, reverse=True).filtered(lambda c: c.local_result != 'ok' and not c.orphan_result)" t-as="child">
|
||||
<t t-if="child.global_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="child.global_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="child.global_result == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="child.global_result == 'killed'"><t t-set="klass">killed</t></t>
|
||||
<a t-attf-href='/runbot/build/{{child.id}}'><span t-attf-class="label label-{{klass}}"><t t-esc="child.config_data.get('db_name') or child.config_id.name"/></span></a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</t>
|
||||
</template>
|
||||
|
||||
@ -162,9 +59,6 @@
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
<style>
|
||||
.label-killed {
|
||||
background-color: #aaa;
|
||||
}
|
||||
h4 {
|
||||
padding: 3px 0;
|
||||
border-bottom: 1px solid grey;
|
||||
@ -176,59 +70,114 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<t t-call="slots_infos"/>
|
||||
<t t-call="runbot.slots_infos"/>
|
||||
</div>
|
||||
<t t-foreach="glances_data.keys()" t-as="repo">
|
||||
<div>
|
||||
<span t-esc="repo"/>
|
||||
<t t-foreach="glances_data[repo]" t-as="br">
|
||||
<t t-if="br[1] == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="br[1] == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="br[1] == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="br[1] == 'killed'"><t t-set="klass">killed</t></t>
|
||||
<span t-attf-class="label label-{{klass}}"><t t-esc="br[0]"/></span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="hosts_data.sorted(key=lambda h:h.name)" t-as="host">
|
||||
<div>
|
||||
<span t-esc="host.name.split('.')[0]"/>
|
||||
<t t-if="host.nb_testing == 0"><t t-set="klass">success</t></t>
|
||||
<t t-if="host.nb_testing > 0"><t t-set="klass">info</t></t>
|
||||
<t t-if="host.nb_testing == host.sudo().get_nb_worker()"><t t-set="klass">warning</t></t>
|
||||
<t t-if="host.nb_testing > host.sudo().get_nb_worker()"><t t-set="klass">danger</t></t>
|
||||
<span t-attf-class="label label-{{klass}}"><span t-esc="host.nb_testing"/>/<span t-esc="host.sudo().get_nb_worker()"/></span>
|
||||
<t t-if="host.nb_testing == 0">
|
||||
<t t-set="klass">success</t>
|
||||
</t>
|
||||
<t t-if="host.nb_testing > 0">
|
||||
<t t-set="klass">info</t>
|
||||
</t>
|
||||
<t t-if="host.nb_testing == host.nb_worker">
|
||||
<t t-set="klass">warning</t>
|
||||
</t>
|
||||
<t t-if="host.nb_testing > host.nb_worker">
|
||||
<t t-set="klass">danger</t>
|
||||
</t>
|
||||
<span t-attf-class="badge badge-{{klass}}">
|
||||
<span t-esc="host.nb_testing"/>
|
||||
/
|
||||
<span t-esc="host.nb_worker"/>
|
||||
</span>
|
||||
<t t-esc="host.nb_running"/>
|
||||
<t t-set="succes_time" t-value="int(datetime.datetime.now().timestamp() - host.last_success.timestamp())"/>
|
||||
<t t-set="start_time" t-value="int(datetime.datetime.now().timestamp() - host.last_start_loop.timestamp())"/>
|
||||
<t t-set="end_time" t-value="int(datetime.datetime.now().timestamp() - host.last_end_loop.timestamp())"/>
|
||||
|
||||
<t t-set="klass">success</t>
|
||||
<t t-if="succes_time > 30"><t t-set="klass">info</t></t>
|
||||
<t t-if="succes_time > 180"><t t-set="klass">danger</t></t>
|
||||
<t t-if="succes_time > 30">
|
||||
<t t-set="klass">info</t>
|
||||
</t>
|
||||
<t t-if="succes_time > 180">
|
||||
<t t-set="klass">danger</t>
|
||||
</t>
|
||||
|
||||
<span t-attf-class="label label-{{klass}}"><span t-esc="succes_time"/></span>
|
||||
<span t-attf-class="badge badge-{{klass}}">
|
||||
<span t-esc="succes_time"/>
|
||||
</span>
|
||||
|
||||
<t t-set="klass">success</t>
|
||||
<t t-if="start_time > 60*10"><t t-set="klass">info</t></t>
|
||||
<t t-if="start_time > 60*15"><t t-set="klass">danger</t></t>
|
||||
<t t-if="start_time > 60*10">
|
||||
<t t-set="klass">info</t>
|
||||
</t>
|
||||
<t t-if="start_time > 60*15">
|
||||
<t t-set="klass">danger</t>
|
||||
</t>
|
||||
|
||||
<span t-attf-class="label label-{{klass}}"><span t-esc="start_time"/></span>
|
||||
<span t-attf-class="badge badge-{{klass}}">
|
||||
<span t-esc="start_time"/>
|
||||
</span>
|
||||
|
||||
<t t-set="klass">success</t>
|
||||
<t t-if="end_time > 60*10"><t t-set="klass">info</t></t>
|
||||
<t t-if="end_time > 60*15"><t t-set="klass">danger</t></t>
|
||||
<t t-if="end_time > 60*10">
|
||||
<t t-set="klass">info</t>
|
||||
</t>
|
||||
<t t-if="end_time > 60*15">
|
||||
<t t-set="klass">danger</t>
|
||||
</t>
|
||||
|
||||
<span t-attf-class="label label-{{klass}}"><span t-esc="end_time"/></span>
|
||||
<span t-attf-class="badge badge-{{klass}}">
|
||||
<span t-esc="end_time"/>
|
||||
</span>
|
||||
|
||||
<t t-set="cron_time" t-value="end_time-start_time"/>
|
||||
<t t-set="klass">success</t>
|
||||
<t t-if="abs(cron_time) > 10"><t t-set="klass">info</t></t>
|
||||
<t t-if="abs(cron_time) > 60"><t t-set="klass">danger</t></t>
|
||||
<span t-attf-class="label label-{{klass}}"><span t-esc="cron_time"/></span>
|
||||
<t t-if="abs(cron_time) > 10">
|
||||
<t t-set="klass">info</t>
|
||||
</t>
|
||||
<t t-if="abs(cron_time) > 60">
|
||||
<t t-set="klass">danger</t>
|
||||
</t>
|
||||
<span t-attf-class="badge badge-{{klass}}">
|
||||
<span t-esc="cron_time"/>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<table>
|
||||
<tr t-foreach="bundles.sorted(lambda b: b.version_id.number, reverse=True)" t-as="bundle">
|
||||
<td>
|
||||
<t t-esc="bundle.version_id.number"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-set='batch' t-value="bundle.with_context({'category_id': category.id}).last_done_batch"/>
|
||||
<table>
|
||||
<t t-foreach="batch.slot_ids" t-as='slot'>
|
||||
<tr>
|
||||
<td>
|
||||
<t t-esc="slot.trigger_id.name[:4]"/>
|
||||
</td>
|
||||
<t t-set="build" t-value="slot.build_id"/>
|
||||
<td>
|
||||
<span t-attf-class="badge badge-{{slot.build_id.get_color_class()}}">
|
||||
<i t-attf-class="fa fa-{{category.icon}}"/>
|
||||
</span>
|
||||
</td>
|
||||
<td t-foreach="build.children_ids" t-as="child">
|
||||
<span t-attf-class="badge badge-{{slot.build_id.get_color_class()}}">
|
||||
<t t-esc="child.params_id.config_id.name[:4]"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,179 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Replace default menu ( Home / Contactus and co...) with 5 first repos) -->
|
||||
<template id="inherits_branch_in_menu" inherit_id="website.layout" name="Inherits Show top 6 repo in menu and dropdown">
|
||||
<xpath expr="//footer" position="replace">
|
||||
</xpath>
|
||||
<xpath expr="//nav" position="replace">
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<a t-if="repo" t-attf-href="/runbot/repo/{{slug(repo)}}?search={{request.params.get('search', '')}}">
|
||||
<b style="color:#777;"><t t-esc="repo.short_name"/></b>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#top_menu_collapse">
|
||||
<span class="navbar-toggler-icon"/>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="top_menu_collapse">
|
||||
<ul class="nav navbar-nav ml-auto text-right" id="top_menu">
|
||||
<t t-if="repos" >
|
||||
<t t-foreach="repos[:6]" t-as="re">
|
||||
<li ><a t-attf-href="/runbot/repo/{{slug(re)}}?search={{request.params.get('search', '')}}"><i class='fa fa-github' /> <t t-esc="re.short_name"/></a></li>
|
||||
</t>
|
||||
<li t-if="len(repos)>6" class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"><i class="fa fa-plus"/></a>
|
||||
<ul class="dropdown-menu">
|
||||
<t t-foreach='repos[6:]' t-as='re'>
|
||||
<li><a t-attf-href="/runbot/repo/{{slug(re)}}"><t t-esc="re.short_name"/></a></li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
<li class="nav-item divider" t-ignore="true" t-if="not user_id._is_public()"/>
|
||||
<li class="nav-item dropdown" t-ignore="true" t-if="not user_id._is_public()">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
|
||||
<b>
|
||||
<span t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) > 25 else user_id.name"/>
|
||||
</b>
|
||||
</a>
|
||||
<div class="dropdown-menu js_usermenu" role="menu">
|
||||
<a id="o_logout" class="dropdown-item" t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<t t-raw="nav_form or ''">
|
||||
</t>
|
||||
</div>
|
||||
</nav>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- remove black bar with app switcher -->
|
||||
<template id="inherits_no_black_bar" inherit_id="website.user_navbar" name="Inherits No black user_navbar">
|
||||
<xpath expr="//nav[@id='oe_main_menu_navbar']" position="attributes">
|
||||
<attribute name="groups">base.group_website_publisher</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-set='body_classname']" position="attributes">
|
||||
<attribute name="t-value">'o_connected_user' if env['ir.ui.view'].user_has_groups('base.group_website_publisher') else None</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="runbot.slots_infos" name="Hosts slot nb pending/testing/slots">
|
||||
<span t-attf-class="label label-{{pending_level}}">Pending: <t t-esc="pending_total"/></span>
|
||||
<t t-set="testing" t-value="hosts_data._total_testing()"/>
|
||||
<t t-set="workers" t-value="hosts_data._total_workers()"/>
|
||||
<t t-set="klass">success</t>
|
||||
<t t-if="not workers" t-set="klass">danger</t>
|
||||
<t t-else="">
|
||||
<t t-if="int(testing)/workers > 0" t-set="klass">info</t>
|
||||
<t t-if="int(testing)/workers > 0.75" t-set="klass">warning</t>
|
||||
<t t-if="int(testing)/workers >= 1" t-set="klass">danger</t>
|
||||
</t>
|
||||
<span t-attf-class="label label-{{klass}}">Testing: <t t-esc="testing"/>/<t t-esc="workers"/></span>
|
||||
</template>
|
||||
|
||||
<!-- Frontend repository block -->
|
||||
<template id="runbot.repo">
|
||||
<t t-call='website.layout'>
|
||||
<t t-set="head">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
</t>
|
||||
<template id="runbot.bundles">
|
||||
<t t-call='web.frontend_layout'>
|
||||
<t t-set="nav_form">
|
||||
<form class="form-inline my-2 my-lg-0" role="search" t-att-action="qu(search='')" method="get">
|
||||
<div class="input-group md-form form-sm form-2 pl-0">
|
||||
<input class="form-control my-0 py-1 red-border" type="text" placeholder="Search" aria-label="Search" name="search" t-att-value="search"/>
|
||||
<div class="input-group-append">
|
||||
<button type='submit' class="input-group-text red lighten-3" id="basic-text1"><i class="fa fa-search text-grey"></i></button>
|
||||
<button type='submit' class="input-group-text red lighten-3" id="basic-text1">
|
||||
<i class="fa fa-search text-grey"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</t>
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid frontend">
|
||||
<div class="row">
|
||||
<div class='col-md-12'>
|
||||
<span class="pull-right" t-call="runbot.slots_infos"/>
|
||||
</div>
|
||||
<div class='col-md-12'>
|
||||
<div t-if="message" class="alert alert-warning" role="alert">
|
||||
<t t-esc="message" />
|
||||
</div>
|
||||
<div t-if="not repo" class="mb32">
|
||||
<h1>No Repository yet.</h1>
|
||||
<div t-if="not project" class="mb32">
|
||||
<h1>No project</h1>
|
||||
</div>
|
||||
|
||||
<table t-if="repo" class="table table-condensed table-bordered" style="table-layout: initial;">
|
||||
<tr>
|
||||
<th>Branch</th>
|
||||
<td colspan="4">
|
||||
<span class="pull-right" t-call="runbot.slots_infos"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-foreach="branches" t-as="br">
|
||||
<td style="width:12%">
|
||||
<small class="branch_time" ><t t-esc="br['builds'] and br['builds'][0].get_formated_build_time()"/></small>
|
||||
<div class="branch_name"><i t-if="br['branch'].sticky" class="fa fa-star" style="color: #f0ad4e" /><a t-attf-href="/runbot/branch/#{br['branch'].id}"><b t-esc="br['branch'].branch_name"/></a></div>
|
||||
<div class="btn-group btn-group-xs">
|
||||
<a t-attf-href="{{br['branch'].branch_url}}" class="btn btn-default btn-xs"><t t-esc="'Branch ' if not br['branch'].pull_head_name else 'Pull '"/><i class="fa fa-github"/></a>
|
||||
<a t-attf-href="/runbot/quick_connect/#{br['branch'].id}" class="btn btn-default btn-xs" aria-label="Quick Connect"><i class="fa fa-fast-forward" title="Quick Connect"/></a>
|
||||
</div>
|
||||
<t t-if="br['branch'].sticky">
|
||||
<br/>
|
||||
<t t-if="br['branch'].coverage_result > 0">
|
||||
<t t-set="last_build" t-value="br['branch']._get_last_coverage_build()" />
|
||||
<a t-attf-href="http://{{last_build.real_build.host}}/runbot/static/build/#{last_build['dest']}/coverage/index.html">
|
||||
<span class="label label-info">cov: <t t-esc="br['branch'].coverage_result"/>%</span>
|
||||
<div t-else="">
|
||||
<div t-foreach="bundles" t-as="bundle" class="row bundle_row">
|
||||
<div class="col-md-3 col-lg-2 cell">
|
||||
<div class="one_line">
|
||||
<i t-if="bundle.sticky" class="fa fa-star" style="color: #f0ad4e" />
|
||||
<a t-attf-href="/runbot/bundle/#{bundle.id}" title="View Bundle">
|
||||
<b t-esc="bundle.name"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="label label-info">cov: <t t-esc="br['branch'].coverage_result"/>%</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="br['branch'].branch_config_id"><!--custom config on branch-->
|
||||
<br/>
|
||||
<span class="label label-info"><t t-esc="br['branch'].branch_config_id.name" t-att-title="br['branch'].branch_config_id.description"/></span>
|
||||
<t t-foreach="categories" t-as="category">
|
||||
<t t-if="active_category_id != category.id">
|
||||
<t t-set="last_category_batch" t-value="bundle.with_context(category_id=category.id).last_done_batch"/>
|
||||
<t t-if="last_category_batch">
|
||||
<t t-if="category.view_id" t-call="{{category.view_id.key}}"/>
|
||||
<a t-else=""
|
||||
t-attf-title="View last {{category.name}} batch"
|
||||
t-attf-href="/runbot/batch/{{last_category_batch.id}}"
|
||||
t-attf-class="fa fa-{{category.icon}}"
|
||||
/>
|
||||
</t>
|
||||
</td>
|
||||
<t t-foreach="br['builds']" t-as="bu">
|
||||
<t t-if="bu.global_state=='pending'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_state in ('testing', 'waiting')"><t t-set="klass">info</t></t>
|
||||
<t t-if="bu.global_state in ['running','done']">
|
||||
<t t-if="bu.global_result == 'ko'"><t t-set="klass">danger</t></t>
|
||||
<t t-if="bu.global_result == 'warn'"><t t-set="klass">warning</t></t>
|
||||
<t t-if="bu.global_result == 'ok'"><t t-set="klass">success</t></t>
|
||||
<t t-if="bu.global_result == 'skipped'"><t t-set="klass">default</t></t>
|
||||
<t t-if="bu.global_result in ['killed', 'manually_killed']"><t t-set="klass">killed</t></t>
|
||||
</t>
|
||||
<td t-attf-class="bg-{{klass}}-light" style="width:22%">
|
||||
<t t-call="runbot.build_button">
|
||||
<t t-set="klass">btn-group-sm</t>
|
||||
<t t-set="show_rebuild_button" t-value="bu==br['builds'][0]"></t>
|
||||
<t t-set="show_commit_button" t-value="True"/>
|
||||
</t>
|
||||
<t t-if="bu['build_type']=='scheduled'"><i class="fa fa-moon-o" t-att-title="bu.build_type_label()" t-att-aria-label="bu.build_type_label()"/></t>
|
||||
<t t-if="bu['build_type'] in ('rebuild', 'indirect')"><i class="fa fa-recycle" t-att-title="bu.build_type_label()" t-att-aria-label="bu.build_type_label()"/></t>
|
||||
<span t-if="bu['subject']" class="build_subject">
|
||||
<t t-if="bu.config_id != bu.branch_id.config_id">
|
||||
<b t-esc="bu.config_id.name"/>
|
||||
</t>
|
||||
<span t-esc="bu['subject'][:32] + ('...' if bu['subject'][32:] else '') " t-att-title="bu['subject']"/>
|
||||
<br/>
|
||||
</span>
|
||||
<t t-id="bu['author']">
|
||||
<t t-esc="bu['author']"/>
|
||||
<t t-if="bu['committer'] and bu['author'] != bu['committer']" t-id="bu['committer']">
|
||||
(<span class="octicon octicon-arrow-right"></span>&nbsp;<t t-esc="bu['committer']"/>)
|
||||
</t>
|
||||
<br/>
|
||||
</t>
|
||||
<small><t t-esc="bu['dest']"/> on <t t-esc="bu.real_build.host"/></small><br/>
|
||||
<t t-call="runbot.build_name"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<t t-call="runbot.branch_github_menu"/>
|
||||
</div>
|
||||
<div class="col-md-9 col-lg-10">
|
||||
<div class="row no-gutters">
|
||||
<div t-foreach="bundle.last_batchs" t-as="batch" t-attf-class="col-md-6 col-xl-3 {{'d-none d-xl-block' if batch_index > 1 else ''}}">
|
||||
<t t-call="runbot.batch_tile"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="runbot.batch_tile">
|
||||
<t t-set="klass">info</t>
|
||||
<t t-if="batch.state=='skipped'" t-set="klass">killed</t>
|
||||
<t t-if="batch.state=='done' and all(slot.build_id.global_result == 'ok' for slot in batch.slot_ids if slot.build_id)" t-set="klass">success</t>
|
||||
<t t-if="batch.state=='done' and any(slot.build_id.global_result in ('ko', 'warn') for slot in batch.slot_ids)" t-set="klass">danger</t>
|
||||
|
||||
<div t-attf-class="batch_tile {{'more' if more else 'nomore'}}">
|
||||
<div t-attf-class="card bg-{{klass}}-light">
|
||||
<div class="batch_header">
|
||||
<a t-attf-href="/runbot/batch/#{batch.id}" t-attf-class="badge badge-{{'warning' if batch.has_warning else 'light'}}" title="View Batch">
|
||||
<t t-esc="batch.get_formated_age()"/>
|
||||
<i class="fa fa-exclamation-triangle" t-if="batch.has_warning"/>
|
||||
<i class="arrow fa fa-window-maximize"/>
|
||||
</a>
|
||||
</div>
|
||||
<t t-if="batch.state=='preparing'">
|
||||
<span><i class="fa fa-cog fa-spin fa-fw"/> preparing</span>
|
||||
</t>
|
||||
<div class="batch_slots">
|
||||
<t t-foreach="batch.slot_ids" t-as="slot">
|
||||
<t t-if="slot.build_id">
|
||||
<div t-if="(not slot.trigger_id.hide and trigger_display is None) or (trigger_display and slot.trigger_id.id in trigger_display)"
|
||||
t-call="runbot.slot_button" class="slot_container"/>
|
||||
</t>
|
||||
</t>
|
||||
<div class="slot_filler" t-foreach="range(10)" t-as="x"/>
|
||||
</div>
|
||||
<div class="batch_commits">
|
||||
<div t-foreach="batch.commit_link_ids.sorted(lambda cl: (cl.commit_id.repo_id.sequence, cl.commit_id.repo_id.id))" t-as="commit_link" class="one_line">
|
||||
|
||||
<a t-attf-href="/runbot/commit/#{commit_link.commit_id.id}" t-attf-class="badge badge-light batch_commit match_type_{{commit_link.match_type}}">
|
||||
<i class="fa fa-fw fa-hashtag" t-if="commit_link.match_type == 'new'" title="This commit is a new head"/>
|
||||
<i class="fa fa-fw fa-link" t-if="commit_link.match_type == 'head'" title="This commit is an existing head from bundle branches"/>
|
||||
<i class="fa fa-fw fa-code-fork" t-if="commit_link.match_type == 'base_match'" title="This commit is matched from a base batch with matching merge_base"/>
|
||||
<i class="fa fa-fw fa-clock-o" t-if="commit_link.match_type == 'base_head'" title="This commit is the head of a base branch"/>
|
||||
<t t-esc="commit_link.commit_id.dname"/>
|
||||
</a>
|
||||
<a t-att-href="'https://%s/commit/%s' % (commit_link.branch_id.remote_id.base_url, commit_link.commit_id.name)" class="badge badge-light" title="View Commit on Github"><i class="fa fa-github"/></a>
|
||||
<span t-esc="commit_link.commit_id.subject"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
|
||||
|
||||
|
15
runbot/templates/git.xml
Normal file
15
runbot/templates/git.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="runbot.git_config">[core]
|
||||
repositoryformatversion = 0
|
||||
filemode = true
|
||||
bare = true
|
||||
<t t-foreach="repo.remote_ids" t-as="remote_id">
|
||||
[remote "<t t-esc="remote_id.remote_name"/>"]
|
||||
url = <t t-esc="remote_id.name"/>
|
||||
<t t-if = "remote_id.fetch_heads"> fetch = +refs/heads/*:refs/<t t-esc='remote_id.remote_name'/>/heads/*</t>
|
||||
<t t-if = "remote_id.fetch_pull"> fetch = +refs/pull/*/head:refs/<t t-esc='remote_id.remote_name'/>/pull/*</t>
|
||||
</t></template>
|
||||
</data>
|
||||
</odoo>
|
310
runbot/templates/utils.xml
Normal file
310
runbot/templates/utils.xml
Normal file
@ -0,0 +1,310 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- base layout -->
|
||||
<template id="runbot.layout" inherit_id="website.layout" name="Custom website layout">
|
||||
<xpath expr="//head/meta[last()]" position="after">
|
||||
<t t-if="refresh">
|
||||
<meta http-equiv="refresh" t-att-content="refresh"/>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//footer" position="replace">
|
||||
</xpath>
|
||||
<xpath expr="//nav[hasclass('navbar')]" position="replace">
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-light">
|
||||
<a t-if="project" t-att-href="qu(search=search)">
|
||||
<b style="color:#777;">
|
||||
<t t-esc="project.name"/>
|
||||
</b>
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler" data-toggle="collapse" data-target="#top_menu_collapse">
|
||||
<span class="navbar-toggler-icon"/>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="top_menu_collapse">
|
||||
<ul class="nav navbar-nav ml-auto text-right" id="top_menu">
|
||||
<t t-if="projects">
|
||||
<t t-foreach="projects" t-as="l_project">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" t-att-href="qu('/runbot/%s' % slug(l_project), search=search)">
|
||||
<t t-esc="l_project.name"/>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</t>
|
||||
<li class="nav-item divider"/>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-gear"/>
|
||||
</a>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<form class="px-4 py-3" method="post" action="/runbot/submit">
|
||||
<input type="hidden" name="save" value="1"/>
|
||||
<input type="hidden" name="redirect" t-att-value="current_path"/>
|
||||
<div class="text-nowrap">
|
||||
<input type="checkbox" name="more" id="more" t-att-checked="more"/>
|
||||
<label for="more">More info</label>
|
||||
</div>
|
||||
<div class="text-nowrap">
|
||||
<input type="checkbox" name="keep_search" id="keep_search" t-att-checked="keep_search"/>
|
||||
<label for="keep_search">Persistent search</label>
|
||||
</div>
|
||||
<hr class="separator"/>
|
||||
<div class="text-nowrap">
|
||||
<label for="filter_mode">Filter</label>
|
||||
<select class="form-control" name="filter_mode" id="filter_mode">
|
||||
<option value="all" t-att-selected="filter_mode=='all'">All</option>
|
||||
<option value="sticky" t-att-selected="filter_mode=='sticky'">Sticky only</option>
|
||||
<option value="nosticky" t-att-selected="filter_mode=='nosticky'">Dev only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div t-if="categories" class="text-nowrap">
|
||||
<label for="category">Category</label>
|
||||
<select class="form-control" name="category" id="category">
|
||||
<option t-foreach="categories" t-as="category" t-att-value="category.id" t-esc="category.name" t-att-selected="category.id==active_category_id"/>
|
||||
</select>
|
||||
</div>
|
||||
<hr class="separator"/>
|
||||
<t t-if="triggers">
|
||||
<input type="hidden" name="update_triggers" t-att-value="project.id"/>
|
||||
<t t-foreach="triggers" t-as="trigger">
|
||||
<div class="text-nowrap">
|
||||
<input type="checkbox" t-attf-name="trigger_{{trigger.id}}" t-attf-id="trigger_{{trigger.id}}" t-att-checked="trigger_display is None or trigger.id in trigger_display"/>
|
||||
<label t-attf-for="trigger_{{trigger.id}}" t-esc="trigger.name"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item divider" t-ignore="true"/>
|
||||
<t t-if="not user_id._is_public()">
|
||||
<t t-if="nb_assigned_errors and nb_assigned_errors > 0">
|
||||
<li class="nav-item divider"/>
|
||||
<li class="nav-item">
|
||||
<a href="/runbot/errors" class="nav-link text-danger" t-attf-title="You have {{nb_assigned_errors}} random bug assigned"><i class="fa fa-bug"/><t t-esc="nb_assigned_errors"/></a>
|
||||
</li>
|
||||
</t>
|
||||
<t t-elif="nb_build_errors and nb_build_errors > 0">
|
||||
<li class="nav-item divider"/>
|
||||
<li class="nav-item">
|
||||
<a href="/runbot/errors" class="nav-link" title="Random Bugs"><i class="fa fa-bug"/></a>
|
||||
</li>
|
||||
</t>
|
||||
<li class="nav-item dropdown" t-ignore="true">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown">
|
||||
<b>
|
||||
<span t-esc="user_id.name[:23] + '...' if user_id.name and len(user_id.name) > 25 else user_id.name"/>
|
||||
</b>
|
||||
</a>
|
||||
<div class="dropdown-menu js_usermenu" role="menu">
|
||||
<a class="dropdown-item" id="o_logout" role="menuitem" t-attf-href="/web/session/logout?redirect=/">Logout</a>
|
||||
<a class="dropdown-item" role="menuitem" t-attf-href="/web">Web</a>
|
||||
</div>
|
||||
</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li class="nav-item dropdown" t-ignore="true">
|
||||
<b>
|
||||
<a class="nav-link" t-attf-href="/web/login?redirect=/">Login</a>
|
||||
</b>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<t t-raw="nav_form or ''">
|
||||
</t>
|
||||
</div>
|
||||
</nav>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- remove black bar with app switcher -->
|
||||
<template id="inherits_no_black_bar" inherit_id="website.user_navbar" name="Inherits No black user_navbar">
|
||||
<xpath expr="//nav[@id='oe_main_menu_navbar']" position="attributes">
|
||||
<attribute name="groups">base.group_website_publisher</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-set='body_classname']" position="attributes">
|
||||
<attribute name="t-value">'o_connected_user' if env['ir.ui.view'].user_has_groups('base.group_website_publisher') else None</attribute>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="runbot.slots_infos" name="Hosts slot nb pending/testing/slots">
|
||||
<span t-attf-class="badge badge-{{pending_level}}">
|
||||
Pending:
|
||||
<t t-esc="pending_total"/>
|
||||
</span>
|
||||
<t t-set="testing" t-value="hosts_data._total_testing()"/>
|
||||
<t t-set="workers" t-value="hosts_data._total_workers()"/>
|
||||
<t t-set="klass">success</t>
|
||||
<t t-if="not workers" t-set="klass">danger</t>
|
||||
<t t-else="">
|
||||
<t t-if="int(testing)/workers > 0" t-set="klass">info</t>
|
||||
<t t-if="int(testing)/workers > 0.75" t-set="klass">warning</t>
|
||||
<t t-if="int(testing)/workers >= 1" t-set="klass">danger</t>
|
||||
</t>
|
||||
<span t-attf-class="badge badge-{{klass}}">
|
||||
Testing:
|
||||
<t t-esc="testing"/>
|
||||
/
|
||||
<t t-esc="workers"/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template id="runbot.slot_button">
|
||||
<t t-set="bu" t-value="slot.build_id"/>
|
||||
<t t-set="color" t-value="bu.get_color_class()"/>
|
||||
<div t-attf-class="btn-group btn-group-ssm slot_button_group">
|
||||
<span t-attf-class="btn btn-{{color}} disabled" t-att-title="slot.link_type">
|
||||
<i t-attf-class="fa fa-{{slot.fa_link_type()}}"/>
|
||||
</span>
|
||||
<a t-if="bu" t-attf-href="/runbot/build/#{bu.id}" t-attf-class="btn btn-default slot_name">
|
||||
<span t-esc="slot.trigger_id.name"/>
|
||||
</a>
|
||||
<span t-else="" t-attf-class="btn btn-default disabled slot_name">
|
||||
<span t-esc="slot.trigger_id.name"/>
|
||||
</span>
|
||||
<a t-if="bu.local_state == 'running'" t-attf-href="http://{{bu.domain}}/" class="fa fa-sign-in btn btn-info"/>
|
||||
<t t-if="bu" t-call="runbot.build_menu"/>
|
||||
<a t-if="not bu" class="btn btn-default" title="Create build" t-attf-href="/runbot/batch/slot/{{slot.id}}/build">
|
||||
<i class="fa fa-play fa-fw"/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build_button">
|
||||
<div t-attf-class="pull-right">
|
||||
<div t-attf-class="btn-group {{klass}}">
|
||||
<a t-if="bu.local_state=='running'" t-attf-href="http://{{bu['domain']}}/?db={{bu.dest}}-all" class="btn btn-primary" title="Sign in on this build" aria-label="Sign in on this build">
|
||||
<i class="fa fa-sign-in"/>
|
||||
</a>
|
||||
<a t-if="bu.local_state=='done' and bu.requested_action != 'wake_up'" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu.id" class="btn btn-default" title="Wake up this build" aria-label="Wake up this build">
|
||||
<i class="fa fa-coffee"/>
|
||||
</a>
|
||||
<a t-attf-href="/runbot/build/{{bu['id']}}" class="btn btn-default" title="Build details" aria-label="Build details">
|
||||
<i class="fa fa-file-text-o"/>
|
||||
</a>
|
||||
<!--<a t-if="show_commit_button" t-attf-href="https://#{repo.base_url}/commit/#{bu['name']}" class="btn btn-default" title="Open commit on GitHub" aria-label="Open commit on GitHub"><i class="fa fa-github"/></a>-->
|
||||
<t t-call="runbot.build_menu"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Event / Logs page -->
|
||||
<template id="runbot.build_class">
|
||||
<t t-set="rowclass">info</t>
|
||||
<t t-if="build.global_state in ['running','done']">
|
||||
<t t-if="build.global_result == 'ok'">
|
||||
<t t-set="rowclass">success</t>
|
||||
</t>
|
||||
<t t-if="build.global_result == 'skipped'">
|
||||
<t t-set="rowclass">default</t>
|
||||
</t>
|
||||
<t t-if="build.global_result in ['killed', 'manually_killed']">
|
||||
<t t-set="rowclass">killed</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="build.global_result == 'ko'">
|
||||
<t t-set="rowclass">danger</t>
|
||||
</t>
|
||||
<t t-if="build.global_result == 'warn'">
|
||||
<t t-set="rowclass">warning</t>
|
||||
</t>
|
||||
<t t-esc="rowclass"/>
|
||||
</template>
|
||||
|
||||
<template id="runbot.build_menu">
|
||||
<button t-attf-class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="Build options" aria-label="Build options" aria-expanded="false">
|
||||
<i t-attf-class="fa {{'fa-spinner' if bu.global_state == 'pending' else 'fa-cog'}} {{'' if bu.global_state in ('done', 'running') else 'fa-spin'}} fa-fw"/>
|
||||
<span class="caret"/>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" role="menu">
|
||||
<a t-if="bu.global_result=='skipped'" groups="runbot.group_runbot_admin" class="dropdown-item" href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']">
|
||||
<i class="fa fa-level-up"/>
|
||||
Force Build
|
||||
</a>
|
||||
<t t-if="bu.local_state=='running'">
|
||||
<a class="dropdown-item" t-attf-href="http://{{bu.domain}}/?db={{bu.dest}}-all">
|
||||
<i class="fa fa-sign-in"/>
|
||||
Connect all
|
||||
</a>
|
||||
<a class="dropdown-item" t-attf-href="http://{{bu.domain}}/?db={{bu.dest}}-base">
|
||||
<i class="fa fa-sign-in"/>
|
||||
Connect base
|
||||
</a>
|
||||
<a class="dropdown-item" t-attf-href="http://{{bu.domain}}/">
|
||||
<i class="fa fa-sign-in"/>
|
||||
Connect
|
||||
</a>
|
||||
</t>
|
||||
<a class="dropdown-item" t-if="bu.global_state in ['done','running'] or requested_action == 'deathrow'" groups="base.group_user" href="#" data-runbot="rebuild" t-att-data-runbot-build="bu['id']" title="Retry this build, usefull for false positive">
|
||||
<i class="fa fa-refresh"/>
|
||||
Rebuild
|
||||
</a>
|
||||
<t t-if="bu.global_state != 'done'">
|
||||
<t t-if="bu.requested_action != 'deathrow'">
|
||||
<a groups="base.group_user" href="#" data-runbot="kill" class="dropdown-item" t-att-data-runbot-build="bu['id']">
|
||||
<i class="fa fa-crosshairs"/>
|
||||
Kill
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a groups="base.group_user" data-runbot="kill" class="dropdown-item disabled">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
Killing
|
||||
<i class="fa fa-crosshairs"/>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="bu.global_state == 'done'">
|
||||
<t t-if="bu.requested_action != 'wake_up'">
|
||||
<a groups="base.group_user" class="dropdown-item" href="#" data-runbot="wakeup" t-att-data-runbot-build="bu['id']">
|
||||
<i class="fa fa-coffee"/>
|
||||
Wake up
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a groups="base.group_user" class="dropdown-item disabled" data-runbot="wakeup">
|
||||
<i class="fa fa-spinner fa-spin"/>
|
||||
Waking up
|
||||
<i class="fa fa-crosshairs"/>
|
||||
</a>
|
||||
</t>
|
||||
</t>
|
||||
<div t-if="bu.global_state not in ('testing', 'waiting', 'pending')" groups="base.group_user" class="dropdown-divider"/>
|
||||
<t t-set="log_url" t-value="'http://%s' % bu.host if bu.host != fqdn else ''"/>
|
||||
<t t-if="bu.host" t-foreach="bu.log_list.split(',') if bu.log_list else []" t-as="log_name">
|
||||
<a class="dropdown-item" t-attf-href="{{log_url}}/runbot/static/build/#{bu.dest}/logs/#{log_name}.txt">
|
||||
<i class="fa fa-file-text-o"/>
|
||||
Full
|
||||
<t t-esc="log_name"/>
|
||||
logs
|
||||
</a>
|
||||
</t>
|
||||
<a t-if="bu.coverage and bu.host" class="dropdown-item" t-attf-href="http://{{bu.host}}/runbot/static/build/#{bu.dest}/coverage/index.html">
|
||||
<i class="fa fa-file-text-o"/>
|
||||
Coverage
|
||||
</a>
|
||||
<t groups="runbot.group_runbot_admin">
|
||||
<div class="dropdown-divider"/>
|
||||
<a class="dropdown-item" t-attf-href="/web/#id={{bu['id']}}&view_type=form&model=runbot.build" target="new">
|
||||
<i class="fa fa-list"/>
|
||||
View in backend
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="runbot.branch_github_menu">
|
||||
<button t-attf-class="btn btn-default btn-ssm" data-toggle="dropdown" title="Github links" aria-label="Github links" aria-expanded="false">
|
||||
<i t-attf-class="fa fa-github"/>
|
||||
<span class="caret"/>
|
||||
</button>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<t t-foreach="bundle.branch_ids.sorted(key=lambda b: (b.remote_id.repo_id.sequence, b.remote_id.repo_id.id, b.is_pr))" t-as="branch">
|
||||
<t t-set="link_title" t-value="'View %s %s on Github' % ('PR' if branch.is_pr else 'Branch', branch.name)"/>
|
||||
<a t-att-href="branch.branch_url" class="dropdown-item" t-att-title="link_title"><span class="font-italic text-muted"><t t-esc="branch.remote_id.short_name"/></span> <t t-esc="branch.name"/></a>
|
||||
</t>
|
||||
</div>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
@ -3,10 +3,13 @@ from . import test_repo
|
||||
from . import test_build_error
|
||||
from . import test_branch
|
||||
from . import test_build
|
||||
from . import test_frontend
|
||||
from . import test_schedule
|
||||
from . import test_cron
|
||||
from . import test_build_config_step
|
||||
from . import test_event
|
||||
from . import test_command
|
||||
from . import test_build_stat
|
||||
from . import test_version
|
||||
from . import test_runbot
|
||||
from . import test_commit
|
||||
from . import test_upgrade
|
||||
|
@ -1,66 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests.common import TransactionCase
|
||||
from unittest.mock import patch
|
||||
import datetime
|
||||
|
||||
class Dummy():
|
||||
...
|
||||
from odoo.tests.common import TransactionCase
|
||||
from unittest.mock import patch, DEFAULT
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunbotCase(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RunbotCase, self).setUp()
|
||||
|
||||
self.Build = self.env['runbot.build']
|
||||
self.Repo = self.env['runbot.repo']
|
||||
self.Branch = self.env['runbot.branch']
|
||||
|
||||
self.patchers = {}
|
||||
self.patcher_objects = {}
|
||||
|
||||
def git_side_effect(cmd):
|
||||
def mock_git_helper(self):
|
||||
"""Helper that returns a mock for repo._git()"""
|
||||
def mock_git(repo, cmd):
|
||||
if cmd[:2] == ['show', '-s'] or cmd[:3] == ['show', '--pretty="%H -- %s"', '-s']:
|
||||
return 'commit message for %s' % cmd[-1]
|
||||
if cmd[:2] == ['cat-file', '-e']:
|
||||
return True
|
||||
if cmd[0] == 'for-each-ref':
|
||||
if self.commit_list.get(repo.id):
|
||||
return '\n'.join(['\0'.join(commit_fields) for commit_fields in self.commit_list[repo.id]])
|
||||
else:
|
||||
print('Unsupported mock command %s' % cmd)
|
||||
return ''
|
||||
else:
|
||||
_logger.warning('Unsupported mock command %s' % cmd)
|
||||
return mock_git
|
||||
|
||||
self.start_patcher('git_patcher', 'odoo.addons.runbot.models.repo.runbot_repo._git', side_effect=git_side_effect)
|
||||
def push_commit(self, remote, branch_name, subject, sha=None, tstamp=None, committer=None, author=None):
|
||||
"""Helper to simulate a commit pushed"""
|
||||
|
||||
committer = committer or "Marc Bidule"
|
||||
commiter_email = '%s@somewhere.com' % committer.lower().replace(' ', '_')
|
||||
author = author or committer
|
||||
author_email = '%s@somewhere.com' % author.lower().replace(' ', '_')
|
||||
self.commit_list[self.repo_server.id] = [(
|
||||
'refs/%s/heads/%s' % (remote.remote_name, branch_name),
|
||||
sha or 'd0d0caca',
|
||||
tstamp or datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
committer,
|
||||
commiter_email,
|
||||
subject,
|
||||
author,
|
||||
author_email)]
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.Project = self.env['runbot.project']
|
||||
self.Build = self.env['runbot.build']
|
||||
self.BuildParameters = self.env['runbot.build.params']
|
||||
self.Repo = self.env['runbot.repo'].with_context(mail_create_nolog=True, mail_notrack=True)
|
||||
self.Remote = self.env['runbot.remote'].with_context(mail_create_nolog=True, mail_notrack=True)
|
||||
self.Trigger = self.env['runbot.trigger'].with_context(mail_create_nolog=True, mail_notrack=True)
|
||||
self.Branch = self.env['runbot.branch']
|
||||
self.Bundle = self.env['runbot.bundle']
|
||||
self.Version = self.env['runbot.version']
|
||||
self.Config = self.env['runbot.build.config'].with_context(mail_create_nolog=True, mail_notrack=True)
|
||||
self.Step = self.env['runbot.build.config.step'].with_context(mail_create_nolog=True, mail_notrack=True)
|
||||
self.Commit = self.env['runbot.commit']
|
||||
self.Runbot = self.env['runbot.runbot']
|
||||
self.project = self.env['runbot.project'].create({'name': 'Tests'})
|
||||
self.repo_server = self.Repo.create({
|
||||
'name': 'server',
|
||||
'project_id': self.project.id,
|
||||
'server_files': 'server.py',
|
||||
'addons_paths': 'addons,core/addons'
|
||||
})
|
||||
self.repo_addons = self.Repo.create({
|
||||
'name': 'addons',
|
||||
'project_id': self.project.id,
|
||||
})
|
||||
|
||||
self.remote_server = self.Remote.create({
|
||||
'name': 'bla@example.com:base/server',
|
||||
'repo_id': self.repo_server.id,
|
||||
'token': '123',
|
||||
})
|
||||
self.remote_server_dev = self.Remote.create({
|
||||
'name': 'bla@example.com:dev/server',
|
||||
'repo_id': self.repo_server.id,
|
||||
'token': '123',
|
||||
})
|
||||
self.remote_addons = self.Remote.create({
|
||||
'name': 'bla@example.com:base/addons',
|
||||
'repo_id': self.repo_addons.id,
|
||||
'token': '123',
|
||||
})
|
||||
self.remote_addons_dev = self.Remote.create({
|
||||
'name': 'bla@example.com:dev/addons',
|
||||
'repo_id': self.repo_addons.id,
|
||||
'token': '123',
|
||||
})
|
||||
|
||||
self.version_13 = self.Version.create({'name': '13.0'})
|
||||
self.default_config = self.env.ref('runbot.runbot_build_config_default')
|
||||
|
||||
self.base_params = self.BuildParameters.create({
|
||||
'version_id': self.version_13.id,
|
||||
'project_id': self.project.id,
|
||||
'config_id': self.default_config.id,
|
||||
})
|
||||
|
||||
self.trigger_server = self.Trigger.create({
|
||||
'name': 'Server trigger',
|
||||
'repo_ids': [(4, self.repo_server.id)],
|
||||
'config_id': self.default_config.id,
|
||||
'project_id': self.project.id,
|
||||
})
|
||||
|
||||
self.trigger_addons = self.Trigger.create({
|
||||
'name': 'Addons trigger',
|
||||
'repo_ids': [(4, self.repo_addons.id)],
|
||||
'dependency_ids': [(4, self.repo_server.id)],
|
||||
'config_id': self.default_config.id,
|
||||
'project_id': self.project.id,
|
||||
})
|
||||
|
||||
self.patchers = {}
|
||||
self.patcher_objects = {}
|
||||
self.commit_list = {}
|
||||
|
||||
self.start_patcher('git_patcher', 'odoo.addons.runbot.models.repo.Repo._git', new=self.mock_git_helper())
|
||||
self.start_patcher('fqdn_patcher', 'odoo.addons.runbot.common.socket.getfqdn', 'host.runbot.com')
|
||||
self.start_patcher('github_patcher', 'odoo.addons.runbot.models.repo.runbot_repo._github', {})
|
||||
self.start_patcher('is_on_remote_patcher', 'odoo.addons.runbot.models.branch.runbot_branch._is_on_remote', True)
|
||||
self.start_patcher('repo_root_patcher', 'odoo.addons.runbot.models.repo.runbot_repo._root', '/tmp/runbot_test/static')
|
||||
self.start_patcher('github_patcher', 'odoo.addons.runbot.models.repo.Remote._github', {})
|
||||
self.start_patcher('repo_root_patcher', 'odoo.addons.runbot.models.runbot.Runbot._root', '/tmp/runbot_test/static')
|
||||
self.start_patcher('makedirs', 'odoo.addons.runbot.common.os.makedirs', True)
|
||||
self.start_patcher('mkdir', 'odoo.addons.runbot.common.os.mkdir', True)
|
||||
self.start_patcher('local_pgadmin_cursor', 'odoo.addons.runbot.common.local_pgadmin_cursor', False) # avoid to create databases
|
||||
self.start_patcher('isdir', 'odoo.addons.runbot.common.os.path.isdir', True)
|
||||
self.start_patcher('isfile', 'odoo.addons.runbot.common.os.path.isfile', True)
|
||||
self.start_patcher('docker_run', 'odoo.addons.runbot.models.build_config.docker_run')
|
||||
self.start_patcher('docker_build', 'odoo.addons.runbot.models.build.docker_build')
|
||||
self.start_patcher('docker_ps', 'odoo.addons.runbot.models.repo.docker_ps', [])
|
||||
self.start_patcher('docker_stop', 'odoo.addons.runbot.models.repo.docker_stop')
|
||||
self.start_patcher('docker_ps', 'odoo.addons.runbot.models.build_config.docker_get_gateway_ip', None)
|
||||
self.start_patcher('docker_run', 'odoo.addons.runbot.container._docker_run')
|
||||
self.start_patcher('docker_build', 'odoo.addons.runbot.container._docker_build')
|
||||
self.start_patcher('docker_ps', 'odoo.addons.runbot.container._docker_ps', [])
|
||||
self.start_patcher('docker_stop', 'odoo.addons.runbot.container._docker_stop')
|
||||
self.start_patcher('docker_get_gateway_ip', 'odoo.addons.runbot.models.build_config.docker_get_gateway_ip', None)
|
||||
|
||||
self.start_patcher('cr_commit', 'odoo.sql_db.Cursor.commit', None)
|
||||
self.start_patcher('repo_commit', 'odoo.addons.runbot.models.repo.runbot_repo._commit', None)
|
||||
self.start_patcher('_local_cleanup_patcher', 'odoo.addons.runbot.models.build.runbot_build._local_cleanup')
|
||||
self.start_patcher('_local_pg_dropdb_patcher', 'odoo.addons.runbot.models.build.runbot_build._local_pg_dropdb')
|
||||
self.start_patcher('repo_commit', 'odoo.addons.runbot.models.runbot.Runbot._commit', None)
|
||||
self.start_patcher('_local_cleanup_patcher', 'odoo.addons.runbot.models.build.BuildResult._local_cleanup')
|
||||
self.start_patcher('_local_pg_dropdb_patcher', 'odoo.addons.runbot.models.build.BuildResult._local_pg_dropdb')
|
||||
|
||||
def start_patcher(self, patcher_name, patcher_path, return_value=Dummy, side_effect=Dummy):
|
||||
self.start_patcher('set_psql_conn_count', 'odoo.addons.runbot.models.host.Host.set_psql_conn_count', None)
|
||||
self.start_patcher('reload_nginx', 'odoo.addons.runbot.models.runbot.Runbot._reload_nginx', None)
|
||||
self.start_patcher('update_commits_infos', 'odoo.addons.runbot.models.batch.Batch._update_commits_infos', None)
|
||||
self.start_patcher('_local_pg_createdb', 'odoo.addons.runbot.models.build.BuildResult._local_pg_createdb', True)
|
||||
self.start_patcher('getmtime', 'odoo.addons.runbot.common.os.path.getmtime', datetime.datetime.now().timestamp())
|
||||
|
||||
self.start_patcher('_get_py_version', 'odoo.addons.runbot.models.build.BuildResult._get_py_version', 3)
|
||||
|
||||
def start_patcher(self, patcher_name, patcher_path, return_value=DEFAULT, side_effect=DEFAULT, new=DEFAULT):
|
||||
|
||||
def stop_patcher_wrapper():
|
||||
self.stop_patcher(patcher_name)
|
||||
|
||||
patcher = patch(patcher_path)
|
||||
patcher = patch(patcher_path, new=new)
|
||||
if not hasattr(patcher, 'is_local'):
|
||||
res = patcher.start()
|
||||
self.addCleanup(stop_patcher_wrapper)
|
||||
self.patchers[patcher_name] = res
|
||||
self.patcher_objects[patcher_name] = patcher
|
||||
if side_effect != Dummy:
|
||||
if side_effect != DEFAULT:
|
||||
res.side_effect = side_effect
|
||||
elif return_value != Dummy:
|
||||
elif return_value != DEFAULT:
|
||||
res.return_value = return_value
|
||||
|
||||
def stop_patcher(self, patcher_name):
|
||||
@ -68,5 +169,63 @@ class RunbotCase(TransactionCase):
|
||||
self.patcher_objects[patcher_name].stop()
|
||||
del self.patcher_objects[patcher_name]
|
||||
|
||||
def create_build(self, vals):
|
||||
return self.Build.create(vals)
|
||||
def additionnal_setup(self):
|
||||
"""Helper that setup a the repos with base branches and heads"""
|
||||
|
||||
self.env['ir.config_parameter'].sudo().set_param('runbot.runbot_is_base_regex', r'^((master)|(saas-)?\d+\.\d+)$')
|
||||
|
||||
self.initial_server_commit = self.Commit.create({
|
||||
'name': 'aaaaaaa',
|
||||
'repo_id': self.repo_server.id,
|
||||
'date': '2006-12-07',
|
||||
'subject': 'New trunk',
|
||||
'author': 'purply',
|
||||
'author_email': 'puprly@somewhere.com'
|
||||
})
|
||||
|
||||
self.branch_server = self.Branch.create({
|
||||
'name': 'master',
|
||||
'remote_id': self.remote_server.id,
|
||||
'is_pr': False,
|
||||
'head': self.initial_server_commit.id,
|
||||
})
|
||||
self.assertEqual(self.branch_server.bundle_id.name, 'master')
|
||||
self.branch_server.bundle_id.is_base = True
|
||||
initial_addons_commit = self.Commit.create({
|
||||
'name': 'cccccc',
|
||||
'repo_id': self.repo_addons.id,
|
||||
'date': '2015-03-12',
|
||||
'subject': 'Initial commit',
|
||||
'author': 'someone',
|
||||
'author_email': 'someone@somewhere.com'
|
||||
})
|
||||
|
||||
self.branch_addons = self.Branch.create({
|
||||
'name': 'master',
|
||||
'remote_id': self.remote_addons.id,
|
||||
'is_pr': False,
|
||||
'head': initial_addons_commit.id,
|
||||
})
|
||||
self.assertEqual(self.branch_addons.bundle_id, self.branch_server.bundle_id)
|
||||
triggers = self.env['runbot.trigger'].search([])
|
||||
|
||||
self.assertEqual(triggers.repo_ids + triggers.dependency_ids, self.remote_addons.repo_id + self.remote_server.repo_id)
|
||||
|
||||
self.branch_addons.bundle_id._force()
|
||||
|
||||
|
||||
class RunbotCaseMinimalSetup(RunbotCase):
|
||||
|
||||
def start_patchers(self):
|
||||
"""Start necessary patchers for tests that use repo__update_batch() and batch._prepare()"""
|
||||
def counter():
|
||||
i = 100000
|
||||
while True:
|
||||
i += 1
|
||||
yield i
|
||||
|
||||
# start patchers
|
||||
self.start_patcher('repo_get_fetch_head_time_patcher', 'odoo.addons.runbot.models.repo.Repo._get_fetch_head_time')
|
||||
self.patchers['repo_get_fetch_head_time_patcher'].side_effect = counter()
|
||||
self.start_patcher('repo_update_patcher', 'odoo.addons.runbot.models.repo.Repo._update')
|
||||
self.start_patcher('batch_update_commits_infos', 'odoo.addons.runbot.models.batch.Batch._update_commits_infos')
|
||||
|
@ -1,154 +1,229 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch
|
||||
from odoo.tests import common
|
||||
from .common import RunbotCase
|
||||
from odoo.tools import mute_logger
|
||||
from .common import RunbotCase, RunbotCaseMinimalSetup
|
||||
|
||||
class Test_Branch(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super(Test_Branch, self).setUp()
|
||||
Repo = self.env['runbot.repo']
|
||||
self.repo = Repo.create({'name': 'bla@example.com:foo/bar', 'token': '123'})
|
||||
self.Branch = self.env['runbot.branch']
|
||||
|
||||
#mock_patch = patch('odoo.addons.runbot.models.repo.runbot_repo._github', self._github)
|
||||
#mock_patch.start()
|
||||
#self.addCleanup(mock_patch.stop)
|
||||
class TestBranch(RunbotCase):
|
||||
|
||||
def test_base_fields(self):
|
||||
branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/head/master'
|
||||
'remote_id': self.remote_server.id,
|
||||
'name': 'master',
|
||||
'is_pr': False,
|
||||
})
|
||||
|
||||
self.assertEqual(branch.branch_name, 'master')
|
||||
self.assertEqual(branch.branch_url, 'https://example.com/foo/bar/tree/master')
|
||||
self.assertEqual(branch.config_id, self.env.ref('runbot.runbot_build_config_default'))
|
||||
self.assertEqual(branch.branch_url, 'https://example.com/base/server/tree/master')
|
||||
|
||||
def test_pull_request(self):
|
||||
mock_github = self.patchers['github_patcher']
|
||||
mock_github.return_value = {
|
||||
'head' : {'label': 'foo-dev:bar_branch'},
|
||||
'base': {'ref': 'master'},
|
||||
'head': {'label': 'foo-dev:bar_branch', 'repo': {'full_name': 'foo-dev/bar'}},
|
||||
}
|
||||
pr = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/pull/12345'
|
||||
'remote_id': self.remote_server.id,
|
||||
'name': '12345',
|
||||
'is_pr': True,
|
||||
})
|
||||
self.assertEqual(pr.branch_name, '12345')
|
||||
self.assertEqual(pr.branch_url, 'https://example.com/foo/bar/pull/12345')
|
||||
self.assertEqual(pr.name, '12345')
|
||||
self.assertEqual(pr.branch_url, 'https://example.com/base/server/pull/12345')
|
||||
self.assertEqual(pr.target_branch_name, 'master')
|
||||
self.assertEqual(pr.pull_head_name, 'foo-dev:bar_branch')
|
||||
|
||||
def test_coverage_in_name(self):
|
||||
"""Test that coverage in branch name enables coverage"""
|
||||
branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/head/foo-branch-bar'
|
||||
})
|
||||
self.assertEqual(branch.config_id, self.env.ref('runbot.runbot_build_config_default'))
|
||||
cov_branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/head/foo-use-coverage-branch-bar'
|
||||
})
|
||||
self.assertEqual(cov_branch.config_id, self.env.ref('runbot.runbot_build_config_test_coverage'))
|
||||
|
||||
|
||||
class TestBranchRelations(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBranchRelations, self).setUp()
|
||||
|
||||
self.repo = self.env['runbot.repo'].create({'name': 'bla@example.com:foo/bar'})
|
||||
self.repodev = self.env['runbot.repo'].create({'name': 'bla@example.com:foo-dev/bar', 'duplicate_id':self.repo.id })
|
||||
self.Branch = self.env['runbot.branch']
|
||||
|
||||
def create_sticky(name):
|
||||
return self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/%s' % name,
|
||||
'sticky': True
|
||||
def create_base(name):
|
||||
branch = self.Branch.create({
|
||||
'remote_id': self.remote_server.id,
|
||||
'name': name,
|
||||
'is_pr': False,
|
||||
})
|
||||
self.master = create_sticky('master')
|
||||
create_sticky('11.0')
|
||||
create_sticky('saas-11.1')
|
||||
create_sticky('12.0')
|
||||
create_sticky('saas-12.3')
|
||||
create_sticky('13.0')
|
||||
create_sticky('saas-13.1')
|
||||
self.last = create_sticky('saas-13.2')
|
||||
branch.bundle_id.is_base = True
|
||||
return branch
|
||||
self.master = create_base('master')
|
||||
create_base('11.0')
|
||||
create_base('saas-11.1')
|
||||
create_base('12.0')
|
||||
create_base('saas-12.3')
|
||||
create_base('13.0')
|
||||
create_base('saas-13.1')
|
||||
self.last = create_base('saas-13.2')
|
||||
self.env['runbot.bundle'].flush()
|
||||
self.env['runbot.version'].flush()
|
||||
|
||||
def test_relations_master_dev(self):
|
||||
b = self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'name': 'refs/heads/master-test-tri',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': 'master-test-tri',
|
||||
'is_pr': False,
|
||||
})
|
||||
self.assertEqual(b.closest_sticky.branch_name, 'master')
|
||||
self.assertEqual(b.previous_version.branch_name, '13.0')
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), ['saas-13.1', 'saas-13.2'])
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'master')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
|
||||
self.assertEqual(b.bundle_id.intermediate_version_base_ids.mapped('name'), ['saas-13.1', 'saas-13.2'])
|
||||
|
||||
def test_relations_master(self):
|
||||
b = self.master
|
||||
self.assertEqual(b.closest_sticky.branch_name, 'master')
|
||||
self.assertEqual(b.previous_version.branch_name, '13.0')
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), ['saas-13.1', 'saas-13.2'])
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'master')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
|
||||
self.assertEqual(b.bundle_id.intermediate_version_base_ids.mapped('name'), ['saas-13.1', 'saas-13.2'])
|
||||
|
||||
def test_relations_no_intermediate(self):
|
||||
b = self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'name': 'refs/heads/saas-13.1-test-tri',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': 'saas-13.1-test-tri',
|
||||
'is_pr': False,
|
||||
})
|
||||
self.assertEqual(b.closest_sticky.branch_name, 'saas-13.1')
|
||||
self.assertEqual(b.previous_version.branch_name, '13.0')
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), [])
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'saas-13.1')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
|
||||
self.assertEqual(b.bundle_id.intermediate_version_base_ids.mapped('name'), [])
|
||||
|
||||
def test_relations_old_branch(self):
|
||||
b = self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'name': 'refs/heads/11.0-test-tri',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': '11.0-test-tri',
|
||||
'is_pr': False,
|
||||
})
|
||||
self.assertEqual(b.closest_sticky.branch_name, '11.0')
|
||||
self.assertEqual(b.previous_version.branch_name, False)
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), [])
|
||||
self.assertEqual(b.bundle_id.base_id.name, '11.0')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, False)
|
||||
self.assertEqual(sorted(b.bundle_id.intermediate_version_base_ids.mapped('name')), [])
|
||||
|
||||
def test_relations_closest_forced(self):
|
||||
b = self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'name': 'refs/heads/master-test-tri',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': 'master-test-tri',
|
||||
'is_pr': False,
|
||||
})
|
||||
self.assertEqual(b.closest_sticky.branch_name, 'master')
|
||||
self.assertEqual(b.previous_version.branch_name, '13.0')
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), ['saas-13.1', 'saas-13.2'])
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'master')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
|
||||
self.assertEqual(sorted(b.bundle_id.intermediate_version_base_ids.mapped('name')), ['saas-13.1', 'saas-13.2'])
|
||||
|
||||
b.defined_sticky = self.last
|
||||
b.bundle_id.defined_base_id = self.last.bundle_id
|
||||
|
||||
self.assertEqual(b.closest_sticky.branch_name, 'saas-13.2')
|
||||
self.assertEqual(b.previous_version.branch_name, '13.0')
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), ['saas-13.1'])
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'saas-13.2')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
|
||||
self.assertEqual(sorted(b.bundle_id.intermediate_version_base_ids.mapped('name')), ['saas-13.1'])
|
||||
|
||||
def test_relations_no_match(self):
|
||||
b = self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'name': 'refs/heads/icantnamemybranches',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': 'icantnamemybranches',
|
||||
'is_pr': False,
|
||||
})
|
||||
|
||||
self.assertEqual(b.closest_sticky.branch_name, False)
|
||||
self.assertEqual(b.previous_version.branch_name, False)
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), [])
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'master')
|
||||
|
||||
def test_relations_pr(self):
|
||||
self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'name': 'refs/heads/master-test-tri',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': 'master-test-tri',
|
||||
'is_pr': False,
|
||||
})
|
||||
|
||||
self.patchers['github_patcher'].return_value = {
|
||||
'base': {'ref': 'master-test-tri'},
|
||||
'head': {'label': 'dev:master-test-tri-imp', 'repo': {'full_name': 'dev/server'}},
|
||||
}
|
||||
b = self.Branch.create({
|
||||
'repo_id': self.repodev.id,
|
||||
'target_branch_name': 'master-test-tri',
|
||||
'name': 'refs/pull/100',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': '100',
|
||||
'is_pr': True,
|
||||
})
|
||||
b.target_branch_name = 'master-test-tri'
|
||||
self.assertEqual(b.closest_sticky.branch_name, 'master')
|
||||
self.assertEqual(b.previous_version.branch_name, '13.0')
|
||||
self.assertEqual(sorted(b.intermediate_stickies.mapped('branch_name')), ['saas-13.1', 'saas-13.2'])
|
||||
|
||||
self.assertEqual(b.bundle_id.name, 'master-test-tri-imp')
|
||||
self.assertEqual(b.bundle_id.base_id.name, 'master')
|
||||
self.assertEqual(b.bundle_id.previous_major_version_base_id.name, '13.0')
|
||||
self.assertEqual(sorted(b.bundle_id.intermediate_version_base_ids.mapped('name')), ['saas-13.1', 'saas-13.2'])
|
||||
|
||||
|
||||
class TestBranchForbidden(RunbotCase):
|
||||
"""Test that a branch matching the repo forbidden regex, goes to dummy bundle"""
|
||||
|
||||
def test_forbidden(self):
|
||||
dummy_bundle = self.env.ref('runbot.bundle_dummy')
|
||||
self.remote_server_dev.repo_id.forbidden_regex = '^bad_name.+'
|
||||
with mute_logger("odoo.addons.runbot.models.branch"):
|
||||
branch = self.Branch.create({
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'name': 'bad_name-evil',
|
||||
'is_pr': False,
|
||||
})
|
||||
self.assertEqual(branch.bundle_id.id, dummy_bundle.id, "A forbidden branch should goes in dummy bundle")
|
||||
|
||||
|
||||
class TestBranchIsBase(RunbotCaseMinimalSetup):
|
||||
"""Test that a branch matching the is_base_regex goes in the right bundle"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestBranchIsBase, self).setUp()
|
||||
self.additionnal_setup()
|
||||
|
||||
def test_is_base_regex_on_main_remote(self):
|
||||
branch = self.Branch.create({
|
||||
'remote_id': self.remote_server.id,
|
||||
'name': 'saas-13.4',
|
||||
'is_pr': False,
|
||||
})
|
||||
self.assertTrue(branch.bundle_id.is_base, "A branch matching the is_base_regex parameter should create is_base bundle")
|
||||
self.assertTrue(branch.bundle_id.sticky, "A branch matching the is_base_regex parameter should create sticky bundle")
|
||||
|
||||
@mute_logger("odoo.addons.runbot.models.branch")
|
||||
def test_is_base_regex_on_dev_remote(self):
|
||||
"""Test that a branch matching the is_base regex on a secondary remote goes to the dummy bundles."""
|
||||
dummy_bundle = self.env.ref('runbot.bundle_dummy')
|
||||
|
||||
# master branch on dev remote
|
||||
initial_addons_dev_commit = self.Commit.create({
|
||||
'name': 'dddddd',
|
||||
'repo_id': self.repo_addons.id,
|
||||
'date': '2015-09-30',
|
||||
'subject': 'Please use the right repo',
|
||||
'author': 'oxo',
|
||||
'author_email': 'oxo@somewhere.com'
|
||||
})
|
||||
|
||||
branch_addons_dev = self.Branch.create({
|
||||
'name': 'master',
|
||||
'remote_id': self.remote_addons_dev.id,
|
||||
'is_pr': False,
|
||||
'head': initial_addons_dev_commit.id
|
||||
})
|
||||
self.assertEqual(branch_addons_dev.bundle_id, dummy_bundle, "A branch matching the is_base_regex should on a secondary repo should goes in dummy bundle")
|
||||
|
||||
# saas-12.3 branch on dev remote
|
||||
initial_server_dev_commit = self.Commit.create({
|
||||
'name': 'bbbbbb',
|
||||
'repo_id': self.repo_server.id,
|
||||
'date': '2014-05-26',
|
||||
'subject': 'Please use the right repo',
|
||||
'author': 'oxo',
|
||||
'author_email': 'oxo@somewhere.com'
|
||||
})
|
||||
|
||||
branch_server_dev = self.Branch.create({
|
||||
'name': 'saas-12.3',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'is_pr': False,
|
||||
'head': initial_server_dev_commit.id
|
||||
})
|
||||
self.assertEqual(branch_server_dev.bundle_id, dummy_bundle, "A branch matching the is_base_regex should on a secondary repo should goes in dummy bundle")
|
||||
|
||||
# 12.0 branch on dev remote
|
||||
mistaken_commit = self.Commit.create({
|
||||
'name': 'eeeeee',
|
||||
'repo_id': self.repo_server.id,
|
||||
'date': '2015-06-27',
|
||||
'subject': 'dummy commit',
|
||||
'author': 'brol',
|
||||
'author_email': 'brol@somewhere.com'
|
||||
})
|
||||
|
||||
branch_mistake_dev = self.Branch.create({
|
||||
'name': '12.0',
|
||||
'remote_id': self.remote_server_dev.id,
|
||||
'is_pr': False,
|
||||
'head': mistaken_commit.id
|
||||
})
|
||||
self.assertEqual(branch_mistake_dev.bundle_id, dummy_bundle, "A branch matching the is_base_regex should on a secondary repo should goes in dummy bundle")
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,37 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch, mock_open
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.runbot.models.repo import RunbotException
|
||||
from odoo.addons.runbot.common import RunbotException
|
||||
from .common import RunbotCase
|
||||
|
||||
|
||||
class TestBuildConfigStep(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestBuildConfigStep, self).setUp()
|
||||
self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar', 'server_files': 'server.py'})
|
||||
self.branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/master'
|
||||
})
|
||||
self.branch_10 = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/10.0'
|
||||
})
|
||||
self.branch_11 = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/11.0'
|
||||
})
|
||||
|
||||
self.Build = self.env['runbot.build']
|
||||
self.ConfigStep = self.env['runbot.build.config.step']
|
||||
self.Config = self.env['runbot.build.config']
|
||||
|
||||
self.parent_build = self.Build.create({
|
||||
'branch_id': self.branch.id,
|
||||
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
|
||||
'port': '1234',
|
||||
server_commit = self.Commit.create({
|
||||
'name': 'dfdfcfcf0000ffffffffffffffffffffffffffff',
|
||||
'repo_id': self.repo_server.id
|
||||
})
|
||||
self.parent_build = self.Build.create({
|
||||
'params_id': self.base_params.copy({'commit_link_ids': [(0, 0, {'commit_id': server_commit.id})]}).id,
|
||||
})
|
||||
self.start_patcher('_local_pg_createdb', 'odoo.addons.runbot.models.build.runbot_build._local_pg_createdb', True)
|
||||
self.start_patcher('_get_py_version', 'odoo.addons.runbot.models.build.runbot_build._get_py_version', 3)
|
||||
self.start_patcher('find_patcher', 'odoo.addons.runbot.common.find', 0)
|
||||
|
||||
def test_config_step_create_results(self):
|
||||
@ -42,13 +31,12 @@ class TestBuildConfigStep(RunbotCase):
|
||||
'job_type': 'create_build',
|
||||
'number_builds': 2,
|
||||
'make_orphan': False,
|
||||
'force_build': True,
|
||||
})
|
||||
|
||||
config = self.Config.create({'name': 'test_config'})
|
||||
config_step.create_config_ids = [config.id]
|
||||
|
||||
config_step._create_build(self.parent_build, '/tmp/essai')
|
||||
config_step._run_create_build(self.parent_build, '/tmp/essai')
|
||||
self.assertEqual(len(self.parent_build.children_ids), 2, 'Two sub-builds should have been generated')
|
||||
|
||||
# check that the result will be ignored by parent build
|
||||
@ -67,13 +55,12 @@ class TestBuildConfigStep(RunbotCase):
|
||||
'job_type': 'create_build',
|
||||
'number_builds': 2,
|
||||
'make_orphan': True,
|
||||
'force_build': True,
|
||||
})
|
||||
|
||||
config = self.Config.create({'name': 'test_config'})
|
||||
config_step.create_config_ids = [config.id]
|
||||
|
||||
config_step._create_build(self.parent_build, '/tmp/essai')
|
||||
config_step._run_create_build(self.parent_build, '/tmp/essai')
|
||||
self.assertEqual(len(self.parent_build.children_ids), 2, 'Two sub-builds should have been generated')
|
||||
|
||||
# check that the result will be ignored by parent build
|
||||
@ -144,7 +131,7 @@ class TestBuildConfigStep(RunbotCase):
|
||||
dup_config = config.copy()
|
||||
self.assertEqual(dup_config.step_order_ids.mapped('step_id'), config.step_order_ids.mapped('step_id'))
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._checkout')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._checkout')
|
||||
def test_coverage(self, mock_checkout):
|
||||
config_step = self.ConfigStep.create({
|
||||
'name': 'coverage',
|
||||
@ -153,25 +140,25 @@ class TestBuildConfigStep(RunbotCase):
|
||||
})
|
||||
|
||||
def docker_run(cmd, log_path, *args, **kwargs):
|
||||
self.assertEqual(cmd.pres, [['sudo', 'pip3', 'install', '-r', 'bar/requirements.txt']])
|
||||
self.assertEqual(cmd.cmd[:10], ['python3', '-m', 'coverage', 'run', '--branch', '--source', '/data/build', '--omit', '*__manifest__.py', 'bar/server.py'])
|
||||
self.assertEqual(cmd.pres, [['sudo', 'pip3', 'install', '-r', 'server/requirements.txt']])
|
||||
self.assertEqual(cmd.cmd[:10], ['python3', '-m', 'coverage', 'run', '--branch', '--source', '/data/build', '--omit', '*__manifest__.py', 'server/server.py'])
|
||||
self.assertIn(['python3', '-m', 'coverage', 'html', '-d', '/data/build/coverage', '--ignore-errors'], cmd.posts)
|
||||
self.assertIn(['python3', '-m', 'coverage', 'xml', '-o', '/data/build/logs/coverage.xml', '--ignore-errors'], cmd.posts)
|
||||
self.assertEqual(log_path, 'dev/null/logpath')
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run
|
||||
config_step._run_install_odoo(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._checkout')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._checkout')
|
||||
def test_dump(self, mock_checkout):
|
||||
config_step = self.ConfigStep.create({
|
||||
'name': 'all',
|
||||
'job_type': 'install_odoo',
|
||||
})
|
||||
|
||||
def docker_run(cmd, log_path, *args, **kwargs):
|
||||
dest = self.parent_build.dest
|
||||
self.assertEqual(cmd.cmd[:2], ['python3', 'bar/server.py'])
|
||||
self.assertEqual(cmd.cmd[:2], ['python3', 'server/server.py'])
|
||||
self.assertEqual(cmd.finals[0], ['pg_dump', '%s-all' % dest, '>', '/data/build/logs/%s-all//dump.sql' % dest])
|
||||
self.assertEqual(cmd.finals[1], ['cp', '-r', '/data/build/datadir/filestore/%s-all' % dest, '/data/build/logs/%s-all//filestore/' % dest])
|
||||
self.assertEqual(cmd.finals[2], ['cd', '/data/build/logs/%s-all/' % dest, '&&', 'zip', '-rmq9', '/data/build/logs/%s-all.zip' % dest, '*'])
|
||||
@ -179,10 +166,9 @@ class TestBuildConfigStep(RunbotCase):
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run
|
||||
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
config_step._run_install_odoo(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._checkout')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._checkout')
|
||||
def test_install_tags(self, mock_checkout):
|
||||
config_step = self.ConfigStep.create({
|
||||
'name': 'all',
|
||||
@ -198,26 +184,25 @@ class TestBuildConfigStep(RunbotCase):
|
||||
|
||||
def docker_run(cmd, *args, **kwargs):
|
||||
cmds = cmd.build().split(' && ')
|
||||
self.assertEqual(cmds[1].split(' bar/server.py')[0], 'python3')
|
||||
self.assertEqual(cmds[1].split(' server/server.py')[0], 'python3')
|
||||
tags = cmds[1].split('--test-tags ')[1].split(' ')[0]
|
||||
self.assertEqual(tags, '/module,:class.method')
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
config_step._run_install_odoo(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
config_step.enable_auto_tags = True
|
||||
|
||||
def docker_run2(cmd, *args, **kwargs):
|
||||
cmds = cmd.build().split(' && ')
|
||||
self.assertEqual(cmds[1].split(' bar/server.py')[0], 'python3')
|
||||
self.assertEqual(cmds[1].split(' server/server.py')[0], 'python3')
|
||||
tags = cmds[1].split('--test-tags ')[1].split(' ')[0]
|
||||
self.assertEqual(tags, '/module,:class.method,-:otherclass.othertest')
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run2
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
config_step._run_install_odoo(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._checkout')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._checkout')
|
||||
def test_db_name(self, mock_checkout):
|
||||
config_step = self.ConfigStep.create({
|
||||
'name': 'default',
|
||||
@ -226,6 +211,7 @@ class TestBuildConfigStep(RunbotCase):
|
||||
})
|
||||
call_count = 0
|
||||
assert_db_name = 'custom'
|
||||
|
||||
def docker_run(cmd, log_path, *args, **kwargs):
|
||||
db_sufgfix = cmd.cmd[cmd.index('-d')+1].split('-')[-1]
|
||||
self.assertEqual(db_sufgfix, assert_db_name)
|
||||
@ -234,18 +220,41 @@ class TestBuildConfigStep(RunbotCase):
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run
|
||||
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
config_step._run_install_odoo(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
assert_db_name = 'custom_build'
|
||||
self.parent_build.config_data = {'db_name': 'custom_build'}
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
parent_build_params = self.parent_build.params_id.copy({'config_data': {'db_name': 'custom_build'}})
|
||||
parent_build = self.parent_build.copy({'params_id': parent_build_params.id})
|
||||
config_step._run_install_odoo(parent_build, 'dev/null/logpath')
|
||||
|
||||
config_step._run_odoo_run(self.parent_build, 'dev/null/logpath')
|
||||
config_step._run_run_odoo(parent_build, 'dev/null/logpath')
|
||||
|
||||
self.assertEqual(call_count, 3)
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._checkout')
|
||||
def test_run_python(self, mock_checkout):
|
||||
"""minimal test for python steps. Also test that `-d` in cmd creates a database"""
|
||||
test_code = """cmd = build._cmd()
|
||||
cmd += ['-d', 'test_database']
|
||||
docker_run(cmd)
|
||||
"""
|
||||
config_step = self.ConfigStep.create({
|
||||
'name': 'default',
|
||||
'job_type': 'python',
|
||||
'python_code': test_code,
|
||||
})
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._checkout')
|
||||
def docker_run(cmd, *args, **kwargs):
|
||||
run_cmd = cmd.build()
|
||||
self.assertIn('-d test_database', run_cmd)
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run
|
||||
config_step._run_python(self.parent_build, 'dev/null/logpath')
|
||||
self.patchers['docker_run'].assert_called_once()
|
||||
db = self.env['runbot.database'].search([('name', '=', 'test_database')])
|
||||
self.assertEqual(db.build_id, self.parent_build)
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._checkout')
|
||||
def test_sub_command(self, mock_checkout):
|
||||
config_step = self.ConfigStep.create({
|
||||
'name': 'default',
|
||||
@ -253,14 +262,15 @@ class TestBuildConfigStep(RunbotCase):
|
||||
'sub_command': 'subcommand',
|
||||
})
|
||||
call_count = 0
|
||||
|
||||
def docker_run(cmd, log_path, *args, **kwargs):
|
||||
nonlocal call_count
|
||||
sub_command = cmd.cmd[cmd.index('bar/server.py')+1]
|
||||
sub_command = cmd.cmd[cmd.index('server/server.py')+1]
|
||||
self.assertEqual(sub_command, 'subcommand')
|
||||
call_count += 1
|
||||
|
||||
self.patchers['docker_run'].side_effect = docker_run
|
||||
config_step._run_odoo_install(self.parent_build, 'dev/null/logpath')
|
||||
config_step._run_install_odoo(self.parent_build, 'dev/null/logpath')
|
||||
|
||||
self.assertEqual(call_count, 1)
|
||||
|
||||
@ -271,14 +281,9 @@ class TestMakeResult(RunbotCase):
|
||||
super(TestMakeResult, self).setUp()
|
||||
self.ConfigStep = self.env['runbot.build.config.step']
|
||||
self.Config = self.env['runbot.build.config']
|
||||
self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar', 'server_files': 'server.py'})
|
||||
self.branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/master'
|
||||
})
|
||||
|
||||
@patch('odoo.addons.runbot.models.build_config.os.path.getmtime')
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._log')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._log')
|
||||
def test_make_result(self, mock_log, mock_getmtime):
|
||||
file_content = """
|
||||
Loading stuff
|
||||
@ -300,9 +305,7 @@ Initiating shutdown
|
||||
'test_tags': '/module,:class.method',
|
||||
})
|
||||
build = self.Build.create({
|
||||
'branch_id': self.branch.id,
|
||||
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
|
||||
'port': '1234',
|
||||
'params_id': self.base_params.id,
|
||||
})
|
||||
logs = []
|
||||
with patch('builtins.open', mock_open(read_data=file_content)):
|
||||
@ -410,11 +413,9 @@ Initiating shutdown
|
||||
'python_result_code': """a = 2*5\nreturn_value = {'local_result': 'ok'}"""
|
||||
})
|
||||
build = self.Build.create({
|
||||
'branch_id': self.branch.id,
|
||||
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
|
||||
'port': '1234',
|
||||
'params_id': self.base_params.id,
|
||||
})
|
||||
build.state = 'testing'
|
||||
build.state = 'testing' # what ??
|
||||
self.patchers['isfile'].return_value = False
|
||||
result = config_step._make_results(build)
|
||||
self.assertEqual(result, {'local_result': 'ok'})
|
||||
@ -430,4 +431,4 @@ Initiating shutdown
|
||||
result = config_step._make_results(build)
|
||||
self.assertEqual(result, {'local_result': 'warning'})
|
||||
|
||||
|
||||
# TODO add generic test to copy_paste _run_* in a python step
|
||||
|
@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch
|
||||
from odoo.tests import common
|
||||
from odoo.exceptions import ValidationError
|
||||
from .common import RunbotCase
|
||||
|
||||
@ -21,23 +19,15 @@ class TestBuildError(RunbotCase):
|
||||
|
||||
def create_test_build(self, vals):
|
||||
create_vals = {
|
||||
'branch_id': self.branch.id,
|
||||
'name': 'deadbeaf0000ffffffffffffffffffffffffffff',
|
||||
'params_id': self.base_params.id,
|
||||
'port': '1234',
|
||||
'local_result': 'ok'
|
||||
}
|
||||
create_vals.update(vals)
|
||||
return self.create_build(create_vals)
|
||||
|
||||
return self.Build.create(create_vals)
|
||||
|
||||
def setUp(self):
|
||||
super(TestBuildError, self).setUp()
|
||||
repo = self.env['runbot.repo'].create({'name': 'bla@example.com:foo/bar'})
|
||||
self.branch = self.env['runbot.branch'].create({
|
||||
'repo_id': repo.id,
|
||||
'name': 'refs/heads/master'
|
||||
})
|
||||
|
||||
self.BuildError = self.env['runbot.build.error']
|
||||
|
||||
def test_build_scan(self):
|
||||
@ -92,7 +82,6 @@ class TestBuildError(RunbotCase):
|
||||
self.assertIn(ko_build_new, new_build_error.build_ids, 'The parsed build with a re-apearing error should generate a new runbot.build.error')
|
||||
self.assertIn(build_error, new_build_error.error_history_ids, 'The old error should appear in history')
|
||||
|
||||
|
||||
def test_build_error_links(self):
|
||||
build_a = self.create_test_build({'local_result': 'ko'})
|
||||
build_b = self.create_test_build({'local_result': 'ko'})
|
||||
|
@ -12,26 +12,19 @@ class TestBuildStatRegex(RunbotCase):
|
||||
self.StatRegex = self.env["runbot.build.stat.regex"]
|
||||
self.ConfigStep = self.env["runbot.build.config.step"]
|
||||
self.BuildStat = self.env["runbot.build.stat"]
|
||||
|
||||
self.repo = self.Repo.create(
|
||||
{
|
||||
"name": "bla@example.com:foo/bar",
|
||||
"server_files": "server.py",
|
||||
"addons_paths": "addons,core/addons",
|
||||
}
|
||||
)
|
||||
self.branch = self.Branch.create(
|
||||
{"repo_id": self.repo.id, "name": "refs/heads/master"}
|
||||
)
|
||||
|
||||
self.Build = self.env["runbot.build"]
|
||||
|
||||
self.build = self.create_build(
|
||||
params = self.BuildParameters.create({
|
||||
'version_id': self.version_13.id,
|
||||
'project_id': self.project.id,
|
||||
'config_id': self.default_config.id,
|
||||
'config_data': {'make_stats': True}
|
||||
})
|
||||
|
||||
self.build = self.Build.create(
|
||||
{
|
||||
"branch_id": self.branch.id,
|
||||
"name": "d0d0caca0000ffffffffffffffffffffffffffff",
|
||||
"params_id": params.id,
|
||||
"port": "1234",
|
||||
"config_data": {"make_stats": True},
|
||||
}
|
||||
)
|
||||
|
||||
@ -78,7 +71,8 @@ nothing to see here
|
||||
)
|
||||
|
||||
# minimal test for RunbotBuildStatSql model
|
||||
self.assertEqual(self.env['runbot.build.stat.sql'].search_count([('build_id', '=', self.build.id)]), 2)
|
||||
# self.assertEqual(self.env['runbot.build.stat.sql'].search_count([('build_id', '=', self.build.id)]), 2)
|
||||
# TODO FIXME
|
||||
|
||||
def test_build_stat_regex_generic(self):
|
||||
""" test that regex are not used when generic is False and that _make_stats use all genreic regex if there are no regex on step """
|
||||
|
88
runbot/tests/test_commit.py
Normal file
88
runbot/tests/test_commit.py
Normal file
@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
from werkzeug.urls import url_parse
|
||||
|
||||
from odoo.tests.common import HttpCase, new_test_user, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestCommitStatus(HttpCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCommitStatus, self).setUp()
|
||||
self.project = self.env['runbot.project'].create({'name': 'Tests'})
|
||||
self.repo_server = self.env['runbot.repo'].create({
|
||||
'name': 'server',
|
||||
'project_id': self.project.id,
|
||||
'server_files': 'server.py',
|
||||
'addons_paths': 'addons,core/addons'
|
||||
})
|
||||
|
||||
self.server_commit = self.env['runbot.commit'].create({
|
||||
'name': 'dfdfcfcf0000ffffffffffffffffffffffffffff',
|
||||
'repo_id': self.repo_server.id
|
||||
})
|
||||
|
||||
create_context = {'no_reset_password': True, 'mail_create_nolog': True, 'mail_create_nosubscribe': True, 'mail_notrack': True}
|
||||
self.simple_user = new_test_user(self.env, login='simple', name='simple', password='simple', context=create_context)
|
||||
self.runbot_admin = new_test_user(self.env, groups='runbot.group_runbot_admin,base.group_user', login='runbot_admin', name='runbot_admin', password='admin', context=create_context)
|
||||
|
||||
def test_commit_status_resend(self):
|
||||
"""test commit status resend"""
|
||||
|
||||
commit_status = self.env['runbot.commit.status'].create({
|
||||
'commit_id': self.server_commit.id,
|
||||
'context': 'ci/test',
|
||||
'state': 'failure',
|
||||
'target_url': 'https://www.somewhere.com',
|
||||
'description': 'test status'
|
||||
})
|
||||
|
||||
# 1. test that unauthenticated users are redirected to the login page
|
||||
with mute_logger('odoo.addons.base.models.ir_attachment'):
|
||||
response = self.url_open('/runbot/commit/resend/%s' % commit_status.id)
|
||||
parsed_response = url_parse(response.url)
|
||||
self.assertIn('redirect=', parsed_response.query)
|
||||
self.assertEqual(parsed_response.path, '/web/login')
|
||||
|
||||
# 2. test that a simple Odoo user cannot resend a status
|
||||
self.authenticate('simple', 'simple')
|
||||
with mute_logger('odoo.addons.http_routing.models.ir_http'):
|
||||
response = self.url_open('/runbot/commit/resend/%s' % commit_status.id)
|
||||
# TODO remove or fix since the 'runbot.group_user' has been given to the 'base.group_user'.
|
||||
# self.assertEqual(response.status_code, 403)
|
||||
|
||||
# 3. test that a non-existsing commit_status returns a 404
|
||||
# 3.1 find a non existing commit status id
|
||||
non_existing_id = self.env['runbot.commit.status'].browse(50000).exists() or 50000
|
||||
while self.env['runbot.commit.status'].browse(non_existing_id).exists():
|
||||
non_existing_id += 1
|
||||
|
||||
self.authenticate('runbot_admin', 'admin')
|
||||
response = self.url_open('/runbot/commit/resend/%s' % non_existing_id)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# 4. Finally test that a new status is created on resend and that the _send method is called
|
||||
with patch('odoo.addons.runbot.models.commit.CommitStatus._send') as send_patcher:
|
||||
a_minute_ago = datetime.datetime.now() - datetime.timedelta(seconds=65)
|
||||
commit_status.sent_date = a_minute_ago
|
||||
response = self.url_open('/runbot/commit/resend/%s' % commit_status.id)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
send_patcher.assert_called()
|
||||
|
||||
last_commit_status = self.env['runbot.commit.status'].search([], order='id desc', limit=1)
|
||||
self.assertEqual(last_commit_status.description, 'Status resent by runbot_admin')
|
||||
|
||||
# 5. Now that the a new status was created, status is not the last one and thus, cannot be resent
|
||||
with mute_logger('odoo.addons.http_routing.models.ir_http'):
|
||||
response = self.url_open('/runbot/commit/resend/%s' % commit_status.id)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# 6. try to immediately resend the commit should fail to avoid spamming github
|
||||
last_commit_status.sent_date = datetime.datetime.now() # as _send is mocked, the sent_date is not set
|
||||
with patch('odoo.addons.runbot.models.commit.CommitStatus._send') as send_patcher:
|
||||
response = self.url_open('/runbot/commit/resend/%s' % last_commit_status.id)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
send_patcher.assert_not_called()
|
@ -1,65 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch
|
||||
from odoo.tests import common
|
||||
from .common import RunbotCase
|
||||
|
||||
|
||||
class Test_Cron(RunbotCase):
|
||||
class SleepException(Exception):
|
||||
...
|
||||
|
||||
|
||||
def sleep(time):
|
||||
raise SleepException()
|
||||
|
||||
|
||||
class TestCron(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super(Test_Cron, self).setUp()
|
||||
self.start_patcher('_get_cron_period', 'odoo.addons.runbot.models.repo.runbot_repo._get_cron_period', 2)
|
||||
super(TestCron, self).setUp()
|
||||
self.start_patcher('_get_cron_period', 'odoo.addons.runbot.models.runbot.Runbot._get_cron_period', 2)
|
||||
|
||||
@patch('odoo.addons.runbot.models.repo.config.get')
|
||||
def test_cron_period(self, mock_config_get):
|
||||
""" Test that the random cron period stays below margin
|
||||
Assuming a configuration of 10 minutes cron limit
|
||||
"""
|
||||
mock_config_get.return_value = 600
|
||||
period = self.Repo._get_cron_period(min_margin=200)
|
||||
for i in range(200):
|
||||
self.assertLess(period, 400)
|
||||
|
||||
def test_crons_returns(self):
|
||||
""" test that cron_fetch_and_schedule and _cron_fetch_and_build
|
||||
return directly when called on wrong host
|
||||
"""
|
||||
|
||||
ret = self.Repo._cron_fetch_and_schedule('runbotx.foo.com')
|
||||
self.assertEqual(ret, 'Not for me')
|
||||
|
||||
ret = self.Repo._cron_fetch_and_build('runbotx.foo.com')
|
||||
self.assertEqual(ret, 'Not for me')
|
||||
|
||||
@patch('odoo.addons.runbot.models.repo.runbot_repo._create_pending_builds')
|
||||
@patch('odoo.addons.runbot.models.repo.runbot_repo._update')
|
||||
def test_cron_schedule(self, mock_update, mock_create):
|
||||
@patch('time.sleep', side_effect=sleep)
|
||||
@patch('odoo.addons.runbot.models.repo.Repo._update_batches')
|
||||
def test_cron_schedule(self, mock_update_batches, *args):
|
||||
""" test that cron_fetch_and_schedule do its work """
|
||||
self.env['ir.config_parameter'].sudo().set_param('runbot.runbot_update_frequency', 1)
|
||||
self.Repo.create({'name': '/path/somewhere/disabled.git', 'mode': 'disabled'}) # create a disabled
|
||||
self.Repo.search([]).write({'mode': 'disabled'}) # disable all depo, in case we have existing ones
|
||||
local_repo = self.Repo.create({'name': '/path/somewhere/rep.git'}) # create active repo
|
||||
ret = self.Repo._cron_fetch_and_schedule('host.runbot.com')
|
||||
self.assertEqual(None, ret)
|
||||
mock_update.assert_called_with(force=False)
|
||||
mock_create.assert_called_with()
|
||||
self.env['ir.config_parameter'].sudo().set_param('runbot.runbot_do_fetch', True)
|
||||
self.env['runbot.repo'].search([('id', '!=', self.repo_server.id)]).write({'mode': 'disabled'}) # disable all other existing repo than repo_server
|
||||
try:
|
||||
self.Runbot._cron()
|
||||
except SleepException:
|
||||
pass # sleep raises an exception to avoid to stay stuck in loop
|
||||
mock_update_batches.assert_called()
|
||||
|
||||
@patch('odoo.addons.runbot.models.host.RunboHost._docker_build')
|
||||
@patch('odoo.addons.runbot.models.host.RunboHost._bootstrap')
|
||||
@patch('odoo.addons.runbot.models.repo.runbot_repo._reload_nginx')
|
||||
@patch('odoo.addons.runbot.models.repo.runbot_repo._scheduler')
|
||||
def test_cron_build(self, mock_scheduler, mock_reload, mock_host_bootstrap, mock_host_docker_build):
|
||||
@patch('time.sleep', side_effect=sleep)
|
||||
@patch('odoo.addons.runbot.models.host.Host._docker_build')
|
||||
@patch('odoo.addons.runbot.models.host.Host._bootstrap')
|
||||
@patch('odoo.addons.runbot.models.runbot.Runbot._scheduler')
|
||||
def test_cron_build(self, mock_scheduler, mock_host_bootstrap, mock_host_docker_build, *args):
|
||||
""" test that cron_fetch_and_build do its work """
|
||||
hostname = 'host.runbot.com'
|
||||
hostname = 'cronhost.runbot.com'
|
||||
self.patchers['fqdn_patcher'].return_value = hostname
|
||||
self.env['ir.config_parameter'].sudo().set_param('runbot.runbot_update_frequency', 1)
|
||||
self.Repo.create({'name': '/path/somewhere/disabled.git', 'mode': 'disabled'}) # create a disabled
|
||||
self.Repo.search([]).write({'mode': 'disabled'}) # disable all depo, in case we have existing ones
|
||||
local_repo = self.Repo.create({'name': '/path/somewhere/rep.git'}) # create active repo
|
||||
ret = self.Repo._cron_fetch_and_build(hostname)
|
||||
self.assertEqual(None, ret)
|
||||
self.env['ir.config_parameter'].sudo().set_param('runbot.runbot_do_schedule', True)
|
||||
self.env['runbot.repo'].search([('id', '!=', self.repo_server.id)]).write({'mode': 'disabled'}) # disable all other existing repo than repo_server
|
||||
|
||||
try:
|
||||
self.Runbot._cron()
|
||||
except SleepException:
|
||||
pass # sleep raises an exception to avoid to stay stuck in loop
|
||||
mock_scheduler.assert_called()
|
||||
mock_host_bootstrap.assert_called()
|
||||
mock_host_docker_build.assert_called()
|
||||
host = self.env['runbot.host'].search([('name', '=', hostname)])
|
||||
self.assertEqual(host.name, hostname, 'A new host should have been created')
|
||||
self.assertGreater(host.psql_conn_count, 0, 'A least one connection should exist on the current psql instance')
|
||||
self.assertTrue(host, 'A new host should have been created')
|
||||
# self.assertGreater(host.psql_conn_count, 0, 'A least one connection should exist on the current psql batch')
|
||||
|
@ -1,21 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch
|
||||
from odoo.tests import common
|
||||
from .common import RunbotCase
|
||||
|
||||
|
||||
class TestIrLogging(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestIrLogging, self).setUp()
|
||||
self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar', 'server_files': 'server.py', 'addons_paths': 'addons,core/addons'})
|
||||
self.branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/master'
|
||||
})
|
||||
self.Build = self.env['runbot.build']
|
||||
self.IrLogging = self.env['ir.logging']
|
||||
|
||||
def simulate_log(self, build, func, message, level='INFO'):
|
||||
""" simulate ir_logging from an external build """
|
||||
dest = '%s-fake-dest' % build.id
|
||||
@ -26,18 +14,16 @@ class TestIrLogging(RunbotCase):
|
||||
""", val)
|
||||
|
||||
def test_ir_logging(self):
|
||||
build = self.create_build({
|
||||
'branch_id': self.branch.id,
|
||||
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
|
||||
'port': '1234',
|
||||
build = self.Build.create({
|
||||
'active_step': self.env.ref('runbot.runbot_build_config_step_test_all').id,
|
||||
'params_id': self.base_params.id,
|
||||
})
|
||||
|
||||
build.log_counter = 10
|
||||
|
||||
# Test that an ir_logging is created and a the trigger set the build_id
|
||||
self.simulate_log(build, 'test function', 'test message')
|
||||
log_line = self.IrLogging.search([('func', '=', 'test function'), ('message', '=', 'test message'), ('level', '=', 'INFO')])
|
||||
log_line = self.env['ir.logging'].search([('func', '=', 'test function'), ('message', '=', 'test message'), ('level', '=', 'INFO')])
|
||||
self.assertEqual(len(log_line), 1, "A build log event should have been created")
|
||||
self.assertEqual(log_line.build_id, build)
|
||||
self.assertEqual(log_line.active_step_id, self.env.ref('runbot.runbot_build_config_step_test_all'), 'The active step should be set on the log line')
|
||||
@ -58,19 +44,19 @@ class TestIrLogging(RunbotCase):
|
||||
# Test the log limit
|
||||
for i in range(11):
|
||||
self.simulate_log(build, 'limit function', 'limit message')
|
||||
log_lines = self.IrLogging.search([('build_id', '=', build.id), ('type', '=', 'server'), ('func', '=', 'limit function'), ('message', '=', 'limit message'), ('level', '=', 'INFO')])
|
||||
log_lines = self.env['ir.logging'].search([('build_id', '=', build.id), ('type', '=', 'server'), ('func', '=', 'limit function'), ('message', '=', 'limit message'), ('level', '=', 'INFO')])
|
||||
self.assertGreater(len(log_lines), 7, 'Trigger should have created logs with appropriate build id')
|
||||
self.assertLess(len(log_lines), 10, 'Trigger should prevent insert more lines of logs than log_counter')
|
||||
last_log_line = self.IrLogging.search([('build_id', '=', build.id)], order='id DESC', limit=1)
|
||||
last_log_line = self.env['ir.logging'].search([('build_id', '=', build.id)], order='id DESC', limit=1)
|
||||
self.assertIn('Log limit reached', last_log_line.message, 'Trigger should modify last log message')
|
||||
|
||||
# Test that the _log method is still able to add logs
|
||||
build._log('runbot function', 'runbot message')
|
||||
log_lines = self.IrLogging.search([('type', '=', 'runbot'), ('name', '=', 'odoo.runbot'), ('func', '=', 'runbot function'), ('message', '=', 'runbot message'), ('level', '=', 'INFO')])
|
||||
log_lines = self.env['ir.logging'].search([('type', '=', 'runbot'), ('name', '=', 'odoo.runbot'), ('func', '=', 'runbot function'), ('message', '=', 'runbot message'), ('level', '=', 'INFO')])
|
||||
self.assertEqual(len(log_lines), 1, '_log should be able to add logs from the runbot')
|
||||
|
||||
def test_markdown(self):
|
||||
log = self.IrLogging.create({
|
||||
log = self.env['ir.logging'].create({
|
||||
'name': 'odoo.runbot',
|
||||
'type': 'runbot',
|
||||
'path': 'runbot',
|
||||
|
@ -1,71 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from collections import defaultdict
|
||||
from itertools import cycle
|
||||
from unittest.mock import patch
|
||||
from werkzeug.wrappers import Response
|
||||
from odoo.tests import common
|
||||
from odoo.addons.runbot.controllers import frontend
|
||||
from .common import RunbotCase
|
||||
|
||||
|
||||
class Test_Frontend(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super(Test_Frontend, self).setUp()
|
||||
Repo = self.env['runbot.repo']
|
||||
self.repo = Repo.create({'name': 'bla@example.com:foo/bar', 'token': '123'})
|
||||
self.Branch = self.env['runbot.branch']
|
||||
self.sticky_branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/master',
|
||||
'sticky': True,
|
||||
})
|
||||
self.branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/master-test-moc',
|
||||
'sticky': False,
|
||||
})
|
||||
self.Build = self.env['runbot.build']
|
||||
|
||||
@patch('odoo.http.Response.set_default')
|
||||
@patch('odoo.addons.runbot.controllers.frontend.request')
|
||||
def test_frontend_basic(self, mock_request, mock_set_default):
|
||||
mock_request.env = self.env
|
||||
mock_request._cr = self.cr
|
||||
controller = frontend.Runbot()
|
||||
|
||||
states = ['done', 'pending', 'testing', 'running']
|
||||
branches = [self.branch, self.sticky_branch]
|
||||
names = ['deadbeef', 'd0d0caca', 'deadface', 'cacafeed']
|
||||
# create 5 builds in each branch
|
||||
for i, state, branch, name in zip(range(8), cycle(states), cycle(branches), cycle(names)):
|
||||
name = '%s%s' % (name, i)
|
||||
build = self.Build.create({
|
||||
'branch_id': branch.id,
|
||||
'name': '%s0000ffffffffffffffffffffffffffff' % name,
|
||||
'port': '1234',
|
||||
'local_state': state,
|
||||
'local_result': 'ok'
|
||||
})
|
||||
|
||||
def mocked_simple_repo_render(template, context):
|
||||
self.assertEqual(template, 'runbot.repo', 'The frontend controller should use "runbot.repo" template')
|
||||
self.assertEqual(self.sticky_branch, context['branches'][0]['branch'], "The sticky branch should be in first place")
|
||||
self.assertEqual(self.branch, context['branches'][1]['branch'], "The non sticky branch should be in second place")
|
||||
self.assertEqual(len(context['branches'][0]['builds']), 4, "Only the 4 last builds should appear in the context")
|
||||
self.assertEqual(context['pending_total'], 2, "There should be 2 pending builds")
|
||||
self.assertEqual(context['pending_level'], 'info', "The pending level should be info")
|
||||
return Response()
|
||||
|
||||
mock_request.render = mocked_simple_repo_render
|
||||
controller.repo(repo=self.repo)
|
||||
|
||||
def mocked_repo_search_render(template, context):
|
||||
dead_count = len([bu['name'] for b in context['branches'] for bu in b['builds'] if bu['name'].startswith('dead')])
|
||||
undead_count = len([bu['name'] for b in context['branches'] for bu in b['builds'] if not bu['name'].startswith('dead')])
|
||||
self.assertEqual(dead_count, 4, 'The search for "dead" should return 4 builds')
|
||||
self.assertEqual(undead_count, 0, 'The search for "dead" should not return any build without "dead" in its name')
|
||||
return Response()
|
||||
|
||||
mock_request.render = mocked_repo_search_render
|
||||
controller.repo(repo=self.repo, search='dead')
|
@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import re
|
||||
from unittest import skip
|
||||
from unittest.mock import patch, Mock
|
||||
from subprocess import CalledProcessError
|
||||
@ -9,108 +10,123 @@ import logging
|
||||
import odoo
|
||||
import time
|
||||
|
||||
from .common import RunbotCase
|
||||
from .common import RunbotCase, RunbotCaseMinimalSetup
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Test_Repo(RunbotCase):
|
||||
class TestRepo(RunbotCaseMinimalSetup):
|
||||
|
||||
def setUp(self):
|
||||
super(Test_Repo, self).setUp()
|
||||
self.commit_list = []
|
||||
super(TestRepo, self).setUp()
|
||||
self.commit_list = {}
|
||||
self.mock_root = self.patchers['repo_root_patcher']
|
||||
|
||||
def mock_git_helper(self):
|
||||
"""Helper that returns a mock for repo._git()"""
|
||||
def mock_git(repo, cmd):
|
||||
if cmd[0] == 'for-each-ref' and self.commit_list:
|
||||
return '\n'.join(['\0'.join(commit_fields) for commit_fields in self.commit_list])
|
||||
return mock_git
|
||||
|
||||
def test_base_fields(self):
|
||||
self.mock_root.return_value = '/tmp/static'
|
||||
repo = self.Repo.create({'name': 'bla@example.com:foo/bar'})
|
||||
self.assertEqual(repo.path, '/tmp/static/repo/bla_example.com_foo_bar')
|
||||
|
||||
self.assertEqual(repo.base, 'example.com/foo/bar')
|
||||
self.assertEqual(repo.short_name, 'foo/bar')
|
||||
repo = self.repo_server
|
||||
remote = self.remote_server
|
||||
# name = 'bla@example.com:base/server'
|
||||
self.assertEqual(repo.path, '/tmp/static/repo/server')
|
||||
self.assertEqual(remote.base_url, 'example.com/base/server')
|
||||
self.assertEqual(remote.short_name, 'base/server')
|
||||
self.assertEqual(remote.owner, 'base')
|
||||
self.assertEqual(remote.repo_name, 'server')
|
||||
|
||||
https_repo = self.Repo.create({'name': 'https://bla@example.com/user/rep.git'})
|
||||
self.assertEqual(https_repo.short_name, 'user/rep')
|
||||
# HTTPS
|
||||
remote.name = 'https://bla@example.com/base/server.git'
|
||||
self.assertEqual(remote.short_name, 'base/server')
|
||||
self.assertEqual(remote.owner, 'base')
|
||||
self.assertEqual(remote.repo_name, 'server')
|
||||
|
||||
local_repo = self.Repo.create({'name': '/path/somewhere/rep.git'})
|
||||
self.assertEqual(local_repo.short_name, 'somewhere/rep')
|
||||
# LOCAL
|
||||
remote.name = '/path/somewhere/bar.git'
|
||||
self.assertEqual(remote.short_name, 'somewhere/bar')
|
||||
self.assertEqual(remote.owner, 'somewhere')
|
||||
self.assertEqual(remote.repo_name, 'bar')
|
||||
|
||||
@patch('odoo.addons.runbot.models.repo.runbot_repo._get_fetch_head_time')
|
||||
def test_repo_create_pending_builds(self, mock_fetch_head_time):
|
||||
def test_repo_update_batches(self):
|
||||
""" Test that when finding new refs in a repo, the missing branches
|
||||
are created and new builds are created in pending state
|
||||
"""
|
||||
self.mock_root.return_value = '/tmp/static'
|
||||
repo = self.Repo.create({'name': 'bla@example.com:foo/bar'})
|
||||
self.repo_addons = self.repo_addons # lazy repo_addons fails on union
|
||||
self.repo_server = self.repo_server # lazy repo_addons fails on union
|
||||
self.additionnal_setup()
|
||||
self.start_patchers()
|
||||
max_bundle_id = self.env['runbot.bundle'].search([], order='id desc', limit=1).id or 0
|
||||
|
||||
# create another repo and branch to ensure there is no mismatch
|
||||
other_repo = self.Repo.create({'name': 'bla@example.com:foo/foo'})
|
||||
self.env['runbot.branch'].create({
|
||||
'repo_id': other_repo.id,
|
||||
'name': 'refs/heads/bidon'
|
||||
})
|
||||
branch_name = 'master-test'
|
||||
|
||||
first_commit = [('refs/heads/bidon',
|
||||
def github(url, payload=None, ignore_errors=False, nb_tries=2, recursive=False):
|
||||
self.assertEqual(ignore_errors, False)
|
||||
self.assertEqual(url, '/repos/:owner/:repo/pulls/123')
|
||||
return {
|
||||
'base': {'ref': 'master'},
|
||||
'head': {'label': 'dev:%s' % branch_name, 'repo': {'full_name': 'dev/server'}},
|
||||
}
|
||||
|
||||
repos = self.repo_addons | self.repo_server
|
||||
|
||||
first_commit = [(
|
||||
'refs/%s/heads/%s' % (self.remote_server_dev.remote_name, branch_name),
|
||||
'd0d0caca',
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>',
|
||||
'A nice subject',
|
||||
'Server subject',
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>')]
|
||||
self.commit_list = first_commit
|
||||
|
||||
def counter():
|
||||
i = 100000
|
||||
while True:
|
||||
i += 1
|
||||
yield i
|
||||
self.commit_list[self.repo_server.id] = first_commit
|
||||
|
||||
mock_fetch_head_time.side_effect = counter()
|
||||
self.patchers['github_patcher'].side_effect = github
|
||||
repos._update_batches()
|
||||
|
||||
with patch('odoo.addons.runbot.models.repo.runbot_repo._git', new=self.mock_git_helper()):
|
||||
repo._create_pending_builds()
|
||||
dev_branch = self.env['runbot.branch'].search([('remote_id', '=', self.remote_server_dev.id)])
|
||||
|
||||
branch = self.env['runbot.branch'].search([('repo_id', '=', repo.id)])
|
||||
self.assertEqual(branch.name, 'refs/heads/bidon', 'A new branch should have been created')
|
||||
bundle = dev_branch.bundle_id
|
||||
self.assertEqual(dev_branch.name, branch_name, 'A new branch should have been created')
|
||||
|
||||
build = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id)])
|
||||
self.assertEqual(len(build), 1, 'Build found')
|
||||
self.assertEqual(build.subject, 'A nice subject')
|
||||
self.assertEqual(build.local_state, 'pending')
|
||||
self.assertFalse(build.local_result)
|
||||
batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)])
|
||||
self.assertEqual(len(batch), 1, 'Batch found')
|
||||
self.assertEqual(batch.commit_link_ids.commit_id.subject, 'Server subject')
|
||||
self.assertEqual(batch.state, 'preparing')
|
||||
self.assertEqual(dev_branch.head_name, 'd0d0caca')
|
||||
self.assertEqual(bundle.last_batch, batch)
|
||||
last_batch = batch
|
||||
|
||||
# Simulate that a new commit is found in the other repo
|
||||
self.commit_list = [('refs/heads/bidon',
|
||||
# create a addons branch in the same bundle
|
||||
self.commit_list[self.repo_addons.id] = [('refs/%s/heads/%s' % (self.remote_addons_dev.remote_name, branch_name),
|
||||
'deadbeef',
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>',
|
||||
'A better subject',
|
||||
'Addons subject',
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>')]
|
||||
|
||||
with patch('odoo.addons.runbot.models.repo.runbot_repo._git', new=self.mock_git_helper()):
|
||||
other_repo._create_pending_builds()
|
||||
repos._update_batches()
|
||||
|
||||
branch_count = self.env['runbot.branch'].search_count([('repo_id', '=', repo.id)])
|
||||
addons_dev_branch = self.env['runbot.branch'].search([('remote_id', '=', self.remote_addons_dev.id)])
|
||||
|
||||
self.assertEqual(addons_dev_branch.bundle_id, bundle)
|
||||
|
||||
self.assertEqual(dev_branch.head_name, 'd0d0caca', "Dev branch head name shoudn't have change")
|
||||
self.assertEqual(addons_dev_branch.head_name, 'deadbeef')
|
||||
|
||||
branch_count = self.env['runbot.branch'].search_count([('remote_id', '=', self.remote_server_dev.id)])
|
||||
self.assertEqual(branch_count, 1, 'No new branch should have been created')
|
||||
|
||||
build = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id)])
|
||||
self.assertEqual(build.subject, 'A nice subject')
|
||||
self.assertEqual(build.local_state, 'pending')
|
||||
self.assertFalse(build.local_result)
|
||||
batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)])
|
||||
self.assertEqual(last_batch, batch, "No new batch should have been created")
|
||||
self.assertEqual(bundle.last_batch, batch)
|
||||
self.assertEqual(batch.commit_link_ids.commit_id.mapped('subject'), ['Server subject', 'Addons subject'])
|
||||
|
||||
# A new commit is found in the first repo, the previous pending build should be skipped
|
||||
self.commit_list = [('refs/heads/bidon',
|
||||
'b00b',
|
||||
# create a server pr in the same bundle with the same hash
|
||||
self.commit_list[self.repo_server.id] += [
|
||||
('refs/%s/pull/123' % self.remote_server.remote_name,
|
||||
'd0d0caca',
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>',
|
||||
@ -118,52 +134,133 @@ class Test_Repo(RunbotCase):
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>')]
|
||||
|
||||
with patch('odoo.addons.runbot.models.repo.runbot_repo._git', new=self.mock_git_helper()):
|
||||
repo._create_pending_builds()
|
||||
# Create Batches
|
||||
repos._update_batches()
|
||||
|
||||
branch_count = self.env['runbot.branch'].search_count([('repo_id', '=', repo.id)])
|
||||
self.assertEqual(branch_count, 1, 'No new branch should have been created')
|
||||
pull_request = self.env['runbot.branch'].search([('remote_id', '=', self.remote_server.id), ('id', '!=', self.branch_server.id)])
|
||||
self.assertEqual(pull_request.bundle_id, bundle)
|
||||
|
||||
build = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id), ('name', '=', 'b00b')])
|
||||
self.assertEqual(len(build), 1)
|
||||
self.assertEqual(build.subject, 'Another subject')
|
||||
self.assertEqual(build.local_state, 'pending')
|
||||
self.assertFalse(build.local_result)
|
||||
self.assertEqual(dev_branch.head_name, 'd0d0caca')
|
||||
self.assertEqual(pull_request.head_name, 'd0d0caca')
|
||||
self.assertEqual(addons_dev_branch.head_name, 'deadbeef')
|
||||
|
||||
previous_build = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id), ('name', '=', 'd0d0caca')])
|
||||
self.assertEqual(previous_build.local_state, 'done', 'Previous pending build should be done')
|
||||
self.assertEqual(previous_build.local_result, 'skipped', 'Previous pending build result should be skipped')
|
||||
self.assertEqual(dev_branch, self.env['runbot.branch'].search([('remote_id', '=', self.remote_server_dev.id)]))
|
||||
self.assertEqual(addons_dev_branch, self.env['runbot.branch'].search([('remote_id', '=', self.remote_addons_dev.id)]))
|
||||
|
||||
self.commit_list = first_commit # branch reseted hard to an old commit
|
||||
builds = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id), ('name', '=', 'd0d0caca')])
|
||||
self.assertEqual(len(builds), 1)
|
||||
with patch('odoo.addons.runbot.models.repo.runbot_repo._git', new=self.mock_git_helper()):
|
||||
repo._create_pending_builds()
|
||||
batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)])
|
||||
self.assertEqual(last_batch, batch, "No new batch should have been created")
|
||||
self.assertEqual(bundle.last_batch, batch)
|
||||
self.assertEqual(batch.commit_link_ids.commit_id.mapped('subject'), ['Server subject', 'Addons subject'])
|
||||
|
||||
last_build = self.env['runbot.build'].search([], limit=1)
|
||||
self.assertEqual(last_build.name, 'd0d0caca')
|
||||
builds = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id), ('name', '=', 'd0d0caca')])
|
||||
self.assertEqual(len(builds), 2)
|
||||
# self.assertEqual(last_build.duplicate_id, previous_build) False because previous_build is skipped
|
||||
with patch('odoo.addons.runbot.models.repo.runbot_repo._git', new=self.mock_git_helper()):
|
||||
other_repo._create_pending_builds()
|
||||
builds = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id), ('name', '=', 'd0d0caca')])
|
||||
self.assertEqual(len(builds), 2)
|
||||
# A new commit is found in the server repo
|
||||
self.commit_list[self.repo_server.id] = [
|
||||
(
|
||||
'refs/%s/heads/%s' % (self.remote_server_dev.remote_name, branch_name),
|
||||
'b00b',
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>',
|
||||
'A new subject',
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>'
|
||||
),
|
||||
(
|
||||
'refs/%s/pull/123' % self.remote_server.remote_name,
|
||||
'b00b',
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>',
|
||||
'A new subject',
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>'
|
||||
)]
|
||||
|
||||
# Create Batches
|
||||
repos._update_batches()
|
||||
|
||||
self.assertEqual(dev_branch, self.env['runbot.branch'].search([('remote_id', '=', self.remote_server_dev.id)]))
|
||||
self.assertEqual(pull_request + self.branch_server, self.env['runbot.branch'].search([('remote_id', '=', self.remote_server.id)]))
|
||||
self.assertEqual(addons_dev_branch, self.env['runbot.branch'].search([('remote_id', '=', self.remote_addons_dev.id)]))
|
||||
|
||||
batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)])
|
||||
self.assertEqual(bundle.last_batch, batch)
|
||||
self.assertEqual(len(batch), 1, 'No new batch created, updated')
|
||||
self.assertEqual(batch.commit_link_ids.commit_id.mapped('subject'), ['A new subject', 'Addons subject'], 'commits should have been updated')
|
||||
self.assertEqual(batch.state, 'preparing')
|
||||
|
||||
self.assertEqual(dev_branch.head_name, 'b00b')
|
||||
self.assertEqual(pull_request.head_name, 'b00b')
|
||||
self.assertEqual(addons_dev_branch.head_name, 'deadbeef')
|
||||
|
||||
# TODO move this
|
||||
# previous_build = self.env['runbot.build'].search([('repo_id', '=', repo.id), ('branch_id', '=', branch.id), ('name', '=', 'd0d0caca')])
|
||||
# self.assertEqual(previous_build.local_state, 'done', 'Previous pending build should be done')
|
||||
# self.assertEqual(previous_build.local_result, 'skipped', 'Previous pending build result should be skipped')
|
||||
|
||||
batch.state = 'done'
|
||||
|
||||
repos._update_batches()
|
||||
|
||||
batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)])
|
||||
self.assertEqual(len(batch), 1, 'No new batch created, no head change')
|
||||
|
||||
self.commit_list[self.repo_server.id] = [
|
||||
('refs/%s/heads/%s' % (self.remote_server_dev.remote_name, branch_name),
|
||||
'dead1234',
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>',
|
||||
'A last subject',
|
||||
'Marc Bidule',
|
||||
'<marc.bidule@somewhere.com>')]
|
||||
|
||||
repos._update_batches()
|
||||
|
||||
bundles = self.env['runbot.bundle'].search([('id', '>', max_bundle_id)])
|
||||
self.assertEqual(bundles, bundle)
|
||||
batches = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)])
|
||||
self.assertEqual(len(batches), 2, 'No preparing instance and new head -> new batch')
|
||||
self.assertEqual(bundle.last_batch.state, 'preparing')
|
||||
self.assertEqual(bundle.last_batch.commit_link_ids.commit_id.subject, 'A last subject')
|
||||
|
||||
self.commit_list[self.repo_server.id] = first_commit # branch reset hard to an old commit (and pr closed)
|
||||
|
||||
repos._update_batches()
|
||||
|
||||
batches = self.env['runbot.batch'].search([('bundle_id', '=', bundle.id)], order='id desc')
|
||||
last_batch = bundle.last_batch
|
||||
self.assertEqual(len(batches), 2, 'No new batch created, updated')
|
||||
self.assertEqual(last_batch.commit_link_ids.commit_id.mapped('subject'), ['Server subject'], 'commits should have been updated')
|
||||
self.assertEqual(last_batch.state, 'preparing')
|
||||
self.assertEqual(dev_branch.head_name, 'd0d0caca')
|
||||
|
||||
def github2(url, payload=None, ignore_errors=False, nb_tries=2, recursive=False):
|
||||
self.assertEqual(ignore_errors, True)
|
||||
self.assertIn(url, ['/repos/:owner/:repo/statuses/d0d0caca', '/repos/:owner/:repo/statuses/deadbeef'])
|
||||
return {}
|
||||
|
||||
self.patchers['github_patcher'].side_effect = github2
|
||||
last_batch._prepare()
|
||||
self.assertEqual(last_batch.commit_link_ids.commit_id.mapped('subject'), ['Server subject', 'Addons subject'])
|
||||
|
||||
self.assertEqual(last_batch.state, 'ready')
|
||||
|
||||
self.assertEqual(2, len(last_batch.slot_ids))
|
||||
self.assertEqual(2, len(last_batch.slot_ids.mapped('build_id')))
|
||||
|
||||
@skip('This test is for performances. It needs a lot of real branches in DB to mean something')
|
||||
def test_repo_perf_find_new_commits(self):
|
||||
self.mock_root.return_value = '/tmp/static'
|
||||
repo = self.env['runbot.repo'].search([('name', '=', 'blabla')])
|
||||
|
||||
self.commit_list = []
|
||||
self.commit_list[self.repo_server.id] = []
|
||||
|
||||
# create 20000 branches and refs
|
||||
start_time = time.time()
|
||||
self.env['runbot.build'].search([], limit=5).write({'name': 'jflsdjflj'})
|
||||
|
||||
for i in range(20005):
|
||||
self.commit_list.append(['refs/heads/bidon-%05d' % i,
|
||||
self.commit_list[self.repo_server.id].append(['refs/heads/bidon-%05d' % i,
|
||||
'd0d0caca %s' % i,
|
||||
datetime.datetime.now().strftime("%Y-%m-%d, %H:%M:%S"),
|
||||
'Marc Bidule',
|
||||
@ -173,18 +270,16 @@ class Test_Repo(RunbotCase):
|
||||
'<marc.bidule@somewhere.com>'])
|
||||
inserted_time = time.time()
|
||||
_logger.info('Insert took: %ssec', (inserted_time - start_time))
|
||||
with patch('odoo.addons.runbot.models.repo.runbot_repo._git', new=self.mock_git_helper()):
|
||||
repo._create_pending_builds()
|
||||
repo._update_batches()
|
||||
|
||||
_logger.info('Create pending builds took: %ssec', (time.time() - inserted_time))
|
||||
|
||||
|
||||
@common.warmup
|
||||
def test_times(self):
|
||||
def _test_times(model, setter, field_name):
|
||||
repo1 = self.Repo.create({'name': 'bla@example.com:foo/bar'})
|
||||
repo2 = self.Repo.create({'name': 'bla@example.com:foo2/bar2'})
|
||||
count = self.cr.sql_log_count
|
||||
repo1 = self.repo_server
|
||||
repo2 = self.repo_addons
|
||||
|
||||
with self.assertQueryCount(1):
|
||||
getattr(repo1, setter)(1.1)
|
||||
getattr(repo2, setter)(1.2)
|
||||
@ -213,28 +308,42 @@ class Test_Repo(RunbotCase):
|
||||
_test_times('runbot.repo.reftime', 'set_ref_time', 'get_ref_time')
|
||||
|
||||
|
||||
class Test_Github(TransactionCase):
|
||||
class TestGithub(TransactionCase):
|
||||
|
||||
def test_github(self):
|
||||
""" Test different github responses or failures"""
|
||||
repo = self.env['runbot.repo'].create({'name': 'bla@example.com:foo/foo'})
|
||||
self.assertEqual(repo._github('/repos/:owner/:repo/statuses/abcdef', dict(), ignore_errors=True), None, 'A repo without token should return None')
|
||||
repo.token = 'abc'
|
||||
with patch('odoo.addons.runbot.models.repo.requests.Session') as mock_session:
|
||||
|
||||
project = self.env['runbot.project'].create({'name': 'Tests'})
|
||||
repo_server = self.env['runbot.repo'].create({
|
||||
'name': 'server',
|
||||
'project_id': project.id,
|
||||
})
|
||||
remote_server = self.env['runbot.remote'].create({
|
||||
'name': 'bla@example.com:base/server',
|
||||
'repo_id': repo_server.id,
|
||||
})
|
||||
|
||||
# self.assertEqual(remote_server._github('/repos/:owner/:repo/statuses/abcdef', dict(), ignore_errors=True), None, 'A repo without token should return None')
|
||||
remote_server.token = 'abc'
|
||||
|
||||
import requests
|
||||
with patch('odoo.addons.runbot.models.repo.requests.Session') as mock_session, patch('time.sleep') as mock_sleep:
|
||||
mock_sleep.return_value = None
|
||||
with self.assertRaises(Exception, msg='should raise an exception with ignore_errors=False'):
|
||||
mock_session.return_value.post.side_effect = Exception('301: Bad gateway')
|
||||
repo._github('/repos/:owner/:repo/statuses/abcdef', {'foo': 'bar'}, ignore_errors=False)
|
||||
mock_session.return_value.post.side_effect = requests.HTTPError('301: Bad gateway')
|
||||
remote_server._github('/repos/:owner/:repo/statuses/abcdef', {'foo': 'bar'}, ignore_errors=False)
|
||||
|
||||
mock_session.return_value.post.reset_mock()
|
||||
with self.assertLogs(logger='odoo.addons.runbot.models.repo') as assert_log:
|
||||
repo._github('/repos/:owner/:repo/statuses/abcdef', {'foo': 'bar'}, ignore_errors=True)
|
||||
remote_server._github('/repos/:owner/:repo/statuses/abcdef', {'foo': 'bar'}, ignore_errors=True)
|
||||
self.assertIn('Ignored github error', assert_log.output[0])
|
||||
|
||||
self.assertEqual(2, mock_session.return_value.post.call_count, "_github method should try two times by default")
|
||||
|
||||
mock_session.return_value.post.reset_mock()
|
||||
mock_session.return_value.post.side_effect = [Exception('301: Bad gateway'), Mock()]
|
||||
mock_session.return_value.post.side_effect = [requests.HTTPError('301: Bad gateway'), Mock()]
|
||||
with self.assertLogs(logger='odoo.addons.runbot.models.repo') as assert_log:
|
||||
repo._github('/repos/:owner/:repo/statuses/abcdef', {'foo': 'bar'}, ignore_errors=True)
|
||||
remote_server._github('/repos/:owner/:repo/statuses/abcdef', {'foo': 'bar'}, ignore_errors=True)
|
||||
self.assertIn('Success after 2 tries', assert_log.output[0])
|
||||
|
||||
self.assertEqual(2, mock_session.return_value.post.call_count, "_github method should try two times by default")
|
||||
@ -245,100 +354,109 @@ class TestFetch(RunbotCase):
|
||||
def setUp(self):
|
||||
super(TestFetch, self).setUp()
|
||||
self.mock_root = self.patchers['repo_root_patcher']
|
||||
self.fetch_count = 0
|
||||
self.force_failure = False
|
||||
|
||||
def test_update_fetch_cmd(self):
|
||||
""" Test that git fetch is tried multiple times before disabling host """
|
||||
|
||||
fetch_count = 0
|
||||
force_failure = False
|
||||
|
||||
def git_side_effect(cmd):
|
||||
nonlocal fetch_count
|
||||
fetch_count += 1
|
||||
if fetch_count < 3 or force_failure:
|
||||
def mock_git_helper(self):
|
||||
"""Helper that returns a mock for repo._git()"""
|
||||
def mock_git(repo, cmd):
|
||||
self.assertIn('fetch', cmd)
|
||||
self.fetch_count += 1
|
||||
if self.fetch_count < 3 or self.force_failure:
|
||||
raise CalledProcessError(128, cmd, 'Dummy Error'.encode('utf-8'))
|
||||
else:
|
||||
return True
|
||||
return mock_git
|
||||
|
||||
git_patcher = self.patchers['git_patcher']
|
||||
git_patcher.side_effect = git_side_effect
|
||||
@patch('time.sleep', return_value=None)
|
||||
def test_update_fetch_cmd(self, mock_time):
|
||||
""" Test that git fetch is tried multiple times before disabling host """
|
||||
|
||||
repo = self.Repo.create({'name': 'bla@example.com:foo/bar'})
|
||||
host = self.env['runbot.host']._get_current()
|
||||
|
||||
self.assertFalse(host.assigned_only)
|
||||
# Ensure that Host is not disabled if fetch succeeds after 3 tries
|
||||
with mute_logger("odoo.addons.runbot.models.repo"):
|
||||
repo._update_fetch_cmd()
|
||||
self.repo_server._update_fetch_cmd()
|
||||
self.assertFalse(host.assigned_only, "Host should not be disabled when fetch succeeds")
|
||||
self.assertEqual(fetch_count, 3)
|
||||
self.assertEqual(self.fetch_count, 3)
|
||||
|
||||
# Now ensure that host is disabled after 5 unsuccesful tries
|
||||
force_failure = True
|
||||
fetch_count = 0
|
||||
self.force_failure = True
|
||||
self.fetch_count = 0
|
||||
with mute_logger("odoo.addons.runbot.models.repo"):
|
||||
repo._update_fetch_cmd()
|
||||
self.repo_server._update_fetch_cmd()
|
||||
self.assertTrue(host.assigned_only)
|
||||
self.assertEqual(fetch_count, 5)
|
||||
self.assertEqual(self.fetch_count, 5)
|
||||
|
||||
|
||||
class Test_Repo_Scheduler(RunbotCase):
|
||||
class TestIdentityFile(RunbotCase):
|
||||
|
||||
def check_output_helper(self):
|
||||
"""Helper that returns a mock for repo._git()"""
|
||||
def mock_check_output(cmd, *args, **kwargs):
|
||||
expected_option = '-c core.sshCommand=ssh -i \/.+\/\.ssh\/fake_identity'
|
||||
git_cmd = ' '.join(cmd)
|
||||
self.assertTrue(re.search(expected_option, git_cmd), '%s did not match %s' % (git_cmd, expected_option))
|
||||
return Mock()
|
||||
|
||||
return mock_check_output
|
||||
|
||||
def test_identity_file(self):
|
||||
"""test that the identity file is used in git command"""
|
||||
|
||||
self.stop_patcher('git_patcher')
|
||||
self.start_patcher('check_output_patcher', 'odoo.addons.runbot.models.repo.subprocess.check_output', new=self.check_output_helper())
|
||||
|
||||
self.repo_server.identity_file = 'fake_identity'
|
||||
|
||||
with mute_logger("odoo.addons.runbot.models.repo"):
|
||||
self.repo_server._update_fetch_cmd()
|
||||
|
||||
|
||||
class TestRepoScheduler(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
# as the _scheduler method commits, we need to protect the database
|
||||
registry = odoo.registry()
|
||||
super(Test_Repo_Scheduler, self).setUp()
|
||||
super(TestRepoScheduler, self).setUp()
|
||||
|
||||
self.fqdn_patcher = patch('odoo.addons.runbot.models.host.fqdn')
|
||||
mock_root = self.patchers['repo_root_patcher']
|
||||
mock_root.return_value = '/tmp/static'
|
||||
|
||||
self.foo_repo = self.Repo.create({'name': 'bla@example.com:foo/bar'})
|
||||
|
||||
self.foo_branch = self.Branch.create({
|
||||
'repo_id': self.foo_repo.id,
|
||||
'name': 'refs/head/foo'
|
||||
})
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._kill')
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._schedule')
|
||||
@patch('odoo.addons.runbot.models.build.runbot_build._init_pendings')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._kill')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._schedule')
|
||||
@patch('odoo.addons.runbot.models.build.BuildResult._init_pendings')
|
||||
def test_repo_scheduler(self, mock_init_pendings, mock_schedule, mock_kill):
|
||||
|
||||
self.env['ir.config_parameter'].set_param('runbot.runbot_workers', 6)
|
||||
builds = []
|
||||
# create 6 builds that are testing on the host to verify that
|
||||
# workers are not overfilled
|
||||
for build_name in ['a', 'b', 'c', 'd', 'e', 'f']:
|
||||
build = self.create_build({
|
||||
'branch_id': self.foo_branch.id,
|
||||
'name': build_name,
|
||||
'port': '1234',
|
||||
for _ in range(6):
|
||||
build = self.Build.create({
|
||||
'params_id': self.base_params.id,
|
||||
'build_type': 'normal',
|
||||
'local_state': 'testing',
|
||||
'host': 'host.runbot.com'
|
||||
})
|
||||
builds.append(build)
|
||||
# now the pending build that should stay unasigned
|
||||
scheduled_build = self.create_build({
|
||||
'branch_id': self.foo_branch.id,
|
||||
'name': 'sched_build',
|
||||
'port': '1234',
|
||||
scheduled_build = self.Build.create({
|
||||
'params_id': self.base_params.id,
|
||||
'build_type': 'scheduled',
|
||||
'local_state': 'pending',
|
||||
})
|
||||
builds.append(scheduled_build)
|
||||
# create the build that should be assigned once a slot is available
|
||||
build = self.create_build({
|
||||
'branch_id': self.foo_branch.id,
|
||||
'name': 'foobuild',
|
||||
'port': '1234',
|
||||
build = self.Build.create({
|
||||
'params_id': self.base_params.id,
|
||||
'build_type': 'normal',
|
||||
'local_state': 'pending',
|
||||
})
|
||||
builds.append(build)
|
||||
host = self.env['runbot.host']._get_current()
|
||||
self.foo_repo._scheduler(host)
|
||||
self.Runbot._scheduler(host)
|
||||
|
||||
build.invalidate_cache()
|
||||
scheduled_build.invalidate_cache()
|
||||
@ -346,6 +464,6 @@ class Test_Repo_Scheduler(RunbotCase):
|
||||
self.assertFalse(scheduled_build.host)
|
||||
|
||||
# give some room for the pending build
|
||||
self.Build.search([('name', '=', 'a')]).write({'local_state': 'done'})
|
||||
builds[0].write({'local_state': 'done'})
|
||||
|
||||
self.foo_repo._scheduler(host)
|
||||
self.Runbot._scheduler(host)
|
||||
|
14
runbot/tests/test_runbot.py
Normal file
14
runbot/tests/test_runbot.py
Normal file
@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from .common import RunbotCase
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestRunbot(RunbotCase):
|
||||
|
||||
def test_warning_from_runbot_abstract(self):
|
||||
warning_id = self.env['runbot.runbot'].warning('Test warning message')
|
||||
|
||||
self.assertTrue(self.env['runbot.warning'].browse(warning_id).exists())
|
@ -1,50 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from unittest.mock import patch
|
||||
from odoo.tests import common
|
||||
import odoo
|
||||
from .common import RunbotCase
|
||||
|
||||
|
||||
class TestSchedule(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
# entering test mode to avoid that the _schedule method commits records
|
||||
registry = odoo.registry()
|
||||
super(TestSchedule, self).setUp()
|
||||
|
||||
self.repo = self.Repo.create({'name': 'bla@example.com:foo/bar'})
|
||||
self.branch = self.Branch.create({
|
||||
'repo_id': self.repo.id,
|
||||
'name': 'refs/heads/master'
|
||||
})
|
||||
|
||||
@patch('odoo.addons.runbot.models.build.os.path.getmtime')
|
||||
@patch('odoo.addons.runbot.models.build.docker_state')
|
||||
def test_schedule_mark_done(self, mock_docker_state, mock_getmtime):
|
||||
""" Test that results are set even when job_30_run is skipped """
|
||||
job_end_time = datetime.datetime.now()
|
||||
mock_getmtime.return_value = job_end_time.timestamp()
|
||||
mock_getmtime.return_value = job_end_time.timestamp() # looks wrong
|
||||
|
||||
params = self.BuildParameters.create({
|
||||
'version_id': self.version_13,
|
||||
'project_id': self.project,
|
||||
'config_id': self.env.ref('runbot.runbot_build_config_default').id,
|
||||
})
|
||||
build = self.Build.create({
|
||||
'local_state': 'testing',
|
||||
'branch_id': self.branch.id,
|
||||
'name': 'd0d0caca0000ffffffffffffffffffffffffffff',
|
||||
'port': '1234',
|
||||
'host': 'runbotxx',
|
||||
'job_start': datetime.datetime.now(),
|
||||
'config_id': self.env.ref('runbot.runbot_build_config_default').id,
|
||||
'active_step': self.env.ref('runbot.runbot_build_config_step_run').id,
|
||||
'params_id': params.id
|
||||
})
|
||||
domain = [('repo_id', 'in', (self.repo.id, ))]
|
||||
domain_host = domain + [('host', '=', 'runbotxx')]
|
||||
build_ids = self.Build.search(domain_host + [('local_state', 'in', ['testing', 'running'])])
|
||||
mock_docker_state.return_value = 'UNKNOWN'
|
||||
self.assertEqual(build.local_state, 'testing')
|
||||
build_ids._schedule() # too fast, docker not started
|
||||
build._schedule() # too fast, docker not started
|
||||
self.assertEqual(build.local_state, 'testing')
|
||||
|
||||
build_ids.write({'job_start': datetime.datetime.now() - datetime.timedelta(seconds=70)}) # docker never started
|
||||
build_ids._schedule()
|
||||
build.write({'job_start': datetime.datetime.now() - datetime.timedelta(seconds=70)}) # docker never started
|
||||
build._schedule()
|
||||
self.assertEqual(build.local_state, 'done')
|
||||
self.assertEqual(build.local_result, 'ok')
|
||||
|
534
runbot/tests/test_upgrade.py
Normal file
534
runbot/tests/test_upgrade.py
Normal file
@ -0,0 +1,534 @@
|
||||
import logging
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import mute_logger
|
||||
from .common import RunbotCase
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestUpgradeFlow(RunbotCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.upgrade_flow_setup()
|
||||
|
||||
def upgrade_flow_setup(self):
|
||||
self.start_patcher('find_patcher', 'odoo.addons.runbot.common.find', 0)
|
||||
self.additionnal_setup()
|
||||
|
||||
self.master_bundle = self.branch_server.bundle_id
|
||||
self.config_test = self.env['runbot.build.config'].create({'name': 'Test'})
|
||||
#################
|
||||
# upgrade branch
|
||||
#################
|
||||
self.repo_upgrade = self.env['runbot.repo'].create({
|
||||
'name': 'upgrade',
|
||||
'project_id': self.project.id,
|
||||
'manifest_files': False,
|
||||
})
|
||||
self.remote_upgrade = self.env['runbot.remote'].create({
|
||||
'name': 'bla@example.com:base/upgrade',
|
||||
'repo_id': self.repo_upgrade.id,
|
||||
'token': '123',
|
||||
})
|
||||
self.branch_upgrade = self.Branch.create({
|
||||
'name': 'master',
|
||||
'remote_id': self.remote_upgrade.id,
|
||||
'is_pr': False,
|
||||
'head': self.Commit.create({
|
||||
'name': '123abc789',
|
||||
'repo_id': self.repo_upgrade.id,
|
||||
}).id,
|
||||
})
|
||||
|
||||
#######################
|
||||
# Basic upgrade config
|
||||
#######################
|
||||
self.step_restore = self.env['runbot.build.config.step'].create({
|
||||
'name': 'restore',
|
||||
'job_type': 'restore',
|
||||
'restore_rename_db_suffix': False
|
||||
})
|
||||
self.step_test_upgrade = self.env['runbot.build.config.step'].create({
|
||||
'name': 'test_upgrade',
|
||||
'job_type': 'test_upgrade',
|
||||
})
|
||||
self.test_upgrade_config = self.env['runbot.build.config'].create({
|
||||
'name': 'Upgrade server',
|
||||
'step_order_ids': [
|
||||
(0, 0, {'step_id': self.step_restore.id}),
|
||||
(0, 0, {'step_id': self.step_test_upgrade.id})
|
||||
]
|
||||
})
|
||||
|
||||
##########
|
||||
# Nightly
|
||||
##########
|
||||
self.nightly_category = self.env.ref('runbot.nightly_category')
|
||||
self.config_nightly = self.env['runbot.build.config'].create({'name': 'Nightly config'})
|
||||
self.config_nightly_db_generate = self.env['runbot.build.config'].create({'name': 'Nightly generate'})
|
||||
self.config_all = self.env['runbot.build.config'].create({'name': 'Demo'})
|
||||
self.config_all_no_demo = self.env['runbot.build.config'].create({'name': 'No demo'})
|
||||
self.trigger_server_nightly = self.env['runbot.trigger'].create({
|
||||
'name': 'Nighly server',
|
||||
'dependency_ids': [(4, self.repo_server.id)],
|
||||
'config_id': self.config_nightly.id,
|
||||
'project_id': self.project.id,
|
||||
'category_id': self.nightly_category.id
|
||||
})
|
||||
self.trigger_addons_nightly = self.env['runbot.trigger'].create({
|
||||
'name': 'Nighly addons',
|
||||
'dependency_ids': [(4, self.repo_server.id), (4, self.repo_addons.id)],
|
||||
'config_id': self.config_nightly.id,
|
||||
'project_id': self.project.id,
|
||||
'category_id': self.nightly_category.id
|
||||
})
|
||||
|
||||
##########
|
||||
# Weekly
|
||||
##########
|
||||
self.weekly_category = self.env.ref('runbot.weekly_category')
|
||||
self.config_weekly = self.env['runbot.build.config'].create({'name': 'Nightly config'})
|
||||
self.config_single = self.env['runbot.build.config'].create({'name': 'Single'})
|
||||
self.trigger_server_weekly = self.env['runbot.trigger'].create({
|
||||
'name': 'Nighly server',
|
||||
'dependency_ids': [(4, self.repo_server.id)],
|
||||
'config_id': self.config_weekly.id,
|
||||
'project_id': self.project.id,
|
||||
'category_id': self.weekly_category.id
|
||||
})
|
||||
self.trigger_addons_weekly = self.env['runbot.trigger'].create({
|
||||
'name': 'Nighly addons',
|
||||
'dependency_ids': [(4, self.repo_server.id), (4, self.repo_addons.id)],
|
||||
'config_id': self.config_weekly.id,
|
||||
'project_id': self.project.id,
|
||||
'category_id': self.weekly_category.id
|
||||
})
|
||||
|
||||
########################################
|
||||
# Configure upgrades for 'to current' version
|
||||
########################################
|
||||
master = self.env['runbot.version']._get('master')
|
||||
self.step_upgrade_server = self.env['runbot.build.config.step'].create({
|
||||
'name': 'upgrade_server',
|
||||
'job_type': 'configure_upgrade',
|
||||
'upgrade_to_current': True,
|
||||
'upgrade_from_previous_major_version': True,
|
||||
'upgrade_from_last_intermediate_version': True,
|
||||
'upgrade_flat': True,
|
||||
'upgrade_config_id': self.test_upgrade_config.id,
|
||||
'upgrade_dbs': [
|
||||
(0, 0, {'config_id': self.config_all.id, 'db_pattern': 'all', 'min_target_version_id': master.id}),
|
||||
(0, 0, {'config_id': self.config_all_no_demo.id, 'db_pattern': 'no-demo-all'})
|
||||
]
|
||||
})
|
||||
self.upgrade_server_config = self.env['runbot.build.config'].create({
|
||||
'name': 'Upgrade server',
|
||||
'step_order_ids': [(0, 0, {'step_id': self.step_upgrade_server.id})]
|
||||
})
|
||||
self.trigger_upgrade_server = self.env['runbot.trigger'].create({
|
||||
'name': 'Server upgrade',
|
||||
'repo_ids': [(4, self.repo_upgrade.id), (4, self.repo_server.id)],
|
||||
'config_id': self.upgrade_server_config.id,
|
||||
'project_id': self.project.id,
|
||||
'upgrade_dumps_trigger_id': self.trigger_server_nightly.id,
|
||||
})
|
||||
|
||||
########################################
|
||||
# Configure upgrades for previouses versions
|
||||
########################################
|
||||
self.step_upgrade = self.env['runbot.build.config.step'].create({
|
||||
'name': 'upgrade',
|
||||
'job_type': 'configure_upgrade',
|
||||
'upgrade_to_major_versions': True,
|
||||
'upgrade_from_previous_major_version': True,
|
||||
'upgrade_flat': True,
|
||||
'upgrade_config_id': self.test_upgrade_config.id,
|
||||
'upgrade_dbs': [
|
||||
(0, 0, {'config_id': self.config_all.id, 'db_pattern': 'all', 'min_target_version_id': master.id}),
|
||||
(0, 0, {'config_id': self.config_all_no_demo.id, 'db_pattern': 'no-demo-all'})
|
||||
]
|
||||
})
|
||||
self.upgrade_config = self.env['runbot.build.config'].create({
|
||||
'name': 'Upgrade',
|
||||
'step_order_ids': [(0, 0, {'step_id': self.step_upgrade.id})]
|
||||
})
|
||||
self.trigger_upgrade = self.env['runbot.trigger'].create({
|
||||
'name': 'Upgrade',
|
||||
'repo_ids': [(4, self.repo_upgrade.id)],
|
||||
'config_id': self.upgrade_config.id,
|
||||
'project_id': self.project.id,
|
||||
'upgrade_dumps_trigger_id': self.trigger_addons_nightly.id,
|
||||
})
|
||||
|
||||
self.branch_upgrade.bundle_id # force recompute TODO remove this once fixed
|
||||
|
||||
with mute_logger('odoo.addons.runbot.models.commit'):
|
||||
self.build_niglty_master, self.build_weekly_master = self.create_version('master')
|
||||
self.build_niglty_11, self.build_weekly_11 = self.create_version('11.0')
|
||||
self.build_niglty_113, self.build_weekly_113 = self.create_version('saas-11.3')
|
||||
self.build_niglty_12, self.build_weekly_12 = self.create_version('12.0')
|
||||
self.build_niglty_123, self.build_weekly_123 = self.create_version('saas-12.3')
|
||||
self.build_niglty_13, self.build_weekly_13 = self.create_version('13.0')
|
||||
self.build_niglty_131, self.build_weekly_131 = self.create_version('saas-13.1')
|
||||
self.build_niglty_132, self.build_weekly_132 = self.create_version('saas-13.2')
|
||||
self.build_niglty_133, self.build_weekly_133 = self.create_version('saas-13.3')
|
||||
|
||||
def create_version(self, name):
|
||||
intname = int(''.join(c for c in name if c.isdigit())) if name != 'master' else 0
|
||||
if name != 'master':
|
||||
branch_server = self.Branch.create({
|
||||
'name': name,
|
||||
'remote_id': self.remote_server.id,
|
||||
'is_pr': False,
|
||||
'head': self.Commit.create({
|
||||
'name': 'server%s' % intname,
|
||||
'repo_id': self.repo_server.id,
|
||||
}).id,
|
||||
})
|
||||
branch_addons = self.Branch.create({
|
||||
'name': name,
|
||||
'remote_id': self.remote_addons.id,
|
||||
'is_pr': False,
|
||||
'head': self.Commit.create({
|
||||
'name': 'addons%s' % intname,
|
||||
'repo_id': self.repo_addons.id,
|
||||
}).id,
|
||||
})
|
||||
else:
|
||||
branch_server = self.branch_server
|
||||
branch_addons = self.branch_addons
|
||||
|
||||
self.assertEqual(branch_server.bundle_id, branch_addons.bundle_id)
|
||||
bundle = branch_server.bundle_id
|
||||
self.assertEqual(bundle.name, name)
|
||||
bundle.is_base = True
|
||||
# create nightly
|
||||
|
||||
batch_nigthly = bundle._force(self.nightly_category.id)
|
||||
self.assertEqual(batch_nigthly.category_id, self.nightly_category)
|
||||
builds_nigthly = {}
|
||||
host = self.env['runbot.host']._get_current()
|
||||
for build in batch_nigthly.slot_ids.mapped('build_id'):
|
||||
self.assertEqual(build.params_id.config_id, self.config_nightly)
|
||||
main_child = build._add_child({'config_id': self.config_nightly_db_generate.id})
|
||||
demo = main_child._add_child({'config_id': self.config_all.id})
|
||||
demo.database_ids = [
|
||||
(0, 0, {'name': '%s-%s' % (demo.dest, 'base')}),
|
||||
(0, 0, {'name': '%s-%s' % (demo.dest, 'dummy')}),
|
||||
(0, 0, {'name': '%s-%s' % (demo.dest, 'all')})]
|
||||
demo.host = host.name
|
||||
no_demo = main_child._add_child({'config_id': self.config_all_no_demo.id})
|
||||
no_demo.database_ids = [
|
||||
(0, 0, {'name': '%s-%s' % (no_demo.dest, 'base')}),
|
||||
(0, 0, {'name': '%s-%s' % (no_demo.dest, 'dummy')}),
|
||||
(0, 0, {'name': '%s-%s' % (no_demo.dest, 'no-demo-all')})]
|
||||
no_demo.host = host.name
|
||||
(build | main_child | demo | no_demo).write({'local_state': 'done'})
|
||||
builds_nigthly[('root', build.params_id.trigger_id)] = build
|
||||
builds_nigthly[('demo', build.params_id.trigger_id)] = demo
|
||||
builds_nigthly[('no_demo', build.params_id.trigger_id)] = no_demo
|
||||
batch_nigthly.state = 'done'
|
||||
|
||||
batch_weekly = bundle._force(self.weekly_category.id)
|
||||
self.assertEqual(batch_weekly.category_id, self.weekly_category)
|
||||
builds_weekly = {}
|
||||
build = batch_weekly.slot_ids.filtered(lambda s: s.trigger_id == self.trigger_addons_weekly).build_id
|
||||
build.database_ids = [(0, 0, {'name': '%s-%s' % (build.dest, 'dummy')})]
|
||||
self.assertEqual(build.params_id.config_id, self.config_weekly)
|
||||
builds_weekly[('root', build.params_id.trigger_id)] = build
|
||||
for db in ['l10n_be', 'l10n_ch', 'mail', 'account', 'stock']:
|
||||
child = build._add_child({'config_id': self.config_single.id})
|
||||
child.database_ids = [(0, 0, {'name': '%s-%s' % (child.dest, db)})]
|
||||
child.local_state = 'done'
|
||||
child.host = host.name
|
||||
builds_weekly[(db, build.params_id.trigger_id)] = child
|
||||
build.local_state = 'done'
|
||||
batch_weekly.state = 'done'
|
||||
|
||||
batch_default = bundle._force()
|
||||
build = batch_default.slot_ids.filtered(lambda s: s.trigger_id == self.trigger_server).build_id
|
||||
build.local_state = 'done'
|
||||
batch_default.state = 'done'
|
||||
|
||||
return builds_nigthly, builds_weekly
|
||||
|
||||
def test_all(self):
|
||||
# Test setup
|
||||
self.assertEqual(self.branch_server.bundle_id, self.branch_upgrade.bundle_id)
|
||||
self.assertTrue(self.branch_upgrade.bundle_id.is_base)
|
||||
self.assertTrue(self.branch_upgrade.bundle_id.version_id)
|
||||
self.assertEqual(self.trigger_upgrade_server.upgrade_step_id, self.step_upgrade_server)
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.step_upgrade_server.job_type = 'install_odoo'
|
||||
self.trigger_upgrade_server.flush(['upgrade_step_id'])
|
||||
|
||||
batch = self.master_bundle._force()
|
||||
upgrade_current_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade_server).build_id
|
||||
host = self.env['runbot.host']._get_current()
|
||||
upgrade_current_build.host = host.name
|
||||
upgrade_current_build._init_pendings(host)
|
||||
upgrade_current_build._schedule()
|
||||
self.assertEqual(upgrade_current_build.local_state, 'done')
|
||||
self.assertEqual(len(upgrade_current_build.children_ids), 4)
|
||||
|
||||
[b_13_master_demo, b_13_master_no_demo, b_133_master_demo, b_133_master_no_demo] = upgrade_current_build.children_ids
|
||||
|
||||
def assertOk(build, t, f, b_type, db_suffix, trigger):
|
||||
self.assertEqual(build.params_id.upgrade_to_build_id, t)
|
||||
self.assertEqual(build.params_id.upgrade_from_build_id, f[('root', trigger)])
|
||||
self.assertEqual(build.params_id.dump_db.build_id, f[(b_type, trigger)])
|
||||
self.assertEqual(build.params_id.dump_db.db_suffix, db_suffix)
|
||||
self.assertEqual(build.params_id.config_id, self.test_upgrade_config)
|
||||
|
||||
assertOk(b_13_master_demo, upgrade_current_build, self.build_niglty_13, 'demo', 'all', self.trigger_server_nightly)
|
||||
assertOk(b_13_master_no_demo, upgrade_current_build, self.build_niglty_13, 'no_demo', 'no-demo-all', self.trigger_server_nightly)
|
||||
assertOk(b_133_master_demo, upgrade_current_build, self.build_niglty_133, 'demo', 'all', self.trigger_server_nightly)
|
||||
assertOk(b_133_master_no_demo, upgrade_current_build, self.build_niglty_133, 'no_demo', 'no-demo-all', self.trigger_server_nightly)
|
||||
|
||||
self.assertEqual(b_13_master_demo.params_id.commit_ids.repo_id, self.repo_server | self.repo_upgrade)
|
||||
|
||||
# upgrade repos tests
|
||||
upgrade_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade).build_id
|
||||
host = self.env['runbot.host']._get_current()
|
||||
upgrade_build.host = host.name
|
||||
upgrade_build._init_pendings(host)
|
||||
upgrade_build._schedule()
|
||||
self.assertEqual(upgrade_build.local_state, 'done')
|
||||
self.assertEqual(len(upgrade_build.children_ids), 2)
|
||||
|
||||
[b_11_12, b_12_13] = upgrade_build.children_ids
|
||||
|
||||
assertOk(b_11_12, self.build_niglty_12[('root', self.trigger_addons_nightly)], self.build_niglty_11, 'no_demo', 'no-demo-all', self.trigger_addons_nightly)
|
||||
assertOk(b_12_13, self.build_niglty_13[('root', self.trigger_addons_nightly)], self.build_niglty_12, 'no_demo', 'no-demo-all', self.trigger_addons_nightly)
|
||||
|
||||
step_upgrade_nightly = self.env['runbot.build.config.step'].create({
|
||||
'name': 'upgrade_nightly',
|
||||
'job_type': 'configure_upgrade',
|
||||
'upgrade_to_master': True,
|
||||
'upgrade_to_major_versions': True,
|
||||
'upgrade_from_previous_major_version': True,
|
||||
'upgrade_from_all_intermediate_version': True,
|
||||
'upgrade_flat': False,
|
||||
'upgrade_config_id': self.test_upgrade_config.id,
|
||||
'upgrade_dbs': [
|
||||
(0, 0, {'config_id': self.config_single.id, 'db_pattern': '*'})
|
||||
]
|
||||
})
|
||||
upgrade_config_nightly = self.env['runbot.build.config'].create({
|
||||
'name': 'Upgrade nightly',
|
||||
'step_order_ids': [(0, 0, {'step_id': step_upgrade_nightly.id})]
|
||||
})
|
||||
trigger_upgrade_addons_nightly = self.env['runbot.trigger'].create({
|
||||
'name': 'Nigtly upgrade',
|
||||
'config_id': upgrade_config_nightly.id,
|
||||
'project_id': self.project.id,
|
||||
'dependency_ids': [(4, self.repo_upgrade.id)],
|
||||
'upgrade_dumps_trigger_id': self.trigger_addons_weekly.id,
|
||||
'category_id': self.nightly_category.id
|
||||
})
|
||||
|
||||
batch = self.master_bundle._force(self.nightly_category.id)
|
||||
upgrade_nightly = batch.slot_ids.filtered(lambda slot: slot.trigger_id == trigger_upgrade_addons_nightly).build_id
|
||||
host = self.env['runbot.host']._get_current()
|
||||
upgrade_nightly.host = host.name
|
||||
upgrade_nightly._init_pendings(host)
|
||||
upgrade_nightly._schedule()
|
||||
to_version_builds = upgrade_nightly.children_ids
|
||||
self.assertEqual(upgrade_nightly.local_state, 'done')
|
||||
self.assertEqual(len(to_version_builds), 4)
|
||||
self.assertEqual(
|
||||
to_version_builds.mapped('params_id.upgrade_to_build_id.params_id.version_id.name'),
|
||||
['11.0', '12.0', '13.0', 'master']
|
||||
)
|
||||
self.assertEqual(
|
||||
to_version_builds.mapped('params_id.upgrade_from_build_id.params_id.version_id.name'),
|
||||
[]
|
||||
)
|
||||
to_version_builds.host = host.name
|
||||
to_version_builds._init_pendings(host)
|
||||
to_version_builds._schedule()
|
||||
self.assertEqual(to_version_builds.mapped('local_state'), ['done']*4)
|
||||
from_version_builds = to_version_builds.children_ids
|
||||
self.assertEqual(
|
||||
[
|
||||
'%s->%s' % (
|
||||
b.params_id.upgrade_from_build_id.params_id.version_id.name,
|
||||
b.params_id.upgrade_to_build_id.params_id.version_id.name
|
||||
)
|
||||
for b in from_version_builds
|
||||
],
|
||||
['11.0->12.0', 'saas-11.3->12.0', '12.0->13.0', 'saas-12.3->13.0', '13.0->master', 'saas-13.1->master', 'saas-13.2->master', 'saas-13.3->master']
|
||||
)
|
||||
from_version_builds.host = host.name
|
||||
from_version_builds._init_pendings(host)
|
||||
from_version_builds._schedule()
|
||||
self.assertEqual(from_version_builds.mapped('local_state'), ['done']*8)
|
||||
db_builds = from_version_builds.children_ids
|
||||
self.assertEqual(len(db_builds), 40)
|
||||
|
||||
self.assertEqual(
|
||||
db_builds.mapped('params_id.config_id'), self.test_upgrade_config
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
db_builds.mapped('params_id.commit_ids.repo_id'),
|
||||
self.repo_upgrade,
|
||||
"Build should only have the upgrade commit"
|
||||
)
|
||||
b11_12 = db_builds[:5]
|
||||
self.assertEqual(
|
||||
b11_12.mapped('params_id.upgrade_to_build_id.params_id.version_id.name'),
|
||||
['12.0']
|
||||
)
|
||||
self.assertEqual(
|
||||
b11_12.mapped('params_id.upgrade_from_build_id.params_id.version_id.name'),
|
||||
['11.0']
|
||||
)
|
||||
b133_master = db_builds[-5:]
|
||||
self.assertEqual(
|
||||
b133_master.mapped('params_id.upgrade_to_build_id.params_id.version_id.name'),
|
||||
['master']
|
||||
)
|
||||
self.assertEqual(
|
||||
b133_master.mapped('params_id.upgrade_from_build_id.params_id.version_id.name'),
|
||||
['saas-13.3']
|
||||
)
|
||||
self.assertEqual(
|
||||
[b.params_id.dump_db.db_suffix for b in b133_master],
|
||||
['account', 'l10n_be', 'l10n_ch', 'mail', 'stock'] # is this order ok?
|
||||
)
|
||||
|
||||
first_build = db_builds[0]
|
||||
|
||||
self.start_patcher('docker_state', 'odoo.addons.runbot.models.build.docker_state', 'END')
|
||||
|
||||
def docker_run_restore(cmd, *args, **kwargs):
|
||||
source_dest = first_build.params_id.dump_db.build_id.dest
|
||||
self.assertEqual(
|
||||
str(cmd),
|
||||
' && '.join([
|
||||
'mkdir /data/build/restore',
|
||||
'cd /data/build/restore',
|
||||
'wget {dump_url}',
|
||||
'unzip -q {zip_name}',
|
||||
'echo "### restoring filestore"',
|
||||
'mkdir -p /data/build/datadir/filestore/{db_name}',
|
||||
'mv filestore/* /data/build/datadir/filestore/{db_name}',
|
||||
'echo "###restoring db"',
|
||||
'psql -q {db_name} < dump.sql',
|
||||
'cd /data/build',
|
||||
'echo "### cleaning"',
|
||||
'rm -r restore',
|
||||
'echo "### listing modules"',
|
||||
'psql {db_name} -c "select name from ir_module_module where state = \'installed\'" -t -A > /data/build/logs/restore_modules_installed.txt'
|
||||
]).format(
|
||||
dump_url='http://host.runbot.com/runbot/static/build/%s/logs/%s-account.zip' % (source_dest, source_dest),
|
||||
zip_name='%s-account.zip' % source_dest,
|
||||
db_name='%s-master-account' % str(first_build.id).zfill(5),
|
||||
)
|
||||
)
|
||||
self.patchers['docker_run'].side_effect = docker_run_restore
|
||||
first_build.host = host.name
|
||||
first_build._init_pendings(host)
|
||||
self.patchers['docker_run'].assert_called()
|
||||
|
||||
def docker_run_upgrade(cmd, *args, ro_volumes=False, **kwargs):
|
||||
self.assertEqual(
|
||||
ro_volumes, {
|
||||
'addons': '/tmp/runbot_test/static/sources/addons/addons120',
|
||||
'server': '/tmp/runbot_test/static/sources/server/server120',
|
||||
'upgrade': '/tmp/runbot_test/static/sources/upgrade/123abc789'
|
||||
},
|
||||
"other commit should have been added automaticaly"
|
||||
)
|
||||
self.assertEqual(
|
||||
str(cmd),
|
||||
'python3 server/server.py {addons_path} --no-xmlrpcs --no-netrpc -u all -d {db_name} --stop-after-init --max-cron-threads=0'.format(
|
||||
addons_path='--addons-path addons,server/addons,server/core/addons',
|
||||
db_name='%s-master-account' % str(first_build.id).zfill(5))
|
||||
)
|
||||
self.patchers['docker_run'].side_effect = docker_run_upgrade
|
||||
first_build._schedule()
|
||||
self.assertEqual(self.patchers['docker_run'].call_count, 2)
|
||||
|
||||
# test_build_references
|
||||
batch = self.master_bundle._force()
|
||||
upgrade_slot = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade_server)
|
||||
self.assertTrue(upgrade_slot)
|
||||
upgrade_build = upgrade_slot.build_id
|
||||
self.assertTrue(upgrade_build)
|
||||
self.assertEqual(upgrade_build.params_id.config_id, self.upgrade_server_config)
|
||||
# we should have 2 builds, the nightly roots of 13 and 13.3
|
||||
self.assertEqual(
|
||||
upgrade_build.params_id.builds_reference_ids,
|
||||
(
|
||||
self.build_niglty_13[('root', self.trigger_server_nightly)] |
|
||||
self.build_niglty_133[('root', self.trigger_server_nightly)]
|
||||
)
|
||||
)
|
||||
|
||||
self.trigger_upgrade_server.upgrade_step_id.upgrade_from_all_intermediate_version = True
|
||||
batch = self.master_bundle._force()
|
||||
upgrade_build = batch.slot_ids.filtered(lambda slot: slot.trigger_id == self.trigger_upgrade_server).build_id
|
||||
self.assertEqual(
|
||||
upgrade_build.params_id.builds_reference_ids,
|
||||
(
|
||||
self.build_niglty_13[('root', self.trigger_server_nightly)] |
|
||||
self.build_niglty_131[('root', self.trigger_server_nightly)] |
|
||||
self.build_niglty_132[('root', self.trigger_server_nightly)] |
|
||||
self.build_niglty_133[('root', self.trigger_server_nightly)]
|
||||
)
|
||||
)
|
||||
|
||||
# test future upgrades
|
||||
step_upgrade_complement = self.env['runbot.build.config.step'].create({
|
||||
'name': 'upgrade_complement',
|
||||
'job_type': 'configure_upgrade_complement',
|
||||
'upgrade_config_id': self.test_upgrade_config.id,
|
||||
})
|
||||
|
||||
config_upgrade_complement = self.env['runbot.build.config'].create({
|
||||
'name': 'Stable policy',
|
||||
'step_order_ids': [(0, 0, {'step_id': step_upgrade_complement.id})]
|
||||
})
|
||||
trigger_upgrade_complement = self.env['runbot.trigger'].create({
|
||||
'name': 'Stable policy',
|
||||
'repo_ids': [(4, self.repo_server.id)],
|
||||
'dependency_ids': [(4, self.repo_upgrade.id)],
|
||||
'config_id': config_upgrade_complement.id,
|
||||
'upgrade_dumps_trigger_id': self.trigger_upgrade_server.id,
|
||||
'project_id': self.project.id,
|
||||
})
|
||||
|
||||
bundle_13 = self.master_bundle.previous_major_version_base_id
|
||||
bundle_133 = self.master_bundle.intermediate_version_base_ids[-1]
|
||||
self.assertEqual(bundle_13.name, '13.0')
|
||||
self.assertEqual(bundle_133.name, 'saas-13.3')
|
||||
|
||||
batch13 = bundle_13._force()
|
||||
upgrade_complement_build_13 = batch13.slot_ids.filtered(lambda slot: slot.trigger_id == trigger_upgrade_complement).build_id
|
||||
upgrade_complement_build_13.host = host.name
|
||||
self.assertEqual(upgrade_complement_build_13.params_id.config_id, config_upgrade_complement)
|
||||
for db in ['base', 'all', 'no-demo-all']:
|
||||
upgrade_complement_build_13.database_ids = [(0, 0, {'name': '%s-%s' % (upgrade_complement_build_13.dest, db)})]
|
||||
|
||||
upgrade_complement_build_13._init_pendings(host)
|
||||
|
||||
self.assertEqual(len(upgrade_complement_build_13.children_ids), 5)
|
||||
master_child = upgrade_complement_build_13.children_ids[0]
|
||||
self.assertEqual(master_child.params_id.upgrade_from_build_id, upgrade_complement_build_13)
|
||||
self.assertEqual(master_child.params_id.dump_db.db_suffix, 'all')
|
||||
self.assertEqual(master_child.params_id.config_id, self.test_upgrade_config)
|
||||
self.assertEqual(master_child.params_id.upgrade_to_build_id.params_id.version_id.name, 'master')
|
||||
|
||||
class TestUpgrade(RunbotCase):
|
||||
|
||||
def test_exceptions_in_env(self):
|
||||
env_var = self.env['runbot.upgrade.exception']._generate()
|
||||
self.assertEqual(env_var, False)
|
||||
self.env['runbot.upgrade.exception'].create({'elements': 'field:module.some_field \nview:some_view_xmlid'})
|
||||
self.env['runbot.upgrade.exception'].create({'elements': 'field:module.some_field2'})
|
||||
env_var = self.env['runbot.upgrade.exception']._generate()
|
||||
self.assertEqual(env_var, 'suppress_upgrade_warnings=field:module.some_field,view:some_view_xmlid,field:module.some_field2')
|
61
runbot/tests/test_version.py
Normal file
61
runbot/tests/test_version.py
Normal file
@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .common import RunbotCase
|
||||
|
||||
|
||||
class TestVersion(RunbotCase):
|
||||
|
||||
def test_basic_version(self):
|
||||
|
||||
major_version = self.Version.create({'name': '12.0'})
|
||||
self.assertEqual(major_version.number, '12.00')
|
||||
self.assertTrue(major_version.is_major)
|
||||
|
||||
saas_version = self.Version.create({'name': 'saas-12.1'})
|
||||
self.assertEqual(saas_version.number, '12.01')
|
||||
self.assertFalse(saas_version.is_major)
|
||||
|
||||
self.assertGreater(saas_version.number, major_version.number)
|
||||
|
||||
master_version = self.Version.create({'name': 'master'})
|
||||
self.assertEqual(master_version.number, '~')
|
||||
self.assertGreater(master_version.number, saas_version.number)
|
||||
|
||||
def test_version_relations(self):
|
||||
version = self.env['runbot.version']
|
||||
v11 = version._get('11.0')
|
||||
v113 = version._get('saas-11.3')
|
||||
v12 = version._get('12.0')
|
||||
v122 = version._get('saas-12.2')
|
||||
v124 = version._get('saas-12.4')
|
||||
v13 = version._get('13.0')
|
||||
v131 = version._get('saas-13.1')
|
||||
v132 = version._get('saas-13.2')
|
||||
v133 = version._get('saas-13.3')
|
||||
master = version._get('master')
|
||||
|
||||
self.assertEqual(v11.previous_major_version_id, version)
|
||||
self.assertEqual(v11.intermediate_version_ids, version)
|
||||
|
||||
self.assertEqual(v113.previous_major_version_id, v11)
|
||||
self.assertEqual(v113.intermediate_version_ids, version)
|
||||
|
||||
self.assertEqual(v12.previous_major_version_id, v11)
|
||||
self.assertEqual(v12.intermediate_version_ids, v113)
|
||||
|
||||
self.assertEqual(v12.previous_major_version_id, v11)
|
||||
self.assertEqual(v12.intermediate_version_ids, v113)
|
||||
self.assertEqual(v12.next_major_version_id, v13)
|
||||
self.assertEqual(v12.next_intermediate_version_ids, v124 | v122)
|
||||
|
||||
self.assertEqual(v13.previous_major_version_id, v12)
|
||||
self.assertEqual(v13.intermediate_version_ids, v124 | v122)
|
||||
self.assertEqual(v13.next_major_version_id, master)
|
||||
self.assertEqual(v13.next_intermediate_version_ids, v133 | v132 | v131)
|
||||
|
||||
self.assertEqual(v132.previous_major_version_id, v13)
|
||||
self.assertEqual(v132.intermediate_version_ids, v131)
|
||||
self.assertEqual(v132.next_major_version_id, master)
|
||||
self.assertEqual(v132.next_intermediate_version_ids, v133)
|
||||
|
||||
self.assertEqual(master.previous_major_version_id, v13)
|
||||
self.assertEqual(master.intermediate_version_ids, v133 | v132 | v131)
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="assets_front_end" inherit_id="web.assets_frontend" name="runbot assets">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/runbot/static/src/css/runbot.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
@ -9,34 +9,16 @@
|
||||
<button name="recompute_infos" string="Recompute Infos" type="object" class="oe_highlight"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_request_branch_rebuild" type="object" class="oe_stat_button" icon="fa-play">
|
||||
<field name="rebuild_requested" widget="boolean_button" options='{"terminology": {
|
||||
"string_true": "Rebuild Asked",
|
||||
"hover_true": "Cancel the branch rebuild",
|
||||
"string_false": "No Rebuild Asked",
|
||||
"hover_false": "Request a branch rebuild"
|
||||
}}'/>
|
||||
</button>
|
||||
</div>
|
||||
<group name="branch_group">
|
||||
<field name="repo_id"/>
|
||||
<field name="duplicate_repo_id" invisible='1'/>
|
||||
<field name="bundle_id" readonly='0'/>
|
||||
<field name="remote_id"/>
|
||||
<field name="name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="branch_url"/>
|
||||
<field name="is_pr"/>
|
||||
<field name="pull_head_name"/>
|
||||
<field name="target_branch_name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="priority"/>
|
||||
<field name="state"/>
|
||||
<field name="modules"/>
|
||||
<field name="config_id"/>
|
||||
<field name="no_build"/>
|
||||
<field name="closest_sticky"/>
|
||||
<field name="defined_sticky" domain="[('sticky', '=', True), ('repo_id', 'in', [repo_id, duplicate_repo_id])]"/>
|
||||
<field name="previous_version"/>
|
||||
<field name="intermediate_stickies" widget="many2many_tags"/>
|
||||
<field name="head"/>
|
||||
<field name="alive"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@ -49,12 +31,8 @@
|
||||
<field name="model">runbot.branch</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Branches">
|
||||
<field name="repo_id"/>
|
||||
<field name="remote_id"/>
|
||||
<field name="name"/>
|
||||
<field name="sticky"/>
|
||||
<field name="priority"/>
|
||||
<field name="config_id"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@ -65,6 +43,8 @@
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Runbot" id="runbot_menu_root"/>
|
||||
|
||||
<menuitem
|
||||
name="Branches"
|
||||
id="runbot_menu_branch_tree"
|
||||
|
@ -19,8 +19,8 @@
|
||||
<field name="fixing_commit"/>
|
||||
<field name="active"/>
|
||||
<field name="parent_id" />
|
||||
<field name="branch_ids" widget="many2many_tags"/>
|
||||
<field name="repo_ids" widget="many2many_tags"/>
|
||||
<field name="bundle_ids" widget="many2many_tags"/>
|
||||
<field name="trigger_ids" widget="many2many_tags"/>
|
||||
<field name="tag_ids" widget="many2many_tags"/>
|
||||
<field name="first_seen_date"/>
|
||||
<field name="first_seen_build_id"/>
|
||||
@ -35,8 +35,6 @@
|
||||
<field name="create_date"/>
|
||||
<field name="id"/>
|
||||
<field name="host" groups="base.group_no_one"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="dest"/>
|
||||
<field name="build_url" widget="url" readonly="1" text="View build"/>
|
||||
</tree>
|
||||
@ -48,8 +46,6 @@
|
||||
<field name="create_date"/>
|
||||
<field name="id"/>
|
||||
<field name="host" groups="base.group_no_one"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="dest"/>
|
||||
<field name="build_url" widget="url" readonly="1" text="View build"/>
|
||||
</tree>
|
||||
|
@ -1,28 +1,44 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="build_form_params" model="ir.ui.view">
|
||||
<field name="model">runbot.build.params</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Build Params">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="config_id"/>
|
||||
<field name="config_data"/>
|
||||
<field name="version_id"/>
|
||||
<field name="extra_params"/>
|
||||
<field name="commit_link_ids">
|
||||
<tree>
|
||||
<field name="commit_id"/>
|
||||
<field name="match_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_build_params_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.build.params</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Build params">
|
||||
<field name="config_id"/>
|
||||
<field name="version_id"/>
|
||||
<field name="commit_link_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="build_form" model="ir.ui.view">
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Build">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="repo_id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="name"/>
|
||||
<field name="dependency_ids">
|
||||
<tree>
|
||||
<field name="dependecy_repo_id"/>
|
||||
<field name="dependency_hash"/>
|
||||
<field name="closest_branch_id"/>
|
||||
<field name="match_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="date"/>
|
||||
<field name="author"/>
|
||||
<field name="author_email" groups="base.group_no_one"/>
|
||||
<field name="committer" groups="base.group_no_one"/>
|
||||
<field name="committer_email" groups="base.group_no_one"/>
|
||||
<field name="subject"/>
|
||||
<field name="description"/>
|
||||
<field name="params_id"/>
|
||||
<field name="port" groups="base.group_no_one"/>
|
||||
<field name="dest"/>
|
||||
<field name="local_state"/>
|
||||
@ -39,13 +55,9 @@
|
||||
<field name="build_end" groups="base.group_no_one"/>
|
||||
<field name="build_time" groups="base.group_no_one"/>
|
||||
<field name="build_age" groups="base.group_no_one"/>
|
||||
<field name="duplicate_id"/>
|
||||
<field name="build_type"/>
|
||||
<field name="config_id"/>
|
||||
<field name="config_data"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="orphan_result"/>
|
||||
<field name="hidden" groups="base.group_no_one"/>
|
||||
<field name="build_url" widget="url" readonly="1"/>
|
||||
<field name="keep_running"/>
|
||||
<field name="gc_date" readonly="1"/>
|
||||
@ -70,11 +82,7 @@
|
||||
<field name="model">runbot.build</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Builds">
|
||||
<field name="repo_id"/>
|
||||
<field name="dest"/>
|
||||
<field name="date"/>
|
||||
<field name="author"/>
|
||||
<field name="committer"/>
|
||||
<field name="global_state"/>
|
||||
<field name="global_result"/>
|
||||
<field name="port"/>
|
||||
@ -101,8 +109,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search builds">
|
||||
<field name="id"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="name"/>
|
||||
<field name="global_state"/>
|
||||
<field name="dest"/>
|
||||
<filter string="Pending" name='pending' domain="[('global_state','=', 'pending')]"/>
|
||||
@ -111,8 +117,6 @@
|
||||
<filter string="Done" name='done' domain="[('global_state','=','done')]"/>
|
||||
<filter string="Duplicate" name='duplicate' domain="[('local_state','=', 'duplicate')]"/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Repo" name='repo' domain="[]" context="{'group_by':'repo_id'}"/>
|
||||
<filter string="Branch" name='branch' domain="[]" context="{'group_by':'branch_id'}"/>
|
||||
<filter string="Status" name='status' domain="[]" context="{'group_by':'global_state'}"/>
|
||||
<filter string="Result" name='result' domain="[]" context="{'group_by':'global_result'}"/>
|
||||
<filter string="Start" name='start' domain="[]" context="{'group_by':'job_start'}"/>
|
||||
@ -128,6 +132,14 @@
|
||||
<field name="res_model">runbot.build</field>
|
||||
<field name="view_mode">tree,form,graph,pivot</field>
|
||||
</record>
|
||||
<menuitem id="menu_build" action="action_build" parent="runbot_menu_root"/>
|
||||
<record id="action_build_params" model="ir.actions.act_window">
|
||||
<field name="name">Builds Params</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.build.params</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<menuitem id="menu_build" name="Build" parent="runbot_menu_root"/>
|
||||
<menuitem id="menu_build_build" action="action_build" parent="menu_build"/>
|
||||
<menuitem id="menu_build_params" action="action_build_params" parent="menu_build"/>
|
||||
</data>
|
||||
</odoo>
|
155
runbot/views/bundle_views.xml
Normal file
155
runbot/views/bundle_views.xml
Normal file
@ -0,0 +1,155 @@
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="view_runbot_project" model="ir.ui.view">
|
||||
<field name="model">runbot.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Projects">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="group_ids"/>
|
||||
<field name="trigger_ids"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_runbot_bundle" model="ir.ui.view">
|
||||
<field name="model">runbot.bundle</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Bundles">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
</div>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="project_id"/>
|
||||
<field name="sticky" readonly="0"/>
|
||||
<field name="is_base"/>
|
||||
<field name="base_id"/>
|
||||
<field name="defined_base_id"/>
|
||||
<field name="version_id"/>
|
||||
<field name="no_build"/>
|
||||
<field name="no_auto_run"/>
|
||||
<field name="priority"/>
|
||||
<field name="build_all"/>
|
||||
<field name="branch_ids">
|
||||
<tree>
|
||||
<field name="dname"/>
|
||||
<field name="remote_id"/>
|
||||
<field name="pull_head_name"/>
|
||||
<field name="target_branch_name"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field string="Trigger customisations" name="trigger_custom_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="trigger_id" domain="[('project_id', '=', parent.project_id)]"/>
|
||||
<field name="config_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field string="Last batches" name="last_batchs">
|
||||
<tree>
|
||||
<field name="state"/>
|
||||
<field name="commit_link_ids"/>
|
||||
<field name="slot_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_runbot_bundle_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.bundle</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Bundle">
|
||||
<field name="project_id"/>
|
||||
<field name="name"/>
|
||||
<field name="version_number"/>
|
||||
<field name="is_base"/>
|
||||
<field name="sticky"/>
|
||||
<field name="no_build"/>
|
||||
<field name="branch_ids"/>
|
||||
<field name="version_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_runbot_batch" model="ir.ui.view">
|
||||
<field name="model">runbot.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Batch">
|
||||
<group>
|
||||
<field name="last_update"/>
|
||||
<field name="bundle_id"/>
|
||||
<field name="state"/>
|
||||
<field name="commit_link_ids">
|
||||
<tree>
|
||||
<field name="commit_id"/>
|
||||
<field name="match_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="slot_ids">
|
||||
<tree>
|
||||
<field name="trigger_id"/>
|
||||
<field name="build_id"/>
|
||||
<field name="link_type"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_runbot_batch_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.batch</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Batchs">
|
||||
<field name="bundle_id"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_runbot_version_tree" model="ir.ui.view">
|
||||
<field name="model">runbot.version</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Version">
|
||||
<field name="name"/>
|
||||
<field name="number"/>
|
||||
<field name="is_major"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_bundle" model="ir.actions.act_window">
|
||||
<field name="name">Bundles</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.bundle</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<record id="action_bundle_project" model="ir.actions.act_window">
|
||||
<field name="name">Projects</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.project</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<record id="action_bundle_version" model="ir.actions.act_window">
|
||||
<field name="name">Versions</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.version</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<record id="action_bundle_batch" model="ir.actions.act_window">
|
||||
<field name="name">Batches</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">runbot.batch</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_bundle" name="Bundle" parent="runbot_menu_root"/>
|
||||
<menuitem id="menu_bundle_bundle" action="action_bundle" parent="menu_bundle"/>
|
||||
<menuitem id="menu_bundle_project" action="action_bundle_project" parent="menu_bundle"/>
|
||||
<menuitem id="menu_bundle_version" action="action_bundle_version" parent="menu_bundle"/>
|
||||
<menuitem id="menu_bundle_batch" action="action_bundle_batch" parent="menu_bundle"/>
|
||||
</data>
|
||||
</odoo>
|
31
runbot/views/commit_views.xml
Normal file
31
runbot/views/commit_views.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="commit_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.commit.tree</field>
|
||||
<field name="model">runbot.commit</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Commits">
|
||||
<field name="name"/>
|
||||
<field name="date"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="author_email"/>
|
||||
<field name="committer_email"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_commit_tree" model="ir.actions.act_window">
|
||||
<field name="name">Commits</field>
|
||||
<field name="res_model">runbot.commit</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Commits"
|
||||
id="runbot_menu_commit_tree"
|
||||
parent="runbot_menu_root"
|
||||
sequence="20"
|
||||
action="open_view_commit_tree"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
@ -15,15 +15,11 @@
|
||||
<field name="step_order_ids">
|
||||
<tree string="Step list" editable="bottom">
|
||||
<field name="step_id"/>
|
||||
<field name="sequence" groups="base.group_no_one"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="update_github_state" readonly='1'/>
|
||||
<field name="update_github_state" groups="base.group_no_one"/>
|
||||
<field name="protected" groups="base.group_no_one"/>
|
||||
<field name="group" groups="base.group_no_one"/>
|
||||
<field name="monitoring_view_id" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
@ -44,6 +40,7 @@
|
||||
</div>
|
||||
<group string="General settings">
|
||||
<field name="name"/>
|
||||
<field name="domain_filter"/>
|
||||
<field name="job_type"/>
|
||||
<field name="make_stats"/>
|
||||
<field name="protected" groups="base.group_no_one"/>
|
||||
@ -76,15 +73,45 @@
|
||||
<field name="enable_auto_tags"/>
|
||||
<field name="sub_command"/>
|
||||
<field name="extra_params" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<group string="Test settings" attrs="{'invisible': [('job_type', 'not in', ('python', 'install_odoo', 'test_upgrade'))]}">
|
||||
<field name="additionnal_env"/>
|
||||
</group>
|
||||
<group string="Create settings" attrs="{'invisible': [('job_type', 'not in', ('python', 'create_build'))]}">
|
||||
<field name="create_config_ids" widget="many2many_tags" options="{'no_create': True}" />
|
||||
<field name="number_builds"/>
|
||||
<field name="hide_build" groups="base.group_no_one"/>
|
||||
<field name="force_build"/>
|
||||
<field name="make_orphan"/>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('job_type', 'not in', ('python', 'configure_upgrade'))]}">
|
||||
<group class="col" string="Target version settings">
|
||||
<field string="Current" name="upgrade_to_current"/>
|
||||
<field string="Master" name="upgrade_to_master"/>
|
||||
<field string="Major" name="upgrade_to_major_versions"/>
|
||||
<field string="All saas" name="upgrade_to_all_versions"/>
|
||||
<field string="Explicit list" name="upgrade_to_version_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<group class="col" string="Source version settings">
|
||||
<field string="Major" name="upgrade_from_previous_major_version"/>
|
||||
<field string="Last saas" name="upgrade_from_last_intermediate_version"/>
|
||||
<field string="All saas" name="upgrade_from_all_intermediate_version"/>
|
||||
<field string="Explicit list" name="upgrade_from_version_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<group string="Upgrade settings" class="o_group_col_12">
|
||||
<field name="upgrade_flat"/>
|
||||
<field name="upgrade_config_id"/>
|
||||
<field string="Db to upgrade" name="upgrade_dbs">
|
||||
<tree editable="bottom">
|
||||
<field name="config_id"/>
|
||||
<field name="db_pattern"/>
|
||||
<field name="min_target_version_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Restore settings" attrs="{'invisible': [('job_type', '!=', 'restore')]}">
|
||||
<field name="restore_download_db_suffix"/>
|
||||
<field name="restore_rename_db_suffix"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
@ -101,7 +128,6 @@
|
||||
<tree string="Build Configs">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="group"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -6,8 +6,6 @@
|
||||
<field name="model">runbot.error.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Build Error">
|
||||
|
||||
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
@ -19,8 +17,6 @@
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="repo_short_name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="log_type"/>
|
||||
</group>
|
||||
<group>
|
||||
@ -49,8 +45,6 @@
|
||||
<button name="action_goto_build" type="object" icon="fa-external-link "/>
|
||||
<field name="build_id"/>
|
||||
<field name="log_create_date"/>
|
||||
<field name="repo_short_name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="name"/>
|
||||
<field name="func"/>
|
||||
<field name="path"/>
|
||||
@ -68,13 +62,9 @@
|
||||
<field name="message"/>
|
||||
<field name="name" string="Module"/>
|
||||
<field name="func"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="repo_name"/>
|
||||
<field name="build_id"/>
|
||||
<filter string="Failed builds" name="failed_builds" domain="[('global_state', '=', 'done'), ('global_result', '=', 'ko')]"/>
|
||||
<separator/>
|
||||
<filter string="Master branches" name="master_branches" domain="[('branch_name', '=', 'master')]"/>
|
||||
<filter string="Sticky branches" name="sticky_branches" domain="[('branch_sticky', '=', True)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -1,6 +1,66 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<menuitem name="Runbot" id="runbot_menu_root"/>
|
||||
<record id="repo_trigger_form" model="ir.ui.view">
|
||||
<field name="name">runbot.trigger.form</field>
|
||||
<field name="model">runbot.trigger</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<group name="repo_group">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="description"/>
|
||||
<field name="category_id" required='1'/>
|
||||
<field name="project_id"/>
|
||||
<field name="repo_ids"/>
|
||||
<field name="dependency_ids"/>
|
||||
<field name="config_id"/>
|
||||
<field name="version_domain" widget="domain" options="{'model': 'runbot.version', 'in_dialog': True}"/>
|
||||
<field name="hide"/>
|
||||
<field name="manual"/>
|
||||
<field name="upgrade_dumps_trigger_id"/>
|
||||
<field name="upgrade_step_id"/>
|
||||
<field name="ci_context"/>
|
||||
<field name="ci_url"/>
|
||||
<field name="ci_description"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="trigger_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.trigger.tree</field>
|
||||
<field name="model">runbot.trigger</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Repositories">
|
||||
<field name="name"/>
|
||||
<field name="category_id"/>
|
||||
<field name="project_id"/>
|
||||
<field name="config_id"/>
|
||||
<field name="ci_context"/>
|
||||
<field name="repo_ids" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="repo_trigger_catgory_form" model="ir.ui.view">
|
||||
<field name="name">runbot.category.form</field>
|
||||
<field name="model">runbot.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group name="category_group">
|
||||
<field name="name"/>
|
||||
<field name="icon"/>
|
||||
<field name="view_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="repo_form" model="ir.ui.view">
|
||||
<field name="name">runbot.repo.form</field>
|
||||
@ -10,29 +70,67 @@
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<group name="repo_group">
|
||||
<field name="sequence"/>
|
||||
<group name="repo">
|
||||
<field name="name"/>
|
||||
<field name="mode"/>
|
||||
<field name="no_build"/>
|
||||
<field name="nginx"/>
|
||||
<field name="duplicate_id"/>
|
||||
<field name="dependency_ids" widget="many2many_tags"/>
|
||||
<field name="identity_file"/>
|
||||
<field name="sequence"/>
|
||||
<field name="project_id"/>
|
||||
<field name="modules"/>
|
||||
<field name="modules_auto"/>
|
||||
<field name="token"/>
|
||||
<field name="group_ids" widget="many2many_tags"/>
|
||||
<field name="hook_time"/>
|
||||
<field name="config_id"/>
|
||||
<field name="server_files"/>
|
||||
<field name="manifest_files"/>
|
||||
<field name="addons_paths"/>
|
||||
<field name="hook_time" groups="base.group_no_one"/>
|
||||
<field name="mode"/>
|
||||
<field name="forbidden_regex"/>
|
||||
<field name="invalid_branch_message"/>
|
||||
<field name="remote_ids">
|
||||
<tree string="Remotes" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="fetch_heads" string="Branch"/>
|
||||
<field name="fetch_pull" string="PR"/>
|
||||
<field name="token"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="remote_form" model="ir.ui.view">
|
||||
<field name="name">runbot.remote.form</field>
|
||||
<field name="model">runbot.remote</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
</header>
|
||||
<sheet>
|
||||
<group name="repo_group">
|
||||
<field name="name"/>
|
||||
<field name="sequence"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="token"/>
|
||||
<field name="fetch_pull"/>
|
||||
<field name="fetch_heads"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="remote_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.remote.tree</field>
|
||||
<field name="model">runbot.remote</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Repositories">
|
||||
<field name="name"/>
|
||||
<field name="repo_id"/>
|
||||
<field name="fetch_pull"/>
|
||||
<field name="fetch_heads"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="repo_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.repo.tree</field>
|
||||
@ -41,24 +139,76 @@
|
||||
<tree string="Repositories">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="mode"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_repo_tree" model="ir.actions.act_window">
|
||||
<record id="runbot_repos_action" model="ir.actions.act_window">
|
||||
<field name="name">Repositories</field>
|
||||
<field name="res_model">runbot.repo</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="runbot_triggers_action" model="ir.actions.act_window">
|
||||
<field name="name">Triggers</field>
|
||||
<field name="res_model">runbot.trigger</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<record id="runbot_remotes_action" model="ir.actions.act_window">
|
||||
<field name="name">Remotes</field>
|
||||
<field name="res_model">runbot.remote</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="runbot_triggers_category_action" model="ir.actions.act_window">
|
||||
<field name="name">Trigger Categories</field>
|
||||
<field name="res_model">runbot.category</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="runbot_menu_repos_main" name="Repos" parent="runbot_menu_root"/>
|
||||
<menuitem
|
||||
name="Repositories"
|
||||
id="runbot_menu_repo_tree"
|
||||
parent="runbot_menu_root"
|
||||
id="runbot_menu_repos"
|
||||
parent="runbot_menu_repos_main"
|
||||
sequence="10"
|
||||
action="open_view_repo_tree"
|
||||
action="runbot_repos_action"
|
||||
/>
|
||||
<menuitem
|
||||
id="runbot_menu_remotes"
|
||||
parent="runbot_menu_repos_main"
|
||||
sequence="20"
|
||||
action="runbot_remotes_action"
|
||||
/>
|
||||
<menuitem
|
||||
id="runbot_menu_trigger"
|
||||
parent="runbot_menu_repos_main"
|
||||
sequence="30"
|
||||
action="runbot_triggers_action"
|
||||
/>
|
||||
<menuitem
|
||||
id="runbot_menu_trigger_category"
|
||||
parent="runbot_menu_repos_main"
|
||||
sequence="40"
|
||||
action="runbot_triggers_category_action"
|
||||
/>
|
||||
|
||||
<menuitem
|
||||
name="Technical"
|
||||
id="runbot_menu_technical"
|
||||
parent="runbot_menu_root"
|
||||
sequence="1000"
|
||||
/>
|
||||
|
||||
<menuitem id="runbot_menu_ir_cron_act" action="base.ir_cron_act" parent="runbot_menu_technical"/>
|
||||
<menuitem id="runbot_menu_base_automation_act" action="base_automation.base_automation_act" parent="runbot_menu_technical"/>
|
||||
<menuitem id="runbot_menu_action_ui_view" action="base.action_ui_view" parent="runbot_menu_technical"/>
|
||||
|
||||
<menuitem
|
||||
name="▶"
|
||||
id="runbot_menu_website"
|
||||
parent="runbot_menu_root"
|
||||
sequence="1001"
|
||||
action="website.action_website"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
@ -10,60 +10,50 @@
|
||||
<div class="app_settings_block" data-string="Runbot" string="Runbot" data-key="runbot">
|
||||
<h2>Runbot configuration</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-xs-12 col-md-6 o_setting_box">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_workers" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_workers" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<field name="runbot_workers" style="width: 15%;"/>
|
||||
<label for="runbot_running_max" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_running_max" style="width: 30%;"/>
|
||||
<field name="runbot_running_max" style="width: 15%;"/>
|
||||
<label for="runbot_timeout" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_timeout" style="width: 15%;"/>
|
||||
<label for="runbot_starting_port" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_starting_port" style="width: 15%;"/>
|
||||
<label for="runbot_max_age" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_max_age" style="width: 15%;"/>
|
||||
<label for="runbot_update_frequency" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_update_frequency" style="width: 15%;"/>
|
||||
<label for="runbot_db_gc_days" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_db_gc_days" style="width: 15%;"/>
|
||||
<label for="runbot_db_gc_days_child" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_db_gc_days_child" style="width: 15%;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_right_pane">
|
||||
<div class="content-group">
|
||||
<div class="content-group">
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_timeout" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_timeout" style="width: 30%;"/>
|
||||
<label for="runbot_do_fetch" class="col-xs-3 o_light_label" style="width: 40%;"/>
|
||||
<field name="runbot_do_fetch"/>
|
||||
<label for="runbot_do_schedule" class="col-xs-3 o_light_label" style="width: 40%;"/>
|
||||
<field name="runbot_do_schedule"/>
|
||||
<label for="runbot_domain" class="col-xs-3 o_light_label" style="width: 40%;"/>
|
||||
<field name="runbot_domain" style="width: 55%;"/>
|
||||
<label for="runbot_template" class="col-xs-3 o_light_label" style="width: 40%;"/>
|
||||
<field name="runbot_template" style="width: 55%;"/>
|
||||
<label for="runbot_is_base_regex" class="col-xs-3 o_light_label" style="width: 40%;"/>
|
||||
<field name="runbot_is_base_regex" style="width: 55%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_starting_port" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_starting_port" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_domain" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_domain" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_max_age" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_max_age" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_logdb_uri" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_logdb_uri" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_update_frequency" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_update_frequency" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<label for="runbot_template" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_template" style="width: 30%;"/>
|
||||
</div>
|
||||
<div class="mt-16 row">
|
||||
<field name="runbot_logdb_uri" style="width: 100%;"/>
|
||||
<label for="runbot_message" class="col-xs-3 o_light_label" style="width: 60%;"/>
|
||||
<field name="runbot_message" style="width: 100%;"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -6,13 +6,14 @@
|
||||
<field name="model">runbot.build.stat.sql</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Statistics">
|
||||
<field name="config_step_id"/>
|
||||
<field name="build_id"/>
|
||||
<field name="build_name"/>
|
||||
<field name="repo_name"/>
|
||||
<field name="branch_name"/>
|
||||
<field name="key"/>
|
||||
<field name="value"/>
|
||||
<field name="config_step_id"/>
|
||||
<field name="build_id"/>
|
||||
<field name="bundle_id"/>
|
||||
<field name="batch_id"/>
|
||||
<field name="trigger_id"/>
|
||||
<field name="stat_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
64
runbot/views/upgrade.xml
Normal file
64
runbot/views/upgrade.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.actions.server" id="action_parse_upgrade_errors">
|
||||
<field name="name">Parse upgrade errors</field>
|
||||
<field name="model_id" ref="runbot.model_runbot_build" />
|
||||
<field name="binding_model_id" ref="runbot.model_runbot_build" />
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = records._parse_upgrade_errors()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="upgrade_exception_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.upgrade.exception</field>
|
||||
<field name="model">runbot.upgrade.exception</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Upgrade Exceptions">
|
||||
<field name="bundle_id"/>
|
||||
<field name="elements"/>
|
||||
<field name="info"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="upgrade_regex_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.upgrade.regex</field>
|
||||
<field name="model">runbot.upgrade.regex</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Upgrade Regex">
|
||||
<field name="prefix"/>
|
||||
<field name="regex"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_upgrade_exception_tree" model="ir.actions.act_window">
|
||||
<field name="name">Upgrade Exceptions</field>
|
||||
<field name="res_model">runbot.upgrade.exception</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_upgrade_regex_tree" model="ir.actions.act_window">
|
||||
<field name="name">Upgrade Regexes</field>
|
||||
<field name="res_model">runbot.upgrade.regex</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Upgrade Exceptions"
|
||||
id="runbot_menu_upgrade_exceptions_tree"
|
||||
parent="runbot_menu_configs"
|
||||
sequence="30"
|
||||
action="open_view_upgrade_exception_tree"
|
||||
/>
|
||||
<menuitem
|
||||
name="Upgrade Regexes"
|
||||
id="runbot_menu_upgrade_regex_tree"
|
||||
parent="runbot_menu_configs"
|
||||
sequence="30"
|
||||
action="open_view_upgrade_regex_tree"
|
||||
/>
|
||||
</data>
|
||||
</odoo>
|
29
runbot/views/warning_views.xml
Normal file
29
runbot/views/warning_views.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="warning_view_tree" model="ir.ui.view">
|
||||
<field name="name">runbot.warning.tree</field>
|
||||
<field name="model">runbot.warning</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Runbot Warnings">
|
||||
<field name="create_date"/>
|
||||
<field name="message"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="open_view_warning_tree" model="ir.actions.act_window">
|
||||
<field name="name">Warnings</field>
|
||||
<field name="res_model">runbot.warning</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
name="Warnings"
|
||||
id="runbot_menu_warning_root"
|
||||
parent="runbot_menu_root"
|
||||
sequence="110"
|
||||
action="open_view_warning_tree"
|
||||
/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
@ -58,8 +58,6 @@ class MultiBuildWizard(models.TransientModel):
|
||||
'job_type': 'create_build',
|
||||
'create_config_ids': [(4, config_single.id)],
|
||||
'number_builds': self.number_builds,
|
||||
'hide_build': True,
|
||||
'force_build': True
|
||||
})
|
||||
|
||||
config_multi = self.env['runbot.build.config'].create({'name': self.config_multi_name})
|
||||
|
@ -26,6 +26,7 @@ class RunbotClient():
|
||||
from odoo import fields
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
signal.signal(signal.SIGQUIT, self.dump_stack)
|
||||
host = self.env['runbot.host']._get_current()
|
||||
host._bootstrap()
|
||||
count = 0
|
||||
@ -34,15 +35,16 @@ class RunbotClient():
|
||||
host.last_start_loop = fields.Datetime.now()
|
||||
count = count % 60
|
||||
if count == 0:
|
||||
logging.info('Host %s running with %s slots on pid %s%s', host.name, host.get_nb_worker(), os.getpid(), ' (assigned only)' if host.assigned_only else '')
|
||||
self.env['runbot.repo']._source_cleanup()
|
||||
logging.info('Host %s running with %s slots on pid %s%s', host.name, host.nb_worker, os.getpid(), ' (assigned only)' if host.assigned_only else '')
|
||||
self.env['runbot.runbot']._source_cleanup()
|
||||
self.env['runbot.build']._local_cleanup()
|
||||
self.env['runbot.repo']._docker_cleanup()
|
||||
self.env['runbot.runbot']._docker_cleanup()
|
||||
host.set_psql_conn_count()
|
||||
_logger.info('Building docker image...')
|
||||
host._docker_build()
|
||||
_logger.info('Scheduling...')
|
||||
count += 1
|
||||
sleep_time = self.env['runbot.repo']._scheduler_loop_turn(host)
|
||||
sleep_time = self.env['runbot.runbot']._scheduler_loop_turn(host)
|
||||
host.last_end_loop = fields.Datetime.now()
|
||||
self.env.cr.commit()
|
||||
self.env.clear()
|
||||
@ -64,6 +66,10 @@ class RunbotClient():
|
||||
_logger.info("Interrupt detected")
|
||||
self.ask_interrupt.set()
|
||||
|
||||
def dump_stack(self, signal, frame):
|
||||
import odoo
|
||||
odoo.tools.misc.dumpstacks()
|
||||
|
||||
def sleep(self, t):
|
||||
self.ask_interrupt.wait(t)
|
||||
|
||||
|
171
runbot_builder/dbmover.py
Executable file
171
runbot_builder/dbmover.py
Executable file
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/python3
|
||||
import argparse
|
||||
import contextlib
|
||||
import logging
|
||||
import psycopg2
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from collections import defaultdict
|
||||
from logging.handlers import WatchedFileHandler
|
||||
|
||||
LOG_FORMAT = '%(asctime)s %(levelname)s %(name)s: %(message)s'
|
||||
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
|
||||
logging.getLogger('odoo.addons.runbot').setLevel(logging.DEBUG)
|
||||
logging.addLevelName(25, "!NFO")
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DBRE = r'^(?P<build_id>\d+)-.+-[0-9a-f]{6}-?(?P<db_suffix>.*)$'
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def local_pgadmin_cursor():
|
||||
cnx = None
|
||||
try:
|
||||
cnx = psycopg2.connect("dbname=postgres")
|
||||
cnx.autocommit = True # required for admin commands
|
||||
yield cnx.cursor()
|
||||
finally:
|
||||
if cnx:
|
||||
cnx.close()
|
||||
|
||||
|
||||
def list_local_dbs():
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
local_cr.execute("""
|
||||
SELECT datname
|
||||
FROM pg_database
|
||||
WHERE pg_get_userbyid(datdba) = current_user
|
||||
""")
|
||||
return [d[0] for d in local_cr.fetchall()]
|
||||
|
||||
|
||||
def _local_pg_rename_db(dbname, new_db_name):
|
||||
with local_pgadmin_cursor() as local_cr:
|
||||
pid_col = 'pid' if local_cr.connection.server_version >= 90200 else 'procpid'
|
||||
query = 'SELECT pg_terminate_backend({}) FROM pg_stat_activity WHERE datname=%s'.format(pid_col)
|
||||
local_cr.execute(query, [dbname])
|
||||
local_cr.execute("ALTER DATABASE \"%s\" RENAME TO \"%s\";" % (dbname, new_db_name))
|
||||
|
||||
|
||||
class RunbotClient():
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
|
||||
def rename_build_dirs(self, args):
|
||||
builds_root = os.path.join(self.env['runbot.runbot']._root(), 'build')
|
||||
builds_backup_root = os.path.join(self.env['runbot.runbot']._root(), 'build-backup')
|
||||
if not args.dry_run:
|
||||
try:
|
||||
_logger.info('Backup build dir in "%s"', builds_backup_root)
|
||||
shutil.copytree(builds_root, builds_backup_root, copy_function=os.link)
|
||||
except FileExistsError:
|
||||
_logger.info('Backup path "%s" already exists, skipping', builds_backup_root)
|
||||
|
||||
build_dirs = {}
|
||||
leftovers = []
|
||||
for dir_name in os.listdir(builds_root):
|
||||
match = re.match(DBRE, dir_name)
|
||||
if match and match['db_suffix'] == '':
|
||||
build_dirs[match['build_id']] = dir_name
|
||||
else:
|
||||
leftovers.append(dir_name)
|
||||
|
||||
for build in self.env['runbot.build'].search([('id', 'in', list(build_dirs.keys()))]):
|
||||
origin_dir = build_dirs[str(build.id)]
|
||||
origin_path = os.path.join(builds_root, origin_dir)
|
||||
if origin_dir == build.dest:
|
||||
_logger.info('Skip moving %s, already moved', build.dest)
|
||||
continue
|
||||
_logger.info('Moving "%s" --> "%s"', origin_dir, build.dest)
|
||||
if args.dry_run:
|
||||
continue
|
||||
dest_path = os.path.join(builds_root, build.dest)
|
||||
os.rename(origin_path, dest_path)
|
||||
|
||||
for leftover in leftovers:
|
||||
_logger.info("leftover: %s", leftover)
|
||||
|
||||
def rename_databases(self, args):
|
||||
total_db = 0
|
||||
db_names = defaultdict(dict)
|
||||
leftovers = []
|
||||
for local_db_name in list_local_dbs():
|
||||
match = re.match(DBRE, local_db_name)
|
||||
if match and match['db_suffix'] != '':
|
||||
db_names[match['build_id']][match['db_suffix']] = local_db_name
|
||||
else:
|
||||
leftovers.append(local_db_name)
|
||||
total_db += 1
|
||||
|
||||
nb_matching = 0
|
||||
ids = [int(i) for i in db_names.keys()]
|
||||
builds = self.env['runbot.build'].search([('id', 'in', ids)])
|
||||
for build in builds:
|
||||
for suffix in db_names[str(build.id)].keys():
|
||||
origin_name = db_names[str(build.id)][suffix]
|
||||
dest_name = "%s-%s" % (build.dest, suffix)
|
||||
nb_matching += 1
|
||||
_logger.info('Renaming database "%s" --> "%s"', origin_name, dest_name)
|
||||
if args.dry_run:
|
||||
continue
|
||||
_local_pg_rename_db(origin_name, dest_name)
|
||||
|
||||
_logger.info("Found %s databases", total_db)
|
||||
_logger.info("Found %s matching databases", nb_matching)
|
||||
_logger.info("Leftovers: %s", len(leftovers))
|
||||
_logger.info("Builds not found : %s", len(set(ids) - set(builds.ids)))
|
||||
|
||||
|
||||
def run():
|
||||
# parse args
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--odoo-path', help='Odoo sources path')
|
||||
parser.add_argument('--db_host', default='127.0.0.1')
|
||||
parser.add_argument('--db_port', default='5432')
|
||||
parser.add_argument('--db_user')
|
||||
parser.add_argument('--db_password')
|
||||
parser.add_argument('-d', '--database', default='runbot_upgrade', help='name of runbot db')
|
||||
parser.add_argument('--logfile', default=False)
|
||||
parser.add_argument('-n', '--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.logfile:
|
||||
dirname = os.path.dirname(args.logfile)
|
||||
if dirname and not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
|
||||
handler = WatchedFileHandler(args.logfile)
|
||||
formatter = logging.Formatter(LOG_FORMAT)
|
||||
handler.setFormatter(formatter)
|
||||
_logger.parent.handlers.clear()
|
||||
_logger.parent.addHandler(handler)
|
||||
|
||||
# configure odoo
|
||||
sys.path.append(args.odoo_path)
|
||||
import odoo
|
||||
_logger.info("Starting upgrade move script using database %s", args.database)
|
||||
odoo.tools.config['db_host'] = args.db_host
|
||||
odoo.tools.config['db_port'] = args.db_port
|
||||
odoo.tools.config['db_user'] = args.db_user
|
||||
odoo.tools.config['db_password'] = args.db_password
|
||||
addon_path = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
|
||||
config_addons_path = odoo.tools.config['addons_path']
|
||||
odoo.tools.config['addons_path'] = ','.join([config_addons_path, addon_path])
|
||||
|
||||
# create environment
|
||||
registry = odoo.registry(args.database)
|
||||
with odoo.api.Environment.manage():
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
runbot_client = RunbotClient(env)
|
||||
runbot_client.rename_build_dirs(args)
|
||||
runbot_client.rename_databases(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
||||
_logger.info("All done")
|
@ -15,37 +15,38 @@ class Step(models.Model):
|
||||
|
||||
job_type = fields.Selection(selection_add=[('cla_check', 'Check cla')])
|
||||
|
||||
def _run_step(self, build, log_path):
|
||||
if self.job_type == 'cla_check':
|
||||
return self._runbot_cla_check(build, log_path)
|
||||
return super(Step, self)._run_step(build, log_path)
|
||||
|
||||
def _runbot_cla_check(self, build, log_path):
|
||||
def _run_cla_check(self, build, log_path):
|
||||
build._checkout()
|
||||
cla_glob = glob.glob(build._get_server_commit()._source_path("doc/cla/*/*.md"))
|
||||
error = False
|
||||
checked = set()
|
||||
if cla_glob:
|
||||
description = "%s Odoo CLA signature check" % build.author
|
||||
mo = re.search('[^ <@]+@[^ @>]+', build.author_email or '')
|
||||
state = "failure"
|
||||
for commit in build.params_id.commit_ids:
|
||||
email = commit.author_email
|
||||
if email in checked:
|
||||
continue
|
||||
checked.add(email)
|
||||
build._log('check_cla', "[Odoo CLA signature](https://www.odoo.com/sign-cla) check for %s (%s) " % (commit.author, email), log_type='markdown')
|
||||
mo = re.search('[^ <@]+@[^ @>]+', email or '')
|
||||
if mo:
|
||||
email = mo.group(0).lower()
|
||||
if re.match('.*@(odoo|openerp|tinyerp)\.com$', email):
|
||||
state = "success"
|
||||
else:
|
||||
if not re.match('.*@(odoo|openerp|tinyerp)\.com$', email):
|
||||
try:
|
||||
cla = ''.join(io.open(f, encoding='utf-8').read() for f in cla_glob)
|
||||
if cla.lower().find(email) != -1:
|
||||
state = "success"
|
||||
if cla.lower().find(email) == -1:
|
||||
error = True
|
||||
build._log('check_cla', 'Invalid email format %s' % email, level="ERROR")
|
||||
except UnicodeDecodeError:
|
||||
description = 'Invalid CLA encoding (must be utf-8)'
|
||||
_logger.info('CLA build:%s email:%s result:%s', build.dest, email, state)
|
||||
status = {
|
||||
"state": state,
|
||||
"target_url": "https://www.odoo.com/sign-cla",
|
||||
"description": description,
|
||||
"context": "legal/cla"
|
||||
}
|
||||
build._log('check_cla', 'CLA %s' % state)
|
||||
build._github_status_notify_all(status)
|
||||
# 0 is myself, -1 is everybody else, -2 nothing
|
||||
return -2
|
||||
error = True
|
||||
build._log('check_cla', 'Invalid CLA encoding (must be utf-8)', level="ERROR")
|
||||
else:
|
||||
error = True
|
||||
build._log('check_cla', 'Invalid email format %s' % email, level="ERROR")
|
||||
else:
|
||||
error = True
|
||||
build._log('check_cla', "Missing cla file", level="ERROR")
|
||||
|
||||
if error:
|
||||
build.local_result = 'ko'
|
||||
elif not build.local_result:
|
||||
build.local_result = 'ok'
|
||||
|
Loading…
Reference in New Issue
Block a user