Круги ада с GitHub Actions (строим CI/CD pipeline для Java-проекта)

un6l1_vcriwkzfwfrmcjm_jjihw.jpeg

Мне частенько приходится строить пайплайн для сборки проектов на Java. Иногда это опенсорс, иногда нет. Недавно я решил попробовать перенести часть своих репозиториев с Travis-CI и TeamCity на GitHub Actions, и вот что из этого получилось.

Что будем автоматизировать


Для начала нам нужен проект, который мы будем автоматизировать, давайте сделаем небольшое приложение на Spring boot / Java 11 / Maven. В рамках этой статьи логика приложения нас интересовать не будет совсем, нам важна инфраструктура вокруг приложения, так что нам хватит простенького REST API контроллера.

Посмотреть исходники можно тут: github.com/antkorwin/github-actions все этапы построения pipeline-конвейера отражены в пулл-реквестах этого проекта.

JIRA и планирование


Стоит сказать, что мы обычно используем JIRA в качестве трекера задач, так что давайте заведем отдельную борду под этот проект и накидаем туда первые задачи:

xzs-uy7iin7o2qqv-3g4_d52dtk.png

Чуть позже мы еще вернемся к тому, что интересного могут дать в связке JIRA и GitHub.

Автоматизируем сборку проекта


Наш тестовый проект собирается через maven, так что сборка его довольно простая, все, что нам нужно, это mvn clean package.

Чтобы сделать это при помощи Github Actions, нам нужно будет создать в репозитории файл с описанием нашего workflow, это можно сделать обычным yml-файлом, не могу сказать что мне нравится «программирование на yml», но что поделать — делаем в директории .github/workflow/ файл build.yml в котором будем описывать действия при сборке мастер ветки:

name: Build

on:
  pull_request:
    branches:
      - '*'
  push:
    branches:
      - 'master'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Package
        run: mvn -B clean package -DskipTests

on — это описание события, по которому будет запускаться наш скрипт.

on: pull_request / push — говорит о том, что этот workflow нужно запускать при каждом пуше в мастер и создании пулл-реквестов.

Дальше идет описание заданий (jobs) и шаги выполнения (steps) для каждой задачи.

runs-on — тут мы можем выбрать целевую ОС, на удивление можно выбрать даже Mac OS, но на приватных репозиториях это довольно дорогое удовольствие (в сравнении с linux).

uses позволяет переиспользовать другие экшены, так например при помощи экшена actions/setup-java мы устанавливаем окружение для Java 11.

При помощи with мы можем указать параметры с которыми запускаем действие, по сути это аргументы, которые будут передаваться в экшен.

Остается только запустить мавеном сборку проекта: run: mvn -B clean package флаг -B говорит о том, что нам нужен non-interactive mode, чтобы мавен вдруг не захотел что-то у нас спросить

xj7p9qsrrl33kzdr8_ohidkf8gg.png

Отлично! Теперь при каждом коммите в мастер, запускается сборка проекта.

Автоматизируем запуск тестов


Сборка это хорошо, но в реальности проект может благополучно собираться, но не работать. Поэтому следующим шагом нужно заняться автоматизацией прогона тестов. К тому же, довольно удобно смотреть результат прохода тестов, когда делаешь ревью PR — ты точно знаешь, что тесты проходят и никто не забыл, перед тем как делать merge, прогнать свою ветку.

Делаем запуск тестов при создании пулл-реквеста и merge в мастер, а заодно добавим построение отчета о code-coverage.

name: Build

on:
  pull_request:
    branches:
      - '*'
  push:
    branches:
      - 'master'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Verify
        run: mvn -B clean verify
      - name: Test Coverage
        uses: codecov/codecov-action@v1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Для покрытия тестов я использую codecov в связке с jacoco плагином. У codecov есть свой экшен, но ему для работы с нашим pull-request-ом нужен токен:

${{ secrets.CODECOV_TOKEN }} — такую конструкцию мы будем встречать еще не один раз, secrets это механизм хранения секретов в гитхабе, мы можем там прописать пароли/токены/хосты/url-ы и прочие данные, которыми не стоит светить в кодовой базе репозитория.

Добавить переменную в secrets, можно в настройках репозитория на GitHub:

bg25ivc_dw5hnfsbtq5xl2uaapw.png

Получить токен можно на codecov.io после авторизации через GitHub, для добавления public проекта нужно просто пройти по ссылке вида: GitHub user name/[repo name]. Приватный репозиторий тоже можно добавить, для этого надо дать права codecov приложению в гитхабе.

pbqognc8tkkii_evzpklgiposfq.png

