Skip to content

깃헙 액션(GitHub Actions) 사용법 알아보기. CI 파이프라인 구축 예시. 깃헙 액션을 이용한 슬랙 알림 전송. JaCoCo를 이용한 코드 커버리지 추가.

Notifications You must be signed in to change notification settings

seungki1011/CICD-using-Github-Actions

Repository files navigation

블로그 참고

해당 내용은 https://seungki1011.github.io/posts/cicd-github-action/에도 포스팅 했습니다.



📌 목차

  1. 🌿 브랜치 설명
  2. 🗣️ GitHub Actions 소개
  3. 🎯 GitHub Actions 주요 개념
  4. 🚀 GitHub Actions 사용법
  5. 🗃️ 캐싱(Caching)
  6. ❌ 테스트 실패 살펴보기
  7. ☑️ 조건문 사용하기
  8. 💬 슬랙으로 알림 설정하기(Secrets 사용)
  9. 📊 JaCoCo를 이용한 코드 커버리지 추가


🌿 브랜치 설명

  • main: 조건문을 적용하지 않은 워크플로우
  • dev: main과 동일
  • test: main과 동일. 해당 브랜치에 PR을 넣으면 워크플로우가 트리거됩니다.
  • using-if: 조건문을 사용한 워크플로우



🗣️ GitHub Actions 소개

GitHub Actions는 GitHub에서 제공하는 자동화 도구로, CI/CD(지속적 통합 및 지속적 배포) 파이프라인과 같은 자동화 워크플로우를 구성할 수 있습니다. GitHub Actions는 GitHub 리포지토리와 통합되어 있어, 개발자들이 PushPull Request와 같은 이벤트에 따라 자동으로 워크플로우를 실행하고, 소프트웨어 빌드, 테스트, 배포 등의 과정을 자동화할 수 있습니다.


CI/CD외의 작업에도 사용할 수 있습니다

위에서도 설명했듯이 깃헙 이벤트와 YAML 기반의 자동화된 워크플로우를 구현하기 위해서 사용되는 도구이다. CI/CD외에도 여러가지 용도로 활용할 수 있습니다.

  • 예시
    • 스케쥴링 작업: schedule 이벤트를 활용한 주기적인 작업 설정
    • 프로젝트 관리: 지라와 같은 이슈 트래킹 도구와 통합한 워크플로우 생성

다음은 GitHub Actions가 사용될 수 있는 몇 가지 사용 케이스입니다.

  • 깃헙 태그, 릴리즈 자동 설정
  • 새로운 브랜치 생성시 특정 작업 실행
  • CI/CD 파이프라인 구현
  • 일정 주기 또는 특정 조건에 따른 문서화 작업

이외에도 다양한 사용 케이스가 존재합니다.

Github Actions 문서: https://docs.github.com/ko/actions



🎯 GitHub Actions 주요 개념

  1. 워크플로우(Workflow)
    • Github Actions에서 자동화 작업의 정의를 담고 있는 YAML 파일
    • .github/workflows/ 디렉토리에 저장해서 사용합니다
    • Enviroment
      • 워크플로우가 배포되는 대상을 관리하는 개념으로, 개발, 스테이징, 프로덕션 환경에 대해 서로 다른 설정과 시나리오를 적용 가능
      • 환경별로 배포 승인 절차를 추가하거나, 특정 Secret를 설정 가능
    • Event
      • 워크플로우가 실행되는 트리거를 의미
      • push, pull_request, schedule과 같은 다양한 이벤트가 존재
    • Jobs
      • 하나의 워크플로우는 여러 개의 Job으로 구성
      • Job은 병렬로 실행 가능
        • 예를 들어, 하나의 워크플로우에서 빌드, 테스트, 배포가 각각 다른 Job으로 병렬 수행 가능
      • 서로 다른 Job에 대한 의존 관계에 대해 순차 실행 가능
    • Step
      • Job은 여러 Step로 이루어지며, 이 Step들은 개별적으로 실행되는 스크립트(명령어) 또는 액션
      • 쉽게 말해서 Job에서 실행되는 개별 작업이 Step
  2. 액션(Action)
    • 워크플로우 내에서 실행되는 작업 단위
    • 액션은 **재사용 가능한 코드 조각(컴포넌트)**으로, 특정 프로그래밍 언어의 테스트를 실행하거나, 클라우드에 애플리케이션을 배포하는 등의 작업을 처리 가능
    • Github Marketplace에서 사전 정의된 다양한 액션들을 찾을 수 있습니다
  3. 러너(Runner)
    • 워크플로우가 실행될 때, GitHub Actions는 해당 워크플로우를 러너라는 가상 머신에서 실행한다
    • 러너는 GitHub에서 제공하는 호스티드 러너 또는 자체 호스팅 러너가 존재
      • 호스티드 러너(GitHub Hosted Runner): GitHub에서 제공하는 기본적인 러너로, 일반적인 OS(예: Ubuntu, Windows, macOS)를 지원하며, 각 워크플로우 실행 시 새롭게 생성되는 가상 머신
      • 자체 호스팅 러너(Self-Hosted Runner): 사용자가 직접 러너를 설정하여 사용할 수 있으며, 더 많은 제어권을 필요로 할 때 유용
        • 자체 서버나 IDC에서 실행 가능
  4. 시크릿(Secrets)
    • API 키나 비밀번호 같은 민감한 정보를 보관하는 기능
    • 워크플로우에서 Secrets로 저장된 값은 보안된 방식으로 사용되며, 리포지토리, 환경, 조직 등 다양한 수준에서 설정 가능


