티스토리 뷰

이전 포스트에서 적용할 CI 파이프라인에서 적용할 사항들에 대한 고찰과 전반적인 설계를 진행하였다.

https://hoplin-dev.tistory.com/5

 

[CI] Github Actions CI 적용하기 #3 - Actions Pipeline 설계하기

이전 포스트에서는 간단하게 Unit Test와 E2E Test를 순차적으로 진행하도록 Actions를 작성하였다. https://hoplin-dev.tistory.com/4 [CI] Github Actions CI 적용하기 2편 - Github Action Script 작성하기 이전 포스트에서

hoplin-dev.tistory.com

이제 실제로 위 설계를 Github Actions로 옮겨볼 차례이다. 

전체 코드는 아래 링크를 참고하자.

https://github.com/J-Hoplin/Online-Judge-System/blob/dev/.github/workflows/pre-test.yml

 

기본 설정하기

먼저 Action에 대한 기본 설정을 진행해준다. 가장 먼저 name필드와 run-name필드를 설정해준다. run-name필드는 github context를 활용하여 Action Trigger 이벤트 이름과 해당 Action을 발생시킨 사람의 이름이 함께 표시되도록 한다.

name: CI-Pull-Request-Pretest
run-name: Unit/E2E test of ${{ github.ref_name }} by ${{ github.actor }}

 

그 다음 on필드를 통해 이 이벤트가 어떠한 상황에 발생될 것인지에 대한 정의가 필요하다. 해당 프로젝트에서는 production브랜치 그리고 dev브랜치Pull Request가 생성될때 테스트가 실행되도록 할것이다.

name: CI-Pull-Request-Pretest
run-name: Unit/E2E test of ${{ github.ref_name }} by ${{ github.actor }}
on:
  pull_request:
    branches:
      - dev
      - production

여기서 한가지 더 고려할 사항이 있다. 모든 브랜치에서 코드가 추가/수정될 가능성도 높지만, 반대로 단순히 Readme와 같은 프로젝트 문서 수정이나 혹은 ERD와 같은 문서 사항들에 대한 추가만 이루어 질 수 도 있다. 이런경우에는 실제 비즈니스 로직에 영향을 주지 않는 요소들이다. 이러한 경우에는 테스트가 꼭 실행될 이유가 없다. 비즈니스 로직에 대한 변경이 발생할때만 테스트를 실행하면 된다. 해당 프로젝트에서는 Nest.js를 사용하기 있기 때문에, .ts확장자에 대한 변경이 있는 경우에만 테스트가 실행되도록 설정한다.

name: CI-Pull-Request-Pretest
run-name: Unit/E2E test of ${{ github.ref_name }} by ${{ github.actor }}
on:
  pull_request:
    branches:
      - dev
      - production
    paths:
      - '**.ts'

 

Job설정하기

Job: Build & Setup

이제 실제 Running Step인 Job을 하나씩 설정한다. 가장 먼저 빌드단계이다. 이전 포스트에서 설계한 내용을 기반으로 빌드 단계에서 이루어져야하는것들을 나열해보자.

 

  1. 프로젝트 코드 Checkout
  2. 프로젝트 의존성 설치 
  3. secrets context에 저장한 Secret을 활용하여 .ci.env파일 프로젝트 디렉토리에 생성하기
  4. Nest.js 빌드 실행
  5. Test DB에 스키마 초기화 후 Prisma Client Generate(Database Setup)
  6. 빌드된 Artifact 업로드
  7. 디스코드 알림

우선 6번 과정까지에 대해 작성한다.

