Github Action을 활용한 소스 안정성 개선

여러분의 소스는 안전하신가요? 소스의 안전을 위한 툴인 Github Action에 대해 알아보고 실제 적용 방법에 대해 설명합니다.

작성일 2023년 10월 18일

저희 IMQA에서는 항상 Staging Server(OTE)에서 테스트를 거친 후에 배포합니다. 그리고 프로젝트 실행 환경은 Local, OTE, Production으로 나뉘죠. 실행 환경에 따라 각각 환경(ENV)에 맞는 설정들이 적용되도록 명령어를 분리했습니다. env마다 분리된 Profile에 따라 설정이 다르게 적용됩니다.

하지만 지난 배포에 QA 테스트 때는 문제가 없었던 페이지가 정상적으로 뜨지 않는 이슈가 있었습니다. 알고 보니 저의 소스가 ESLint에서 정의한 규칙에 위배되는 소스였습니다. (큰따옴표, 공백 등)

물론 제가 개발 환경을 세팅할 때 팀에서 정의한 eslint 설정을 적용하지 않은 문제였지만, lint가 production일 때만 체크 되도록 하는 소스가 있었습니다. (local, ote 환경에선 lint를 체크하지 않았죠.)

그렇게 팀에 lint 관련 이슈가 공개되었습니다.

미션이다!!

단순히 lint 이슈를 해결하면 되는 문제였습니다. 하지만 개발자로서 조금 부끄러운 행동을 또 해버렸습니다. 개발 브랜치에 소스 충돌(Conflict)이 해결되지 않은 소스를 머지해버린 겁니다. 머지를 하는 과정에서 Conflict를 하나 빠뜨렸는데요. 1주일 뒤에 발견됐습니다. (다행히 배포나 다른 고객사에 영향이 가진 않았습니다.)

머지를 했으면서 머지가 잘 됐는지 확인하지 않은 인재(人災)였습니다. 개발자로서 도덕을 지키지 않은 수준이었죠. (길 가다 침을 뱉는 것처럼 부도덕한 일 수준이라고 선임님께 엄청나게 혼났어요.)

하지만! 우리 IMQA팀엔 룰이 있죠!

똥을 싸면, 침을 뱉으면 자기가 치운다.
두 번 다시 똥을 싸지 않기 위한, 침을 막기 위한 대책을 마련해라.
- Roh -

사고를 친 거는 사고를 친 거고 과거에 연연하기보단, 앞으로 이러한 황당한 이슈가 재발하지 않도록 대책을 마련해야합니다. 침을 뱉는 건 제가 조심해야 하는 부분이지만, 저는 남들이 침 뱉는 것도 막을 겁니다. 우리의 소스는 깨끗해야 하니까요!

저는 우리의 소스를 안전한 상태로 유지하기 위해 Github Action을 이용하려고 합니다.

Github Action 이란?

GitHub Action은 GitHub에서 제공하는 자동화 CI/CD 툴입니다.

이미지 출처: Jeongs, 2022.04.06, 'GitHub Actions 시작하기'

push와 merge와 같은 Event로 인해 트리거 되면 Runner라는 가짜 컴퓨터를 빌려줍니다. Runner 위에서는 정의한 Job들이 실행됩니다.

Job은 여러 개의 Step으로 이뤄졌으며 Step에는 하나의 Action이 있습니다. Job안에 Step은 순차적으로 실행되며 Job끼리는 병렬로 실행시킬 수 있습니다.

Step안에는 실제로 할 동작인 Action이 정의되어 있는데 Runner에서 실행한 명령어를 기술하거나 GitHub Marketplace와 같은 확장프로그램 상점을 통해 다른 분들이 만들어 놓은 Action을 가져와 사용할 수도 있습니다.

이러한 Job들이 담긴 것을 Workflow라고 하며 .yml 형식으로 작성합니다.

우리의 목표

소스의 안전을 위한 툴인 Github Action에 대해 간단하게 알아봤습니다. 이제 작업을 해야겠죠! 저의 목표는 '안정적인 제품의 소스 퀄리티 유지!' 이슈가 없는 안전한 소스는 안전한 배포로 이어지고, 좋은 제품을 의미하죠.

Github Action은 CI/CD 툴로써 배포 자동화의 목적이 있지만 저는 다른 목적으로 이용하려고 합니다. 안정적인 소스의 퀄리티 유지를 위해 그를 방해하는 문제를 사전에 검사하고 우리에게 Noti가 될 수 있도록 하려고 합니다. 그래야 알아채고 고치니까요. (Conflict 미해결 사건도 1주일 동안 눈치채지 못했습니다.)

하지만 단순히 시스템으로만 소스의 안정성을 유지하기엔 어려움이 있었습니다.

작업 전 고민들

이슈가 있는 소스는 remote Repository에 들어가지 못하게 한다.