🚀 GitHub Actions 사용법

대략적인 과정

Github Action으로 워크플로우를 정의해서 사용하기 위해서는 크게 다음의 단계를 거칩니다.

  1. YAML 파일 작성

    • 사용할 워크플로우를 .github/workflows/ 디렉토리에 YAML 파일로 작성합니다
    • 다음의 순서의 내용을 해당 파일에 작성합니다.
  2. Triggers(Event)

    • 워크플로우가 실행되는 이벤트를 정합니다

    • 예: push, pull_request

  3. Jobs

    • 워크플로우는 여러개의 Job으로 나눌 수 있습니다
    • 각 Job은 병렬, 순차, 의존성에 의한 순차에 따라 실행될 수 있습니다
    • 각 Job은 서로 독립된 러너(Runner) 환경에서 동작하기 때문에, 어떤 환경(운영체제)에서 실행할지 명시해야 합니다
  4. Steps

    • Job안에서 Step을 정의하여 각 단계를 실행합니다.
    • 각 Step은 run을 사용한 명령어를 실행하거나, uses를 사용한 액션이 될 수 있습니다
  5. Secrets

    • 필요한 경우 민감한 정보(SSH, API 키)는 Github Secrets에 저장해서, 안전하게 관리합니다

실제 예시를 통해 알아봅시다.

현재 자바17를 사용한 프로젝트를 진행중이고 다음과 같은 CI 파이프라인을 구현한다고 가정해봅시다.

  • test 브랜치로 PR 요청을 하는 경우 트리거 됩니다
  • buildtest라는 2 개의 Job으로 구성됩니다. 이때 testbuild가 성공적으로 완료되어야 수행됩니다
  • 각 Job의 목적은 다음과 같습니다
    • build: Gradle 빌드
    • test: Gradle 테스트
  • build 또는 test실패하면 CI 파이프라인도 실패하고 PR을 수행하지 못합니다


예시

워크플로우를 정의해서 사용하기 위해서 .github/workflows/ 아래에 ci.yml이라는 파일을 만들어서 사용합니다.

ci.yml

# 워크플로우의 이름을 지정합니다
# 대시보드에서 워크플로우를 구분할 때 사용됩니다
name: CI Pipeline

# Event
on:
  pull_request: # Pull Request가 test 브랜치로 들어올 때 실행
    branches:
      - test

