내 블로그의 최신 글을 깃허브 프로필에 자동으로 등록하기

Github Actions를 활용해, 내 블로그에 올라온 최신 글 목록을 자동으로 깃허브 프로필에 올려보자

2023-05-06에 씀

이 포스트에서 다루는 내용

작년까지는 꽤 평범한(?) 깃허브 프로필을 사용하고 있었다.

그런데 이런 형식은 다른 사람들과 너무 비슷한 느낌인 것 같아서, 나를 좀 더 잘 드러내는 프로필을 쓰고 싶었다.


어떤 식으로 프로필을 만들어야 나를 더 잘 드러낼 수 있을지 고민하던 끝에…

결국 인삿말만 남기고 나머지는 삭제했다. 😓


그러다가 How to Create a Dynamic Github Profile 이라는 포스팅을 읽게 됐다. 요약하자면 github action에 스케줄을 설정해, 매일 자정마다 자신의 블로그의 최신 포스트를 긁어와 README 파일에 최신 포스팅을 작성하는 내용이다.

나를 가장 잘 드러낼 수 있는 건 블로그 포스팅이라고 할 수 있고, 최근에 글또 활동을 하면서 글을 많이 쓰고 있기도 해서 깃허브 프로필에 최신 블로그 포스팅 목록을 보여주면 좋을 것 같았다. 그리고 블로그를 직접 개발해 깃허브 레포지토리에 올려 두고 있기 때문에, 깃허브 액션을 사용해 푸시가 발생할 때마다 프로필 레포지토리를 수정해 주면 될 것 같았다.


그래서 깃허브 액션을 적용한 지금, 이런 프로필이 됐다. :)

구현 아이디어

가장 먼저 생각한 아이디어는 블로그 레포지토리에서 프로필 readme.md 파일을 만들어서 push 해주는 방법이었다.

블로그 레포지토리의 push 이벤트가 발생하면, make-post-list-action을 발생시켜 블로그에 작성된 최신 포스트 5개의 정보를 알아온다. 이 데이터를 적절히 가공해 프로필에서 보여줄 마크다운 형식으로 변환한다.

깃허브 프로필의 정적인 부분의 정보를 담고 있는 profile-readme-template.md 파일을 열어, 동적으로 변경해줄 부분에 최신 포스트 리스트를 넣어준다. 이 파일의 내용을 프로필 레포지토리에 있는 readme.md 파일에 덮어씌워서 프로필 레포지토리에 커밋 후 푸시한다.


이 구조의 문제점은 블로그 레포지토리에서 최신 포스트 데이터를 뽑아내는 것 뿐만 아니라 프로필을 어떻게 작성할 지에 대해서도 알고 있어야 한다는 것이다.

즉, 만약 내 프로필을 변경하고 싶다면, 프로필 레포지토리의 파일을 수정하는 게 아니라 블로그 레포지토리의 profile-readme-template.md 파일을 수정해줘야 한다. 프로필 레포지토리의 readme.md 파일은 블로그 레포지토리에 push가 일어나면 덮어씌워질 파일이기 때문이다.

이렇게 프로필 레포지토리에서 자신의 README.md 파일이 어떻게 변경될지 모르는 구조는 유지보수하기 어려워 적절하지 않다.


물론 이런 구조도 가능하다! profile-readme-template.md를 프로필 레포지토리가 가지고 있고, 블로그 레포지토리는 그 템플릿을 가져다가 내용을 변경한 뒤, 프로필 레포지토리의 readme.md를 덮어쓰고 프로필 레포지토리에 커밋 후 푸시하는 방식이다.

이렇게 하면 프로필을 변경하기 위해 블로그 레포지토리의 파일을 변경해야 하는 문제는 해결할 수 있다.

그렇지만 애초에 블로그 레포지토리에서 프로필 레포지토리의 파일을 변경한다는 것 자체가 이상하다. 프로필 레포지토리는 언제 어떻게 변경이 일어날 지도 모르는 채 파일이 변경되는 것이다.

블로그 레포지토리도 여전히 프로필 레포지토리가 가지고 있는 파일에 대해 알아야 한다. profile-readme-template.md의 파일 내용 중에서 어느 부분에 최신 포스트 목록에 대해 작성해 줄 지 알아야 하기 때문이다.


그래서, 두 레포지토리가 모두 액션을 가지고 있는 구조로 만들었다.

먼저, 블로그 레포지토리에 push 이벤트가 발생하면 dispatch-post-list-action 액션이 실행된다. 이 액션은 블로그 포스트 중 최신 5개 포스트의 정보를 가져오고, 이 데이터를 프로필 레포지토리에 dispatch 해주는 역할 한다.

