From 05186901fc301fd170cdd55beb2ff613d708a2ea Mon Sep 17 00:00:00 2001 From: Cora <17833654450@163.com> Date: Mon, 13 Apr 2026 14:19:21 +0800 Subject: [PATCH 1/3] - fix feishu task addition - add notify when pr open and reopen --- .github/workflows/pr-to-feishu.yml | 258 +++++++++++++++++------------ 1 file changed, 153 insertions(+), 105 deletions(-) diff --git a/.github/workflows/pr-to-feishu.yml b/.github/workflows/pr-to-feishu.yml index 943c9e9..9d02aaf 100644 --- a/.github/workflows/pr-to-feishu.yml +++ b/.github/workflows/pr-to-feishu.yml @@ -11,7 +11,6 @@ on: permissions: contents: read - issues: write jobs: sync-task: @@ -34,12 +33,12 @@ jobs: return } - const markerPrefix = '', idx) - if (end === -1) { - continue + async function listTasklistTasks(tenantToken) { + const tasks = [] + let pageToken = '' + + while (true) { + const url = new URL('https://open.feishu.cn/open-apis/task/v2/tasks') + url.searchParams.set('tasklist_guid', process.env.FEISHU_TASKLIST_GUID) + url.searchParams.set('page_size', '100') + if (pageToken) { + url.searchParams.set('page_token', pageToken) } - const taskGuid = body.slice(idx + markerPrefix.length, end).trim() - if (taskGuid) { - return taskGuid + const data = await feishuRequest(url.toString(), { + method: 'GET', + headers: { + Authorization: `Bearer ${tenantToken}`, + 'Content-Type': 'application/json' + } + }) + + const items = data?.data?.items || [] + tasks.push(...items) + + if (!data?.data?.has_more) { + break + } + + pageToken = data?.data?.page_token || '' + if (!pageToken) { + break + } + } + + return tasks + } + + async function findTaskByPRNumber(tenantToken, prNumber) { + const tasks = await listTasklistTasks(tenantToken) + for (const task of tasks) { + if (isPRTask(task, prNumber)) { + return task } } @@ -136,12 +202,6 @@ jobs: core.info(`head=${pr.head?.repo?.full_name || 'unknown'}`) if (['opened', 'reopened', 'ready_for_review', 'synchronize'].includes(context.payload.action)) { - const existingTaskGuid = await findTaskGuidFromPRComments() - if (existingTaskGuid) { - core.info(`Task marker already exists for PR #${pr.number}: ${existingTaskGuid}`) - return - } - const tenantToken = await getTenantToken() core.setSecret(tenantToken) @@ -152,73 +212,76 @@ jobs: `Created At (UTC): ${pr.created_at}` ].join('\n') - const createResp = await fetch('https://open.feishu.cn/open-apis/task/v2/tasks', { - method: 'POST', - headers: { - Authorization: `Bearer ${tenantToken}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - summary: taskTitle, - description: taskDescription, - tasklists: [ - { - tasklist: { - tasklist_guid: process.env.FEISHU_TASKLIST_GUID + 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 feishuRequest('https://open.feishu.cn/open-apis/task/v2/tasks', { + method: 'POST', + headers: { + Authorization: `Bearer ${tenantToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + summary: taskTitle, + description: taskDescription, + tasklists: [ + { + tasklist: { + tasklist_guid: process.env.FEISHU_TASKLIST_GUID + } } - } - ] + ] + }) }) - }) - const createRaw = await createResp.text() - let createData = null - try { - createData = JSON.parse(createRaw) - } catch { - createData = null + finalTaskGuid = createData?.data?.task?.guid || createData?.data?.guid || null + actionText = 'created' } - if (!createResp.ok || !createData || createData.code !== 0) { - core.setFailed(`Failed to create Feishu task: HTTP ${createResp.status}, ${createRaw}`) + if (!finalTaskGuid) { + core.setFailed(`Feishu task ${actionText} but guid not found in response`) return } - const taskGuid = createData.data?.task?.guid || createData.data?.guid - if (!taskGuid) { - core.setFailed(`Feishu task created but guid not found: ${createRaw}`) - return - } - - const marker = `${markerPrefix}${taskGuid} -->` - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: [ - 'Feishu task synced for this PR.', - marker - ].join('\n') - }) - await notifyGroup([ - 'GitHub 新 PR 已同步到飞书任务清单:', + `GitHub PR 已同步到飞书任务清单(${actionText}):`, `- 标题: ${pr.title}`, `- 链接: ${pr.html_url}`, `- Contributor: ${pr.user?.login || 'unknown'}`, `- 时间(UTC): ${pr.created_at}`, `- 编号: #${pr.number}`, - `- Task GUID: ${taskGuid}` + `- Task GUID: ${finalTaskGuid}`, + `- 任务链接: ${buildTaskLink(finalTaskGuid)}` ]) - core.info(`Created Feishu task ${taskGuid} for PR #${pr.number}`) + core.info(`Feishu task ${actionText}: ${finalTaskGuid} for PR #${pr.number}`) return } if (context.payload.action === 'closed') { - const taskGuid = await findTaskGuidFromPRComments() + 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 marker found for PR #${pr.number}. Skip completion.`) + core.info(`No Feishu task found for PR #${pr.number}. Skip completion.`) await notifyGroup([ 'GitHub PR 已关闭,但未找到关联的飞书任务:', `- 标题: ${pr.title}`, @@ -230,20 +293,17 @@ jobs: return } - const tenantToken = await getTenantToken() - core.setSecret(tenantToken) - - let completeResp = await fetch(`https://open.feishu.cn/open-apis/task/v2/tasks/${taskGuid}/complete`, { - method: 'POST', - headers: { - Authorization: `Bearer ${tenantToken}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({}) - }) - - if (!completeResp.ok) { - completeResp = await fetch(`https://open.feishu.cn/open-apis/task/v2/tasks/${taskGuid}`, { + 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}`, @@ -255,19 +315,6 @@ jobs: }) } - const completeRaw = await completeResp.text() - let completeData = null - try { - completeData = JSON.parse(completeRaw) - } catch { - completeData = null - } - - if (!completeResp.ok || (completeData && completeData.code !== 0)) { - core.setFailed(`Failed to complete Feishu task ${taskGuid}: HTTP ${completeResp.status}, ${completeRaw}`) - return - } - await notifyGroup([ 'GitHub PR 已关闭,飞书任务已自动完成:', `- 标题: ${pr.title}`, @@ -275,7 +322,8 @@ jobs: `- Contributor: ${pr.user?.login || 'unknown'}`, `- 状态: ${pr.merged ? 'merged' : 'closed'}`, `- 编号: #${pr.number}`, - `- Task GUID: ${taskGuid}` + `- Task GUID: ${taskGuid}`, + `- 任务链接: ${buildTaskLink(taskGuid)}` ]) core.info(`Completed Feishu task ${taskGuid} for PR #${pr.number}`) From f5b09d5b2c84ced16e3b87cffc4394826abdbe8b Mon Sep 17 00:00:00 2001 From: Cora <17833654450@163.com> Date: Mon, 13 Apr 2026 14:22:22 +0800 Subject: [PATCH 2/3] update feishu api request structure --- .github/workflows/pr-to-feishu.yml | 78 +++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr-to-feishu.yml b/.github/workflows/pr-to-feishu.yml index 9d02aaf..adfee25 100644 --- a/.github/workflows/pr-to-feishu.yml +++ b/.github/workflows/pr-to-feishu.yml @@ -165,6 +165,65 @@ jobs: return null } + async function createTaskWithFallback(tenantToken, taskTitle, taskDescription) { + const payloads = [ + { + summary: taskTitle, + description: taskDescription, + tasklists: [ + { + tasklist: { + tasklist_guid: process.env.FEISHU_TASKLIST_GUID + } + } + ] + }, + { + summary: taskTitle, + description: taskDescription, + tasklists: { + tasklist: { + tasklist_guid: process.env.FEISHU_TASKLIST_GUID + } + } + }, + { + summary: taskTitle, + description: taskDescription, + tasklist: { + tasklist_guid: process.env.FEISHU_TASKLIST_GUID + } + }, + { + summary: taskTitle, + description: taskDescription, + tasklists: [ + { + tasklist_guid: process.env.FEISHU_TASKLIST_GUID + } + ] + } + ] + + let lastError = null + for (const payload of payloads) { + try { + return await feishuRequest('https://open.feishu.cn/open-apis/task/v2/tasks', { + method: 'POST', + headers: { + Authorization: `Bearer ${tenantToken}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + } catch (error) { + lastError = error + } + } + + throw lastError || new Error('Failed to create Feishu task') + } + async function notifyGroup(lines) { const resp = await fetch(process.env.FEISHU_WEBHOOK_URL, { method: 'POST', @@ -231,24 +290,7 @@ jobs: }) }) } else { - const createData = await feishuRequest('https://open.feishu.cn/open-apis/task/v2/tasks', { - method: 'POST', - headers: { - Authorization: `Bearer ${tenantToken}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - summary: taskTitle, - description: taskDescription, - tasklists: [ - { - tasklist: { - tasklist_guid: process.env.FEISHU_TASKLIST_GUID - } - } - ] - }) - }) + const createData = await createTaskWithFallback(tenantToken, taskTitle, taskDescription) finalTaskGuid = createData?.data?.task?.guid || createData?.data?.guid || null actionText = 'created' From 209d28acb35a18d9cc95d4f425fdaa2786ba7b33 Mon Sep 17 00:00:00 2001 From: Cora <17833654450@163.com> Date: Mon, 13 Apr 2026 14:30:05 +0800 Subject: [PATCH 3/3] Optimize Feishu task creation logic and simplify API request structure --- .github/workflows/pr-to-feishu.yml | 71 ++++++------------------------ 1 file changed, 13 insertions(+), 58 deletions(-) diff --git a/.github/workflows/pr-to-feishu.yml b/.github/workflows/pr-to-feishu.yml index adfee25..8036660 100644 --- a/.github/workflows/pr-to-feishu.yml +++ b/.github/workflows/pr-to-feishu.yml @@ -123,8 +123,7 @@ jobs: let pageToken = '' while (true) { - const url = new URL('https://open.feishu.cn/open-apis/task/v2/tasks') - url.searchParams.set('tasklist_guid', process.env.FEISHU_TASKLIST_GUID) + 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') if (pageToken) { url.searchParams.set('page_token', pageToken) @@ -165,63 +164,19 @@ jobs: return null } - async function createTaskWithFallback(tenantToken, taskTitle, taskDescription) { - const payloads = [ - { - summary: taskTitle, - description: taskDescription, - tasklists: [ - { - tasklist: { - tasklist_guid: process.env.FEISHU_TASKLIST_GUID - } - } - ] - }, - { - summary: taskTitle, - description: taskDescription, - tasklists: { - tasklist: { - tasklist_guid: process.env.FEISHU_TASKLIST_GUID - } - } - }, - { - summary: taskTitle, - description: taskDescription, - tasklist: { - tasklist_guid: process.env.FEISHU_TASKLIST_GUID - } + 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, - tasklists: [ - { - tasklist_guid: process.env.FEISHU_TASKLIST_GUID - } - ] - } - ] - - let lastError = null - for (const payload of payloads) { - try { - return await feishuRequest('https://open.feishu.cn/open-apis/task/v2/tasks', { - method: 'POST', - headers: { - Authorization: `Bearer ${tenantToken}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(payload) - }) - } catch (error) { - lastError = error - } - } - - throw lastError || new Error('Failed to create Feishu task') + description: taskDescription + }) + }) } async function notifyGroup(lines) { @@ -290,7 +245,7 @@ jobs: }) }) } else { - const createData = await createTaskWithFallback(tenantToken, taskTitle, taskDescription) + const createData = await createTask(tenantToken, taskTitle, taskDescription) finalTaskGuid = createData?.data?.task?.guid || createData?.data?.guid || null actionText = 'created'