From 22667085303583f85d77386c1ca1975e67afb5a3 Mon Sep 17 00:00:00 2001
From: Cora <17833654450@163.com>
Date: Mon, 13 Apr 2026 15:15:03 +0800
Subject: [PATCH 1/2] Rewrite feishu workflow
---
.github/workflows/pr-to-feishu.yml | 254 +++++++----------------------
1 file changed, 56 insertions(+), 198 deletions(-)
diff --git a/.github/workflows/pr-to-feishu.yml b/.github/workflows/pr-to-feishu.yml
index 8036660..10c9237 100644
--- a/.github/workflows/pr-to-feishu.yml
+++ b/.github/workflows/pr-to-feishu.yml
@@ -5,26 +5,23 @@ on:
types:
- opened
- reopened
- - ready_for_review
- - synchronize
- - closed
permissions:
contents: read
jobs:
- sync-task:
- name: Sync PR to Feishu Task
+ notify-reviewers:
+ name: Notify Feishu Code Reviewers
if: ${{ github.event.pull_request.base.repo.full_name == 'gkit-org/libgkit' }}
runs-on: ubuntu-latest
steps:
- - name: Create/complete Feishu task for PR
+ - name: Notify Feishu group and mention code reviewers
uses: actions/github-script@v7
env:
FEISHU_APP_ID: ${{ secrets.FEISHU_APP_ID }}
FEISHU_APP_SECRET: ${{ secrets.FEISHU_APP_SECRET }}
- FEISHU_TASKLIST_GUID: ${{ secrets.FEISHU_TASKLIST_GUID }}
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
+ FEISHU_CODE_REVIEWER_GROUP_ID: ${{ secrets.FEISHU_CODE_REVIEWER_GROUP_ID }}
with:
script: |
const pr = context.payload.pull_request
@@ -36,8 +33,8 @@ jobs:
const requiredSecrets = [
'FEISHU_APP_ID',
'FEISHU_APP_SECRET',
- 'FEISHU_TASKLIST_GUID',
- 'FEISHU_WEBHOOK_URL'
+ 'FEISHU_WEBHOOK_URL',
+ 'FEISHU_CODE_REVIEWER_GROUP_ID'
]
for (const key of requiredSecrets) {
@@ -47,28 +44,6 @@ jobs:
}
}
- async function getTenantToken() {
- const authResp = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- app_id: process.env.FEISHU_APP_ID,
- app_secret: process.env.FEISHU_APP_SECRET
- })
- })
-
- if (!authResp.ok) {
- throw new Error(`Failed to request Feishu access token: HTTP ${authResp.status}`)
- }
-
- const authData = await authResp.json()
- if (authData.code !== 0 || !authData.tenant_access_token) {
- throw new Error(`Failed to get Feishu access token: ${JSON.stringify(authData)}`)
- }
-
- return authData.tenant_access_token
- }
-
async function feishuRequest(url, options) {
const resp = await fetch(url, options)
const raw = await resp.text()
@@ -86,45 +61,33 @@ jobs:
return data
}
- function getTaskGuidFromTask(task) {
- return task?.guid || task?.task_guid || task?.id || null
- }
-
- function getSummaryText(task) {
- const summary = task?.summary
- if (typeof summary === 'string') {
- return summary
- }
+ async function getTenantToken() {
+ const data = await feishuRequest('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ app_id: process.env.FEISHU_APP_ID,
+ app_secret: process.env.FEISHU_APP_SECRET
+ })
+ })
- if (summary && typeof summary === 'object') {
- if (typeof summary.content === 'string') {
- return summary.content
- }
- if (typeof summary.text === 'string') {
- return summary.text
- }
+ const token = data.tenant_access_token
+ if (!token) {
+ throw new Error(`Failed to parse tenant_access_token: ${JSON.stringify(data)}`)
}
- return ''
+ return token
}
- function isPRTask(task, prNumber) {
- const summary = getSummaryText(task)
- const m = summary.match(/\[PR\s*#(\d+)\]/i)
- return !!m && Number(m[1]) === Number(prNumber)
- }
-
- function buildTaskLink(taskGuid) {
- return `https://applink.feishu.cn/client/todo/detail?guid=${encodeURIComponent(taskGuid)}`
- }
-
- async function listTasklistTasks(tenantToken) {
- const tasks = []
+ async function listGroupMembersOpenId(tenantToken) {
+ const members = []
let pageToken = ''
while (true) {
- const url = new URL(`https://open.feishu.cn/open-apis/task/v2/tasklists/${encodeURIComponent(process.env.FEISHU_TASKLIST_GUID)}/tasks`)
- url.searchParams.set('page_size', '100')
+ const url = new URL(`https://open.feishu.cn/open-apis/contact/v3/group/${encodeURIComponent(process.env.FEISHU_CODE_REVIEWER_GROUP_ID)}/member/simplelist`)
+ url.searchParams.set('page_size', '50')
+ url.searchParams.set('member_id_type', 'open_id')
+ url.searchParams.set('member_type', 'user')
if (pageToken) {
url.searchParams.set('page_token', pageToken)
}
@@ -137,8 +100,12 @@ jobs:
}
})
- const items = data?.data?.items || []
- tasks.push(...items)
+ const list = data?.data?.memberlist || []
+ for (const member of list) {
+ if (member?.member_type === 'user' && member?.member_id) {
+ members.push(member.member_id)
+ }
+ }
if (!data?.data?.has_more) {
break
@@ -150,36 +117,10 @@ jobs:
}
}
- return tasks
- }
-
- async function findTaskByPRNumber(tenantToken, prNumber) {
- const tasks = await listTasklistTasks(tenantToken)
- for (const task of tasks) {
- if (isPRTask(task, prNumber)) {
- return task
- }
- }
-
- return null
- }
-
- async function createTask(tenantToken, taskTitle, taskDescription) {
- const endpoint = `https://open.feishu.cn/open-apis/task/v2/tasklists/${encodeURIComponent(process.env.FEISHU_TASKLIST_GUID)}/tasks`
- return await feishuRequest(endpoint, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${tenantToken}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- summary: taskTitle,
- description: taskDescription
- })
- })
+ return Array.from(new Set(members))
}
- async function notifyGroup(lines) {
+ async function notifyWebhook(text) {
const resp = await fetch(process.env.FEISHU_WEBHOOK_URL, {
method: 'POST',
headers: {
@@ -188,7 +129,7 @@ jobs:
body: JSON.stringify({
msg_type: 'text',
content: {
- text: lines.join('\n')
+ text
}
})
})
@@ -215,116 +156,33 @@ jobs:
core.info(`base=${pr.base?.repo?.full_name || 'unknown'}`)
core.info(`head=${pr.head?.repo?.full_name || 'unknown'}`)
- if (['opened', 'reopened', 'ready_for_review', 'synchronize'].includes(context.payload.action)) {
- const tenantToken = await getTenantToken()
- core.setSecret(tenantToken)
-
- const taskTitle = `[PR #${pr.number}] ${pr.title}`
- const taskDescription = [
- `PR: ${pr.html_url}`,
- `Contributor: ${pr.user?.login || 'unknown'}`,
- `Created At (UTC): ${pr.created_at}`
- ].join('\n')
-
- const existingTask = await findTaskByPRNumber(tenantToken, pr.number)
- const existingTaskGuid = getTaskGuidFromTask(existingTask)
-
- let finalTaskGuid = existingTaskGuid
- let actionText = 'updated'
-
- if (existingTaskGuid) {
- await feishuRequest(`https://open.feishu.cn/open-apis/task/v2/tasks/${existingTaskGuid}`, {
- method: 'PATCH',
- headers: {
- Authorization: `Bearer ${tenantToken}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- summary: taskTitle,
- description: taskDescription
- })
- })
- } else {
- const createData = await createTask(tenantToken, taskTitle, taskDescription)
-
- finalTaskGuid = createData?.data?.task?.guid || createData?.data?.guid || null
- actionText = 'created'
- }
+ const tenantToken = await getTenantToken()
+ core.setSecret(tenantToken)
- if (!finalTaskGuid) {
- core.setFailed(`Feishu task ${actionText} but guid not found in response`)
- return
- }
-
- await notifyGroup([
- `GitHub PR 已同步到飞书任务清单(${actionText}):`,
+ const reviewerOpenIds = await listGroupMembersOpenId(tenantToken)
+ if (reviewerOpenIds.length === 0) {
+ await notifyWebhook([
+ 'GitHub 有新的 PR,但未查询到 code_reviewer 用户组成员:',
+ `- 事件: ${context.payload.action}`,
`- 标题: ${pr.title}`,
`- 链接: ${pr.html_url}`,
`- Contributor: ${pr.user?.login || 'unknown'}`,
- `- 时间(UTC): ${pr.created_at}`,
`- 编号: #${pr.number}`,
- `- Task GUID: ${finalTaskGuid}`,
- `- 任务链接: ${buildTaskLink(finalTaskGuid)}`
- ])
-
- core.info(`Feishu task ${actionText}: ${finalTaskGuid} for PR #${pr.number}`)
- return
- }
-
- if (context.payload.action === 'closed') {
- const tenantToken = await getTenantToken()
- core.setSecret(tenantToken)
-
- const task = await findTaskByPRNumber(tenantToken, pr.number)
- const taskGuid = getTaskGuidFromTask(task)
- if (!taskGuid) {
- core.info(`No Feishu task found for PR #${pr.number}. Skip completion.`)
- await notifyGroup([
- 'GitHub PR 已关闭,但未找到关联的飞书任务:',
- `- 标题: ${pr.title}`,
- `- 链接: ${pr.html_url}`,
- `- Contributor: ${pr.user?.login || 'unknown'}`,
- `- 状态: ${pr.merged ? 'merged' : 'closed'}`,
- `- 编号: #${pr.number}`
- ])
- return
- }
-
- try {
- await feishuRequest(`https://open.feishu.cn/open-apis/task/v2/tasks/${taskGuid}/complete`, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${tenantToken}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({})
- })
- } catch {
- await feishuRequest(`https://open.feishu.cn/open-apis/task/v2/tasks/${taskGuid}`, {
- method: 'PATCH',
- headers: {
- Authorization: `Bearer ${tenantToken}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- completed: true
- })
- })
- }
-
- await notifyGroup([
- 'GitHub PR 已关闭,飞书任务已自动完成:',
- `- 标题: ${pr.title}`,
- `- 链接: ${pr.html_url}`,
- `- Contributor: ${pr.user?.login || 'unknown'}`,
- `- 状态: ${pr.merged ? 'merged' : 'closed'}`,
- `- 编号: #${pr.number}`,
- `- Task GUID: ${taskGuid}`,
- `- 任务链接: ${buildTaskLink(taskGuid)}`
- ])
-
- core.info(`Completed Feishu task ${taskGuid} for PR #${pr.number}`)
+ `- 用户组ID: ${process.env.FEISHU_CODE_REVIEWER_GROUP_ID}`
+ ].join('\n'))
+ core.info('No members found in reviewer group; sent fallback notification.')
return
}
- core.info(`No handling for action: ${context.payload.action}`)
+ const mentions = reviewerOpenIds.map((id) => `code_reviewer`).join(' ')
+ const message = [
+ `${mentions}`,
+ `GitHub PR 需要 Review(${context.payload.action}):`,
+ `- 标题: ${pr.title}`,
+ `- 链接: ${pr.html_url}`,
+ `- Contributor: ${pr.user?.login || 'unknown'}`,
+ `- 编号: #${pr.number}`
+ ].join('\n')
+
+ await notifyWebhook(message)
+ core.info(`Notified reviewer group members: ${reviewerOpenIds.length}`)
From 37a0e071bce92d51a4a989adfb38f00d61bb00ef Mon Sep 17 00:00:00 2001
From: Cora <17833654450@163.com>
Date: Mon, 13 Apr 2026 15:18:22 +0800
Subject: [PATCH 2/2] add feishu notify when pr close
---
.github/workflows/pr-to-feishu.yml | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/pr-to-feishu.yml b/.github/workflows/pr-to-feishu.yml
index 10c9237..c8da406 100644
--- a/.github/workflows/pr-to-feishu.yml
+++ b/.github/workflows/pr-to-feishu.yml
@@ -5,6 +5,7 @@ on:
types:
- opened
- reopened
+ - closed
permissions:
contents: read
@@ -174,15 +175,23 @@ jobs:
return
}
- const mentions = reviewerOpenIds.map((id) => `code_reviewer`).join(' ')
+ const action = context.payload.action
+ const titleLine = action === 'closed'
+ ? `GitHub PR 已关闭(${pr.merged ? 'merged' : 'closed'}):`
+ : `GitHub PR 需要 Review(${action}):`
+
+ const mentions = action === 'closed'
+ ? ''
+ : reviewerOpenIds.map((id) => `code_reviewer`).join(' ')
+
const message = [
- `${mentions}`,
- `GitHub PR 需要 Review(${context.payload.action}):`,
+ mentions,
+ titleLine,
`- 标题: ${pr.title}`,
`- 链接: ${pr.html_url}`,
`- Contributor: ${pr.user?.login || 'unknown'}`,
`- 编号: #${pr.number}`
- ].join('\n')
+ ].filter((line) => line && line.trim().length > 0).join('\n')
await notifyWebhook(message)
core.info(`Notified reviewer group members: ${reviewerOpenIds.length}`)