Сегодня я расскажу вам как можно опубликовать своё Spring Boot приложение в GitHub Packages с помощью GitHub Actions. Вот так. В общем-то всё. Вот. Спасибо за внимание.

Ну, а здесь, для самых любознательных, приведу немного информации о том, что делает этот GitHub Action.

Старт

Прежде чем перейти к самому GitHub Action приведу сначала то, что он должен уметь делать.

  1. Он должен собрать докер образ.

  2. Выложить его в GitHub Packages.

В качестве примера собирать буду вот этот проект. Подробнее о нём я писал в этой статье. Здесь лишь отмечу, что в качестве системы сборки в нём используется gradle.

Сборка docker-образа

Как же можно собрать docker-образ своего приложения? Ну конечно же, написать Dockerfile! Делать я этого, конечно же, не буду, а воспользуюсь замечательным проектом Cloud Native Buildpacks.

Что же в этом проекте такого замечательного?!. В Spring Boot имеется поддержка этого проекта, что позволяет собрать docker-образ без написания Dockerfile всего одной командой. Например, при использовании в качестве сборщика gradle это команда будет выглядеть так:

./gradlew bootBuildImage

А при использовании maven - так:

mvn spring-boot:build-image

В результате выполнения команды будет создан и локально опубликован docker-образ. Версия java определиться автоматически из настроек сборщика, а в качестве JDK по умолчанию будет использоваться сборка BellSoft Liberica.

Параметры по умолчанию можно изменить. Как это сделать можно посмотреть в настройках плагина для соответствующей системы сборки (maven, gradle). Возможно, через конфигурацию плагинов получится изменить не все параметры, которые есть в buildpack. В этом случае собирать образ можно будет с помощью утилиты pack. Подробнее о сборке java приложений с помощью этой утилиты можно почитать, например, тут. Правда, на мой взгляд, это будет уже не так лаконично, как было с помощью соответствующих плагинов.

Немного о возможных проблемах

Изначально в проекте у меня была java 15. Но при вызове таскиbootBuildImage я получал ошибку вида:

    [creator]     Paketo BellSoft Liberica Buildpack 8.4.0
    [creator]       unable to find dependency
    [creator]       no valid dependencies for jdk, 15.*, and io.buildpacks.stacks.bionic in [(jdk, 8.0.302, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *]) (jre, 8.0.302, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *]) (jdk, 11.0.12, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *]) (jre, 11.0.12, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *]) (native-image-svm, 11.0.12, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *]) (jdk, 16.0.2, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *]) (jre, 16.0.2, [io.buildpacks.stacks.bionic io.paketo.stacks.tiny *])]
    [creator]     ERROR: failed to build: exit status 1

Как оказалось, ошибка была связана с тем, что у BellSoft нет сборки java 15. Но когда-то она была, но после того как они собрали 16-ю версию, 15-ю решили удалить. И это выглядит, мягко говоря, не очень надёжным. Ну и с выходом 17-й так же удалили и 16-ю.

Сборка и публикация через GitHub Actions

Немного о GitHub Actions. Это штука, которая позволяет автоматизировать рабочий процесс прямо в GitHub (настроить CI/CD). Например, с помощью неё можно собирать приложение по коммиту, где-то публиковать, прогонять тесты и делать многое-многое другое.

Переходим к сборке. Для этого нужно создать свой action, который будет этим заниматься. Либо можно добавить существующий из marketplace. Мне, правда, не удалось найти нужный, который бы использовал для сборки docker-образа Cloud Native Buildpacks через gradle-плагин, но, возможно, на момент, когда вы читаете эту статью, этот плагин там есть, либо он и был, а я просто плохо искал.

Для создания своего action в GitHub-репозитории нужно перейти на вкладку Actions. Здесь можно либо самостоятельно настроить workflow нажав на set up a workflow yourself. Либо можно выбрать уже готовое workflow из предложенного списка.

Проще всего, конечно же, выбрать уже готовое workflow и видоизменить его требуемым образом. Так как для сборки проекта используется gradle, то и взять лучше всего workflow для gradle-проекта. На текущих момент чтобы найти это workflow, нужно пролистать страницу вниз до кнопки More continuous integration workflows... и нажать на неё.

Среди предложенных workflow нужно найти Java with Gradle и нажать на кнопку Set up this workflow.

После этого мы перейдём на страницу создания нового action для проекта, на которой уже будет доступен код, который можно редактировать.

В верху страницы для редактирования доступно имя файла, в котором будет находится код этого action.

По умолчанию файл будет называться gradle.yml. Так как данный action будет заниматься сборкой проекта, предлагаю переименовать его в build.yml.

Ниже представлен код данного action, который был сгенерирован автоматически.

# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle

name: Java CI with Gradle

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up JDK 11
      uses: actions/setup-java@v2
      with:
        java-version: '11'
        distribution: 'adopt'
        cache: gradle
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build

Немного разберём что здесь происходит.

name: Java CI with Gradle - имя workflow, будет отображаться на страничке Actions в GitHub.

Блок on описывает, какие события должны запускать данный workflow. В данном случае это push и pull_request, кроме того можно указать, в каких ветках должны происходить данные события с помощью branches. Более подробно о синтаксисе можно почитать в документации.

jobs. В этом блоке описываются job'ы (например, сборка, тестирование и т.п.). Их может быть несколько. По умолчанию они будут запускаться параллельно, но документация говорит, что можно запустить их последовательно. В данном случае есть лишь одна job, которая называется build.

