LogoSkills

zenhub-changelog

ZenHub의 종료된 이슈로부터 릴리스 노트/체인지로그를 생성합니다(태그 또는 스프린트 기간 기준).

/cc-dev:zenhub:changelog — ZenHub 릴리스 노트 생성기#

항목내용
실행 명령/cc-dev:zenhub:changelog
분류워크플로우
난이도●●○ 보통
MCP 서버zenhub

한마디로#

지난 출시 이후 "끝난 일(닫힌 이슈)"을 모아서 릴리스 노트를 자동으로 써 주는 도구입니다. 회의록 대신, 영수증을 모아 한 장의 "이번에 한 일" 요약지를 출력해 준다고 보면 됩니다. 커밋 메시지가 아니라 ZenHub에서 닫힌 이슈를 근거(source)로 삼습니다.

누가·언제 쓰나요#

  • 새 버전을 배포하기 직전, "이번 릴리스에 뭐가 들어갔지?"를 정리할 때
  • 스프린트가 끝나서 그 기간에 마무리한 일을 한 장으로 보고할 때
  • 기존 커밋 기반 CHANGELOG가 prefix(feat:, fix:)에 의존해 들쭉날쭉할 때, 이슈 타입을 기준으로 깔끔하게 정리하고 싶을 때

무엇을 해주나요#

  • "어디서부터 어디까지"의 기간(윈도우)을 정해 줍니다 — 마지막 git tag 이후, 또는 지정한 스프린트 기간
  • 그 기간에 닫힌 이슈만 모읍니다 (보드 칸이 아니라 GitHub에서 실제로 닫힌 것 기준)
  • 이슈 타입·라벨로 자동 분류해 줍니다 — Added(기능 추가), Fixed(버그 수정), Changed(개선), Docs 등
  • 각 항목에 연결된 PR 링크를 붙여 추적할 수 있게 해 줍니다
  • 결과를 마크다운으로 출력하고, 원하면 CHANGELOG.md 맨 위에 새 섹션으로 끼워 넣어 줍니다

어떻게 쓰나요#

# 가장 최근 tag 이후 닫힌 이슈로 릴리스 노트 생성 (출력만)
/cc-dev:zenhub:changelog --dry-run

# 특정 tag 이후로 집계해 CHANGELOG.md 에 prepend
/cc-dev:zenhub:changelog --since-tag v2.13.0 --out CHANGELOG.md

# 현재(활성) 스프린트 기간으로 집계
/cc-dev:zenhub:changelog --since-sprint current

# 이름으로 스프린트 지정
/cc-dev:zenhub:changelog --since-sprint  " Sprint 24 "
  • --since-tag <tag> — 이 tag 이후 닫힌 이슈만 (생략 시 가장 최근 tag)
  • --since-sprint current|next|<이름> — tag 대신 스프린트 기간으로 윈도우 결정
  • --dry-run — 파일을 건드리지 않고 화면에만 출력
  • --out <경로> — 결과를 넣을 파일 (기본: stdout / CHANGELOG.md 섹션)

안에서 무슨 일이 벌어지나요#

  1. 기간 정하기--since-tag면 그 tag의 날짜를, --since-sprint면 스프린트의 시작·종료일을 윈도우로 잡습니다.
  2. 닫힌 이슈 모으기 — ZenHub에서 그 기간에 닫힌 이슈를 검색해 모읍니다.
  3. 분류하기 — 이슈 타입(Feature/Bug/Task 등)과 라벨을 보고 Added·Fixed·Changed·Docs로 묶습니다.
  4. PR 연결 — 각 이슈에 연결된 PR을 찾아 링크를 붙입니다.
  5. 출력/저장 — 마크다운 릴리스 노트를 출력하고, --dry-run이 아니면 지정 파일 맨 위에 새 섹션으로 끼워 넣습니다.

