JiraSAFe jira-safe

在 Jira 中实施 SAFe 方法论,优化 Epic、特性、故事和任务的管理,提高敏捷规模化效率。

DevOps 0 次安装 0 次浏览 更新于 2/28/2026

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();
}

参考资料