Post

PS 자동화: 풀고 Push 하면 끝!

GitHub Actions로 알고리즘 문서화 자동화 하기

PS 자동화: 풀고 Push 하면 끝!

들어가며

알고리즘 문제 풀이를 본격적으로 다시 시작하면서, 기존 Algorithm 레포지토리를 어떻게 하면 효과적으로 활용할 수 있을지 고민했다. 자동화를 통해 효율을 높이고 싶었고 그 과정에서 겪은 문제와 해결 방법을 기록해보려 한다.

기존의 방법과 한계

alt text alt text

기존에는 LeetHub를 통해 자동 푸시되는 레포와 일반 Algorithm 레포, 두 개를 운영하고 있었다. 하지만 이렇게 운영하면서 몇 가지 한계를 느꼈다.

  1. 각 플랫폼(백준, 프로그래머스, LeetCode)의 문제들이 한 곳으로 모이지 않음
  2. 접근 방식을 함께 기록하고 싶지만, 코드 푸시 후 따로 README를 작성해야 하는 번거로움
  3. README와 코드의 분리로 인한 문서화 피로

가장 큰 문제는 코드와 문서의 분리, 그리고 메인 README로 옮기는 작업이었다.

쉽게 말하면 문제 풀이 코드, 접근 방식을 담은 README, 이 README들이 리스트업된 메인 README — 이 세 개가 독립적으로 존재하다 보니 이론상 총 3번의 푸시가 필요했다. 이렇게 되니 학습 효율도 떨어졌고, 이를 GitHub Actions로 해결해보기로 했다.

개선 방안

그렇다면 github actions로 이를 어떻게 해결할 수 있을까? 제일 효율적으로 레포를 운영하는 방식은 다음과 같다:

그렇다면 GitHub Actions로 이를 어떻게 해결할 수 있을까? 가장 효율적인 운영 방식은 다음과 같다:

문제 패키지 생성 → 코드 작성 후 주석으로 문제 링크, 접근 방식, 카테고리 작성 → 푸시하면 자동으로 README 생성

구현 방법

일단 기존에 있었던 패키지들은 ver1으로 옮기고 ver2에서 새롭게 시작해보았다.

GitHub Actions 워크플로우

GitHub Actions는 레포지토리에서 특정 이벤트가 발생하면 자동으로 스크립트를 실행해주는 기능이다. .github/workflows/ 폴더에 yml 파일을 만들면 GitHub이 자동으로 인식한다.

1
2
3
4
5
6
7
name: Update README

on:
  push:
    branches: [main]
    paths:
      - 'src/ver2/**'

on 블록에서 언제 실행할지 정의한다. 위 설정은 main 브랜치에 푸시할 때, src/ver2/ 경로에 변경이 있을 때만 실행된다는 의미다.

1
2
3
4
5
6
jobs:
  update-readme:
    runs-on: ubuntu-latest
    
    permissions:
      contents: write

runs-on은 실행 환경이다. GitHub이 제공하는 우분투 가상 머신에서 돌아간다. permissions: contents: write는 레포에 커밋/푸시할 권한을 부여하는 설정이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    steps:
      - name: code checkout
        uses: actions/checkout@v4
          
      - name: run update_readme.py
        run: python scripts/update_readme.py
        
      - name: commit
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add .
          git diff --staged --quiet || git commit -m "docs: update README.md"
          git push

steps는 순서대로 실행되는 작업들이다.

  1. actions/checkout@v4: 레포 코드를 가상 머신으로 가져온다
  2. python scripts/update_readme.py: Python 스크립트를 실행한다
  3. 변경사항이 있으면 자동으로 커밋하고 푸시한다

git diff --staged --quiet || git commit은 변경사항이 있을 때만 커밋하는 트릭이다. 변경이 없으면 커밋을 스킵해서 에러를 방지한다.

Python 스크립트

Python 스크립트는 크게 세 가지 역할을 한다.

  1. Java 파일에서 주석 블록 파싱
  2. 문제별 README.md 생성
  3. 메인 README.md 생성 (카테고리별 분류)

