利用 Github Action 實現部落格自動發版

語言: CN / TW / HK

## 背景

先說下背景需求,在摸魚週報的整理流程中,最後一步需要生成公眾號的原文連結,原文連結指向的是個人部落格地址。部落格需要釋出才能產生外部連結,釋出到不費事,但是操作步驟重複,且因為涉及部落格推送相關的配置都在我的個人電腦裡,所有步驟必須由我來完成。來回多次之後就考慮將這個流程做成自動化了,目標是讓週報協作者都可以實現部落格推送,用到的實現方式是 Github Action。

實現思路

在開始之前先了解下原先的釋出流程,如下圖表示:

整個過程涉及 3 個倉庫:

  • Moyu Repo。管理週報文章的公共倉庫,協作者可以通過它拉取和推送內容。

  • Blog Repo。管理部落格內容的私有倉庫,週報只是其中一部分。

  • Blog Website。部落格的網站,它部署在一臺騰訊雲伺服器上,它也是私有的。

因為涉及兩個私有倉庫,普通協作者都沒有他們的訪問許可權,所以釋出流程都依賴我來完成。解決方案就是消除釋出流程對許可權的依賴,理想流程是這樣的:

這樣觸發入口就都集中在了共有倉庫,協作者也可以參與部落格釋出。要實現這個流程需要將需求分為兩步:

1、Moyu Repo 通過 Github Action 推送 Moyu 內容到 Blog Repo。

2、Blog Repo 通過 Github Action 釋出內容到網站。

這其中最關鍵的是訪問私有倉庫時如何處理許可權的問題。

Github Action

這裡先簡單瞭解下 Github Action。它是 Github 提供的為倉庫定義自動化流程的方案,類似 Jenkins、GitLab CI/CD。Github Action 有一套自己的流水線配置方式,所有的流程都可以通過一個 yml 檔案下發。Gtihub Action 有自己的虛擬機器,支援 Windows Server/Linux/Mac,使用者無需關心環境配置問題,可以直接使用。

配置入口如下圖所示:

點選set up a workflow yourself,即建立了一個用於編排自動化任務的 workflow,它對應一個 yml 檔案,所有的配置流程都在這裡進行。

自動化任務配置前我們需要先考慮這幾個問題:什麼時機觸發?在什麼裝置執行?如何執行自動化任務?我們看一個簡單的例子來學習下 Github Action 如何定義這些行為:

yml name: GitHub Actions Demo on: [push] jobs: Explore-GitHub-Actions: runs-on: ubuntu-latest steps: - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." - name: Check out repository code uses: actions/checkout@v3

name 表示當前流水線的名字。

什麼時機觸發

在什麼場景觸發,對應的key 是 on。上面Demo裡的[push],表示倉庫發生push行為時觸發任務。on 還有其他幾種觸發途徑:

  • pull_request:提交 PR 時觸發
  • schedule:定時觸發,可以按 cron 語法配置定頻
  • workflow_dispatch:手動觸發,有使用者手動啟用觸發行為

在什麼裝置執行

對應的關鍵詞是runs-on,Demo裡指定值為ubuntu-latest,表示執行裝置是一個 ubuntu 裝置。Github Action 還支援 macOS 環境,目前有三個 macOS 版本可以支援:

| 虛擬環境 | YAML標籤 | | -------------------- | -------------------------- | | macOS Monterey 12 | macos-12 | | macOS Big Sur 11 | macos-latestmacos-11 | | macOS Catalina 10.15 | macos-10.15 |

需要注意:macos-latest 不是最新的 macos 版本,而是 macOS 11。iOS 開發中我們可能還會關心 Xcode 版本,Ruby 版本等。以 macOS 12 虛擬機器為例,Xcode 版本:

| Version | Build | Path | | ---------------- | -------- | ------------------------------ | | 13.4.1 (default) | 13F100 | /Applications/Xcode_13.4.1.app | | 13.4 | 13F17a | /Applications/Xcode_13.4.app | | 13.3.1 | 13E500a | /Applications/Xcode_13.3.1.app | | 13.2.1 | 13C100 | /Applications/Xcode_13.2.1.app | | 13.1 | 13A1030d | /Applications/Xcode_13.1.app |

Ruby版本:2.7.6/3.0.4/3.1.2。

其他預製環境可以參考這篇文件:macos-12-Readme

另外 Github Action 還支援將自己的裝置定義為執行機,你可以在這裡瞭解:About self-hosted runners;支援聯機除錯,可以通過這個外掛瞭解:A debugger for actions

如何執行自動化任務

有兩種執行任務的方式,一種是直接在 yml 檔案裡編輯指令碼,關鍵詞是run。像是 Demo 裡的 echo 命令,我們可以直接輸入 shell 命令進行執行。

另一種方式是外掛市場,像下面這種形式:

yml - name: Check out repository code uses: actions/checkout@v3

就是使用了 actions/checkout@v3 這個外掛。Github 有一個外掛市場,可以搜尋所需外掛。像是 Code review,SSH 登入等都有封裝好的外掛可以直接使用。

