Skip to content

Commit a0a54d2

Browse files
Merge pull request #1171 from RonnyPfannschmidt/fix-311-document-ci-with-pypi-upload
document recommended ci additions for pypi/test-pypi uploads
2 parents 59b5b92 + 538251c commit a0a54d2

File tree

4 files changed

+312
-2
lines changed

4 files changed

+312
-2
lines changed

.github/workflows/python-tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ jobs:
2222
package:
2323
name: Build & inspect our package.
2424
runs-on: ubuntu-latest
25+
env:
26+
# Use no-local-version for package builds to ensure clean versions for PyPI uploads
27+
SETUPTOOLS_SCM_NO_LOCAL: "1"
2528

2629
steps:
2730
- uses: actions/checkout@v4
@@ -117,6 +120,7 @@ jobs:
117120

118121
test-pypi-upload:
119122
runs-on: ubuntu-latest
123+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
120124
needs: [test]
121125
permissions:
122126
id-token: write

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
- fix #1059: add `SETUPTOOLS_SCM_PRETEND_METADATA` environment variable to override individual ScmVersion fields
1111
- add `scm` parameter support to `get_version()` function for nested SCM configuration
1212
- fix #987: expand documentation on git archival files and add cli tools for good defaults
13+
- fix #311: document github/gitlab ci pipelines that enable auto-upload to test-pypi/pypi
14+
1315

1416
### Changed
1517

_own_version_helper.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from __future__ import annotations
1010

1111
import logging
12+
import os
1213

1314
from typing import Callable
1415

@@ -22,6 +23,7 @@
2223
from setuptools_scm.fallbacks import parse_pkginfo
2324
from setuptools_scm.version import ScmVersion
2425
from setuptools_scm.version import get_local_node_and_date
26+
from setuptools_scm.version import get_no_local_node
2527
from setuptools_scm.version import guess_next_dev_version
2628

2729
log = logging.getLogger("setuptools_scm")
@@ -48,11 +50,18 @@ def parse(root: str, config: Configuration) -> ScmVersion | None:
4850

4951

5052
def scm_version() -> str:
53+
# Use no-local-version if SETUPTOOLS_SCM_NO_LOCAL is set (for CI uploads)
54+
local_scheme = (
55+
get_no_local_node
56+
if os.environ.get("SETUPTOOLS_SCM_NO_LOCAL")
57+
else get_local_node_and_date
58+
)
59+
5160
return get_version(
5261
relative_to=__file__,
5362
parse=parse,
5463
version_scheme=guess_next_dev_version,
55-
local_scheme=get_local_node_and_date,
64+
local_scheme=local_scheme,
5665
)
5766

5867

docs/integrations.md

Lines changed: 296 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,299 @@ build:
4747
- export SETUPTOOLS_SCM_OVERRIDES_FOR_${READTHEDOCS_PROJECT//-/_}='{scm.git.pre_parse="fail_on_shallow"}'
4848
```
4949

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

Comments
 (0)