주석 파싱

1
2
3
4
5
6
def parse_problem_file(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # /* */ 블록 안의 내용 추출
    comment_match = re.search(r'/\*\s*([\s\S]*?)\s*\*/', content)

정규식으로 /* */ 주석 블록을 찾는다. [\s\S]*?는 줄바꿈 포함 모든 문자를 매칭한다.

1
2
3
    # 카테고리 파싱
    category_match = re.search(r'# 카테고리\s*\n([^\n#]+)', comment)
    categories = [c.strip() for c in category_match.group(1).split(',')]

# 카테고리 다음 줄을 찾아서 쉼표로 분리한다. 투 포인터, 큐처럼 다중 카테고리도 처리된다.

문제별 README 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def generate_problem_readme(problem_name, metadata):
    categories_str = ' '.join([f'`{c}`' for c in metadata['categories']])
    
    return f"""# {problem_name}

## 문제 링크
{metadata['problem_link']}

## 카테고리
{categories_str}

## 접근 방식
{metadata['approach']}

## 코드
```java
{metadata['code']}
```
"""

파싱한 데이터로 마크다운 형식의 README를 생성한다. 카테고리는 백틱으로 감싸서 태그처럼 보이게 했다.

메인 README 생성

1
2
3
4
5
6
7
8
9
10
11
12
def generate_main_readme(problems_by_category):
    content = "# 알고리즘 풀이\n\n"
    
    for category in sorted(problems_by_category.keys()):
        content += f"## {category}\n"
        
        for problem in problems_by_category[category]:
            content += f"- [{problem['name']}]({problem['path']})\n"
        
        content += "\n"
    
    return content

카테고리별로 문제를 분류해서 리스트 형태로 만든다. 다중 카테고리인 경우 각 카테고리에 중복 등록된다.

테스트 Leet_54_SpiralMatrix

Leetcode에서 문제를 풀고 그 코드 밑에 다음의 형식에 맞게 작성하고 push만 하면 된다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package ver2.Leet_54_SpiralMatrix;

import java.util.ArrayList;
import java.util.List;

class Solution {
    int[] dx = {0,1,0,-1};
    int[] dy = {1,0,-1,0};

    public List<Integer> spiralOrder(int[][] matrix) {
        int max = matrix[0].length * matrix.length;
        int cx = 0;
        int cy = 0;
        int dir = 0;
        int n = matrix.length;
        int m = matrix[0].length;

        boolean[][] visited = new boolean[n][m];


        List<Integer> ans = new ArrayList<>();

        while(max -- > 0){
            ans.add(matrix[cx][cy]);

            visited[cx][cy] = true;

            if(cx + dx[dir] < 0
                    || cx + dx[dir] >= n
                    || cy + dy[dir] < 0
                    || cy + dy[dir] >= m||
                    visited[cx + dx[dir]][cy + dy[dir]]){
                dir++;

                dir = dir % 4;
            }

            cx += dx[dir];
            cy += dy[dir];
        }
        return ans;
    }
}

/*
# 카테고리
2차원 배열

# 접근 방식
방문을 처리하기 위한 visited 2차원 배열을 설정하여 방향전환을 할 수 있게 구현하였다.

# 문제 링크
https://leetcode.com/problems/spiral-matrix/?envType=problem-list-v2&envId=array
 */

push 하게되면 actions에서 감지해서 작성했던 워크플로우와 scripts를 자동으로 실행하는 모습을 볼 수 있다.

alt text

alt text alt text

문제의 readme와 메인 readme에도 정상적으로 작성이 된 것을 확인할 수 있다.

마치며

복잡한 3단계의 흐름을 한 번의 push 형태로 바꿀 수 있어서 뿌듯한 작업이었다. 활용해서 알고리즘 공부도 다시 열심히 시작해봐야할듯 하다.

ref

GitHub Actions Docs

This post is licensed under CC BY 4.0 by the author.