孔子能從心所欲不逾矩。正是因為他已經掌握 GitHub Actions,能夠根據自己的需求自動化操作。

今日範例程式:https://github.com/Edit-Mr/2024-GitHub-Actions/tree/main/17

今天,我們將介紹如何使用 Bash 腳本或 Node.js 編寫 GitHub Actions,自動從 Notion 獲取待辦事項並更新到 Discord 頻道。希望能透過這個小專題幫助你了解如何利用進行自動化操作呼叫 API。如果你不會使用 Notion 的話,~~那你該去學學。~~你可以參考大致的流程與程式的邏輯。

首先讓我們看一下成果。你可以看到 Discord 頻道的標題會根據 Notion 中的待辦事項進行更新。

Discord Demo
Discord Demo

事前準備:建立 Notion 資料庫並獲取 API

我們在開始之前需要獲得幾個變數:

Notion 資料庫 ID

在我們開始時做之前請先打開 Notion 建立一個資料庫。這裡我以中電會的 Notion 為例。可以看到左到右分別是預設的 To-do、In progress、Complete。我們將會從這個資料庫中獲取待辦事項。

建立資料庫
建立資料庫

開啟資料庫連結並複製 ID。記得要點擊 view database 進入整個畫面都是資料庫的頁面喔。比如說如下的連結:

1https://www.notion.so/6e7c50281a8b406dbxxxxxxxx7892659?v=fe8e4b0c57e24axxxxxxxxxx13271567

這個連結中的 6e7c50281a8b406dbxxxxxxxx7892659 就是資料庫 ID。

進入分頁並並複製 ID
進入分頁並並複製 ID

取得 API Token

在 Notion 的設定中,可以取得 API Token。這個 Token 將會用來取得資料庫的資料。請記得不要將這個 Token 洩漏給他人。請至 Notion Developers 取得 API,輸入基本資料並選擇要應用的 Workspace。

建立新的應用
建立新的應用

請你複製這一串 Integration Token。生成完 API 之後,記得要邀請你剛才創建的機器人。點擊 Notion 右上角的三個點,Connect to,並選擇剛才創建的機器人。

請你複製這串 ID
請你複製這串 ID

邀請加入 Workspace
邀請加入 Workspace

建立 Discord Bot

在 Discord 開發者中心建立一個 Bot,並取得 Token。這個 Token 將會用來更新 Discord 頻道的標題。請至 Discord Developer Portal 建立一個新的應用程式。

Discord 頻道 ID

在 Discord 中,請先開啟開發者模式,接著右鍵要更新的頻道,並複製頻道 ID。這個 ID 將用於更新頻道的標題。這個頻道原則上就是用來顯示數字用的,所以可以鎖定權限讓其他人不能在裡面聊天。

開發者模式在 Discord 中的位置:設定 -> 進階 -> 開發者模式

今天我們會將一樣的專題分別使用 Bash 腳本或 Node.js 撰寫。首先讓我們先試著用 bash 寫看看。

Bash 版

Bash 自定義 Actions 允許我們使用 Shell 腳本來執行操作。這是一種簡單且強大的方式,可以實現許多自動化需求。相較於 JavaScript 自定義 Actions,Bash 版本通常較為簡單,適合用來執行簡單的 Shell 命令和操作。

步驟 1:設置專案結構

首先,創建一個新的 GitHub 存儲庫來容納我們的自定義 Action。在存儲庫中,創建以下目錄結構:

1my-custom-action/
2├── action.yml
3├── script.sh
4└── README.md

步驟 2:編寫 Action 配置文件

action.yml 文件中,我們需要定義 Action 的輸入、執行和輸出。以下是 action.yml 的內容:

1name: "Update Notion to Discord"
2description: "Fetch tasks from Notion and update Discord channel"
3inputs:
4    notion_database_id:
5        description: "Notion database ID"
6        required: true
7    notion_token:
8        description: "Notion API token"
9        required: true
10    discord_channel_id:
11        description: "Discord channel ID"
12        required: true
13    discord_token:
14        description: "Discord bot token"
15        required: true
16runs:
17    using: "composite"
18    steps:
19        - name: Run script
20          run: ./script.sh
21          env:
22              NOTION_DATABASE_ID: ${{ inputs.notion_database_id }}
23              NOTION_TOKEN: ${{ inputs.notion_token }}
24              DISCORD_CHANNEL_ID: ${{ inputs.discord_channel_id }}
25              DISCORD_TOKEN: ${{ inputs.discord_token }}

欸你發現了嗎?我們這裡使用了一個之前沒使用過的參數 composite。這個參數可以讓我們在 Action 中執行多個步驟,這樣我們就可以在 Action 中執行多個 Shell 命令。同時我們還使用了之前沒使用過的 inputs 關鍵字,這個關鍵字可以讓我們在 Action 中訪問輸入的參數。

