티스토리 뷰

오늘은 Github issue 내용을 Github 파일로 동기화 시키는 Github action 개발기를 이야기 해볼까 해요.

오늘 이야기 할 Github action은 Marketplace에서 확인 및 사용할 수 있어요.

 

Issue to File Sync - GitHub Marketplace

Sync GitHub issues to files with customizable paths and labels

github.com

 


개발 동기

 

개발자들은 Github repository에 문서를 모아두기도 하고, 블로그를 운영하며 글을 작성해두기도 해요.

저 또한 마찬가지였어요. 문서를 정리해 Github repository에 올려두기도 하고

(지금은 미사용 중이긴 하지만) github.io 블로그가 있어 해당 글을 작성할 때 로컬에서 작성해 repository에 올려두기도 했죠.

 

이럴 때마다 몇 가지 불편한 점이 있었어요.

로컬에서 직접 IDE 혹은 Text editor로 ‘작성 > 저장’ 후 Git add/commit/push 과정을 수행해야 됬어요.

이미지 파일을 첨부하고 싶은 경우, 직접 추가해서 경로를 문서에 입력해줘야했어요.
(vscode 같은 IDE를 이용한다면 plugin의 도움을 받을 수 있지만, 가끔 오류가 발생할 때마다 스트레스를 받았어요)

로컬에서 작성한 문서의 Preview가 Github에 올라갔을 때 차이가 발생했어요.
수정을 하고자 하면 다시 add/commit/push를 수행해야 했죠.

 

그래서 문서를 Repository에 직접 올리는 것 대신, Github issue를 활용해보는 방법을 고민하게 되었어요.

맞아요, 사실 Github issue는 Github repository에 대해서 문제가 발생하거나,
추가할 기능이 있는 등 개발적으로 필요한 사항에 대해 이야기하고, 이를 Pull Request로 해결해가는 용도긴 하죠.

하지만, 제가 일하고 있는 곳에서는 기술 문서화/공유 용도로 Github issue를 사용하고 있었고,
문서 관련 Repository에서는 실제 이슈 트래킹 용도의 Github issue 기능이 거의 필요하지 않았어요.

 

그래서 Github issue로 작성한 글을 바로 파일로 동기화 시켜서 Repository에 추가해보자 라고 생각하게 됬습니다.
이 방식이라면 위에서 문제 삼았던 부분들을 다 해결할 수 있을 것 같았어요.

add/commit/push의 귀찮음에서 해방 될 수 있고, 이미지도 Github에 올라간 이미지 링크로 바로 참조가 가능하겠죠.
Issue Preview를 통해 보여지는 형태를 미리 살펴볼 수도 있을거에요.

 


개발하기

Github action을 개발한 경험은 조금 있었기에 일단 목적에 맞는 action 프로세스를 구상해봤어요.

  1. issue를 작성했나?
  2. issue의 글을 가져온다.
  3. 글을 파일로 만들어 repository에 commit/push 한다.
  4. push 후 문제 없으면 issue에 완료 comment를 추가한다.

간단하죠?
근데 위 방식으로 진행하면, 아직 완성되지 않은 글이 바로 동기화 될 수 있는 문제가 있어요.
그리고 동기화 시키고 싶지 않은 issue 임에도 무조건 동기화 될 수 밖에 없죠.

 

그래서 label을 이용하기로 했어요.

특정 label이 추가된 issue만 동기화를 시키는거죠. 그럼 위 문제를 해결할 수 있겠죠?

 

그래서 수정한 최종 파이프라인은 아래와 같아요.

  1. issue에 label이 달렸나? => 그 label이 특정 label인가?
  2. issue의 글을 가져온다.
  3. 글을 파일로 만들어 repository의 특정 위치에 commit/push 한다.
  4. push 후 문제 없으면 issue에 완료 comment를 추가한다.

그리고 반대 케이스로 'label이 제거된 경우 파일제거', 'issue가 제거된 경우 파일 제거' 경우도 고려해서 다른 Job으로 추가했죠.

 


Label 추가/제거 이벤트 받기

Trigger 조건

먼저 issue를 통해 트리거 받을 수 있는 조건들을 살펴봐야 해요.

 

워크플로를 트리거하는 이벤트 - GitHub Docs

GitHub에 대한 특정 작업이 예약된 시간에 발생하거나 GitHub 외부의 이벤트가 발생할 때 실행되도록 워크플로를 구성할 수 있습니다.

