본문 바로가기

2023년/ETC💁‍♂️

[github] github에서 github actions로 ReadMe.md 자동으로 수정되게 하는 방법 (feat. 백준)

github actionspython을 통해서 repository안의 readme를 push 할때마다 자동으로 업데이트 되도록 설계하는 방법

 

동작과정

프로그램 동작원리는 위와 같다

  • 소스코드를 깃허브에 push 하게 되면 github actions 에서 변경을 감지하고 python 소스코드를 동작하게 된다.
  • 파이썬으로 repository 각종 디렉토리를 탐색하게되고 조건문에 해당되는 데이터들을 자료구조에 저장한다.
  • 저장된 데이터를 통해 ReadMe를 새로 작성하게 되고 기존 repo에 덮어쓰기된다.

 

 

결과화면

각 주에 몇문제, 총 푼 문제수, 세부적으로 어떤문제를 풀었는지 update 된다.

 

코드 설명 

전체적인 소스코드는 아래에 남겼으니 그 부분을 참고하시면 되겠습니다.

세부적으로 하나씩 리뷰하겠습니다.

gitactions setting 방법

레포지토리에서 workflow를 새로 만들어준다
새로운 workflow를 생성한다.

아래 패키지로 생성해도 소스만 바꿔도 무방하다.

빨간 네모난 칸에 소스를 입력하자

  1. 파이썬 버전을 다운하고
  2. python 스크립트를 실행한다.
  3. 실행된 파일을 git add 하고 commit하고 push

actions에서 내용을 확인 할 수 있습니다
나중에 참고할때 빨간 네모를 확인하시면 py 파일이 어떻게 돌아가는지 확인 할 수 있습니다.

python 소스

디렉토리 구조는 script/ 안에 위치한다.

 내부의 README와 data-problems는 dummy 파일이니까 상관 없다.

 

 

문제 클래스

객체 지향 공부를 하다보니 파이썬을 객체지향적으로 접근하기엔 다소 어려움이 있었습니다.

객체 반복하려면 iter 재정의한다던지, next 구문 작성해야된다던지 등등 파이썬을 잘 모르는 저에게 접근하는 방법이 많이 어려웠습니다. 그래서 기존 자료구조에 append 하는 방식으로 구현했습니다.

문제 클래스는 week, day, filename, address 를 가지고  `void__str__()` 메소드에 맞은 양식으로 출력하게됩니다.

id 같은 경우는 인덱스 용도로 사용하려 했으나 for 문 안에서 index를 불러 올 수 있는 기능을 찾아 사용하지 않습니다.

 

디렉토리 탐색 메소드

전역변수 dic(dictionary)매개변수 problems(list)를 받고 있습니다

global dic 은 주간 문제 푼 횟수를 key : value 타입으로 저장하고 있습니다 ( ex. week1 : 3 ) 자바로 보면 Map<String, Integer>

매개변수로 받은 list는 OS가 .java 파일에 접근할 때마다 Problem 객체를 만들어 list 에 저장하게 됩니다.

os.walk(dir) 를 통해서 해당 주소의 디렉토리와 파일들에 접근하게 됩니다.

print 결과를 보면 각 디렉토리별 .java 파일에 접근함을 볼 수 있습니다

 if ext == '.java':
                    print("%s %s" % (root, filename))
                    split_dir = root.split("/")
                    address = root.replace("../","")
                    address += "/"+filename
                    if len(split_dir) >= 4:
                        week = str(split_dir[2])
                        dic_key = dic.get(week)
                        if dic_key is None:
                            dic[week] = 1
                        else:
                            dic[week] = dic.get(week) + 1
                        day = str(split_dir[3])
                        filename = str(filename)
                        if week and day and filename:
                            problems.append(Problem(str(value),week,day,filename,address))
                            value += 1

조건문안에서 root 를 출력했을때 위 ../src/week/day 순으로 문자열이 나타나게되고

각각 split 을 통해서 배열 size 가 4인 문자열 split을 추려냈습니다.

`if len(split_dir) >= 4` 배열 size가 4 미만일 경우 indexOutOfError 가 발생 할 수 있기 때문에 

디렉토리 규칙에 따른 src/ week / day 순으로 데이터 파일을 저장하도록 합시다

