Releasing
ftsdata follows the OpenAstronomy packaging
guide:
the version is derived from git tags by
setuptools_scm, and the release is
built, tested and uploaded by CI.
Version numbers
There is no version string to edit. setuptools_scm runs git describe and
turns the distance from the last tag into a PEP 440 version:
Repository state |
Version |
|---|---|
exactly on tag |
|
22 commits after |
|
uncommitted changes |
|
Release tags must look like vX.Y.Z. ftsdata/version.py is generated at
build time and must not be edited or committed.
Because a dirty working tree changes the version, releases are always built by CI from the tagged commit.
One release tag per commit
Never put two release tags on the same commit, for example v0.1.0rc1 and
v0.1.0. git describe cannot tell which tag is meant, so setuptools_scm
may report either version: a v0.1.0 tag can end up building 0.1.0rc1.
This is not hypothetical. On 2026-10-30 the first v0.1.0 tag was placed on
the same commit as v0.1.0rc1, the GitHub workflow rebuilt 0.1.0rc1, and
PyPI rejected the duplicate files with HTTPError: 400 Bad Request.
Tag a later commit than the previous release: a follow-up fix, a documentation change, or, if nothing else changed, an empty commit.
The GitHub workflow checks this before building. It compares the version
reported by setuptools_scm with the tag name and aborts with a clear error
instead of building or uploading the wrong release.
Release checklist
Make sure
mainis green and the changelog (if any) is up to date.Create an annotated tag on the commit you intend to release, verify it, then push it:
git tag -a v0.1.0 -m "Release v0.1.0"
Check that the tag resolves to the version you intend to publish:
git tag --points-at HEAD # must list only v0.1.0 python -m pip install setuptools_scm # once python -m setuptools_scm # must print 0.1.0
The version must equal the tag name without the leading
v, andHEADmust carry no other release tag. Then push:git push origin v0.1.0
If the GitHub mirror is used for the publish workflow, push the tag there too:
git push <github-remote> v0.1.0
Let CI build, test and publish (see below).
Check https://pypi.org/project/ftsdata/ and install from PyPI to confirm.
Update the installation section of
README.rstonce the first release is available.
Moving or re-creating a tag
If a release tag points at the wrong commit, delete it everywhere and create it again before PyPI has accepted the version:
git tag -d v0.1.0
git push origin :refs/tags/v0.1.0
git push <github-remote> :refs/tags/v0.1.0
# make sure the intended commit is on main first
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0
git push <github-remote> v0.1.0
Protected tag patterns (GitLab v*, GitHub tag rulesets) can block deleting or
re-pushing a tag; relax them temporarily. Once PyPI has accepted a version,
never re-tag it: bump the version instead.
GitHub Actions (default)
.github/workflows/publish.yml calls the OpenAstronomy reusable workflow
publish_pure_python.yml@v1. It runs on pushes to main, on v* tags and on
manual dispatch; only v* tags are uploaded, so pushes to main and manual
runs are safe dry runs of the full build-and-test path.
The workflow:
builds the sdist and the pure-Python wheel,
installs the sdist in a clean virtual environment with the
all,testextras,runs
pytest --pyargs ftsdata,uploads
dist/*to PyPI when the ref is av*tag.
One-off setup: create a PyPI API token and store it as the pypi_token
repository (or organisation) secret in GitHub. The reusable workflow uses
token authentication, not PyPI Trusted Publishing.
GitLab CI (optional)
.gitlab-ci.yml contains a publish-pypi job that mirrors the GitHub
workflow. It is manual and only exists in pipelines for v* tags, so
nothing is published without an explicit action:
push the tag,
wait for the tag pipeline’s test jobs to pass,
press play on
publish-pypi.
The job builds the distributions from the tag, runs twine check, then
uploads them. The upload uses --skip-existing: if the GitHub workflow has
already published the same tag (or the job is re-run), the files that are
already on PyPI are skipped instead of failing with an HTTP 400 “File already
exists”. It expects a masked and protected PYPI_API_TOKEN CI/CD variable
(TWINE_USERNAME is set to __token__ in the job). Protect the v* tag
pattern as well, so only maintainers can create release tags.
Remove the job if releases are managed through GitHub Actions only.
Manual upload
The CI jobs can always be reproduced locally from a clean checkout of the tag:
python -m pip install --upgrade build twine
rm -rf dist/
python -m build
python -m twine check dist/*
Dry run against TestPyPI:
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-... # TestPyPI token
python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
python -m pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple ftsdata==0.1.0
Publish to PyPI:
export TWINE_USERNAME=__token__
export TWINE_PASSWORD=pypi-... # PyPI token
python -m twine upload dist/*
A version can never be re-uploaded to PyPI: if a release is wrong, bump the
version and tag again. If only some files were already uploaded, add
--skip-existing so a retry uploads just the missing ones.
For a one-off build with an explicit version (for example to inspect the metadata without tagging):
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 python -m build