Вступление

Привет, Хабр! Это мой первый пост на данной площадке, давно читаю, но писать все не решался, но, как говорится, когда-то все в жизни бывает в первый раз.

Коротко о том, что будет в статье

  • быстро настроим Gitlab для имитации CI-CD как у взрослых дядь (образно выражаясь)

  • напишем простое Hello world web-приложение (без излишеств, максимально быстро и просто)

  • превратим наше web-приложение в Android-приложение при помощи Cordova (как мне кажется, самый простой метод, для не вовлеченных в индустрию людей)

  • соберем тестовую версию .apk приложения и запустим на Android-телефоне

Дисклеймер

Web- и Android-разработка не являются моими прямыми должностными обязанностями, это мое хобби, на которое я предпочитаю тратить свободное от работы и прочих дел время, я понимаю, что в некоторых случаях могу показать антипаттерн в методах реализации демонстрационного проекта. Надеюсь, вы подсветите мне все нюансы в комментариях! Цель статьи — показать общую концепцию одного из вариантов реализации конвейера для быстрого выпуска простых Android-приложений

Глава 1. Gitlab CI-CD

Основная задача на данном этапе — получить локальный инстанс Gitlab-CE и раннеры для того, чтобы можно было хранить наш код, иметь систему контроля версий и быстро собирать продукт из исходников.

Воспользуемся контейнеризацией (Docker + Docker Compose) для того, чтобы упростить процесс деплоя Gitlab на локальную машину, нам понадобится собственно Gitlab-CE и несколько раннеров (в моем примере — 3), для того чтобы не только хранить код, но и собирать проект (таблица 1).

таблица 1 — Используемые образы

Наименование

Ссылка на Docker hub

Описание

Gitlab-CE

https://hub.docker.com/r/gitlab/gitlab-ce

gitlab-ce server 16.1.0-ce.0

Gitlab Runner

https://hub.docker.com/r/gitlab/gitlab-runner

gitlab build runner alpine3.18-v16.1.0 shell

Для того чтобы иметь возможность тестировать код и собирать web- и android-приложения, нам понадобится модифицировать образ Gitlab Runner под свои нужды; так как мы взяли за основу версию Alpine, сделать это будет совсем просто, напишем простой Dockerfile, для того чтобы добавить все необходимое:

FROM gitlab/gitlab-runner:alpine3.18-v16.1.0

# set LANG env var
ENV LANG=C.UTF-8

#add Glibc 2.34 for Alpine linux
RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \
    ALPINE_GLIBC_PACKAGE_VERSION="2.34-r0" && \
    ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \
    apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \
    echo \
        "-----BEGIN PUBLIC KEY-----\
        MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\
        y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\
        tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\
        m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\
        KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\
        Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\
        1QIDAQAB\
        -----END PUBLIC KEY-----" | sed 's/   */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \
    wget \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    mv /etc/nsswitch.conf /etc/nsswitch.conf.bak && \
    apk add --no-cache --force-overwrite \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \
    \
    mv /etc/nsswitch.conf.bak /etc/nsswitch.conf && \
    rm "/etc/apk/keys/sgerrand.rsa.pub" && \
    (/usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true) && \
    echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \
    \
    apk del glibc-i18n && \
    \
    rm "/root/.wget-hsts" && \
    apk del .build-dependencies && \
    rm \
        "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \
        "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME"

# set env vars
ENV ANDROID_SDK_ROOT "/opt/sdk"
ENV ANDROID_HOME ${ANDROID_SDK_ROOT}
ENV PATH $PATH:${ANDROID_SDK_ROOT}/gradle/6.5/bin:${ANDROID_SDK_ROOT}/cmdline-tools/latest_supported/bin:${ANDROID_SDK_ROOT}/platform-tools:${ANDROID_SDK_ROOT}/extras/google/instantapps:${ANDROID_SDK_ROOT}/build-tools/34.0.0

# add core apps and libs (jdk8, jdk11, cordova10)
RUN apk add --no-cache --update \
    docker docker-compose openrc doas nodejs-current npm curl python3 python3-dev py3-pip gcc musl-dev openjdk8 openjdk11 curl unzip wget
RUN python3 -m pip install --upgrade --no-cache semgrep && \
    npm install -g cordova@10
