[{"data":1,"prerenderedAt":1549},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits\u002F":3,"content-directory":1399},{"id":4,"title":5,"body":6,"date":1384,"description":1385,"difficulty":1386,"draft":1387,"extension":1388,"meta":1389,"navigation":270,"path":1390,"seo":1391,"stem":1392,"tags":1393,"updated":1384,"__hash__":1398},"content\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits\u002Findex.md","Automating Changelogs with Conventional Commits",{"type":7,"value":8,"toc":1373},"minimark",[9,26,31,107,111,119,129,166,238,248,302,312,316,319,429,441,445,459,539,545,610,626,630,638,874,887,934,941,945,961,1027,1033,1037,1049,1200,1207,1252,1272,1276,1339,1343,1369],[10,11,12,13,17,18,21,22,25],"p",{},"A hand-written ",[14,15,16],"code",{},"CHANGELOG.md"," rots the moment someone forgets to update it. If your commit messages already describe every change in a structured way, the changelog can be generated instead of remembered — and the same structured messages can tell you whether the next release is a patch, a minor, or a major. This guide shows the Conventional Commits format, how it maps to semantic versioning, how to enforce it, and how to turn your git history into release notes with ",[14,19,20],{},"git-cliff"," (with ",[14,23,24],{},"towncrier"," as the alternative).",[27,28,30],"h2",{"id":29},"tldr","TL;DR",[32,33,34,56,72,80,96],"ul",{},[35,36,37,38,41,42,45,46,45,49,45,52,55],"li",{},"Write commits as ",[14,39,40],{},"type(scope): summary"," — ",[14,43,44],{},"feat:",", ",[14,47,48],{},"fix:",[14,50,51],{},"docs:",[14,53,54],{},"refactor:",", etc.",[35,57,58,60,61,63,64,67,68,71],{},[14,59,48],{}," → patch bump, ",[14,62,44],{}," → minor bump, ",[14,65,66],{},"BREAKING CHANGE:"," (or ",[14,69,70],{},"feat!:",") → major bump.",[35,73,74,75,79],{},"Enforce the format at commit time with ",[76,77,78],"strong",{},"commitlint"," run from a pre-commit hook so bad messages never land.",[35,81,82,83,85,86,88,89,92,93,95],{},"Generate ",[14,84,16],{}," from history with ",[76,87,20],{}," and a small ",[14,90,91],{},"cliff.toml","; ",[76,94,24],{}," is the fragment-file alternative.",[35,97,98,99,102,103,106],{},"Keep ",[76,100,101],{},"one source of version truth"," (",[14,104,105],{},"pyproject.toml",") and let the release step read it — don't maintain the version in three places.",[27,108,110],{"id":109},"the-conventional-commits-format","The Conventional Commits format",[10,112,113,114,118],{},"Conventional Commits is a lightweight convention layered on your commit ",[115,116,117],"em",{},"subject line",". The structure is:",[120,121,126],"pre",{"className":122,"code":124,"language":125},[123],"language-text","\u003Ctype>[optional scope][!]: \u003Cdescription>\n\n[optional body]\n\n[optional footer, e.g. BREAKING CHANGE: ...]\n","text",[14,127,124],{"__ignoreMap":128},"",[10,130,131,132,135,136,139,140,143,144,45,147,45,150,45,153,45,156,45,159,45,162,165],{},"The ",[14,133,134],{},"type"," is a small vocabulary. The two that drive releases are ",[14,137,138],{},"feat"," (a new feature) and ",[14,141,142],{},"fix"," (a bug fix); the rest — ",[14,145,146],{},"docs",[14,148,149],{},"refactor",[14,151,152],{},"test",[14,154,155],{},"chore",[14,157,158],{},"ci",[14,160,161],{},"perf",[14,163,164],{},"build"," — describe non-release-affecting work but still organize the changelog. Real examples from a CLI project:",[120,167,171],{"className":168,"code":169,"language":170,"meta":128,"style":128},"language-bash shiki shiki-themes github-light github-dark","$ git commit -m \"feat(config): support TOML config files\"\n$ git commit -m \"fix(parser): reject negative --retries values\"\n$ git commit -m \"docs: document the --json output flag\"\n$ git commit -m \"refactor(core): split resolver into its own module\"\n","bash",[14,172,173,196,210,224],{"__ignoreMap":128},[174,175,178,182,186,189,193],"span",{"class":176,"line":177},"line",1,[174,179,181],{"class":180},"sScJk","$",[174,183,185],{"class":184},"sZZnC"," git",[174,187,188],{"class":184}," commit",[174,190,192],{"class":191},"sj4cs"," -m",[174,194,195],{"class":184}," \"feat(config): support TOML config files\"\n",[174,197,199,201,203,205,207],{"class":176,"line":198},2,[174,200,181],{"class":180},[174,202,185],{"class":184},[174,204,188],{"class":184},[174,206,192],{"class":191},[174,208,209],{"class":184}," \"fix(parser): reject negative --retries values\"\n",[174,211,213,215,217,219,221],{"class":176,"line":212},3,[174,214,181],{"class":180},[174,216,185],{"class":184},[174,218,188],{"class":184},[174,220,192],{"class":191},[174,222,223],{"class":184}," \"docs: document the --json output flag\"\n",[174,225,227,229,231,233,235],{"class":176,"line":226},4,[174,228,181],{"class":180},[174,230,185],{"class":184},[174,232,188],{"class":184},[174,234,192],{"class":191},[174,236,237],{"class":184}," \"refactor(core): split resolver into its own module\"\n",[10,239,240,241,244,245,247],{},"A breaking change is signaled two equivalent ways: a ",[14,242,243],{},"!"," after the type\u002Fscope, or a ",[14,246,66],{}," footer. Either is enough for tooling to trigger a major bump.",[120,249,251],{"className":168,"code":250,"language":170,"meta":128,"style":128},"$ git commit -m \"feat(cli)!: rename --output to --out\"\n\n# or, with a footer explaining the migration\n$ git commit -m \"feat(cli): rename --output to --out\n\nBREAKING CHANGE: the --output flag is now --out; update scripts accordingly.\"\n",[14,252,253,266,272,278,291,296],{"__ignoreMap":128},[174,254,255,257,259,261,263],{"class":176,"line":177},[174,256,181],{"class":180},[174,258,185],{"class":184},[174,260,188],{"class":184},[174,262,192],{"class":191},[174,264,265],{"class":184}," \"feat(cli)!: rename --output to --out\"\n",[174,267,268],{"class":176,"line":198},[174,269,271],{"emptyLinePlaceholder":270},true,"\n",[174,273,274],{"class":176,"line":212},[174,275,277],{"class":276},"sJ8bj","# or, with a footer explaining the migration\n",[174,279,280,282,284,286,288],{"class":176,"line":226},[174,281,181],{"class":180},[174,283,185],{"class":184},[174,285,188],{"class":184},[174,287,192],{"class":191},[174,289,290],{"class":184}," \"feat(cli): rename --output to --out\n",[174,292,294],{"class":176,"line":293},5,[174,295,271],{"emptyLinePlaceholder":270},[174,297,299],{"class":176,"line":298},6,[174,300,301],{"class":184},"BREAKING CHANGE: the --output flag is now --out; update scripts accordingly.\"\n",[10,303,304,305,45,308,311],{},"The scope in parentheses is optional but valuable in a CLI: scoping commits to ",[14,306,307],{},"parser",[14,309,310],{},"config",", or a specific subcommand lets you group changelog entries by area later.",[27,313,315],{"id":314},"mapping-commit-types-to-semantic-versioning","Mapping commit types to semantic versioning",[10,317,318],{},"The reason to bother with the format is that it makes the version bump mechanical. Given the commits since your last tag, the highest-priority change wins:",[320,321,322,338],"table",{},[323,324,325],"thead",{},[326,327,328,332,335],"tr",{},[329,330,331],"th",{},"Commit signal",[329,333,334],{},"Semantic version bump",[329,336,337],{},"Example",[339,340,341,366,385,407],"tbody",{},[326,342,343,352,361],{},[344,345,346,348,349],"td",{},[14,347,66],{}," \u002F ",[14,350,351],{},"type!:",[344,353,354,102,357,360],{},[76,355,356],{},"major",[14,358,359],{},"1.4.2 → 2.0.0",")",[344,362,363],{},[14,364,365],{},"feat!: drop Python 3.10",[326,367,368,372,380],{},[344,369,370],{},[14,371,44],{},[344,373,374,102,377,360],{},[76,375,376],{},"minor",[14,378,379],{},"1.4.2 → 1.5.0",[344,381,382],{},[14,383,384],{},"feat: add --json flag",[326,386,387,394,402],{},[344,388,389,348,391],{},[14,390,48],{},[14,392,393],{},"perf:",[344,395,396,102,399,360],{},[76,397,398],{},"patch",[14,400,401],{},"1.4.2 → 1.4.3",[344,403,404],{},[14,405,406],{},"fix: handle empty input",[326,408,409,421,424],{},[344,410,411,348,413,348,416,348,418],{},[14,412,51],{},[14,414,415],{},"chore:",[14,417,54],{},[14,419,420],{},"test:",[344,422,423],{},"none by default",[344,425,426],{},[14,427,428],{},"chore: bump ruff",[10,430,431,432,434,435,440],{},"For a CLI this discipline matters more than for a library, because your users pin your tool and script around its flags. A renamed flag or a changed exit code is a breaking change even if the code diff looks tiny — mark it with ",[14,433,243],{},". Choosing which changes are breaking is closely tied to how you assign meaning to ",[436,437,439],"a",{"href":438},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","exit codes",": changing an exit code is a contract change your changelog must announce.",[27,442,444],{"id":443},"enforcing-the-format-with-commitlint-and-pre-commit","Enforcing the format with commitlint and pre-commit",[10,446,447,448,450,451,454,455,458],{},"Automation only works if the messages are actually well-formed, so enforce the convention at commit time rather than hoping. ",[14,449,78],{}," validates a message against the Conventional Commits rules, and you can wire it into the ",[14,452,453],{},"commit-msg"," stage via the ",[14,456,457],{},"pre-commit"," framework:",[120,460,464],{"className":461,"code":462,"language":463,"meta":128,"style":128},"language-yaml shiki shiki-themes github-light github-dark","# .pre-commit-config.yaml\nrepos:\n  - repo: https:\u002F\u002Fgithub.com\u002Fcompilerla\u002Fconventional-pre-commit\n    rev: v3.6.0\n    hooks:\n      - id: conventional-pre-commit\n        stages: [commit-msg]\n","yaml",[14,465,466,471,481,495,505,512,525],{"__ignoreMap":128},[174,467,468],{"class":176,"line":177},[174,469,470],{"class":276},"# .pre-commit-config.yaml\n",[174,472,473,477],{"class":176,"line":198},[174,474,476],{"class":475},"s9eBZ","repos",[174,478,480],{"class":479},"sVt8B",":\n",[174,482,483,486,489,492],{"class":176,"line":212},[174,484,485],{"class":479},"  - ",[174,487,488],{"class":475},"repo",[174,490,491],{"class":479},": ",[174,493,494],{"class":184},"https:\u002F\u002Fgithub.com\u002Fcompilerla\u002Fconventional-pre-commit\n",[174,496,497,500,502],{"class":176,"line":226},[174,498,499],{"class":475},"    rev",[174,501,491],{"class":479},[174,503,504],{"class":184},"v3.6.0\n",[174,506,507,510],{"class":176,"line":293},[174,508,509],{"class":475},"    hooks",[174,511,480],{"class":479},[174,513,514,517,520,522],{"class":176,"line":298},[174,515,516],{"class":479},"      - ",[174,518,519],{"class":475},"id",[174,521,491],{"class":479},[174,523,524],{"class":184},"conventional-pre-commit\n",[174,526,528,531,534,536],{"class":176,"line":527},7,[174,529,530],{"class":475},"        stages",[174,532,533],{"class":479},": [",[174,535,453],{"class":184},[174,537,538],{"class":479},"]\n",[10,540,541,542,544],{},"Install the hook into the ",[14,543,453],{}," stage and the check runs on every commit:",[120,546,548],{"className":168,"code":547,"language":170,"meta":128,"style":128},"$ pre-commit install --hook-type commit-msg\n\n$ git commit -m \"fixed the bug\"\n[bad commit message] does not follow Conventional Commits — rejected\n\n$ git commit -m \"fix(parser): handle empty --config file\"\n[ok]\n",[14,549,550,566,570,583,588,592,605],{"__ignoreMap":128},[174,551,552,554,557,560,563],{"class":176,"line":177},[174,553,181],{"class":180},[174,555,556],{"class":184}," pre-commit",[174,558,559],{"class":184}," install",[174,561,562],{"class":191}," --hook-type",[174,564,565],{"class":184}," commit-msg\n",[174,567,568],{"class":176,"line":198},[174,569,271],{"emptyLinePlaceholder":270},[174,571,572,574,576,578,580],{"class":176,"line":212},[174,573,181],{"class":180},[174,575,185],{"class":184},[174,577,188],{"class":184},[174,579,192],{"class":191},[174,581,582],{"class":184}," \"fixed the bug\"\n",[174,584,585],{"class":176,"line":226},[174,586,587],{"class":479},"[bad commit message] does not follow Conventional Commits — rejected\n",[174,589,590],{"class":176,"line":293},[174,591,271],{"emptyLinePlaceholder":270},[174,593,594,596,598,600,602],{"class":176,"line":298},[174,595,181],{"class":180},[174,597,185],{"class":184},[174,599,188],{"class":184},[174,601,192],{"class":191},[174,603,604],{"class":184}," \"fix(parser): handle empty --config file\"\n",[174,606,607],{"class":176,"line":527},[174,608,609],{"class":479},"[ok]\n",[10,611,612,613,616,617,621,622,625],{},"Because this hangs off the same framework you already use for linting and formatting, it costs almost nothing to add. The broader setup — installing hooks, pinning ",[14,614,615],{},"rev","s, running in CI — is covered in ",[436,618,620],{"href":619},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002F","pre-commit hooks for CLI projects",". Enforcing the format is the piece that makes the ",[115,623,624],{},"generation"," step below trustworthy.",[27,627,629],{"id":628},"generating-changelogmd-with-git-cliff","Generating CHANGELOG.md with git-cliff",[10,631,632,634,635,637],{},[14,633,20],{}," reads your git history, groups commits by type, and renders a Markdown changelog from a template. Its config lives in ",[14,636,91],{},". Here is a compact one tuned for a CLI:",[120,639,643],{"className":640,"code":641,"language":642,"meta":128,"style":128},"language-toml shiki shiki-themes github-light github-dark","# cliff.toml\n[changelog]\nheader = \"# Changelog\\n\\nAll notable changes to this project are documented here.\\n\"\nbody = \"\"\"\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n### {{ group | upper_first }}\n{% for commit in commits %}\n- {{ commit.message | upper_first }}{% endfor %}\n{% endfor %}\n\"\"\"\ntrim = true\n\n[git]\nconventional_commits = true\nfilter_unconventional = true\ncommit_parsers = [\n  { message = \"^feat\", group = \"Features\" },\n  { message = \"^fix\", group = \"Bug Fixes\" },\n  { message = \"^perf\", group = \"Performance\" },\n  { message = \"^docs\", group = \"Documentation\" },\n  { message = \"^refactor\", group = \"Refactor\" },\n  { message = \"^chore\", skip = true },\n]\ntag_pattern = \"v[0-9]*\"\n","toml",[14,644,645,650,660,680,688,693,698,703,709,715,720,729,734,744,752,760,766,784,799,814,829,844,860,865],{"__ignoreMap":128},[174,646,647],{"class":176,"line":177},[174,648,649],{"class":276},"# cliff.toml\n",[174,651,652,655,658],{"class":176,"line":198},[174,653,654],{"class":479},"[",[174,656,657],{"class":180},"changelog",[174,659,538],{"class":479},[174,661,662,665,668,671,674,677],{"class":176,"line":212},[174,663,664],{"class":479},"header = ",[174,666,667],{"class":184},"\"# Changelog",[174,669,670],{"class":191},"\\n\\n",[174,672,673],{"class":184},"All notable changes to this project are documented here.",[174,675,676],{"class":191},"\\n",[174,678,679],{"class":184},"\"\n",[174,681,682,685],{"class":176,"line":226},[174,683,684],{"class":479},"body = ",[174,686,687],{"class":184},"\"\"\"\n",[174,689,690],{"class":176,"line":293},[174,691,692],{"class":184},"{% for group, commits in commits | group_by(attribute=\"group\") %}\n",[174,694,695],{"class":176,"line":298},[174,696,697],{"class":184},"### {{ group | upper_first }}\n",[174,699,700],{"class":176,"line":527},[174,701,702],{"class":184},"{% for commit in commits %}\n",[174,704,706],{"class":176,"line":705},8,[174,707,708],{"class":184},"- {{ commit.message | upper_first }}{% endfor %}\n",[174,710,712],{"class":176,"line":711},9,[174,713,714],{"class":184},"{% endfor %}\n",[174,716,718],{"class":176,"line":717},10,[174,719,687],{"class":184},[174,721,723,726],{"class":176,"line":722},11,[174,724,725],{"class":479},"trim = ",[174,727,728],{"class":191},"true\n",[174,730,732],{"class":176,"line":731},12,[174,733,271],{"emptyLinePlaceholder":270},[174,735,737,739,742],{"class":176,"line":736},13,[174,738,654],{"class":479},[174,740,741],{"class":180},"git",[174,743,538],{"class":479},[174,745,747,750],{"class":176,"line":746},14,[174,748,749],{"class":479},"conventional_commits = ",[174,751,728],{"class":191},[174,753,755,758],{"class":176,"line":754},15,[174,756,757],{"class":479},"filter_unconventional = ",[174,759,728],{"class":191},[174,761,763],{"class":176,"line":762},16,[174,764,765],{"class":479},"commit_parsers = [\n",[174,767,769,772,775,778,781],{"class":176,"line":768},17,[174,770,771],{"class":479},"  { message = ",[174,773,774],{"class":184},"\"^feat\"",[174,776,777],{"class":479},", group = ",[174,779,780],{"class":184},"\"Features\"",[174,782,783],{"class":479}," },\n",[174,785,787,789,792,794,797],{"class":176,"line":786},18,[174,788,771],{"class":479},[174,790,791],{"class":184},"\"^fix\"",[174,793,777],{"class":479},[174,795,796],{"class":184},"\"Bug Fixes\"",[174,798,783],{"class":479},[174,800,802,804,807,809,812],{"class":176,"line":801},19,[174,803,771],{"class":479},[174,805,806],{"class":184},"\"^perf\"",[174,808,777],{"class":479},[174,810,811],{"class":184},"\"Performance\"",[174,813,783],{"class":479},[174,815,817,819,822,824,827],{"class":176,"line":816},20,[174,818,771],{"class":479},[174,820,821],{"class":184},"\"^docs\"",[174,823,777],{"class":479},[174,825,826],{"class":184},"\"Documentation\"",[174,828,783],{"class":479},[174,830,832,834,837,839,842],{"class":176,"line":831},21,[174,833,771],{"class":479},[174,835,836],{"class":184},"\"^refactor\"",[174,838,777],{"class":479},[174,840,841],{"class":184},"\"Refactor\"",[174,843,783],{"class":479},[174,845,847,849,852,855,858],{"class":176,"line":846},22,[174,848,771],{"class":479},[174,850,851],{"class":184},"\"^chore\"",[174,853,854],{"class":479},", skip = ",[174,856,857],{"class":191},"true",[174,859,783],{"class":479},[174,861,863],{"class":176,"line":862},23,[174,864,538],{"class":479},[174,866,868,871],{"class":176,"line":867},24,[174,869,870],{"class":479},"tag_pattern = ",[174,872,873],{"class":184},"\"v[0-9]*\"\n",[10,875,876,879,880,882,883,886],{},[14,877,878],{},"commit_parsers"," decides which commits appear and under what heading; here ",[14,881,415],{}," commits are skipped from user-facing notes. Generating (or refreshing) the changelog is one command, and you can preview just the ",[115,884,885],{},"unreleased"," section:",[120,888,890],{"className":168,"code":889,"language":170,"meta":128,"style":128},"# Write\u002Frefresh the whole file\n$ git-cliff --output CHANGELOG.md\n\n# Preview only the notes since the last tag (great for release PRs)\n$ git-cliff --unreleased --strip header\n",[14,891,892,897,910,914,919],{"__ignoreMap":128},[174,893,894],{"class":176,"line":177},[174,895,896],{"class":276},"# Write\u002Frefresh the whole file\n",[174,898,899,901,904,907],{"class":176,"line":198},[174,900,181],{"class":180},[174,902,903],{"class":184}," git-cliff",[174,905,906],{"class":191}," --output",[174,908,909],{"class":184}," CHANGELOG.md\n",[174,911,912],{"class":176,"line":212},[174,913,271],{"emptyLinePlaceholder":270},[174,915,916],{"class":176,"line":226},[174,917,918],{"class":276},"# Preview only the notes since the last tag (great for release PRs)\n",[174,920,921,923,925,928,931],{"class":176,"line":293},[174,922,181],{"class":180},[174,924,903],{"class":184},[174,926,927],{"class":191}," --unreleased",[174,929,930],{"class":191}," --strip",[174,932,933],{"class":184}," header\n",[10,935,936,937,940],{},"git-cliff can also compute the next version for you from the commit types with ",[14,938,939],{},"git-cliff --bumped-version",", which closes the loop between \"what changed\" and \"what number comes next.\"",[27,942,944],{"id":943},"towncrier-the-fragment-file-alternative","towncrier: the fragment-file alternative",[10,946,947,949,950,952,953,956,957,960],{},[14,948,20],{}," derives everything from commit messages. ",[14,951,24],{}," takes the opposite approach: each change ships a small ",[115,954,955],{},"news fragment"," file in a ",[14,958,959],{},"newsfragments\u002F"," directory, and towncrier stitches them into the changelog at release time.",[120,962,964],{"className":168,"code":963,"language":170,"meta":128,"style":128},"# One fragment per change; the number is the PR\u002Fissue id, the suffix is the type\n$ cat newsfragments\u002F142.feature.md\nAdd a --json flag to emit machine-readable output.\n\n$ towncrier build --version 1.5.0\n",[14,965,966,971,981,1007,1011],{"__ignoreMap":128},[174,967,968],{"class":176,"line":177},[174,969,970],{"class":276},"# One fragment per change; the number is the PR\u002Fissue id, the suffix is the type\n",[174,972,973,975,978],{"class":176,"line":198},[174,974,181],{"class":180},[174,976,977],{"class":184}," cat",[174,979,980],{"class":184}," newsfragments\u002F142.feature.md\n",[174,982,983,986,989,992,995,998,1001,1004],{"class":176,"line":212},[174,984,985],{"class":180},"Add",[174,987,988],{"class":184}," a",[174,990,991],{"class":191}," --json",[174,993,994],{"class":184}," flag",[174,996,997],{"class":184}," to",[174,999,1000],{"class":184}," emit",[174,1002,1003],{"class":184}," machine-readable",[174,1005,1006],{"class":184}," output.\n",[174,1008,1009],{"class":176,"line":226},[174,1010,271],{"emptyLinePlaceholder":270},[174,1012,1013,1015,1018,1021,1024],{"class":176,"line":293},[174,1014,181],{"class":180},[174,1016,1017],{"class":184}," towncrier",[174,1019,1020],{"class":184}," build",[174,1022,1023],{"class":191}," --version",[174,1025,1026],{"class":191}," 1.5.0\n",[10,1028,1029,1030,1032],{},"The trade-off is explicit. towncrier fragments are prose written for humans, so the changelog reads better and avoids leaking terse commit subjects — but contributors must remember to add a fragment (enforceable via CI). git-cliff needs zero extra files but is only as good as your commit hygiene. Fragment files also sidestep merge conflicts on a single ",[14,1031,16],{},", which is why large projects often prefer towncrier. Choose git-cliff when you trust the commit convention; choose towncrier when you want editorial control over release notes.",[27,1034,1036],{"id":1035},"wiring-it-into-a-release-with-one-source-of-version-truth","Wiring it into a release with one source of version truth",[10,1038,1039,1040,45,1042,1045,1046,1048],{},"The failure mode to avoid is a version number that lives in three places — ",[14,1041,105],{},[14,1043,1044],{},"__init__.py",", and a git tag — that drift apart. Keep ",[14,1047,105],{}," as the single source and derive the rest. A minimal release flow:",[120,1050,1052],{"className":168,"code":1051,"language":170,"meta":128,"style":128},"# 1. Compute the next version from Conventional Commits\n$ NEXT=$(git-cliff --bumped-version)\n\n# 2. Update the one source of truth\n$ uv version \"${NEXT#v}\"        # or: poetry version \"${NEXT#v}\"\n\n# 3. Regenerate the changelog through the new tag\n$ git-cliff --tag \"$NEXT\" --output CHANGELOG.md\n\n# 4. Commit, tag, push\n$ git commit -am \"chore(release): $NEXT\"\n$ git tag \"$NEXT\"\n$ git push --follow-tags\n",[14,1053,1054,1059,1077,1081,1086,1115,1119,1124,1146,1150,1155,1173,1188],{"__ignoreMap":128},[174,1055,1056],{"class":176,"line":177},[174,1057,1058],{"class":276},"# 1. Compute the next version from Conventional Commits\n",[174,1060,1061,1063,1066,1069,1071,1074],{"class":176,"line":198},[174,1062,181],{"class":180},[174,1064,1065],{"class":184}," NEXT=",[174,1067,1068],{"class":479},"$(",[174,1070,20],{"class":180},[174,1072,1073],{"class":191}," --bumped-version",[174,1075,1076],{"class":479},")\n",[174,1078,1079],{"class":176,"line":212},[174,1080,271],{"emptyLinePlaceholder":270},[174,1082,1083],{"class":176,"line":226},[174,1084,1085],{"class":276},"# 2. Update the one source of truth\n",[174,1087,1088,1090,1093,1096,1099,1102,1106,1109,1112],{"class":176,"line":293},[174,1089,181],{"class":180},[174,1091,1092],{"class":184}," uv",[174,1094,1095],{"class":184}," version",[174,1097,1098],{"class":184}," \"${",[174,1100,1101],{"class":479},"NEXT",[174,1103,1105],{"class":1104},"szBVR","#",[174,1107,1108],{"class":479},"v",[174,1110,1111],{"class":184},"}\"",[174,1113,1114],{"class":276},"        # or: poetry version \"${NEXT#v}\"\n",[174,1116,1117],{"class":176,"line":298},[174,1118,271],{"emptyLinePlaceholder":270},[174,1120,1121],{"class":176,"line":527},[174,1122,1123],{"class":276},"# 3. Regenerate the changelog through the new tag\n",[174,1125,1126,1128,1130,1133,1136,1139,1142,1144],{"class":176,"line":705},[174,1127,181],{"class":180},[174,1129,903],{"class":184},[174,1131,1132],{"class":191}," --tag",[174,1134,1135],{"class":184}," \"",[174,1137,1138],{"class":479},"$NEXT",[174,1140,1141],{"class":184},"\"",[174,1143,906],{"class":191},[174,1145,909],{"class":184},[174,1147,1148],{"class":176,"line":711},[174,1149,271],{"emptyLinePlaceholder":270},[174,1151,1152],{"class":176,"line":717},[174,1153,1154],{"class":276},"# 4. Commit, tag, push\n",[174,1156,1157,1159,1161,1163,1166,1169,1171],{"class":176,"line":722},[174,1158,181],{"class":180},[174,1160,185],{"class":184},[174,1162,188],{"class":184},[174,1164,1165],{"class":191}," -am",[174,1167,1168],{"class":184}," \"chore(release): ",[174,1170,1138],{"class":479},[174,1172,679],{"class":184},[174,1174,1175,1177,1179,1182,1184,1186],{"class":176,"line":731},[174,1176,181],{"class":180},[174,1178,185],{"class":184},[174,1180,1181],{"class":184}," tag",[174,1183,1135],{"class":184},[174,1185,1138],{"class":479},[174,1187,679],{"class":184},[174,1189,1190,1192,1194,1197],{"class":176,"line":736},[174,1191,181],{"class":180},[174,1193,185],{"class":184},[174,1195,1196],{"class":184}," push",[174,1198,1199],{"class":191}," --follow-tags\n",[10,1201,1202,1203,1206],{},"If your code needs to report its own version at runtime (",[14,1204,1205],{},"mycli --version","), read it from the installed metadata rather than duplicating the string:",[120,1208,1212],{"className":1209,"code":1210,"language":1211,"meta":128,"style":128},"language-python shiki shiki-themes github-light github-dark","from importlib.metadata import version\n\n__version__ = version(\"mycli\")  # reads the value packaged from pyproject.toml\n","python",[14,1213,1214,1228,1232],{"__ignoreMap":128},[174,1215,1216,1219,1222,1225],{"class":176,"line":177},[174,1217,1218],{"class":1104},"from",[174,1220,1221],{"class":479}," importlib.metadata ",[174,1223,1224],{"class":1104},"import",[174,1226,1227],{"class":479}," version\n",[174,1229,1230],{"class":176,"line":198},[174,1231,271],{"emptyLinePlaceholder":270},[174,1233,1234,1237,1240,1243,1246,1249],{"class":176,"line":212},[174,1235,1236],{"class":191},"__version__",[174,1238,1239],{"class":1104}," =",[174,1241,1242],{"class":479}," version(",[174,1244,1245],{"class":184},"\"mycli\"",[174,1247,1248],{"class":479},")  ",[174,1250,1251],{"class":276},"# reads the value packaged from pyproject.toml\n",[10,1253,1254,1255,1257,1258,1261,1262,1266,1267,1271],{},"This keeps ",[14,1256,105],{}," authoritative: the build backend stamps it into the wheel, ",[14,1259,1260],{},"importlib.metadata"," reads it back, and git-cliff tags it — one number, three consumers, zero drift. The surrounding policy — how you choose the number and communicate it — is the subject of the parent guide, ",[436,1263,1265],{"href":1264},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning and changelogs",". Once tagged, the release is what you hand to ",[436,1268,1270],{"href":1269},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","publishing a Python CLI to PyPI",".",[27,1273,1275],{"id":1274},"production-notes","Production notes",[32,1277,1278,1287,1297,1309,1318,1330],{},[35,1279,1280,1283,1284,1286],{},[76,1281,1282],{},"Run generation in CI, not by hand."," A release job that runs ",[14,1285,20],{}," on tag push guarantees the changelog matches the history; a human running it locally will eventually forget.",[35,1288,1289,1292,1293,1296],{},[76,1290,1291],{},"Squash-merge PRs to a Conventional subject."," If you squash-merge, the PR title becomes the commit — lint the ",[115,1294,1295],{},"PR title",", since that is the message git-cliff will read.",[35,1298,1299,1305,1306,1308],{},[76,1300,1301,1304],{},[14,1302,1303],{},"chore(release):"," commits should be skipped."," Filter your own release commits out of the changelog (as the ",[14,1307,91],{}," above does) or they clutter every entry.",[35,1310,1311,1314,1315,1317],{},[76,1312,1313],{},"Breaking changes for a CLI are broader than for a library."," Renamed flags, changed defaults, altered exit codes, and changed output formats are all breaking — mark them with ",[14,1316,243],{}," even when the code change is small.",[35,1319,1320,1323,1324,1326,1327,1329],{},[76,1321,1322],{},"Pin the generator version."," Pin ",[14,1325,20],{},"\u002F",[14,1328,24],{}," in your CI image so a tool upgrade cannot silently reformat your entire changelog.",[35,1331,1332,1335,1336,1338],{},[76,1333,1334],{},"Don't hand-edit generated sections."," If you use git-cliff, treat ",[14,1337,16],{}," as generated output; put editorial notes in a fragment tool like towncrier instead if you need prose control.",[27,1340,1342],{"id":1341},"related","Related",[32,1344,1345,1351,1357,1363],{},[35,1346,1347,1350],{},[436,1348,1349],{"href":1264},"Managing CLI versioning and changelogs"," — the parent guide on versioning policy and release notes.",[35,1352,1353,1356],{},[436,1354,1355],{"href":619},"Pre-commit hooks for CLI projects"," — where the commit-message enforcement hook lives.",[35,1358,1359,1362],{},[436,1360,1361],{"href":1269},"Publishing a Python CLI to PyPI"," — the release step your tagged version feeds into.",[35,1364,1365,1368],{},[436,1366,1367],{"href":438},"Choosing exit codes for CLI tools"," — why exit-code changes count as breaking changes worth a major bump.",[1370,1371,1372],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":128,"searchDepth":198,"depth":198,"links":1374},[1375,1376,1377,1378,1379,1380,1381,1382,1383],{"id":29,"depth":198,"text":30},{"id":109,"depth":198,"text":110},{"id":314,"depth":198,"text":315},{"id":443,"depth":198,"text":444},{"id":628,"depth":198,"text":629},{"id":943,"depth":198,"text":944},{"id":1035,"depth":198,"text":1036},{"id":1274,"depth":198,"text":1275},{"id":1341,"depth":198,"text":1342},"2026-07-05","Generate CLI changelogs from Conventional Commits: enforce the format, derive semantic version bumps, and produce release notes with git-cliff.","intermediate",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits",{"title":5,"description":1385},"project-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits\u002Findex",[657,1394,1395,1396,1397],"versioning","conventional-commits","cli","distribution","5fBRYRHTKTc8G7MuZeOZRTpofDuVQfweIFNyVSrT1SM",[1400,1403,1406,1409,1412,1415,1418,1421,1424,1427,1430,1433,1436,1439,1442,1445,1448,1451,1454,1456,1459,1462,1465,1468,1471,1474,1477,1480,1483,1486,1489,1492,1495,1498,1501,1504,1507,1508,1511,1514,1517,1520,1522,1525,1528,1531,1534,1537,1540,1543,1546],{"path":1401,"title":1402},"\u002Fabout","About Python CLI Toolcraft",{"path":1404,"title":1405},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1407,"title":1408},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1410,"title":1411},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1413,"title":1414},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1416,"title":1417},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1419,"title":1420},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1422,"title":1423},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1425,"title":1426},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":1428,"title":1429},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1431,"title":1432},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":1434,"title":1435},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1437,"title":1438},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":1440,"title":1441},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1443,"title":1444},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":1446,"title":1447},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1449,"title":1450},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1452,"title":1453},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1326,"title":1455},"Python CLI Toolcraft",{"path":1457,"title":1458},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1460,"title":1461},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1463,"title":1464},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1466,"title":1467},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1469,"title":1470},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1472,"title":1473},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1475,"title":1476},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1478,"title":1479},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1481,"title":1482},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":1484,"title":1485},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":1487,"title":1488},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1490,"title":1491},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1493,"title":1494},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":1496,"title":1497},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1499,"title":1500},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1502,"title":1503},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1505,"title":1506},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1390,"title":5},{"path":1509,"title":1510},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1512,"title":1513},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1515,"title":1516},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1518,"title":1519},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1521,"title":1361},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1523,"title":1524},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1526,"title":1527},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1529,"title":1530},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1532,"title":1533},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":1535,"title":1536},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1538,"title":1539},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":1541,"title":1542},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":1544,"title":1545},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1547,"title":1548},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",1783281867199]