docs.github.com

 

여러 옵션들이 제공하지만,
저희 목적에 맞는 것은 labeled (레이블 추가) , unlabeled (레이블 제거), deleted (이슈 제거) 이렇게 총 3가지 이벤트였어요.

on:
  issues:
    types: [labeled, unlabeled, deleted]

 

 

Label 이름 포함 조건

특정 조건의 Label을 확인할 수 있어야 하니 Label 이름을 파악할 수 있어야 했어요.
이는 github.event.label context에서 제공되고 있어요. 예시를 github docs에서 확인할 수 있죠.

 

워크플로 트리거 - GitHub Docs

GitHub Actions 워크플로를 자동으로 트리거하는 방법

docs.github.com

 

저는 Label에 이모지를 추가 및 변경을 하고 싶어, 특정 이름의 label이 완벽히 같은지보단 느슨하게 포함 여부로 하기로 결정했어요.
이렇게 포함여부 연산을 지원하기 위해 github action에서는 contains 함수가 존재해서 이걸 이용하기로 했죠.

jobs:
  check-label-and-sync:
    # Label이 추가됬으면서, Label 이름에 'Ready to publish'이 포함되면 아래오는 Action을 수행
    if: github.event.action == 'labeled' && contains(github.event.label.name, 'Ready to publish')

 

참고로 github action에서는 여러 편의 함수들이 제공되요. 아래 주소를 참고해보세요.

 

Evaluate expressions in workflows and actions - GitHub Docs

You can use expressions to programmatically set environment variables in workflow files and access contexts. An expression can be any combination of literal values, references to a context, or functions. You can combine literals, context references, and fu

docs.github.com

 

 

Issue 가져와 파일로 동기화

위에서 계획한 파이프라인의 2~4번 작업은 개발로 작업해야 해요.

이를 위해 저는 python을 이용했어요.
javascript, go 등 다양한 언어들이 지원되는데, 그냥 제가 개발하기 편한 언어를 선택했어요.

구현 코드에 대해서도 상세히 설명하고 싶지만, 그냥 Python 언어 설명이 되는 것 같아서 생략할게요.
코드가 어렵지 않아서, 아래 코드와 주석만 봐도 충분히 이해가 되실거에요. (참 쉽죠?)

- name: Setup Python
  uses: actions/setup-python@v5
  with:
    python-version: '3.13'
    
- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
    pip install requests

- name: Sync file
  run: |
    import os
    import json
    import requests
    from datetime import datetime
    
    # GitHub API 설정
    GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
    GITHUB_API = "<https://api.github.com>"
    headers = {
        "Authorization": f"Bearer {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.v3+json"
    }
    
    # 이벤트 데이터 읽기
    event_path = os.getenv('GITHUB_EVENT_PATH')
    with open(event_path, 'r', encoding='utf-8') as f:
        event = json.load(f)
    
    # 이슈 정보 가져오기
    issue = event['issue']
    issue_number = issue['number']
    repo_full_name = event['repository']['full_name']
    title = issue['title']
    body = issue['body'] or ''
    
    # 현재 날짜 가져오기 (UTC 기준)
    current_date = datetime.utcnow().strftime('%Y-%m-%d')
    
    # 파일명 생성 (날짜-이슈제목.md)
    safe_title = title.lower().replace(' ', '-')

    # 파일명에 사용할 수 없는 문자 제거
    safe_title = ''.join(c for c in safe_title if c.isalnum() or c in '-_')
    
    # 디렉토리(_posts) 및 파일이름 지정
    filename = f"_posts/{current_date}-issue#{issue_number}-{safe_title}.md"
    
    # 디렉토리가 없으면 생성
    os.makedirs(os.path.dirname(filename), exist_ok=True)
    
    # 마크다운 파일 생성/수정
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(body)
    
    # Git 설정
    os.system('git config --global user.name "github-actions[bot]"')
    os.system('git config --global user.email "github-actions[bot]@users.noreply.github.com"')
    
    # 변경사항 커밋
    os.system(f'git add "{filename}"')
    os.system(f'git commit -m "sync: Update issue #{issue_number} to markdown"')
    os.system('git push')
    
    # 완료 코멘트 추가
    comments_url = f"{GITHUB_API}/repos/{repo_full_name}/issues/{issue_number}/comments"
    comment_data = {
        "body": "🚀 Publish complete 🌎"
    }
    requests.post(comments_url, headers=headers, json=comment_data)
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  shell: python


