Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ hyperi-ci check
it once with `uv tool install hyperi-ci` (or `pipx install hyperi-ci`), or run it
ad hoc with `uvx hyperi-ci check`. It runs the project's full local validation --
the same suite CI runs -- and reports what to fix. Run it before every push and before opening a
pull request; it is the single best way to give your change the best chance of
pull request. It is the single best way to give your change the best chance of
surviving CI.

## CI/CD Workflow
Expand Down Expand Up @@ -198,17 +198,72 @@ When adding a config field:

Site-specific defaults ship as YAML files under `profiles/` and load
opt-in via `CULVERT_PROFILE=/etc/vpn/profiles/<name>.yaml`. The
shipped `example.yaml` profile is a placeholder template; copy it and
shipped `example.yaml` profile is a placeholder template. Copy it and
edit the values for your own site profile. Env vars always override
profile values.

### Template variables

Config templates under `config/*.template` use envsubst-style
`${VAR}` placeholders. Only keys rendered by `entrypoint.py` belong
here; do not introduce new template placeholders without wiring them
here. Do not introduce new template placeholders without wiring them
through `scripts/lib/openvpn.py` or `scripts/lib/stunnel.py`.

## Questions

If you have questions about contributing, please open an issue or contact us.

## Context

### What this is

A VPN server shipped as one container image. Python inside, but not a Python
package -- the PyPI publish is opted out and a wheel is built only for pipeline
parity. Anyone deploys it, so defaults are generic and anything site-shaped
belongs in a profile or the deployment values. It replaced dfe-vpn, now
retired, so older references to that repo are stale.

### Where things live

| Path | What it holds |
|---|---|
| `scripts/entrypoint.py` | Thin orchestrator. Owns the startup ORDER |
| `scripts/lib/` | One module per concern -- config, network, pki, openvpn, wireguard, stunnel, wstunnel, oauth2, health, metrics, process, download |
| `config/*.template` | `${VAR}` templates rendered at runtime |
| `profiles/` | Opt-in site presets loaded by `CULVERT_PROFILE` |
| `deploy/` | Deployment contract and compose fragment, GENERATED by `scripts/generate-deploy-artefacts.py`. The Helm chart is assembled from the contract at release and committed nowhere |
| `docs/architecture.md` | Startup order, the two iptables chains, the PKI invariants |

### Commands that prove a change

`hyperi-ci check` locally, `tests/run-all.sh` for the tiers.

Green in CI is the UNIT tier alone, with coverage of `scripts/` held to hyperi-ci's default 80% floor. `.hyperi-ci.yaml` disables integration, e2e and k8s and leaves `ty` non-blocking. Run the rest yourself:

```bash
pytest tests/e2e/ -m e2e -v # needs Docker and TUN
pytest tests/k8s/ -m k8s -v # needs a kubeconfig and CULVERT_K8S_*
```

`addopts` carries no `-m "not e2e"`, so a bare `pytest` tries the e2e tier and
fails without Docker rather than skipping.

### What tends to bite

| Don't | Do | Why |
|---|---|---|
| Write the skip-CI marker literally in a commit message | Describe it | GitHub scans the whole message, body included, so a literal marker anywhere in it gives the PR no CI run at all |
| Hand-edit `deploy/deployment-contract.json` or `deploy/compose/culvert.yaml` | Re-run `scripts/generate-deploy-artefacts.py` | Both render from the scalo deployment contract in `scripts/lib/deployment.py`, and a unit test compares the committed output against a fresh render |
| Call `easyrsa init-pki` | Create the directories directly, as `lib/pki.py` does | On 3.2.x it removes the PKI directory unconditionally. That is a volume mount, so it fails outright, and where it succeeds it takes the CA ([issue #11](https://github.com/hyperi-io/culvert/issues/11)) |
| Check a renamed doc's links on macOS | Let `tests/unit/test_docs_links.py` do it | It compares against `git ls-files`, so case matters. A new doc also has to be linked from `docs/README.md` |
| Read a green PR as proof the image runs | Confirm the Container job built | Only the Container job starts the image, so a base-image bump or a broken config can pass every other check and die at startup |

### Where this sits

Inbound: **hyperi-io/scalo-py**, the only suite edge. `pyproject.toml` declares
`scalo[metrics,secrets-vault,secrets-aws]` bounded below 3 at runtime, plus a
dev-only `scalo[deployment]` range the artefact generators need and the runtime
never imports. Granular extras on purpose.

Outbound: nothing in the suite graph consumes culvert, and it is an optional
member. It reaches consumers as a public container image on GHCR.
Loading
Loading