본문 바로가기

개발정보

GitHub Action Feature 합치기

GitHub Action에 대해서는 앞서 어떤 요소들이 있는지 알아봤다. 

최종적으로 나 역시 CI / CD를 구축하는 것을 목표로 GitHub Action을 사용하려고 한다. 

 

CI의 베이스가 되는 브랜치 전략으로 Git Flow를 사용하려고 한다. 

GitHub Flow 등도 있을 텐데 최초 브랜치 전략을 공부할 때 Git Flow를 알아봤었고 아직까지 많이 사용되는 

전략이라서 굳이 다른 방향을 알아보진 않았다.  

어떤 방향으로 GitHub Action을 사용할 것인가! 

이게 중요할 것 같은데, Git Flow를 이야기하는 이유는 브랜치의 속성에 따라 Action을 설정해줄 생각이다. 

내가 사용할 브랜치로

  • Main : 프로젝트 | 서비스의 메인 브랜치로 정식 배포가 된 환경의 브랜치다. 
  • Develop : 개발 환경을 가지고 있는 브랜치다. 해당 브랜치에서 개발자들이 작업을 한다고 생각하면 된다. 
  • Feat/[작업] : Develop 브랜치를 기준으로 기능 단위로 개발할 때 만들어지는 브랜치이다. 
  • Release : 정식 배포가 되기 전 사전 배포를 진행해서 테스트를 진행하는 브랜치이다. 
  • Hotfix: Develop => Feat 단계를 거치지 않고 급하게 작업 후 배포하는 경우 사용되는 브랜치이다. 

다음과 같은 용도의 브랜치가 있다. 

여기서 GitHub Action을 사용이 사용되는 부분은 다음과 같다. 

  • Feat 브랜치 병합 : 전날까지 작업해서 Develop 브랜치에 Merge를 자동으로 해주는 Workflow
    코드 리뷰를 진행다고 PR을 올린 사람이 수동으로 Merge를 해도 되지만 간혹 리뷰에 답글만 달고 넘어가는 경우가 발생해서 Auto Merge를 지원한다면 다른 사람이 추후 작업에 충돌이 발생하는 문제가 없을 것이라고 생각된다. 
  • PR 검사 : 개발자가 PR을 올릴 때 테스트 코드를 실행시켜서 오류가  발견된다면 PR을 자동으로 닫는 용도의 Workflow
    테스트 코드를 작성하고 있을 때도 매번 PR을 올릴 때 테스트 코드를 체크하지 않고 올리는 경우가 있어서 예방하기 위해서 사용한다. 
  • Release 브랜치 병합 및 배포 : 주기적으로 Develop 브랜치의 코드를 Release 브랜치에 Merge 시켜주고 테스트 환경에 자동으로 배포를 진행시켜주는 Workflow 
    웹의 경우, lighthouse 까지 검사해서 성능을 확인할 수 있는 방향으로 고려할 생각이다. 
  • Main 브랜치 병합 및 배포 : Release 브랜치에 문제가 없는 상태일 때 정식 배포를 진행해주는 Workflow 

다음과 같은 기능을 해주는 GitHub Action을 만드려고 한다. 

우선 첫 번째로 Feat 브랜치 병합이다. 

 

내가 작성한 Feat 브랜치 병합은 다음과 같다. 

  • 매주 월요일에서 금요일 오전 10시에 자동으로 동작한다. 
  • PR을 전체적으로 확인해서 feat/ 가 포함된 브랜치를 Merge해준다. 
  • 이때, 충돌이 발생한 경우 Merge가 아닌 충돌이 발생했다는 댓글을 작성해준다. 
name: project-merge-actions-feature
run-name: Project Merge Feature
on:
  schedule:
    - cron: "0 10 * * 1-5"

