Files
handbook/docs/standards.md
T
dsql 7ed388c69c new compose + deploy standard: generic compose, deploy injects identity
Supersedes the prior 'name everything after the repo' guidance, which caused
container-name collisions at fleet scale. The dev's compose now names nothing
repo-specific; the deploy layer injects identity and host paths.

deploy.md:
- replace the 'One name everywhere' danger callout with 'Your compose names
  nothing repo-specific' (no container_name, no hardcoded names/paths)
- rewrite the compose example to the standard: svc service key,
  restart: unless-stopped (required — host unit is oneshot), host paths via
  ${LOGS_DIR}/${CONFIG_DIR}/${MOUNTS_DIR} with :-./ local fallbacks, bare
  'cache' volume. Keep user: "1337:1337" + HOME=/tmp (the services account
  identity — not repo-specific)
- add 'How deploy fills it in' (injected env vars a dev can rely on + the
  resulting docker ps names) and 'Onboarding a service' (deploy <host>
  <workspace> <git-url> [name], deploy key handled via Gitea API)
- update paths to <name>, subprocess example to svc, checklist to the new
  rules; drop docker CLI from secret rotation (host-side edit, we restart)

standards.md:
- add a 'Service compose' section: short convention + Right/Wrong tabs
  (the nova before/after), linking to the Deploy guide for the full detail

Kept the services-account (1337) section and uid-agnostic Dockerfile notes.
Verified in-browser; mkdocs build --strict clean (cross-ref anchor resolves).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-07-01 01:04:44 -04:00

172 lines
5.3 KiB
Markdown

# Coding Standards
The house standards every Rethink Studios project follows. They keep our code
consistent, readable, and predictable across the whole suite.
## Files and style
- Files end with a **single trailing newline** (LF / Unix line endings), and
carry **no trailing whitespace** on any line.
- **4 spaces** for indentation by default. Respect language norms — TS/JS use
**2 spaces**, Go uses **tabs** — and when editing an existing file, follow that
file's existing indentation.
- **flake8 clean**, max line length **120**. **Type hints** on public functions.
A well-formed module — module docstring, type hints, lowercase-start docstring:
```python
"""async key/value store backed by a single json file"""
from pathlib import Path
def load_state(path: Path) -> dict[str, str]:
"""read the json state file, returning an empty dict if it's missing"""
if not path.exists():
return {}
return _read_json(path)
```
flake8 tells you the moment you drift — keep the tree clean:
```text
$ flake8
./aiokv/store.py:14:80: E501 line too long (96 > 79 characters)
./aiokv/store.py:22:1: F401 'json' imported but unused
./aiokv/store.py:31:5: E303 too many blank lines (3)
```
!!! tip "Run it locally"
Wire the shared config into an alias so every project lints the same way:
`alias flake8='flake8 --config ~/.config/flake8'` (max line 120). See the
[Workflow](workflow.md#handy-shell-setup) page.
## Documentation
- Public functions get a **docstring****lowercase-start, no trailing period**.
- Keep inline comments minimal; prefer docstrings. Use an inline comment only
where the code is genuinely complex.
- Each module/file states its scope and purpose via a **module docstring**
(header string) — **not** a license or copyright header.
- Every library and project has a **README** with the install line, what it does,
and a usage example.
!!! note "Licensing — no per-file headers"
Don't prepend license/copyright boilerplate to source files. If a repo needs a
license, it's a single top-level **`LICENSE`** file — never repeated per file.
Most internal repos don't carry one; add it only when a repo is meant for
outside use and the terms are decided.
=== "Do"
```python
def mask_secret(value: str, keep: int = 4) -> str:
"""mask all but the last ``keep`` characters of a secret"""
if len(value) <= keep:
return "*" * len(value)
return "*" * (len(value) - keep) + value[-keep:]
```
=== "Don't"
```python
# Masks a secret. <- license-header-style noise, capitalized, trailing period
def mask_secret(value, keep=4): # no type hints
# loop over the chars and hide them
return "*" * (len(value) - keep) + value[-keep:] # crashes if short
```
## Quality and error handling
**Fail loud** — never swallow exceptions. Catch the **specific** exception and
**log** it; don't paper over failures with a bare `except`.
=== "Do"
```python
import logging
log = logging.getLogger(__name__)
def fetch(url: str) -> bytes:
"""fetch ``url``, logging and re-raising on failure"""
try:
return _client.get(url).content
except TimeoutError:
log.warning("fetch timed out: %s", url)
raise
```
=== "Don't"
```python
def fetch(url):
try:
return _client.get(url).content
except Exception:
pass # swallowed — the caller has no idea anything broke
```
When something does break, a loud failure gives you a real traceback to act on:
```python-traceback
Traceback (most recent call last):
File "run.py", line 42, in <module>
data = fetch("https://example.test/feed")
File "aioweb/session.py", line 88, in fetch
return self._client.get(url).content
TimeoutError: request timed out after 30s
```
…and the log line that precedes it tells you where to look:
```text
2026-06-29 14:03:11,204 WARNING aioweb.session fetch timed out: https://example.test/feed
```
!!! note "Logging belongs to the app, not the library"
Libraries **emit only** — `log = logging.getLogger(__name__)` and nothing
else. Handlers, levels, and formatting are configured once at the
application entry point, so a lib never dictates how its host logs.
## Service compose
A deployable service ships a `compose.yaml` that names **nothing repo-specific** —
the deploy layer injects identity and host paths. See the
[Deploy guide](deploy.md#your-composeyaml) for the full convention and the variables
you can rely on. The short version:
=== "Right"
```yaml
services:
svc:
user: "1337:1337"
restart: unless-stopped
volumes:
- ${LOGS_DIR:-./logs}:/app/logs
- profile:/app/profile
volumes:
profile:
```
=== "Wrong (causes collisions)"
```yaml
services:
nova:
container_name: nova # repo-specific name -> collides
volumes:
- /srv/logs/ricky/nova:/app/logs # hardcoded host path
- nova_profile:/app/profile # repo-prefixed volume
volumes:
nova_profile:
```
Generic service key `svc`, no `container_name`, host paths from `${...}` variables,
and **bare** volume names — that's what makes a service collision-proof on the
fleet.