소스 검사 action을 push나 merge 등은 바로 remote에 있는 소스에 영향을 끼친 이후에 체크하게 됩니다. 만약 이슈가 있는 소스였다면 revert를 해야 하는데, revert를 하게 되면 작업했던 소스가 날아가서 너무 위험합니다.
개발 브랜치엔 Pull Request로만 merge가 가능하도록 설정, Pull Request의 check point로 action 추가

소스 검사는 Push에 실행한다.

→ 조금 더 타이트하게 관리 되었으면 하므로 Action의 트리거를 PR마다 체크 → push마다 체크

시스템적 제한보단 이슈화, 공론화가 목적

물론 정확하고 깐깐한 절차를 거친 후에 소스를 반영하는 것이 정석입니다. 그러나 새로운 기능을 빠르게 배포하고, 이슈를 빠르게 수정하는 기민한 제품을 장점으로 생각하기 때문에 간단한 이슈 해결도 PR을 날리는 절차를 추가하기보단, 이슈를 놓치지 않고 알아차리는 것이 저의 Github Action을 사용한 목적입니다.
→ push마다 문제가 있는지 확인하고 문제가 있다면 Slack으로 메시지를 보내 공론화

주요 Point

소스에 문제가 있다고 판단하는 주요포인트는 아래와 같습니다.

  1. lint 이슈
  2. conflict 미해결
  3. 빌드 실패

이러한 이슈가 발생했을 때 바로바로 팀 Slack 채널에 메시지가 오도록 Workflow를 짜볼까요?

작업 과정 (테스트)

완성된 Workflow

name: Build and Lint Workflow

on:
  push:

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14

    - name: Install dependencies
      run: npm install

    - name: Run ESLint
      run: npm run lint

    - name: Send Slack Message on Lint Failure
      if: failure()
      run: |
        curl -X POST -H 'Content-type: application/json' --data '{
          "text": ":exclamation: Linting has failed for the main branch in the repository '"$GITHUB_REPOSITORY"'. Please investigate."
        }' https://hooks.slack.com/services/

  merge_conflict_job:
    runs-on: ubuntu-latest
    name: Find merge conflicts
    steps:
      - uses: actions/checkout@v2
      - name: Merge Conflict finder
        uses: olivernybroe/action-conflict-finder@v4.0
      - name: Send Slack Message on Founded Conflict
        if: failure()
        run: |
          curl -X POST -H 'Content-type: application/json' --data '{
            "text": ":exclamation: Conflict is no resolved in the main branch of the repository '"$GITHUB_REPOSITORY"'. Please investigate."
          }' https://hooks.slack.com/services/
          
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14

    - name: Install dependencies
      run: npm install

    - name: Build Project
      run: npm run build

    - name: Send Slack Message on Build Failure
      if: failure()
      run: |
        curl -X POST -H 'Content-type: application/json' --data '{
          "text": ":exclamation: Build has failed for the main branch in the repository '"$GITHUB_REPOSITORY"'. Please investigate."
        }' https://hooks.slack.com/services/

Lint 체크

lint:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14

    - name: Install dependencies
      run: npm install

    - name: Run ESLint
      run: npm run lint

    - name: Send Slack Message on Lint Failure
      if: failure()
      run: |
        curl -X POST -H 'Content-type: application/json' --data '{
          "text": ":exclamation: Linting has failed for the main branch in the repository '"$GITHUB_REPOSITORY"'. Please investigate."
        }' https://hooks.slack.com/services/

프로젝트별로 Lint Rule를 정의합니다. 그리고 runner가 프로젝트 내의 npm script중 lint를 확인하는 npm run lint를 실행합니다.

1. Checkout

actions/checkout@2
marketplace에 있는 체크아웃 action, runner에 현재 버전의 소스를 checkout 받는다.

2. Set up Node

actions/setup-node@v2
marketplace에 있는 node 설치 action, 이용 할 node version을 선택합니다.

3. Install Dependencies

npm install
node module을 설치합니다.

4. Run ESLint

npm run lint
프로젝트 내의 lint를 검사하는 npm script 실행

5. Send Slack Message on Lint Failure

사전에 Slack 채널에 웹훅을 설정하고 에러 내용 관련 메시지 api 호출

Conflict 체크

merge_conflict_job:
    runs-on: ubuntu-latest
    name: Find merge conflicts
    steps:
      - uses: actions/checkout@v2
      - name: Merge Conflict finder
        uses: olivernybroe/action-conflict-finder@v4.0
      - name: Send Slack Message on Founded Conflict
        if: failure()
        run: |
          curl -X POST -H 'Content-type: application/json' --data '{
            "text": ":exclamation: Conflict is not resolved in the main branch of the repository '"$GITHUB_REPOSITORY"'. Please investigate."
          }' https://hooks.slack.com/services/

Conflict 미해결 소스는 Lint 체크에서 찾지 못합니다. 또한 저희 프로젝트 중 빌드가 없는 프레임워크를 사용하여 빌드로 소스 안정성을 확인하지 못하여 별도의 작업이 필요합니다. 그래서 confilct-finder 액션을 이용했습니다.