# Jobs: 필요한 Job을 병렬 또는 (의존성에 의해)순차적으로 실행
jobs:
  # Build Job
  build:
    runs-on: ubuntu-latest

    steps:
      # 1. 리포지토리에서 소스 코드를 체크아웃(복제)
      - name: Checkout code
        uses: actions/checkout@v3

      # 2. Gradle 라이브러리 캐싱
      - name: Cache Gradle packages
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      # 3. Java 17 설치
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
          
      # 4. gradlew에 권한 부여
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # 5. 의존성 설치 및 빌드
      - name: Build with Gradle
        run: ./gradlew build --no-daemon

  # Test Job
  # 빌드에서 테스트를 진행하기 때문에 해당 Job은 제외해도 됩니다. 그러나 needs를 설명하기 위해 편의상 추가했습니다.
  test:
    runs-on: ubuntu-latest
    needs: build  # build Job이 성공해야 실행
    
    steps:
      # 기존 체크아웃, JDK 설치 과정
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: '17'
          
      # 6. 테스트 실행
      - name: Run tests
        run: ./gradlew test --no-daemon
  • Event

    • on: # 워크플로우가 실행될 조건을 정의합니다
        pull_request: # PR을 조건으로 설정합니다
          branches:
            - test # test 브랜치로 PR이 열리면 실행되도록 지정
  • Jobs

    • jobs: # 아래 계층에 수행할 Job을 정의합니다
        # (1)build라는 Job
        build:
          runs-on: ubuntu-latest # 해당 Job의 러너 환경을 지정합니다
          # 아래에 build의 Step들을 정의합니다
          step:
            # 아래 계층에 수행할 Step을 정의합니다
          	# Step 체크아웃, JDK 설정, 캐싱, 권한, 빌드...
          	
        # (2)test라는 Job
        test:
          runs-on: ubuntu-latest
          needs: build  # build가 성공적으로 완료되어야 수행된다는 의미
    • runs-on: Job이 실행될 환경을 지정합니다. 위 예시의 경우 ubuntu-latest를 통해 최신 버전의 Ubuntu 환경에서 동작하도록 합니다. Job들은 서로 별개의 환경을 사용합니다.

    • needs: Job의 의존성을 지정합니다

    • 의존하는 Job을 명시적으로 지정하지 않는 경우 병렬로 수행됩니다

  • Steps

    • # 하나의 Job안에 정의된 Step들은 기본적으로 순차적으로 실행됩니다
      steps:
            # 1. 리포지토리에서 소스 코드를 체크아웃(복제)
            - name: Checkout code # name으로 각 Step의 이름을 부여합니다
              uses: actions/checkout@v3 # uses를 통해 액션을 호출합니다
      
            # 2. Gradle 라이브러리 캐싱
            - name: Cache Gradle packages
              uses: actions/cache@v3
              with: # with를 통해 액션에 대한 추가적인 설정을 지정할 수 있습니다
                path: |
                  ~/.gradle/caches
                  ~/.gradle/wrapper
                key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
                restore-keys: |
                  ${{ runner.os }}-gradle-
      
            # 3. Java 17 설치(JDK 설치)
            - name: Set up JDK 17
              uses: actions/setup-java@v3
              with:
                distribution: 'temurin'
                java-version: '17'
                
            # 4. gradlew에 권한 부여
            - name: Grant execute permission for gradlew
              run: chmod +x gradlew # run으로 직접 명령어를 실행할 수 있습니다
      
            # 5. 의존성 설치 및 빌드
            - name: Build with Gradle
              run: ./gradlew build --no-daemon
    • steps: 아래 계층에 순차적으로 수행할 Step들을 명시합니다

    • name: Step에 부여할 이름입니다. GitHub Action 로그에 표시됩니다.

    • uses: 액션(Action)을 호출하기 위해서 사용합니다

    • run: 직접 명령어를 실행하기 위해서 사용합니다

    • id: 예시에서 사용하지 않았지만 id를 부여해서 다른 Stepid를 사용해서 해당 idStep의 출력값이 상태를 참조해서 사용할 수 있습니다.



uses 필드

uses 필드를 자세히 살펴보자.

uses 필드는 GitHub Actions의 워크플로우 파일에서 다른 사람이 작성한 액션(Action)을 호출할 때 사용되는 필드입니다. 즉, uses 필드를 통해 GitHub Marketplace에 등록된 액션이나 공용 리포지토리에서 제공하는 액션을 가져와서 사용할 수 있습니다.

다음과 같은 구조를 사용합니다.

uses: {repository}@{version, branch, commit}
  • repository: 액션이 위치한 리포지토리의 경로를 지정합니다. 예를 들어 actions/checkoutactions라는 사용자 또는 조직이 만든 checkout이라는 리포지토리를 의미합니다.
  • version, branch, commit: 액션의 특정 버전(@v2), 브랜치(@main), 또는 커밋 해시(@a1b2c3d4)를 지정할 수 있습니다

정리하자면 uses 필드를 통해서 특정 액션을 재사용하고 해당 액션에 대한 버전 지정을 할 수 있습니다.


액션 커스터마이징

커스터마이즈하고 싶은 액션의 레포지토리를 포크하고, 커스텀 후에 기존 사용자 또는 조직 대신 내 깃헙 아이디를 사용하면 커스텀한 액션을 사용할 수 있습니다.

  • 예: slackapi/slack-github-action을 포크하고 필요한 부분을 커스터마이징 후에 myrespository/slack-github-action 같은 형태로 사용이 가능합니다


run 필드

이번에는 run 필드를 자세히 살펴봅시다.