프로필 레포지토리는 repository_dispatch 이벤트 시점에 update-profile 액션을 실행시킨다. 이 액션에서는 dispatch를 통해 받은 최신 포스트 데이터를 잘 가공해서 profile-readme-template.md 파일의 특정 부분에 넣어준 다음, readme.md 파일을 덮어쓰고 커밋 후 푸시해서 깃허브 프로필을 변경할 것이다.

이렇게 하면, 블로그 레포지토리는 최신 포스트를 뽑아내는 일만, 프로필 레포지토리는 readme 파일을 가공하는 일만 하면 되고, 두 레포지토리는 서로를 몰라도 되는 구조가 된다.

Repository Dispatch Event

다른 레포지토리의 깃허브 액션 워크플로우를 트리거시키기 위해서, repository_dispatch 웹훅을 사용한다. 이 웹훅에 대해서는 공식 문서에서 확인할 수 있다.

이 웹훅을 사용하기 위해서는 웹훅을 받을 레포지토리의 Contents write 권한을 가진 Personal access token이 필요하다.

repository_dispatch 웹훅을 테스트해보기 위해 간단한 테스트 레포지토리를 만들었다. 먼저, repository_dispatch 웹훅을 받으면 client_payload에 포함된 message를 출력하는 액션을 만들었다.

1on:
2 repository_dispatch:
3 types: [test_result] # event_type이 test_result인 이벤트를 받았을 때 액션이 트리거된다
4
5jobs:
6 test:
7 runs-on: ubuntu-latest
8 steps:
9 - env:
10 MESSAGE: ${{ github.event.client_payload.message }}
11 run: echo $MESSAGE

그런 다음, 아래와 같은 HTTP POST 요청을 보내 웹훅을 트리거한다.

1curl -L \
2 -X POST \
3 -H 'Accept: application/vnd.github+json' \
4 -H "Authorization: Bearer ${TOKEN}" \
5 -H "X-GitHub-Api-Version: 2022-11-28" \
6 https://api.github.com/repos/${owner}/${repo-name}/dispatches \
7 -d '{"event_type": "test_result", "client_payload": {"message":"test"}}'

위에서 발급받은 토큰을 ${TOKEN} 부분에 넣고, owner와 repo-name 부분에 레포지토리 소유자와 레포지토리 이름을 적어준다.

데이터로는 event_type에 트리거시킬 이벤트 타입 문자열을, client_payload에 전달할 JSON 데이터를 문자열로 변환해 넣어주면 된다.


curl 커맨드를 실행하면, repository_dispatch 이벤트가 트리거되어 깃허브 액션이 실행된다. 전달한 데이터를 잘 출력하고 있다.

최근 포스트 가져오는 액션 만들기

우선, node로 파일을 가져오는 코드를 만들었다. 전체 코드는 여기에서 볼 수 있다.

이 자바스크립트 코드는 블로그 포스트가 있는 디렉토리의 파일을 모두 불러온 다음, 최근에 생성된 5개 파일의 메타 데이터를 뽑아내 반환하는 코드이다.

포스트 데이터를 가공해서 내보내기

뽑아낸 데이터를 액션 워크플로우로 내보내주기 위해 @actions/core 라이브러리를 사용했다.

core.setOutput('key', 'value') 함수를 호출하면, 액션 워크플로우 내에서 steps.<STEP ID>.outputs.<OUTPUT KEY> 형태로 사용할 수 있다.


포스트 데이터를 가져와 가공하는 자바스크립트 코드는 make post list 스텝에서 실행된다.

1- name: make post list
2 id: posts
3 run: node .github/make-post-list.js

포스트 데이터 가공이 끝나면, setOutput 을 호출해 데이터를 내보낸다.

1// make-post-list.js
2const core = require("@actions/core");
3
4// 데이터 가공하는 부분 생략 ...
5
6core.setOutput("posts", JSON.stringify(JSON.stringify(posts)));

그러면 다음 스텝부터는 이 데이터에 아래 코드의 마지막 줄의 posts 부분과 같이 ${{ steps.<step_id>.outputs.<output_key> }} 값으로 접근할 수 있다.

1- run: |-
2 curl -L \
3 -X POST \
4 -H 'Accept: application/vnd.github+json' \
5 -H "Authorization: Bearer ${{ secrets.DISPATCH_TARGET_TOKEN }}" \
6 -H "X-GitHub-Api-Version: 2022-11-28" \
7 https://api.github.com/repos/ooooorobo/ooooorobo/dispatches \
8 -d '{"event_type": "recent_post", "client_payload": { "posts": ${{ steps.posts.outputs.posts }} } }'

git으로 받아온 파일의 생성시점 가져오기

문제는, git clone 을 통해 가져온 파일에는 정확한 생성, 수정 시점이 기록되지 않는다는 것이다. 즉, 파일에는 파일이 실제로 생성됐던 시점이 아니라 git clone 을 받은 시점이 기록되어 있다.

