Jira SAFe (Scaled Agile Framework) 技能
在 Jira Cloud 中实现 SAFe 方法论,用于创建 Epic、特性、故事和任务管理。
何时使用
- 创建具有业务成果和验收标准的 Epic
- 以 SAFe 格式编写用户故事(“作为一个… 我想要… 以便…”)
- 将特性分解为带有验收标准的故事
- 在故事下创建子任务
- 以正确的层级结构链接工作项(Epic → 特性 → 故事 → 子任务)
重要:Next-Gen 与经典项目
SCRUM 项目是 Next-Gen(团队管理)。 主要区别:
| 方面 | 经典(公司管理) | Next-Gen(团队管理) |
|---|---|---|
| Epic 链接 | customfield_10014 | parent: { key: ‘EPIC-KEY’ } |
| Epic 名称 | customfield_10011 | 无法使用 |
| 子任务类型 | ‘子任务’ | ‘Subtask’ |
| 项目风格 | classic | next-gen, simplified: true |
首先总是检测项目类型:
const projectInfo = await fetch(`${JIRA_URL}/rest/api/3/project/${PROJECT_KEY}`, { headers });
const project = await projectInfo.json();
const isNextGen = project.style === 'next-gen' || project.simplified === true;
Jira 中的 SAFe 层级结构
投资组合级别:
└── Epic (战略举措)
└── 特性 (效益假设)
└── 故事 (用户价值)
└── 子任务 (技术工作)
SAFe 模板
Epic 模板(Next-Gen)
// 注意:Next-Gen 项目不使用 customfield_10011(Epic 名称)
const epic = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Epic' },
summary: '[Epic ID]: [Epic 名称] - [业务成果]',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: '业务成果' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: '描述可衡量的业务价值...' }]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: '成功指标' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '指标 1: [可衡量目标]' }] }]
}
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: '范围' }]
},
{
type: 'paragraph',
content: [{ type: 'text', text: '什么是范围内和范围外的...' }]
}
]
},
labels: ['epic-label'] // 使用标签而不是 Epic 名称进行分类
}
};
故事模板(SAFe 格式,Next-Gen)
// 注意:Next-Gen 使用 'parent' 字段,而不是 customfield_10014
const story = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Story' },
summary: '[US-ID]: 作为一个 [角色],我想要 [目标],以便 [好处]',
description: {
type: 'doc',
version: 1,
content: [
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: '用户故事' }]
},
{
type: 'paragraph',
content: [
{ type: 'text', text: '作为一个 ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[角色]' },
{ type: 'text', text: ',我想要 ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[目标]' },
{ type: 'text', text: ',以便 ', marks: [{ type: 'strong' }] },
{ type: 'text', text: '[好处]' }
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: '验收标准' }]
},
{
type: 'heading',
attrs: { level: 3 },
content: [{ type: 'text', text: '场景 1: [名称]' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'GIVEN [前提条件]', marks: [{ type: 'strong' }] }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'WHEN [行动]', marks: [{ type: 'strong' }] }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'THEN [预期结果]', marks: [{ type: 'strong' }] }] }]
}
]
},
{
type: 'heading',
attrs: { level: 2 },
content: [{ type: 'text', text: '完成定义' }]
},
{
type: 'bulletList',
content: [
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] 代码审查并获得批准' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] 单元测试编写并通过' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] 集成测试通过' }] }]
},
{
type: 'listItem',
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[ ] 文档更新' }] }]
}
]
}
]
},
// Next-Gen: 使用 'parent' 字段链接到父 Epic
parent: { key: 'EPIC_KEY' },
labels: ['category-label', 'epic-id']
}
};
子任务模板(Next-Gen)
// 注意:Next-Gen 使用 'Subtask'(没有连字符),而不是 'Sub-task'
const subtask = {
fields: {
project: { key: 'PROJECT_KEY' },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', 经典: 'Sub-task'
summary: '[技术任务描述]',
// 父故事(子任务必需)
parent: { key: 'STORY_KEY' }
// 注意:子任务的描述是可选的
}
};
API 实现(Next-Gen 项目)
创建带有故事的 Epic(Next-Gen)
async function createEpicWithStories(epicFields, storyDefinitions) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// 1. 创建 Epic
const epicResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields: epicFields })
});
if (!epicResponse.ok) {
const error = await epicResponse.text();
throw new Error(`Epic 创建失败:${error}`);
}
const createdEpic = await epicResponse.json();
console.log(`创建 Epic: ${createdEpic.key}`);
// 2. 使用 'parent' 字段创建与 Epic 链接的故事(Next-Gen)
const createdStories = [];
for (const storyDef of storyDefinitions) {
const storyFields = {
...storyDef,
parent: { key: createdEpic.key } // Next-Gen: 使用 'parent', 不是 customfield_10014
};
const storyResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({ fields: storyFields })
});
if (!storyResponse.ok) {
const error = await storyResponse.text();
console.error(`故事创建失败:${error}`);
continue;
}
const createdStory = await storyResponse.json();
createdStories.push(createdStory);
console.log(` 创建故事: ${createdStory.key}`);
// 速率限制
await new Promise(r => setTimeout(r, 100));
}
return { epic: createdEpic, stories: createdStories };
}
创建带有子任务的故事(Next-Gen)
async function createStoryWithSubtasks(storyFields, epicKey, subtaskSummaries) {
const headers = {
'Authorization': `Basic ${Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
// 1. 在 Epic 下创建故事
const storyRequest = {
fields: {
...storyFields,
parent: { key: epicKey } // 链接到 Epic
}
};
const storyResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify(storyRequest)
});
if (!storyResponse.ok) {
throw new Error(`故事创建失败:${await storyResponse.text()}`);
}
const createdStory = await storyResponse.json();
// 2. 在故事下创建子任务
const createdSubtasks = [];
for (const summary of subtaskSummaries) {
const subtaskResponse = await fetch(`${JIRA_URL}/rest/api/3/issue`, {
method: 'POST',
headers,
body: JSON.stringify({
fields: {
project: { key: storyFields.project.key },
issuetype: { name: 'Subtask' }, // Next-Gen: 'Subtask', 不是 'Sub-task'
summary: summary,
parent: { key: createdStory.key }
}
})
});
if (subtaskResponse.ok) {
createdSubtasks.push(await subtaskResponse.json());
}
await new Promise(r => setTimeout(r, 50)); // 速率限制
}
return { story: createdStory, subtasks: createdSubtasks };
}
获取 Epic 链接字段 ID
Epic 链接字段因 Jira 实例而异。找到它:
async function findEpicLinkField() {
const response = await fetch(`${JIRA_URL}/rest/api/3/field`, { headers });
const fields = await response.json();
const epicLinkField = fields.find(f =>
f.name === 'Epic Link' ||
f.name.toLowerCase().includes('epic link')
);
return epicLinkField?.id; // 通常是 customfield_10014
}
批量删除问题
async function bulkDeleteIssues(projectKey, maxResults = 100) {
// 搜索所有问题
const jql = encodeURIComponent(`project = ${projectKey} ORDER BY key ASC`);
const searchResponse = await fetch(
`${JIRA_URL}/rest/api/3/search/jql?jql=${jql}&maxResults=${maxResults}&fields=key`,
{ headers }
);
const { issues } = await searchResponse.json();
// 删除每个问题
for (const issue of issues) {
await fetch(`${JIRA_URL}/rest/api/3/issue/${issue.key}?deleteSubtasks=true`, {
method: 'DELETE',
headers
});
console.log(`已删除: ${issue.key}`);
await new Promise(r => setTimeout(r, 100)); // 速率限制
}
return issues.length;
}
SAFe 最佳实践
Epic 命名
- 格式:
[领域] - [业务成果] - 示例:
Marketing Copilot - 实现 24/7 品牌意识内容生成
故事命名(INVEST 标准)
- Independent: 可以独立开发
- Negotiable: 细节可以讨论
- Valuable: 提供用户价值
- Estimable: 可以估算大小
- Small: 适合在一个 sprint 中完成
- Testable: 有清晰的验收标准
故事格式
作为一个 [特定角色],
我想要 [具体行动/能力],
以便 [可衡量的好处]。
验收标准(Given-When-Then)
场景:[描述性名称]
GIVEN [初始上下文/前提条件]
WHEN [行动/事件发生]
THEN [预期结果]
AND [如果需要,额外的结果]
问题链接类型(Next-Gen)
| 链接类型 | 用例 | 字段 |
|---|---|---|
| 父级 (Next-Gen) | 故事 → Epic | parent: { key: ‘EPIC-KEY’ } |
| 父级 (Next-Gen) | 子任务 → 故事 | parent: { key: ‘STORY-KEY’ } |
| Blocks/Is blocked by | 依赖关系 | 链接类型 |
| Relates to | 相关项目 | 链接类型 |
仅限经典项目:
| 链接类型 | 用例 | 字段 |
|---|---|---|
| Epic 链接 | 故事 → Epic | customfield_10014 |
| Epic 名称 | Epic 简称 | customfield_10011 |
按项目类型划分的自定义字段
Next-Gen(团队管理) - SCRUM 项目
| 目的 | 方法 |
|---|---|
| 将故事链接到 Epic | parent: { key: ‘EPIC-KEY’ } |
| 将子任务链接到故事 | parent: { key: ‘STORY-KEY’ } |
| 子任务问题类型 | issuetype: { name: ‘Subtask’ } |
经典(公司管理)
| 字段 | ID (典型) | 目的 |
|---|---|---|
| Epic 链接 | customfield_10014 | 将故事链接到 Epic |
| Epic 名称 | customfield_10011 | Epic 的简称 |
| 故事点 | customfield_10016 | 估算 |
| Sprint | customfield_10007 | Sprint 分配 |
错误处理
async function safeJiraRequest(url, options = {}) {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
const error = await response.text();
throw new Error(`Jira API ${response.status}: ${error.substring(0, 200)}`);
}
if (response.status === 204) return null;
return response.json();
}