참고: "닫힘"은 보드의 칸이 아니라 GitHub에서 이슈가 실제로 닫힌 상태를 뜻합니다. (Done 칸은 닫힘이 아닙니다.) 그래서 이 도구는 커밋이 아니라 닫힌 이슈를 source of truth로 씁니다.


⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Triggers#

  • Before cutting a release — assemble "what shipped" from closed issues
  • At sprint end — summarize work completed in the sprint window
  • When commit-prefix-based CHANGELOG drifts; switch source of truth to issue type
  • Can be surfaced as a /cc-dev:session:wrap follow-up on sprint close (see Key Rules)

Context Trigger Pattern#

/cc-dev:zenhub:changelog [--since-tag  < tag > ] [--since-sprint current|next| < name > ] [--dry-run] [--out  < path > ]

Options / Actions#

OptionDescriptionDefault
--since-tag <tag>Window start = the tag's date; collect issues closed after itlatest tag (git describe --tags --abbrev=0)
--since-sprint current|next|<name>Window = the sprint's start/end (overrides --since-tag)
--dry-runPrint to stdout only; do not write any fileoff
--out <path>File to prepend the new section intostdout / CHANGELOG.md

Execution Flow#

1. Resolve the window (date range)#

// sprint window takes precedence over tag window
let windowStart, windowEnd = new Date().toISOString();
if (opts.sinceSprint) {
  const sprint = await resolveSprint(opts.sinceSprint); // see zenhub-conventions  " Sprint Selector Resolution " 
   if (!sprint) throw new Error(`스프린트  ' ${opts.sinceSprint} '   못 찾음 (current/next/ < name > ).`);
  windowStart = sprint.startAt;            // sprint 기간 = 윈도우
  windowEnd   = sprint.endAt ?? windowEnd;
} else {
  const tag = opts.sinceTag
    ?? (await Bash(`git describe --tags --abbrev=0`)).trim(); // 기본: 가장 최근 tag
  // tag 날짜 (release 우선, 없으면 git log)
  windowStart = (await Bash(
    `gh release view ${tag} --json createdAt -q .createdAt 2 > /dev/null `
    + `|| git log -1 --format=%cI ${tag}`
  )).trim();
}

2. Collect issues closed inside the window#

// searchClosedIssues 는 close 검증에 쓰던 도구 — 여기선 윈도우 집계로 확장.
const raw = await mcp__zenhub__searchClosedIssues({ query:  " * "   });
const inWindow = raw.filter(i = > 
   i.closedAt  & &   i.closedAt  > = windowStart  & &   i.closedAt  < = windowEnd
);
// 정확 매칭이 필요하면 #N 은 번호로(부분 문자열 아님): i.number === n

3. Group by issue type + label#

const issueTypes = await mcp__zenhub__getIssueTypes();
// 타입/라벨 → 섹션 매핑 (커밋 prefix 대신 이슈 타입이 source of truth)
function sectionOf(issue) {
  const type = (issue.issueType?.name ??  " " ).toLowerCase();
  const labels = (issue.labels ?? []).map(l = >   l.name.toLowerCase());
  if (type ===  " feature "   || labels.includes( " feature " ) || labels.includes( " feat " )) return  " Added " ;
  if (type ===  " bug "       || labels.includes( " bug " )     || labels.includes( " fix " ))  return  " Fixed " ;
  if (type ===  " task "      || labels.includes( " chore " )   || labels.includes( " refactor " )) return  " Changed " ;
  if (labels.includes( " docs " ) || labels.includes( " documentation " )) return  " Docs " ;
  return  " Other " ;
}
const sections = { Added: [], Fixed: [], Changed: [], Docs: [], Other: [] };
for (const i of inWindow) sections[sectionOf(i)].push(i);
for (const i of inWindow) {
  // gh 로 이슈에 연결된 PR (timeline 의 cross-reference / closed-by)
  const prs = (await Bash(
    `gh issue view ${i.number} --json closedByPullRequestsReferences `
    + `-q  ' .closedByPullRequestsReferences[]?.url '   2 > /dev/null`
  )).trim().split( " \n " ).filter(Boolean);
  i._prLinks = prs; // 비면 PR 백링크 생략
}

