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