@@ -47,4 +47,299 @@ build:
47
47
- export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}'
48
48
` ` `
49
49
50
- This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow.
50
+ This configuration uses the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `scm.git.pre_parse` setting specifically for your project when building on ReadTheDocs, forcing setuptools-scm to fail with a clear error if the repository is shallow.
51
+
52
+ # # CI/CD and Package Publishing
53
+
54
+ # ## Publishing to PyPI from CI/CD
55
+
56
+ When publishing packages to PyPI or test-PyPI from CI/CD pipelines, you often need to remove local version components that are not allowed on public package indexes according to [PEP 440](https://peps.python.org/pep-0440/#local-version-identifiers).
57
+
58
+ setuptools-scm provides the `no-local-version` local scheme and environment variable overrides to handle this scenario cleanly.
59
+
60
+ # ### The Problem
61
+
62
+ By default, setuptools-scm generates version numbers like :
63
+ - ` 1.2.3.dev4+g1a2b3c4d5` (development version with git hash)
64
+ - ` 1.2.3+dirty` (dirty working directory)
65
+
66
+ These local version components (`+g1a2b3c4d5`, `+dirty`) prevent uploading to PyPI.
67
+
68
+ # ### The Solution
69
+
70
+ Use the `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` environment variable to override the `local_scheme` to `no-local-version` when building for upload to PyPI.
71
+
72
+ # ## GitHub Actions Example
73
+
74
+ Here's a complete GitHub Actions workflow that :
75
+ - Runs tests on all branches
76
+ - Uploads development versions to test-PyPI from feature branches
77
+ - Uploads development versions to PyPI from the main branch (with no-local-version)
78
+ - Uploads tagged releases to PyPI (using exact tag versions)
79
+
80
+ ` ` ` yaml title=".github/workflows/ci.yml"
81
+ name: CI/CD
82
+
83
+ on:
84
+ push:
85
+ branches: ["main", "develop"]
86
+ pull_request:
87
+ branches: ["main", "develop"]
88
+ release:
89
+ types: [published]
90
+
91
+ jobs:
92
+ test:
93
+ runs-on: ubuntu-latest
94
+ strategy:
95
+ matrix:
96
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
97
+
98
+ steps:
99
+ - uses: actions/checkout@v4
100
+ with:
101
+ # Fetch full history for setuptools-scm
102
+ fetch-depth: 0
103
+
104
+ - name: Set up Python ${{ matrix.python-version }}
105
+ uses: actions/setup-python@v4
106
+ with:
107
+ python-version: ${{ matrix.python-version }}
108
+
109
+ - name: Install dependencies
110
+ run: |
111
+ python -m pip install --upgrade pip
112
+ pip install build pytest
113
+ pip install -e .
114
+
115
+ - name: Run tests
116
+ run: pytest
117
+
118
+ publish-test-pypi:
119
+ needs: test
120
+ runs-on: ubuntu-latest
121
+ if: github.event_name == 'push' && github.ref != 'refs/heads/main'
122
+ env:
123
+ # Replace MYPACKAGE with your actual package name (normalized)
124
+ # For package "my-awesome.package", use "MY_AWESOME_PACKAGE"
125
+ SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}'
126
+
127
+ steps:
128
+ - uses: actions/checkout@v4
129
+ with:
130
+ fetch-depth: 0
131
+
132
+ - name: Set up Python
133
+ uses: actions/setup-python@v4
134
+ with:
135
+ python-version: "3.11"
136
+
137
+ - name: Install build dependencies
138
+ run: |
139
+ python -m pip install --upgrade pip
140
+ pip install build twine
141
+
142
+ - name: Build package
143
+ run: python -m build
144
+
145
+ - name: Upload to test-PyPI
146
+ uses: pypa/gh-action-pypi-publish@release/v1
147
+ with:
148
+ repository-url: https://test.pypi.org/legacy/
149
+ password: ${{ secrets.TEST_PYPI_API_TOKEN }}
150
+
151
+ publish-pypi:
152
+ needs: test
153
+ runs-on: ubuntu-latest
154
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
155
+ env:
156
+ # Replace MYPACKAGE with your actual package name (normalized)
157
+ SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}'
158
+
159
+ steps:
160
+ - uses: actions/checkout@v4
161
+ with:
162
+ fetch-depth: 0
163
+
164
+ - name: Set up Python
165
+ uses: actions/setup-python@v4
166
+ with:
167
+ python-version: "3.11"
168
+
169
+ - name: Install build dependencies
170
+ run: |
171
+ python -m pip install --upgrade pip
172
+ pip install build twine
173
+
174
+ - name: Build package
175
+ run: python -m build
176
+
177
+ - name: Upload to PyPI
178
+ uses: pypa/gh-action-pypi-publish@release/v1
179
+ with:
180
+ password: ${{ secrets.PYPI_API_TOKEN }}
181
+
182
+ publish-release:
183
+ needs: test
184
+ runs-on: ubuntu-latest
185
+ if: github.event_name == 'release'
186
+
187
+ steps:
188
+ - uses: actions/checkout@v4
189
+ with:
190
+ fetch-depth: 0
191
+
192
+ - name: Set up Python
193
+ uses: actions/setup-python@v4
194
+ with:
195
+ python-version: "3.11"
196
+
197
+ - name: Install build dependencies
198
+ run: |
199
+ python -m pip install --upgrade pip
200
+ pip install build twine
201
+
202
+ - name: Build package
203
+ run: python -m build
204
+
205
+ - name: Upload to PyPI
206
+ uses: pypa/gh-action-pypi-publish@release/v1
207
+ with:
208
+ password: ${{ secrets.PYPI_API_TOKEN }}
209
+ ` ` `
210
+
211
+ # ## GitLab CI Example
212
+
213
+ Here's an equivalent GitLab CI configuration :
214
+
215
+ ` ` ` yaml title=".gitlab-ci.yml"
216
+ stages:
217
+ - test
218
+ - publish
219
+
220
+ variables:
221
+ PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
222
+
223
+ cache:
224
+ paths:
225
+ - .cache/pip/
226
+
227
+ before_script:
228
+ - python -m pip install --upgrade pip
229
+
230
+ test:
231
+ stage: test
232
+ image: python:3.11
233
+ script:
234
+ - pip install build pytest
235
+ - pip install -e .
236
+ - pytest
237
+ parallel:
238
+ matrix:
239
+ - PYTHON_VERSION: ["3.8", "3.9", "3.10", "3.11", "3.12"]
240
+ image: python:${PYTHON_VERSION}
241
+
242
+ publish-test-pypi:
243
+ stage: publish
244
+ image: python:3.11
245
+ variables:
246
+ TWINE_USERNAME: __token__
247
+ TWINE_PASSWORD: $TEST_PYPI_API_TOKEN
248
+ # Replace MYPACKAGE with your actual package name (normalized)
249
+ SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}'
250
+ script:
251
+ - pip install build twine
252
+ - python -m build
253
+ - twine upload --repository testpypi dist/*
254
+ rules:
255
+ - if: $CI_COMMIT_BRANCH != "main" && $CI_PIPELINE_SOURCE == "push"
256
+
257
+ publish-pypi:
258
+ stage: publish
259
+ image: python:3.11
260
+ variables:
261
+ TWINE_USERNAME: __token__
262
+ TWINE_PASSWORD: $PYPI_API_TOKEN
263
+ # Replace MYPACKAGE with your actual package name (normalized)
264
+ SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE: '{"local_scheme": "no-local-version"}'
265
+ script:
266
+ - pip install build twine
267
+ - python -m build
268
+ - twine upload dist/*
269
+ rules:
270
+ - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
271
+
272
+ publish-release:
273
+ stage: publish
274
+ image: python:3.11
275
+ variables:
276
+ TWINE_USERNAME: __token__
277
+ TWINE_PASSWORD: $PYPI_API_TOKEN
278
+ script:
279
+ - pip install build twine
280
+ - python -m build
281
+ - twine upload dist/*
282
+ rules:
283
+ - if: $CI_COMMIT_TAG
284
+ ` ` `
285
+
286
+ # ## Configuration Details
287
+
288
+ # ### Environment Variable Format
289
+
290
+ The environment variable `SETUPTOOLS_SCM_OVERRIDES_FOR_${NORMALIZED_DIST_NAME}` must be set where :
291
+
292
+ 1. **`${NORMALIZED_DIST_NAME}`** is your package name normalized according to PEP 503 :
293
+ - Convert to uppercase
294
+ - Replace hyphens and dots with underscores
295
+ - Examples : ` my-package` → `MY_PACKAGE`, `my.package` → `MY_PACKAGE`
296
+
297
+ 2. **Value** must be a valid TOML inline table format :
298
+ ` ` ` bash
299
+ SETUPTOOLS_SCM_OVERRIDES_FOR_MYPACKAGE='{"local_scheme": "no-local-version"}'
300
+ ` ` `
301
+
302
+ # ### Alternative Approaches
303
+
304
+ **Option 1: pyproject.toml Configuration**
305
+
306
+ Instead of environment variables, you can configure this in your `pyproject.toml` :
307
+
308
+ ` ` ` toml title="pyproject.toml"
309
+ [tool.setuptools_scm]
310
+ # Use no-local-version by default for CI builds
311
+ local_scheme = "no-local-version"
312
+ ` ` `
313
+
314
+ However, the environment variable approach is preferred for CI/CD as it allows different schemes for local development vs. CI builds.
315
+
316
+ # ### Version Examples
317
+
318
+ **Development versions from main branch** (with `local_scheme = "no-local-version"`):
319
+ - Development commit : ` 1.2.3.dev4+g1a2b3c4d5` → `1.2.3.dev4` ✅ (uploadable to PyPI)
320
+ - Dirty working directory : ` 1.2.3+dirty` → `1.2.3` ✅ (uploadable to PyPI)
321
+
322
+ **Tagged releases** (without overrides, using default local scheme):
323
+ - Tagged commit : ` 1.2.3` → `1.2.3` ✅ (uploadable to PyPI)
324
+ - Tagged release on dirty workdir : ` 1.2.3+dirty` → `1.2.3+dirty` ❌ (should not happen in CI)
325
+
326
+ # ## Security Notes
327
+
328
+ - Store PyPI API tokens as repository secrets
329
+ - Use separate tokens for test-PyPI and production PyPI
330
+ - Consider using [Trusted Publishers](https://docs.pypi.org/trusted-publishers/) for enhanced security
331
+
332
+ # ## Troubleshooting
333
+
334
+ **Package name normalization**: If your override isn't working, verify the package name normalization:
335
+
336
+ ` ` ` python
337
+ import re
338
+ dist_name = "my-awesome.package"
339
+ normalized = re.sub(r"[-_.]+", "-", dist_name)
340
+ env_var_name = normalized.replace("-", "_").upper()
341
+ print(f"SETUPTOOLS_SCM_OVERRIDES_FOR_{env_var_name}")
342
+ # Output: SETUPTOOLS_SCM_OVERRIDES_FOR_MY_AWESOME_PACKAGE
343
+ ` ` `
344
+
345
+ **Fetch depth**: Always use `fetch-depth: 0` in GitHub Actions to ensure setuptools-scm has access to the full git history for proper version calculation.
0 commit comments