Добавляем jacoco плагин в POM-файл:


	org.jacoco
	jacoco-maven-plugin
	0.8.4
	
		
			
				prepare-agent
			
		
		
		
			report
			test
			
				report
			
		
	


	org.apache.maven.plugins
	maven-surefire-plugin
	2.22.2
	
		plain
		
			**/*Test*.java
			**/*IT*.java
		
	

Теперь в каждый наш пулл-реквест будет заходить codecov бот и добавлять график изменения покрытия:

mw71npinurilxp7lqyivauh1et8.png

Добавим статический анализатор


В большинстве своих oпенсорс-проектов я использую sonar cloud для статического анализа кода, его довольно легко подключить к travis-ci. Так что это логичный шаг при миграции на GitHub Actions, сделать тоже самое. Маркет экшенов — клевая штука, но в этот раз он немного подвел, потому что я по привычке нашел нужный экшен и прописал его в workflow. А оказалось, что sonar не поддерживает работу через действие для анализа проектов на maven или gradle. Об этом конечно написано в документации, но кто же ее читает?!

Через действие нельзя, поэтому будем делать через mvn плагин:

name: SonarCloud

on:
  push:
    branches:
      - master
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  sonarcloud:
    runs-on: ubuntu-16.04
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Analyze with SonarCloud
#       set environment variables:
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
#       run sonar maven plugin:
        run: mvn -B verify sonar:sonar -Dsonar.projectKey=antkorwin_github-actions -Dsonar.organization=antkorwin-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml

SONAR_TOKEN — можно получить в sonarcloud.io и нужно прописать его в secrets. GITHUB_TOKEN — это встроенный токен, который генерит гитхаб, с помощью него sonarcloud[bot] сможет авторизоваться в гите, чтобы оставлять нам сообщения в пулл-реквестах.

Dsonar.projectKey — название проекта в сонаре, посмотреть можно в настройках проекта.

Dsonar.organization — название организации из GitHub.

Делаем пулл-реквест и ждем, когда sonarcloud[bot] придет в комментарии:

4hohh12hbfyjr6o_7f_zvxbvmvy.png

Release management


Билд настроили, тесты прогнали, можно и релиз сделать. Давайте посмотрим, как GitHub Actions помогает существенно упростить release managеment.

На работе у меня есть проекты, кодовая база которых лежит в bitbucket (все как в той истории «днем пишу в битбакет, ночью коммичу в GitHub»). К сожалению, в bitbucket нет встроенных средств для управления релизами. Это проблема, потому что под каждый релиз приходится руками заводить страничку в confluence и скидывать туда все фичи вошедшие в релиз, шерстить чертоги разума, таски в jira, коммиты в репозитории. Шансов ошибиться много, можно что-то забыть или вписать то, что уже релизили в прошлый раз, иногда просто не понятно, к чему отнести какой-то пулл-реквест — это фича или фикс багов, или правка тестов, или что-то инфраструктурное.

Как нам может помочь GitHub actions? Есть отличный экшен — release drafter, он позволяет задать шаблон файла release notes, чтобы настроить категории пулл-реквестов и автоматически группировать их в release notes файле:

fwrqfprnrcq3iiinqdyzuqufpay.png

Пример шаблона для настройки отчета (.github/release-drafter.yml):

name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
  - title: ' New Features'
    labels:
      - 'type:features'
# в эту категорию собираем все PR с меткой type:features

  - title: ' Bugs Fixes'
    labels:
      - 'type:fix'
# аналогично для метки type:fix и т.д.

  - title: ' Documentation'
    labels:
      - 'type:documentation'

  - title: ' Configuration'
    labels:
      - 'type:config'

change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
  ## Changes
  $CHANGES

Добавляем скрипт для генерации черновика релиза (.github/workflows/release-draft.yml):

name: "Create draft release"

on:
  push:
    branches:
      - master

jobs:
  update_draft_release:
    runs-on: ubuntu-18.04
    steps:
      - uses: release-drafter/release-drafter@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Все пулл-реквесты с этого момента будут собираться в release notes автоматически — magic!

Тут может возникнуть вопрос:, а что если разработчики забудут проставить метки в PR? Тогда непонятно, в какую категорию его отнести, и опять придется разбираться вручную, с каждым PR-ом отдельно. Чтобы исправить эту проблему, мы можем воспользоваться еще одним экшеном — label verifier — он проверяет наличие тэгов на пулл-реквесте. Если нет ни одного обязательного тэга, то проверка будет завалена и сообщение об этом мы увидим в нашем пулл-реквесте.

