Developing localghost¶
Repository layout¶
compose.yamlis the stable public interface and must remain self-contained.examples/compose.yamlsupplies primary, secondary, and unlabelled fixtures.scripts/integration-test.shexercises the compatibility contract.src/localghost/contains the packaged Click CLI, bundled hub Compose definition, override generator, and source-loaded Traefik provider.pyproject.tomlanduv.lockdefine the build and locked development environment..github/workflows/ci.ymlvalidates the project on Linux with Docker Compose 5.1.4, which is the fixed CI baseline rather than a required user-side patch version..github/dependabot.ymlproposes reviewed Python, Traefik, and GitHub Actions updates. The pinned Caddy host-bridge image lives ingenerator.py, so review it explicitly during dependency maintenance.
The hub Compose file must not gain application-specific mounts, state, or
configuration files. Consumers rely on the fixed localghost project and
localghost network names.
Static validation¶
Resolve and validate both Compose files:
LOCALGHOST_IMAGE_TAG="v$(uv run localghost --version)" \
docker compose -f compose.yaml config --quiet
COMPOSE_PROJECT_NAME=localghost-fixture-a \
docker compose -f examples/compose.yaml config --quiet
bash -n scripts/integration-test.sh
uv run ruff check .
uv run pytest
(cd src/localghost/traefik_plugin/src/github.com/SmileyChris/traefik-localghost-ca \
&& go test ./...)
The pytest suite covers generator logic in isolation and exercises the CLI against resolved Compose models. It validates generated Compose, Dockerfile, host-bridge, extension, backup, and refusal behavior.
Review the fully rendered fixture configuration when changing interpolated labels:
Integration suite¶
Run:
The suite is destructive only to Docker resources named localghost,
localghost-fixture-a, localghost-fixture-b, localghost-fixture-host, and
localghost-fixture-dockerfile. It refuses to begin if any of those resources
already exist and cleans up resources it creates even after failure. Ports 80,
18080, 18443, and 19090 must be available.
It also destroys the CA volumes. Teardown runs with --volumes, and the
volumes holding the bootstrapped root are named localghost like everything
else. On a machine with trusted HTTPS configured that leaves the hub unable to
start — it crash-loops on a missing bootstrapped root — until
localghost trust install runs again. The suite therefore refuses to begin in
that state. Either point
LOCALGHOST_STATE_DIR at a scratch directory, which leaves your own trust
configuration untouched, or set LOCALGHOST_ACCEPT_CA_RESET=1 to accept the
reset and re-bootstrap afterwards.
Coverage includes:
- validation and hub-first external-network failure;
- repeated idempotent hub startup and container health;
- exact loopback port publication and absence of raw API port publication;
- two concurrent fixture projects with isolated primary routes;
- secondary-service routing and rejection of an unlabelled container;
- explicit backend-port selection when another port is exposed;
- generated bridging to an HTTP application running directly on the host;
- trusted HTTPS routing with the bootstrapped public root, without changing the host trust store;
- dashboard root redirection and internal dashboard access;
- removal of one application without affecting another or the hub;
- hub restart and forced reconciliation without consumer recreation; and
- recreation on a non-default loopback port.
Override test names or ports only when necessary:
TEST_DEFAULT_PORT=18081 \
TEST_ALTERNATE_PORT=18082 \
TEST_HTTPS_PORT=18444 \
./scripts/integration-test.sh
LOCALGHOST_STATE_DIR and LOCALGHOST_ACCEPT_CA_RESET control the trust
guard described above.
CI should retain the default port-80 run because loopback publication on the public default is part of the release contract.
Documentation¶
Documentation is built with Zensical. Preview it locally with:
Build the static site with:
Build the CLI package¶
Build both the source distribution and wheel:
Test the local package through the same isolated tool mechanism used after PyPI publication:
uvx --from . localghost --help
uvx --from . localghost hub down --help
uvx --from . localghost save --help
uvx --from . localghost run --help
Release-candidate test¶
The checked-in source test does not prove that the packaged CLI contains the hub definition it starts. Before release, build a candidate wheel, then run the lifecycle commands from that wheel:
uv build --no-sources
wheel=$(find dist -maxdepth 1 -name '*.whl' -print -quit)
uvx --isolated --from "$wheel" localghost hub up
uvx --isolated --from "$wheel" localghost hub down
CI performs the same wheel smoke test before the source integration suite.
PyPI authentication¶
For releases from GitHub Actions, use PyPI Trusted Publishing (recommended).
After configuring the repository, workflow, and environment in PyPI, uv
publish --trusted-publishing always obtains a short-lived credential through
GitHub Actions OIDC; no long-lived PyPI token is stored in GitHub.
For a manual publish, provide a project-scoped PyPI API token to uv:
uv auth login https://upload.pypi.org/legacy/ is an alternative for storing
the token in uv's credential store. Do not commit tokens or credentials. PyPI
does not support account username-and-password uploads; use an API token.
Release checklist¶
- Review dependency changes and security implications, including the Traefik hub image and generated Caddy host-bridge image.
- Run static validation and the local integration suite.
- Pilot two independent checkouts with unique Compose project names (e.g. two checkouts of the same application).
- Update
CHANGELOG.mdandpyproject.tomlversion, and all example version tags. - Make a trial build with
uv build --no-sources, test the resulting wheel withuvx, and exercise the lifecycle commands from the release commit in CI. Push that exact commit tomain, then wait for its CI run to succeed. When documentation changed, also wait for the GitHub Pages deployment to succeed: - After those workflows pass, use a clean checkout of that exact commit to
build artifacts for the GitHub release, smoke-test the wheel, and record
both checksums. PyPI receives the workflow's own build rather than these,
so treat the checksums as a record of what was attached to the release:
test -z "$(git status --porcelain)" rm -rf dist uv build --no-sources wheel=$(find dist -maxdepth 1 -name '*.whl' -print -quit) sdist=$(find dist -maxdepth 1 -name '*.tar.gz' -print -quit) test -n "$wheel" && test -n "$sdist" uvx --isolated --from "$wheel" localghost hub up uvx --isolated --from "$wheel" localghost hub down sha256sum "$wheel" "$sdist" - Create the immutable SemVer tag and a draft GitHub release for that commit,
attaching the exact artifacts from step 6. Patch-release notes should be short and
front-facing: one summary sentence, 3–6 practical highlights, and an
uvx --refresh localghostupgrade command. Reserve a full introduction and CLI reference for major releases. - Pushing the tag publishes to PyPI: the
publishjob inci.ymlruns on anyv*tag once the integration job passes, builds on a clean checkout, and uploads through Trusted Publishing from thepypienvironment. Watch that run rather than uploading by hand, and note that it rebuilds, so the files on PyPI are not byte-identical to step 6's: Publish by hand only if that workflow is unavailable: - Wait for the published version to resolve from PyPI, then refresh and verify its lifecycle. Confirm a refreshed unpinned resolution selects the same version: PyPI propagation can take a short time; retry the exact-version check until it resolves rather than assuming an upload is immediately available.
- Publish the GitHub release draft only after the PyPI verification succeeds:
Consumer-visible changes to fixed names, labels, hostname conventions, or lifecycle commands require a major version. Additive compatible features may be minor releases; compatible fixes, reviewed image updates, and documentation updates may be patch releases.