run 필드는 GitHub Actions의 워크플로우에서 직접 명령어를 실행할 때 사용됩니다. 주로 쉘 스크립트나 단일 명령어를 실행하는 데 사용되며, CI/CD 파이프라인의 다양한 단계에서 커스텀 작업을 수행할 수 있게 해줍니다. run 필드를 통한 명령어를 사용해서 빌드 도구를 사용하여 애플리케이션을 빌드하거나 테스트 스크립트를 실행할 수 있습니다.


여러 명령어를 한번에 실행하는 것도 가능합니다.

# 한번에 여러 명령어를 사용하는 예시
- name: Install dependencies and build
  run: |
    npm install
    npm run build


🗃️ 캐싱(Caching)

CI 파이프라인에서 라이브러리를 캐싱하는 것은 빌드 시간을 단축하고 리소스 사용량을 줄이는 데 효과적입니다. 특히, Gradle과 같은 빌드 도구를 사용하는 경우 의존성 캐싱을 통해 매번 라이브러리를 다시 다운로드하지 않도록 할 수 있습니다.

캐싱은 캐시 액션인 actions/cache를 통해서 설정할 수 있습니다.


- name: Cache Gradle packages
  uses: actions/cache@v3 # GitHub에서 제공하는 액션인 cache@v3 사용
  with:
    path: | # 캐시 경로 지정
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
    restore-keys: |
      ${{ runner.os }}-gradle-
  • path: 캐싱할 경로를 지정합니다
    • ~/.gradle/caches: Gradle이 다운로드한 의존성(라이브러리)이 저장되는 경로
    • ~/.gradle/wrapper: Gradle 래퍼의 캐시 경로

key와 캐시 무효화에 대해서 알아봅시다.

key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
  • key는 다음의 역할을 가집니다

    • 캐시 식별: key는 저장된 캐시를 식별하고 검색하는 데 사용됩니다. 동일한 key를 가진 캐시가 존재하면, 해당 캐시가 복원됩니다.
    • 캐시 업데이트: 캐시를 저장할 때 key를 사용하여 기존 캐시와 구분합니다. key가 변경되면 새로운 캐시가 생성됩니다.
  • 캐시 무효화

    • 프로젝트의 의존성이나 Gradle 설정 파일이 변경될 경우, 이전 캐시가 더 이상 유효하지 않게 됩니다. 예를 들어, build.gradle 파일이나 gradle-wrapper.properties 파일이 수정되면 새로운 캐시가 필요할 수 있습니다.
    • 이 설정에서는 build.gradle, settings.gradle, gradle-wrapper.properties 파일의 해시 값을 기반으로 key를 생성합니다다. 이러한 파일이 변경되면 해시 값이 바뀌고, 캐시 키도 변경되면서 캐시가 무효화됩니다.

restore-keys: |
      ${{ runner.os }}-gradle-
  • restore-keys

    • key정확히 일치하지 않는 경우에도 유사한 키를 가진 캐시를 복원할 수 있습니다
    • key와 비슷한 값을 가진 캐시를 찾기 위한 보조 역할을 한다고 보면 됩니다
  • ${{ runner.os }}-gradle-이라는 prefix 기준으로, 캐시에 저장된 key 중에서 prefix를 포함하는 key를 찾고 가장 최근의 캐시를 복원합니다


정리하자면 캐시 설정을 통해서 다음의 효과를 얻을 수 있습니다.

  • 빌드 시간 단축: 캐시된 의존성을 재사용하여 의존성 다운로드 시간을 줄이고, 전체 빌드 시간을 단축
  • 네트워크 트래픽 절약: 의존성을 매번 다운로드하지 않고, 캐시에서 복원함으로써 네트워크 사용량을 줄임

gradle/gradle-build-action을 사용할 수 있다?

이 액션은 gradle에서 제공하는 액션입니다. Gradle을 사용해서 빌드하는데 도움을 줍니다. 해당 액션을 사용하면 다음의 이점을 얻을 수 있습니다.

  • run을 사용하지 않고 Gradle 관련 설정과 명령어를 더 쉽게 실행할 수 있도록 도와준다
  • 기존의 actions/cache를 사용하지 않고 캐싱 관련 설정을 할 수 있다

간단히 사용법을 알아봅시다.

- name: Build with Gradle
  uses: gradle/gradle-build-action@v2
  with:
	  arguments: build
	  cache-disabled: false 
  • cache-disabled: 캐시를 비활성화 할 수 있는 옵션. 기본값이 false입니다.

- name: Build with Gradle
  uses: gradle/gradle-build-action@v2
  with:
	  arguments: build
	  cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }}
  • cache-read-only: true일 경우 캐시를 읽기만 하고 저장하지 않습니다. false일 경우 캐시 읽기/저장 모두 수행합니다.
    • 위 예시에서는 maindevelop 브랜치일 때만 false로 설정합니다