jobs:
  build:
    name: Application Build Test & Set up & Initialize Test Database Server
    runs-on: ubuntu-latest
    steps:
      # Checkout repository codes
      - name: Repository Checkout
        uses: actions/checkout@v4
      # Setup environment for Node.js 18
      - name: Node.js version 18 Environment setting
        uses: actions/setup-node@v4
      - name: Initialize .ci.env
        run: |
          touch .ci.env
          echo "DATABASE_URL=${{ secrets.DATABASE_URL}}" >> .ci.env
          echo "ADMIN_EMAIL=${{secrets.ADMIN_EMAIL}}" >> .ci.env
          echo "ADMIN_PW=${{secrets.ADMIN_PW}}" >> .ci.env
          echo "JWT_SECRET=${{secrets.JWT_SECRET}}" >> .ci.env
          echo "JUDGE_SERVER_ENDPOINT=${{secrets.JUDGE_SERVER_ENDPOINT}}" >> .ci.env
          echo "AWS_REGION"=${{secrets.AWS_REGION}}>> .ci.env
          echo "AWS_ACCESS_ID=${{secrets.AWS_ACCESS_ID}}" >> .ci.env
          echo "AWS_ACCESS_SECRET=${{secrets.AWS_ACCESS_SECRET}}" >> .ci.env
          echo "AWS_SQS_QUEUE=${{secrets.AWS_SQS_QUEUE}}" >> .ci.env
          echo "AWS_S3_BUCKET=${{secrets.AWS_S3_BUCKET}}" >> .ci.env
      - name: Install Node.js Dependencies
        run: yarn install --force
      - name: Install dotenv-cli for CI script
        run: yarn global add dotenv-cli
      - id: build-phase
        name: Check Nest.js Application Build
        continue-on-error: true
        run: yarn build
      # Push Prisma Schema to test DB
      - name: Initialize Prisma Client
        run: yarn ci:init

 

이전 포스트에서 언급했듯, 빌드가 된 후 빌드된 파일들을 actions/upload-artifact@v4  를 활용하여 의존성 설치와 빌드파일을 보존한다. 그 다음  id: build-phasecontinue-on-error를 설정한것을 볼 수 있다. 일반적으로 Github Actions는 step을 실행하다가 오류가 나면 Workflow 자체를 멈춘다. 하지만 continue-on-error를 true로 설정해주게 되면, 오류가 나더라도 다음 step을 정상적으로 작동시킨다

 

그렇다면 여기서는 왜 빌드 에러가 났는데도 다음 step이 실행되도록 작성했는지 의문을 가질 수 있다. 이에 대한 이유는 알림은 항상 빌드 뒤에 와야한다. 만약 빌드에서 오류가 나서 끝났다면, 성공 여부에 대한 알림 step에 도달하지 못할것이다. 즉 디스코드에 해당 job의 성공여부 알림을 주기 위해서이다. (이 이유는 다음 다른 step들에도 모두 동일한 이유로 적용된다)

 

이제 마지막 7번 step을 마저 작성해본다.

  - name: 'Discord Alert - Success'
    if: steps.build-phase.outcome == 'success'
    uses: rjstone/discord-webhook-notify@v1
    with:
      severity: info
      details: 'Build & Setup: Success'
      webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
  - name: 'Discord Alert - Failed'
    if: steps.build-phase.outcome != 'success'
    uses: rjstone/discord-webhook-notify@v1
    with:
      severity: error
      details: 'Build & Setup: Fail'
      webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
  - name: 'Fail check'
    if: steps.build-phase.outcome != 'success'
    run: exit1

디스코드 알림은 rjstone/discord-webhook-notify@v1를 활용한다. 여기서 중요한것은 stepif문이다. build를 하는 step에서 id를 지정한것을 볼 수 있을것이다.  

      - id: build-phase
        name: Check Nest.js Application Build
        continue-on-error: true

step에 id를 지정해주면 steps context를 활용할 수 있다.

 

Document를 보면 steps.<step_id>.conclusionsteps.<step_id>.outcome 두가지가 존재하는것을 볼 수 있다. 이 두 가지는 모두 continue-on-error의 결과에 영향을 받게 된다. Document에서 step after continue-on-error, 그리고 step before continue-on-error 이 두가지가 얼핏 헷갈릴 수 있다.