action/checkout을 통해 레포지토리를 클론받은 후 각 파일의 birthtime을 출력해본 결과, 액션을 실행한 시점인 2023년 4월 26일에 생성된 것으로 되어 있다. 그런데 package.json 파일의 실제 생성일은 2022년 1월 19일이다.


이 birthdate를 보정해주기 위해, git log에 저장된 정보를 사용한다. 먼저, 쉘 스크립트 코드는 아래와 같다.

1git ls-tree -r --name-only main ./src/pages/posts | while read filename; do
2 unixtime=$(git log --follow --format=%at ${filename} | tail -1)
3 touchtime=$(date -r ${unixtime} +'%Y%m%d%H%M.%S')
4 touch -t ${touchtime} "${filename}"
5done

타임스탬프는 블로그 포스트만 변경해주면 되기 때문에, git ls-tree 커맨드를 사용해 포스트 경로에 있는 파일의 이름을 모두 가져오도록 했다. 그리고 각 파일마다 아래 과정을 거쳐 파일에 타임스탬프를 설정해준다.


우선, 어떤 파일의 생성 시점이 언제인가를 알기 위해 git log 커맨드를 사용한다.

1git log --follow --format=%at ${filename}

그러면 아래와 같은 타임스탬프가 출력된다.

11682443716
21682443581
31682443415
41682443033
51661444014
61638254829
71637654274
81637654159
91629966134
101629966075
111629962563
12(END)

가장 첫번째 로그가 파일의 생성 시점이기 때문에, tail -1 커맨드로 첫번째 로그의 타임스탬프를 가져온다. 이 값을 unixtime 변수에 저장했다.


touch 명령어의 -t 옵션을 사용해서 특정 파일의 타임스탬프를 변경할 수 있다. 이때 타임스탬프는 [[CC]YY]MMDDhhmm[.SS] 형식을 사용해야 한다.

위의 UNIX 형식의 타임스탬프를 원하는 형식으로 변경하려면 date 커맨드를 사용하면 된다.

1date -r ${unixtime} +'%Y%m%d%H%M.%S'

이 값을 touchtime 변수에 저장하고, touch -t ${touchtime} "${filename}" 커맨드로 파일의 타임스탬프를 변경해주면 된다.

이 과정을 거친 후 파일의 타임스탬프를 찍으면 파일이 처음 커밋된 시간으로 타임스탬프가 찍힌다!


이제 정말 최신 순으로 정렬된 포스팅 리스트를 가져올 수 있다. 액션의 마지막 단계에서 이 데이터를 curl 커맨드로 프로필 레포지토리에 전송해주게 했다.

1 - run: |-
2 curl -L \
3 -X POST \
4 -H 'Accept: application/vnd.github+json' \
5 -H "Authorization: Bearer ${{ secrets.DISPATCH_TARGET_TOKEN }}" \
6 -H "X-GitHub-Api-Version: 2022-11-28" \
7 https://api.github.com/repos/ooooorobo/ooooorobo/dispatches \
8 -d '{"event_type": "recent_post", "client_payload": { "posts": ${{ steps.posts.outputs.posts }} } }'

Action secrets

토큰은 yml 파일에 직접 작성하면 안되고, 레포지토리 Security 옵션에서 시크릿 값으로 설정해 줘야 한다. 아래 스크린샷처럼 레포지토리 설정에 들어가 시크릿 값을 추가해 줄 수 있다. 이렇게 추가한 시크릿 값은 위와 같이 ${{ secrets.KEY }} 형태로 접근할 수 있다.

시크릿 값은 액션 로그에서도 숨김 처리된다.

Workflow permissions

간혹 토큰에 올바른 권한을 줬는데도 아래와 같은 오류가 발생하는 경우가 있다.

1{
2 "message": "Resource not accessible by personal access token",
3 "documentation_url": "https://docs.github.com/rest/reference/repos#create-a-repository-dispatch-event"
4}

이럴 경우, 레포지토리 설정 > Actions > General > Workflow permissions 에서 권한이 Read and write permissions로 설정해줘야 한다.

프로필 레포지토리

프로필 레포지토리의 README.md 파일의 정적인 부분을 정의하기 위해, TEMPLATE.md 파일을 아래와 같이 추가했다.

1## 📮 New Posts!
2
3<!-- posts here -->

workflow_dispatch 이벤트를 받으면 이 템플릿을 열어서 <!-- posts here--> 부분을 포스트 목록으로 교체해주면 된다. 자세한 액션 코드는 여기에서 볼 수 있다.


그렇게 만들어진게 지금의 프로필!

이것만으론 아직 부족해 보이기도 하지만, 앞으로도 재밌는 방법으로 더 많은 내용을 채워볼 수 있을 것 같다!

프로필 사진

조예진

이전 포스트
비지터 패턴
다음 포스트
Act - 로컬에서 GitHub Actions 실행하기