白居易在看到滿滿的 issue 後不經感嘆:「野火燒不盡,春風吹又生。」然後毅然決然地把 issue 同步到 Notion,這樣就不會忘記了。

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

今天,我們將介紹如何編寫一個自定義的 GitHub Actions,將 GitHub 的 issue 自動同步到 Notion。這個工具將使你能夠將 GitHub 的問題跟踪和管理整合到 Notion 的資料庫中,讓問題管理更加高效。

Demo
Demo

實作

事前準備:建立 Notion 整合

首先請你參考昨天的步驟,建立一個 Notion 整合。請至 Notion Developers 建立一個新的整合,並取得 API 密鑰,然後整合至 Workspace。這個 API 密鑰將用於存取 Notion 的 API,以便將 GitHub 的 issue 同步到 Notion 中。

步驟 1:設定專案結構

首先,建立一個新的 GitHub 存儲庫來容納我們的自定義 Action。結構如下:

1
2
3
4
5
github-issue-2-notion/
├── action.yml
├── script.js
└── README.md

今天我們直接使用 Node.js 來編寫。

步驟 2:編寫 Action 配置文件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: "Sync GitHub Issues to Notion"
description: "Synchronize GitHub issues to a Notion database"
inputs:
  repo:
    description: "The GitHub repository (e.g., owner/repo)"
    required: true
  NOTION_API_KEY:
    description: "The API key for the Notion integration"
    required: true
  NOTION_DATABASE_ID:
    description: "The ID of the Notion database"
    required: true
runs:
  using: "node20"
  steps:
    - name: Run script
      uses: actions/setup-node@v3
      with:
        node-version: "20"
    - run: npm install
    - run: node script.js
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        NOTION_API_KEY: ${{ inputs.NOTION_API_KEY }}
        NOTION_DATABASE_ID: ${{ inputs.NOTION_DATABASE_ID }}

步驟 3:編寫 Node.js 腳本

script.js 文件中,編寫 Node.js 腳本來實現從 GitHub 取得 issue 並同步到 Notion。以下是 script.js 的完整內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/** @format */

const core = require("@actions/core");
const axios = require("axios");
const { markdownToBlocks } = require("@tryfabric/martian");

async function main() {
	const repo = core.getInput("repo");
	const notionToken = core.getInput("NOTION_API_KEY");
	const notionDatabaseId = core.getInput("NOTION_DATABASE_ID");

	// GitHub Issues API URL
	const issuesUrl = `https://api.github.com/repos/${repo}/issues?state=all`;

	// Fetch issues from GitHub
	const issuesResponse = await axios.get(issuesUrl, {
		headers: {
			"User-Agent": "request",
			Authorization: `token ${process.env.GITHUB_TOKEN}`
		}
	});

	for (const issue of issuesResponse.data) {
		const issueId = issue.id;
		const notionUrl = `https://api.notion.com/v1/databases/${notionDatabaseId}/query`;

		// Check if the issue already exists in Notion
		const notionResponse = await axios.post(
			notionUrl,
			{
				filter: {
					property: "ID",
					number: {
						equals: issueId
					}
				}
			},
			{
				headers: {
					Authorization: `Bearer ${notionToken}`,
					"Notion-Version": "2022-06-28",
					"Content-Type": "application/json"
				}
			}
		);

		const body = {
			parent: { database_id: notionDatabaseId },
			icon: {
				emoji: "⚡"
			},
			properties: {
				Name: {
					title: [
						{
							text: {
								content: issue.title
							}
						}
					]
				},
				ID: {
					number: issueId
				},
				State: {
					select: {
						name: issue.state.charAt(0).toUpperCase() + issue.state.slice(1)
					}
				},
				Status: {
					status: {
						name: "Not started"
					}
				},
				Labels: {
					multi_select: issue.labels.map(label => ({
						name: label.name
					}))
				},
				URL: {
					url: issue.html_url
				}
			},
			children: issue.body != null ? markdownToBlocks(issue.body) : []
		};

		if (notionResponse.data.results.length > 0) {
			console.log(`Issue ${issueId} already exists in Notion, updating it`);
			// Update existing issue
			const notionPageId = notionResponse.data.results[0].id;
			delete body.properties.Status;
			await axios.patch(`https://api.notion.com/v1/pages/${notionPageId}`, body, {
				headers: {
					Authorization: `Bearer ${notionToken}`,
					"Content-Type": "application/json",
					"Notion-Version": "2022-06-28"
				}
			});
		} else {
			console.log(`Creating new issue ${issueId} in Notion`);
			// Create new issue
			await axios.post("https://api.notion.com/v1/pages", body, {
				headers: {
					Authorization: `Bearer ${notionToken}`,
					"Content-Type": "application/json",
					"Notion-Version": "2022-06-28"
				}
			});
			console.log(`Issue ${issueId} created in Notion`);
		}
	}
}

main().catch(error => {
	console.error(error);
	process.exit(1);
});

這裡要來介紹一下裡面使用到的一些套件:

  • axios: 用於發送 HTTP 請求。
  • @tryfabric/martian: 用於將 Markdown 轉換為 Notion 的 block。
  • @actions/core: 用於存取 Action 的輸入和輸出。

步驟 4:設定 GitHub Secrets

在 GitHub 存儲庫的設定中,添加以下 Secrets:

  • NOTION_API_KEY: 你的 Notion API 密鑰。
  • NOTION_DATABASE_ID: 你的 Notion 資料庫 ID。

步驟 5:建立工作流程文件

在你的 GitHub 存儲庫的 .github/workflows 目錄下建立一個新的 YAML 文件,例如 sync-issues.yml,並添加以下內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: Sync GitHub Issues to Notion

on:
  issues:
    types: [opened, edited, deleted, closed, reopened]
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Sync issues to Notion
        uses: ./path-to-your-action # 使用自定義 Action 的路徑
        with:
          repo: ${{ github.repository }}
          NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}
          NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}

這個工作流程將在 GitHub 的 issue 有變化時觸發,並運行我們的自定義 Action 來同步 issue 到 Notion。

小結

今天我們探討了如何編寫一個 GitHub Actions 來將 GitHub 的 issue 同步到 Notion。通過這個實踐,我們掌握了如何使用 Node.js 和 GitHub Actions 來自動化從 GitHub 取得問題並更新 Notion 資料庫的過程。希望這篇教程能幫助你更高效地管理問題並保持項目組織的井然有序。

毛哥 EM

數位創作者,全端工程龍