조금 더 자세히 서술하면 continue-on-error가 정의된 줄을 기점으로 전/후를 의미한다. continue-on-error는 빌드단계가 이루어진 이후에 동작하게 된다.(빌드 프로세스의 성공 여부에 따른 분기가 이루어 지므로) 결론적으로, continue-on-error이전에 대한 결과가 빌드의 성공 여부에 대한 분기가 되는것이므로 steps.<step_id>.outcome을 사용해야한다. 이를 활용하여 빌드의 성공여부를 전송하고, 실패하였으면 exit 1을 실행하여 Workflow를 정지시킨다.

Job: Unit, E2E

이제 그 다음 Unit test, E2E test를 하는 Job을 정의한다. 위와 동일하게 어떤 작업이 이루어져야하는지 먼저 나열해본다.

 

  1. Build & Setup단계에서 업로드한 artifact download
  2. 테스트 실행
  3. 성공 여부 알림

비교적 간단하다. 각각 작성해보면 아래와 같다. 여기서 주의해줄 점은, Github Actions에서 actions/download-artifact@v4 를 활용하여 artifact를 다운로드하면 Permission 오류가 발생한다.(관련 이슈) 이를 해결하기 위해서는 다운로드한 artifact 디렉토리를 재귀적으로 권한을 변경해주면 된다.(필자처럼 꼭 777을 해줄 필요는 없다)(chmod)

   unit-testing:
    needs: build
    runs-on: ubuntu-latest
    steps:
      # Setup environment for Node.js 18
      - name: Node.js version 18 Environment setting
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Mount volume of pre-build
        uses: actions/download-artifact@v4
        with:
          name: build-result
          path: .
      - name: Grant Permission to artifacts
        run: chmod -R 777 .
      - name: Install dotenv-cli for CI script
        run: yarn global add dotenv-cli
      - id: unit-test-phase
        name: Run Unit Test
        continue-on-error: true
        run: yarn ci:unit
      - name: 'Discord Alert - Success'
        if: steps.unit-test-phase.outcome == 'success'
        uses: rjstone/discord-webhook-notify@v1
        with:
          severity: info
          details: 'Unit Test Result: Success'
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
      - name: 'Discord Alert - Failed'
        if: steps.unit-test-phase.outcome != 'success'
        uses: rjstone/discord-webhook-notify@v1
        with:
          severity: error
          details: 'Unit Test Result: Fail'
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
      - name: 'Fail check'
        if: steps.unit-test-phase.outcome != 'success'
        run: exit1

  e2e-testing:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Node.js version 18 Environment setting
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Mount volume of pre-build
        uses: actions/download-artifact@v4
        with:
          name: build-result
          path: .
      - name: Grant Permission to artifacts
        run: chmod -R 777 .
      - name: Install dotenv-cli for CI script
        run: yarn global add dotenv-cli
      - id: e2e-test-phase
        name: Run E2E Test
        continue-on-error: true
        run: yarn ci:e2e
      - name: 'Discord Alert - Success'
        if: steps.e2e-test-phase.outcome == 'success'
        uses: rjstone/discord-webhook-notify@v1
        with:
          severity: info
          details: 'E2E Test Result: Success'
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
      - name: 'Discord Alert - Fail'
        if: steps.e2e-test-phase.outcome != 'success'
        uses: rjstone/discord-webhook-notify@v1
        with:
          severity: error
          details: 'E2E Test Result: Failed'
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
      - name: 'Fail check'
        if: steps.e2e-test-phase.outcome != 'success'
        run: exit1

 

Job: Tear Down

위의 테스트 단계에서 데이터베이스를 사용하여 테스트용 DB에는 데이터가 보존되어있는 상태이다.(필자는 unit,e2e테스트를 통합테스트와 합쳐서 작성했기 때문이다). 테스트가 모두 끝난뒤에 Tear Down과정을 통해 테스트 데이터베이스를 정리해 주어야 한다

 