캐시 액션과 Gradle 빌드 명령어를 직접 사용하지 않고 gradle-build-action으로 한번에 처리할 수 있습니다.



❌ 테스트 실패 살펴보기

테스트가 실패하도록 테스트 코드를 수정하고dev 브랜치에서 test 브랜치로 PR을 진행하면 어떤 일이 일어나는지 확인해봅시다.


cifail1

CI 파이프라인 build 실패

테스트 코드가 실패하니 빌드도 실패하는 것을 확인할 수 있습니다. test Job도 build에 의존하고 있기 때문에 build에서 실패하니 test도 수행되지 못합니다.

로그를 살펴봅시다.


cifail2

통과한 Step과 실패한 Step을 확인 가능

cifail2

테스트 코드가 실패했다는 것을 확인 가능


현재 상황은 devtestPR 요청 중에 CI 파이프라인이 실패하여 PR 통과가 안되는 상황입니다. 이번에는 테스트를 통과하도록 코드를 수정하고, dev 브랜치로 푸시를 넣어봅시다.


cifail2

테스트 코드가 깨지지 않기 때문에 이번엔 성공!



☑️ 조건문 사용하기

조건문을 사용해서 특정 브랜치에 대해서만 실행되는 Job을 정의해봅시다. 다음과 같은 상황이라고 가정합시다.

  • 모든 브랜치에 대해서 PR 요청이 들어가면 하면 build Job 수행
  • dev 브랜치에 푸시가 일어나면 test Job 수행

name: CI Pipeline

on:
  pull_request:
    branches:
      - '**'  # 모든 브랜치에 대해 PR이 생성될 때 실행

  push:
    branches:
      - dev  # dev 브랜치에 푸시될 때 실행

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
			# 기존 Step들
			# 체크아웃, JDK 설치, 캐싱, 권한...

      - name: Build with Gradle
        run: ./gradlew build --no-daemon

  test:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/dev'  # dev 브랜치로 푸시될 때만 실행하는 조건문
    steps:
			# 기존 Step들
			# 체크아웃, JDK 설치...

      - name: Run tests
        run: ./gradlew test --no-daemon
  • if:를 통해서 조건문을 사용할 수 있습니다
  • if: github.ref == 'refs/heads/dev': dev 브랜치로 푸시될 때만 해당 Job이 실행되도록 합니다

위의 케이스 말고도 다음과 같은 케이스에서도 활용 가능합니다.

  • 상태 기반의 조건문
    • success(): 이전 모든 Step이 성공한 경우 참(true)
    • failure(): 이전 Step 중 하나라도 실패한 경우 참(true)
    • always(): 모든 조건에 관계없이 항상 참(true)
    • cancelled(): 워크플로우가 취소된 경우 참(true)
  • 브랜치 기반 조건문
    • github.ref: 참조를 나타내며, 특정 브랜치나 태그에서만 Step을 실행하도록 조건을 설정합니다
    • 예시: github.ref == 'refs/heads/main' (main 브랜치에서만 실행)
  • 환경 변수 또는 시크릿 기반 조건문
    • 환경 변수나 시크릿 값을 비교하여 조건을 설정할 수 있습니다
    • 예시: if: env.MY_ENV_VAR == 'production'
    • 당연히 env에 환경 변수를 정의하거나, 시크릿에 등록해야 합니다
  • 결과 기반 조건문
    • 이전 Job의 결과에 따라 실행할 수 있습니다
    • 예시: needs.job1.result == 'success' (job1이 성공한 경우)


💬 슬랙으로 알림 설정하기(Secrets 사용해보기)

**시크릿(Secrets)**의 사용법에 대해 알아봅시다. CI/CD 파이프라인을 구성하면 대부분 경우 외부 API, 서버 등과 요청/응답을 주고 받는 과정이 포함됩니다. 하지만 API 또는 서버와 통신하기 위해서 YAML 파일에 URL 또는 API 키를 그대로 하드 코딩하는 것은 보안에 민감한 정보를 그대로 노출시키는 것입니다.시크릿은 API 키, URL, 비밀번호와 같은 민감한 정보를 안전하게 보관하는데 사용됩니다.

CI 파이프라인이 실패하거나 성공하면 Slack Notification을 보내는 기능을 추가해보면서 시크릿을 사용해봅시다.


슬랙 채널 추가

먼저 사용할 슬랙 워크스페이스 내에서 알림을 받을 채널을 개설합니다(필요한 경우 워크스페이스 부터 만들어요).


cifail2