name: "Verify type labels"

on:
  pull_request:
    types: [opened, labeled, unlabeled, synchronize]

jobs:
  triage:
    runs-on: ubuntu-18.04
    steps:
      - uses: zwaldowski/match-label-action@v2
        with:
          allowed: 'type:fix, type:features, type:documentation, type:tests, type:config'

Теперь любой pull-request нужно пометить одним из тэгов: type: fix, type: features, type: documentation, type: tests, type: config.

kc5k6rk_-0chuslyrvwpnexptmg.png

Авто-аннотирование пулл-реквестов


Раз уж мы коснулись такой темы как эффективная работа с пулл-реквестами, то стоит сказать еще о таком экшене, как labeler, он проставляет метки в PR на основании того, какие файлы были изменены. Например, мы можем пометить как [build] любой пул-реквест в котором есть изменения в каталоге .github/workflow.

Подключить его довольно просто:

name: "Auto-assign themes to PR"

on:
  - pull_request

jobs:
  triage:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/labeler@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

Еще нам понадобится файл с описанием соответствия каталогов проекта с тематиками пулл-реквестов:

theme:build:
  - ".github/**"
  - "pom.xml"
  - ".travis.yml"
  - ".gitignore"
  - "Dockerfile"

theme:code:
  - "src/main/*"

theme:tests:
  - "src/test/*"

theme:documentation:
  - "docs/**"

theme:TRASH:
  - ".idea/**"
  - "target/**"

Подружить действие автоматически проставляющее метки в пулл-реквесты и действие, проверяющее наличие обязательных меток, у меня не вышло, match-label на отрез не хочет видеть проставленные ботом метки. Похоже проще написать свое действие, совмещающее оба этапа. Но даже в таком виде пользоваться довольно удобно, нужно выбрать метку из списка при создании пулл-реквеста.

Пора деплоить


mk07mc-cd0mtdobukpvuw2udi8c.png

Я попробовал несколько вариантов деплоя через GitHub Actions (через ssh, через scp, и при помощи docker-hub), и могу сказать, что, скорее всего, вы найдете способ залить бинарку на сервер, каким бы извращенным не был ваш pipeline.

Мне понравился вариант держать всю инфраструктуру в одном месте, поэтому рассмотрим, как сделать деплой в GitHub Packages (это репозиторий для бинарного контента, npm, jar, docker).

Cкприпт сборки docker образа и публикации его в GitHub Packages:

name: Deploy docker image

on:
  push:
    branches:
      - 'master'

jobs:

  build_docker_image:
    runs-on: ubuntu-18.04
    steps:

#     Build JAR:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Package
        run: mvn -B clean compile package -DskipTests

#     Set global environment variables:
      - name: set global env
        id: global_env
        run: |
          echo "::set-output name=IMAGE_NAME::${GITHUB_REPOSITORY#*/}"
          echo "::set-output name=DOCKERHUB_IMAGE_NAME::docker.pkg.github.com/${GITHUB_REPOSITORY}/${GITHUB_REPOSITORY#*/}"

#     Build Docker image:
      - name: Build and tag image
        run: |
          docker build -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:latest" -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:${GITHUB_SHA::8}" .

      - name: Docker login
        run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}

#     Publish image to github package repository:
      - name: Publish image
        env:
          IMAGE_NAME: $GITHUB_REPOSITORY
        run: docker push "docker.pkg.github.com/$GITHUB_REPOSITORY/${{ steps.global_env.outputs.IMAGE_NAME }}"

Для начала нам надо собрать JAR-файл нашего приложения, после чего мы вычисляем путь к GitHub docker registry и название нашего образа. Тут есть несколько хитростей, с которыми мы еще не сталкивались:

  • конструкция вида: echo »:: set-output name=NAME: VALUE» позволяет задать значение переменной в текущем шаге, так чтобы его потом можно было прочитать во всех остальных шагах.
  • получить значение переменной установленой на предыдущем шаге можно через идентификатор этого шага: ${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}
  • В стандартной переменной GITHUB_REPOSITORY хранится название репозитория и его владелец («owner/repo-name»). Для того чтобы вырезать из этой строки все кроме названия репозитория, воспользуемся bash синтаксисом: ${GITHUB_REPOSITORY#*/}

Далее нам нужно собрать докер-образ:

docker build -t "docker.pkg.github.com/antkorwin/github-actions/github-actions:latest"

Авторизоваться в registry:

docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}

И опубликовать образ в GitHub Packages Repository:

docker push "docker.pkg.github.com/antkorwin/github-actions/github-actions"