또한 당연하지만 Github Actions 또한 무료가 아니다. 위에서 빌드한 Artifact(빌드결과물)를 다른 Job들간의 공유를 위해 업로드 하였다. 프로젝트마다 다르겠지만, 우선 필자의 프로젝트에서는 대략 94.3MB라는 적지 않은 크기를 가지고 있다. Github Actions는 Artifact에 대해서 요금을 청구 한다. 빌드 결과물은 Test에서 사용한 이후 보존할 이유가 없으므로, 삭제하여주는것이 좋다(요금 및 리소스 절약)

Github에서 공식적으로 Upload한 Artifact를 삭제하는 Actions를 제공하지는 않는다. 서드파티 action인 geekyeggo/delete-artifact@v4를 활용하여 업로드한 Artifact를 삭제해준다.

 

Tear Down에서 이루어져야하는 작업 또한 비교적 간단하다.

  1. Build & Setup단계에서 업로드한 artifact download
  2. Tear Down
  3. 성공 여부 알림

이를 작성하면 아래와 같다.

  tear-down:
    needs: [unit-testing, e2e-testing]
    runs-on: ubuntu-latest
    steps:
      - name: Node.js version 18 Environment setting
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Mount volume of pre-build
        uses: actions/download-artifact@v4
        with:
          name: build-result
          path: .
      - name: Grant Permission to artifacts
        run: chmod -R 777 .
      - name: Install dotenv-cli for CI script
        run: yarn global add dotenv-cli
      - id: tear-down-phase
        name: Test Database tear-down
        continue-on-error: true
        run: dotenv -e .ci.env -- node node_modules/prisma/build/index.js db push --force-reset
      - uses: geekyeggo/delete-artifact@v4
        with:
          name: build-result
          token: ${{secrets.TOKEN}}
      - name: 'Discord Alert - Success'
        if: steps.tear-down-phase.outcome == 'success'
        uses: rjstone/discord-webhook-notify@v1
        with:
          severity: info
          details: 'Test DB Tear down: Success'
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
      - name: 'Discord Alert - Fail'
        if: steps.tear-down-phase.outcome != 'success'
        uses: rjstone/discord-webhook-notify@v1
        with:
          severity: error
          details: 'Test DB Tear down: Failed'
          webhookUrl: ${{ secrets.DISCORD_WEBHOOK }}
      - name: 'Fail check'
        if: steps.tear-down-phase.outcome != 'success'
        run: exit1

 

이제 설계한 사항들에 대한 Github Action 작성이 완료되었다. Pull Request를 생성하면 아래와 같이 테스트가 동작하는것을 볼 수 있다.

모든 Job이 성공하는 경우
일부 Job이 실패하는 경우

그리고 각 Job별로 설정한 Discord 알림도 잘 오는것을 확인한다. Github Action 탭에서 계속 확인하고 있는것보다 훨씬 편했다.(개인적인 견해)

 

PR Merge 이전 테스트 강제하기

Github Actions 스크립트를 작성하여 Pull Request를 생성하면 테스트를 실행하게끔 하였다. 하지만 이 상태에서는 Action이 실행되는것과 별개로 Pull Request를 하는것은 가능하다. 만약에 Pull Request Merge를 하기 위해서는 모든 테스트 과정을 통과해야만 하고, 테스트를 통과하지 못하는 경우 Merge를 막을려면 어떻게 해야할까?

 

Repository settings - Branchs에 들어가서 특정 브랜치에 대해 Rule을 적용해주면 된다. 여기서는 Pull Request Merge 이전에 대한 필수사항을 제약걸기 때문에 Require status checks to pass before merging에 체크를 해주고, 꼭 success 상태여야하는 Job들을 Require branches to be up to date before merging에 추가해주면 된다.

위와 같이 설정하면 지정한 Job들이 모두 성공하면 Merge가 가능해진다.(물론 강제로 Merge하는것도 가능하다.)

 

다음 포스트는 Github Cache를 활용하여 Build & Setup Job의 실행시간을 약 30초 줄여본 경험을 공유하면서 이 시리즈를 마무리 지어볼 것이다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함