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}`)