split 을 할 경우 해당 java 파일의 주소에서 문자열의 2번째와 3번째에는 week 와 day 의 대한 정보가 추출되게됩니다.

dictionary 자료구조에서(dic)   key는 weekN 으로 저장하고 value는 없으면 1을 있으면 해당 value값을 가져와서 1을 추가하게 됩니다. 이는 주간 몇문제 풀었는지에 대한 정보가 담기게 됩니다

`if week and day and filename`  만약 week, day, filename 에 대한 정보가 있을 경우

list 자료구조인 (problems)에 Problem 객체를 생성해서 리스트에 append 하게 됩니다. 

value 같은 경우는 id 값을 넣어주려 했으나 사용하진 않습니다

 

 

readMe 만드는 메소드

def make_info_header(dic):
    info = f"| # | week | day |\n"
    info += f"|---|---|---| \n"
    for index in range(0,len(dic)):
        info += f"| {index+1} | {dic[index][0]} | {dic[index][1]} | \n"
    print(info)
    return info


def make_info_data(problems):
    info = f"### 총 푼 문제수 = {len(problems)} 🎉\n\n"
    info += f"| # | week | day | problem |\n| ------------- | ------------- | ------------- | ------------- |\n"
    for index in range( 0, len(problems)):
        temp = f"| {index+1} {problems[index]}"
        info += temp

    info += """"""
    return info

dictionary(dic)와 list(problems) 자료구조를 받아 반복문을 돌리면서 readMe의 문자열을 완성시킵니다.

make_info_header에는 주간별로 몇문제 풀었는지

make_info_data에는 총 푼 문제수와 문제 내용들이 저장되게 됩니다.

 

메인 메소드

if __name__ == "__main__":
    problems = []
    personal_dir = "../src/"
    print_files_in_dir(personal_dir, "",problems)
    projects = sorted(problems, key=attrgetter('week','day'),reverse=False)
    sorted_dic = sorted(dic.items(),key= lambda item: item[0],reverse= False)
    
    info = make_info_data(projects)
    header = make_info_header(sorted_dic)

    with open("../README.md", 'w', encoding='utf-8') as f:
        f.write(title_project + "\n")
        f.write(sub_project + "\n")
        f.write(header + "\n")
        f.write(info)
        f.close()

problems의 초기화 그리고 탐색할 디렉토리의 초기값

메소드를 호출해서 정보들을 자료구조에 담습니다

객체를 정렬하려고 하다보니 비교에 있어서 모호한 부분이 있어 단계정렬을 위해 attrgetter 라이브러리를 활용하여 전체 문제에 대한 정보들을 week - day 순으로 정렬 했스빈다.

주간 푼 문제수 같은 경우도 마찬가지로 단계정렬은 아니고 sorted 메소드 규격에 맞게 람다식으로 정렬을 구현했습니다.

이후 해당 데이터들로 파일처리를 진행하고 github acitons 에서 commit 하게 됩니다.

 

마주친 에러

  • 정렬문제

객체들을 정렬할 수 없으니 sorted 함수와 라이브러리를 활용

https://wikidocs.net/109327

 

034 다양한 기준으로 정렬하려면? ― operator.itemgetter

operator.itemgetter는 주로 sorted와 같은 함수의 key 매개변수에 적용하여 다양한 기준으로 정렬할 수 있도록 하는 모듈이다. ## 문제 학생의 이름, …

wikidocs.net

  • 파일시스템 탐색 문제 

os.walk 사용방법

https://codechacha.com/ko/python-walk-files/

 

Python - os.walk()를 사용하여 디렉토리, 파일 탐색

os.listdir() 또는 os.walk()를 사용하면 어떤 경로의 모든 하위 디렉토리를 탐색할 수 있습니다. os.walk()는 기본적으로 top-down 방식으로 상위 폴더부터 출력하며 bottom-up 방향으로 출력하고 싶다면 topdo

codechacha.com

 

 

리팩토링 및 발전방향

  • 다른 문제푸는 사이트와 연동하고 싶다.

파일명이나 디렉토리명으로 분류해서 백준, 프로그래머스, SWEA 등등 다양한 플랫폼에서 푼 문제들을 정렬해보기

 

  • github organization 에서 팀원들과 함께하는 레포지토리 안에서 몇문제 풀었는지 확인하는 시스템 구현하기