RUN rc-update add docker boot
RUN echo 'permit nopass gitlab-runner as root' > /etc/doas.d/doas.conf

# add Gradle 6.5
RUN wget -q https://services.gradle.org/distributions/gradle-6.5-all.zip -O /tmp/gradle.zip && \
    mkdir -p ${ANDROID_SDK_ROOT}/gradle && \
    unzip -qq /tmp/gradle.zip -d ${ANDROID_SDK_ROOT}/gradle && \
    mv ${ANDROID_SDK_ROOT}/gradle/* ${ANDROID_SDK_ROOT}/gradle/6.5 && \
    rm -v /tmp/gradle.zip

# add latest android command line tools
RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-9477386_latest.zip -O /tmp/tools.zip && \
    mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools && \
    unzip -qq /tmp/tools.zip -d ${ANDROID_SDK_ROOT}/cmdline-tools && \
    mv ${ANDROID_SDK_ROOT}/cmdline-tools/* ${ANDROID_SDK_ROOT}/cmdline-tools/latest_supported && \
    rm -v /tmp/tools.zip && \
    mkdir -p ~/.android/ && touch ~/.android/repositories.cfg

# add android platform tools, build tools 34.0.0
RUN yes | sdkmanager --sdk_root=${ANDROID_SDK_ROOT} --licenses && \
    sdkmanager --sdk_root=${ANDROID_SDK_ROOT} --install "platform-tools" "extras;google;instantapps" "build-tools;34.0.0"

# remove jdk11
RUN apk del openjdk11

# fix build tools "dx" bug
RUN mv /opt/sdk/build-tools/34.0.0/d8 /opt/sdk/build-tools/34.0.0/dx &&\
    mv /opt/sdk/build-tools/34.0.0/lib/d8.jar /opt/sdk/build-tools/34.0.0/lib/dx.jar

# set ownership of android sdk to gitlab-runner user
RUN chown gitlab-runner -R /opt/sdk

# set default jdk to jdk8
ENV JAVA_HOME "/usr/lib/jvm/java-1.8-openjdk"
ENV PATH $PATH:${JAVA_HOME}/bin

Из необычного хотелось бы отметить несколько моментов:

  • добавление библиотеки Glibc (строка 6)

  • добавление jdk8 и jdk11 (строка 52)

  • удаление jdk11 (строка 79)

  • фикс нэйминга файлов Android SDK (строка 82)

Теперь по порядку. Glibc нам понадобится, чтобы поставить все компоненты, необходимые для сборки Android-приложения, при помощи Cordova (я не стал искать сложных путей и воспользовался уже готовым решением из Github). Jdk11 нам понадобится для того, чтобы иметь возможность использовать Android command line tools, впоследствии мы его удалим, так как использов��ть будем Cordova 10, которая дружит с jdk8 (пожалуйста, не ругайте за легаси, да, я тоже слышал шутку Влада Тэна в подкасте, где он прерывает влажные фантазии о AI фразой в духе «Какой к черту AI, люди до сих пор 8 джавой пользуются!»). Фикс нэйминга файлов Android SDK нам понадобится для того, чтобы Gradle не падал с ошибкой на этапе сборки андроид-приложения.

Помимо вышеописанных действий мы также добавим в контейнер Nodejs, Python, Semgrep и Gradle и выполним ряд дополнительных манипуляций (в Dockerfile есть комментарии, не буду их дублировать)

Теперь у нас есть все для того, чтобы создать импровизированную версию конвейера в домашних условиях, давайте напишем docker-compose.yml

version: "3.9"

networks:
  gitlab_ce:
    driver: bridge

volumes:
  gitlab_conf:
  gitlab_data:
  gitlab_logs:
  build_runner_home:
  build_runner_conf:
  test_runner_home:
  test_runner_conf:
  deploy_runner_home:
  deploy_runner_conf:

services:
  # gitlab-ce
  gitlab-ce:
    image: gitlab/gitlab-ce:16.1.0-ce.0
    shm_size: '2gb'
    container_name: gitlab-ce
    hostname: gitlab-ce.local
    restart: always
    depends_on:
      - build-runner
      - test-runner
      - deploy-runner
    volumes:
      - gitlab_conf:/etc/gitlab
      - gitlab_data:/var/opt/gitlab
      - gitlab_logs:/var/log/gitlab
    ports:
      - "127.0.0.1:22:22"
      - "127.0.0.1:80:80"
    networks:
      - gitlab_ce
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: 8G

  # gitlab-build-runner
  build-runner:
    build: ./
    container_name: gitlab-build-runner
    hostname: gitlab-build-runner.local
    restart: always
    volumes:
      - build_runner_conf:/etc/gitlab-runner
      - build_runner_home:/home/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock:rw
    networks:
      - gitlab_ce
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: 4G

  # gitlab-test-runner
  test-runner:
    build: ./
    container_name: gitlab-test-runner
    hostname: gitlab-test-runner.local
    restart: always
    volumes:
      - test_runner_conf:/etc/gitlab-runner
      - test_runner_home:/home/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock:rw
    networks:
      - gitlab_ce
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 1G

  # gitlab-deploy-runner
  deploy-runner:
    build: ./
    container_name: gitlab-deploy-runner
    hostname: gitlab-deploy-runner.local
    restart: always
    volumes:
      - deploy_runner_conf:/etc/gitlab-runner
      - deploy_runner_home:/home/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock:rw
    networks:
      - gitlab_ce
    deploy:
      resources:
        limits:
          cpus: "1"
          memory: 1G

Стоит обратить внимание на секцию deploy в каждом сервисе, в ней можно указать, сколько системных ресурсов Вы хотите утилизировать под каждый сервис, также внимание стоит обратить на build: здесь указывается, из какой директории собирать образ для сервиса (где находится Dockerfile), не пропустите shm_size — это поможет заметно повысить производительность сервера Gitlab.

В данной конфигурации заложена избыточность по количеству раннеров (можно обойтись и одним, или же для каждой стадии (build, test, deploy) использовать отдельный образ, оптимизированный под определенную задачу) альтернативным методом может быть использование раннера c типом docker, однако в данном примере, несмотря на то что все раннеры являются Docker-контейнерами, я буду использовать их как shell-раннеры. Да, здесь используется импровизированный docker in docker (у сообщества есть вопросы к целесообразности использования такого метода не в последнюю очередь из-за проблем с безопасностью, но в целях эксперимента, думаю, можно попробовать и такую конфигурацию).

Теперь давайте автоматизируем процесс регистрации раннера (как вы знаете, недостаточно просто запустить раннер, его еще нужно зарегистрировать на сервере Gitlab)

#!/bin/sh

RUNNER_CONTAINER_ID=$1
RUNNER_TAG=$2
GITLAB_RUNNER_REG_TOKEN=$3

GITLAB_SUBNET=$(docker network ls | grep gitlab_ce | cut -d " " -f 4)

echo "Registering ...";

docker exec -it $RUNNER_CONTAINER_ID bash -c 'gitlab-runner register \
  --non-interactive \
  --executor "shell" \
  --docker-image alpine:latest \
  --url "http://gitlab-ce.local/" \
  --registration-token "'$GITLAB_RUNNER_REG_TOKEN'" \
  --description "docker-'$RUNNER_TAG'-runner" \
  --maintenance-note "Docker runner for gitlab-ce" \
  --tag-list "docker,'$RUNNER_TAG'" \
  --run-untagged="true" \
  --locked="false" \
  --access-level="not_protected" > /dev/null 2>&1; \
  echo "    network_mode = \"'$GITLAB_SUBNET'\"" >> /etc/gitlab-runner/config.toml'; 

echo "Done"

Данный скрипт принимает id контейнера с раннером, тэг для него и регистрационный токен. В ходе выполнения скрипта в контейнер передается команда регистрации раннера, также вносится правка в его конфигурацию, для того чтобы указать сеть, из которой ему будет доступен сервер Gitlab.

Почти все готово для запуска своего инстанса Gitlab, финальный скрипт, связывающий все в единую картину и автоматизирующий рутину.

#!/bin/sh

echo "################### Build & deploy Gitlab-CE ###################"
docker compose -p gitlab-ce up -d --build --wait gitlab-ce
echo "###############################################################"

echo "..."

echo "#################### Check Gitlab-CE state ####################"
docker compose -p gitlab-ce ps
echo "###############################################################"

echo "..."

echo "############## Gitlab runner registration token ###############"
RUNNER_REG_TOKEN=$(docker exec -it gitlab-ce gitlab-rails runner -e production 'puts Gitlab::CurrentSettings.current_application_settings.runners_registration_token')
echo "Token: $RUNNER_REG_TOKEN"
echo "###############################################################"

echo "..."

echo "################ Gitlab initial root password #################"
docker exec -it gitlab-ce bash -c 'grep 'Password:' /etc/gitlab/initial_root_password 2>/dev/null || echo "Custom root password already exists."'
echo "###############################################################"

echo "..."

echo "################# Base runners registration ###################"
echo "Registering  gitlab-build-runner ..."
./reg_new_runner.sh gitlab-build-runner build $RUNNER_REG_TOKEN
echo "Registering  gitlab-test-runner ..."
./reg_new_runner.sh gitlab-test-runner test $RUNNER_REG_TOKEN
echo "Registering  gitlab-deploy-runner ..."
./reg_new_runner.sh gitlab-deploy-runner deploy $RUNNER_REG_TOKEN
echo "###############################################################"

В итоге получаем готовый к тестам Gitlab-инстанс, который может собирать приложения и тестировать код.

зарегестрированныые раннеры
зарегестрированныые раннеры

Глава 2. Простое web-приложение

Как вы уже, наверное, догадались, здесь мы сделаем очень простое web-приложение и соберем его при помощи Webpack, на эту тему есть очень много хороших материалов, официальная документация тоже на высоте.

От себя лишь добавлю пару скриншотов из своих pet-проектов для того, чтобы показать, как организована структура файлов у меня.

файлы web-приложения выделены красным
файлы web-приложения выделены красным
содержание папки src
содержание папки src

Не буду подробно останавливаться на этом пункте, единственное, на что обращу ваше внимание, задайте в качестве директории, куда будет собираться Ваш фронтенд, папку с названием www, это упростит понимание дальнейших процессов.

конфигурация webpack
конфигурация webpack

Глава 3. Hi Cordova, pls transform my website to android app

Подведем промежуточные итоги: у нас есть простое web-приложение и Gitlab, готовый собирать наш проект и тестировать его, давайте соберем все компоненты нашего пазла воедино.

Для начала добавим конфигурационный файл config.xml для нашего приложения (я положу его в папку Cordova).

<?xml version='1.0' encoding='utf-8'?>
<widget id="com.moviefinder.main" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0" xmlns:android="http://schemas.android.com/apk/res/android">
    <name>Hello-world-app</name>
    <description>My first app</description>
    <author email="username@domain.com" href="https://link-to-my.app">
        Name Surname
    </author>
    
    <preference name="android-targetSdkVersion" value="34" />
    <preference name="android-minSdkVersion" value="21" />
    
    <preference name="AndroidPersistentFileLocation" value="Compatibility" />
    
    <preference name="DisallowOverscroll" value="true" />
    
    <allow-navigation href="*" />
    <allow-intent href="*" />
    <access origin="*" />
    
    <edit-config file="app/src/main/AndroidManifest.xml" mode="merge" target="/manifest/application/activity">
        <activity android:exported="true"/>
    </edit-config>  

</widget>

Обратите внимание на секцию edit-config: в ней задается параметр android:exported="true" — это необходимо для того, чтобы ваше приложение было доступно на последних версиях Android.

Так как наше андроид-приложение — это по факту еще и обычный веб-сайт, добавим сборку web-приложения в Nginx-контейнере, для того чтобы мы сразу могли видеть результат нашей работы (например, если у нас нет под рукой android-эмулятора или устройства, на которое можно поставить приложение), для этого создадим Dockerfile со следующим содержанием:

FROM nginx
COPY ./www /usr/share/nginx/html

Таким образом мы получим на выходе веб-сервер с нашим web-приложением (ранее мы собирали наш web-проект в папку www)

Почти все готово, осталось только написать .gitlab-ci.yml конфигурацию для нашего пайплайна, давайте сделаем это!

stages:
  - build
  - test
  - deploy

build-code-job:
  stage: build
  tags:
    - build
    - docker
  before_script:
    - curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20?%20%5B$CI_PROJECT_NAME%5D%20Build%20job%20started"
  script:
    - PATH=$PATH:/opt/sdk/gradle/6.5/bin
    - mkdir -p ~/.android/ && touch ~/.android/repositories.cfg
    - npm i
    - npm run build
    - doas docker build -t $CI_PROJECT_NAME .
    - cordova create ${CI_PROJECT_NAME}
    - cd ${CI_PROJECT_NAME}
    - rm -Rf ./www/*
    - cp -r ../www/* ./www/
    - cp ../cordova/* ./
    - cordova platform add android
    - cordova build android
  cache:
    paths:
      - node_modules
  artifacts:
    paths:
      - ./${CI_PROJECT_NAME}/platforms/android
    expire_in: 1 week
    name: ${CI_PROJECT_NAME}_${CI_JOB_ID}_android
  after_script:
    - >
      if [ $CI_JOB_STATUS == 'success' ]; then
        curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20✅%20%5B$CI_PROJECT_NAME%5D%20Build%20job%20completed"
        curl -F document=@"./${CI_PROJECT_NAME}/platforms/android/app/build/outputs/apk/debug/app-debug.apk" https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendDocument?chat_id=$TELEGRAM_CHAT_ID
      else
        curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20❌%20%5B$CI_PROJECT_NAME%5D%20Build%20job%20failed%20$CI_PIPELINE_URL"
      fi

test-code-job:
  stage: test
  tags:
    - test
    - docker
  script:
    - doas docker image ls | grep "$CI_PROJECT_NAME"
    - semgrep scan src --config auto --output $CI_PROJECT_NAME-sast-report.json --json
  artifacts:
    paths:
      - $CI_PROJECT_NAME-sast-report.json
    expire_in: 4 week
    name: ${CI_PROJECT_NAME}_${CI_JOB_ID}_sast_report
  after_script:
    - >
      if [ $CI_JOB_STATUS == 'success' ]; then
        curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20✅%20%5B$CI_PROJECT_NAME%5D%20All%20tests%20passed%20successfuly"
        curl -F document=@"$CI_PROJECT_NAME-sast-report.json" https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendDocument?chat_id=$TELEGRAM_CHAT_ID
      else
        curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20❌%20%5B$CI_PROJECT_NAME%5D%20Tests%20failed%20$CI_PIPELINE_URL"
      fi

deploy-code-job:
  stage: deploy
  tags:
    - deploy
    - docker
  script:
    - doas docker stop $CI_PROJECT_NAME || true && doas docker rm $CI_PROJECT_NAME || true
    - doas docker run -d --name $CI_PROJECT_NAME -p $HOST_PORT:80 --restart unless-stopped $CI_PROJECT_NAME
  after_script:
    - >
      if [ $CI_JOB_STATUS == 'success' ]; then
        curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20?%20%5B$CI_PROJECT_NAME%5D%20Deploy%20job%20completed%20$PROJECT_URL"
      else
        curl "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage?chat_id=$TELEGRAM_CHAT_ID&text=%20❌%20%5B$CI_PROJECT_NAME%5D%20Deploy%20job%20failed%20$CI_PIPELINE_URL"
      fi

Данная конфигурация включает 3 шага:

  • build

  • test

  • deploy

Вы, конечно же, понимаете, что происходит на каждом этапе, но я поясню нюансы: помимо непосредственно сборки, тестирования и внедрения происходит также процесс информирования в Telegram-канал. Обратите внимание на условие в after_script — [ $CI_JOB_STATUS == 'success' ] — таким образом мы понимаем, выполнены ли все шаги успешно или же есть ошибки, и в зависимости от статуса конкретного этапа отправляем соответствующее сообщение.

Также обратите внимание на артефакты; на стадии сборки мы получаем в качестве артефакта папку с андроид-проектом — его потом можно открыть в Android Studio, а на стадии тестирования мы получаем отчет SAST-тестирования (вот, наконец, нам и пригодился Python и Semgrep)

Также в случае успешной сборки в Телеграм отправляется apk-файл с приложением и после прохождения тестирования SAST отчет также дублируется в мессенджер.

пример оповещений в телеграм
пример оповещений в телеграм

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

И финальный штрих — определим переменные окружения для нашего проекта:

переменные окружения
переменные окружения

Теперь нам достаточно запушить изменения в репозиторий или запустить пайплайн вручную, для того чтобы конвейер заработал:

Спасибо за внимание, буду рад услышать любые советы и рекомендации от настоящих разработчиков, напоминаю: в этой области я не специалист, просто делюсь своим хобби с вами.


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