Lesson 12 of 12 · The release

Ship it. Then keep shipping it.

The package works. The tests are green. Now turn the working repository into an artifact other people can pip install and use. This lesson is different from every prior one: almost no new code, but every file you add matters disproportionately for whether anyone adopts your work. We’ll set up a license, a README that earns trust, a CHANGELOG that documents what each release contains, an academic citation file, and a GitHub Actions workflow that publishes to PyPI on a git tag. Then we’ll cut v0.1.0 — not as a one-off ceremony, but as the first turn of a loop you’ll repeat for every future release.

Time: ~90 min You'll touch: LICENSE · NOTICE · README · CHANGELOG · CITATION · release.yml · pyproject Result: pip install opentrash works for anyone, anywhere

· Objective

Six new files, one workflow, one version bump. Then a tag, a push, and PyPI does the rest.

  • LICENSE — Apache License 2.0 verbatim. Permissive, recognized, protects attribution.
  • NOTICE — the short attribution file Apache 2.0 expects alongside the license.
  • README.md — the front door. Concise, with install + a real quickstart code example.
  • CHANGELOG.mdKeep a Changelog format. First entry: 0.1.0.
  • CITATION.cff — academic citation metadata. GitHub renders a "Cite this repository" button once public.
  • .github/workflows/release.yml — tag-triggered build, test, and publish-to-PyPI.
  • pyproject.toml — version 0.1.0, license metadata, classifiers, project URLs.
  • MANIFEST.in — tells setuptools to include LICENSE/NOTICE/etc. in the sdist.
The discipline this lesson teaches: a release is a loop, not a one-time event. v0.1.0 is the first turn. v0.2.0, v0.3.0, ..., v1.0.0 are the same six-step dance every time. The workflow automates the build and publish; you stay focused on what changed and why.

· Build it, step by step

1 License: Apache 2.0

Apache 2.0 is the right tool when the goal is maximum adoption with proper attribution. Free for researchers, students, public entities, AND companies, with a strict attribution requirement, an explicit patent grant, and an industry-standard footprint that no legal team will object to. Source-available non-commercial licenses (PolyForm and friends) make sense when you want to charge companies; if you don’t, Apache is the better fit.

  • Download the canonical text from apache.org/licenses/LICENSE-2.0.txt and save as LICENSE.
  • Fill in your copyright line in the appendix (e.g., Copyright 2026 Your Name / Your Organization).
  • Add a short NOTICE file with your attribution and a pointer to the LICENSE.

2 README.md — the front door

The single most important file in the repository for adoption. People decide whether to engage in 30 seconds based on what they see here. Concise, with three blocks that earn trust:

  1. What it is, in 2–3 sentences. No marketing language. State the function.
  2. Install instructions immediately under the description. Don’t make readers scroll.
  3. A real quickstart code example that proves the package does something concrete. Not pseudocode — actual imports, actual function calls, actual output paths.

Then: architecture diagram, doc links, license, citation, acknowledgments. Resist the urge to put everything in the README; deep documentation belongs on the docs site.

3 CHANGELOG.md — document what each release contains

Keep a Changelog format with semver. First entry:

## [0.1.0] — 2026-06-07

First public release.

### Added
- `opentrash.core` — CRS, DuckDB session, vehicle IDs
- `opentrash.engine` — the routing engine
- `opentrash.patterns` — per-parcel signature detection
- `opentrash.routeview` — interactive HTML maps
- ...

Future entries follow the same template: ### Added, ### Changed, ### Deprecated, ### Removed, ### Fixed, ### Security. The discipline pays off when someone asks "what changed in 0.4.0?" and you point at the file.

4 CITATION.cff — for academic users

A simple YAML file that GitHub uses to render a "Cite this repository" button (once the repo is public). Tells researchers exactly how to cite the package in papers:

cff-version: 1.2.0
title: "opentrash: open-source geospatial analytics for residential waste collection"
authors:
  - given-names: Your
    family-names: Name
    affiliation: "Your Organization"
license: Apache-2.0
version: "0.1.0"
date-released: "2026-06-07"
url: "https://opentrash.app/"

Small file, big payoff: it’s the difference between getting properly cited and being lumped under "various open-source tools."

5 pyproject.toml — the metadata that lands on PyPI

Three changes from the dev version:

  • Version: 0.1.0.dev00.1.0.
  • License: { text = "Apache-2.0" }. Classifier: "License :: OSI Approved :: Apache Software License".
  • Project URLs + classifiers: Homepage, Documentation, Changelog, Issues; classifiers for Development Status, Intended Audience, Topic. These show up on the PyPI page.

One TOML gotcha caught during this lesson: [project.urls] must come AFTER dependencies and [project.optional-dependencies] in the TOML — otherwise dependencies ends up parsed as a key inside project.urls. Setuptools yells. Easy to fix; easy to miss.

6 MANIFEST.in — what gets shipped in the sdist

Setuptools’ defaults don’t include LICENSE, NOTICE, CHANGELOG, or CITATION in the sdist. Tell it to:

include LICENSE
include NOTICE
include CHANGELOG.md
include CITATION.cff
include README.md
recursive-include opentrash/routeview/templates *.html

Verify by building and inspecting the artifact:

pip install build
python -m build
tar tzf dist/opentrash-0.1.0.tar.gz | grep -E "(LICENSE|NOTICE|CHANGELOG|CITATION|README|template)"

7 Release workflow — .github/workflows/release.yml

Tag-triggered automation. Four jobs in sequence:

  1. verify-version — refuses to proceed if the git tag doesn’t match the version in pyproject.toml. Catches typos before they hit PyPI.
  2. test — runs the full suite + lint on Python 3.11 and 3.12. Fail fast if something regressed.
  3. build — produces sdist + wheel into dist/, uploads as a CI artifact.
  4. publish-pypi — downloads the dist artifact, runs twine upload using the PYPI_API_TOKEN repository secret.

