Использование кэшей в GitHub Actions может существенно ускорить выполнение ваших пайплайнов, а потому игнорировать эту возможность глупо. Тем не менее без нюансов не обошлось. Дело в том, что кэши в GitHub Actions представляют из себя достаточно закрытый сервис, который предлагает пользователям минимум настроек, но обо всем по порядку.
Использование кэшей в GitHub Actions
В качестве задачи нужно подключить использование кэшей Gradle для сборки Java-приложений. Обычно это позволяет значительно ускорить процесс сборки за счет переиспользования существующих артефактов вместо повторного их сбора с нуля.
В использовании кэшей вам поможет экшн actions/cache@v3 1 и официальная документация 2. На вход он принимает совсем немного параметров, но некоторые из них имеют принципиальное влияние на эффективность переиспользования кэшей в последующем. Всего доступно три опции, описание которых вы найдете в официальной документации, а я постараюсь дать их расширенную трактовку, чтобы пролить свет на принцип работы экшна. Итак.
path
Path – это список путей до файлов или директорий, которые нужно будет сохранить (закэшировать). То есть GitHub загрузит их себе с раннера, на котором запускалась сборка вашего проекта, и сохранит в свое хранилище, предварительно заархивировав. Все это произойдет разумеется только при успешном выполнении задания.
Пример использования экшна:
1 2 3 4 5 6 7 |
- uses: actions/cache@v3 with: path: | /root/.gradle/caches /root/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('./*.gradle*', './subfolder*/*.gradle*') }} restore-keys: ${{ runner.os }}-gradle- |
В примере выше будет создан архив, содержащий в себе два каталога (/root/.gradle/{caches,wrapper}).
Теперь подробнее об остальных.
key
Key – Это основной параметр, правильная установка которого очень важна. Собственно это ни что иное, как индекс вашего сохраненного архива с кэшами. По этому индексу будет оцениваться факт попадания в кэши (cache hit).
Основной нюанс в том, что кэши будут устаревать и если вы сохраните кэш со статическим ключом, например ${{ runner.os }}-gradle-cache, то впоследствии каждая сборка вероятно будет всегда использовать именно его. По мере развития вашего проекта с каждым коммитом процент попадания в кэши будет все меньше и меньше, а следовательно эффективность их использования радикально упадет. Перезаписывать существующий кэш при этом нельзя.
Нужен механизм обновления кэшей и он есть. Можно формировать индекс кэша динамически на основе хэша файлов проекта. Для этого используется функция hashFiles.
В итоге ключ может выглядеть следующим образом:
1 |
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} |
Идея в том, что при изменении файлов проекта будет меняться и их суммарный хэш. То есть в итоге у вас будет множество сохраненных архивов кэшей с разными индексами.
Ошибки
Если вдруг столкнетесь с ошибкой:
1 |
hashFiles('**/*.gradle*') failed. Fail to hash files under directory |
Причиной могут служить дополнительные каталоги/файлы, которые создает ваш проект во время сборки. hashFiles вероятно не может получить к ним доступ и вываливается с ошибкой. В качестве решения попробуйте указать пути до файлов более конкретно (либо выставить необходимые разрешения), например так:
1 |
key: ${{ runner.os }}-gradle-${{ hashFiles('*/*.gradle*') }} |
restore-keys
Параметр отвечает за порядок подбора ключей (каждому ключу, напомню, сопоставлен архив с кэшами), если вдруг точного попадания не получилось. Например ваш ключ был такой:
1 |
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*) }} |
При вычислении хэшей получили значение:
1 |
Linux-gradle-15077f2d4ec4ec53212f39404fa603175c0401cdd148c1b0668344673c5d886e |
Потом файлы, попадающие под маску **/*.gradle*, поменялись и при слующей сборке ключ стал таким:
1 |
Linux-gradle-2757dca223878df84baa17d9fd9114b625203b6dff186243621c63cd3150edde |
Понятно, что точного попадания уже не произойдет, но это и не требуется. Можно отбросить часть с хэшем и сделать маску более общей:
1 2 3 |
restore-keys: | ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} ${{ runner.os }}-gradle- |
Первая строка тут не нужна и добавлена больше для наглядности, ведь сначала и так будет проверяться факт 100% попадания key в какой-либо существующий в хранилище ключ. Если попаданий несколько, то будет взят тот ключ, у которого последнее время использования самое свежее. Дальше экшн загрузит на раннер и распакует архив с кэшами как раз в то место, которое указано в path.
Если даже в этом случае попаданий не произошло, то экшн проверит кэши в вышестоящей и основной ветках. Именно туда и только туда у него есть доступ. Кэши соседних веток он посмотреть уже не сможет. При этом разные workflow будут иметь доступ к соответствующим кэшам одной и той же ветки.
Важно отметить, что у кэшей есть срок “годности”, который по умолчанию составляет 7 дней. После этого кэш удаляется. Есть также и максимальный объем хранилища кэшей – 10ГБ. Если вдруг максимальный объем достигнут, но вы продолжаете складывать кэши, то данные будут вытесняться из хранилища по принципу “первый пришел, первый ушел”, то есть будут удаляться самые старые.
При использовании self-hosted раннеров у вас также есть возможность пользоваться кэшами, но вы должны учитывать время, в течение которого данные будут выкачиваться с серверов GitHub до вашего раннера. В итоге собрать проект без кэшей может получиться быстрее, чем выкачивать их.
В любом случае проектирование своего собственного решения не такая простая задача. Если все же этот путь для вас актуален, то важно учесть все нюансы, иначе потом придется заплатить за ошибки в разы больше. Технически это все реализовать не так сложно, а с проектированием и аудитом я могу помочь. На этом все, успехов!