目錄 Table of contents
前言
這篇接續上一篇 CI in Unity using Github Actions 使用Github Actions自動化測試建置Unity專案 後面所提及的容量限制問題。
上次說過,依照原本的方法大概build不用10次,Github容量就會爆了。
於是大概在第7次的時候就會收到這種信:
我是收到了這封信的時候才驚覺原來github有容量限制,之前一直以為可以隨便我塞到飽,或許寫個接口還能當作雲端硬碟使用(?)
總之收到這封信以後我才開始正視這個問題,好險是有找到解決方案,不然我絕對會放棄使用Github Actions作為重要專案的CI服務。
我只是單純記錄解決的過程,文筆會有些雜亂且廢話居多。
目標
目標很簡單,需要可以自動刪除過期或多餘的artifacts的方法。
過期或多餘的定義也很單純,我只要保留前N個版本的執行檔/輸出檔,其餘就是多餘且過期的。
操作Artifacts的API
Github的REST API v3有提供這樣的API,參考這裡。
- List all artifact of the repo
GET /repos/:owner/:repo/actions/artifacts
- Delete specific artifact
DELETE /repos/:owner/:repo/actions/artifacts/:artifact_id
以我上次的專案為例,想取得所有artifacts的資訊可以直接連到
https://api.github.com/repos/qwe321qwe321qwe321/Unity-with-Github-Actions-example/actions/artifacts
來查看。
有了這些API就好解決了,可以寫個腳本來爬artifacts的資訊再進行刪除。
清除腳本
本來想自己寫,但結果不用。查到這篇發現有人已經寫好了!再次感謝 lelegard 這位仁兄。
他的gist: https://gist.github.com/lelegard/6a428f67ee08e86d0c2f1af3f4a633d0
使用前記得dependencies要裝一裝,主要就jq要另外裝而已,也就apt-get install jq
解決。
但是我用了一下發現一個小bug: 遇到名稱包含空白的artifact時print
會噴錯,
於是就fork順便幫他改了一下,以下是修改後的版本:
#!/usr/bin/env bash
# Customize those three lines with your repository and credentials:
REPO=https://api.github.com/repos/OWNER/REPO
GITHUB_USER=your-github-user-name
GITHUB_TOKEN=token-with-workflow-rights-on-repo # 這個token需要repo相關的權限
# Number of most recent versions to keep for each artifact:
KEEP=5 # 保留的前N新的版本數量
# A shortcut to call GitHub API.
ghapi() { curl --silent --location --user $GITHUB_USER:$GITHUB_TOKEN "$@"; }
# A temporary file which receives HTTP response headers.
TMPFILE=/tmp/tmp.$$
# An associative array, key: artifact name, value: number of artifacts of that name.
declare -A ARTCOUNT
# Process all artifacts on this repository, loop on returned "pages".
URL=$REPO/actions/artifacts
while [[ -n "$URL" ]]; do
# Get current page, get response headers in a temporary file.
JSON=$(ghapi --dump-header $TMPFILE "$URL")
# Get URL of next page. Will be empty if we are at the last page.
URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*<//' -e 's/>.*//')
rm -f $TMPFILE
# Number of artifacts on this page:
COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) # 這裡是取得artifact的數量
# Loop on all artifacts on this page.
for ((i=0; $i < $COUNT; i++)); do
# Get name of artifact and count instances of this name.
name=$(jq <<<$JSON -r ".artifacts[$i].name?") # 這裡是取得第i個artifact的名稱
ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) # 以名稱為key,儲存目前有多少個相同名稱的artifacts
#printf "#%d %s - %d\n" $i "$name" ${ARTCOUNT[$name]}
# Check if we must delete this one.
if [[ ${ARTCOUNT[$name]} -gt $KEEP ]]; then #若超過$KEEP的量則刪除
id=$(jq <<<$JSON -r ".artifacts[$i].id?") # 取得id以便得到url
size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") ))
printf "Deleting %s #%d, %d bytes\n" "$name" ${ARTCOUNT[$name]} $size
ghapi -X DELETE $REPO/actions/artifacts/$id # DELETE API
fi
done
done
參數設定好後直接運行即可清除多餘的Artifacts。
注意DELETE API需要額外的存取repo權限,所以需要去申請一個personal access token,或是直接用上一篇發送repository_dispath
用的那組token也行。
整合Github Actions
有了腳本以後,再來就需要把腳本整合到Action內,最好是在完成上傳artifact以後執行清除腳本。
將Token加入Secrets
因為我們不希望直接明碼保存personal token在repo裡,加上github也不會讓你這麼做(當你上傳token明碼時會自動撤銷該token)。
所以我們需要把token放到repo的secrets中,之後再從action config裡傳入shell script。
(2020/03/01更新) 不需要手動做這個步驟,Github會自動產生「該repo的讀寫相關權限之token」在Secrets中命名為GITHUB_TOKEN
(雖然你從web上看不到),詳細請參考這篇官方文件。
Shell Script
再次修改剛剛上面提供的腳本,由於我希望參數化都在action的config檔裡面處理就好,而這個腳本不要帶有任何參數,所以修改一下腳本,如下。
#!/usr/bin/env bash
# Customize those three lines with your repository and credentials:
# Split the string "username/repo" into two parts.
GITHUB_USER="$(cut -d'/' -f1 <<< $OWNER_AND_REPO)"
GITHUB_REPO="$(cut -d'/' -f2 <<< $OWNER_AND_REPO)"
GITHUB_TOKEN=$PERSONAL_TOKEN
REPO=https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO
# Number of most recent versions to keep for each artifact:
KEEP=$KEEPING_COUNT
echo USER: $GITHUB_USER
echo REPO: $GITHUB_REPO
echo Keep: $KEEP
# A shortcut to call GitHub API.
ghapi() { curl --silent --location --user $GITHUB_USER:$GITHUB_TOKEN "$@"; }
# A temporary file which receives HTTP response headers.
TMPFILE=/tmp/tmp.$$
# An associative array, key: artifact name, value: number of artifacts of that name.
declare -A ARTCOUNT
# Process all artifacts on this repository, loop on returned "pages".
URL=$REPO/actions/artifacts
while [[ -n "$URL" ]]; do
# Get current page, get response headers in a temporary file.
JSON=$(ghapi --dump-header $TMPFILE "$URL")
# Get URL of next page. Will be empty if we are at the last page.
URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*<//' -e 's/>.*//')
rm -f $TMPFILE
# Number of artifacts on this page:
COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') ))
echo There are $COUNT artifacts in $OWNER_AND_REPO
# Loop on all artifacts on this page.
for ((i=0; $i < $COUNT; i++)); do
# Get name of artifact and count instances of this name.
name=$(jq <<<$JSON -r ".artifacts[$i].name?")
ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1))
#printf "#%d %s - %d\n" $i "$name" ${ARTCOUNT[$name]}
# Check if we must delete this one.
if [[ ${ARTCOUNT[$name]} -gt $KEEP ]]; then
id=$(jq <<<$JSON -r ".artifacts[$i].id?")
size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") ))
printf "Deleting %s #%d, %d bytes\n" "$name" ${ARTCOUNT[$name]} $size
ghapi -X DELETE $REPO/actions/artifacts/$id
fi
done
done
第5、6行是字串切割,把"owner/repo"的字串切成owner和repo兩個,從這篇找到如何在shell切割字串。之所以要這樣搞是因為action config那邊只能取得"owner/repo"這樣的字串資料。
有了這個腳本後,我把它放到.github/workflows/purging-artifacts.sh
,因為它只跟actions有依賴關係,我不希望它出現在我的repo首頁或其他地方。
Action Config(YAML)
多做一個job如下。
purgeArtifacts: # 清除多餘的artifacts
needs: [testAllModes] # 等到全部人跑完以後
name: Purging the redundant artifacts.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2 # checkout方法用來clone本專案(repo)到裝置內。
with:
lfs: true # 要不要下載git-lfs檔,即大型檔案。
- name: Purging the redundant artifacts.
run: | # 這個|符號表示多行輸入
chmod +x ./.github/workflows/purging-artifacts.sh # 改執行權限
./.github/workflows/purging-artifacts.sh # 跑寫好的shell
env:
PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 將GITHUB_TOKEN設定為環境變數
KEEPING_COUNT: 1 # 保存前N新的artifacts
OWNER_AND_REPO: ${{ github.repository }} # The string combine owner and repository. For example, Codertocat/Hello-World.
我把這段放在Tester.yml的testAllMode之後,詳情請看原始碼 (←但是注意這是舊版的原始碼,我還手動塞了個PERSONAL_TOKEN
到Secrets裡面,實際上可以直接用GITHUB_TOKEN
)。
先看看成果
把剛剛修改的成果push到一個新分支(branch),只讓它觸發tester事件。
之後我們再看一次artifacts列表:
分別是:
- Test results for playmode
- Test results for editmode
- StandaloneWindows64 Build
- StandaloneOSX Build
- StandaloneLinux64 Build
- Unity_v2018.4.13f1.alf
蛤?明明只有6個。
其實是Test result都各多了一個,這不算是我預期的結果。猜想應該是當下runner那次的結果還沒更新到artifacts list上,所以purging的時候還沒有它們,因此才會多出來一組。
這樣的話也算好解決,目前想到兩個方法:
- 多傳個當前runner生成的artifact名稱傳給purging script檢測,將該名稱的數量額外減1。
- 把purging的步驟移至單獨一個action,該action必須檢測其它所有actions都跑完了才執行。
第2個應該比較好,因為purging畢竟是全部檢查全部清掃的script,理論上test+build之後只要最後運行一次就好了,這樣也不用跑好幾次沒意義的步驟了。
但是,目前先不花時間測試這些方法,現在這樣已經解決1G容量限制的大麻煩了。
我選擇第2個方法來修正這個問題。
添加Purging Action
我在Github上找到了 github-action-wait-for-status,它可以等待其它Actions跑完後取得status(success/failure)才繼續執行下一個step,這樣我就可以利用這個方法做Purging Artifacts。
但是它目前release版本只有0.1.0且star不多,感覺還不是很穩定,斟酌使用
- 把原本的Tester.yml改回原樣。
- 添加新的config至
.github/workflows/purging-artifacts.yml
,內容如下:name: Auto Purging Artifacts on: push: { } # 任意push觸發(配合Tester用) pull_request: { branches: [master] } # 僅在pull request to master時觸發(配合Builder使用) jobs: autoPurge: runs-on: ubuntu-latest steps: - name: 'Wait for status checks' id: waitforstatuschecks uses: "WyriHaximus/github-action-wait-for-status@0.1.0" with: ignoreActions: autoPurge # 略過檢查自己 checkInterval: 13 # 每13秒檢查一次 env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - uses: actions/checkout@v2 # checkout方法用來clone本專案(repo)到裝置內。 - name: Purging the redundant artifacts. run: | # 這個|符號表示多行輸入 chmod +x ./.github/workflows/purging-artifacts.sh # 改執行權限 ./.github/workflows/purging-artifacts.sh # 跑寫好的shell env: PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 將GITHUB_TOKEN設定為環境變數 KEEPING_COUNT: 1 # 保存前N新的artifacts OWNER_AND_REPO: ${{ github.repository }} # The string combine owner and repository. For example, Codertocat/Hello-World.
- 完成,commit並push上去。
成果
等跑完action後,再打開一次artifacts列表。
這樣就正確無誤了,同名的artifacts只保留最新的一份。
這樣應該算是成功透過保留最少的檔案來避免塞爆1G容量限制了。
專案網址
Github: https://github.com/qwe321qwe321qwe321/Unity-with-Github-Actions-example/tree/purging-artifacts
參考網站
- https://github.community/t5/GitHub-Actions/Delete-artifacts/td-p/38188
- https://developer.github.com/v3/actions/artifacts/#delete-an-artifact
- https://gist.github.com/lelegard/6a428f67ee08e86d0c2f1af3f4a633d0
- https://unix.stackexchange.com/questions/312280/split-string-by-delimiter-and-get-n-th-element