슬랙 채널 개설

  • cicd 파이프라인 테스트라는 채널을 추가했습니다


슬랙 앱 생성: Webhook URL 발급

GitHub Action을 이용한 파이프라인의 통과 여부에 대한 결과를 받기 위해서 슬랙 앱을 만들고 Slack Webhook URL을 통해 연동할 것입다. 이 Webhook URL이 우리가 보관할 시크릿이 될 겁니다.

다음 링크에 들어가서 슬랙 앱을 만들 수 있습니다. https://api.slack.com/apps

Create New App을 누르고 From Scratch를 고릅니다.


cifail2

슬랙 앱 생성

  • 앱의 이름과 앱을 사용할 워크스페이스-채널을 선택합니다


시크릿 등록

앱의 생성을 완료했으면, 해당 앱의 설정 페이지에서 Incoming Webhooks를 선택하고 활성화 시킵니다. 그리고 Webhook URL을 복사합니다.


cifail2

Webhook URL 복사


복사한 Webhook URL은 깃헙 레포지토리의 Settings > Secrets and variables > Actions에 들어가서 New repository secret에 추가합니다.


cifail2

Webhook URL을 Secret으로 추가


해당 시크릿은 GitHub Action 워크플로우에서 ${{ secrets.SLACK_WEBHOOK_URL }}과 같은 형태로 사용할 수 있습니다.



워크플로우 작성1: slack-github-action(현재 버그 있음)

이제 기존의 ci.yml에 슬랙 알림을 보내는 작업을 추가해봅시다. slackapi/slack-github-action을 사용할 것입니다.


버그

현재 이 방법은 payload-file-path를 사용하는 경우 필드가 ???로 표시되는 버그가 있습니다!

159#discussion 참고


# 6. 슬랙 채널로 빌드의 성공 또는 실패에 따라 알림 보내기
# 성공시 슬랙 알람 보내기
- name: Notify Slack on Success
  if: success() # 이전 스텝이 모두 성공하면 해당 스텝을 실행하는 조건문
  id: slack-success
  uses: slackapi/[email protected]
  with:
    payload-file-path: "./slack/success-payload-content.json" # 미리 정의된 json 파일에 payload를 정의해서 사용가능
    # payload를 json 형태로 직접 제공해서 사용할 수 있다
    # payload는 payload-file-path 보다 높은 우선순위를 가진다
    # payload: |
    #      {
    #        "key": "value",
    #        "foo": "bar"
    #      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
# 실패시 슬랙 알람 보내기
- name: Notify Slack on Failure
  if: failure() # 이전 스텝 중 하나라도 실패하면 해당 스텝을 실행하는 조건문
  id: slack-failure
  uses: slackapi/[email protected]
  with:
    payload-file-path: "./slack/failure-payload-content.json"
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
  • 파이프라인 성공과 실패를 나누어서 success-payload-content.jsonfailure-payload-content.json를 따로 정의해서 사용하고 있습니다
  • payload 필드를 사용해서 바로 정의해서 사용하는 방법도 존재합니다(현재 이 방법은 버그가 없음)

그러면 success-payload-content.json을 한번 살펴봅시다.

{
  "channel": "C07L4BQTKGE",
  "attachments": [
    {
      "color": "#36a64f",
      "title": "${{ github.repository }}",
      "title_link": "https://github.com/${{github.repository}}",
      "text": "✅ CI/CD 파이프라인 성공",
      "fields": [
        {
          "title": "Repository",
          "value": "${{ github.repository }}",
          "short": true
        },
        {
          "title": "Tag",
          "value": "${{ github.ref_name }}",
          "short": true
        },
        {
          "title": "Commit",
          "value": "${{ github.sha }}",
          "short": true
        },
        {
          "title": "Commit Message",
          "value": "${{ github.event.head_commit.message }}",
          "short": true
        },
        {
          "title": "Author",
          "value": "${{ github.event.head_commit.author.name }}",
          "short": true
        }
      ]
    }
  ]
}
  • 위 케이스에서는 채널 ID를 그대로 노출했지만, 해당 ID도 시크릿에 등록해서 사용할 수 있습니다

이 방법의 경우 아래 처럼 필드가 ???로 랜더링 버그가 있습니다.

(@v1.27.0 기준. Maintainer들이 현재 해결 방법을 찾아보고 있는 것 같습니다. 언제 해결 될지는 😭)


cifail2



워크플로우 작성2: slack-github-action(버그 우회)

버그 우회(Workaround)

이 방법은 버그 해결이 아닌 우회하는 방법입니다.

Problems persist using payload file #203 참고


