Contributing
Contributing to htmforge
Thank you for your interest in contributing!
Before you open a PR
- PRs target
develop, notmain.mainonly receives merges fromdevelopvia release PRs (see "Release Process" below) — opening a PR directly againstmainwill need to be redirected before it can be reviewed. - Open an issue first for anything beyond a trivial fix. New components or API changes need prior discussion.
- Check that a similar issue or PR doesn't already exist.
Setup
git clone https://github.com/mondi04/htmforge.git
cd htmforge
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
Run tests and type checks:
pytest # run all tests
mypy htmforge/ # strict type check
ruff check htmforge/ # lint
ruff format htmforge/ # format
Workflow
- Fork the repository and create a branch:
git checkout -b feat/my-feature - Write your code and add tests — all new public functions need a docstring
- Make sure
pytest,mypy htmforge/, andruff check htmforge/all pass - Open a pull request against
developwith a clear description
Coding Standards
- Formatting:
ruff formatwith the project config (88 chars, double quotes) - Linting:
ruff checkmust pass with zero warnings - Types:
mypy --strictmust pass; annotate every function signature - Docstrings: All public classes and functions require a Google-style docstring
- Tests: Every new feature needs at least one positive and one edge-case test
Commit Convention
This project uses Conventional Commits:
| Prefix | When to use |
|---|---|
feat: |
New feature or component |
fix: |
Bug fix |
docs: |
Documentation only |
test: |
Adding or fixing tests |
chore: |
Build, CI, dependency updates |
refactor: |
Code change without behavior change |
Examples:
feat: add Breadcrumb component
fix: escape attribute values in _render_attrs
docs: add FormField usage example to README
Release Process (Maintainers)
This section is only relevant for repository maintainers merging PRs into
main. Contributors can ignore it.
Releases are triggered automatically by a tag push, which is itself created
automatically when a PR is merged into main — if the merge commit title
follows a specific convention.
How it works
- A PR targeting
developis reviewed and merged as usual — nothing special here. - When
developis ready for a release, open a PR fromdevelopintomain. - When merging that PR via the GitHub UI, GitHub pre-fills the merge commit
title with
Merge pull request #X from mondi04/develop. Overwrite this title with:release: vX.Y.Z(e.g.release: v0.4.3), matching the next version per Semantic Versioning. This must be typed manually — there is no default that produces a release. - On merge, the Auto Tag workflow (
.github/workflows/auto-tag.yml) runs onmainand: - Reads the merge commit title.
- If it matches
release: vX.Y.Zat the start of the message, creates and pushes tagvX.Y.Z, then fast-forwardsdevelopontomain. - If it does not match, the workflow fails (red ❌ in the Actions tab) — no tag is created. This is intentional: it makes a forgotten title change immediately visible instead of silently doing nothing.
- The tag push triggers the existing Release workflow
(
.github/workflows/release.yml), which runs tests, builds the package, publishes to PyPI, creates the GitHub Release (using the matching section fromdocs/changelog.md), and deploys docs.
Normal merges into main without a release
Not every merge into main needs to ship a release. If you merge without
changing the commit title to release: vX.Y.Z, the Auto Tag workflow will
fail with a red ❌. This failure is expected and harmless for non-release
merges — it does not block or revert the merge itself, it only means no tag
was created. It exists purely as a visible reminder; it's safe to ignore for
merges that were never meant to be a release.
Before merging a release PR
- [ ]
docs/changelog.mdhas a## [X.Y.Z] - YYYY-MM-DDsection for the new version (the Release workflow extracts its body for the GitHub Release notes — falls back to a generic message if missing). - [ ] You're merging
develop→main(not some other branch).
Note: the package version itself does not need to be set anywhere by
hand — pyproject.toml uses hatch-vcs (dynamic = ["version"],
tool.hatch.version.source = "vcs"), so the version is derived directly from
the git tag at build time.
Things that can go wrong
| Symptom | Cause | Fix |
|---|---|---|
| Auto Tag fails with "kein Tag-Pattern gefunden" | Forgot to overwrite the merge commit title | If this should've been a release: revert/re-merge with the correct title. Otherwise: ignore, expected for non-release merges. |
| Auto Tag fails with "Tag existiert bereits" | Version number wasn't bumped, or this version was already released | Bump the version and re-merge with a new tag. |
develop not synced after a release |
develop had commits not yet in main (fast-forward not possible), or push to develop was rejected by branch protection |
Sync manually: git checkout develop && git merge --ff-only main && git push. |
Pre-release tags (v1.2.3-beta) |
Not currently supported — the version regex only matches plain X.Y.Z, the suffix would be silently dropped from the tag |
Avoid pre-release tags via this workflow for now; tag manually if ever needed. |
Good First Issues
If you're new to the project, these are great places to start:
1. Admin Panel Example: Screenshot
Add at least one screenshot to examples/admin-panel/README.md. Run the admin
panel locally, take a screenshot of the users page, save it as
examples/admin-panel/static/screenshot.png, and reference it in the README.
2. DataTable: render Component cells in legacy rows mode
Currently rows: list[list[str]] only supports strings. Add support for
list[list[str | Element]] so Element values in legacy rows are rendered
correctly, not just stringified.
3. Page: add lang attribute support
Add an optional lang: str = "en" field to Page that sets
<html lang="en">. Update the render() method to pass it to the html()
element. Add a test in tests/test_components.py.