Label 제거 및 Issue 제거를 통한 동기화 파일 제거도 비슷해요.

자세한 내용은 아래를 참고해주세요.

더보기
remove-on-unlabel:
    if: github.event.action == 'unlabeled' && contains(github.event.label.name, 'Ready to publish')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install requests

      
      - name: Remove Markdown
        run: |
          import os
          import json
          import glob
          import requests
          
          # GitHub API 설정
          GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
          GITHUB_API = "https://api.github.com"
          headers = {
              "Authorization": f"Bearer {GITHUB_TOKEN}",
              "Accept": "application/vnd.github.v3+json"
          }
          
          # 이벤트 데이터 읽기
          event_path = os.getenv('GITHUB_EVENT_PATH')
          with open(event_path, 'r', encoding='utf-8') as f:
              event = json.load(f)
          
          issue_number = event['issue']['number']
          repo_full_name = event['repository']['full_name']
          # 해당 이슈 번호를 가진 파일 찾기
          files = glob.glob(f"_posts/*issue#{issue_number}-*.md")
          
          if files:
              # Git 설정
              os.system('git config --global user.name "github-actions[bot]"')
              os.system('git config --global user.email "github-actions[bot]@users.noreply.github.com"')
              
              # 파일 삭제 및 커밋
              for file in files:
                  os.remove(file)
                  os.system(f'git add "{file}"')
              
              os.system(f'git commit -m "sync: Remove markdown for issue #{issue_number} (label removed)"')
              os.system('git push')

              # 삭제 완료 코멘트 추가
              comments_url = f"{GITHUB_API}/repos/{repo_full_name}/issues/{issue_number}/comments"
              comment_data = {
                  "body": "🗑️ Markdown file has been removed 🗑️"
              }
              requests.post(comments_url, headers=headers, json=comment_data)
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: python

  delete-markdown:
    if: github.event.action == 'deleted'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install requests

      - name: Delete Markdown
        run: |
          import os
          import json
          import glob
          import requests
          
          # 이벤트 데이터 읽기
          event_path = os.getenv('GITHUB_EVENT_PATH')
          with open(event_path, 'r', encoding='utf-8') as f:
              event = json.load(f)
          
          issue_number = event['issue']['number']
          
          # 해당 이슈 번호를 가진 파일 찾기
          files = glob.glob(f"_posts/*issue#{issue_number}-*.md")
          
          if files:
              # Git 설정
              os.system('git config --global user.name "github-actions[bot]"')
              os.system('git config --global user.email "github-actions[bot]@users.noreply.github.com"')
              
              # 파일 삭제 및 커밋
              for file in files:
                  os.remove(file)
                  os.system(f'git add "{file}"')
              
              os.system(f'git commit -m "sync: Remove markdown for deleted issue #{issue_number}"')
              os.system('git push')
        shell: python

 

사용하기

이렇게 직접 Github action을 만들어서 활용해보았는데요.

아래와 같이 제대로 동작하는 것을 확인할 수 있었어요.

 

물론 삭제하는 것도 제대로 동작했어요.

 


Github action 배포하기

만들어놓고 나니, 다른 프로젝트에도 쉽게 적용할 수 있는 방안을 고민하기 시작했어요.

필요할 때마다 레포에 복사/붙여넣기 방식은 귀찮기도 하고,
디렉토리명 등 커스텀하게 수정이 필요할텐데 전체 코드를 보면서 수정하다보니 실수할 포인트들이 있었죠.

그래서 그냥 Custom Github Action으로 쉽게 사용 가능하도록 배포하고, Marketplace에 올려보고자 했어요.

 

Custom Github Action 만들기

사실 기존에 만든 Github Action을 Custom Github Action로 만드는 과정은 간단해요.

  • 배포할 Github Action에 대한 정보를 담는 actions.yml 작성이 필요해요. (docs)
  • 작성한 코드를 실행할 방법 선택 및 코드 구성이 필요해요.
    기본적으로 Dockerfile, Javascript, Composite 3가지 방법을 지원해요.
    요약하면 Javascript를 제외한 언어라면 Dockerfile을 통해서 구성해야 해요.

 

관련해 자세한 내용을 확인하고 싶다면, 아래 문서를 참고해주세요.

 

About custom actions - GitHub Docs

You can create actions by writing custom code that interacts with your repository in any way you'd like, including integrating with GitHub's APIs and any publicly available third-party API. For example, an action can publish npm modules, send SMS alerts wh

