Github Actions 를 이용한 CI 테스트 자동화
3차 데모 요구사항에 테스트 자동화가 있었습니다.
저희 팀은 Github Actions를 이용해 PR생성 시, 추가 Push 발생 시, merge 발생 시,
Github에서 제공하는 기능을 이용하여 테스트 자동화를 시도하였습니다.
관련한 내용을 기록해봅니다.
학습 방법
가장 먼저 이전 기수 선배분들의 리포지토리를 탐색했습니다.
.github/workflows 경로 내에 파일들이 관리되어야 하기 때문에,
각 팀의 리포지토리에 해당 경로에만 들어가면 어떻게 CI 테스트 자동화를 구성했는지,
어떤 이벤트에 대해 어떤 동작을 설정했는지 어렵지 않게 파악할 수 있었습니다.
yaml 파일이기 때문에 Github Action에 대한 배경지식이 없더라도
대략적으로 어떤 상황일 때 어떤 동작을 하도록 구성했는지 파악할 수 있었습니다.
몇 가지 참고할 만한 자료들의 인상적이었던 점을 꼽자면 다음과 같았습니다.
- 최소한의 기능만 심플하게 담아서 초기 진입 장벽을 낮춰줄 참고 자료
- 하나의 파일에 백엔드, 프론트, 테스트, 빌드 모두 관리하는 참고 자료
- 파트별로, 상황별로 파일을 분리하여 관리하는 참고 자료
- SonarQube 빌드를 활용한 참고 자료
- Slack 알림을 활용한 참고자료
추가로 생활코딩의 영상도 참고하였습니다.
역시 새로운 개념을 학습할 땐 우아한Tech 채널의 10분 테코톡과 생활코딩이 가장 잘 맞더라고요.
적용 방법
.github/workflows/파일명.yml 경로에 내용을 작성하기만 하면 됩니다!
아래 내용은 저희 프로젝트에 적용한 코드입니다.
name: 줍줍 백엔드 CI 테스트 자동화
on:
push:
branches:
- main
- release/*
- develop
paths: 'backend/**'
pull_request:
branches:
- main
- release/*
- develop
paths: 'backend/**'
defaults:
run:
working-directory: backend
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 리포지토리를 가져옵니다
uses: actions/checkout@v3
with:
token: ${{ secrets.SUBMODULE_TOKEN }}
submodules: recursive
- name: JDK 11을 설치합니다
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Gradle 명령 실행을 위한 권한을 부여합니다
run: chmod +x gradlew
- name: Gradle build를 수행합니다
run: ./gradlew build
- name: 테스트 결과를 PR에 코멘트로 등록합니다
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:
files: '**/build/test-results/test/TEST-*.xml'
- name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
token: ${{ github.token }}
- name: build 실패 시 Slack으로 알립니다
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: 백엔드 빌드 실패 알림
fields: repo, message, commit, author, action, eventName, ref, workflow, job, took
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure()
적용할 이벤트 설정하기
on:
push:
branches:
- main
- release/*
- develop
paths: 'backend/**'
pull_request:
branches:
- main
- release/*
- develop
paths: 'backend/**'
두 가지 경우에 대해 동작을 수행하라고 작성해주었습니다.
push와 pull_request 인데요, push는 merge를 포함하고, pull_request는 생성과 추가 push를 포함합니다.
적용 대상 A
1. 푸시 이벤트가 발생했고
2. 대상 브랜치 이름이 main 이거나, release/로 시작하거나, develop이고
3. backend 이하 경로에 변경이 있었을 때
적용 대상 B
1. PR 이벤트가 발생했고
2. 대상 브랜치 이름이 main 이거나, release/로 시작하거나, develop이고
3. backend 이하 경로에 변경이 있었을 때
두 이벤트 중 하나라도 발생하면 아래에 작성될 동작을 수행하는 것입니다.
기본 작업 경로 설정
defaults:
run:
working-directory: backend
더 아래에서 명시될 수행 작업들의 기본 경로를 설정해줍니다.
현재 줍줍의 리포지토리는 루트에 backend, frontend로 구분이 되어있기 때문에,
전체 리포지토리를 clone 해올 경우, backend 경로로 들어가야 gradle build 명령을 수행할 수 있기 때문입니다.
만약 더욱 복잡한 작업이 늘어나게 될 경우, 수행할 작업마다 경로가 달라질 수도 있습니다만
현재는 불필요하게 같은 경로 선언이 반복되기 보다는 기본 설정을 잡아주는 쪽을 선택해봤습니다.
리포지토리 Check out
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 리포지토리를 가져옵니다
uses: actions/checkout@v3
with:
token: ${{ secrets.SUBMODULE_TOKEN }}
submodules: recursive
jobs부터 작업들을 선언해주면 됩니다.
build는 Github에 표기될 작업의 이름이니 본인이 편한 다른 이름을 사용하셔도 됩니다.
jobs 내에는 여러 개의 작업들이 들어올 수 있고,
하나의 작업은 steps에 명시된 내용을 순차적으로 수행하는 식입니다.
이번 프로젝트에 적용한 내용은 하나의 build라는 이름의 작업에
여러 개의 step을 선언하여 적용해보았습니다.
steps 에는 순차적으로 수행할 내용들을 명시합니다.
- name 에는 작업할 내용이 무엇인지 식별할 수 있는 값을 넣습니다.
uses 에는 외부 라이브러리를 활용할 때 해당 라이브러리를 선언해주는 곳입니다.
with에는 라이브러리에 대한 추가적인 옵션 또는 설정을 선언해둡니다.
위 예시는 checkout 라이브러리를 활용하는 것인데요,
팀 프로젝트의 리포지토리는 public이지만, 내부에서 환경설정 파일을 숨기기 위해
별도 private 팀 리포지토리를 submodule로 품고 있습니다.
따라서 checkout 할 때 해당 submodule private 리포지토리도 가져올 수 있도록
토큰 설정과 submodule 설정을 with 문에 추가한 모습입니다.
Repository Secret 설정
Secrets are environment variables that are encrypted.
Anyone with collaborator access to this repository can use these secrets for Actions.
Github Action에서 사용될 민감 정보들을 리포지토리 자체에 환경변수처럼 심어두는 설정입니다.
Repository의 Settings -> Secrets -> Actions에 들어가면 추가할 수 있고,
수정, 삭제도 가능하지만, 수정 시 기존 설정값을 확인할 수는 없습니다.
또한 포크 된 리포지토리에서는 업스트림 리포지토리에 설정된 값을 사용할 수 없습니다.
보안적으로도 훌륭하게 처리되어 있는 모습이네요.
이번 CI 테스트 자동화에서는 Slack 알림을 위한 Webhook URL,
서브모듈 checkout 을 위한 private 리포지토리에 접근 권한이 있는 토큰
이렇게 두 가지 정보를 민감 정보로 숨겨야 해서 Secret 설정을 사용했습니다.
token: ${{ secrets.SUBMODULE_TOKEN }} 와 같이 선언하면
SUBMODULE_TOKEN 에 설정해둔 값이 자동으로 주입되어 Github Actions이 수행됩니다.
빌드 테스트
- name: JDK 11을 설치합니다
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Gradle 명령 실행을 위한 권한을 부여합니다
run: chmod +x gradlew
- name: Gradle build를 수행합니다
run: ./gradlew build
리포지토리를 가져온 이후에 빌드를 진행하기 위한 step들입니다.
JDK를 설치하고, gradlew 명령어 권한을 부여하고, build를 수행합니다.
gradle의 build 명령어는 다음과 같은 작업들을 순차적으로 수행합니다.
따라서 build 명령어만 호출해도 충분합니다. 테스트까지 이미 포함되어 있기 때문입니다.
다만 빌드 시점에 테스트에 관련된 로그들을 모두 확인하고 싶다면
./gradlew --info build와 같이 선언하는 것도 방법이 될 수 있지만 아무래도 성능을 약간 양보해야 할 겁니다.
PR에 테스트 결과 코멘트로 등록하기
- name: 테스트 결과를 PR에 코멘트로 등록합니다
uses: EnricoMi/publish-unit-test-result-action@v1
if: always()
with:
files: '**/build/test-results/test/TEST-*.xml'
빌드 테스트를 수행한 뒤에 선언된 step입니다.
gradle build를 수행하고 나면 아래와 같은 경로에 테스트 결과에 대한 내용을 담은 파일이 생성되는데요,
이 파일들을 이용해 테스트 결과를 포매팅하여 PR에 코멘트 형식으로 추가해주는 라이브러리입니다.
테스트 결과를 코멘트로만 달아주는 작업은 성공 실패 여부와 상관없이
항상 동작하기를 기대해서 always 선언을 해준 모습입니다.
파일은 테스트 파일이 생성되는 경로를 명시해둔 모습입니다.
테스트 결과 코멘트는 위와 같이 작성됩니다.
see this check를 클릭하면 아래와 같은 이미지가 나옵니다.
어떤 테스트 메서드가 실패했는지, 실제 로그는 어떠한지까지 볼 수 있습니다.
이 기능 덕분에 gradle build 명령어에서 --info를 굳이 선언하지 않아도 된다고 판단했습니다.
오류가 발생한 코드라인에 Check 코멘트 자동 생성하기
- name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다
uses: mikepenz/action-junit-report@v3
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
token: ${{ github.token }}
테스트 실패 케이스가 발생할 경우, 해당 실패가 발생한 코드라인에 코멘트를 자동 생성해주는 기능입니다.
PR에서 File Changed에 들어가서 바로 어느 코드라인에서 왜 실패했는지 쉽게 파악할 수 있습니다.
always로 선언하더라도, 실패할 경우만 작성됩니다.
다만 신기한 점은, 만약 해당 PR에 오류를 수정해서 추가 PUSH를 하게 되면,
새롭게 Github Actions이 동작하는데요,
이때 실패했던 테스트가 성공하게 되면 기존에 생성됐던 Check 코멘트가 자동으로 삭제됩니다.
따라서 이전에 실패했는데 지금은 고쳐지도록 반영된 거 아닌지 헷갈릴 일이 없겠네요!
설정에는 동일하게 테스트 결과 파일이 생성되는 경로를 선언해주었고,
토큰으로 선언된 github.token은 별도 secret 설정된 값이 아니고 기본값이니 저대로 명시하시면 됩니다.
빌드 테스트 실패 시, Slack 알림 받기
- name: build 실패 시 Slack으로 알립니다
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
author_name: 백엔드 빌드 실패 알림
fields: repo, message, commit, author, action, eventName, ref, workflow, job, took
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure()
별도로 설정해야 할 내용은 SLACK_WEBHOOK_URL을 Repository Action Secret으로 추가하는 내용입니다.
${{ job.status }} 는 별도로 설정해줄 내용 없이 그대로 명시하시면 됩니다.
failure()로 선언하면 실패 시에만, always()로 선언하면 성공 실패 여부와 상관없이 항상 알림을 보내줍니다.
위 사례는 실제 오늘 있었던 사례인데요,
Github Actions가 적용되기 이전에 생성됐던 PR이었고, Github Actions가 추후 적용되었으며,
그리고 나서 PR이 merge 되어 Github Actions가 main 브랜치에 push 이벤트로 인해 트리거 된 상황이었습니다.
테스트 격리가 온전히 되지 않아 한 가지 테스트 케이스가 실패하고 있었고,
Slack 알림 덕분에 merge 된 결과 코드가 정상 동작하지 않음을 인지할 수 있었습니다.
앞으로는 PR 생성 시, 코드 추가 PUSH 시 바로바로 테스트가 진행될 테니
오늘처럼 merge가 일어난 뒤에 이슈를 발견할 일은 없지 않을까 싶네요.
수행된 Github Actions 목록 확인하기
Actions 탭의 존재 자체를 이번 Github Actions를 적용하며 처음 깨달았습니다.
PR 이벤트에 적용되는 내용들은 PR 페이지에서 볼 수 있기 때문에 상관없을 수 있지만,
merge 이벤트, push 이벤트 등에 대응하는 내용들은 해당하는 별도 페이지가 존재하지 않기 때문에
결과를 보기 위한 페이지가 없습니다.
따라서 리포지토리의 Actions 탭에 들어가서 수행된 내용을 확인해야 합니다.
위 이미지에서 빌드 실패한 feat: 채널명 변경 ~ 의 경우, merge 이벤트로 인해 수행되었던 테스트인데,
실패하여 x 표시되어 있고, 이후 hotfix 브랜치에서 처리한 테스트 격리는 정상 완료되었기에 초록색으로 나타나네요.