Для того чтобы указать версию образа, мы используем первые цифры из SHA-хэша коммита — GITHUB_SHA тут тоже есть нюансы, если вы будете делать такие сборки не только при merge в master, а еще и по событию создания пулл-реквеста, то SHA может не совпадать с хэшем, который мы видим в истории гита, потому что действие actions/checkout делает свой уникальный хэш, чтобы избежать взаимных блокировок действий в PR.

jhptfhpfkbgnjjlnvtlnn2nhkng.png

Если все получилось благополучно, то открыв раздел packages (https://github.com/antkorwin/github-actions/packages) в репозитории, вы увидите новый докер образ:

tegsx5tmshvo28a9t1wb5epq7ho.png

Там же можно посмотреть список версий докер-образа.

Остается только настроить наш сервер на работу с этим registry и запустить перезапуск сервиса. О том как это сделать через systemd, я, пожалуй, расскажу в другой раз.

Мониторинг


Давайте посмотрим несложный вариант, как делать health check нашего приложения при помощи GitHub Actions. В нашем бутовом приложении есть actuator, так что API для проверки его состояния даже и писать не надо, для ленивых уже все сделали. Нужно только дернуть хост: SERVER-URL:PORT/actuator/health

$ curl -v 127.0.0.1:8080/actuator/health

> GET /actuator/health HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.1
> Accept: */*

< HTTP/1.1 200
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< Transfer-Encoding: chunked
< Date: Thu, 04 Jun 2020 12:33:37 GMT

{"status":"UP"}

Все, что нам нужно — написать таск проверки сервера по крону, ну, а если вдруг он нам не ответит, то будем слать уведомление в телеграм.

Для начала разберемся, как запустить workflow по крону:

on:
  schedule:
    - cron:  '*/5 * * * *'

Все просто, даже не верится что в гитхабе можно сделать такие ивенты, которые совсем не укладываются в webhook-и. Детали есть в документации: help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule

Проверку статуса сервера сделаем руками через curl:

jobs:
  ping:
    runs-on: ubuntu-18.04
    steps:

      - name: curl actuator
        id: ping
        run: |
          echo "::set-output name=status::$(curl ${{secrets.SERVER_HOST}}/api/actuator/health)"

      - name: health check
        run: |
          if [[ ${{ steps.ping.outputs.status }} != *"UP"* ]]; then
            echo "health check is failed"
            exit 1
          fi
          echo "It's OK"

Сначала сохраняем в переменную то, что ответил сервер на запрос, на следующем шаге проверяем что статус UP и, если это не так, то выходим с ошибкой. Если нужно руками «завалить» действие, то exit 1 — подходящее оружие.

  - name: send alert in telegram
    if: ${{ failure() }}
    uses: appleboy/telegram-action@master
    with:
      to: ${{ secrets.TELEGRAM_TO }}
      token: ${{ secrets.TELEGRAM_TOKEN }}
      message: |
        Health check of the:
        ${{secrets.SERVER_HOST}}/api/actuator/health
        failed with the result:
        ${{ steps.ping.outputs.status }}

Отправку в телеграм делаем, только если действие завалилось на предыдущем шаге. Для отправки сообщения используем appleboy/telegram-action, о том, как получить токен бота и id чата можно почитать в документации: github.com/appleboy/telegram-action

oh2nlfgbkid6yzrr1wkzasp-vj8.png

Не забудьте прописать в секретах на гитхабе: URL для сервера и токены для телеграм бота.

Бонус трек — JIRA для ленивых


Я обещал что мы вернемся к JIRA, и мы вернулись. Сотни раз наблюдал на стендапах ситуацию, когда разработчики сделали фичу, слили ветку, но забыли перетянуть задачу в JIRA. Конечно, если бы все это делалось в одном месте, то было бы проще, но фактически мы пишем код в IDE, сливаем ветки в bitbucket или GitHub, а задачи потом таскаем в Jira, для этого надо открывать новые окна, иногда логиниться еще раз и т.д. Когда ты прекрасно помнишь, что надо делать дальше, то открывать борду лишний раз нет смысла. В итоге, утром на стендапе надо тратить время на актуализацию доски задач.

GitHub поможет нам и в этом рутинном занятии, для начала мы можем перетягивать задачи автоматом, в колонку code_review, когда закинули пулл-реквест. Все, что нужно — это придерживаться соглашения в наименовании веток:

[имя проекта]-[номер таска]-название

например, если ключ проекта «GitHub Actions» будет GA, то GA-8-jira-bot может быть веткой для реализации задачи GA-8.

Интеграция с JIRA работает через экшены от Atlassian, они не идеальны, надо сказать, что некоторые из них у меня вообще не заработали. Но мы обсудим только те, что точно работают и активно используются.

Для начала нужно пройти авторизацию в JIRA при помощи действия: atlassian/gajira-login

jobs:
  build:
    runs-on: ubuntu-latest
    name: Jira Workflow
    steps:
      - name: Login
        uses: atlassian/gajira-login@master
        env:
          JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}

