[{"data":1,"prerenderedAt":1517},["ShallowReactive",2],{"page-\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci\u002F":3,"content-directory":1271},{"id":4,"title":5,"body":6,"date":1256,"description":1257,"difficulty":1258,"draft":1259,"extension":1260,"meta":1261,"navigation":185,"path":1262,"seo":1263,"stem":1264,"tags":1265,"updated":1256,"__hash__":1270},"content\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci\u002Findex.md","Building Cross-Platform Release Binaries in CI",{"type":7,"value":8,"toc":1233},"minimark",[9,18,23,65,69,87,91,95,102,117,121,757,760,775,779,782,957,960,964,967,975,982,986,1000,1010,1024,1028,1031,1037,1093,1102,1106,1109,1113,1118,1125,1129,1132,1135,1139,1142,1146,1149,1153,1160,1164,1167,1171,1178,1182,1185,1189,1192,1196,1229],[10,11,12,13,17],"p",{},"A frozen binary must be built on the platform it targets, so shipping to Linux, macOS and Windows\nmeans three or four builds per release. Doing that by hand is where release processes rot; doing\nit in a tagged CI workflow makes the whole thing one ",[14,15,16],"code",{},"git push --follow-tags",".",[19,20,22],"h2",{"id":21},"tldr","TL;DR",[24,25,26,38,44,51,58],"ul",{},[27,28,29,30,34,35,17],"li",{},"One job builds the ",[31,32,33],"strong",{},"wheel","; a matrix builds one ",[31,36,37],{},"binary per platform",[27,39,40,43],{},[31,41,42],{},"Verify inside the matrix"," — run the artifact on the runner that produced it.",[27,45,46,47,50],{},"Name artifacts ",[14,48,49],{},"mytool-\u003Cos>-\u003Carch>"," so downloads are unambiguous.",[27,52,53,54,57],{},"Publish ",[31,55,56],{},"checksums"," with every release, generated in CI rather than by hand.",[27,59,60,61,64],{},"Trigger on a ",[31,62,63],{},"tag",", never on a branch push, so releases are reproducible from history.",[19,66,68],{"id":67},"prerequisites","Prerequisites",[10,70,71,72,77,78,82,83,17],{},"A project that already builds a wheel, a working freeze or zipapp step (see\n",[73,74,76],"a",{"href":75},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller\u002F","PyInstaller","\nand ",[73,79,81],{"href":80},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv\u002F","shiv","),\nand a single-sourced version as described in\n",[73,84,86],{"href":85},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002F","managing CLI versioning",[19,88,90],{"id":89},"the-target-matrix","The target matrix",[92,93],"inline-diagram",{"name":94},"release-matrix",[10,96,97,98,101],{},"Four targets cover the overwhelming majority of users. macOS needs two entries because Apple\nsilicon and Intel are different architectures and there is no cross-compilation; the Intel build\nruns on an older ",[14,99,100],{},"macos-13"," image, which still has x86-64 runners.",[10,103,104,105,108,109,112,113,116],{},"Linux deserves one decision: build on the ",[31,106,107],{},"oldest"," distribution you intend to support, because a\nbinary linked against a newer glibc will not run on an older system. ",[14,110,111],{},"ubuntu-22.04"," is a\nreasonable default; a ",[14,114,115],{},"manylinux"," container is the thorough answer if you need to reach further\nback. Alpine and other musl systems need their own build entirely.",[19,118,120],{"id":119},"the-workflow","The workflow",[122,123,128],"pre",{"className":124,"code":125,"language":126,"meta":127,"style":127},"language-yaml shiki shiki-themes github-light github-dark","name: release\non:\n  push:\n    tags: [\"v*\"]\n\npermissions:\n  contents: write        # to create the release\n  id-token: write        # for trusted publishing to PyPI\n\njobs:\n  wheel:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: astral-sh\u002Fsetup-uv@v3\n      - run: uv build\n      - uses: actions\u002Fupload-artifact@v4\n        with: { name: wheel, path: dist\u002F }\n\n  binaries:\n    strategy:\n      fail-fast: false             # one broken platform must not hide the others\n      matrix:\n        include:\n          - { os: ubuntu-22.04,  target: linux-x86_64,   ext: tar.gz }\n          - { os: macos-latest,  target: macos-arm64,    ext: tar.gz }\n          - { os: macos-13,      target: macos-x86_64,   ext: tar.gz }\n          - { os: windows-latest, target: windows-x86_64, ext: zip }\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - uses: astral-sh\u002Fsetup-uv@v3\n      - run: uv sync --frozen --no-dev\n      - run: uv run --with pyinstaller pyinstaller mytool.spec --clean --noconfirm\n\n      # verification, on the platform that produced it\n      - run: .\u002Fdist\u002Fmytool\u002Fmytool --version\n        if: runner.os != 'Windows'\n      - run: .\\dist\\mytool\\mytool.exe --version\n        if: runner.os == 'Windows'\n\n      - name: package\n        shell: bash\n        run: |\n          cd dist\n          if [ \"${{ matrix.ext }}\" = \"zip\" ]; then\n            7z a \"..\u002Fmytool-${{ matrix.target }}.zip\" mytool\n          else\n            tar czf \"..\u002Fmytool-${{ matrix.target }}.tar.gz\" mytool\n          fi\n      - uses: actions\u002Fupload-artifact@v4\n        with: { name: \"binary-${{ matrix.target }}\", path: \"mytool-${{ matrix.target }}.*\" }\n","yaml","",[14,129,130,147,157,165,180,187,195,210,223,228,236,244,255,263,277,289,302,314,343,348,356,364,378,386,394,431,463,494,526,536,543,554,565,577,589,594,600,612,623,635,645,650,662,673,685,691,697,703,709,715,721,732],{"__ignoreMap":127},[131,132,135,139,143],"span",{"class":133,"line":134},"line",1,[131,136,138],{"class":137},"s9eBZ","name",[131,140,142],{"class":141},"sVt8B",": ",[131,144,146],{"class":145},"sZZnC","release\n",[131,148,150,154],{"class":133,"line":149},2,[131,151,153],{"class":152},"sj4cs","on",[131,155,156],{"class":141},":\n",[131,158,160,163],{"class":133,"line":159},3,[131,161,162],{"class":137},"  push",[131,164,156],{"class":141},[131,166,168,171,174,177],{"class":133,"line":167},4,[131,169,170],{"class":137},"    tags",[131,172,173],{"class":141},": [",[131,175,176],{"class":145},"\"v*\"",[131,178,179],{"class":141},"]\n",[131,181,183],{"class":133,"line":182},5,[131,184,186],{"emptyLinePlaceholder":185},true,"\n",[131,188,190,193],{"class":133,"line":189},6,[131,191,192],{"class":137},"permissions",[131,194,156],{"class":141},[131,196,198,201,203,206],{"class":133,"line":197},7,[131,199,200],{"class":137},"  contents",[131,202,142],{"class":141},[131,204,205],{"class":145},"write",[131,207,209],{"class":208},"sJ8bj","        # to create the release\n",[131,211,213,216,218,220],{"class":133,"line":212},8,[131,214,215],{"class":137},"  id-token",[131,217,142],{"class":141},[131,219,205],{"class":145},[131,221,222],{"class":208},"        # for trusted publishing to PyPI\n",[131,224,226],{"class":133,"line":225},9,[131,227,186],{"emptyLinePlaceholder":185},[131,229,231,234],{"class":133,"line":230},10,[131,232,233],{"class":137},"jobs",[131,235,156],{"class":141},[131,237,239,242],{"class":133,"line":238},11,[131,240,241],{"class":137},"  wheel",[131,243,156],{"class":141},[131,245,247,250,252],{"class":133,"line":246},12,[131,248,249],{"class":137},"    runs-on",[131,251,142],{"class":141},[131,253,254],{"class":145},"ubuntu-latest\n",[131,256,258,261],{"class":133,"line":257},13,[131,259,260],{"class":137},"    steps",[131,262,156],{"class":141},[131,264,266,269,272,274],{"class":133,"line":265},14,[131,267,268],{"class":141},"      - ",[131,270,271],{"class":137},"uses",[131,273,142],{"class":141},[131,275,276],{"class":145},"actions\u002Fcheckout@v4\n",[131,278,280,282,284,286],{"class":133,"line":279},15,[131,281,268],{"class":141},[131,283,271],{"class":137},[131,285,142],{"class":141},[131,287,288],{"class":145},"astral-sh\u002Fsetup-uv@v3\n",[131,290,292,294,297,299],{"class":133,"line":291},16,[131,293,268],{"class":141},[131,295,296],{"class":137},"run",[131,298,142],{"class":141},[131,300,301],{"class":145},"uv build\n",[131,303,305,307,309,311],{"class":133,"line":304},17,[131,306,268],{"class":141},[131,308,271],{"class":137},[131,310,142],{"class":141},[131,312,313],{"class":145},"actions\u002Fupload-artifact@v4\n",[131,315,317,320,323,325,327,329,332,335,337,340],{"class":133,"line":316},18,[131,318,319],{"class":137},"        with",[131,321,322],{"class":141},": { ",[131,324,138],{"class":137},[131,326,142],{"class":141},[131,328,33],{"class":145},[131,330,331],{"class":141},", ",[131,333,334],{"class":137},"path",[131,336,142],{"class":141},[131,338,339],{"class":145},"dist\u002F",[131,341,342],{"class":141}," }\n",[131,344,346],{"class":133,"line":345},19,[131,347,186],{"emptyLinePlaceholder":185},[131,349,351,354],{"class":133,"line":350},20,[131,352,353],{"class":137},"  binaries",[131,355,156],{"class":141},[131,357,359,362],{"class":133,"line":358},21,[131,360,361],{"class":137},"    strategy",[131,363,156],{"class":141},[131,365,367,370,372,375],{"class":133,"line":366},22,[131,368,369],{"class":137},"      fail-fast",[131,371,142],{"class":141},[131,373,374],{"class":152},"false",[131,376,377],{"class":208},"             # one broken platform must not hide the others\n",[131,379,381,384],{"class":133,"line":380},23,[131,382,383],{"class":137},"      matrix",[131,385,156],{"class":141},[131,387,389,392],{"class":133,"line":388},24,[131,390,391],{"class":137},"        include",[131,393,156],{"class":141},[131,395,397,400,403,405,407,410,413,415,418,421,424,426,429],{"class":133,"line":396},25,[131,398,399],{"class":141},"          - { ",[131,401,402],{"class":137},"os",[131,404,142],{"class":141},[131,406,111],{"class":145},[131,408,409],{"class":141},",  ",[131,411,412],{"class":137},"target",[131,414,142],{"class":141},[131,416,417],{"class":145},"linux-x86_64",[131,419,420],{"class":141},",   ",[131,422,423],{"class":137},"ext",[131,425,142],{"class":141},[131,427,428],{"class":145},"tar.gz",[131,430,342],{"class":141},[131,432,434,436,438,440,443,445,447,449,452,455,457,459,461],{"class":133,"line":433},26,[131,435,399],{"class":141},[131,437,402],{"class":137},[131,439,142],{"class":141},[131,441,442],{"class":145},"macos-latest",[131,444,409],{"class":141},[131,446,412],{"class":137},[131,448,142],{"class":141},[131,450,451],{"class":145},"macos-arm64",[131,453,454],{"class":141},",    ",[131,456,423],{"class":137},[131,458,142],{"class":141},[131,460,428],{"class":145},[131,462,342],{"class":141},[131,464,466,468,470,472,474,477,479,481,484,486,488,490,492],{"class":133,"line":465},27,[131,467,399],{"class":141},[131,469,402],{"class":137},[131,471,142],{"class":141},[131,473,100],{"class":145},[131,475,476],{"class":141},",      ",[131,478,412],{"class":137},[131,480,142],{"class":141},[131,482,483],{"class":145},"macos-x86_64",[131,485,420],{"class":141},[131,487,423],{"class":137},[131,489,142],{"class":141},[131,491,428],{"class":145},[131,493,342],{"class":141},[131,495,497,499,501,503,506,508,510,512,515,517,519,521,524],{"class":133,"line":496},28,[131,498,399],{"class":141},[131,500,402],{"class":137},[131,502,142],{"class":141},[131,504,505],{"class":145},"windows-latest",[131,507,331],{"class":141},[131,509,412],{"class":137},[131,511,142],{"class":141},[131,513,514],{"class":145},"windows-x86_64",[131,516,331],{"class":141},[131,518,423],{"class":137},[131,520,142],{"class":141},[131,522,523],{"class":145},"zip",[131,525,342],{"class":141},[131,527,529,531,533],{"class":133,"line":528},29,[131,530,249],{"class":137},[131,532,142],{"class":141},[131,534,535],{"class":145},"${{ matrix.os }}\n",[131,537,539,541],{"class":133,"line":538},30,[131,540,260],{"class":137},[131,542,156],{"class":141},[131,544,546,548,550,552],{"class":133,"line":545},31,[131,547,268],{"class":141},[131,549,271],{"class":137},[131,551,142],{"class":141},[131,553,276],{"class":145},[131,555,557,559,561,563],{"class":133,"line":556},32,[131,558,268],{"class":141},[131,560,271],{"class":137},[131,562,142],{"class":141},[131,564,288],{"class":145},[131,566,568,570,572,574],{"class":133,"line":567},33,[131,569,268],{"class":141},[131,571,296],{"class":137},[131,573,142],{"class":141},[131,575,576],{"class":145},"uv sync --frozen --no-dev\n",[131,578,580,582,584,586],{"class":133,"line":579},34,[131,581,268],{"class":141},[131,583,296],{"class":137},[131,585,142],{"class":141},[131,587,588],{"class":145},"uv run --with pyinstaller pyinstaller mytool.spec --clean --noconfirm\n",[131,590,592],{"class":133,"line":591},35,[131,593,186],{"emptyLinePlaceholder":185},[131,595,597],{"class":133,"line":596},36,[131,598,599],{"class":208},"      # verification, on the platform that produced it\n",[131,601,603,605,607,609],{"class":133,"line":602},37,[131,604,268],{"class":141},[131,606,296],{"class":137},[131,608,142],{"class":141},[131,610,611],{"class":145},".\u002Fdist\u002Fmytool\u002Fmytool --version\n",[131,613,615,618,620],{"class":133,"line":614},38,[131,616,617],{"class":137},"        if",[131,619,142],{"class":141},[131,621,622],{"class":145},"runner.os != 'Windows'\n",[131,624,626,628,630,632],{"class":133,"line":625},39,[131,627,268],{"class":141},[131,629,296],{"class":137},[131,631,142],{"class":141},[131,633,634],{"class":145},".\\dist\\mytool\\mytool.exe --version\n",[131,636,638,640,642],{"class":133,"line":637},40,[131,639,617],{"class":137},[131,641,142],{"class":141},[131,643,644],{"class":145},"runner.os == 'Windows'\n",[131,646,648],{"class":133,"line":647},41,[131,649,186],{"emptyLinePlaceholder":185},[131,651,653,655,657,659],{"class":133,"line":652},42,[131,654,268],{"class":141},[131,656,138],{"class":137},[131,658,142],{"class":141},[131,660,661],{"class":145},"package\n",[131,663,665,668,670],{"class":133,"line":664},43,[131,666,667],{"class":137},"        shell",[131,669,142],{"class":141},[131,671,672],{"class":145},"bash\n",[131,674,676,679,681],{"class":133,"line":675},44,[131,677,678],{"class":137},"        run",[131,680,142],{"class":141},[131,682,684],{"class":683},"szBVR","|\n",[131,686,688],{"class":133,"line":687},45,[131,689,690],{"class":145},"          cd dist\n",[131,692,694],{"class":133,"line":693},46,[131,695,696],{"class":145},"          if [ \"${{ matrix.ext }}\" = \"zip\" ]; then\n",[131,698,700],{"class":133,"line":699},47,[131,701,702],{"class":145},"            7z a \"..\u002Fmytool-${{ matrix.target }}.zip\" mytool\n",[131,704,706],{"class":133,"line":705},48,[131,707,708],{"class":145},"          else\n",[131,710,712],{"class":133,"line":711},49,[131,713,714],{"class":145},"            tar czf \"..\u002Fmytool-${{ matrix.target }}.tar.gz\" mytool\n",[131,716,718],{"class":133,"line":717},50,[131,719,720],{"class":145},"          fi\n",[131,722,724,726,728,730],{"class":133,"line":723},51,[131,725,268],{"class":141},[131,727,271],{"class":137},[131,729,142],{"class":141},[131,731,313],{"class":145},[131,733,735,737,739,741,743,746,748,750,752,755],{"class":133,"line":734},52,[131,736,319],{"class":137},[131,738,322],{"class":141},[131,740,138],{"class":137},[131,742,142],{"class":141},[131,744,745],{"class":145},"\"binary-${{ matrix.target }}\"",[131,747,331],{"class":141},[131,749,334],{"class":137},[131,751,142],{"class":141},[131,753,754],{"class":145},"\"mytool-${{ matrix.target }}.*\"",[131,756,342],{"class":141},[92,758],{"name":759},"release-pipeline-timeline",[10,761,762,763,766,767,770,771,774],{},"Three choices in there are worth calling out. ",[14,764,765],{},"fail-fast: false"," means a broken Windows build does\nnot cancel the three that were about to succeed, so you learn everything from one run.\n",[14,768,769],{},"uv sync --frozen --no-dev"," freezes from exactly the runtime dependency set, which keeps the binary\nsmall and catches a dev dependency that leaked into runtime. And the verification step runs\n",[31,772,773],{},"before"," packaging, so a broken artifact never reaches the upload.",[19,776,778],{"id":777},"checksums-and-the-release","Checksums and the release",[92,780],{"name":781},"checksum-flow",[122,783,785],{"className":124,"code":784,"language":126,"meta":127,"style":127},"  publish:\n    needs: [wheel, binaries]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fdownload-artifact@v4\n        with: { path: artifacts, merge-multiple: true }\n      - name: checksums\n        run: |\n          cd artifacts\n          sha256sum mytool-* > checksums.txt\n          cat checksums.txt\n      - uses: softprops\u002Faction-gh-release@v2\n        with:\n          files: artifacts\u002F*\n          generate_release_notes: true\n      - uses: pypa\u002Fgh-action-pypi-publish@release\u002Fv1\n        with: { packages-dir: artifacts }\n",[14,786,787,794,810,818,824,835,860,871,879,884,889,894,905,911,921,931,942],{"__ignoreMap":127},[131,788,789,792],{"class":133,"line":134},[131,790,791],{"class":137},"  publish",[131,793,156],{"class":141},[131,795,796,799,801,803,805,808],{"class":133,"line":149},[131,797,798],{"class":137},"    needs",[131,800,173],{"class":141},[131,802,33],{"class":145},[131,804,331],{"class":141},[131,806,807],{"class":145},"binaries",[131,809,179],{"class":141},[131,811,812,814,816],{"class":133,"line":159},[131,813,249],{"class":137},[131,815,142],{"class":141},[131,817,254],{"class":145},[131,819,820,822],{"class":133,"line":167},[131,821,260],{"class":137},[131,823,156],{"class":141},[131,825,826,828,830,832],{"class":133,"line":182},[131,827,268],{"class":141},[131,829,271],{"class":137},[131,831,142],{"class":141},[131,833,834],{"class":145},"actions\u002Fdownload-artifact@v4\n",[131,836,837,839,841,843,845,848,850,853,855,858],{"class":133,"line":189},[131,838,319],{"class":137},[131,840,322],{"class":141},[131,842,334],{"class":137},[131,844,142],{"class":141},[131,846,847],{"class":145},"artifacts",[131,849,331],{"class":141},[131,851,852],{"class":137},"merge-multiple",[131,854,142],{"class":141},[131,856,857],{"class":152},"true",[131,859,342],{"class":141},[131,861,862,864,866,868],{"class":133,"line":197},[131,863,268],{"class":141},[131,865,138],{"class":137},[131,867,142],{"class":141},[131,869,870],{"class":145},"checksums\n",[131,872,873,875,877],{"class":133,"line":212},[131,874,678],{"class":137},[131,876,142],{"class":141},[131,878,684],{"class":683},[131,880,881],{"class":133,"line":225},[131,882,883],{"class":145},"          cd artifacts\n",[131,885,886],{"class":133,"line":230},[131,887,888],{"class":145},"          sha256sum mytool-* > checksums.txt\n",[131,890,891],{"class":133,"line":238},[131,892,893],{"class":145},"          cat checksums.txt\n",[131,895,896,898,900,902],{"class":133,"line":246},[131,897,268],{"class":141},[131,899,271],{"class":137},[131,901,142],{"class":141},[131,903,904],{"class":145},"softprops\u002Faction-gh-release@v2\n",[131,906,907,909],{"class":133,"line":257},[131,908,319],{"class":137},[131,910,156],{"class":141},[131,912,913,916,918],{"class":133,"line":265},[131,914,915],{"class":137},"          files",[131,917,142],{"class":141},[131,919,920],{"class":145},"artifacts\u002F*\n",[131,922,923,926,928],{"class":133,"line":279},[131,924,925],{"class":137},"          generate_release_notes",[131,927,142],{"class":141},[131,929,930],{"class":152},"true\n",[131,932,933,935,937,939],{"class":133,"line":291},[131,934,268],{"class":141},[131,936,271],{"class":137},[131,938,142],{"class":141},[131,940,941],{"class":145},"pypa\u002Fgh-action-pypi-publish@release\u002Fv1\n",[131,943,944,946,948,951,953,955],{"class":133,"line":304},[131,945,319],{"class":137},[131,947,322],{"class":141},[131,949,950],{"class":137},"packages-dir",[131,952,142],{"class":141},[131,954,847],{"class":145},[131,956,342],{"class":141},[10,958,959],{},"Generating the checksums in CI rather than locally is the point: they then describe the artifacts\nthat were actually published, produced by a workflow anyone can read. A hash computed on a laptop\nand pasted into a release description proves considerably less.",[19,961,963],{"id":962},"naming-and-stability","Naming and stability",[10,965,966],{},"Artifact names are an interface. Scripts, package manager manifests and documentation all encode\nthem, so pick a convention and keep it:",[122,968,973],{"className":969,"code":971,"language":972},[970],"language-text","mytool-1.4.0-linux-x86_64.tar.gz\nmytool-1.4.0-macos-arm64.tar.gz\nmytool-1.4.0-windows-x86_64.zip\nchecksums.txt\n","text",[14,974,971],{"__ignoreMap":127},[10,976,977,978,981],{},"Include the version in the filename so downloads are self-describing, and — if you can — publish a\n",[14,979,980],{},"latest"," redirect so install snippets do not need editing on every release. Renaming an artifact\nbetween versions breaks every Homebrew formula, Scoop manifest and install script that referenced\nit, which makes it a breaking change in everything but name.",[19,983,985],{"id":984},"ux-considerations","UX considerations",[10,987,988,991,992,994,995,999],{},[31,989,990],{},"Make failure visible per platform."," With ",[14,993,765],{}," and clear job names, a red release\nrun says ",[996,997,998],"em",{},"which"," platform broke. That is the difference between a five-minute fix and an\ninvestigation.",[10,1001,1002,1005,1006,1009],{},[31,1003,1004],{},"Publish release notes from the changelog."," ",[14,1007,1008],{},"generate_release_notes: true"," produces something\nfrom commit titles; a curated changelog entry is better, and if you already derive one from\nconventional commits it is free.",[10,1011,1012,1015,1016,1019,1020,1023],{},[31,1013,1014],{},"Keep the download instructions next to the artifacts."," A release page listing four ",[14,1017,1018],{},".tar.gz","\nfiles and nothing else leaves a reader guessing which is theirs. Two lines in the release body —\nthe recommended ",[14,1021,1022],{},"pipx"," command and the verification snippet — cost nothing to template.",[19,1025,1027],{"id":1026},"testing-the-behaviour","Testing the behaviour",[10,1029,1030],{},"The release workflow itself deserves a dry run before you rely on it. Two techniques help.",[10,1032,1033,1036],{},[31,1034,1035],{},"Build on pull requests without publishing."," A second trigger that runs the matrix and uploads\nartifacts, but skips the release and PyPI steps, keeps the build honest between releases — so the\nfirst time you tag is not the first time the matrix runs.",[122,1038,1040],{"className":124,"code":1039,"language":126,"meta":127,"style":127},"on:\n  push:\n    tags: [\"v*\"]\n  pull_request:\n    paths: [\"mytool.spec\", \".github\u002Fworkflows\u002Frelease.yml\", \"pyproject.toml\"]\n",[14,1041,1042,1048,1054,1064,1071],{"__ignoreMap":127},[131,1043,1044,1046],{"class":133,"line":134},[131,1045,153],{"class":152},[131,1047,156],{"class":141},[131,1049,1050,1052],{"class":133,"line":149},[131,1051,162],{"class":137},[131,1053,156],{"class":141},[131,1055,1056,1058,1060,1062],{"class":133,"line":159},[131,1057,170],{"class":137},[131,1059,173],{"class":141},[131,1061,176],{"class":145},[131,1063,179],{"class":141},[131,1065,1066,1069],{"class":133,"line":167},[131,1067,1068],{"class":137},"  pull_request",[131,1070,156],{"class":141},[131,1072,1073,1076,1078,1081,1083,1086,1088,1091],{"class":133,"line":182},[131,1074,1075],{"class":137},"    paths",[131,1077,173],{"class":141},[131,1079,1080],{"class":145},"\"mytool.spec\"",[131,1082,331],{"class":141},[131,1084,1085],{"class":145},"\".github\u002Fworkflows\u002Frelease.yml\"",[131,1087,331],{"class":141},[131,1089,1090],{"class":145},"\"pyproject.toml\"",[131,1092,179],{"class":141},[10,1094,1095,1005,1098,1101],{},[31,1096,1097],{},"Rehearse the whole thing on a pre-release tag.",[14,1099,1100],{},"v1.4.0-rc.1"," exercises every step including\nthe release creation, and a pre-release is easy to delete. Doing it once after any change to the\nworkflow is cheap insurance against discovering a permissions problem halfway through a real\nrelease.",[19,1103,1105],{"id":1104},"conclusion","Conclusion",[10,1107,1108],{},"A tagged workflow with a build matrix, in-matrix verification and generated checksums turns\nmulti-platform distribution from a manual ritual into a push. The parts that matter are verifying\neach artifact on its own platform, never failing fast across the matrix, and keeping artifact\nnames stable enough that everything downstream can depend on them.",[19,1110,1112],{"id":1111},"frequently-asked-questions","Frequently asked questions",[1114,1115,1117],"h3",{"id":1116},"should-the-release-run-on-a-tag-or-on-a-branch","Should the release run on a tag or on a branch?",[10,1119,1120,1121,1124],{},"A tag. It makes the release reproducible from history, prevents accidental publishes from a merge,\nand gives you a natural version source. Pair it with a ",[14,1122,1123],{},"workflow_dispatch"," trigger if you need to\nre-run the publish step without creating another tag.",[1114,1126,1128],{"id":1127},"how-do-i-handle-a-release-that-fails-halfway-through","How do I handle a release that fails halfway through?",[10,1130,1131],{},"Design the jobs so re-running is safe: builds are idempotent, and the publish job should tolerate\nartifacts that already exist. If PyPI has already accepted the wheel, that version is spent — yank\nit and release a patch rather than trying to overwrite.",[92,1133],{"name":1134},"signing-decision",[1114,1136,1138],{"id":1137},"do-i-need-to-sign-the-binaries","Do I need to sign the binaries?",[10,1140,1141],{},"For direct downloads on macOS, effectively yes: Gatekeeper blocks unsigned executables and most\nusers will not work around it. Windows SmartScreen warns rather than blocks, which is survivable\nbut unhelpful. Distributing through Homebrew or Scoop avoids the question, since the manager\nhandles trust.",[1114,1143,1145],{"id":1144},"can-one-job-build-for-several-architectures","Can one job build for several architectures?",[10,1147,1148],{},"For pure-Python artifacts like a wheel or a zipapp, yes — one build serves everyone. For frozen\nbinaries, no: each needs a native runner. Emulation through QEMU works for Linux arm64 and is slow\nenough that a native runner is usually worth the configuration.",[1114,1150,1152],{"id":1151},"how-large-should-a-release-be","How large should a release be?",[10,1154,1155,1156,1159],{},"For a typical CLI, four archives of 15–40 MB each. If you are pushing past that, check what the\nfreeze pulled in with a ",[14,1157,1158],{},"--onedir"," build — the usual culprit is a transitive dependency dragging in\na scientific stack that only one command needs, which is a good argument for making it an optional\nextra.",[1114,1161,1163],{"id":1162},"how-do-i-keep-ci-costs-reasonable-with-a-four-way-matrix","How do I keep CI costs reasonable with a four-way matrix?",[10,1165,1166],{},"Only run the matrix when it can produce something: on tags, and on pull requests that touch the\npackaging configuration. macOS runners in particular are billed at a multiple of Linux ones, so a\nmatrix that fires on every commit to every branch is expensive for no benefit. Caching the\ndependency install with a key derived from the lockfile removes most of the remaining time.",[1114,1168,1170],{"id":1169},"should-the-wheel-and-the-binaries-come-from-the-same-commit","Should the wheel and the binaries come from the same commit?",[10,1172,1173,1174,1177],{},"Always, and from the same tag. If the wheel is built in one workflow and the binaries in another,\nnothing guarantees they contain the same code, and a user comparing ",[14,1175,1176],{},"pipx install"," behaviour with\na downloaded binary will eventually find the difference. One workflow, one tag, one set of\nartifacts.",[1114,1179,1181],{"id":1180},"what-belongs-in-the-release-body","What belongs in the release body?",[10,1183,1184],{},"The install line for the recommended route, the verification command for direct downloads, and the\nchangelog entry for this version. Everything else — a table of every artifact, build metadata, a\nlist of contributors — is generated noise that pushes the useful three lines below the fold.",[1114,1186,1188],{"id":1187},"how-do-i-roll-back-a-bad-binary-release","How do I roll back a bad binary release?",[10,1190,1191],{},"Delete or mark the GitHub release as a pre-release so it stops being \"latest\", then tag a patch and\nlet the workflow publish a corrected set. Do not replace assets on an existing release: anyone who\nalready downloaded has a file whose checksum no longer matches the published list, which is far\nmore confusing than a new version. If a package manager already picked it up, revert the formula\nor manifest in the same pass.",[19,1193,1195],{"id":1194},"related","Related",[24,1197,1198,1205,1211,1217,1224],{},[27,1199,1200,1201],{},"Up: ",[73,1202,1204],{"href":1203},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002F","Distributing CLIs as standalone binaries",[27,1206,1207,1208],{},"Sideways: ",[73,1209,1210],{"href":75},"Bundling a Python CLI with PyInstaller",[27,1212,1207,1213],{},[73,1214,1216],{"href":1215},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis\u002F","Homebrew and Scoop packaging for Python CLIs",[27,1218,1219,1220],{},"Related: ",[73,1221,1223],{"href":1222},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi\u002F","Publishing a Python CLI to PyPI",[27,1225,1219,1226],{},[73,1227,1228],{"href":85},"Managing CLI versioning and changelogs",[1230,1231,1232],"style",{},"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 .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 pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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);}",{"title":127,"searchDepth":149,"depth":149,"links":1234},[1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1255],{"id":21,"depth":149,"text":22},{"id":67,"depth":149,"text":68},{"id":89,"depth":149,"text":90},{"id":119,"depth":149,"text":120},{"id":777,"depth":149,"text":778},{"id":962,"depth":149,"text":963},{"id":984,"depth":149,"text":985},{"id":1026,"depth":149,"text":1027},{"id":1104,"depth":149,"text":1105},{"id":1111,"depth":149,"text":1112,"children":1245},[1246,1247,1248,1249,1250,1251,1252,1253,1254],{"id":1116,"depth":159,"text":1117},{"id":1127,"depth":159,"text":1128},{"id":1137,"depth":159,"text":1138},{"id":1144,"depth":159,"text":1145},{"id":1151,"depth":159,"text":1152},{"id":1162,"depth":159,"text":1163},{"id":1169,"depth":159,"text":1170},{"id":1180,"depth":159,"text":1181},{"id":1187,"depth":159,"text":1188},{"id":1194,"depth":149,"text":1195},"2026-08-01","Build, verify and publish Python CLI binaries for Linux, macOS and Windows from one tagged release - matrix jobs, artifact naming, checksums and rollbacks.","advanced",false,"md",{},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci",{"title":5,"description":1257},"project-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci\u002Findex",[1266,1267,807,1268,1269],"ci","release","github-actions","packaging","gmthvitdpSfvF7M0mK3A6bNBr2pzL53C_JmgEJCuCGM",[1272,1275,1278,1281,1284,1287,1290,1293,1296,1299,1302,1305,1308,1311,1314,1317,1320,1323,1326,1329,1332,1335,1338,1341,1344,1347,1350,1353,1356,1359,1362,1365,1368,1371,1374,1377,1380,1383,1386,1389,1392,1395,1398,1401,1404,1407,1410,1413,1416,1419,1422,1425,1428,1431,1434,1437,1440,1443,1446,1449,1452,1453,1455,1458,1461,1464,1467,1470,1473,1476,1479,1482,1485,1487,1490,1493,1496,1499,1502,1505,1508,1511,1514],{"path":1273,"title":1274},"\u002Fabout","About Python CLI Toolcraft",{"path":1276,"title":1277},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":1279,"title":1280},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":1282,"title":1283},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":1285,"title":1286},"\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":1288,"title":1289},"\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":1291,"title":1292},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":1294,"title":1295},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":1297,"title":1298},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":1300,"title":1301},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":1303,"title":1304},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":1306,"title":1307},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":1309,"title":1310},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":1312,"title":1313},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":1315,"title":1316},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":1318,"title":1319},"\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":1321,"title":1322},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":1324,"title":1325},"\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":1327,"title":1328},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":1330,"title":1331},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":1333,"title":1334},"\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":1336,"title":1337},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":1339,"title":1340},"\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":1342,"title":1343},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":1345,"title":1346},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":1348,"title":1349},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":1351,"title":1352},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":1354,"title":1355},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":1357,"title":1358},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":1360,"title":1361},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":1363,"title":1364},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":1366,"title":1367},"\u002F","Python CLI Toolcraft",{"path":1369,"title":1370},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":1372,"title":1373},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":1375,"title":1376},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":1378,"title":1379},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":1381,"title":1382},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":1384,"title":1385},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":1387,"title":1388},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":1390,"title":1391},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":1393,"title":1394},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":1396,"title":1397},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":1399,"title":1400},"\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":1402,"title":1403},"\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":1405,"title":1406},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":1408,"title":1409},"\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":1411,"title":1412},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":1414,"title":1415},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":1417,"title":1418},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":1420,"title":1421},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":1423,"title":1424},"\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":1426,"title":1427},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":1429,"title":1430},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":1432,"title":1433},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":1435,"title":1436},"\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":1438,"title":1439},"\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":1441,"title":1442},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":1444,"title":1445},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":1447,"title":1448},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":1450,"title":1451},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":1262,"title":5},{"path":1454,"title":1210},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller",{"path":1456,"title":1457},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":1459,"title":1460},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":1462,"title":1463},"\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":1465,"title":1466},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":1468,"title":1469},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":1471,"title":1472},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":1474,"title":1475},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":1477,"title":1478},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":1480,"title":1481},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":1483,"title":1484},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":1486,"title":1223},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi",{"path":1488,"title":1489},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":1491,"title":1492},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":1494,"title":1495},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":1497,"title":1498},"\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":1500,"title":1501},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":1503,"title":1504},"\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":1506,"title":1507},"\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":1509,"title":1510},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":1512,"title":1513},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":1515,"title":1516},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",1785614690034]