jobs:
  merge:
    name: "Feature Auto Merge"
    runs-on: "ubuntu-latest"

    strategy:
      matrix:
        node-version: ["18.x"]

    steps:
      - name: "Check for GitHub Token"
        run: |
          if [ -z "${{ secrets.ACTION_TOKEN }}" ]; then
            echo "GitHub token is missing"
            exit 1
          else
            echo "GitHub token is present"
          fi

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}

      - name: "Feature Merge"
        uses: "actions/github-script@v3"
        with:
          github-token: ${{ secrets.ACTION_TOKEN }}
          script: |
            try {
                const query = `query($owner:String!, $name:String!) {
                    repository(owner: $owner, name: $name) {
                        pullRequests(last: 100, states: OPEN) {
                            edges {
                                node {
                                    number
                                    headRefName
                                    baseRefName
                                    author {
                                        login
                                    }
                                    repository {
                                        name
                                    }
                                    mergeable
                                }
                            }
                        }
                    }
                }`

                const variables = {
                    owner: context.repo.owner,
                    name: context.repo.repo
                }

                const FEATURE = "feat/"

                const { repository: {pullRequests: {edges: list}}} = await github.graphql(query, variables)
                for (let { node } of list) {
                    if (!node.headRefName.includes(FEATURE)) continue;
                    try { 
                        if (node.mergeable === "CONFLICTING") {
                            console.log(`Requesting changes for PR: ${node.number} by ${node.author.login}`);
                            await github.issues.createComment({
                                owner: context.repo.owner,
                                repo: context.repo.repo,
                                issue_number: node.number,
                                body: "이 PR에 충돌이 발생했습니다. 충돌을 해결해주세요."
                            });
                        } else if (node.mergeable === "MERGEABLE") {
                            console.log(`Merging PR: ${node.number} by ${node.author.login}`);
                            await github.pulls.merge({
                                owner: context.repo.owner,
                                repo: context.repo.repo,
                                pull_number: node.number,
                            });
                        } 
                    } catch (e) {
                        console.log(`Error handling PR ${node.number}`, e);
                    }
                }
            } catch (error) {
                console.log("Error fetching PRs:", error)
            }

전체적인 코드는 다음과 같다. 매일 병합만 해주는 Workflow라서 그렇게 어려운 기능이 들어가 있지는 않다. 

한 덩어리씩 같이 알아보자. 

on:
  schedule:
    - cron: "0 10 * * 1-5"

Event가 실행되는 조건을 작성했다. 

스케쥴로 cron에 들어가는 조건은 다음과 같다. "분[0, 59] 시[0, 23] 월[1,31] 년[1,12] 일[0,6]" 

"0 10 * * 1-5"는 월요일[1]에서 금요일[5] 10시에 동작되는 이벤트이다. 


※ 자주 사용될 수 있는 이벤트 

# 모든 브런치에서 push 이벤트 발생 시, WorkFlow 실행
on:
  push:

# develop 브런치에서 push 이벤트 발생 시, WorkFlow 실행
on:
  push:
    branches: [develop]

# 모든 브런치에서 pull-request 이벤트 발생 시, WorkFlow 실행
on:
  pull_request:

# develop 브런치에서 pull-request 이벤트 발생 시, WorkFlow 실행
on:
  pull_request:
    branches: [develop]

schedule 말고도 push, pr 등 다양한 경우에 사용될 수 있다. 더 많은 이벤트도 존재한다.   


- name: "Check for GitHub Token"
  run: |
    if [ -z "${{ secrets.ACTION_TOKEN }}" ]; then
      echo "GitHub token is missing"
      exit 1
    else
      echo "GitHub token is present"
    fi

아래에서 사용할 GitHub Token이 존재하는지 확인하는 코드이다. 

설정해둔 GitHub Action을 여러 곳에서 사용하면 secret에 토큰을 등록하는 작업을 잊을 수 있다고 생각해서 설정이 되어 있지 않은 경우 콘솔로 알려주기 위해서 추가해두었다. 

 

strategy:
   matrix:
     node-version: ["18.x"]

steps:
// ...
  - name: Use Node.js ${{ matrix.node-version }}
    uses: actions/setup-node@v4
    with:
      node-version: ${{ matrix.node-version }}

아래에서 사용할 actions/github-script@v3를 사용하기 위해서는 node 16 이상의 버전이 필요하다. 

따로 node 설정을 하지 않으면 버전 문제로 오류가 발생한다. 

 

