How I publish open source on GitHub
This is the checklist I follow before and after a repository goes public. A checklist is easy to write, so each practice links to where it actually lives in docling-batch-extract, my first project built this way, and says in a few lines how to set it up.
Items marked Done are in place today. Items marked Next are the gaps I know about. I keep both on the page, because a list with no gaps usually isn't honest.
1. Confidential data stays out
The project started from real client documents, so this comes first. Memory isn't a control; the repository itself has to refuse the data.
- DoneDocuments are ignored by rule, not by habit
*.pdfis ignored at the root, with an exception only for the licensed demo file, and each data folder (inputs/,outputs/,completed/,errors/,logs/) has its own.gitignorethat keeps the folder and ignores its contents.Proof: .gitignore, inputs/.gitignore
How: add
*.pdfand!scripts/demo-files/*.pdfto the root.gitignore; in each data folder add a.gitignorecontaining*and!.gitignore. - DoneCI fails if a document or output is ever committed
A data guard runs on every push and pull request, before any other check.
Proof: the "No documents or outputs committed" step in ci.yml; the documentation build has its own "No PDFs or data on the site" step in pages.yml
How: a CI step that runs
git ls-files, filters for*.pdfand the data folders, and exits with an error if anything is listed. - DoneA search for client identifiers before every commit and release
File names, case numbers, local paths and phone numbers. The tests use synthetic PDFs and a demo paper published under CC BY 4.0, with its license recorded next to it.
Proof: tests/make_test_pdfs.py, scripts/demo-files/README.md
How: keep a short list of identifiers outside the repository and run
git grep -n -i -f <list>andgit log --all -p | grep -f <list>before each commit and tag; both must return nothing. - DonePrivate measurements stay private
Benchmarks from real client servers aren't published. The public numbers come from a machine I control, and the README says which hardware they came from.
Proof: the hardware notes in the README's memory profile
How: publish only numbers from hardware you can name and share, and label the hardware next to every table.
2. Repository basics
What someone sees in the first minute decides whether they trust the rest.
- DoneA README that answers what, why, how and how much
Background, how it works, setup, sizing from measured numbers, cost compared with cloud OCR, security, maintenance policy and troubleshooting. CI, license and release badges at the top.
Proof: README
How: start with one paragraph a stranger understands, then setup, then the numbers. Badges: Actions, pick the workflow, the three-dot menu, Create status badge.
- DoneA license, a description, a website link and topics
MIT. The About box has a one-sentence description, the documentation site as its website, and topics people actually search for.
Proof: LICENSE and the repository's About box
How: Add file, Create new file, name it LICENSE, Choose a license template. Then the gear icon next to About for description, website and topics, or
gh repo edit --description "..." --homepage <url> --add-topic <topic>. - DoneSemantic versioning with a written public contract
The changelog defines what counts as breaking (the JSON schema, folder layout, options, exit codes, log lines), and the version is printed by the tool and written into every output.
Proof: CHANGELOG.md
How: one
__version__in the code, a changelog in Keep a Changelog format, and a table in it listing what counts as a breaking change. - DoneThe design history is part of the repository
Versioned plans with requirement IDs and the evidence behind each decision, the story of how it was built, and the prompts to rebuild it. The prompts improve themselves: after every successful run, whatever had to be corrected is folded back in and logged.
Proof: versions/, STORY.md, prompts.md and its improvements log
How: keep
versions/plan-vN.mdfiles that start with "Changes from" the previous one; endprompts.mdwith an improvements log, and tell the coding agent inCLAUDE.mdto update it after each successful run. - DoneInstructions for coding agents
A
CLAUDE.mdwith the commands, architecture and the rules that must not be broken (never expose the server port, never commit documents), so an AI assistant works within the same limits I do.Proof: CLAUDE.md
How: run
/initin Claude Code, then put the rules that matter most near the top. - DoneClear about contributions
It's a side project I maintain part-time, so there's no contributing guide inviting pull requests. Issues and security reports are welcome.
How: leave out CONTRIBUTING.md until you want contributions, and say in SECURITY.md how to report a security issue.
3. Security settings
Most of these are switches in the repository settings. They cost nothing and are easy to forget.
- DonePrivate vulnerability reporting, with a security policy
Reports go through the Security tab, not public issues. The policy says what's in scope, which versions get fixes, and never to attach real documents.
Proof: SECURITY.md, the "Report a vulnerability" button on the Security tab
How: Settings, Advanced Security, Private vulnerability reporting: Enable (or
gh api -X PUT repos/<owner>/<repo>/private-vulnerability-reporting), then commit a short SECURITY.md. - DoneSecret scanning and push protection
GitHub scans for leaked credentials and blocks a push that contains one. The project also needs no credentials to run.
On both public repositories. Secret scanning on a private repository needs a paid plan.
How: Settings, Advanced Security: turn on Secret Protection and Push protection (free for public repositories).
- DoneDependabot alerts, security updates and nightly updates that merge themselves
Every night at 01:30 India time, Dependabot looks for new stable releases. Patch and minor updates arrive as one grouped pull request per ecosystem and merge automatically, but only after CI passes. Major updates stay open for me to review, because moving to a new major version is a deliberate decision. Policy pins are respected: docling's engine image is upgraded only through the gate, and MkDocs stays on 1.x.
On all three repositories: docling-batch-extract, saibal-roy.github.io and the private repository behind saibalroy.com.
Proof: .github/dependabot.yml, dependabot-auto-merge.yml
How: Settings, Advanced Security: Dependabot alerts and Dependabot security updates on; Settings, General: Allow auto-merge. Commit a
dependabot.ymlwithinterval: daily, atimeandtimezone, and a group forminorandpatch. Add a workflow that runsdependabot/fetch-metadataand thengh pr merge --auto --squashfor anything that isn'tsemver-major. Auto-merge waits for the required checks in the ruleset below. - DoneWorkflows get the least access they need
The default workflow token is read-only. Each workflow declares its own
permissions: CI reads only, Pages adds just what deployment needs, and only the release and auto-merge jobs can write.On all three repositories: docling-batch-extract, saibal-roy.github.io and the private repository behind saibalroy.com.
Proof: the
permissionsblocks in ci.yml, pages.yml and release.ymlHow: Settings, Actions, General, Workflow permissions: Read repository contents. Then a
permissions:block at the top of every workflow, widened only on the job that needs it. - DoneGitHub Actions pinned to commit hashes
Every action is pinned to a full commit SHA with its version in a comment, so a moved tag can't change what runs. The repositories refuse workflows with unpinned actions, and Dependabot keeps the pins current.
On all three repositories: docling-batch-extract, saibal-roy.github.io and the private repository behind saibalroy.com.
Proof: any
uses:line in ci.ymlHow: replace
actions/checkout@v7withactions/checkout@<sha> # v7.0.1, taking the SHA fromgh api repos/actions/checkout/commits/v7.0.1 --jq .sha. Then Settings, Actions, General: Require actions to be pinned to a full-length commit SHA. Listgithub-actionsindependabot.yml. - DoneThe main branch is protected
Two rulesets on the default branch. "Protect main" blocks deleting it and force pushes, for everyone. "Require CI" lets a pull request merge only once CI passes; as the owner I can still push directly, which a solo maintainer needs.
On all three repositories: docling-batch-extract, saibal-roy.github.io and the private repository behind saibalroy.com.
How: Settings, Rules, Rulesets, New branch ruleset: target the default branch and tick Restrict deletions and Block force pushes. A second ruleset ticks Require status checks to pass, lists the CI job names, and adds Repository admin to the bypass list.
- DoneSecure by default in the product, and tested
The conversion server has no authentication, so it's published on
127.0.0.1only. An acceptance check fails the build if that ever changes, and the docs say never to open the port in a firewall or security group.Proof: docker-compose.yml, check A25 in tests/run_acceptance.sh
How: publish ports as
127.0.0.1:5001:5001, never5001:5001, and add a test that readsdocker portand fails on anything but loopback. - DonePinned versions, upgraded on purpose
Python packages, the container image (an exact tag, never
latest) and the tools are pinned to their latest stable or LTS release. Engine upgrades go through the same gate as a release.Proof: requirements.txt, the maintenance policy in the README
How: pin with
==(Python) or an exact tag (Docker); let the nightly Dependabot run move patch and minor versions, and upgrade majors and the engine by hand after the gate passes.
4. CI and releases
Reliability you can prove: nothing is called done until a check says so.
- DoneCI on every push and pull request
Python lint, shell lint, a compose file check and the data guard, then a full setup on a fresh Ubuntu 26.04 LTS runner with a smoke test, the demo run and the acceptance checks.
Proof: CI runs
How: one workflow on
pushto main and onpull_request: a fast lint job, then a job that installs the tool the way a user would and runs the tests. - DoneA go-ahead gate before anything ships
The whole lifecycle (setup, smoke test, cleanup, demo, acceptance) rehearsed in a clean Ubuntu 26.04 container limited to the target's 2 CPUs. A run only passes if its log reaches the final marker, because a test that silently runs nothing also "passes".
Proof: tests/ubuntu_container_test.sh
How:
docker run --cpuset-cpus 0-1on the supported OS image, run every step inside it, write a final marker at the end, and pass only if the log contains it. - DoneReleases that can't disagree with themselves
Pushing a tag starts the release workflow. It checks that the tag, the version in the code and a dated changelog section all agree, runs the full CI again, then publishes the release with that changelog section as its notes.
Proof: release.yml, v0.1.0
How: date the changelog section, then
git tag -a v0.1.0 -m v0.1.0andgit push origin v0.1.0; the workflow does the rest. - DoneCommits that are mine
Every commit and tag is authored under my own name and email, and the history has no personal or client paths in it.
How: set
git config user.nameanduser.emailin each repository, and checkgit log -1 --format=%Bbefore pushing.
5. Documentation and discovery
If people can't find it or read it, the rest doesn't matter.
- DoneA documentation site on GitHub Pages
Built from the repository's own Markdown by GitHub Actions on every push to
main, served over HTTPS, built in strict mode and crawled for broken links, anchors and assets, first locally and then in the workflow.Proof: the site, scripts/preview_site.sh
How: Settings, Pages, Source: GitHub Actions. A workflow builds the site, runs a link checker, uploads it with
upload-pages-artifactand deploys it withdeploy-pages. - DoneA first run anyone can try
One script checks the environment, converts the demo file and prints a benchmark report for that machine.
Proof: scripts/demo_run.sh
How: ship one openly licensed sample input and a script that runs it end to end.
- DoneLinked from my profile and this page
Featured in my GitHub profile README and listed on this site. The personal repositories around it were cleaned up first, after taking verified backups.
How: a repository named after your username holds the profile README; back up with
git clone --mirrorbefore deleting anything. - NextSocial preview images and pinned repositories
Both preview images are ready at 1280 x 640: docling-batch-extract and this site. GitHub has no API for either setting, so they take a minute by hand. The private repository doesn't need one; previews only show for public repositories.
How: Settings, General, Social preview, Edit, Upload an image. For pins: your profile page, Customize your pins, tick docling-batch-extract and spatie/laravel-health, Save.
- DoneUnused features turned off
The wiki and Projects tabs are off everywhere, issues are off where nobody files them (this site and the private repository), and merged branches are deleted automatically.
On all three repositories: docling-batch-extract, saibal-roy.github.io and the private repository behind saibalroy.com.
How: Settings, General, Features: untick Wikis, Projects and (if unused) Issues; under Pull Requests tick Automatically delete head branches. Or
gh repo edit --enable-wiki=false --enable-projects=false --delete-branch-on-merge.
The checklist for my next project
The short version, in the order I do it.
- Ignore rules for every kind of private data, plus a CI step that fails if any of it is committed.
- Synthetic or openly licensed test files only; record the license next to them.
- README, LICENSE, CHANGELOG with a written public contract, SECURITY.md, CLAUDE.md, and a prompts.md that improves itself.
- Description, website link and topics in the About box; a 1280 x 640 social preview image.
- Private vulnerability reporting, secret scanning, push protection, Dependabot alerts and security updates.
- Nightly Dependabot updates: patch and minor merge themselves after CI, majors wait for me.
- Read-only default workflow token; a
permissionsblock in every workflow. - Actions pinned to commit hashes, enforced in the settings; dependencies pinned to the latest stable or LTS release.
- Rulesets: no deletion or force pushes on
main; pull requests need CI to pass. - CI with lint, the data guard and an end-to-end run on the one supported platform.
- A go-ahead gate that rehearses a fresh install at the target size before any release.
- A release workflow that checks tag, version and dated changelog agree, then runs CI again.
- Documentation built strictly and link-checked locally before Pages publishes it.
- A search for client identifiers and local paths before every commit and release.
- Wiki and Projects off, merged branches deleted; the repository featured on my profile and on this site.