Skip to content
Billy Box edited this page Aug 24, 2026 · 2 revisions

Post-mortem: ctxpack-cli 1.0.0 — issue sweep #20–#27 + first PyPI publish

Mirrored from the HBD knowledge base (2026-08-24 session). Full trail lives in notes/2026-08-24-ctxpack-issue-sweep-and-pypi-publish.md there; this page carries the repo-facing version.

What shipped

Work Issue PR Commit
CJK token-budget truncation overshoot (~2.7x) #20 #26 e27b75e
print_summary counted omitted tombstones as included #21 #28 8ecd8a7
--strict-secrets dropped CLI/config exclude patterns #22 #29 a23c650
Custom base_name outputs re-packed on next run #23 #30 2c59715
Triple-backtick content broke markdown fences #24 #31 eb955b4
UTF-16/BOM text files silently skipped #25 #32 d7d3e80
Project icon + README badge row #27 #33 (+#34) 8367b87, 0b2dcbb

Plus: first PyPI release of ctxpack-cli 1.0.0, built from post-fix main. Test suite grew 130 → 141 passing (2 pre-existing platform skips).

The PyPI publish forensics

The naive path failed with 403 Forbidden — and the failure was correct:

  1. The API token was project-scoped (its base64url payload names project ID 03d89091-… = ctxpack-cli).
  2. Tag v1.0.0 predates the distribution rename: its pyproject builds name = "ctxpack".
  3. ctxpack on PyPI belongs to an unrelated project — which is exactly why the repo renamed the distribution to ctxpack-cli.

So building from the tag produced an artifact the scoped token rightly refused. Resolution: build from current main (correct name + six weeks of fixes), verify wheel METADATA (Name: ctxpack-cli, console-script present), smoke-test the installed CLI on a scratch repo, then upload. Verified end-to-end afterward via clean-venv pip download + run.

Tag v1.0.0 remains as historical record (option 2): its GitHub Release now carries an [!IMPORTANT] callout stating the PyPI artifact was built from a later commit on main including fixes #20–#25, and that future tags will be cut from post-merge main so tag ↔ PyPI always match.

Gotchas worth remembering

  • Repro audits matter. Issue #22's literal repro (--exclude "*.log") passed even on buggy code — *.log was already a built-in default, so it couldn't discriminate. Regression tests must use inputs with no built-in coverage.
  • Fixture shape matters. In trim_to_budget(), two files means file #2 gets truncated; tombstones (omitted=True) only appear from the third file onward.
  • shields.io caches "not found". Pre-publication URL probes seeded a negative cache; live PyPI badges rendered broken for hours. Committed URLs carry ?style=flat (fresh cache key) — self-heals globally when the 3h TTL expires.
  • CI runs only on push-to-main or PRs targeting main. A bare branch push produces zero check runs. Open the PR before claiming CI status.
  • Run both gates locally: ruff check and ruff format --check — at the 88-char boundary they can disagree about the same line.
  • TDD RED-first, but confirm each test fails for the right reason before trusting it as a regression guard.
  • Race window: a PR can merge while you're still pushing follow-up commits to its branch (happened with #33/#34 — recovered by cherry-pick onto fresh main).
  • git-bash heredocs containing backticks break bash eval — write commit messages to temp files and use git commit -F.