- name: "Feature Merge"
  uses: "actions/github-script@v3"
  with:
    github-token: ${{ secrets.ACTION_TOKEN }}
    script: |
    try {
        const query = `query($owner:String!, $name:String!) {
            repository(owner: $owner, name: $name) {
                pullRequests(last: 100, states: OPEN) {
                    edges {
                        node {
                            number
                            headRefName
                            baseRefName
                            author {
                                login
                            }
                            repository {
                                name
                            }
                            mergeable
                        }
                    }
                }
            }
        }`

        const variables = {
            owner: context.repo.owner,
            name: context.repo.repo
        }

        const FEATURE = "feat/"

        const { repository: {pullRequests: {edges: list}}} = await github.graphql(query, variables)
        for (let { node } of list) {
            if (!node.headRefName.includes(FEATURE)) continue;
            try { 
                if (node.mergeable === "CONFLICTING") {
                    console.log(`Requesting changes for PR: ${node.number} by ${node.author.login}`);
                    await github.issues.createComment({
                        owner: context.repo.owner,
                        repo: context.repo.repo,
                        issue_number: node.number,
                        body: "이 PR에 충돌이 발생했습니다. 충돌을 해결해주세요."
                    });
                } else if (node.mergeable === "MERGEABLE") {
                    console.log(`Merging PR: ${node.number} by ${node.author.login}`);
                    await github.pulls.merge({
                        owner: context.repo.owner,
                        repo: context.repo.repo,
                        pull_number: node.number,
                    });
                } 
            } catch (e) {
                console.log(`Error handling PR ${node.number}`, e);
            }
        }
    } catch (error) {
        console.log("Error fetching PRs:", error)
    }

본격적인 Merge 작업을 위해서 사용되는 부분이다. 

github-script 액션은 자바스크립트를 사용할 수 있게 해주면서 octokit을 제공해준다. 

 

※ Octokit ?

GitHub API용 클라이언트 공식 컬렉션이다. 

쉽게 octokit/rest.js를 사용할 수 있게 제공해주는 것이 GitHub Script이다. 

 

const query = `query($owner:String!, $name:String!) {
    repository(owner: $owner, name: $name) {
        pullRequests(last: 100, states: OPEN) {
            edges {
                node {
                    number
                    headRefName
                    baseRefName
                    author {
                        login
                    }
                    repository {
                        name
                    }
                    mergeable
                }
            }
        }
    }
}`

github에서 PR을 가지고 올 때 사용하는 Query이다. 

graphQL을 사용하기 때문에 일반적인 MySQL 등의 Query와는 다른 형태이다.

 

const variables = {
    owner: context.repo.owner,
    name: context.repo.repo
}

context는 github-script에서 제공하는 Workflow 컨텍스트를 포함하는 객체이다. 

 

const FEATURE = "feat/"

const { repository: {pullRequests: {edges: list}}} = await github.graphql(query, variables)
for (let { node } of list) {
    if (!node.headRefName.includes(FEATURE)) continue;
    try { 
        if (node.mergeable === "CONFLICTING") {
            console.log(`Requesting changes for PR: ${node.number} by ${node.author.login}`);
            await github.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: node.number,
                body: "이 PR에 충돌이 발생했습니다. 충돌을 해결해주세요."
            });
        } else if (node.mergeable === "MERGEABLE") {
            console.log(`Merging PR: ${node.number} by ${node.author.login}`);
            await github.pulls.merge({
                owner: context.repo.owner,
                repo: context.repo.repo,
                pull_number: node.number,
            });
        } 
    } catch (e) {
        console.log(`Error handling PR ${node.number}`, e);
    }
}

핵심적인 부분으로 feat/ 키워드가 포함된 브랜치의 PR만 Merge 시켜주고 그 외 PR은 continue로 넘어간다. 

여기서 node에는 3개의 속성이 존재한다. 

  • MERGEABLE : PR이 충돌 없이 병합 가능함을 의미한다. 
  • CONFLICTING : PR이 충돌로 인해 병합이 불가능함을 의미한다. 
  • UNKNOWN : PR의 병합 가능 상태를 결정할 수 없음을 나타낸다. 보통 GitHub에서 병합 가능 여부를 계산하는 중에 
                          이 상태가 될 수 있다. 

충돌이 발생한 경우 issues를 사용해서 충돌 알림 댓글을 작성하고 그렇지 않다면 병합시켜준다. 

관련 기능은 링크를 보면 확인할 수 있다. 

 

이렇게 매주 월~금 10시에 PR을 Auto Merge해주는 코드를 작성할 수 있었다. 

반응형

'개발정보' 카테고리의 다른 글

GitHub Action Lighthouse  (5) 2024.09.18
GitHub Action PR 검사  (1) 2024.09.14
GitHub Action 알아보기  (2) 2024.08.28
VSCode Git 계정 변경  (0) 2023.04.13
소프트웨어 개발 3대 원칙 - KISS, YAGNI, DRY  (0) 2023.01.26