🌏 개요
현재 Ncloud 서버를 무료 크레딧으로 임대하고 있고
파이썬, 쉘 스크립트를 기반으로 1일 1커밋 자동화 코드를 작성하여
서버에서 매일 23:30분에 해당 스크립트가 실행 되도록 cron에 올려 사용 중임
그런데 ncloud 크레딧 사용 기간이 2개월 정도 남아서 크레딧을 다 소진하거나 기간이 끝나면 서버를 그대로 두거나 종료하거나
둘 중 하나인데,
서버를 그대로 두자니 요금이 부담되어서 고민 중인 찰나에 깃허브 액션을 이용해서 돈 안들이고(?) 커밋을 자동화할 수 있지 않을까 하는 생각이 들어 깃헙 액션을 사용한 커밋 자동화를 차근차근 진행 해보도록 하겠음.
🤷♂️ 진행 순서
parsing.py
스크립트 작성블로그 글 파싱 기능 구현
parsing.yaml
워크플로우 작성- 해당 워크플로우는 파싱이 우분투 서버내에서 정상적으로 실행 되는지 테스트하는 용도임
- 파싱이 정상적으로 실행되면 해당 파일은 더 이상 사용되지 않음.
1
,2
번 진행 중 생기는 트러블슈팅 기록 작성test-parsing.py
스크립트 작성parsing.py
단위 테스트 스크립트
test-parsing.yaml
워크플로우 작성- 해당 워크플로우는 파싱 스크립트에 대한 단위 테스트용 워크플로우임
blog-post.sh
쉘 스크립트 작성
- 파싱 스크립트 실행 및 README.md 업데이트 하는 쉘 스크립트
commit.sh
쉘 스크립트 작성
- 변경사항 있는 파일들을 staging area에 add 및 commit 그리고 push 까지 하는 스크립트
- 최종적으로
blog-post.yaml
워크플로우 작성
📂 폴더 tree
일반 파일
$ tree .
.
├── OLD-README.md
├── README.md
├── contributions.svg
├── csv
│ └── parsing.csv
└── scripts
├── blog-post.sh
├── commit.sh
├── parsing.py
├── requirements.txt
└── test
├── __init__.py
└── test-parsing.py
.github/
tree .github/
.github/
└── workflows
├── blog-post.yaml
├── parsing.yaml
└── test-parsing.yaml
✍️ 파싱 스크립트 작성
parsing.py
blog-post.sh
commit.sh
test-parsing.py
: 요건parsing.py
단위 테스트용
🔵 parsing.py
- 내 블로그 글 파싱용 스크립트
from typing import Dict,List
from bs4 import BeautifulSoup as bs
import requests as rq
import os
import csv
RESULTS = List[Dict[str,str]]
class Parser:
def __init__(self,url:str) -> None:
self.url = url
self._headers : Dict[str,str] = {'user-agent': 'Mozilla/5.0'}
@staticmethod
def get_soup_object(res:rq.Response)-> bs:
return bs(res.text,'html.parser')
def parsing(self)-> RESULTS:
# data on list
save_list : RESULTS = []
with rq.Session() as session:
with session.get(url=self.url,headers=self._headers) as response:
if response.ok:
soup : bs = self.get_soup_object(res=response)
# Get content length
content_length : int = len(soup.select('li.list_horizontal_item',limit=5))
for idx in range(content_length):
# data on dict
save_dict : Dict[str,str] = {}
# Get Content
contents : list = soup.select('li.list_horizontal_item',limit=5)
# 제목
title : str = contents[idx].select_one('strong.title_post').text.strip().replace(',','')
# 날짜
dated : str = contents[idx].select_one('.date').text.strip().split(' ')[0].replace('.','-')
# 링크
link : str = "https://www.waytothem.com" + contents[idx].select_one('.link_article').attrs['href']
save_dict['text'] = f"[{title}]({link}) -"
save_dict['dated'] = dated
save_list.append(save_dict)
return save_list
class CSV(Parser):
def __init__(self,url:str,save_path:str,file_name:str) -> None:
super().__init__(url=url)
self.save_path = save_path
self.file_name = file_name
def upload_data_to_csv(self)-> None:
if not os.path.exists(self.save_path):
os.mkdir(self.save_path)
# Get results
results : RESULTS = self.parsing()
leng : List[str] = [v for v in range(len(results))]
leng.reverse()
with open(self.file_name,'w',newline='') as fp:
writer = csv.writer(fp)
for r_idx in leng:
writer.writerow([results[r_idx]['text'],results[r_idx]['dated']])
print(f'{results[r_idx]}\n')
def main()-> None:
url : str = 'https://waytothem.com/blog/'
save_path : str = 'csv/'
file_name : str = os.path.join(save_path,'parsing.csv')
# Create CSV instance
csv : CSV = CSV(url=url,save_path=save_path,file_name=file_name)
csv.upload_data_to_csv()
if __name__ == '__main__':
main()
🔵 test-parsing.py
parsing.py
에 대한 단위 테스트용 스크립트
from __init__ import Parser,CSV
from typing import Dict,List
import requests as rq
import os
import unittest
import shutil
class ParserTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.parser : Parser = Parser(url='https://waytothem.com/blog/')
def test_get_soup_object(self) -> None:
response : rq.Response = rq.get(url=self.parser.url,headers=self.parser._headers)
soup = self.parser.get_soup_object(res=response)
title = soup.select_one('span.header__text').text.strip()
self.assertEqual(title,"WTT's Blog")
def test_parsing(self) -> None:
results : List[Dict[str,str]] = self.parser.parsing()
self.assertEqual(len(results),5)
class CSVTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
url = 'https://waytothem.com/blog/'
cls.save_path = 'scripts/test-csv/'
cls.file_name = os.path.join(cls.save_path,'test-parsing.csv')
cls.csv : CSV = CSV(url=url,save_path=cls.save_path,file_name=cls.file_name)
def tearDown(self) -> None:
shutil.rmtree(self.save_path)
def test_upload_data_to_csv(self) -> None:
self.csv.upload_data_to_csv()
dir_true : bool = os.path.isdir(self.save_path)
file_true : bool = os.path.isfile(self.file_name)
self.assertTrue(dir_true)
self.assertTrue(file_true)
if __name__ == '__main__':
unittest.main()
🔵 blog-post.sh
- 파싱해온 블로그 글 정제
- 날짜 형식을 en_US 형식에 맞게 수정
- time badge 업데이트 코드 작성
- 커밋 스크립트 실행
#!/bin/bash
#### Set Python
PYTHON="$(which python3)"
#
#### Set Files
PYTHON_FILE='./scripts/parsing.py'
COMMIT_FILE='./scripts/commit.sh'
#
#### Execute Python
"${PYTHON}" "${PYTHON_FILE}" 1>/dev/null && echo -e "Parsing Done\n"
#
#### Output save ./line
SAVE_F='./line'
exec 3>> "${SAVE_F}"
echo -e "\n\n<!-- Blog-Post -->\n" >&3
#while IFS=','
while IFS=',' read text dated
do
formattedDate="$(LANG=en_US date '+%b %-d, %Y' -d ${dated})"
echo "- ${text} ${formattedDate}" >&3
done < csv/parsing.csv
echo -e "\n<!-- Blog-Post End -->" >&3
#
#### Modified Time badge
D="$(date '+%Y/%m/%d_%H:%M')"
TIME_BADGE_URL="<img src=\"https://img.shields.io/badge/Last%20Modified-${D}-%23121212?style=flat\">"
echo -e "\n\n${TIME_BADGE_URL}" >&3
#
#### ADD README.md
cat ./OLD-README.md > ./README.md
cat "${SAVE_F}" >> README.md
#
#### line 파일 삭제
rm -rf "${SAVE_F}"
#
🔵 commit.sh
- 커밋 스크립트
#!/bin/bash
if [[ $(git status --porcelain) ]]
then
git config --global user.name "${USER_NAME}"
git config --global user.email "${USER_EMAIL}"
git remote set-url origin https://"${USER_NAME}":"${TOKEN}"@github.com/"${REPOSITORY}"
git add -A
git commit -m "${MESSAGE}"
git push -u origin main
fi
✍️ 워크플로우 작성
parsing.yml
test-parsing.yml
blog-post.yml
🔵 parsing.yml
- parsing.py 파싱 데이터 출력 값 확인 워크플로우
name: "Blog Contents Parsing"
on: [
push:
branches: [main]
]
jobs:
build:
name: "Blog Content Parsing Steps"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 1. Install Dependencies
run: |
pip install --upgrade pip
if [ -f ./scripts/requirements.txt ]
then
pip install -r ./scripts/requirements.txt
else
pip install bs4 requests
fi
- name: 2. parsing 스크립트 실행
run: python ./scripts/parsing.py
- name: 3. csv/ 폴더 체크
run: |
if [ -d csv/ ]
then
echo "csv exists"
fi
- name: 4. parsing.csv 파일 체크
run: |
if [ -f csv/parsing.csv ]
then
cat csv/parsing.csv
fi
🟡 build 결과
🔵 test-parsing.yml
- 단위 테스트 성공 유/무 워크플로우
name: "parsing Script Unit Test"
on: [
push:
branches: [main]
]
jobs:
unit-test:
name: Parsing Script Unit Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 1. Install dependencies
run: |
pip install --upgrade pip
if [ -f ./scripts/requirements.txt ]
then
pip install -r ./scripts/requirements.txt
else
pip install requests bs4
fi
- name: 2. 단위 테스트 스크립트 실행
run: |
python ./scripts/test/test-parsing.py
🟡 build 결과
🔵 blog-post.yml
name: Set recent blog post in my fucking README
on:
push:
branches: [main]
jobs:
update-readme-blog-post:
name: Update README with latest blog posts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 1. Install dependencies
run: |
pip install --upgrade pip
if [ -f ./scripts/requirements.txt ]
then
pip install -r ./scripts/requirements.txt
else
pip install bs4 requests
fi
- name: 2. Run Script
run: |
bash ./scripts/blog-post.sh
- name: 3. Commit
env:
TOKEN: ${{ secrets.TOKEN }}
REPOSITORY: ${{ github.repository }}
MESSAGE: "Update blog post"
USER_NAME: "JaehyoJJAng"
USER_EMAIL: "yshrim12@naver.com"
run: |
bash ./scripts/commit.sh
🔴 옵션 분석
on
on:
push:
branches: [main]
이 Action이 실행되어야 하는 이벤트 정의. push
이벤트가 발생하고 브랜치가 main
일 때, 해당 Action이 실행된다.
jobs
update-readme-blog-post:
Action이 실행될 Job을 정의. 여기서는 update-readme-blog-post
라는 하나의 Job을 정의하였음
runs-on
Job이 실행되는 환경 정의. 여기서는 ubuntu-latest
로 정의
steps:
- uses: actions/checkout@v3
- name: 1. Install dependencies
run: |
pip install --upgrade pip
if [ -f ./scripts/requirements.txt ]
then
pip install -r requirements.txt
else
pip install bs4 requests
fi
- name: 2. Run Script
run: |
bash ./scripts/blog-post.sh || echo -e "🚨 Failed to update blog post\n\nCheck: https://www.waytothem.com/blog" && exit 1
- name: 3. Commit
env:
TOKEN: secrets.TOKEN }
REPOSITORY: ${{ github.repository }}
MESSAGE: "Update blog post"
USER_NAME: "JaehyoJJAng"
USER_EMAIL: "yshrim12@naver.com"
run: |
bash ./scripts/commit.sh || echo -e "Failed to commit changes" && exit 1
각 단계는 다음과 같이 구성된다
- actions/checkout@v3을 사용하여 저장소를 체크 아웃합니다.
- 필요한 종속성을 설치합니다. 만약
./scripts/requirements.txt
파일이 있다면, 해당 파일에 기재된 종속성을 설치하고, 없다면 BeautifulSoup4와 requests를 설치합니다. 그리고./scripts/blog-post.sh
스크립트를 실행합니다. 이때, 스크립트가 실패하면 메시지를 출력하고 exit code 1로 프로세스를 종료합니다. - commit.sh 스크립트를 사용하여 변경 내용을 커밋하고, 커밋 메시지는 "Update blog post"로 지정합니다. 이때,
secrets.TOKEN
으로 인증을 하고, 커밋을 수행한 사용자 이름과 이메일 주소를 "JaehyoJJAng"와 "yshrim12@naver.com"으로 설정합니다. 이 스크립트도 실패하면 메시지를 출력하고 exit code 1로 프로세스를 종료합니다.
여기서 사용된 각 옵션은 다음과 같습니다.
여기서 사용된 각 옵션은 다음과 같다
uses
: actions/checkout@v3를 사용하여 저장소를 체크 아웃합니다.run
: shell 스크립트를 실행합니다.if
조건문: 파일이 존재하는지 확인하여 조건문 실행을 결정합니다.env
: 환경 변수를 설정합니다. 해당 환경 변수들은 스크립트에서 참조할 수 있습니다.exit
: 프로세스를 종료합니다.echo
: 터미널에 메시지를 출력합니다.
🟡 build 결과
🔐 Github 환경변수
- Actions Secret 변수 설정
Myrepo - Settings - Secrets and variables
탭에 들어가서
New repository secret
클릭 후 아래와 같이 워크플로우용 토큰 저장
🔴 트러블슈팅 기록
1. 첫번째 트러블슈팅
parsing.yml
파일을 작성하고 actions 빌드를 진행하면서 수 차례의 빌드 에러가 발생하였는데
그 중 대부분이 아래와 같은 내용의 에러였다.
No steps defined in `steps` and no workflow called in `uses` for the following jobs: build
위 에러가 발생 하였을 당시 parsing.yml 파일의 코드는 아래와 같았는데
name: "Blog Contents Parsing"
on: [push]
jobs:
build:
blog-parsing:
name: "Blog Content Parsing Steps"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 1. Install Dependencies
run: |
pip install --upgrade pip
if [ -f ./scripts/requirements.txt ]
then
pip install -r requirements.txt
else
pip install bs4 requests
fi
- name: 2. parsing 스크립트 실행
run: python
build 안에 또 빌드명을 지어넣으니 매칭되는 uses가 없다고 뜬 것 같다.
build:
요 부분을 blog-parsing
으로 바꾸고 기존에 있던 blog-parsing
을 삭제하니 정상적으로 actions가 빌드 되었다
아래 코드 참고
jobs:
blog-parsing:
name: "Blog Content Parsing Steps"
...
...
2. 두번째 트러블슈팅
blog-post.yaml
워크플로우를 빌드하면서 총 10건 정도의 빌드 에러가 발생하였고
발생 부분은 3. Commit
부분이었다.
에러 내용은 아래와 같다
❌ 3. Commit
...
remote: Permission to JaehyoJJAng/commit-auto-github-action.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/JaehyoJJAng/commit-auto-github-action/': The requested URL returned error: 403
Failed to commit changes
Error: Process completed with exit code 1.
분명 깃헙 환경 변수에 토큰 값도 정상적으로 잘 들어가 있는데도 불구하고 왜 자꾸 에러가 나는지 이유를 몰라서
구글에 Permission to JaehyoJJAng/commit-auto-github-action.git denied to github-actions[bot].
를 검색해서 해결방법을 찾아봤다.
찾아 본 결과 워크플로우에 대한 권한이 문제였다!
해결방법은 아래와 같다
You have to configure your repository - Settings -> Action -> General -> Workflow permissions
and choose read and write permissions