實現方案

有了這些 Github Action 知識,我們就可以開始實現我們的需求了。最終效果分成兩個需求。

Moyu Repo 向 Blog Repo 推送內容

我們按照前面的三個問題來設計這個功能。

什麼時機觸發?

釋出之前需要經過多次修改,會有多個 PR 和 Push 行為,而 Blog 釋出需要等所有內容都準備完成才會執行,一般只有一次。所以考慮使用手動釋出的方式,以下是配置內容:

```yml

Action name

name: Weekly Article Deploy

Controls when the workflow will run

on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: inputs: weekly_index: description: 'weekly index for deploy'
```

手動釋出還補充了一個 inputs,用於接收輸出引數,weekly_index為引數名,用於表示要釋出第幾期。執行效果如下所示:

在什麼裝置執行?

這個需要根據執行任務來定,這裡只涉及一些文字轉換和倉庫操作,所以任意機器都滿足需求,ubuntu 資源比較多,排程會快那麼一點點,所以都可的情況優先選 ubuntu。

```yml

A workflow run is made up of one or more jobs that can run sequentially or in parallel

jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest ```

Github Action 裡有幾個名詞:workflow,job,steps,這裡簡單捋一下。整個 Yml 檔案對應為一個 workflow,它表示一次完整的自動化任務執行過程。當前倉庫的整個配置都是一個 workflow。

一個 workflow 可以包含一個或多個 job,這裡的 jobs 下面一級內容就是各個 job。不同 job 之間可以序列也可以並行。build為會其中一個 job,也是本 workflow 唯一的 job。

如何執行自動化任務?

這個流程需要做的事情是把 Moyu Repo 內容轉成 Blog Repo 的格式,然後推送到 Blog Repo 裡。前一步可以封裝成一個指令碼,後一步往私有倉庫推送需要生成一個具有推送私有倉庫許可權的 token。

token 的生成需要到這裡:個人頭像 -> Settings -> Developer settings -> Personal access tokens,點選 Generate new token。這一步需要輸入密碼,然後我們可以選擇所需許可權去生成一個token。對於私有倉庫的推送,我們選中這一個許可權就可以了:

為了安全考慮,這個token生成之後只會可見一次,因為後面的步驟會使用,所以我們需要做好儲存。

注意這個 token 是使用者級別的,它可以用於訪問修改該賬戶名下的任意倉庫。

為了讓 Github Action 可以訪問到這個token,需要給它做一個配置。配置路徑是:在該倉庫下的 Settings(注意這個是倉庫下的設定而非個人下的設定) -> Secrets -> Actions 點選 New repository secret。

Name 的話可以命名為 ACCESS_TOKEN,Value 為上一步生成的訪問 token。這裡配置的任意內容都可以通過Github Action 訪問到,且是加密的,建立之後只能看到 Name 看不到 Value。

下面是具體配置:

```yaml env: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}

Steps represent a sequence of tasks that will be executed as part of the job

steps: - name: print inputs run: | echo "Weekly Index: ${{ github.event.inputs.weekly_index }}"

  • name: Git Config run: | git config --global user.email [email protected] git config --global user.name moyuweekly # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
  • uses: actions/checkout@v3

# Runs a single command using the runners shell - name: Run a one-line script run: ./Script/ci_run.sh ${{ github.event.inputs.weekly_index }} shell: bash ```

secrets.ACCESS_TOKEN 表示的是該倉庫下面 secrets 裡 name 為 ACCESS_TOKEN 的內容。${{}}為 action 語法獲取變數的寫法。

因為我把對私有倉庫的獲取和推送都放到了執行腳本里,所以這裡通過環境變數的形式把這個值傳給指令碼。

steps 用於表述執行步驟,這裡是順序執行的。上述流程涉及到4個執行步驟:

1、列印外部傳參。${{github.event.inputs.weekly_index}} 表示輸入的引數。

2、配置 git user 的 email 和 name。因為執行內容涉及提交操作,這裡是為了標記自動化流程的提交。

3、uses 語法對應的是外掛功能,這裡輸入外掛名即可執行對應外掛配置的功能。 actions/checkout@v3 是官方外掛,用於拉取本倉庫程式碼的。

4、執行指令碼。我把私有倉庫的拉取,內容的格式化,私有倉庫推送都放到了這個腳本里。

私有倉庫的管理需要考慮 Git 連結的形式,Git 連結有兩種方式,一種是給 SSH 形式,這對於本地機器比較容易,對於不固定的打包機配置起來較為麻煩。第二種是 HTTPS 形式,HTTPS 是公有連結無法處理許可權問題,但 Github 支援把 token 傳入連結的形式來管理許可權。這裡選擇使用HTTPS形式,配置的 Git 地址如下:

https://{github_token}@github.com/username/repo.git

對倉庫的操作使用這個連結就可以解決許可權問題了,執行結果如下:

左邊是 job 描述,右邊是 steps 描述,每個 steps 都可以展開檢視詳情。因為這裡的步驟只有程式碼拉取推送和格式處理,所以執行很快。

