LogoSkills

zenhub:changelog

Generate release notes / changelog from ZenHub closed issues (tag- or sprint-windowed)

ํ•ญ๋ชฉ๋‚ด์šฉ
Invoke/zenhub:changelog
Categorykobic-workflow
Complexitymoderate
MCP Serverszenhub

/zenhub:changelog โ€” ZenHub ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ ์ƒ์„ฑ๊ธฐ#

ํ•œ๋งˆ๋””๋กœ#

์ง€๋‚œ ์ถœ์‹œ ์ดํ›„ "๋๋‚œ ์ผ(๋‹ซํžŒ ์ด์Šˆ)"์„ ๋ชจ์•„์„œ ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ๋ฅผ ์ž๋™์œผ๋กœ ์จ ์ฃผ๋Š” ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ํšŒ์˜๋ก ๋Œ€์‹ , ์˜์ˆ˜์ฆ์„ ๋ชจ์•„ ํ•œ ์žฅ์˜ "์ด๋ฒˆ์— ํ•œ ์ผ" ์š”์•ฝ์ง€๋ฅผ ์ถœ๋ ฅํ•ด ์ค€๋‹ค๊ณ  ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ZenHub์—์„œ ๋‹ซํžŒ ์ด์Šˆ๋ฅผ ๊ทผ๊ฑฐ(source)๋กœ ์‚ผ์Šต๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ยท์–ธ์ œ ์“ฐ๋‚˜์š”#

  • ์ƒˆ ๋ฒ„์ „์„ ๋ฐฐํฌํ•˜๊ธฐ ์ง์ „, "์ด๋ฒˆ ๋ฆด๋ฆฌ์Šค์— ๋ญ๊ฐ€ ๋“ค์–ด๊ฐ”์ง€?"๋ฅผ ์ •๋ฆฌํ•  ๋•Œ
  • ์Šคํ”„๋ฆฐํŠธ๊ฐ€ ๋๋‚˜์„œ ๊ทธ ๊ธฐ๊ฐ„์— ๋งˆ๋ฌด๋ฆฌํ•œ ์ผ์„ ํ•œ ์žฅ์œผ๋กœ ๋ณด๊ณ ํ•  ๋•Œ
  • ๊ธฐ์กด ์ปค๋ฐ‹ ๊ธฐ๋ฐ˜ CHANGELOG๊ฐ€ prefix(feat:, fix:)์— ์˜์กดํ•ด ๋“ค์ญ‰๋‚ ์ญ‰ํ•  ๋•Œ, ์ด์Šˆ ํƒ€์ž…์„ ๊ธฐ์ค€์œผ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ

๋ฌด์—‡์„ ํ•ด์ฃผ๋‚˜์š”#

  • "์–ด๋””์„œ๋ถ€ํ„ฐ ์–ด๋””๊นŒ์ง€"์˜ ๊ธฐ๊ฐ„(์œˆ๋„์šฐ)์„ ์ •ํ•ด ์ค๋‹ˆ๋‹ค โ€” ๋งˆ์ง€๋ง‰ git tag ์ดํ›„, ๋˜๋Š” ์ง€์ •ํ•œ ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๊ฐ„
  • ๊ทธ ๊ธฐ๊ฐ„์— ๋‹ซํžŒ ์ด์Šˆ๋งŒ ๋ชจ์๋‹ˆ๋‹ค (๋ณด๋“œ ์นธ์ด ์•„๋‹ˆ๋ผ GitHub์—์„œ ์‹ค์ œ๋กœ ๋‹ซํžŒ ๊ฒƒ ๊ธฐ์ค€)
  • ์ด์Šˆ ํƒ€์ž…ยท๋ผ๋ฒจ๋กœ ์ž๋™ ๋ถ„๋ฅ˜ํ•ด ์ค๋‹ˆ๋‹ค โ€” Added(๊ธฐ๋Šฅ ์ถ”๊ฐ€), Fixed(๋ฒ„๊ทธ ์ˆ˜์ •), Changed(๊ฐœ์„ ), Docs ๋“ฑ
  • ๊ฐ ํ•ญ๋ชฉ์— ์—ฐ๊ฒฐ๋œ PR ๋งํฌ๋ฅผ ๋ถ™์—ฌ ์ถ”์ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด ์ค๋‹ˆ๋‹ค
  • ๊ฒฐ๊ณผ๋ฅผ ๋งˆํฌ๋‹ค์šด์œผ๋กœ ์ถœ๋ ฅํ•˜๊ณ , ์›ํ•˜๋ฉด CHANGELOG.md ๋งจ ์œ„์— ์ƒˆ ์„น์…˜์œผ๋กœ ๋ผ์›Œ ๋„ฃ์–ด ์ค๋‹ˆ๋‹ค

์–ด๋–ป๊ฒŒ ์“ฐ๋‚˜์š”#

# ๊ฐ€์žฅ ์ตœ๊ทผ tag ์ดํ›„ ๋‹ซํžŒ ์ด์Šˆ๋กœ ๋ฆด๋ฆฌ์Šค ๋…ธํŠธ ์ƒ์„ฑ (์ถœ๋ ฅ๋งŒ)
/zenhub:changelog --dry-run

# ํŠน์ • tag ์ดํ›„๋กœ ์ง‘๊ณ„ํ•ด CHANGELOG.md ์— prepend
/zenhub:changelog --since-tag v2.13.0 --out CHANGELOG.md

# ํ˜„์žฌ(ํ™œ์„ฑ) ์Šคํ”„๋ฆฐํŠธ ๊ธฐ๊ฐ„์œผ๋กœ ์ง‘๊ณ„
/zenhub:changelog --since-sprint current

# ์ด๋ฆ„์œผ๋กœ ์Šคํ”„๋ฆฐํŠธ ์ง€์ •
/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 /session:wrap follow-up on sprint close (see Key Rules)

Context Trigger Pattern#

/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 /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" (currentโ†’getSprint(), nextโ†’getUpcomingSprint(), 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: /session:wrap may surface /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.