| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Invoke | /zenhub:changelog |
| Category | kobic-workflow |
| Complexity | moderate |
| MCP Servers | zenhub |
/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์น์ )
์์์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ง๋์#
-
๊ธฐ๊ฐ ์ ํ๊ธฐ โ
--since-tag๋ฉด ๊ทธ tag์ ๋ ์ง๋ฅผ,--since-sprint๋ฉด ์คํ๋ฆฐํธ์ ์์ยท์ข ๋ฃ์ผ์ ์๋์ฐ๋ก ์ก์ต๋๋ค. - ๋ซํ ์ด์ ๋ชจ์ผ๊ธฐ โ ZenHub์์ ๊ทธ ๊ธฐ๊ฐ์ ๋ซํ ์ด์๋ฅผ ๊ฒ์ํด ๋ชจ์๋๋ค.
- ๋ถ๋ฅํ๊ธฐ โ ์ด์ ํ์ (Feature/Bug/Task ๋ฑ)๊ณผ ๋ผ๋ฒจ์ ๋ณด๊ณ AddedยทFixedยทChangedยทDocs๋ก ๋ฌถ์ต๋๋ค.
- PR ์ฐ๊ฒฐ โ ๊ฐ ์ด์์ ์ฐ๊ฒฐ๋ PR์ ์ฐพ์ ๋งํฌ๋ฅผ ๋ถ์ ๋๋ค.
- ์ถ๋ ฅ/์ ์ฅ โ ๋งํฌ๋ค์ด ๋ฆด๋ฆฌ์ค ๋
ธํธ๋ฅผ ์ถ๋ ฅํ๊ณ ,
--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:wrapfollow-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#
| Option | Description | Default |
|---|---|---|
--since-tag <tag> | Window start = the tag's date; collect issues closed after it | latest tag (git describe --tags --abbrev=0) |
--since-sprint current|next|<name> | Window = the sprint's start/end (overrides --since-tag) | โ |
--dry-run | Print to stdout only; do not write any file | off |
--out <path> | File to prepend the new section into | stdout / 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 === n3. 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);4. Backlink the PR(s) for each issue#
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#
- 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 onfeat:/fix:commit hygiene. - Respect the 2-state model. "Closed" = GitHub issue state (
searchClosedIssues/closedAt), never aDone/Closedboard 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). #Nis an exact number match, not a substring โ filter withi.number === n, neverString(i.number).includes(...), to avoid#1matching#1413.- Window precedence:
--since-sprintoverrides--since-tag. Tag date prefersgh release view, falls back togit log. Filter strictly onclosedAtwithin[windowStart, windowEnd]. - Sprint resolution is shared, not redefined โ use
resolveSprintper zenhub-conventions "Sprint Selector Resolution" (currentโgetSprint(),nextโgetUpcomingSprint(), num/nameโlistRecentSprints().openSprints).currentmust NEVER map togetUpcomingSprint. - Only real MCP tools. No
getIssue/getChildrenOfParentโ resolve single issues viasearchLatestIssues. Matrix signals (effort=setIssueEstimate, impact=immutableimpact-*label) per zenhub-conventions are read-only context here, not written. --dry-runwrites nothing; without it, prepend the new section to the top of--out(defaultCHANGELOG.md), preserving prior content.- Sprint-close trigger:
/session:wrapmay surface/zenhub:changelog --since-sprint currentas a follow-up when a sprint ends, so the release note is generated from the just-closed sprint window.