docs.github.com

 

actions.yml

actions.yml은 구현한 Github Action에 대한 메타데이터를 나타내요.

Github Action의 이름과 어떤 목적을 가지고 있는지, 어떤 input 데이터를 넣어야 하는지,
어떤 방식으로 동작하는지 등 정보를 담고 있어요.

저는 아래와 같이 작성했어요.

name: 'Issue to File Sync'
description: 'Sync GitHub issues to files with customizable paths and labels'
author: '@KimDoubleB'

inputs:
  github-token:
    description: 'GitHub token for API access'
    required: true
  output-dir:
    description: 'Directory path for files to be created'
    required: true
  trigger-label:
    description: 'Label that triggers the sync'
    required: true
    default: 'documentation'
  timezone:
    description: 'Timezone for date formatting'
    required: false
    default: 'Asia/Seoul'
  file-extension:
    description: 'File extension for output files (without dot)'
    required: false
    default: 'md'

runs:
  using: 'docker'
  image: 'Dockerfile'

branding:
  icon: 'refresh-cw'
  color: 'blue'
  • 기능을 확장하고자 파일 경로, 타임존, 파일 확장자를 받도록 추가했어요.
  • branding 부분에 icon과 color를 입력하지 않으면 marketplace에 배포할 수 없어요.

 

Code 및 Dockerfile

action.yml에서 Dockerfile을 이용하도록 설정했으니, 이젠 여기서부턴 개발자 자유에요.

소스에 맞게 Dockerfile을 구성해서, 동작하고자 하는 목적을 이루면 되는거죠.

저는 앞서 작성한 code를 python file(.py)로 추출하고, 이를 실행시킬 수 있는 Dockerfile로 구성했어요.

# Dockerfile
FROM python:3.9-slim

RUN apt-get update && \\
    apt-get install -y git && \\
    apt-get clean && \\
    rm -rf /var/lib/apt/lists/*

COPY requirements.txt /requirements.txt
RUN pip install -r /requirements.txt

COPY src /src
ENTRYPOINT ["python", "/src/main.py"]

 

배포 및 릴리즈

작성을 완료했다면, release하고 사용하기만 하면되요.
Release 섹션에 가면 아래와 같이 Github Marketplace에 배포할지 묻는 체크박스가 추가되어 있는 것을 볼 수 있어요.

배포를 원한다면 체크를 하고, Release를 하면 됩니다.

앗 참고로 배포에는 몇 가지 조건(README 작성 등)이 필요해요.
해당 조건들을 다 채우고 Release 하면, 아래와 같이 Marketplace에서 배포된 것을 확인할 수 있어요.

 

근데 사실 배포를 하지 않아도 다른 프로젝트에서 사용하는 것은 문제 되지 않아요.
Marketplace는 단순히 공유 용도로 사용된다는 점을 알아두세요.

대신 Release는 하는 편이 좋으니 꼭 해주세요.

 

사용하기

사용하기 위해선 정의한 Github action repository와 Release version을 명시하면 되요.

name: Sync Issues to Markdown

on:
  issues:
    types: [labeled, unlabeled, deleted]

jobs:
  sync-issues:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Sync Issue to File
        uses: KimDoubleB/issue-to-file-action@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          output-dir: '_posts'
          trigger-label: 'Ready to publish'
          timezone: 'Asia/Seoul'
          file-extension: 'md'

 

간단하죠~?
맨 처음에 직접 Github action에 모든 코드를 작성했을 때보다 훨씬 간결해진 것을 볼 수 있어요.


결론

이렇게 필요한 기능을 Github action을 작성해보고,
직접 다른 프로젝트에서 쉽게 활용할 수 있도록 Marketplace에 배포해 볼 수 있었어요.

그리고 직접 Github 블로그에 적용해서 블로그 글을 단순 issue에 작성하고 Label을 달아줌으로써 발행할 수 있었죠
(IDE를 켜지 않아도 되고, Add/Commit/Push를 하지 않아도 됬어요 🕺🏻).

Dockerfile로 구성이 가능하다는 것을 보셨다시피, Github action의 사용처는 무궁무진해요.
무언가 Github에서 트리거되어 동작하는 기능을 구현하고 싶었다면, Github action을 구성해보시는 것은 어떨까요?

긴 글 읽어주셔서 감사해요.

320x100
반응형
댓글
반응형
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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
글 보관함