過去我試過一個簡單的方法:把 GitLab 發送的通知 Email 直接轉發到 Slack
雖然能用,但缺點是過濾不精準、訊息格式也很死板
某天突發奇想:既然通知都是透過 Email 送來的,那我能不能用 Gmail + Google Apps Script + Slack Workflow,做一個「屬於自己的 MR 通知系統」?
最後實作出來的效果大概長這樣:



整個流程可以分成三個部分:
首先,我需要讓 Gmail 自動幫 MR 通知信分類,這樣後續腳本才知道哪些要處理推送
mr-merged、mr-draft、mr-conflict
你的名字 改成自己的 GitLab 帳號名稱
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xmlns:apps='http://schemas.google.com/apps/2006'>
<entry>
<category term='filter'></category>
<title>Mail Filter</title>
<id></id>
<updated>2025-09-14T00:00:00Z</updated>
<content></content>
<apps:property name='hasTheWord' value='"marked merge request", "as draft"'/>
<apps:property name='label' value='mr-draft'/>
<apps:property name='sizeOperator' value='s_sl'/>
<apps:property name='sizeUnit' value='s_smb'/>
</entry>
<entry>
<category term='filter'></category>
<title>Mail Filter</title>
<id></id>
<updated>2025-09-14T00:00:00Z</updated>
<content></content>
<apps:property name='hasTheWord' value='"Merge request", "was merged", "Author: 你的名字"'/>
<apps:property name='label' value='mr-merged'/>
<apps:property name='sizeOperator' value='s_sl'/>
<apps:property name='sizeUnit' value='s_smb'/>
</entry>
<entry>
<category term='filter'></category>
<title>Mail Filter</title>
<id></id>
<updated>2025-09-14T00:00:00Z</updated>
<content></content>
<apps:property name='hasTheWord' value='"Merge request", "can no longer be merged due to conflict.", "Author: 你的名字"'/>
<apps:property name='label' value='mr-conflict'/>
<apps:property name='sizeOperator' value='s_sl'/>
<apps:property name='sizeUnit' value='s_smb'/>
</entry>
</feed>
接著要在 Slack 建立一個 Workflow,接收 Webhook。
{{EventName}}: {{Title}} ({{GitlabLink}})
最後按 Publish 儲存,並把 Webhook URL 留下來,下一步會用到。
最後一步就是寫一個 Google Apps Script,每分鐘檢查 Gmail,如果有符合標籤的 MR 通知,就轉發到 Slack。
// 個人設定:填入剛剛從 Slack Workflow 複製的 Webhook URL
var slackWebhook = "https://hooks.slack.com/triggers/......";
// 信件主旨的正則表達式
// 範例主旨:Re: 專案名稱 | MR 標題 (!(123))
var subjectRegex = /Re: ([^|]+) \| (.+) \(\!(\d+)\)/;
// GitLab 連結的正則表達式
var gitlabLinkRegex = /View it on GitLab: (https:\/\/[^\s]+)/;
// 發送訊息到 Slack
function sendMessageToSlack(eventName, title, gitlabLink, projectName) {
var payload = {
"EventName": eventName,
"Title": title,
"GitlabLink": gitlabLink,
"ProjectName": projectName
};
var res = UrlFetchApp.fetch(slackWebhook, {
method : 'post',
contentType : 'application/json',
payload : JSON.stringify(payload)
});
if (res.getResponseCode() != 200) {
Logger.log("發送 Slack 訊息失敗,內容為:" + JSON.stringify(payload));
}
}
// 讀取 Gmail 標籤,轉發 MR 通知
function forwardEmailsToSlack(eventName, labelName) {
var label = GmailApp.getUserLabelByName(labelName);
var threads = label.getThreads(0, 20); // 每次最多抓 20 封
if (!threads || threads.length == 0) {
return;
}
for (var i = 0; i < threads.length; i++) {
var subject = threads[i].getFirstMessageSubject();
var subjectMatchs = subject.match(subjectRegex);
if (!subjectMatchs || subjectMatchs.length != 4) {
Logger.log("主旨匹配非預期");
break;
}
var projectName = subjectMatchs[1];
var title = subjectMatchs[2];
var mrId = subjectMatchs[3];
// 取出最後一封信件,解析 GitLab 連結
var messages = threads[i].getMessages();
var mailBodyContent = messages[messages.length - 1].getPlainBody();
var bodyMatchs = mailBodyContent.match(gitlabLinkRegex);
if (!bodyMatchs || bodyMatchs.length != 2) {
Logger.log("信件內容匹配非預期");
break;
}
var gitlabLink = bodyMatchs[1];
// 發送到 Slack
sendMessageToSlack(eventName, title, gitlabLink, projectName);
// 後續處理:標記已讀
threads[i].markRead();
// threads[i].moveToArchive(); // 如果想自動封存可以打開
// threads[i].moveToTrash(); // 如果想直接刪掉可以打開
}
// 移除標籤,避免重複處理
label.removeFromThreads(threads);
}
// 主程式
function Main() {
forwardEmailsToSlack(":merged: MR 已合併", "mr-merged");
forwardEmailsToSlack(":memo: MR 轉草稿", "mr-draft");
forwardEmailsToSlack(":warning: MR 有衝突", "mr-conflict");
Logger.log("執行完成");
}
到這裡應該就完成了
之後,系統會定時檢查 Gmail:
這個方法雖然不是最「正統」的 GitLab–Slack 整合,但好處是: