Publishing guide¶
How racinglines reaches PyPI and the public repo. Read The rules first. They are the owner's (18 Oct 2026),
and every release, script and workflow below exists to keep them.
The rules¶
- Two repos, one direction.
racinglines/racinglinesis private: all development, full history, Jev, the TUI, other sports, data, ops.racinglines/racinglines-publicis public: the released code only, one commit per release, no development history. Code moves private → public only, and only through the export. - The export is the only door. A file reaches racinglines-public or a PyPI package only if
oss/manifest.tomllists it.scripts/oss/export.pycopies just those tracked files and fails, with nothing pushed or built, when a file matches a banned pattern (real sportsbook names, personal paths, the bucket), is over the size cap (data never ships), or would leave an import broken. Anything not on the allowlist stays private, including new files by default. To ship something, add it to the manifest in a reviewed PR. Never copy files to the public repo by hand. - Every PyPI version is one public commit and tag.
vXon PyPI = tagvXon racinglines-public = one commit whose message names the private source commit. Nothing is uploaded that isn't in the public repo. - The private repo never uploads (from 1.0.1). It tags and exports; racinglines-public's own workflow builds and publishes, so PyPI's provenance (the attestation) names a public commit anyone can read.
- The real upload waits on the owner. The
pypijob waits for the owner's approval in GitHub. A PyPI version can be yanked but never reused or replaced, so a bad release is fixed with the next patch version. - What the package is. A snapshot of how the fantasy run started: F1 trading, the UCI downhill sim, the MK8DX
(kart) demo and the web app with its live viewer (
pip install racinglines[web]). No Jev, no TUI, no other sports. Fantasy bug fixes and tweaks ship as 1.x. A private or internal package is a later decision, not now. - Test from TestPyPI the safe way. Install only racinglines from TestPyPI (
--no-deps) and every dependency from PyPI (see Each release).
Where things stand (19 Oct 2026)¶
| What | State |
|---|---|
| PyPI | racinglines 1.0.1 (19 Oct), the first release through the flow below: built and uploaded by racinglines-public, so its attestation names racinglines/racinglines-public (tag v1.0.1, private source fab9c6b). 1.0.0 (18 Oct) was uploaded by the private repo |
| racinglines-public | One commit per release: v1.0.0 (pushed by hand), v1.0.1rc2, v1.0.1rc3, v1.0.1 (pushed by the private release job) |
| Next | Switch-over step 5: remove the private repo's publishers and environments |
How a release flows (from 1.0.1)¶
| Step | Where | What happens |
|---|---|---|
| 1. Tag | private racinglines/racinglines |
The owner tags a commit on staging or main vX (rc first) from the Mac |
| 2. Export | private, .github/workflows/release.yml (from deploy/ci/release.yml) |
Allowlist export of the tag at version X, then a build and smoke test of a copy (RELEASE=1: the plain MIT LICENSE, the version must match). Any failure stops here |
| 3. Push | private → public | The export replaces racinglines-public's tree as one commit (racinglines X (PyPI X; export of the private source at <sha>)) with tag vX, using the write deploy key. It refuses if the tag already exists |
| 4. Publish | racinglines-public, .github/workflows/release.yml (from oss/overlay/) |
Builds the tagged tree as it is, smoke-tests it, uploads to TestPyPI, then to PyPI after the owner approves pypi. rc/a/b tags stop after TestPyPI |
Outside contributions to racinglines-public are ported into private by hand. The next export replaces the public tree,
so a change that wasn't ported disappears. The public repo's CONTRIBUTING.md (oss/overlay/) says so.
What each release ships¶
| Version | Install | What works |
|---|---|---|
| 1.0.1 | the same | 1.0.0 plus: published from racinglines-public, GitHub links and the from-source steps point there, CHANGELOG.md |
| 1.0.0 | pip install racinglines, pip install "racinglines[web]" |
Tiers core, mtb_dh, web of the manifest: the F1 engine and CLI, the downhill sim, the kart demo, the database schema and migrations, the web app (Markets, Trade, History, Lab, the live viewer), F$ fantasy trading. web --demo creates the demo accounts; / is the sign-in page. The README's "Everything" steps are the test |
The scope is the manifest's, not this page's: python scripts/oss/export.py --list prints the file list, and the
export prints the lazy imports of left-out modules (commands that stop with an ImportError in the package).
How an installed package finds its files¶
racinglines/paths.py: in a checkout (any folder with pyproject.toml next to the package, including pip install
-e . on the VM and the Mac) nothing changes. In a wheel, the committed configs sports/, markets/, exchanges/
and migrations/ are read from the copies bundled at racinglines/_bundled/ (the file list is in setup.py), and
data/ defaults to ~/.racinglines/data (RACINGLINES_DATA still overrides it).
The pieces¶
| File | What it does |
|---|---|
oss/manifest.toml |
The allowlist: tiers of include/exclude globs, the banned patterns and the size cap |
scripts/oss/export.py |
Copies a ref's tracked files that the manifest allows, lays oss/overlay/ over them, sets the version; fails on the scrub, the size cap or a broken import |
oss/overlay/ |
What only the public tree has: the plain MIT LICENSE, the README.md (the PyPI page), the CHANGELOG.md, the CONTRIBUTING.md, .github/workflows/release.yml (the public publish workflow) |
deploy/ci/release.yml |
The private workflow: tag → export → smoke → push to racinglines-public. The owner copies it to .github/workflows/release.yml |
scripts/release/package_smoke.sh |
Builds the sdist and wheel, twine check --strict, checks the bundled configs are in and no tests/data/docs are, installs the wheel in a fresh venv and runs the CLI from an empty folder. RELEASE=1 also requires the plain MIT LICENSE |
setup.py |
The package's file list (packages, templates, the bundled configs) |
Switch-over (owner, once)¶
Before 1.0.1. Everything is in the browser or on the Mac; no token or password goes in a terminal.
- A write deploy key for racinglines-public. Deploy keys need an admin of racinglines-public (the
racinglinesaccount;lalligaggeris a collaborator, andgh repo deploy-key addas lalligagger answers 404). On the Mac:ssh-keygen -t ed25519 -N "" -C racinglines-release -f ~/tmp/rl-release-key && pbcopy < ~/tmp/rl-release-key.pub(the public half only). In the browser asracinglines: racinglines-public → Settings → Deploy keys → Add, paste, tick Allow write access. Then the private half goes straight from the file into a repository Actions secret of the private repo (not an environment secret, not on racinglines-public):gh secret set PUBLIC_DEPLOY_KEY --repo racinglines/racinglines < ~/tmp/rl-release-key && rm ~/tmp/rl-release-key*. If the release log showsDEPLOY_KEY:empty andLoad key ...: error in libcrypto, the secret is missing; if it showsPermission denied (publickey)alone, the public half isn't on racinglines-public. Redo this step either way. - Environments in racinglines-public (Settings → Environments):
testpypiwith a deployment rule of Ref type Tag, patternv*(the dropdown defaults to Branch, which doesn't match tags);pypiwith Required reviewers =lalligaggerand the same Tag rule. Untick "Allow administrators to bypass". - Trusted publishers for racinglines-public. On pypi.org: the project
racinglines→ Manage → Publishing → add a GitHub publisher: ownerracinglines, repositoryracinglines-public, workflowrelease.yml, environmentpypi. The same on test.pypi.org with environmenttestpypi. - The private workflow, as the owner's own PR (cloud sessions can't push workflows):
cp deploy/ci/release.yml .github/workflows/release.ymlon a branch fromstaging, commit, PR, merge. - After 1.0.1 is on PyPI: remove the
racinglines/racinglinespublishers on pypi.org and test.pypi.org, and itstestpypi/pypienvironments, so the private repo can't upload at all.
Each release¶
The changelog first. Add a ## X (date) entry for the version to oss/overlay/CHANGELOG.md (what a user of the
package sees, newest first) in the PR that goes out with it. The export of vX or vXrcN fails without it, and the
file ships as racinglines-public's CHANGELOG.md (linked from PyPI's sidebar as Changelog).
Dry run first (TestPyPI only). Tag the commit with an rc version from the Mac (cloud git can't push tags; outside
conda, so git is /usr/bin/git):
# LOCAL (Mac)
cd ~/py_dev/racinglines && conda deactivate; /usr/bin/git fetch origin staging && /usr/bin/git log --oneline -1 origin/staging
/usr/bin/git tag v1.0.1rc1 origin/staging && /usr/bin/git push origin v1.0.1rc1
The private run "release" exports, smokes and pushes; then racinglines-public's run "release" builds and uploads to
TestPyPI. Install it the way a user would: only racinglines from TestPyPI (--no-deps), every dependency from
PyPI. Never let pip resolve dependencies against TestPyPI: anyone can upload there, and squatted names win on version
number (1.0.0rc1's test pulled a broken "FASTAPI 1.0" that fails to build).
# LOCAL (Mac): outside conda, so pip and racinglines are the venv's
rm -rf ~/tmp/rl-try && python3.14 -m venv ~/tmp/rl-try && source ~/tmp/rl-try/bin/activate
pip install --no-cache-dir --index-url https://test.pypi.org/simple/ --no-deps "racinglines==1.0.1rc1"
pip install "racinglines[web]==1.0.1rc1"
hash -r && which racinglines && cd /tmp && racinglines --version && racinglines check --offline --no-db --sport f1
which racinglines must end in rl-try/bin/racinglines, and the version must be the rc's. Then run the README's
"Everything" steps against a throwaway container on a spare port (docker run --name rl-rc-db ... -p 5434:5432 ...
with DATABASE_URL=...@localhost:5434/racinglines and RACINGLINES_DATA exported), so the test touches none of your
own containers or data. Never remove the racinglines-db container or its volume for a test.
The real one, on the owner's go: tag the same commit vX (git tag v1.0.1 v1.0.1rc1, push it). After TestPyPI,
the public run waits on pypi: Review deployments → tick pypi → Approve and deploy. Then check
https://pypi.org/project/racinglines/ and pip install racinglines==X with no index flags.
If something goes wrong¶
- The export fails (a banned name, an oversized file, a broken import, no changelog entry): nothing was pushed. Fix it in private, tag a new rc.
- The smoke fails: nothing pushed or uploaded. Same fix.
- The push says the tag already exists: that version is released; tag the next one.
- The public run fails before PyPI: the public commit and tag exist but PyPI doesn't have the version. Fix in private and release the next patch version, never re-tag.
- "Tag vX is not allowed to deploy to pypi": the environment's rule is a Branch rule or missing; make it Ref type
Tag,
v*, then "Re-run failed jobs". - A bad build reached PyPI: yank it on pypi.org (Manage → Releases), never delete, and ship the next patch version.