Blog Repo 釋出網站

這個階段對應的是 Blog Repo 推送內容到騰訊雲伺服器。還是按上面的流程設計實現方式:

如何觸發任務?

這個歷程是上一步的承接,前一步已經定好了推送頻率,這裡可以接收到 push 即觸發任務。

yml push: branches: [ "master" ]

在什麼機器觸發?

這需要考慮到所使用的部落格框架,如果是 Hexo/Jekyll 使用 Ubutu 就可以了。因為我將部落格框架遷移到了 Publish,Publish 是一個由 Swift 編寫的靜態部落格框架,所以執行機器只能是 macOS。測試時發現 Publish 引用了libswift_Concurrency.dylib 這個庫,所以還需要指定版本為 macos-12。

yml jobs: build: # macOS Monterey 12 runs-on: macos-12

如何執行自動化任務?

執行流程大概是這樣的:編譯 publish -> 使用 publish 把 md 原始檔轉成靜態網站格式 -> 釋出到騰訊雲。

正常能獲取到 publish 執行檔案是無需編譯的,但因為我為了讓它相容 hexo 的格式,做了一些魔改,所以我使用的 publish 是一個動態的版本,它需要隨修改隨時編譯。

釋出至騰訊雲,也需要考慮許可權問題,個人伺服器沒有 Github 那種 token 授權形式,只能藉助於 SSH 了。

SSH

在開始之前再簡單回顧下 SSH 登入的一點原理。SSH 支援密碼和金鑰兩種登入方式,我們一般為省去繁瑣的密碼輸入,會採用金鑰登入的形式。金鑰登入使用的是對稱加密,一般的做法是登入端生成一對公私鑰,把公鑰放到服務端,私鑰儲存在本地。對稱加密解決的是資訊傳輸不會被篡改的問題,它無法防止中間人攻擊,因為它沒有HTTPS 那樣的 CA 來驗證可信性。SSH 選擇的是通過手動驗證的方式,關於手動驗證不知你是否還還記得這段內容:

The authenticity of host 'host (172.168.*.*)' can't be established. RSA key fingerprint is 98:2e:d7:e0:de:9f:ac:67:28:c2:42:2d:37:16:58:4d. Are you sure you want to continue connecting (yes/no)?

它就是用來手動驗證的,我們需要通過 host ip 驗證該連結是來自於可信的伺服器還是中間人。確定過一次之後,這個資訊會被寫到本地的 known_hosts 檔案中,之後對於同一 ip 的伺服器登入就不會再彈這個驗證了。對於自動化流程來說,我們應該將私有伺服器的驗證資訊直接填入known_hosts檔案,跳過阻塞式的二次確認。

流程配置

有了以上知識,我們對於金鑰的配置流程應該是如下圖所示:

藍色的鑰匙為 pub_key,紅色鑰匙為 private_key,帶鑰匙的檔案是 know_hosts。Github Action Runner 的配置流程如果都手動實現比較麻煩,我們可以使用install-ssh-key這個外掛快速實現這個功能:

yml - name: Install SSH Key uses: shimataro/[email protected] with: key: ${{ secrets.SSH_PRIVATE_KEY }} # it's value doesn't matter known_hosts: 'knowen_host_value for ssh-rsa'

它要求兩個引數,key 為 ssh 的 private key。SSH 私鑰可以使用上個章節介紹的 Secrets Actions 進行儲存,將其命名為 SSH_PRIVATE_KEY

known_hosts 是對 Server 端的信任記錄,用於免去手動確認的流程。這個內容的獲取,有兩種方式,你可以檢視本地的 known_hosts 檔案找到對應的目標伺服器的記錄,也可以利用 ssh-keyscan 去手動查詢。

bash $ ssh-keyscan -H <host-ip>

這個結果會按多種加密演算法產生多個結果,我們需要選擇型別為 ssh-rsa 的內容,因為Github Action 僅支援這一種加密結果

我們把這一條內容新增 known_hosts 引數即可。當然你也可以選擇使用金鑰的形式存放。

最重要的步驟已經完成了,下面就可以編譯 publish 併發布內容了。

yml - name: Create Publish run: | git clone https://github.com/zhangferry/Publish.git ./publish cd publish make cd .. - name: Blog Deploy run: | echo "begin deploy" ./publish/.build/release/publish-cli deploy

最後執行結果:

遺留問題

往騰訊雲伺服器推送內容時遇到一個騰訊雲給我發了告警郵件,說是檢測到異地登入,來源IP是美國,危險等級高危。這個登入應該指的是 Github Action Runner 裝置的登入,這個目前還沒有找到有效的解決辦法,因為IP是動態的,無法通過手動加白的形式避免。

另外也可關閉異地登入的報警,在告警設定裡關閉異地登入報警選項即可。但這種方式也存在一定的安全風險,因為它不是解決問題而是無視問題。

我暫時沒有找到更好的解決方案,如果有人知道更好的處理方式,歡迎告知。