Jira / Linear 集成实战:从需求卡片到 Agent 执行计划
需求管理系统(Jira / Linear)是团队的工作入口,但需求卡片和 Agent 执行之间有一道鸿沟:需求描述模糊、验收标准缺失、字段映射复杂、状态流转不同步。本文把"需求卡片 → Agent 执行计划 → 结果回写"做成一条完整的自动化链路,让 Agent 直接从 Jira / Linear 接活、交付、回写。
一、集成的价值与难点
| 维度 | 手动流程 | 集成后 |
|---|---|---|
| 任务创建 | PM 写 Issue → 手动拆任务 → 分配给开发者 | PM 写 Issue → 自动转 Agent 任务 |
| 执行状态 | 开发者手动更新状态 | Agent 自动回写状态 |
| 验收 | 人工对照验收标准逐条检查 | Agent 自动执行验收命令并回写结果 |
| 沟通成本 | 开发者在 Jira 和 IDE 之间反复切换 | 所有信息在 Jira 评论中汇总 |
难点在于:
- 字段差异:Jira 和 Linear 的字段模型完全不同
- 验收标准:多数需求卡片没有可执行的验收条件
- 状态映射:Jira 的
To Do → In Progress → Done和 Agent 的pending → running → completed → review不是一一对应 - 权限隔离:Agent 不应该有 Jira 管理员权限
二、集成架构
┌──────────────┐ ┌──────────────┐
│ Jira │ │ Linear │
│ ┌─────────┐ │ │ ┌─────────┐ │
│ │ Issue │ │ │ │ Issue │ │
│ │ 字段/状态│ │ │ │ 字段/状态│ │
│ └────┬────┘ │ │ └────┬────┘ │
└───────┼──────┘ └──────┼───────┘
│ │
Webhook / API Webhook / API
│ │
┌───────▼───────────────────────────────────▼───────┐
│ 需求适配层 (Issue Adapter) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 字段映射:Jira/Linear 字段 → 统一任务模型 │ │
│ │ 状态映射:外部状态 ↔ Agent 内部状态 │ │
│ │ 验收提取:从 description 提取可执行验收条件 │ │
│ └──────────────────┬───────────────────────────┘ │
└─────────────────────┼──────────────────────────────┘
│
┌─────────────────────▼──────────────────────────────┐
│ Agent 工作台 │
│ ┌─────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │任务队列 │→│上下文包生成│→│ Agent 执行 │ │
│ └─────────┘ └──────────┘ └────────┬─────────┘ │
│ │ │
│ ┌────────────────────────────────────▼──────────┐ │
│ │ 结果回写层 │ │
│ │ · 状态同步(In Progress → Done) │ │
│ │ · 评论写入(执行日志 + PR 链接) │ │
│ │ · 附件上传(测试报告、截图) │ │
│ │ · 验收结果回写 │ │
│ └──────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘三、统一任务模型
3.1 字段映射配置
# issue-adapter-config.yaml
# Jira 字段映射
jira:
base_url: "https://your-company.atlassian.net"
project_key: "ENG"
field_mapping:
title: "summary"
description: "description"
priority: "priority.name"
assignee: "assignee.emailAddress"
labels: "labels"
# 自定义字段
acceptance_criteria: "customfield_10042" # 验收标准(文本)
story_points: "story_points"
sprint: "sprint.name"
# 状态映射
status_mapping:
"To Do": "pending"
"In Progress": "running"
"In Review": "review"
"Done": "completed"
"Blocked": "blocked"
# 反向映射(Agent → Jira)
reverse_status_mapping:
"pending": "To Do"
"queued": "To Do"
"running": "In Progress"
"completed": "In Review" # Agent 完成后进入 Review
"review": "In Review"
"failed": "Blocked"
"approved": "Done"
# 自动触发的标签
agent_labels:
trigger: "agent-task" # 有此标签的 Issue 自动进入 Agent 队列
in_progress: "agent-running"
needs_review: "agent-review"
completed: "agent-done"
# Linear 字段映射
linear:
field_mapping:
title: "title"
description: "description"
priority: "priority" # 0-4 数字
assignee: "assignee.email"
labels: "labels.name"
status_mapping:
"Backlog": "pending"
"Todo": "pending"
"In Progress": "running"
"In Review": "review"
"Done": "completed"
"Canceled": "cancelled"
reverse_status_mapping:
"pending": "Todo"
"running": "In Progress"
"completed": "Done"
"review": "In Review"
"failed": "Blocked"3.2 统一任务模型
# app/models/unified_task.py
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class TaskStatus(Enum):
PENDING = "pending"
QUEUED = "queued"
RUNNING = "running"
REVIEW = "review"
COMPLETED = "completed"
FAILED = "failed"
APPROVED = "approved"
CANCELLED = "cancelled"
@dataclass
class AcceptanceCriterion:
"""单条验收标准"""
description: str # 人类可读描述
command: Optional[str] # 可执行的验收命令
expected_result: str # 预期结果
verified: bool = False
actual_result: str = ""
@dataclass
class UnifiedTask:
"""统一任务模型——Jira 和 Linear 都转成这个格式"""
# 来源信息
source_platform: str # "jira" | "linear"
source_id: str # Jira Issue Key 或 Linear Issue ID
source_url: str # 原始链接
# 基本信息
title: str
description: str
priority: str # "P0" | "P1" | "P2" | "P3"
labels: list[str]
# 人员
creator: str
assignee: Optional[str]
# 验收标准
acceptance_criteria: list[AcceptanceCriterion] = field(default_factory=list)
# Agent 执行信息
status: TaskStatus = TaskStatus.PENDING
agent_context: dict = field(default_factory=dict)
execution_log: list[str] = field(default_factory=list)
result: Optional[dict] = None
# 时间
created_at: str = ""
started_at: Optional[str] = None
completed_at: Optional[str] = None3.3 验收标准提取
# app/adapters/acceptance_extractor.py
"""
从需求描述中提取可执行的验收标准。
多数需求卡片的验收标准写在描述中,格式不统一,需要 Agent 解析。
"""
import re
def extract_acceptance_criteria(description: str, platform: str = "jira") -> list:
"""
从描述中提取验收标准。
支持多种常见格式:
- Jira 的 - [ ] 复选框格式
- Given/When/Then BDD 格式
- 编号列表格式
"""
criteria = []
# 1. 复选框格式:- [ ] xxx
checkboxes = re.findall(r"-\s*\[\s*\]\s*(.+)", description)
for cb in checkboxes:
criteria.append({
"description": cb.strip(),
"command": None,
"expected_result": cb.strip(),
})
# 2. Given/When/Then 格式
bdd_blocks = re.findall(
r"Given\s+(.+?)\s+When\s+(.+?)\s+Then\s+(.+?)(?=\n\n|\Z|Given)",
description,
re.DOTALL
)
for given, when, then in bdd_blocks:
criteria.append({
"description": f"Given {given.strip()} When {when.strip()} Then {then.strip()}",
"command": None,
"expected_result": then.strip(),
})
# 3. 编号列表:1. xxx
if not criteria:
numbered = re.findall(r"^\d+\.\s+(.+)$", description, re.MULTILINE)
for item in numbered:
criteria.append({
"description": item.strip(),
"command": None,
"expected_result": item.strip(),
})
return criteria
def agent_enhance_criteria(criteria: list, context: dict) -> list:
"""
用 Agent 把模糊的验收标准转成可执行命令。
例如:"用户能正常登录" → "POST /api/login 返回 200 + token"
"""
enhanced = []
for c in criteria:
if c.get("command"):
enhanced.append(c) # 已经有可执行命令
continue
# 调用 Agent 生成可执行验收命令
prompt = f"""
把以下验收标准转成可执行的验收命令。
验收标准:{c['description']}
项目技术栈:{context.get('tech_stack', 'unknown')}
测试框架:{context.get('test_framework', 'pytest')}
输出 JSON:
{{
"command": "具体的验收命令",
"expected_result": "预期的输出或退出码"
}}
"""
# agent_result = call_agent(prompt)
# c["command"] = agent_result["command"]
# c["expected_result"] = agent_result["expected_result"]
enhanced.append(c)
return enhanced四、结果回写
4.1 状态同步
# app/sync/status_sync.py
"""
Agent 任务状态变化时,同步更新 Jira / Linear。
"""
async def sync_status(task: UnifiedTask, new_status: str):
"""同步任务状态到来源平台"""
if task.source_platform == "jira":
# Jira 状态转换需要先获取 transition ID
transitions = await jira_client.get_transitions(task.source_id)
target_transition = None
target_jira_status = JIRA_REVERSE_STATUS_MAP.get(new_status)
for t in transitions:
if t["to"]["name"] == target_jira_status:
target_transition = t["id"]
break
if target_transition:
await jira_client.transition_issue(task.source_id, target_transition)
else:
log.warning(f"No transition found for {new_status} → {target_jira_status}")
elif task.source_platform == "linear":
target_state = LINEAR_REVERSE_STATUS_MAP.get(new_status)
if target_state:
await linear_client.update_issue(
task.source_id,
state_id=target_state,
)
async def add_comment(task: UnifiedTask, comment: str):
"""在来源平台添加评论"""
formatted = f"""🤖 **Agent 工作台更新**
{comment}
---
任务 ID: `{task.source_id}` | [查看原始需求]({task.source_url})
"""
if task.source_platform == "jira":
await jira_client.add_comment(task.source_id, formatted)
elif task.source_platform == "linear":
await linear_client.add_comment(task.source_id, formatted)4.2 验收结果回写
# app/sync/verification_sync.py
"""
验收完成后,把结果回写到需求卡片。
"""
async def sync_verification_results(task: UnifiedTask, results: list):
"""回写验收结果"""
# 生成 Markdown 表格
table = "| # | 验收标准 | 状态 | 实际结果 |\n"
table += "|---|---------|------|----------|\n"
all_passed = True
for i, r in enumerate(results, 1):
status_icon = "✅" if r["passed"] else "❌"
if not r["passed"]:
all_passed = False
table += f"| {i} | {r['description']} | {status_icon} | {r['actual'][:100]} |\n"
# 生成评论
summary = "## 验收结果\n\n"
summary += f"**总结**:{'全部通过 ✅' if all_passed else '部分未通过 ❌'}\n\n"
summary += table
summary += f"\n**执行时间**:{datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
if all_passed:
summary += "\n所有验收标准已通过,任务可以关闭。\n"
else:
failed = [r for r in results if not r["passed"]]
summary += f"\n{len(failed)} 项未通过,需要人工介入。\n"
await add_comment(task, summary)
# 如果全部通过,更新状态为完成
if all_passed:
await sync_status(task, "completed")五、真实经验与踩坑
5.1 Jira 的自定义字段是最大陷阱
场景:验收标准字段在 Jira 中是自定义字段,字段 ID 是 customfield_10042。
问题:不同 Jira 实例的自定义字段 ID 不同。在一个客户那里是 customfield_10042,在另一个是 customfield_10087。部署到新客户时验收标准全丢了。
解决方案:用字段名而不是 ID 做配置。在初始化时调用 Jira 的 /rest/api/3/field 接口,动态建立 name → id 的映射。配置文件里只写字段名:acceptance_criteria: "验收标准"。
5.2 状态同步要防止死循环
场景:Agent 更新 Jira 状态触发 Webhook,Webhook 又触发 Agent 处理状态变化事件,Agent 又更新 Jira……
问题:死循环,API 被限流。
解决方案:在状态更新时加一个 updated_by: "agent" 标记。Webhook handler 收到事件时检查这个标记,如果是 Agent 自己更新的就跳过。同时限制同一 Issue 的 Webhook 处理频率(1 分钟内同事件只处理 1 次)。
5.3 Linear 的 Webhook 比 Jira 简洁得多
场景:同时集成 Jira 和 Linear,以为工作量差不多。 问题:Jira 的 Webhook 配置需要在管理后台手动操作,字段 API 返回几百个字段(大量自定义),状态转换需要查 transition ID。Linear 用 GraphQL API,一个 webhook 注册搞定,字段模型清晰,状态直接通过 ID 更新。 解决方案:集成层做了适配抽象,但内部实现明显 Linear 更简单。建议新项目优先用 Linear,老系统从 Jira 迁移时注意字段映射的兼容性测试。
六、参数说明表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
platform |
string | 必填 | "jira" 或 "linear" |
base_url |
string | 必填 | Jira 实例 URL 或 Linear API URL |
api_token |
string | 必填 | API Token(加密存储) |
project_key |
string | 无 | Jira 项目 Key(Linear 不需要) |
field_mapping |
object | 见 3.1 | 字段映射配置 |
status_mapping |
object | 见 3.1 | 状态映射配置 |
agent_trigger_label |
string | "agent-task" |
触发 Agent 的标签 |
auto_sync_status |
bool | true |
是否自动同步状态 |
sync_verification |
bool | true |
是否回写验收结果 |
comment_template |
string | 内置 | 评论模板 |
webhook_dedup_window |
int | 60 |
Webhook 去重窗口(秒) |
七、落地检查清单
- Jira/Linear 的字段映射配置正确(包含自定义字段)
- 状态映射双向配置一致(外部 → Agent,Agent → 外部)
- 验收标准能从描述中自动提取
- Agent 更新状态后不会触发死循环
- Webhook 有幂等去重机制
- API Token 加密存储,不硬编码
- 结果评论格式清晰,包含验收结果表格
- 全部验收通过时自动更新状态为完成
- 失败时通知需求创建者
- 审计日志记录每次状态同步
八、系列导航
上一篇:GitHub 集成实战:Issue、PR、Checks 与 Agent 任务流 下一篇:Lab 006:同一需求从 PRD 到代码,哪个 Agent 最会问问题?