Сегодня в этой небольшой заметке мы покажем пример файла .gitlab-ci.yml, который описывает пайплайн, осуществляющий какие-либо действия с нашим репозиторием в момент, когда в другом проекте выходит новая версия. Типичный сценарий, при котором подобное может пригодиться – это необходимость вызова скрипта для обновления локальной копии репозитория, представляющего собой форк другого проекта (назовем его upstream), когда в оригинальном репозитории появляются изменения.
В первую очередь, необходимо решить, каким образом мы будем определять наиболее актуальную версию upstream. Чтобы не скачивать репозиторий целиком, удобнее будет воспользоваться командой git ls-remote, которая показывает список всех ссылок (веток, тегов и HEAD) в удаленном репозитории вместе с их ID коммитов (SHA-1). Так мы узнаем, появились ли новые коммиты. В нашем случае будем искать по тегам, так как это весьма удобный способ получить список готовых к использованию версий с номерами, чтобы по ним фильтровать и отслеживать, как далеко ушли изменения.
Если запустить команду без дополнительных флагов, то мы получим нечто вроде
b9a2c14f7...1a2b3c4d HEAD
b9a2c14f7...1a2b3c4d refs/heads/main
a1b2c3d4e...5f6g7h8i refs/tags/v1.0.0
где будет длинный список вообще всех ссылок. Чтобы из этого получить последнюю актуальную версию, придется применить несколько фильтров. В итоге получится подобная команда:
git ls-remote --tags "$UPSTREAM_URL" | awk -F'/' '{print $3}' | grep -v '\^{}' | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1
Здесь мы последовательно указываем, что берем лишь теги; очищаем строки от хэшей коммитов и части с refs/..; оставляем только числа и точки, чтобы вычленить версию; сортируем и берем только последний (самый актуальный) тег. В итоге будет лишь строка с версией, которая соответствует тегу.
Чтобы понять, есть ли в нашем репозитории самая актуальная версия upstream, можно проверить, существует ли ветка, содержащая в названии номер версии (хотя это сильно зависит от специфики проекта, но вариант кажется логичным). Для этого также вполне подойдет git ls-remote, но уже с флагом --heads. Так мы сможем получить ветку нашего репозитория, которая содержит в себе тег (точки заменили на дефис)
git ls-remote --heads "${YOUR_REPO}" | grep "${LATEST_TAG//./-}"
Переходя же непосредственно к пайплайну, предположим, что исходным у нас является базовый питоновский докер образ (например python:3.10-slim), в который через Dockerfile добавили произвольный python скрипт, призванный обновить локальный репозиторий правками из upstream. Его логику рассматривать не будем, поскольку она своя для кажого конкретного случая. Но что может понадобиться, так это определить ту ветку в нашем проекте, где лежат самые актуальные изменения (например, чтобы перенести их оттуда в ветку для новой версии). Для этого придется использовать уже git branch -r --sort=-committerdate | grep "origin/.*-main$", чтобы отсортировать ветки репозитория по дате последнего коммита.
В итоге получим вот такой файл .gitlab-ci.yml (с поясняющими комментариями):
image:
name: dr.example.ru/your-script:latest
entrypoint: [""]
variables:
UPSTREAM_URL: "https://github.com/something.git"
YOUR_APPLICATION_REPO_URL: "${CI_SERVER_HOST}/your-application.git"
YOUR_APPLICATION_REPO_PATH: "${CI_PROJECT_DIR}/your-application"
stages:
- check-version
- update-repo
# проверка, есть ли новая версия upstream репозитория
check_new_version:
stage: check-version
rules:
# Запускаем по планировщику, а после коммита - только при ручном действии
- if: $CI_PIPELINE_SOURCE == "schedule"
when: always
- if: $CI_PIPELINE_SOURCE == "web"
when: always
- if: $CI_PIPELINE_SOURCE == "push"
when: manual
script:
- YOUR_REPO="https://oauth2:${PROJECT_PUSH_TOKEN}@${YOUR_APPLICATION_REPO_URL}"
# самый актуальный тег версии из upstream, как описывали
- LATEST_TAG=$(git ls-remote --tags "$UPSTREAM_URL" | awk -F'/' '{print $3}' | grep -v '\^{}' | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)
# проверка на наличие ветки с таким тегом в названии уже в нашем репозитории
- IF_CORRESPONDING_BRANCH_EXISTS=$(git ls-remote --heads "${YOUR_REPO}" | grep "${LATEST_TAG//./-}" || true)
# если есть века - все обновлено, выставляем флаг, если нет - то нужно какое-то действие
- |
if [ -n "$IF_CORRESPONDING_BRANCH_EXISTS" ]; then
echo "Состояние актуально. Новой версии upstream не обнаружено"
echo "NEEDS_UPDATE=false" >> check.env
else
echo "Обнаружена новая версия ($LATEST_TAG)"
echo "NEEDS_UPDATE=true" >> check.env
echo "NEW_UPSTREAM_VERSION=$LATEST_TAG" >> check.env
fi
artifacts:
# сохраняем переменные окружения для следующего этапа
reports:
dotenv: check.env
# полезная работа при обновлении upstream
do_on_update:
stage: update-repo
before_script:
# проверяем переменную, если обновление не нужно, то выходим, не вызывая ошибку
- |
if [ "$NEEDS_UPDATE" != "true" ]; then
echo "Состояние актуально. Ничего не требуется"
exit 0
fi
script:
# клонируем наш репозиторий с фильтром blob:none, чтобы он не занимал много места, а хранил лишь саму структуру репозитория (что надо подгрузится лениво)
- git clone --filter=blob:none --no-checkout https://oauth2:${PROJECT_PUSH_TOKEN}@${YOUR_APPLICATION_REPO_URL} ${YOUR_APPLICATION_REPO_PATH}
- cd ${YOUR_APPLICATION_REPO_PATH}
# получаем по регулярному выражению самую свежую ветку в локальном репозитории (например, забрать оттуда наши правки)
- REQUIRED_BRANCH=$(git branch -r --sort=-committerdate | grep "origin/.*-main$" | head -n 1 | awk '{$1=$1; print $1}')
# запустить скрипт, который нужно запустить при новой версии upstream
- python /app/your_script.py --branch ${REQUIRED_BRANCH}
Для настройки запуска по планировщику стандартно надо перейти в интерфейс Gitlab, выбрать там Build -> Pipeline schedules и задать нужные параметры.
В конечном итоге мы получим пайплайн, который запускается в указанный промежуток времени, проверяет upstream. Если там появляется новый тег (в нашем репозитории еще нет ветки для такой версии), то запускается скрипт для обновления, который обрабатывает самую новую ветку из локального репозитория. Если такой тег у нас уже есть (то есть нет обновлений в upstream), то пайплайн просто покажет все стадии успешно выполненными.