GitHub Actions

Overview

GitHub Actions is a continuous integration service launched by GitHub. Workflows can be created to build and test each pull request for a repository, or to deploy merged pull requests to a production environment. GitHub Actions goes beyond DevOps and allows you to run workflows when other events occur in the repository. For example, you can run workflows to automatically add the appropriate tags when someone creates a new issue in your repository. GitHub provides Linux, Windows, and macOS virtual machines to run workflows, or you can host your own self-hosted runner in your own data center or cloud infrastructure.

What can it do?

GitHub Actions supports most of the languages you know, and you can use it for code checking, testing, CI/CD, project management, and even running Kubernetes.

If we want to search for some common actions, we generally think of awesome actions, and you can also search at GitHub Market to search.

Core Concepts

3 main points: workflow, operations, and steps. For the rest, please look up documentation on your own. But then again, it’s enough to understand these 3 concepts. Once you know how it works, you can think about what steps you need to do each thing in your head, and then search for ready-made steps you can use, but of course, there’s no solution, so you have to create your own.

First, let’s talk about the workflow. If you want to do something on GitHub, you have to have a repository, which is the most basic requirement. Second, as you may have noticed before, many repositories have a .github/workflows folder, which is a requirement for GitHub Actions. Under that folder, you’ll find YAML files, which are actually the yaml files for a workflow.

For example, if I create a new github-actions-demo.yml file, I’m creating a workflow named github-actions-demo. Of course, the name can be configured in the yaml file.

github-actions

Here’s an image I copied from the documentation, based on which we can understand the 3 concepts. A “workflow” is an automated process that can be configured to run when triggered by an event defined in the YAML file, or can be triggered manually or at regular intervals. We can see that a workflow can contain multiple “jobs”, i.e. jobs, for example you can define job1, job2 … In each job, there can also be multiple “steps”, which can be custom scripts, other operations, and so on.

A new concept is mentioned here, called “events”. It can be understood as “event-triggered process”. For example, you can define someone to create a pr, open an issue, push it to the repository, or run it on a timer to trigger a workflow to run.

Here is an easily misunderstood point, the official diagram looks like each job is run sequentially, but they can also be run in parallel oh~😊

Theoretically, within the limits of GitHub Actions, a single workflow can be run all the time, and that makes it much more playable! But it’s not recommended for other uses (someone once used this for mining), after all, it’s a free resource, so you can’t abuse it.

Spring Boot

Before Spring Cloud, we need to talk about Spring Boot, on which it is also based after all. Let’s use the Pisces-Lfs project, which is based on Spring Boot, as an example. Since we are building a Docker-based image, we have to write the Dockerfile file first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Base images that the image needs to depend on
FROM openjdk:8-jdk-alpine
# Set environment variables
ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m" SPRING_CONFIG="-Dspring.config.location=/root/lfs/application-docker.yml"
# Set time zone
RUN set -eux; \
    ln -snf /usr/share/zoneinfo/$TZ /etc/localtime; \
    echo $TZ > /etc/timezone
# Create Folder
RUN mkdir -p /root/lfs
# Copy the jar and rename it
COPY lfs-admin/target/lfs-admin-1.0.jar /lfs-admin.jar
# When the docker container starts, run the jar 
ENTRYPOINT exec java ${JAVA_OPTS} -jar ${SPRING_CONFIG} /lfs-admin.jar
# The name of the maintainer
MAINTAINER besscroft

Then we create the docker-buildx.yml file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# Workflow Name
name: "Java CI with Multi-arch Docker Image"

# Event triggering conditions and branching
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

# a job
jobs:
  docker:
    name: Running Compile Java Multi-arch Docker Image
    # Based on the ubuntu-latest runtime
    runs-on: ubuntu-latest
    steps:
      # Setting up the Java environment
      - uses: actions/checkout@v3
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          # jdk version
          java-version: '8'
          # Note, the adopt here is because I need to use this, you can change to other distributions according to your situation
          distribution: 'adopt'
          cache: 'maven'
      - name: Build with Maven
        # maven build
        run: mvn -B package -Dmaven.test.skip=true --file pom.xml
      - name: Login to Docker Hub
        # Login to the docker repository
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Set up QEMU
        # Set up QEMU to support multi-platform builds
        uses: docker/setup-qemu-action@v1
      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v1
      - name: Build and push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          context: ./
          # Dockerfile file location
          file: ./Dockerfile
          # Supported image architectures, since I need an aarch64 image, so I added arm64 here
          platforms: linux/amd64,linux/arm64
          push: true
          # image tags
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/lfs:latest

In the workflow, there are places where environment variables are used, i.e. user name and token, which cannot be placed directly in the file, otherwise it would be leaked. Generally, we store them in Actions secrets and read them using the environment variable configuration, as shown in the following figure.

Actions secrets

Spring Cloud

After understanding how Spring Boot was built automatically through github actions in the previous section, Spring Cloud is actually quite simple. I’ll use my own Pisces-Cloud project as an example. A submodule under a microservice, assuming it’s all in one project space, usually only requires one build to get all the services built. Then the problem is much better.

Again, we create a Dockerfile file for each service, and then a new docker-buildx.yml file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
name: "Java CI with Multi-arch Docker Image"

on:
  push:
    branches: [ main ]

jobs:
  docker:
    name: Running Compile Java Multi-arch Docker Image=
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          java-version: '8'
          distribution: 'adopt'
          cache: 'maven'
      - name: Build with Maven
        run: mvn -B package -Dmaven.test.skip=true --file pom.xml
      - name: Login to Docker Hub
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v1
      - name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v1
      # ------------------------
      # Start from here
      # ------------------------
      - name: Build and push admin
        id: docker_build_admin
        uses: docker/build-push-action@v2
        with:
          context: ./
          file: ./pisces-admin/admin-boot/Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/pisces-admin:latest
      - name: Build and push auth
        id: docker_build_auth
        uses: docker/build-push-action@v2
        with:
          context: ./
          file: ./pisces-auth/Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/pisces-auth:latest
      - name: Build and push gateway
        id: docker_build_gateway
        uses: docker/build-push-action@v2
        with:
          context: ./
          file: ./pisces-gateway/Dockerfile
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ secrets.DOCKERHUB_USERNAME }}/pisces-gateway:latest

Notice the difference? In fact, there are only more “steps”, that is, each Dockerfile creates a separate “step” to package the image, and you just need to put all these steps after the maven build.

Some suggestions

I only provide a container build solution for both x86 and arm platforms, the rest of the functionality is actually implemented by Docker itself.

1
2
3
4
5
docker run -d --name pisces-gateway \
  -p 8000:8000 \
  -e JAVA_OPTS="-Xms512m -Xmx512m -Duser.timezone=GMT+08 -Dfile.encoding=UTF8" \
  -e SPRING_CONFIG="--spring.profiles.active=prod --spring.cloud.nacos.discovery.server-addr=http://127.0.0.1:8848" \
  besscroft/pisces-gateway:latest

For example, here the Docker container sets JVM parameters at startup, and configuration parameters.

For release images, a more standardized way to play in the open source community is to create release.yml files that trigger events and perform workflows by tagging branches.

Reference https://blog.besscroft.com/articles/2022/spring-cloud-on-github-actions/