이슈에서 나온 우회 방법을 그대로 첨부했습니다.

ci.yml
# 추가 1
env:
  PULL_REQUEST_TITLE: ${{ github.event.pull_request.title }}
  PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
  PULL_REQUEST_AUTHOR: ${{ github.event.pull_request.user.login }}
  PULL_REQUEST_EVENT_NAME: ${{ github.event_name }}
  PULL_REQUEST_LOGS_URL: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }}
  PULL_REQUEST_COMMIT_URL: ${{ github.event.pull_request.head.repo.html_url }}/commit/${{ github.event.pull_request.head.sha }}
  
jobs:
  # Build Job
  build:
    runs-on: ubuntu-latest
			
    steps:
    # 기존 Step
    
      # 추가 2
      - name: Slack message with build result
        if: success() || failure()
        uses: slackapi/[email protected]
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
          PULL_REQUEST_JOB_STATUS: ${{ job.status }}
          PULL_REQUEST_JOB_STATUS_COLOR: ${{ job.status == 'success' && vars.COLOR_GREEN || job.status == 'failure' && vars.COLOR_RED || vars.COLOR_ORANGE }}
        with:
          channel-id: C07L4BQTKGE
          payload-file-path: ./slack/ci_slack_message.json
ci_slack_message.json
{
  "attachments": [
    {
      "color": "${{ env.PULL_REQUEST_JOB_STATUS_COLOR }}",
      "fallback": "test",
      "blocks": [
        {
          "type": "section",
          "fields": [
            {
              "type": "mrkdwn",
              "text": "*Pull Request:*\n<${{ env.PULL_REQUEST_URL }}|${{ env.PULL_REQUEST_TITLE }}>"
            },
            {
              "type": "mrkdwn",
              "text": "*Status:*\n`${{ env.PULL_REQUEST_JOB_STATUS }}`"
            }
          ]
        },
        {
          "type": "section",
          "fields": [
            {
              "type": "mrkdwn",
              "text": "*Author:*\n`${{ env.PULL_REQUEST_AUTHOR }}`"
            },
            {
              "type": "mrkdwn",
              "text": "*Event:*\n`${{ env.PULL_REQUEST_EVENT_NAME }}`"
            }
          ]
        },
        {
          "type": "divider"
        },
        {
          "type": "actions",
          "elements": [
            {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "Logs"
              },
              "url": "${{ env.PULL_REQUEST_LOGS_URL }}"
            },
            {
              "type": "button",
              "text": {
                "type": "plain_text",
                "text": "Commit"
              },
              "url": "${{ env.PULL_REQUEST_COMMIT_URL }}"
            }
          ]
        }
      ]
    }
  ]
}

이 경우 다음과 같이 표시됩니다.


cifail2



워크플로우 작성3: action-slack

이번에는 더 간편한 방법인 8398a7/action-slack를 사용하는 방법에 대해 알아봅시다.

아래의 Step만 추가하면 됩니다.


- name: Slack message with build result
  if: always() # 이전의 Step들의 성공과 실패 여부에 상관 없이 항상 실행
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
    if_mention: failure,cancelled
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

다음과 같은 슬랙 알람을 받을 수 있습니다.


cifail2


이 방법은 간편하지만 slack-github-action에 비해 필드를 원하는데로 커스터마이징 하기가 어렵다는 단점이 있습니다.



📊 JaCoCo를 이용한 코드 커버리지 추가

build.gradle 설정

JaCoCo (Java Code Coverage)Java 애플리케이션의 코드 커버리지를 측정하기 위한 오픈 소스 도구입니다. 코드 커버리지는 단위 테스트나 통합 테스트에서 코드의 어느 부분이 실행되었는지를 나타내는 지표입니다. 이를 통해 작성된 테스트가 얼마나 효과적인지, 코드의 어느 부분이 테스트되지 않았는지 확인할 수 있습니다.

JaCoCo를 이용해서 코드 커버리지(code coverage)를 측정하고, 해당 커버리지 리포트를 댓글(comment)로 달아주는 워크플로우를 추가 해봅시다.


먼저 JaCoCo를 사용하기 위해서 build.gradle에 필요한 설정을 추가해줍니다.

plugins {
  // ...
  id 'jacoco' // 추가
}

jacoco {
  toolVersion = "0.8.10" // jacoco 버전 명시
}