추후 반영

 

  • 클린코드 작성

변수도 난잡하게 사용하고 있고 파이썬에 대한 문법도 모르고 있어서 클린코드라고 말할 순 없을 것 같다.

남들이 보기에 쉽고 이용할 수 있는 코드를 짤 수있도록 추 후 리팩토링

 

주의사항

  • 디렉토리 구조

해당 소스를 잘 보면 데이터 추출 과정에서 split을 통해서 각 디렉토리 명들이 String 배열에 담기게된다.

디렉토리 구조를 src/week/day/.java 순으로 만들지 않는다면 제대로 된 조회를 할 수 없게 된다.

따라서 파이썬 라이브러리들을 더 찾아봐서 주소디렉토리를 추출 할 수 있는 기능을 만들면 좋을 듯 하다.

 

소스코드

더보기

update-readme.py

import os
from operator import itemgetter, attrgetter

title_project = "# EveryDay - Practice"

sub_project = "### push 후 자동으로 README 수정 기능"

dic = {}

class Problem:
    def __init__(self,id, week, day, filename, address):
        self.id =id
        self.week = week
        self.day = day
        self.filename = filename
        self.address = address
    def get_week(self):
        return self.week
    def get_day(self):
        return self.day
    def get_filename(self):
        return self.filename
    def __str__(self) -> str:
        return " | " + self.week + " | " + self.day + " | [" + self.filename + "](" + self.address + ")"  + "|\n"


def print_files_in_dir(root_dir, prefix ,problems):
    value = 1
    global dic
    try:
        for (root, dirs, files) in os.walk(root_dir):
            for filename in files:
                ext = os.path.splitext(filename)[-1]
                if ext == '.java':
                    print("%s %s" % (root, filename))
                    split_dir = root.split("/")
                    address = root.replace("../","")
                    address += "/"+filename
                    if len(split_dir) >= 4:
                        week = str(split_dir[2])
                        dic_key = dic.get(week)
                        if dic_key is None:
                            dic[week] = 1
                        else:
                            dic[week] = dic.get(week) + 1
                        day = str(split_dir[3])
                        filename = str(filename)
                        if week and day and filename:
                            problems.append(Problem(str(value),week,day,filename,address))
                            value += 1
    except PermissionError:
        pass


def make_info_header(dic):
    info = f"| # | week | day |\n"
    info += f"|---|---|---| \n"
    for index in range(0,len(dic)):
        info += f"| {index+1} | {dic[index][0]} | {dic[index][1]} | \n"
    print(info)
    return info


def make_info_data(problems):
    info = f"### 총 푼 문제수 = {len(problems)} 🎉\n\n"
    info += f"| # | week | day | problem |\n| ------------- | ------------- | ------------- | ------------- |\n"
    for index in range( 0, len(problems)):
        temp = f"| {index+1} {problems[index]}"
        info += temp

    info += """"""
    return info


if __name__ == "__main__":
    problems = []
    personal_dir = "../src/"
    print_files_in_dir(personal_dir, "",problems)
    projects = sorted(problems, key=attrgetter('week','day'),reverse=False)
    sorted_dic = sorted(dic.items(),key= lambda item: item[0],reverse= False)
    
    info = make_info_data(projects)
    header = make_info_header(sorted_dic)

    with open("../README.md", 'w', encoding='utf-8') as f:
        f.write(title_project + "\n")
        f.write(sub_project + "\n")
        f.write(header + "\n")
        f.write(info)
        f.close()

update-readme.yml

name: Update readme

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - name: Set up Python 3.10
      uses: actions/setup-python@v3
      with:
        python-version: "3.10"
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
    - name: Run update-readme.py
      working-directory: script
      run: |
        python update-readme.py
    - name: Commit changes
      run: |
        git config --global user.name 'userName'
        git config --global user.email 'userId@gmail.com'
        git add -A
        git commit -am "auto-update README.md"
    - name: Push changes
      run: |
        git push

 

참고

git repository

https://github.com/rnrudejr9/every_practice/tree/master

 

GitHub - rnrudejr9/every_practice: 매일매일 공부하는 습관

매일매일 공부하는 습관. Contribute to rnrudejr9/every_practice development by creating an account on GitHub.

github.com