Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Система непрерывной интеграции

Оригинал: A Continuous Integration System
Автор: Malini Das
Дата публикации: July 12, 2016
Перевод: Н.Ромоданов
Дата перевода: октябрь 2016 г.

Предыдущая часть статьи

Компоненты

Наблюдатель за репозиторием (repo_observer.py)

Наблюдатель за репозиторием проверяет репозиторий и когда обнаруживает новый коммит, то сообщает об этом диспетчеру, Чтобы можно было работать со всеми системами контроля версий (дело в том, что не во всех таких системах есть встроенные средства уведомления), подобный наблюдатель самостоятельно периодически проверяет наличие в репозитории новых коммитоза, а не полагается на средства системы контроля версий, которые бы его уведомляли о сделанных изменениях.

Наблюдатель будет периодически опрашивать хранилище, и когда будет обнаружено изменение, он для того, чтобы для самого последнего коммита можно было выполнить тесты, сообщит диспетчеру идентификатор этого коммита. Наблюдатель проверяет новые коммиты следующим образом: ищет в своем репозитории идинтификатор текущего коммита, затем обновляет репозиторий, после этого он ищет идентификатор последнего коммита и, наконец, сравнивает оба идентификатора. В данном примере наблюдатель будет передавать на тестирование только последний коммит. Это означает, что если между периодическими проверками будет выполнено два коммита, то наблюдатель запустит тесты только для последнего коммита. Как правило, система непрерывной интеграции обнаруживает все коммиты после последнего протестированного коммита и передает данные для тестирования о всех новых коммитах, но мы с целью упрощения это изменили.

Наблюдатель должен знать о том, за каким репозиторием он наблюдает. Ранее мы создали клон нашего репозитория /path/to/test_repo_clone_obs. Наблюдатель будет использовать этот клон для того, чтобы обнаруживать изменения. Чтобы позволить наблюдателю использовать именно этот клон, мы, когда вызываем файл repo_observer.py, указываем ему этот путь. Наблюдатель будет использовать этот клон для того, чтобы переносить в него данные из основного репозитория.

Чтобы наблюдатель мог посылать диспетчеру сообщения, мы должны также дать ему адрес диспетчера. Когда вы запускаете наблюдателя, вы можете с помощью аргумента командной строки --dispatcher-server указать адрес сервера диспетчера. Если вы этого не сделаете, то, по умолчанию, будет использоваться адрес localhost:8888.

def poll():
    parser = argparse.ArgumentParser()
    parser.add_argument("--dispatcher-server",
                        help="dispatcher host:port, " \
                        "by default it uses localhost:8888",
                        default="localhost:8888",
                        action="store")
    parser.add_argument("repo", metavar="REPO", type=str,
                        help="path to the repository this will observe")
    args = parser.parse_args()
    dispatcher_host, dispatcher_port = args.dispatcher_server.split(":")

После того, как файл наблюдателя будет вызван, он запустит функцию poll(). Эта функция анализирует аргументы командной строки, а затем запускает бесконечный цикл while. Цикл while используется для того, чтобы периодически проверять наличие изменений в репозитории. Первое, что он делает, это вызывает скрипт Bash update_repo.sh [1].

    while True:
        try:
            # вызывается скрипт bash, который обновляет репозиторий и проверяет
            # наличие изменений. Если изменения имеются, то он помещает в текущий 
		# рабочий каталог файл .commit_id последнего коммита
            subprocess.check_output(["./update_repo.sh", args.repo])
        except subprocess.CalledProcessError as e:
            raise Exception("Could not update and check repository. " +
                            "Reason: %s" % e.output)

Для идентификации новых коммитов используется скрипт update_repo.sh, который сообщает наблюдателю за репозиторием о наличии нового коммита. Он делает это следующим образом: запоминает ID текущего коммита, загрузка выполняется репозитория и проверка идентификатора последнего коммита. Если запомненный комит и новый текущий коммит совпадают, то это означает, что никаких изменений нет, поэтому наблюдателю ничего не нужно делать, но если в ID коммитов есть различие, то мы узнаем, что был сделан новый коммит. В этом случае с помощью update_repo.sh создается файл .commit_id с хранящимся в нем идентификатором последнего коммита.

