diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..782d0c4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: Release + +# Tag-triggered publish to PyPI via Trusted Publishing (OIDC, no stored tokens). +# Same shape as the crucible workflow, deliberately: one publish mechanism across the +# fleet means one thing to get right and one thing to audit. +# +# Push a tag that matches the pyproject version, e.g.: +# git tag v0.3.0 && git push origin v0.3.0 +# A manual run (workflow_dispatch) builds and twine-checks WITHOUT publishing, which is +# the only dry run available: there is no TestPyPI path here. +# +# ONE-TIME SETUP, on PyPI rather than in this repo. The publish step authenticates by +# OIDC, so it fails until pypi.org/manage/project/cotdata/settings/publishing/ carries a +# GitHub publisher for owner `mspinola`, repo `cotdata`, workflow `release.yml`, and +# environment `pypi`. All four must match what is written below. + +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Guard — tag must match pyproject version + if: startsWith(github.ref, 'refs/tags/') + run: | + TAG="${GITHUB_REF_NAME#v}" + VER="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" + echo "tag=v$TAG pyproject=$VER" + test "$TAG" = "$VER" || { echo "::error::tag v$TAG does not match pyproject version $VER"; exit 1; } + - name: Guard — __version__ must match pyproject version + run: | + PY="$(python -c "import re; print(re.search(r'__version__ = \"([^\"]+)\"', open('src/cotdata/__init__.py').read()).group(1))")" + VER="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")" + echo "__init__=$PY pyproject=$VER" + test "$PY" = "$VER" || { echo "::error::__version__ $PY does not match pyproject version $VER"; exit 1; } + - name: Build sdist + wheel + run: | + python -m pip install --upgrade build twine + python -m build + twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish-pypi: + needs: build + # tag push -> PyPI. A manual run stops after build, as a dry run. + if: github.event_name == 'push' + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write # required for Trusted Publishing + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f99f99..4bd4f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.3.0] - 2026-08-02 + +**The first release actually published to PyPI since 0.1.0.** 0.2.0 was tagged earlier +today and never uploaded: the repo had no release workflow at the time, and the only +alternative was a hand-rolled `twine` upload with a stored token. That tag stands as a git +tag and nothing more. Everything in it ships here, described in the 0.2.0 section below. + +A PyPI consumer moving from 0.1.0 therefore gets the whole vintage subsystem plus this +release's removal in one step, and `docs/WINDOWS_SETUP.md`'s "PyPI is well behind this +repo, install editable from a clone" note stops being true once this lands. + ### Removed - **`vintage_flow.decompose` and the `cotdata-vintage flow` subcommand.** Flow decomposition was duplicated in `crowdmon.futures.flow`, and measurement showed the two diff --git a/pyproject.toml b/pyproject.toml index ba8365d..084c7b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cotdata" -version = "0.2.0" +version = "0.3.0" description = "Canonical data layer for COT/futures strategies — a producer/consumer split over a file-based store." readme = "README.md" authors = [ diff --git a/src/cotdata/__init__.py b/src/cotdata/__init__.py index 63e56c3..f738239 100644 --- a/src/cotdata/__init__.py +++ b/src/cotdata/__init__.py @@ -4,7 +4,7 @@ from .registry import REGISTRY, Symbol, all_symbols, symbol from .store import load_manifest, require_schema, schema_version -__version__ = "0.2.0" +__version__ = "0.3.0" __all__ = [ "get_prices", "roll_dates", "get_cot", "symbol", "all_symbols", "REGISTRY", "Symbol",