Merge Conflict finder

olivernybroe/action-conflict-finder@v4.0
marketplace에 있는 Conflict 미해결 소스를 찾는 action 소스 내의 미해결 Conflict를 찾습니다.

Send Slack Message on Founded Conflict

사전에 Slack 채널에 웹훅을 설정하고 에러 내용 관련 메시지 api 호출

Build 체크

build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: 14

    - name: Install dependencies
      run: npm install

    - name: Build Project
      run: npm run build

    - name: Send Slack Message on Build Failure
      if: failure()
      run: |
        curl -X POST -H 'Content-type: application/json' --data '{
          "text": ":exclamation: Build has failed for the main branch in the repository '"$GITHUB_REPOSITORY"'. Please investigate."
        }' https://hooks.slack.com/services/

빌드를 시도합니다. 소스에 문제가 있다면 빌드에 실패하겠죠?

1. Checkout

actions/checkout@2
marketplace에 있는 체크아웃 action, runner에 현재 버전의 소스를 checkout 받는다.

2. Set up Node

actions/setup-node@v2
marketplace에 있는 node 설치 action, 이용할 node version을 선택합니다.

3. Install Dependencies

npm install
node module을 설치합니다.

4. Run ESLint

npm run lint
프로젝트 내의 lint를 검사하는 npm script 실행

5. Send Slack Message on Lint Failure

사전에 Slack 채널에 웹훅을 설정하고 에러 내용 관련 메시지 api 호출

이렇게 lint 이슈, conflict 미해결 이슈, 빌드 실패 이슈를 찾는 workflow가 완성됐습니다.

검증

Lint Check

Lint 단계에서 실패하여 Slack에 메시지가 오는 것을 확인했습니다.

Unresolved Conflict Check

views/container/detail_new/scripts.ejs

이렇게 conflict 미해결 소스를 push 해봤습니다.

conflict이 해결되지 않았다는 Slack 메시지가 왔습니다.

Build Check

Conflict 미해결 소스로 빌드를 실패한 Slack 메시지 확인

이렇게 각각의 단계에서 실패할 경우 Slack 메시지가 오는것을 확인했습니다.

추가로 누가 문제를 발생시켰으며, 정확한 에러 메시지, 로그를 보기 위한 액션 링크 가 메시지에 포함되면 좋겠네요.

앞으로는...

ESLint 🆚 Prettier

사실 프로젝트 내부의 eslint와 prettier의 충돌이 있습니다. 작업 중 저장 시 prettier가 돌아 소스가 format되게 되어 있는데 prettier대로 format한다면 eslint rule에 위배되어 push 전 lint rule에 맞게 다시 작업해야 합니다.

Cache

현재 매 Job 마다 node를 설치하고 node module을 다시 다운받고 있습니다. 병렬로 이루어져 각각의 job마다 해야 하긴 하지만 캐싱을 통해 node_module의 변화가 없으면 캐시 데이터 활용하여 보다 빠르게 action이 마무리될 수 있습니다. (현재 약 2분 소요)

이미지 출처: 카카오엔터테인먼트 FE 기술블로그, 2022.01.06, 카카오웹툰은 GitHub Actions를 어떻게 사용하고 있을까?

Auto Merge

제품 특성상 고객사별 커스텀 기능이 있어 고객사별로 브랜치가 나뉘어져 있습니다. 그래서 고객사 배포시 공통 기능을 머지하는 데 어려움이 있습니다.

github action을 통해 develop 브랜치에 머지가 된다면 각각의 고객사별 브랜치에 머지가 되도록 하는 Job을 추가할 수 있습니다. Github Action은 브랜치 명을 기준으로 판단할 수 있는데, Branch Naming Rule을 정의하여 해당 문자열이 포함된 브랜치에 머지하도록 설정할 수 있습니다.

Build File Upload

현재 Front 프로젝트들은 빌드된 파일만 배포되는 것이 아닌 소스 전체가 배포시에 이용됩니다.

배포 전 빌드를 다시하여 시간이 오래 소요되는 이슈가 있는데, Master나 고객사별 Master 브랜치에 머지가 잘 된다면 build하여 build된 파일만 관리하는 Repo에 반영하여 가벼운 파일만 이동하면 되도록 할 수 있습니다.


저희 IMQA는 이렇게 자동화툴을 이용해 소스와 제품의 안정성을 높이기 위해 노력합니다. 큰 실수를 하긴 했지만 “다음엔 잘해야지”라는 마음가짐은 물론이고, 시스템적으로 보완할 수 있는지 생각해보고 적용하는 팀의 문화가 있어서 좋은것 같습니다.

저의 실수와 보완 방법이 다른 분께도 도움이 되었으면 좋겠습니다! 감사합니다.

Share on

Tags

IMQA 뉴스레터 구독하기

국내외 다양한 기술 소식을 선별하여 매월 전달해드립니다. IMQA 뉴스레터를 통해 기술 이야기를 함께해보세요.

구독하기