runs-on. В этом блоке указывается runner, т.е., по сути, виртуальная машина, на которой будет выполняться job. В данном случае это ubuntu-latest.

steps. Блок, в котором описаны шаги, из которых состоит job. Каждый шаг является отдельным GitHub Action или shell-командой.

uses: actions/checkout@v2. uses означает, что требуется взять GitHub Action, далее указывается имя GitHub Action, в данном случае actions/checkout@v2. Данный GitHub Action вытягивает код из репозитория в runner.

uses: actions/setup-java@v2. GitHub Action actions/setup-java@v2 устанавливает java в runner. С помощью with можно передавать параметры для данного GitHub Action. java-version задаёт версию java. distribution задаёт дистрибутив. cache: gradle указывает, что необходимо сохранять локальный кэш Gradle в инфраструктуре GitHub Actions таким образом, чтобы он использовался в будущих запусках workflow. Подробнее о допустимых значениях параметров можно почитать на страничке данного GitHub Action. Так же для данного шага указано name. Будет отображаться в логах выполнения job на GitHub.

run: chmod +x gradlew. run позволяет выполнять shell-команды. Команда chmod +x gradlew даёт права на выполнение файла gradlew.

run: ./gradlew build - cобирает проект.

Изменим скрипт таким образом чтобы он делал требуемые нам действия. В результате получится так.

name: build

on: [push]

env:
  IMAGE_NAME: list-keep

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up JDK 17
      uses: actions/setup-java@v2
      with:
        java-version: '17'
        distribution: 'adopt'
    - name: Login to GitHub Container Registry
      run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build a container image from our Spring Boot app using Paketo.io / Cloud Native Build Packs
      run: ./gradlew bootBuildImage --imageName=$IMAGE_NAME
    - name: Tag & publish to GitHub Container Registry
      run: |
        IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
        VERSION=latest
        docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
        docker push $IMAGE_ID:$VERSION

Разберём что изменилось.

on: [push]. Данный workflow теперь запускает любое событие push.

В блоке env объявлена переменна окружения IMAGE_NAME, которая используется далее в скрипте.

java-version: '17'. Используется 17-я версия java. Так же, как можно заметить, в данной конфигурации GitHub Action actions/setup-java@v2 не используется cache для gradle.

run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin. Команда помогает залогиниться в GitHub Packages. С помощью docker login логинимся на ghcr.io, с помощью параметра -u передаём логин пользователя GitHub. Сам логин берём из github-контекста, в котором хранится различная информация, как о workflow, так и о событии которое его запустило. Параметр --password-stdin говорит о том, что пароль, а точнее токен, будет получен из stdin. С помощью echo "${{ secrets.GITHUB_TOKEN }}" передаём токен в stdin. В качестве токена здесь используется GITHUB_TOKEN. Этот токен создаётся в автоматическом режиме в начале каждого запуска workflow, и он доступен как секрет secrets.GITHUB_TOKEN либо его можно получить из github-контекста как github.token.

Немного о правах GITHUB_TOKEN

GITHUB_TOKEN не всесилен. Его набор прав ограничен из коробки. Но его можно расширить с помощью блока permissions.

Так же, например, правами на packages можно управлять на странице настроек пакета (думаю, другими правами так же можно управлять на соответствующих страницах).

На этой странице в блоке Manage Actions access можно добавлять репозитории и давать им права на этот пакет. И если, например, дать права только на чтение репозиторию, то он не сможет с GITHUB_TOKEN сюда публиковать, не смотря на то что у GITHUB_TOKEN по умолчанию есть доступ на запись.

./gradlew bootBuildImage --imageName=$IMAGE_NAME - собирает docker-образ с помощью Cloud Native Buildpacks. По умолчанию имя docker-образа будет docker.io/library/${project.name}:${project.version}, а точнее, в данном конкретном примере, docker.io/library/list-keep:0.0.1-SNAPSHOT. Гораздо более удобно было бы, на мой взгляд, если бы версия была latest. Сделать это можно, переопределив имя docker-образ с помощью параметра --imageName. В этот параметр можно передать как полное название docker-образа - docker.io/library/list-keep:latest, так и часть его, например, list-keep. В этом случае docker.io/library и latest (если версия не указана, то по умолчанию выставляется latest) допишутся автоматически.

Далее идёт многострочный скрипт публикации docker-образа в GitHub Packages. Чтобы скрипт был многострочным, необходимо в самом начале скрипта указать символ |.

IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME превратится в ghcr.io/vanbv/list-keep. Это адрес пакета в GitHub Packages, куда будет опубликован собранный docker-образ.

VERSION=latest. В качестве версии будем использовать latest. Если требуется делать что-то более сложное, например, проставлять версию в соответствии с версией приложения, то в документации GitHub Actions есть неплохой пример того, как это можно сделать.

docker tag $IMAGE_NAME $IMAGE_ID:$VERSION превратится в docker tag list-keep ghcr.io/vanbv/list-keep:latest. Команда docker tag создаст тег ghcr.io/vanbv/list-keep:latest, который будет ссылаться на list-keep:latest (т.к. версия не указана, то по умолчанию возьмётся latest).

docker push $IMAGE_ID:$VERSION превратится в docker push ghcr.io/vanbv/list-keep:latest. docker push опубликует docker-образ в GitHub Packages.

Заключение

Вот, в общем-то, и всё. Надеюсь, было интересно и полезно. Берегите там себя.