Для этого надо получить токен в JIRA, как это сделать расписано тут: confluence.atlassian.com/cloud/api-tokens-938839638.html

Вычленяем идентификатор задачи из названия ветки:

  - name: Find Issue
    id: find_issue
    shell: bash
    run: |
      echo "::set-output name=ISSUE_ID::$(echo ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}')"
      echo brach name: $GITHUB_HEAD_REF
      echo extracted issue: ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}'

  - name: Check Issue
    shell: bash
    run: |
      if [[ "${{steps.find_issue.outputs.ISSUE_ID}}" == "" ]]; then
        echo "Please name your branch according to the JIRA issue: [project_key]-[task_number]-branch_name"
        exit 1
      fi
      echo succcessfully found JIRA issue: ${{steps.find_issue.outputs.ISSUE_ID}}

Если поискать в GitHub marketplace, то можно найти действие для этой задачи, но мне пришлось написать тоже самое через grep по названию ветки, потому что это действие от Atlassian ни в какую не захотело работать на моем проекте, разбираться, что же там не так — дольше, чем сделать руками тоже самое.

Осталось только переместить задачу в колонку «Code review» при создании пулл-реквеста:

  - name: Transition issue
    if: ${{ success() }}
    uses: atlassian/gajira-transition@master
    with:
      issue: ${{ steps.find_issue.outputs.ISSUE_ID }}
      transition: "Code review"

Для этого есть специальное действие на GitHub, все, что ему нужно — это идентификатор задачи, полученный на предыдущем шаге и авторизация в JIRA, которую мы делали выше.

tmcu2ly_devknm-hwczz6lf4nno.gif

Таким же образом можно перетягивать задачи при merge в мастер, и других событиях из GitHub workflow. В общем, все зависит от вашей фантазии и желания автоматизировать рутинные процессы.

Выводы


Если посмотреть на классическую диаграмму DEVOPS, то мы покрыли все этапы, разве что кроме operate, думаю, если постараться, то можно найти какой-нибудь экшен в маркете для интеграции с help-desk системой, так что будем считать что pipeline получился основательный и на основании его использования можно сделать выводы.

zpa-6ljmrj41slucmv1u-bon71o.jpeg

Плюсы:

  • Marketplace с готовыми действиями на все случаи жизни, это очень круто. В большинстве из них еще и исходники можно посмотреть, чтобы понять как решить похожую задачу либо запостить feature request автору прямо в гитхаб репозитории.
  • Выбор целевой платформы для сборки: Linux, mac os, windows довольно интересная фича.
  • Github Packages отличная вещь, держать всю инфраструктуру в одном месте удобно, не надо серфить по разным окошкам, все в радиусе одного-двух кликов мыши и прекрасно интегрировано с GitHub Actions. Поддержка docker registry в бесплатной версии — это тоже хорошее преимущество.
  • GitHub прячет секреты в логах сборки, поэтому пользоваться им для хранения паролей и токенов не так уж и страшно. За все время экспериментов мне не удалось ни разу увидеть секрет в чистом виде в консоли.
  • Бесплатен для Open Source проектов


Минусы:

  • YML, ну не люблю я его. При работе с таким флоу у меня самый частый commit message это «fix yml format», то забудешь где-то таб поставить, то не на той строке напишешь. В общем, сидеть перед экраном с транспортиром и линейкой не самое приятное занятие.
  • DEBUG, отлаживать флоу коммитами, запуском пересборки и выводом в консоль не всегда удобно, но это больше из разряда «вы зажрались», привыкли работать с удобными IDEA, когда можно отлаживать все, что угодно.
  • Свой экшен можно написать на чем угодно если завернуть его в докер, но нативно поддерживается только javascript, конечно это дело вкуса, но я бы предпочел что-то другое заместо js.


Напомню, что репозиторий со всеми скриптами тут: github.com/antkorwin/github-actions

На следующей неделе я буду выступать с докладом на конференции Heisenbug 2020 Piter. Расскажу не только, как избежать ошибок при подготовке тестовых данных, но и поделюсь своими секретами работы с наборами данных в Java-приложениях!

© Habrahabr.ru