5. Render markdown + (optionally) prepend#

const version = opts.sinceSprint ?? `since ${opts.sinceTag ??  " latest tag " }`;
const date = windowEnd.slice(0, 10);
let md = `## ${version} — ${date}\n\n`;
for (const [name, items] of Object.entries(sections)) {
  if (!items.length) continue;
  md += `### ${name}\n`;
  for (const i of items) {
    const pr = i._prLinks?.length ? ` (${i._prLinks.join( " ,  " )})` :  " " ;
    md += `- ${i.title} (#${i.number})${pr}\n`;
  }
  md +=  " \n " ;
}

if (opts.dryRun) {
  console.log(md);                      // 파일 안 쓰고 출력만
} else {
  const out = opts.out ??  " CHANGELOG.md " ;
  const prev = (await Bash(`cat ${out} 2 > /dev/null || true`));
  await Write(out, md + (prev ?  " \n "   + prev :  " " )); // 맨 위에 prepend
}

MCP Tool Usage#

// 스프린트 해석 — zenhub-conventions  " Sprint Selector Resolution "   그대로 재사용 (재정의 금지)
async function resolveSprint(selector) {
  if (selector ===  " next " ) return await mcp__zenhub__getUpcomingSprint();
  if (!selector || selector ===  " current " ) return await mcp__zenhub__getSprint(); // 활성
  const { openSprints } = await mcp__zenhub__listRecentSprints();
  return openSprints.find(s = >   s.name.includes(String(selector)));
}

// 닫힌 이슈 집계 (윈도우 필터는 closedAt 으로)
const closed = await mcp__zenhub__searchClosedIssues({ query:  " * "   });

// 타입 분류용
const issueTypes = await mcp__zenhub__getIssueTypes();

// 단건 #N 확인이 필요하면 (getIssue 없음) searchLatestIssues 로 조회 후 number 정확 매칭
const hit = (await mcp__zenhub__searchLatestIssues({ query: `#${n}` }))
  .find(i = >   i.number === n);

Key Rules#

  1. Closed issues are the source of truth, not commit prefixes. Group by getIssueTypes() + labels (Feature→Added, Bug→Fixed, Task/refactor→Changed, docs→Docs). This removes dependence on feat:/fix: commit hygiene.
  2. Respect the 2-state model. "Closed" = GitHub issue state (searchClosedIssues / closedAt), never a Done/Closed board column — this workspace has no such pipeline. This command reads closed state; it never closes (close = gh issue close + updateIssue state:CLOSED, defined in /cc-dev:zenhub:manage).
  3. #N is an exact number match, not a substring — filter with i.number === n, never String(i.number).includes(...), to avoid #1 matching #1413.
  4. Window precedence: --since-sprint overrides --since-tag. Tag date prefers gh release view, falls back to git log. Filter strictly on closedAt within [windowStart, windowEnd].
  5. Sprint resolution is shared, not redefined — use resolveSprint per zenhub-conventions "Sprint Selector Resolution" (currentgetSprint(), nextgetUpcomingSprint(), num/name→listRecentSprints().openSprints). current must NEVER map to getUpcomingSprint.
  6. Only real MCP tools. No getIssue / getChildrenOfParent — resolve single issues via searchLatestIssues. Matrix signals (effort=setIssueEstimate, impact=immutable impact-* label) per zenhub-conventions are read-only context here, not written.
  7. --dry-run writes nothing; without it, prepend the new section to the top of --out (default CHANGELOG.md), preserving prior content.
  8. Sprint-close trigger: /cc-dev:session:wrap may surface /cc-dev:zenhub:changelog --since-sprint current as a follow-up when a sprint ends, so the release note is generated from the just-closed sprint window.