From abbdfa8b3b82eb0a8efc7c22d184c4b20f10d393 Mon Sep 17 00:00:00 2001 From: Matt Spinola Date: Sun, 2 Aug 2026 17:05:49 -0400 Subject: [PATCH 1/2] ci: tag-triggered PyPI publish via Trusted Publishing cotdata had no release workflow, so publishing meant a manual twine upload with a stored API token from somebody's laptop. crucible already solved this the other way, with OIDC Trusted Publishing and no stored credentials, and one publish mechanism across the fleet is one thing to get right and one thing to audit. This is that workflow, adapted. Two guards rather than crucible's one, because cotdata carries its version in two places: pyproject.toml and src/cotdata/__init__.py. A release that bumps one and forgets the other installs fine and then lies about itself at runtime, which is exactly the kind of thing nobody checks by hand twice. Does not publish anything on its own. It needs a one-time GitHub publisher on PyPI (owner mspinola, repo cotdata, workflow release.yml, environment pypi) which can only be configured from the project owner's PyPI account, and it fires on a tag push whose version matches pyproject. Co-Authored-By: Claude Opus 5 --- .github/workflows/release.yml | 69 +++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/release.yml 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 From 3dfa96b457f9d4c957bf9bb3fb70bc5a3a8443fe Mon Sep 17 00:00:00 2001 From: Matt Spinola Date: Sun, 2 Aug 2026 17:11:51 -0400 Subject: [PATCH 2/2] Release 0.3.0: the first PyPI publish since 0.1.0 0.2.0 was tagged earlier today and never uploaded, because this repo had no release workflow at the time and the alternative was a hand-rolled twine upload with a stored token. That tag stands as a git tag and nothing more. Main has since moved past it (#93 removed the flow decomposition), so publishing the tagged tree would ship something that was never main's head, and re-pointing the tag would change what 0.2.0 means after the fact. 0.3.0 is what main actually is. Minor rather than patch because a public CLI subcommand is gone, and everything from 0.2.0 rides along, so a consumer stepping up from PyPI's 0.1.0 gets the whole vintage subsystem in one move. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- src/cotdata/__init__.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) 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",