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
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@ contributions open when the repo does.
- Every inbound surface (webhook, API endpoint) ships with its threat model noted in the PR description.
- Specs before code, per the project's constitution.

## Documentation describes the released artefact, not this checkout

**A capability is documented from the release that carries it, and not before.** The published
documents are checked against [`docs/published-surface.json`](docs/published-surface.json) — every
`hullwork …` command and `--flag` they show has to be one the pinned image accepts.

The rule exists because the same defect happened three times, and every time a person running the
artefact found it rather than a check:

* `0.1.0a1` shipped without its optional extras, so `HULLWORK_ERROR_DSN` made the container exit 3
and a Postgres URL died in a traceback — both documented in three places each;
* `0.1.0a2` predated the module that reports upstream, so it could not do what its own docs said;
* `projects add --credential-file` was documented while every document pinned `0.1.0a5`, which
answers `unrecognized arguments`.

Each sentence was true of `main` and false of the thing a reader pulls, and a check against the local
parser would have passed all three, because in all three the local parser was right.

**So what you do depends on which you are changing:**

| you are | then |
|---|---|
| adding a flag or command | ship it. Do not document it yet — the gate fails, and it is right |
| documenting one already released | nothing extra; the surface already has it |
| cutting a release | after it publishes, `./scripts/record-the-published-surface.py`, move the pins, and the documentation you were holding can land in that commit |
| needing the artefact to *measure* something | that is what `edge` is for, and it is not a release ([docs/releasing.md](docs/releasing.md)) |

`docs/published-surface.json` is **generated by asking the image**, never by reading this checkout —
a locally generated one would agree with the docs in exactly the cases that matter. Hand-editing it
is the one way to make this whole check lie, which is why the file says so in its first field.

## Signing off your work

We use the [Developer Certificate of Origin](https://developercertificate.org/) 1.1 — the full text is
Expand Down
33 changes: 33 additions & 0 deletions docs/deployment-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,39 @@ protect you, and the machine has a public IP — `--host 0.0.0.0` would publish
token and all, to the internet. The healthcheck has to be overridden to match, since the one baked
into the image assumes port 8000.

> [!warning] **And host networking moves the receiver behind the host's firewall, which is where this
> failed silently for ten days.** With the receiver on the host and the tracker in a container, every
> delivery is now *container → host*, and a host firewall usually has an opinion about that. Measured
> on this deployment on **2026-08-07**: deliveries had stopped on 27-jul and nothing said so until
> `doctor` grew a check for it, because the inventory sweep kept filing items and the receiver runs
> with `--no-access-log` — **a successful webhook leaves no trace, so an empty log proves nothing
> either way.**
>
> The cause, and the part worth remembering: `/etc/ufw/before.rules` held
>
> ```
> -A ufw-before-input -i br+ -m conntrack --ctstate NEW -j DROP
> ```
>
> a reasonable piece of hardening — containers may not reach host services — and the two
> `ufw allow` rules added later to permit the webhook had **0 packets on their counters**. They could
> never match: `before.rules` runs *before* `ufw-user-input`, so **no ufw user rule can override a
> rule in `before.rules`**. The exception has to go in the same file, above the `DROP`:
>
> ```
> -A ufw-before-input -i br+ -d 10.0.0.5 -p tcp --dport 8000 -m conntrack --ctstate NEW -j ACCEPT
> ```
>
> Two things to check rather than assume. **Counters, not rule listings** — `iptables -L … -v` on the
> rule you believe is working answers in one line what reading the config cannot. And **probe from
> inside the tracker's container**, not from the host: the host reaching its own address proves
> nothing about the path a delivery takes.
>
> Recovering what was dropped does not need a fabricated error. A tracker records the notifications it
> created, so the ones that failed can be re-sent — on GlitchTip,
> `send_notification.func(<id>)` from `manage.py shell`. Two real dropped reports were replayed here;
> one deduped onto an item the sweep had already filed, which is the dedup working across both paths.

## Production compose, in full

```yaml
Expand Down
126 changes: 126 additions & 0 deletions docs/published-surface.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"_": "Generated by scripts/record-the-published-surface.py from the image itself. Do not hand-edit: a documented capability that is not in here is one nobody can run, and editing this file would make that true silently.",
"image": "ghcr.io/easybytehub/hullwork:0.1.0a6",
"version": "0.1.0a6",
"recorded": "2026-08-07",
"commands": {
"hullwork": [
"--help",
"--version"
],
"hullwork status": [
"--help",
"--json"
],
"hullwork doctor": [
"--compose-file",
"--env-file",
"--help",
"--json"
],
"hullwork projects": [
"--help"
],
"hullwork projects add": [
"--credential-file",
"--forge",
"--help",
"--manifest",
"--repo",
"--slug"
],
"hullwork projects list": [
"--help"
],
"hullwork projects disable": [
"--help"
],
"hullwork projects rotate-secret": [
"--credential-file",
"--help"
],
"hullwork projects refresh": [
"--help",
"--manifest"
],
"hullwork projects lanes": [
"--checkout",
"--help"
],
"hullwork projects set-tracker": [
"--help"
],
"hullwork propose": [
"--checkout",
"--forge",
"--help"
],
"hullwork approve": [
"--help"
],
"hullwork requeue": [
"--help"
],
"hullwork lease": [
"--help"
],
"hullwork lease release": [
"--help"
],
"hullwork config": [
"--help",
"--telemetry"
],
"hullwork try": [
"--approve",
"--error",
"--help",
"--into"
],
"hullwork work": [
"--help",
"--limit",
"--loop",
"--no-publish",
"--project",
"--release-stale"
],
"hullwork sweep": [
"--confirm",
"--from-now",
"--help",
"--limit"
],
"hullwork republish": [
"--attempt",
"--give-",
"--give-up",
"--help",
"--why"
],
"hullwork gateway": [
"--allow-model",
"--allow-network",
"--auth-style",
"--credential-file",
"--help",
"--journal",
"--max-tokens",
"--model",
"--port",
"--upstream"
],
"hullwork init": [
"--help",
"--into"
],
"hullwork page-token": [
"--help",
"--rotate"
],
"hullwork prune": [
"--help",
"--older-than-days"
]
}
}
14 changes: 14 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ The workflow refuses a tag whose version disagrees with `hullwork.__version__`,
anything — a release whose image says one version and whose wheel says another is unusable for
anybody pinning either, and the bug report arrives months later from somebody who cannot reproduce it.

**Then, once the image is public**, and in this order, because each step needs the one before it:

```bash
./scripts/record-the-published-surface.py # asks the new image what it accepts
# move the pins: README.md, docker-compose.yml, docs/install.md, PRIVACY.md
```

That is what lets the documentation describe the new release — including anything held back for it,
per [CONTRIBUTING](../CONTRIBUTING.md#documentation-describes-the-released-artefact-not-this-checkout).
Skipping either half is not quiet: re-record without moving the pins and the pins disagree with the
recording; move the pins without re-recording and they disagree the other way. Both fail
`tests/test_the_documentation_describes_the_published_artefact.py`, which is the whole point of
storing the recording rather than trusting the sequence.

## The private forge

Development happens on a Forgejo instance, and this repository is a **derivation** of it: the publish
Expand Down
Loading
Loading