Appendix B — Common pitfalls
Mistakes that cost the most time to undo
Every mistake in this appendix has cost someone at least a day of debugging. Most are easy to avoid if you know they’re coming.
1. Committing credentials to git history
The mistake: .env with an API key gets committed. You notice a week later. You add it to .gitignore and think you’re done.
The consequence: the key is still in git history forever. Anyone who clones your repo can extract it with git log -p. Bots that scan public GitHub for credentials will find it in minutes.
The fix:
Rotate the credential immediately. Assume it is already leaked. Generate a new one at the provider’s dashboard. Update any places that use it.
Rewrite git history with
git-filter-repo:pip install git-filter-repo git filter-repo --path .env --invert-paths --forceForce-push to
main:git push origin main --forceTell every collaborator to re-clone. Their existing clones still have the credential in reflog.
Prevention: .env in .gitignore from day one. Do not commit anything you would not print on a poster.
Force-push destroys history that other people may have based work on. Only do it when the alternative (a leaked credential) is worse. Rotate the credential first — if the rotation is done, the leak is a bounded problem even if the history rewrite fails.
2. PyPI name-squatting
The mistake: you develop myawesomepackage for six months, get it working perfectly, decide to publish. Go to PyPI to register the name. It’s taken.
The consequence: you either rename your package (breaking any existing users, links, and documentation) or negotiate with the squatter (usually futile). Some names are actively contested; some are cybersquatted intentionally.
The fix: on the day you name your project, go to https://pypi.org/manage/account/publishing/ and register a pending publisher with the name. This reserves it in your name against the eventual publish workflow. Costs 5 minutes. No obligation to publish anything.
Real-world example: several BuckAI-adjacent projects have discovered common single-word names taken by test packages that were never maintained. There’s no PyPI mechanism to reclaim these.
Prevention: reserve early. Reserve twice if you’re not sure of the name (register both variants, drop the one you don’t end up using).
4. Docker image published but visibility left as private
The mistake: your GHCR workflow succeeds, your image is published, but the default visibility on GitHub Container Registry is private. docker pull ghcr.io/yourorg/yourthing:latest fails with “unauthorized” for anyone who is not logged in as you.
The consequence: users get an install failure that looks like a broken image but is actually a permissions issue. Confusing, wastes their time and yours.
The fix: on your repo landing page, in the right-hand sidebar, find Packages → click the package → Package settings → Change visibility → Public → confirm.
Prevention: the first time your workflow succeeds, immediately go check the visibility. Consider adding a note in your release checklist.
5. Trailing setuptools_scm version like 0.0.dev0+d20260101
The mistake: you publish to PyPI, and the version string is 0.0.dev0+d20260101 instead of 0.1.0.
The consequence: users can install but import yourthing; yourthing.__version__ returns nonsense. PyPI puts your release in a low sort order. Reproducibility citations point at a fake version.
Root cause: your build workflow forgot fetch-depth: 0 on the checkout step. Setuptools_scm needs full git history to see the version tag.
The fix:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # <-- this linePrevention: always fetch-depth: 0 in the workflow that builds distributions. Yank the bad PyPI release and republish.
6. Unicode glyphs missing from the JOSS template font
The mistake: you write F1 ≈ 0.95 in paper.md. The Inara/pandoc pipeline compiles your PDF. The ≈ glyph renders as a Unicode replacement character (�) because the JOSS template’s default font doesn’t have it.
The consequence: your PDF looks amateurish. Reviewers ask what happened. You wonder why your local Markdown preview looked fine.
The fix: replace bare Unicode maths glyphs with inline LaTeX:
F1 $\approx 0.95$ # good
F1 ≈ 0.95 # renders as F1 � 0.95 in some fontsSame applies to ±, ≤, ≥, arrows, Greek letters, and anything else outside basic Latin.
Prevention: render your paper.md to PDF early — the draft-pdf GitHub Action makes this a per-commit check. Once you’ve seen the JOSS-rendered version once, you’ll know what to watch for.
7. Notebook code that only works in Colab
The mistake: you develop your demo notebook in Google Colab. It uses !apt-get install ... in a bash cell and reads sample data from !wget https://.... You commit the notebook to your repo. A reader tries to run it locally in Jupyter.
The consequence: !apt-get fails on macOS. !wget fails on Windows. The notebook is silently Colab-only.
The fix: notebooks that ship with the package should be Colab-first but Colab-compatible-not-Colab-exclusive. Use Python-side alternatives:
import urllib.request
urllib.request.urlretrieve("https://...", "data.tif") # works anywhere
# NOT: !wget https://... (works only where wget is installed)For install: use pip install in a first cell so both Colab and local runs get the deps. Guard OS-specific paths with sys.platform checks.
Prevention: test your notebooks in both a fresh Colab runtime and a local JupyterLab. If either fails, fix it before shipping.
8. Massive files accidentally in the repo
The mistake: you commit data/big_training_set.npz (2 GB), realise the mistake, and git rm it in a follow-up commit. But the file is now in history — every clone downloads 2 GB.
The consequence: cloning your repo takes forever. GitHub imposes soft limits and may warn you. PyPI won’t accept an sdist that big.
The fix: same as credentials — git-filter-repo to remove the file from history, then force-push. Same collaborators-re-clone footnote.
Prevention: Git LFS for genuine binary artefacts that must be versioned. For training data, Zenodo or Hugging Face Hub — not git.
9. paper.md word-count check that catches you by surprise
The mistake: you write a beautiful, thorough paper.md. You submit. The editor’s first pre-check note is: “Please shorten to under the 1000-word soft cap.”
The consequence: a round trip that could have been avoided by a wc -w check on your side.
The fix: the JOSS docs suggest 250-1000 words. This is a soft cap — some published papers are longer. But if you’re at 1500 and every paragraph feels essential, look at your Related-tooling section — usually the fat is there.
Prevention: count words minus code blocks minus front matter minus citation tokens:
python3 -c "
import re
text = open('paper.md').read()
text = re.sub(r'\`\`\`.*?\`\`\`', '', text, flags=re.DOTALL)
text = re.sub(r'^---.*?---', '', text, flags=re.DOTALL)
text = re.sub(r'\[@[^\]]+\]', '', text)
print(len(text.split()), 'words')
"Aim for 800-1000. Under 500 is too short.
Meta-pattern: check the observable state right after every step
The theme running through every pitfall above: check what you actually shipped, not what you think you shipped.
- After publishing to PyPI:
pip install yourthingon a different machine. - After publishing to GHCR:
docker pullon a different machine. - After a release: check the Zenodo record.
- After a
git-filter-repo: check git log on a fresh clone. - After writing
paper.md: build the PDF.
Every one of these is a 30-second check. Every one has saved someone a week of debugging.