The journey from CircleCI to GitHub Actions
So, you’re a longtime user of CircleCI with projects set up and running like clockwork. But, one fine morning, it strikes you to switch to GitHub Actions. Why? Maybe you’d like to use fewer third party services and want to keep your code and CI process in one place? Perhaps you already have a paid GitHub account with free GitHub Actions minutes just lying around? One way or another, the decision has been made, and it’s time to figure out what awaits us on the way from CircleCI to GitHub Actions.
In this article, we’ll answer these questions:
- Is it possible to migrate a Ruby on Rails or a Node.js project from CircleCI to GitHub Actions with (almost) no changes?
- What are the differences between CircleCI and GitHub Actions, and most importantly, how are they similar?
- Is it be possible to make the CI process on GitHub Actions faster than on CircleCI?
- Do we really need containers for CI?
- How to speed up CI with Docker image build caching?
An overview of GitHub Actions
Before proceeding to the configuration, let’s take a general look at GitHub Actions. As you can see from the overview, it’s very similar to CircleCI. It has the same workflows
, which consist of jobs
, which in turn are a sequence of one or more steps
.
steps
can be executed both in the Docker container and directly on the runner’s OS. Each individual step
can be either a regular shell command or an action.
Actions are analogous to orbs
from CircleCI; they’re used for the same purpose: to combine many operations under one roof and to simplify their reuse. For example, the setup-node action installs the desired version of Node.js, adds it to PATH
, and so on.
Most actions are written in JavaScript, but this is not the only way to create them.
You can even use a Docker image as an action! I’ll show an example with this approach at the end of the article.
CircleCI setup
As a test case, I’ll be using a site based on Ruby on Rails and Node.js. Our starting point is the .circleci/config.yml
file:
version: 2.1
workflows:
version: 2
build-and-deploy:
jobs:
- checkout_code
- bundle_install:
requires:
- checkout_code
- yarn_install:
requires:
- checkout_code
- rubocop:
requires:
- bundle_install
- yarn_build:
requires:
- yarn_install
- test:
requires:
- bundle_install
- yarn_build
executors:
ruby:
docker:
- image: cimg/ruby:3.1.2-node
environment:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
rails:
docker:
- image: cimg/ruby:3.1.2-browsers
environment:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
INCLUDE_FIXTURES: true
DATABASE_URL: postgresql://root@localhost:5432/circle_test
FAIL_SCREEN: 1
- image: cimg/postgres:14.5
environment:
POSTGRES_USER: root
POSTGRES_DB: circle_test
jobs:
checkout_code:
executor: ruby
steps:
- attach_workspace:
at: .
- checkout
- persist_to_workspace:
root: .
paths: .
bundle_install:
executor: ruby
steps:
- attach_workspace:
at: .
- restore_cache:
keys:
- bundle-{{ checksum "Gemfile.lock" }}
- run:
name: Configure Bundler
command: |
echo 'export BUNDLER_VERSION=$(cat Gemfile.lock | tail -1 | tr -d " ")' >> $BASH_ENV
source $BASH_ENV
gem install bundler
- run:
name: Bundle Install
command: bundle check || bundle install
- save_cache:
key: bundle-{{ checksum "Gemfile.lock" }}
paths: vendor/bundle
- persist_to_workspace:
root: .
paths: vendor/bundle
yarn_install:
executor: ruby
steps:
- attach_workspace:
at: .
- restore_node_modules_cache
- run:
name: Yarn Install
command: yarn install --verbose
- save_node_modules_cache
yarn_build:
executor: ruby
steps:
- attach_workspace:
at: .
- restore_node_modules_cache
- restore_assets_cache
- run:
name: Yarn Build
command: yarn build --verbose
- save_assets_cache
rubocop:
executor: ruby
steps:
- attach_workspace:
at: .
- run:
name: Rubocop
command: |
bundle exec rubocop --fail-level E
test:
parallelism: 1
executor: rails
steps:
- attach_workspace:
at: .
- restore_node_modules_cache
- restore_assets_cache
- run:
name: Wait for database
command: dockerize -wait tcp://localhost:5432 -timeout 1m
- run:
name: Database setup
command: bundle exec rails db:test:prepare
- run:
name: RSpec system tests
command: |
BUILT_ASSETS_AVAILABLE=1 bundle exec rspec
commands:
restore_node_modules_cache:
description: Restore node_modules/
steps:
- restore_cache:
keys:
- v1-yarn-dependency-cache-{{ checksum "yarn.lock" }}
- v1-yarn-dependency-cache
save_node_modules_cache:
description: Save node_modules/
steps:
- save_cache:
key: v1-yarn-dependency-cache-{{ checksum "yarn.lock" }}
paths:
- node_modules
- ~/.cache/yarn
restore_assets_cache:
description: Restore assets cache
steps:
- restore_cache:
keys:
- v1-assets-cache-{{ .Branch }}-{{ .Revision }}
- v1-assets-cache-{{ .Branch }}
- v1-assets-cache
save_assets_cache:
description: Save compiled assets
steps:
- save_cache:
key: v1-assets-cache-{{ .Branch }}-{{ .Revision }}
paths:
- public/front
Everything is as usual here: bundle
, yarn
, rspec
and rubocop
.
A few important notes on the configuration above:
- In CircleCI there is such a thing as an executor. There are four types that can be declared and configured:
docker
,machine
,macos
, orwindows
. Later, for eachjob
, it’s sufficient to specifyexecutor: name-of-executor
to execute all subsequent steps using it. This is an important note because GitHub Actions does not use this approach. In our CircleCI configuration, one of two executors is used for all steps:ruby
andrails
. We use the standard Docker images provided and maintained by CircleCI and configure them by passing in environment variables. - To avoid doing a
checkout
at the beginning of each step, we use thecheckout_code
job, which runsgit clone
once and stores the contents of the repository in theworkspace
, which in turn is passed between the individual steps. This speeds up the wholeworkflow
, especially if the repository is very large. - The
bundler
andyarn
dependencies, as well as compiled assets, are cached to speed up rebuilds.
So, it’s time to turn your existing CircleCI config into a GitHub Actions config, but how? Let’s go with the easiest way: we’ll try to repeat the same steps with minimal changes and see what happens:
name: Release workflow for Kuvaq
on:
push:
branches:
- main
jobs:
checkout_code:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Save repository's content as artifact
uses: actions/upload-artifact@v4
with:
name: repo
path: ${{ github.workspace }}
bundle_install:
runs-on: ubuntu-24.04
needs: checkout_code
container: cimg/ruby:3.1.2-node
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
steps:
- name: Setup file system permissions
run: sudo chmod -R 777 $GITHUB_WORKSPACE /github /__w/_temp
- name: Get repository's content
uses: actions/download-artifact@v4
with:
name: repo
- uses: actions/cache@v4
with:
path: vendor/bundle
key: bundle-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
bundle-${{ hashFiles('**/Gemfile.lock') }}
bundle-
- name: Bundle install
run: |
gem install bundler -v "$(cat Gemfile.lock | tail -1 | tr -d " ")"
bundle install
yarn_install:
runs-on: ubuntu-24.04
needs: checkout_code
container: cimg/ruby:3.1.2-node
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
steps:
- name: Setup file system permissions
run: sudo chmod -R 777 $GITHUB_WORKSPACE /github /__w/_temp
- name: Get repository's content
uses: actions/download-artifact@v4
with:
name: repo
- uses: actions/cache@v4
with:
path: |
node_modules
~/.cache/yarn
key: v1-yarn-dependency-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
v1-yarn-dependency-cache-${{ hashFiles('**/yarn.lock') }}
v1-yarn-dependency-cache-
- name: Yarn Install
run: yarn install --verbose
yarn_build:
runs-on: ubuntu-24.04
needs: yarn_install
container: cimg/ruby:3.1.2-node
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
steps:
- name: Setup file system permissions
run: sudo chmod -R 777 $GITHUB_WORKSPACE /github /__w/_temp
- name: Get repository's content
uses: actions/download-artifact@v4
with:
name: repo
- uses: actions/cache@v4
with:
path: |
node_modules
~/.cache/yarn
key: v1-yarn-dependency-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
v1-yarn-dependency-cache-${{ hashFiles('**/yarn.lock') }}
v1-yarn-dependency-cache-
- uses: actions/cache@v4
with:
path: public/front
key: v1-assets-cache-${{ github.ref }}-${{ github.sha }}
restore-keys: |
v1-assets-cache-${{ github.ref }}-${{ github.sha }}
v1-assets-cache-${{ github.ref }}
v1-assets-cache
- name: Yarn Build
run: yarn build --verbose
rubocop:
needs: bundle_install
runs-on: ubuntu-24.04
container: cimg/ruby:3.1.2-node
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
steps:
- name: Setup file system permissions
run: sudo chmod -R 777 $GITHUB_WORKSPACE /github /__w/_temp
- name: Get repository's content
uses: actions/download-artifact@v4
with:
name: repo
- uses: actions/cache@v4
with:
path: vendor/bundle
key: bundle-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
bundle-${{ hashFiles('**/Gemfile.lock') }}
bundle-
- name: Rubocop
run: bundle exec rubocop --fail-level E
test:
needs:
- bundle_install
- yarn_build
runs-on: ubuntu-24.04
container: cimg/ruby:3.1.2-browsers
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
INCLUDE_FIXTURES: true
DATABASE_URL: postgresql://root@postgres:5432/circle_test
FAIL_SCREEN: 1
BUILT_ASSETS_AVAILABLE: 1
services:
postgres:
image: cimg/postgres:14.5
env:
POSTGRES_USER: root
POSTGRES_DB: circle_test
# Add a health check
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Setup file system permissions
run: sudo chmod -R 777 $GITHUB_WORKSPACE /github /__w/_temp
- name: Get repository's content
uses: actions/download-artifact@v4
with:
name: repo
- uses: actions/cache@v4
with:
path: vendor/bundle
key: bundle-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
bundle-${{ hashFiles('**/Gemfile.lock') }}
bundle-
- uses: actions/cache@v4
with:
path: public/front
key: v1-assets-cache-${{ github.ref }}-${{ github.sha }}
restore-keys: |
v1-assets-cache-${{ github.ref }}-${{ github.sha }}
v1-assets-cache-${{ github.ref }}
v1-assets-cache
- name: Setup database
run: bundle exec rails db:test:prepare
- name: RSpec system tests
run: bundle exec rspec
The end.
I hope you found this article useful, and be sure to keep reading our blog!
…okay, okay, let’s actually see what we did.
CircleCI and GitHub Actions: differences and similarities
We’ll go through our new GitHub Actions configuration and try to suss out what is similar to CircleCI and what is different. Let’s move from top to bottom, and gradually, we’ll deal with everything as we encounter it.
Workflow → Job → Step
In CircleCI, the order in which jobs are executed and the dependencies between them are described in the workflows
block. There is no such block in GitHub Actions. Instead, each job has a needs field that accepts a list of other job names that must complete successfully before the current job can begin executing.
Due to the lack of a workflow
block in GitHub Actions, the situation is handled differently when you want to run a specific job
when you commit to a specific branch.
So, if in CircleCI, you have something like this:
workflows:
version: 2
build-and-deploy:
jobs:
- . . .
- bundle_audit:
requires:
- bundle_install
filters:
branches:
only: audit
- . . .
Then, in GitHub Actions, you’ll have to use the if
block at the “jobs” level:
on:
push:
branches:
- main
jobs:
# . . .
bundle_audit:
runs-on: ubuntu-24.04
needs: bundle_install
if: github.ref == 'refs/heads/audit'
steps:
# . . .
# . . .
# . . .
persist_to_workspace
vs. upload-artifact
Let’s see how the checkout_code
job has changed.
The closest equivalent for the persist_to_workspace
and attach_workspace
pair from the GitHub Actions world are the upload-artifact
and download-artifact
actions.
Note that the GitHub Action does not use a special checkout step to download code from the repository. Instead, the checkout action is responsible for getting the code, which is no different from other actions you can find in the marketplace.
The container
and env
fields
Now let’s take a look at the bundle_install
job.
As I said before, in GitHub Actions each step
can be a shell command or an action. An action itself can be a JavaScript program or a Docker container. Both shell commands and actions of all kinds are executed directly on the runner (if the job
description does not have the container field). If present, all steps
are executed inside the container that has been created from the specified image.
jobs:
job_name_here:
steps:
# shell command
- name: This step will run by shell on runner OS
run: echo "Hotel Menetekel"
# JavaScript action
- name: This step will run JavaScript code on runner OS
uses: actions/hello-world-javascript-action@main
# Docker container action
- name: This step will run separate container
uses: actions/hello-world-docker-action@main
jobs:
job_name_here:
container: ruby:3.1
steps:
# shell command
- name: This step will run by shell inside ruby container
run: echo "Hotel Menetekel"
# JavaScript action
- name: This step will run JavaScript code inside ruby container
uses: actions/hello-world-javascript-action@main
# Docker container action
- name: This step will still run separate container, yes
uses: uses: actions/hello-world-docker-action@main
An action can be a Docker image and run as Docker container. The question immediately arises: what will happen to actions of that kind? Will it be docker-in-docker
? As you can see from above example, no. Actually the container actions will be run as sibling containers on the same network with the same volume mounts.
The env block specifies the environment variables that will be passed to each step
. These variables are the same for all jobs except for checkout_code
and test
. It would be convenient to use YAML anchors to avoid repetition, but unfortunately, they’re still not supported in Github Actions.
Now, let’s take an individual look at some of the steps
.
The CircleCI Docker images nuisance
- name: Setup file system permissions
run: sudo chmod -R 777 $GITHUB_WORKSPACE /github /__w/_temp
This step gives full access to the $GITHUB_WORKSPACE
, /github
and /__w/_temp
directories for any user. This is a side effect of using CircleCI images. Their Dockerfiles use the USER circleci
statement, so the user in the container does not have sufficient rights to access these directories.
To exclude this step
, you need to use Docker images without the USER
statement. We use these images because we want to replicate our CircleCI config as the first step to our new GitHub Actions config.
Caching in GitHub Actions
- uses: actions/cache@v4
with:
path: vendor/bundle
key: bundle-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
bundle-${{ hashFiles('**/Gemfile.lock') }}
bundle-
As with checkout
, caching in GitHub Actions is performed by an action, not by the reserved save_cache
and restore_cache
. actions/cache@v4
performs the save and restore in one step. If a cache with the required key is found, then it is connected and used in the current step. If a cache is not found, it will be created and saved in case of successful completion of the job (not the current step, but the entire job).
Just like in CircleCI, you can specify a list of keys by which the cache can be retrieved if no exact match is found.
services
Block
Well, all interesting parts of the bundle_install
job have now been examined. It makes no sense to focus on the yarn_install
, yarn_build
and rubocop
jobs, because they’re almost the same as bundle_install
.
Let’s move on to the test
job, which is slightly different from what we’ve seen before:
test:
# . . .
container: cimg/ruby:3.1.2-browsers
env:
# . . .
DATABASE_URL: postgresql://root@postgres:5432/circle_test
services:
postgres:
image: cimg/postgres:14.5
env:
POSTGRES_USER: root
POSTGRES_DB: circle_test
# Add a health check
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
In CircleCI, additional containers within the same job can be listed in an executor
declaration. All the steps described in the job are performed in the first of the listed containers and the remaining containers are connected to the same network and are available through localhost
.
GitHub Actions uses the container and services fields for this purpose. The first specifies the main container and the second lists additional containers that will be needed during the execution of the job. For example, in our case it is a database.
Note that the value of the DATABASE_URL
variable in the env
block has changed from postgresql://root@localhost:5432/circle_test
to postgresql://root@postgres:5432/circle_test
. Unlike CircleCI, in GitHub Actions, additional containers are not available through localhost
, but by their names, which are specified in the services
block when they are created.
Testing the new Github Action configuration
Well, we’ve finished discussing the configuration for GitHub Actions! Let’s check what came out of it. We’ll save the contents to the file asis.yml
and put it in the root of the repository along the path .github/workflows/asis.yml
. Next, we’ll invoke the most famous spell from The Standard Book of Spells, which goes something like this: git add - git commit - git push
. We then go to the Actions tab in the web interface of the GitHub repository.
Not the best result! The entire workflow
takes as much as 10 minutes, whereas on CircleCI, it only takes about 4. Let’s try to figure out the stage where we are losing so much time. We’ll take the job bundle_install
as an example.
You can notice (and this is also visible in all other jobs) that the Get repository's content
step, during which the artifact with the contents of the repository is downloaded, takes the longest time. Apparently, the upload-artifact - download-artifact
action pair is not a good replacement for the persist_to_workspace - attach_workspace
pair from CircleCI.
Let’s try to replace this step in all jobs with actions/checkout@v4
. If you think about it, checkout
in GitHub Actions should be faster than its CircleCI counterpart checkout_code
because both the runner and the server that hosts the repository are likely to be close to each other from a network point of view, and it’s likely that the throughput between them will be very high.
Let’s test this theory and see the result.
Well, this is a good result!
We got rid of the checkout_code
job, and we added a first step which downloads the contents of the repository using actions/checkout@v4
to each job, thus reducing the time to 5 minutes.
Can we go even further and speed up the whole process even more?
No more containers
Let’s take a look at the execution statistics for each individual job again.
Have you noticed which step is repeated in all jobs and takes much more time than the rest? Initialize containers
takes 30-50% of the entire job execution time! Most of that time is spent downloading huge Docker images on runner. For example, the compressed image cimg/ruby:3.1.2-node
is 592.69 MB and needs to be downloaded to complete almost every job in our workflow.
A natural question emerges here: is it really necessary to perform all actions inside containers? Is there another way to get an almost equally repeatable environment, access to the right utilities at the execution stage of each individual step, while at the same time reducing the overall preparation time?
There is such a way, and I think it will come as no surprise that all of this can be done using actions!
There are many actions, like setup-name-of-language-here
, suitable for every taste: Ruby, Java, Go, Node.js, Python and so on.
All of them perform the same function: preparing the environment for the corresponding language. Instead of downloading a huge Ruby container, you can use the action ruby/setup-ruby@v1
and it will install the desired version in seconds, add it to $PATH
, run bundle install
and set up gem caching!
We only need a few lines of configuration:
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.2
bundler-cache: true
Similarly, we add everything necessary for Node.js:
- uses: actions/setup-node@v4
with:
node-version: '16'
cache: 'yarn'
After replacing all containers with the corresponding actions, the resulting workflow looks like this:
---
name: Release workflow for Kuvaq
on:
push:
branches:
- main
jobs:
bundle_install:
runs-on: ubuntu-24.04
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.2
bundler-cache: true
yarn_install:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16'
cache: 'yarn'
- name: Yarn Install
run: yarn install --verbose
yarn_build:
runs-on: ubuntu-24.04
needs: yarn_install
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16'
cache: 'yarn'
- uses: actions/cache@v4
with:
path: public/front
key: v1-assets-cache-${{ github.ref }}-${{ github.sha }}
restore-keys: |
v1-assets-cache-${{ github.ref }}-${{ github.sha }}
v1-assets-cache-${{ github.ref }}
v1-assets-cache
- name: Yarn Build
run: yarn build --verbose
rubocop:
needs: bundle_install
runs-on: ubuntu-24.04
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.2
bundler-cache: true
- name: Rubocop
run: bundle exec rubocop --fail-level E
test:
needs:
- bundle_install
- yarn_build
runs-on: ubuntu-24.04
env:
BUNDLE_PATH: vendor/bundle
GEM_HOME: vendor/bundle
BUNDLE_JOBS: 3
BUNDLE_RETRY: 3
RAILS_ENV: test
INCLUDE_FIXTURES: true
DATABASE_URL: postgresql://root:postgres@localhost:5432/circle_test
FAIL_SCREEN: 1
BUILT_ASSETS_AVAILABLE: 1
services:
postgres:
image: postgres:14.5
env:
POSTGRES_DB: circle_test
POSTGRES_USER: root
POSTGRES_PASSWORD: postgres
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: 3.1.2
bundler-cache: true
# we need this step for restoring of cache
- uses: actions/setup-node@v4
with:
node-version: '16'
cache: 'yarn'
- uses: actions/cache@v4
with:
path: public/front
key: v1-assets-cache-${{ github.ref }}-${{ github.sha }}
restore-keys: |
v1-assets-cache-${{ github.ref }}-${{ github.sha }}
v1-assets-cache-${{ github.ref }}
v1-assets-cache
- name: Setup database
run: bundle exec rails db:test:prepare
- name: RSpec system tests
run: bundle exec rspec
With that, let’s see if we managed to reduce the time of the entire workflow:
Now, this is a really good result! We’ve not only managed to reach the CircleCI time, but we’ve also significantly surpassed it!
Of course, this result is a consequence of the compromise we made when we abandoned containers. On one hand, containers allow you to achieve close to 100% environment repeatability, but at the same time, they greatly slow the entire CI process.
Sure, using actions doesn’t guarantee complete repeatability (you have no control over the state of the runner’s operating system and the packages installed there), but in my opinion, there are many situations where this level of repeatability is enough.
Take a close look at your CircleCI configuration when you transfer it to GitHub Actions—perhaps many steps do not require containers and can be replaced by existing actions. Doing this, as we saw from our example, can significantly reduce the total time of the entire workflow. And this means speeding up the development process and reducing your monthly bill for GitHub Actions!
Bonus: Docker layer caching
As a bonus for those who have read this article to the end, I offer a small addition. It’s highly likely that at the end of the workflow, you’ll want to build a Docker image with your application. It’s even more likely that you’ll want to do this on every commit, and most importantly, as quickly as possible.
A year ago, our blog already had an article about caching when building docker images in GitHub Actions, but since then, everything has become much easier.
Let’s take a look at a job that builds and pushes the image, and then we’ll deal with the most interesting parts:
build-and-push:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Login to GAR
uses: docker/login-action@v3
with:
registry: us-docker.pkg.dev
username: _json_key
password: ${{ secrets.GA_GCP_JSON_KEY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile.k8s
build-args: |
RUBY_VERSION=3.1.2
PG_MAJOR=14
NODE_MAJOR=16
BUNDLER_VERSION=2.3.21
RAILS_ENV=development
NODE_ENV=development
push: true
tags: us-docker.pkg.dev/deponia/portafisco/kuvaq:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Save Google credentials to file
run: echo "${GOOGLE_APPLICATION_CREDENTIALS}" > "${GITHUB_WORKSPACE}/ga_gcp_json_key.json"
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GA_GCP_JSON_KEY }}
- name: Clean Google Artifact Registry
uses: docker://us-docker.pkg.dev/gcr-cleaner/gcr-cleaner/gcr-cleaner-cli@sha256:959419dd9402edd27f4116d95bb21a9ed6024adec8ea777588a2031a80b91df2
with:
args: >
-repo=us-docker.pkg.dev/deponia/portafisco/kuvaq
-grace=2160h
-keep=30
-allow-tagged=true
-dry-run=false
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ github.workspace }}/ga_gcp_json_key.json
As you can see in the Login to GAR
step, in our case, we’re using the Google Artifact Registry as a place to store our images, but many other repositories are also supported.
The next step is preparing the Docker Buildx, but the most interesting part happens in the Build and push
step.
Previously, caching required type=local
and a separate step to save and restore the cache from the temporary directory, today however, a simple type=gha
in the cache-from
and cache-to
fields is enough!
Although this type of cache is still in the experimental stage, it works great and has never caused problems. If you want to get more confidence with this, you can find the type of cache that suits your needs, as well as other options here.
Pay attention to the last step. Here, the uses
field does not specify an action, but a usual Docker image, to which arguments and environment variables are passed. This entire step is a big replacement for the docker run ...
command. It uses the gcr-cleaner program, which removes old images that do not meet the conditions listed in the args
field. This is a very handy way of controlling the size of a docker repository and keeping it within reasonable limits.
Well, that just about wraps things up. The journey from CircleCI to GitHub Actions was quite a ride, but hopefully you found it to be relatively turbulence free! And one more thing: if you have a problem or project in need: frontend, backend, SRE services, mobile, blockchain, or ML—Evil Martians are ready to help! Give us a shout!