inputs 關鍵字用於定義 Action 的輸入參數,這些參數可以在 Action 運行時從外部設置。在這個例子中,我們定義了四個輸入參數:notion_database_idnotion_tokendiscord_channel_iddiscord_token。這些參數將用於從 Notion 獲取待辦事項並更新 Discord 頻道。當使用者第一次安裝 Action 時,他們需要提供這些參數的值。

步驟 3:編寫 Bash 腳本

script.sh 文件中,我們將編寫 Bash 腳本來實現具體的操作。以下是 script.sh 的內容:

1#!/bin/bash
2
3set -e
4
5update_tasks() {
6    local notion_database_id="$NOTION_DATABASE_ID"
7    local notion_token="$NOTION_TOKEN"
8    local discord_channel_id="$DISCORD_CHANNEL_ID"
9    local discord_token="$DISCORD_TOKEN"
10
11    # 從 Notion 獲取待辦事項
12    response=$(curl -s -X POST -H "Authorization: ${notion_token}" -H "Notion-Version: 2022-06-28" -H "Content-Type: application/json" "https://api.notion.com/v1/databases/${notion_database_id}/query")
13
14    if [ -n "$(echo "${response}" | jq '.results')" ]; then
15        not_started_count=0
16        in_progress_count=0
17        for row in $(echo "${response}" | jq -r '.results[] | @base64'); do
18            status_name=$(echo "${row}" | base64 -d | jq -r '.properties.Status.status.name')
19            echo "${status_name}"
20            if [ "${status_name}" = "Not started" ]; then
21                (( not_started_count++ ))
22            elif [ "${status_name}" = "In progress" ]; then
23                (( in_progress_count++ ))
24            fi
25        done
26
27        # 更新 Discord 頻道標題
28        update_discord_channel_title "還有 ${not_started_count} 件事沒人做"
29        update_discord_channel_title "${in_progress_count} 件事處理中"
30    else
31        echo "Error: Unable to retrieve data from Notion API."
32        exit 1
33    fi
34}
35
36update_discord_channel_title() {
37    local new_title="$1"
38    local channel_id="$DISCORD_CHANNEL_ID"
39    local url="https://discord.com/api/v10/channels/${channel_id}"
40    local token="Bot ${DISCORD_TOKEN}"
41
42    response=$(curl -s -X PATCH -H "Authorization: ${token}" -H "Content-Type: application/json" -d "{\"name\": \"${new_title}\"}" "${url}")
43    updated_title=$(echo "${response}" | jq -r '.name')
44    echo "Channel title updated successfully: ${updated_title}"
45}
46
47update_tasks

在腳本中,我們使用 curl 來從 Notion API 獲取待辦事項,並根據狀態更新 Discord 頻道的標題。

步驟 4:設置 GitHub Secrets

在 GitHub 存儲庫的設置中,添加所需的 Secrets:

  • NOTION_DATABASE_ID
  • NOTION_TOKEN
  • DISCORD_CHANNEL_ID
  • DISCORD_TOKEN

這些 Secrets 將用於在 Action 中安全地傳遞敏感信息。

步驟 5:運行和測試 Actions

當你將更改推送到 GitHub 存儲庫後,GitHub Actions 將會自動運行你的工作流程。你可以查看 Actions 頁面,確保自定義 Action 正確執行並更新 Discord 頻道標題。

JavaScript 版本

步驟 1:設置專案結構

首先,我們需要創建一個新的 GitHub 存儲庫來容納我們的自定義 Action。在存儲庫中,創建以下目錄結構:

1my-custom-action/
2├── nodejs.yml
3├── app.js
4└── package.json

老樣子,使用 npm init -y 來初始化一個 Node.js 專案。

步驟 2:編寫 Action 配置文件

action.yml 文件中,我們需要定義 Action 的輸入、執行和輸出。以下是 action.yml 的範例內容:

1name: "Update Notion to Discord"
2description: "Fetch tasks from Notion and update Discord channel"
3inputs:
4    notion_database_id:
5        description: "Notion database ID"
6        required: true
7    notion_token:
8        description: "Notion API token"
9        required: true
10    discord_channel_id:
11        description: "Discord channel ID"
12        required: true
13    discord_token:
14        description: "Discord bot token"
15        required: true
16runs:
17    using: "node20"
18    main: "app.js"

步驟 3:編寫 Action 腳本

app.js 文件中,我們將編寫 JavaScript 代碼來完成具體的操作。以下是 index.js 的內容:

1const core = require("@actions/core");
2const axios = require("axios");
3
4async function updateTasks() {
5    try {
6        // 讀取輸入參數
7        const notionDatabaseId = core.getInput("notion_database_id");
8        const notionToken = core.getInput("notion_token");
9        const discordChannelId = core.getInput("discord_channel_id");
10        const discordToken = core.getInput("discord_token");
11
12        // 從 Notion 獲取待辦事項
13        const notionResponse = await axios.post(
14            `https://api.notion.com/v1/databases/${notionDatabaseId}/query`,
15            {},
16            {
17                headers: {
18                    Authorization: `Bearer ${notionToken}`,
19                    "Notion-Version": "2022-06-28",
20                    "Content-Type": "application/json"
21                }
22            }
23        );
24
25        let notStartedCount = 0;
26        let inProgressCount = 0;
27
28        // 解析 Notion API 的響應
29        notionResponse.data.results.forEach((result) => {
30            const status = result.properties.Status.status.name;
31            if (status === "Not started") {
32                notStartedCount++;
33            } else if (status === "In progress") {
34                inProgressCount++;
35            }
36        });
37
38        // 更新 Discord 頻道標題
39        await axios.patch(
40            `https://discord.com/api/v10/channels/${discordChannelId}`,
41            {
42                name: `還有 ${notStartedCount} 件事沒人做`
43            },
44            {
45                headers: {
46                    Authorization: `Bot ${discordToken}`,
47                    "Content-Type": "application/json"
48                }
49            }
50        );
51
52        await axios.patch(
53            `https://discord.com/api/v10/channels/${discordChannelId}`,
54            {
55                name: `${inProgressCount} 件事處理中`
56            },
57            {
58                headers: {
59                    Authorization: `Bot ${discordToken}`,
60                    "Content-Type": "application/json"
61                }
62            }
63        );
64
65        console.log("Discord channel title updated successfully");
66    } catch (error) {
67        core.setFailed(`Action failed with error: ${error.message}`);
68    }
69}
70
71updateTasks();

首先我們引入了 @actions/core 來訪問 Action 的輸入參數,並引入 axios 來發送 HTTP 請求。在 updateTasks 函數中,我們讀取輸入參數,從 Notion API 獲取待辦事項,並更新 Discord 頻道的標題。

步驟 4:安裝套件

在專案根目錄下執行以下命令來安裝依賴套件:

1 npm install @actions/core axios

配置 GitHub Actions 工作流程

現在,我們需要創建一個 GitHub Actions 工作流程來使用我們的自定義 Action。在 .github/workflows 目錄下創建一個新的 YAML 文件,例如 main.yml,並添加以下內容:

1name: Update Notion to Discord
2
3on:
4    push:
5        branches:
6            - main
7
8jobs:
9    update:
10        runs-on: ubuntu-latest
11
12        steps:
13            - name: Checkout repository
14              uses: actions/checkout@v3
15
16            - name: Use custom action
17              uses: ./ # 使用自定義 Action 的路徑
18              with:
19                  notion_database_id: ${{ secrets.NOTION_DATABASE_ID }}
20                  notion_token: ${{ secrets.NOTION_TOKEN }}
21                  discord_channel_id: ${{ secrets.DISCORD_CHANNEL_ID }}
22                  discord_token: ${{ secrets.DISCORD_TOKEN }}

4. 設置 GitHub Secrets

在 GitHub 存儲庫的設置中,添加所需的 Secrets:

  • NOTION_DATABASE_ID
  • NOTION_TOKEN
  • DISCORD_CHANNEL_ID
  • DISCORD_TOKEN

這些 Secrets 將用於在 Action 中安全地傳遞敏感信息。

on 的部分定義了觸發條件。你可以根據具體需求選擇不同的觸發條件,例如 schedule(定時觸發),或其他事件。

1on:
2  schedule:
3    - cron: "0 0 * * *"  # 每天 00:00 UTC 觸發

今天的 GitHub Actions 怎麼怪怪的?

今天和以前不一樣,我們的 Action 並不是直接執行的,而是透過一個腳本來執行的。這樣的好處是我們可以在腳本中執行多個命令,並且可以更靈活地控制流程。這樣的設計可以讓我們更好地利用 GitHub Actions 來實現自動化操作。

小結

今天我們探討了如何使用 Bash 腳本編寫 GitHub Actions 自定義 Action。我們編寫了一個簡單的 Bash 腳本,從 Notion 獲取待辦事項並更新 Discord 頻道的標題。通過這個實踐,我們了解了如何使用 Shell 腳本來實現自動化操作,並且學會了如何配置和使用自定義 Actions。希望這篇教程對你有所幫助。s

毛哥EM

一位喜歡把簡單的事,做得不簡單的高三生。