jacocoTestReport {
  reports {
    xml.required = true // madrapps/jacoco-report를 사용하기 위해서 xml 리포트 사용
    html.required = true
	}
  
  // 각 리포트 타입 마다 저장 경로를 설정할 수 있다
  //  html.destination file("$buildDir/jacoco/html")
  //  xml.destination file("$buildDir/jacoco/xml")

  // xml 기본 저장 경로: $buildDir/reports/jacoco/test/jacocoTestReport.xml

  // 보고서에 표시되는 걸 제외하고 싶은 클래스를 명시
  afterEvaluate {
    classDirectories.setFrom(
        files(classDirectories.files.collect {
          fileTree(dir: it, excludes: [
            // 나의 프로젝트에 맞게 변경 {그룹이름}/{프로젝트명}
              "seungki/cicdpractice/api/domain/**",
              "**/*Application*",
              "**/*Request*",
              "**/*Response*",
              "**/*Exception*"
          ])
        })
    )
  // 보고서를 생성하고 나서야 테스트 커버리지 검증을 진행
  finalizedBy(jacocoTestCoverageVerification)
}
  
// 커버리지 검증을 위한 기준 제시
jacocoTestCoverageVerification {
  violationRules {
    rule {

      /**
       * element: 커버리지를 체크하는 기준
       *
       * BUNDLE: 전체 프로젝트의 모든 파일(default)
       * CLASS: 클래스
       * METHOD: 메서드
       * PACKAGE: 패키지
       * SOURCEFILE: 소스 파일
       **/
      enabled = true
      element = 'CLASS'

      /**
       * counter: 커버리지 측정을 위한 최소의 단위
       *
       * BRANCH: 조건문의 분기 수
       * CLASS: 클래스의 수
       * COMPLEXITY: 복잡도
       * INSTRUCTION: Java 바이트코드 명령의 수(default)
       * METHOD: 메서드의 수
       * LINE: 빈 줄을 제외한 실제 코드의 라인 수
       **/

      /**
       * value: 커버리지의 측정 메트릭(metric)
       *
       * TOTALCOUNT: 전체 개수
       * MISSEDCOUNT: 커버되지 않은 개수
       * COVEREDCOUNT: 커버된 개수
       * MISSEDRATIO: 커버되지 않은 비율. 0 ~ 1 사이의 숫자로, 1이 100%.
       * COVEREDRATIO: 커버된 비율. 0 ~ 1 사이의 숫자로, 1이 100%. (default)
       **/

      limit {
        counter = 'LINE'
        value = 'COVEREDRATIO'
        minimum = 0.80 // value에 대한 최소 통과 기준
      }

      // 커버리지 체크를 제외할 클래스를 명시
      // jacocoTestReport과 가르게 패키지 경로를 적어줘야 한다
      excludes = [
          "seungki.cicdpractice.api.domain.**",
          "**.*Application*",
          "**.*Request*",
          "**.*Response*",
          "**.*Exception*"
      ]
    }
  }
}

tasks.named('test') {
  useJUnitPlatform()
  finalizedBy jacocoTestReport // 항상 테스트가 완료되어야 리포트 생성
}

.gradlew test 또는 인텔리제이의 Gradle 도구에 들어가서 Tasks>verification>test를 실행해봅시다.


cifail2

Gradle > Tasks > verification > test


다음과 같은 결과를 얻었습니다.


cifail2

cifail2

service 커버리지의 최소 조건을 만족하지 못한다

현재 컨트롤러 계층의 테스트만 작성했기 때문에, 서비스 계층에 대한 커버리지 조건을 만족하지 못하여 실패했습니다. 서비스에 대한 테스트를 추가하고 다시 시도해보겠습니다.


cifail2

최소 기준을 만족하고, 통과된다



워크플로우 추가

이번에는 madrapps/jacoco-report를 사용해서 코드 커버리지에 대한 리포트를 자동으로 댓글로 달아주는 스텝을 추가해봅시다.


# 추가
- name: Leave a comment for test coverage
  id: jacoco
  uses: madrapps/[email protected]
  with:
    title: 📊 Test Coverage Report
    paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml # JaCoCo 리포트 위치
    token: ${{ github.token }}
    min-coverage-overall: 80 # 전체 프로젝트의 커버리지가 80% 이상이어야 통과
    min-coverage-changed-files: 80 # 수정된 파일에 대한 최소 커버리지가 80% 이상이어야 통과
    pass-emoji: '' # 통과 이모지 (default : ":apple:")
    # update-comment: true # true로 설정하는 경우 기존 코멘트를 업데이트하는 형태로 동작


📑 Reference

About

깃헙 액션(GitHub Actions) 사용법 알아보기. CI 파이프라인 구축 예시. 깃헙 액션을 이용한 슬랙 알림 전송. JaCoCo를 이용한 코드 커버리지 추가.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages