티스토리 뷰

업무를 하다보면 반복되는 일들이 발생하는데, 이러한 부분들은 자동화가 필요하다. 업무의 효율 측면에서도 그렇지만 이따금씩 발생하는 휴먼에러(단순한 일일 수록 집중력이 떨어져서 휴먼 에러도 자주 발생 🥲) 때문에라도 자동화로 전환하는 것이 좋은 방법이라고 생각한다.

최근에 자동화 업무를 하면서, 제공되는 API가 생각보다 굉장히 다양하다는 것을 알게되었다. 가장 많이 사용했던 것은 slack api이었긴 하지만 이번에 github api도 사용하면서 익숙하지 않은 용어들이 많이 보여서 정리를 해보는게 좋겠다는 생각이 들었다.

우리 팀 업무의 경우에는 각 MS DB에서 발생한 변경 사항에 따라서 대응할 작업들이 있는데, 반복적인 작업이라 이 부분을 최근에 자동화 하려는 작업을 하고 있다. 변경 사항에 따라서 자동으로 수정할 부분을 작성하고 PR을 올리면 사람이 PR 확인만 하면 될 수 있도록 하는게 1차적인 목표이고 최종적으로는 올라간 PR을 자동으로 테스트하고 머지까지 되어도 문제가 없도록 하는 것이 가장 이상적일 것 같긴 하다.

GitHub REST API

Github REST API docs를 보면 굉장히 많은 API들이 제공되고 있다는 것을 알 수 있다. 그 중 내가 필요했던 프로세스는 feature branch 생성 -> 내용 작성 -> PR 생성이다.

사전준비

  • GitHub API 호출에 필요한 Token이 필요하다. classic, fine-grained 모두 상관은 없다. fine-grained 사용시 적절한 권한을 부여하지 않으면 API 호출시 에러가 발생할 수 있다. 권한 관련 문서에서 필요한 권한을 확인할 수 있다.

1. Feature Branch 생성

Git Ref?
Git의 객체들은 모두 hash 값을 사용하여 다루게 되는데, 매번 필요할때마다 이 hash들을 기억하기는 쉽지 않으므로 commit object들의 hash들이 저장된 파일을 제공하는데 이를 git reference(.git/refs)라고 한다. branch는 새로운 commit hash를 저장한 git reference를 의미한다.

  • [GET] Get a reference
    /repos/{owner}/{repo}/git/ref/{ref}에서 주의할 점은 {ref}에 main만 넣으면 되는 것이 아니라 heads/main을 넣어야 한다. (.git/refs를 확인해보면 그 안에 heads/main으로 존재하기 때문이다.)
  • [POST] Create a reference
  • Body Parameter
    • ref: 생성할 브랜치 이름(ex. refs/heads/{branch_name})
    • sha: get reference에서 호출했을때 결과로 나온 sha

Get a reference으로 기준이 되는 브랜치의 sha 값을 가져오고 Create a reference 단계에서 해당 sha 값을 파라미터로 넣어준다.
main 브랜치를 기준으로 feature-1이라는 브랜치를 생성하려고 한다면 아래와 같은 과정이 된다.

headers = {"Authorization": f"token {github_token}"}
new_branch_name = 'feature-1'
url = "https://api.github.com/repos/HyunlangBan/my-repo/git/ref/heads/main"
branch = requests.get(url, headers=headers).json()
branch_sha = branch['object']['sha']
res = requests.post('https://api.github.com/repos/HyunlangBan/my-repo/git/refs', json={
    "ref": f"refs/heads/{new_branch_name}",
    "sha": branch_sha
}, headers=headers)

2. Tree 생성 (파일 작성)

Git Blob?
먼저 blob은 Binary Large Object를 의미하며 바이너리 데이터를 저장하는 데이터 타입이다. Git blob은 레포의 파일의 내용을 저장하는 타입으로 사용되며 파일의 sha-1 해시 값을 blob object에 함께 저장하여 이 해시값을 사용해서 github에 저장된 blob 객체를 읽고 쓸 수 있도록 해준다.

  • [POST] create a blob
  • Body Paramter
    • content: 작성이 필요한 내용
    • encoding: utf-8로 설정함
      url = 'https://api.github.com/repos/HyunlangBan/my-repo/git/blobs'
      res = requests.post(url, json={
      "content": "print(\"Hello, World!\")",
      "encoding": "utf-8"
      }, headers=headers).json()
      blob_sha = res['sha']

위에서 추가된 내용을 포함하여 Git tree를 생성해주어야 한다.

Git Tree?
유닉스의 디렉토리에 대응되며 Git 트리 개체를 사용하여 디렉터리와 디렉터리에 포함된 파일 간의 관계를 만들 수 있다.

먼저 기준이 될 base tree의 sha 값이 필요하다.

  • [GET] Get a tree
    글의 처음에서 생성해주었던 branch를 base로 설정할 것이므로 /repos/{owner}/{repo}/git/trees/{tree_sha}의 {tree_sha} 부분을 feature-1으로 변경해준다.
branch_name = "feature-1"
url = 'https://api.github.com/repos/HyunlangBan/my-repo/git/trees/feature-1'
base_tree = requests.get(url, headers=headers).json()
print(f'get_base_tree: {base_tree}')
base_tree_sha = base_tree['sha']
  • [POST] Create a tree
  • Body Parameter
    • tree: tree 구조를 정의
      • path: repo 상에서의 파일의 경로
      • mode: file mode. 파일인 경우 100644로 설정
      • type: Either blob, tree, or commit.
      • sha: 트리 내의 객체의 sha 값. 위에서 생성했던 blob의 sha 값을 넣어주면 된다.
    • base_tree: 새로운 트리의 base가 될 기존 tree의 sha(위에서 얻은 base tree의 sha값)
file_path = "test/hello.py"
body = {
        "tree": [
            # 파일 여러개 추가시 파일 개수 만큼 하위 dict를 추가
            {
                "path": file_path,
                "mode": "100644",  # 100644 for file (blob), 100755 for executable (blob), 040000 for subdirectory (tree), 160000 for submodule (commit), or 120000 for a blob that specifies the path of a symlink.
                "type": "blob",
                "sha": blob_sha
            }
        ],
        "base_tree": base_tree_sha
    }
url = 'https://api.github.com/repos/HyunlangBan/my-repo/git/trees'
tree = requests.post(url, json=body, headers=headers).json()
tree_sha = tree['sha']

3. Commit

Git Commit?
Git 리포지토리의 계층 구조(Git 트리) 및 파일 내용(Git BLOB)의 스냅샷

  • [POST] Create a commit
    위에서 생성한 tree에 commit을 추가한다.
  • Body Parameter
    • message: commit message
    • tree: tree sha
    • parents: base tree의 sha
url = 'https://api.github.com/repos/HyunlangBan/my-repo/git/commits'
commit = requests.post(url, json={
    "message": "my commit message",
    "tree": tree_sha,
    "parents": [base_tree_sha]
}, headers=headers).json()
commit_sha = commit['sha']

다음으로는 생성해두었던 feature-1 브랜치의 ref에 위의 commit으로 업데이트를 해주어야 한다.

branch_name = "feature-1"
url = f'https://api.github.com/repos/HyunlangBan/my-repo/git/refs/heads/{branch_name}'
ref = requests.patch(url, json={
    "sha": commit_sha
}, headers=headers).json()

4. Pull Request 생성하기

  • [POST]Create a Pull Request
  • Body Parameter
    • title: PR 타이틀
    • body: PR 내용
    • head: 수정이 적용되어야 할 브랜치의 이름 (feature-1)
    • base: 수정된 내용을 당겨갈 브랜치의 이름
      • headbase는 PR 생성할때 이 부분을 생각해보면 된다.
res = requests.post('https://api.github.com/repos/HyunlangBan/my-repo/pulls', json={
    "title": "Test Pull Request",
    "body": "Please pull these awesome changes in!",
    "head": "feature-1",
    "base": "main"
}, headers=headers).json()

결과

위의 일련의 과정을 수행하게 되면 Pull Request가 성공적으로 생성된 것을 확인할 수 있다.


참고🔍

https://docs.github.com/en/rest
https://siddharthav.medium.com/push-multiple-files-under-a-single-commit-through-github-api-f1a5b0b283ae
https://aboullaite.me/deep-dive-into-git-git-refs/
https://git-scm.com/book/ko/v2/Git%EC%9D%98-%EB%82%B4%EB%B6%80-Git-%EA%B0%9C%EC%B2%B4

댓글