One-time PyPI setup before the first release:

  1. Create a PyPI account at pypi.org.
  2. Generate a project-scoped API token at pypi.org/manage/account/token/. Scope it to the opentrash project once it exists; for the very first upload, use an account-wide token, then narrow it.
  3. In the repo, add a secret named PYPI_API_TOKEN at Settings → Secrets and variables → Actions → New repository secret.

Private repo → public PyPI is fine. They’re independent systems. The PyPI artifact contains the source code snapshot, but the git history, issues, and drafts stay private until you flip the repository’s public switch.

8 Cut the release

# 1. CHANGELOG entry already added in step 3
# 2. pyproject.toml version already bumped to 0.1.0 in step 5

# 3. Commit the release-prep changes
git add LICENSE NOTICE README.md CHANGELOG.md CITATION.cff \
        MANIFEST.in pyproject.toml \
        .github/workflows/release.yml
git commit -m "Release 0.1.0: license, README, CHANGELOG, citation, release workflow"
git push origin main

# 4. Tag and push
git tag -a v0.1.0 -m "First public release"
git push origin v0.1.0

# 5. Watch GitHub Actions run the workflow. It verifies, tests, builds,
#    and publishes to PyPI automatically.

# 6. Verify (from any clean environment, ~5 minutes after the workflow finishes)
pip install opentrash
python -c "import opentrash; print(opentrash.__version__)"
# expected: 0.1.0

· The release loop (for every future version)

v0.1.0 was the first turn. Every future release is the same six steps. Memorize this; you’ll do it many times.

# 1. Update CHANGELOG with the new entry
#    (keep [Unreleased] at top; add a new ## [0.2.0] — YYYY-MM-DD section below)

# 2. Bump version in pyproject.toml AND opentrash/__init__.py to 0.2.0

# 3. Commit
git add CHANGELOG.md pyproject.toml opentrash/__init__.py
git commit -m "Release 0.2.0"
git push origin main

# 4. Tag and push
git tag -a v0.2.0 -m "Release 0.2.0: "
git push origin v0.2.0

# 5. Watch the workflow run. It does the rest.

# 6. Verify
pip install --upgrade opentrash
python -c "import opentrash; print(opentrash.__version__)"

That’s it. The discipline isn’t in the steps; it’s in following them every time. Six steps, every release, forever — through 0.2.0, 0.3.0, the eventual 1.0.0, and beyond.

One nuance worth knowing about semver: while you’re in 0.x, breaking changes between minor versions (0.1 → 0.2) are acceptable as long as the CHANGELOG documents them. At 1.0.0 the contract tightens: breaking changes require a major bump (1.x → 2.0.0). Stay in 0.x until the API has had real-world use and you’re confident you don’t want to change it. opentrash will probably live in 0.x for a few months yet.

· Package anatomy after this lesson

The final shape. new marks files added this lesson.

opentrash/ ├── LICENSE # [new] Apache 2.0 ├── NOTICE # [new] Apache attribution ├── README.md # [updated] real README ├── CHANGELOG.md # [new] Keep a Changelog format ├── CITATION.cff # [new] academic citation ├── MANIFEST.in # [new] sdist inclusion list ├── pyproject.toml # [updated] version 0.1.0, Apache, urls ├── .github/workflows/ │ ├── ci.yml │ ├── docs.yml │ └── release.yml # [new] tag-triggered PyPI publish ├── docs/{index,architecture}.md ├── opentrash/ │ ├── __init__.py # [updated] __version__ = "0.1.0" │ ├── adapters/ · cache/ · core/ · prep/ · tonnage/ │ ├── engine/ · patterns/ · routeview/ │ └── (all module trees from L0–L11) └── tests/ (18 test files, 180 tests)

· What you built — the whole course

12 lessons. One working open-source package. Here’s the entire arc:

  • L0–L2: notebook → package, repo skeleton, core foundations (CRS, DuckDB, parcels).
  • L3–L5: static GIS layers, sites, tonnage pipeline with idempotent upsert.
  • L6–L7: GPS adapters, cache, master index, WKB substrate.
  • L8–L9: the routing engine — one spatial-join pass — and load-organized segments timeline.
  • L10: route-agnostic per-parcel pattern detection.
  • L11: RouteView interactive HTML map with patterns overlay.
  • L12: license, README, CHANGELOG, citation, release workflow, and the published pip install opentrash artifact.
The architectural rule held across all 12 lessons: spatial joins are infrastructure; products are calculations. The engine does the spatial work once; everything downstream is pure aggregation. Patterns + RouteView are two views of the same enriched stream — macro and micro — forming a validation loop between long-period prediction and single-day reality.

The package is now live on PyPI, citable, properly licensed, and ready for the next turn of the loop. Congratulations: you’ve built and shipped a real open-source artifact.

· Companion resources

For going further.

· What’s next

The course ends here, but the package’s life is just starting. Some honest next directions if you’d like to keep building:

  • 0.2.0: the next release. Could be road-network mileage to replace the haversine approximation; could be a tonnage-into-segments product; could be tile-server alternatives in RouteView. Whatever real use surfaces first.
  • Per-commodity awareness: the engine currently collapses to one route_id; production has three (refuse, organics, recycling). A future release could carry the commodity dimension through the engine.
  • conda-forge package: PyPI handles pip users; conda-forge reaches the scientific-Python conda crowd. The recipe is small once PyPI is live.
  • Public repo flip: when you’re ready, making the GitHub repo public unlocks issues, PRs, GitHub Stars for visibility, and the "Cite this repository" button.

Thanks for walking through 12 lessons. Now go build the next one. 🚀