Ниже мы поэтапно рассмотрим работу скрипта update_repo.sh. Во-первых, происходит обращение к исходному коду скрипта run_or_fail.sh, который представляет собой метод-хелпер run_or_fail.sh, используемый всеми нашими скриптами. Этот метод используется для запуска команд, или выдачи сообщений об ошибках.

#!/bin/bash

source run_or_fail.sh

Затем скрипт пытается удалить файл с именем .commit_id. Поскольку скрипт updaterepo.sh вызывается в файле repo_observer.py бесконечное количество раз, то в случае, когда у нас ранее был новый коммит, то уже был создан файл .commit_id, но в нем указан коммит, который мы уже протестировали. Поэтому мы хотим удалить этот файл, и создать новый только в том случае, если будет найден новый коммит.

bash rm -f .commit_id

После того, как файл будет удален (если он был), выполняется проверка наличия репозитория, за которым мы наблюдаем, а затем в либом случае определяем самый последний коммит с тем, чтобы избежать рассинхронизации.

run_or_fail "Repository folder not found!" pushd $1 1> /dev/null
run_or_fail "Could not reset git" git reset --hard HEAD

После этого вызывается журнал git и выполняется его анализ с целью найти ID самого последнего коммита.

COMMIT=$(run_or_fail "Could not call 'git log' on repository" git log -n1)
if [ $? != 0 ]; then
  echo "Could not call 'git log' on repository"
  exit 1
fi
COMMIT_ID=`echo $COMMIT | awk '{ print $2 }'`

После этого перезаливаем репозиторий для того, чтобы иметь все последние изменения, а затем находим ID самого последнего коммитта.

run_or_fail "Could not pull from repository" git pull
COMMIT=$(run_or_fail "Could not call 'git log' on repository" git log -n1)
if [ $? != 0 ]; then
  echo "Could not call 'git log' on repository"
  exit 1
fi
NEW_COMMIT_ID=`echo $COMMIT | awk '{ print $2 }'`

И, наконец, если ID коммита не совпадает с предыдущим ID, то мы знаем, что нужно найти новые коммиты, поэтому скрипт сохраняет ID самого последний коммита в файле .commit_id.

# if the id changed, then write it to a file
if [ $NEW_COMMIT_ID != $COMMIT_ID ]; then
  popd 1> /dev/null
  echo $NEW_COMMIT_ID > .commit_id
fi

После того, как update_repo.sh завершит свою работу в repo_observer.py, наблюдатель за репозитарием проверяет наличие файла .commit_id. Если файл существует, то мы знаем, у нас есть новый коммит, и нам необходимо уведомить об этом диспетчер, чтобы тот мог начать тестирование. Наблюдатель для того, чтобы убедиться, что с сервером диспетчера нет никаких проблем и чтобы убедиться, что он готов получать инструкции, проверит статус сервера диспетчера при помощи подключения к нему и передача ему запроса 'status'.

        if os.path.isfile(".commit_id"):
            try:
                response = helpers.communicate(dispatcher_host,
                                               int(dispatcher_port),
                                               "status")
            except socket.error as e:
                raise Exception("Could not communicate with dispatcher server: %s" % e)

Если ответом будет "OK", то наблюдатель за репозитарием открывает файл .commit_id, считывает ID последнего коммита и посылает этот ID диспетчеру с помощью запроса dispatch:<commit ID>. Потом он засыпает на пять секунд и процесс повторяется. В случае, если что-то пошло не так, мы также попытаемся сделать это еще раз через пять секунд.

            if response == "OK":
                commit = ""
                with open(".commit_id", "r") as f:
                    commit = f.readline()
                response = helpers.communicate(dispatcher_host,
                                               int(dispatcher_port),
                                               "dispatch:%s" % commit)
                if response != "OK":
                    raise Exception("Could not dispatch the test: %s" %
                    response)
                print "dispatched!"
            else:
                raise Exception("Could not dispatch the test: %s" %
                response)
        time.sleep(5)

Наблюдатель за репозитарием будет постоянно повторять этот процесс до тех пор, пока вы не уничтожите процесс с помощью команды KeyboardInterrupt (Ctrl+c), или не отправите ему сигнал kill.

Перейти к следующей части статьи.