Compare commits
10 Commits
d3c85acd1e
...
78e3c34d8d
| Author | SHA1 | Date | |
|---|---|---|---|
| 78e3c34d8d | |||
| 2e631136c6 | |||
| 613d8f28ea | |||
| e684ac2853 | |||
| 4fd19bf620 | |||
| e5ca1c0ada | |||
| 8fc81fc4f4 | |||
| 142a0dbff6 | |||
| da47923088 | |||
| 5e787edbb0 |
36
README.md
Normal file
36
README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# handbook
|
||||||
|
|
||||||
|
The Rethink Studios handbook — our public reference for the shared library suite,
|
||||||
|
coding standards, the dev workflow, Python environments, and how to deploy on our
|
||||||
|
network.
|
||||||
|
|
||||||
|
Live at **[docs.rethinkstudios.io](https://docs.rethinkstudios.io)**.
|
||||||
|
|
||||||
|
## What this is
|
||||||
|
|
||||||
|
A static documentation site built with [MkDocs](https://www.mkdocs.org/) +
|
||||||
|
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/). Markdown lives
|
||||||
|
under `docs/`, builds to a static `site/`, and is served by the reverse proxy at
|
||||||
|
the subdomain — no app process, just static HTML/CSS/JS.
|
||||||
|
|
||||||
|
The **Libraries** page is the one dynamic part: it fetches the `rethink-public`
|
||||||
|
repo list from the Gitea API client-side at view time, so new libraries appear on
|
||||||
|
the next page load with no rebuild. Everything else is static markdown.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
mkdocs build # -> static site/
|
||||||
|
mkdocs serve # local preview at http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
- One topic per page, grouped under `docs/`, wired into the nav in `mkdocs.yml`.
|
||||||
|
- **Public — sanitize:** no real hostnames, internal IPs, secrets, or exact
|
||||||
|
topology. Use placeholders (`<dev>`, `<project>`, `/srv/...`).
|
||||||
|
- Markdown: trailing newline, no trailing whitespace, LF line endings.
|
||||||
|
- Commits signed (`git commit -s`).
|
||||||
10
docs/assets/logo.svg
Normal file
10
docs/assets/logo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.0 KiB |
143
docs/deploy.md
143
docs/deploy.md
@ -4,76 +4,120 @@
|
|||||||
> **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever
|
> **rethink-net** — our fleet of Ubuntu 26.x servers, ready to host whatever
|
||||||
> you've built.
|
> you've built.
|
||||||
|
|
||||||
**Eligible:** APIs, websites, applets, bots, monitors.
|
!!! tip "Eligible"
|
||||||
|
APIs, websites, applets, bots, monitors. The whole network runs on a few
|
||||||
The whole network runs on a few simple, consistent rules. Get your container to
|
simple, consistent rules — get your container to follow them and deploying is
|
||||||
follow them and deploying is mostly handing us a `compose.yaml`.
|
mostly handing us a `compose.yaml`.
|
||||||
|
|
||||||
## Docker — the services account
|
## Docker — the services account
|
||||||
|
|
||||||
Every service runs containerized as the shared **`services`** account:
|
Every service runs containerized as the shared **`services`** account:
|
||||||
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
|
**uid/gid 1337**, fixed fleet-wide. Build your image to be **uid-agnostic** so it
|
||||||
runs cleanly as that account:
|
runs cleanly as that account.
|
||||||
|
|
||||||
- `user: "1337:1337"` in compose.
|
|
||||||
- `chmod -R a+rwX /app` in the Dockerfile (covers non-mounted dirs — see the
|
|
||||||
bind-mount note below).
|
|
||||||
- `HOME=/tmp`.
|
|
||||||
- **No** in-container `user`/`useradd` — don't bake a user into the image.
|
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
ENV HOME=/tmp
|
ENV HOME=/tmp # (1)!
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
# git in the build if you pip-install from git
|
RUN apt-get update \
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends git \
|
&& apt-get install -y --no-install-recommends git \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/* # (2)!
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt # (3)!
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pip install --no-cache-dir . \
|
RUN chmod -R a+rwX /app # (4)!
|
||||||
&& chmod -R a+rwX /app
|
|
||||||
CMD ["python", "-m", "yourapp"]
|
CMD ["python", "-m", "yourapp"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. `HOME=/tmp` — the `services` account has no home dir; anything writing to
|
||||||
|
`$HOME` (caches, configs) needs a writable target.
|
||||||
|
2. Include `git` **only if the container itself needs it** — e.g. you
|
||||||
|
`pip install` from git, or the app shells out to git at runtime. The build and
|
||||||
|
host always have git; this line is about what's *inside* the image.
|
||||||
|
3. Install deps **before** copying the code (see [layer
|
||||||
|
caching](#layer-caching)).
|
||||||
|
4. `chmod -R a+rwX /app` makes the app tree writable by **any** uid — that's what
|
||||||
|
"uid-agnostic" means.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
yourapp:
|
yourapp:
|
||||||
build: .
|
build: .
|
||||||
user: "1337:1337"
|
user: "1337:1337" # (1)!
|
||||||
environment:
|
environment:
|
||||||
HOME: /tmp
|
HOME: /tmp
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/configs/<project>:/app/config:ro # host-managed, read-only
|
- /srv/configs/<project>:/app/config:ro # (2)!
|
||||||
- /srv/<dev>/<project>:/app/logs # live + rolled logs
|
- /srv/logs/<dev>/<project>:/app/logs # (3)!
|
||||||
- yourapp-data:/app/data # named volume — the rest
|
- yourapp-data:/app/data # (4)!
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
yourapp-data:
|
yourapp-data:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. Run as the shared account. **No** in-container `user`/`useradd` — don't bake a
|
||||||
|
user into the image; set it here.
|
||||||
|
2. Configs: host-managed bind mount, mounted **read-only**.
|
||||||
|
3. Logs: bind mount — live and rolled, scraped for monitoring.
|
||||||
|
4. Everything else: a **named volume**. Docker owns it, so there are no host
|
||||||
|
permissions to fiddle with.
|
||||||
|
|
||||||
## Paths and mounts
|
## Paths and mounts
|
||||||
|
|
||||||
- **Configs** → `/srv/configs/<project>/` — bind mount, host-managed.
|
| What | Where | How |
|
||||||
- **Logs** → `/srv/<dev>/<project>/` — bind mount; live and rolled, scraped for
|
| --- | --- | --- |
|
||||||
monitoring.
|
| Configs | `/srv/configs/<project>/` | bind mount, host-managed, read-only |
|
||||||
- **Everything else** (caches, browser profiles, scratch) → **named volumes**.
|
| Logs | `/srv/logs/<dev>/<project>/` | bind mount; live + rolled, scraped |
|
||||||
Docker manages ownership, so there are no host permissions to fiddle with.
|
| Caches, profiles, scratch | named volume | Docker manages ownership |
|
||||||
|
|
||||||
## Permissions — the bind-mount footgun
|
!!! note "If your service won't start or its logs aren't persisting"
|
||||||
|
That's usually a host-side bind-mount **ownership** thing — the kind of detail
|
||||||
|
**we sort out at deploy time**, not something you need to chown or provision.
|
||||||
|
If a bot won't come up or logs/caches keep vanishing, flag it and we'll fix
|
||||||
|
the mount perms. Stick to a clean `compose.yaml` and let us handle the host.
|
||||||
|
|
||||||
`docker compose up` does **not** create bind-mount directories as you. If a
|
## Layer caching
|
||||||
bind-mount source is missing, the Docker daemon (**root**) creates it **as
|
|
||||||
root** — and your container (**1337**) then can't write it. Logs fall back to
|
|
||||||
console-only, caches re-download every run.
|
|
||||||
|
|
||||||
So:
|
Copy `requirements.txt` and `pip install` **before** `COPY . .`. Docker caches
|
||||||
|
layers in order, so deps only reinstall when `requirements.txt` changes — not on
|
||||||
|
every code edit. Get this backwards and every one-line change triggers a full
|
||||||
|
dependency reinstall.
|
||||||
|
|
||||||
- **Bind-mount sources (configs, logs) must EXIST and be 1337-owned _before_
|
```dockerfile
|
||||||
`up`.** This is handled at provisioning, not a per-deploy chown hook.
|
COPY requirements.txt .
|
||||||
- **Named volumes avoid this entirely** — use them for anything that doesn't need
|
RUN pip install --no-cache-dir -r requirements.txt # cached until deps change
|
||||||
host visibility.
|
COPY . . # changes every build
|
||||||
- The Dockerfile `chmod -R a+rwX /app` only covers **non-mounted** dirs. A bind
|
```
|
||||||
mount overrides the image directory with the host directory, so for mounted
|
|
||||||
paths the **host-side ownership wins**.
|
## Subprocess and browser workloads
|
||||||
|
|
||||||
|
Bots that spawn Chrome, Xvfb, ffmpeg, or other child processes need three extra
|
||||||
|
knobs in compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
yourbot:
|
||||||
|
build: .
|
||||||
|
user: "1337:1337"
|
||||||
|
init: true # (1)!
|
||||||
|
shm_size: "2gb" # (2)!
|
||||||
|
mem_limit: "4g" # (3)!
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Runs **tini** as PID 1 to reap zombie subprocesses and forward signals.
|
||||||
|
Without it, spawned Chrome/Xvfb processes leak as zombies.
|
||||||
|
2. Chrome and most headless browsers crash on Docker's default **64 MB**
|
||||||
|
`/dev/shm`. Bump it for any browser workload.
|
||||||
|
3. Bound memory — especially when each worker spawns a browser. Raise it as
|
||||||
|
worker count grows.
|
||||||
|
|
||||||
|
!!! warning "The PID-1 gotcha with shell-wrapper CMDs"
|
||||||
|
If your `CMD` is a shell-script wrapper (e.g. `xvfb-run ...`), it must **not**
|
||||||
|
be PID 1, or the real process dies on startup. `init: true` is exactly what
|
||||||
|
fixes this — tini takes PID 1, and your wrapper runs as a normal child.
|
||||||
|
|
||||||
## What your compose / Dockerfile needs
|
## What your compose / Dockerfile needs
|
||||||
|
|
||||||
@ -83,16 +127,21 @@ So:
|
|||||||
- secrets bind-mounted **`:ro`**
|
- secrets bind-mounted **`:ro`**
|
||||||
- `HOME=/tmp`
|
- `HOME=/tmp`
|
||||||
- `chmod -R a+rwX /app`
|
- `chmod -R a+rwX /app`
|
||||||
- `git` in the build if you `pip install` from git
|
- deps installed **before** the code copy (layer caching)
|
||||||
|
- `git` in the image **if the container needs it**
|
||||||
|
- for browser/subprocess workloads: `init: true`, `shm_size`, `mem_limit`
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
|
|
||||||
We do **not** commit secrets (usually, lol). The rule:
|
!!! warning "Secrets never go in the image"
|
||||||
|
We do **not** commit secrets (usually, lol). They stay **gitignored**, live on
|
||||||
- Secrets stay **gitignored**.
|
the host at `/srv/configs/<project>/`, and are bind-mounted **read-only** at
|
||||||
- They're placed on the host at `/srv/configs/<project>/`.
|
runtime. Add them to `.dockerignore` so a `COPY . .` can't sweep them into a
|
||||||
- They're bind-mounted **read-only** at runtime.
|
layer.
|
||||||
- **Never** baked into the image — add them to `.dockerignore` so `COPY . .`
|
|
||||||
can't grab them.
|
|
||||||
|
|
||||||
Rotating a secret = edit the host file and restart. No rebuild.
|
Rotating a secret = edit the host file and restart. No rebuild.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vim /srv/configs/<project>/secrets.env # edit on the host
|
||||||
|
docker compose restart yourapp # pick up the change — no rebuild
|
||||||
|
```
|
||||||
|
|||||||
145
docs/environments.md
Normal file
145
docs/environments.md
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
# Virtual environments
|
||||||
|
|
||||||
|
How we keep a project's Python isolated — from the system Python and from every
|
||||||
|
other project. The rule underneath all of it: **never install into, or upgrade,
|
||||||
|
the system Python.** The OS depends on it; a project must never touch it.
|
||||||
|
|
||||||
|
!!! danger "Leave the system Python alone"
|
||||||
|
No `sudo pip install`, no upgrading the system interpreter for a project. If a
|
||||||
|
project needs a different version or a package, that goes in a **virtual
|
||||||
|
environment** — never the system one. Breaking system Python can break the OS.
|
||||||
|
|
||||||
|
## Project-based isolation
|
||||||
|
|
||||||
|
Every project runs against its own isolated environment. There are three ways that
|
||||||
|
happens, depending on where the project runs:
|
||||||
|
|
||||||
|
=== "Local `.venv`"
|
||||||
|
|
||||||
|
A virtualenv living in the repo. Simplest for day-to-day local work.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv # create it (once)
|
||||||
|
source .venv/bin/activate # activate for this shell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep `.venv/` **gitignored** — it's per-machine, never committed.
|
||||||
|
|
||||||
|
=== "Makefile-driven"
|
||||||
|
|
||||||
|
Wrap the venv in a `make` target so every dev (and CI) sets up identically —
|
||||||
|
no "did you activate it?" drift.
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
VENV := .venv
|
||||||
|
PY := $(VENV)/bin/python
|
||||||
|
|
||||||
|
$(VENV): requirements.txt
|
||||||
|
python -m venv $(VENV)
|
||||||
|
$(PY) -m pip install -r requirements.txt
|
||||||
|
|
||||||
|
.PHONY: run
|
||||||
|
run: $(VENV)
|
||||||
|
$(PY) -m yourapp
|
||||||
|
```
|
||||||
|
|
||||||
|
`make run` creates the venv if missing, installs deps, and runs — all against
|
||||||
|
the isolated interpreter, no manual activation.
|
||||||
|
|
||||||
|
=== "Docker"
|
||||||
|
|
||||||
|
The container **is** the isolation — its own filesystem, its own interpreter,
|
||||||
|
nothing shared with the host. You don't need a `.venv` inside an image; install
|
||||||
|
straight into the container's Python.
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.12-slim
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how things run in production — see the [Deploy guide](deploy.md) for the
|
||||||
|
full container standard (uid 1337, mounts, layer caching).
|
||||||
|
|
||||||
|
!!! tip "Which one?"
|
||||||
|
**Local `.venv`** for quick iteration, **Makefile** when you want repeatable
|
||||||
|
setup across the team, **Docker** for anything that ships. They're not
|
||||||
|
exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for
|
||||||
|
deploy.
|
||||||
|
|
||||||
|
## Local dev with pyenv
|
||||||
|
|
||||||
|
For local work you also need the right **Python version**, not just isolated deps.
|
||||||
|
We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and
|
||||||
|
switches versions per-project without touching the system Python.
|
||||||
|
|
||||||
|
- **Per-project pinning:** a `.python-version` file in a repo makes pyenv
|
||||||
|
auto-select that interpreter when you `cd` in — everyone on the project runs the
|
||||||
|
same one.
|
||||||
|
- **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an
|
||||||
|
isolated version *and* isolated deps.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation)
|
||||||
|
for the installer and build dependencies — no point reproducing it here. Then add
|
||||||
|
the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell.
|
||||||
|
|
||||||
|
### Shell init
|
||||||
|
|
||||||
|
Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and
|
||||||
|
auto-version-switching won't work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pyenv — Python version management
|
||||||
|
export PYENV_ROOT="$HOME/.pyenv"
|
||||||
|
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||||
|
eval "$(pyenv init --path)" # (1)!
|
||||||
|
eval "$(pyenv init -)" # (2)!
|
||||||
|
eval "$(pyenv virtualenv-init -)" # (3)!
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected
|
||||||
|
version instead of the system one.
|
||||||
|
2. Shell integration — command rehashing and completion.
|
||||||
|
3. Auto-activates a project's virtualenv on `cd` — **only** if you use
|
||||||
|
`pyenv-virtualenv`. Drop this line if you don't.
|
||||||
|
|
||||||
|
### Everyday use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pyenv install 3.10.14 # install a version (one-time)
|
||||||
|
pyenv install --list # see available versions
|
||||||
|
cd <project>
|
||||||
|
pyenv local 3.10.14 # writes .python-version -> auto-selects here
|
||||||
|
python --version # confirms the pinned version
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`pyenv local <ver>`** per project — commit the `.python-version` so the team
|
||||||
|
matches.
|
||||||
|
- **`pyenv global <ver>`** for your default outside any project.
|
||||||
|
|
||||||
|
## Shell quality-of-life extras
|
||||||
|
|
||||||
|
A couple of optional lines worth having alongside pyenv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# flake8 with our shared config (max line 120)
|
||||||
|
alias flake8='flake8 --config ~/.config/flake8'
|
||||||
|
|
||||||
|
# local bins on PATH (pip --user installs, npm globals)
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
- The **flake8 alias** keeps everyone linting with the same config (our 120
|
||||||
|
max-line, etc. — see [Standards](standards.md#files-and-style)).
|
||||||
|
- The **`.local/bin` / npm-global** PATH lines stop "command not found" after a
|
||||||
|
`pip install --user` or a global npm install.
|
||||||
|
|
||||||
|
!!! note "More shell setup"
|
||||||
|
This is the Python-env slice. The fuller dev shell setup — the WSL/Windows
|
||||||
|
clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on
|
||||||
|
the [Workflow](workflow.md#handy-shell-setup) page.
|
||||||
@ -1,4 +1,4 @@
|
|||||||
# Rethink Studios Handbook
|
# rethink development
|
||||||
|
|
||||||
The public reference for building and shipping with Rethink Studios: our shared
|
The public reference for building and shipping with Rethink Studios: our shared
|
||||||
libraries, our coding standards, and how to deploy a project on our network.
|
libraries, our coding standards, and how to deploy a project on our network.
|
||||||
@ -25,6 +25,20 @@ out of it; examples use placeholders like `<dev>`, `<project>`, and `/srv/...`.
|
|||||||
House coding standards — file hygiene, docstrings, type hints, linting, and
|
House coding standards — file hygiene, docstrings, type hints, linting, and
|
||||||
how we handle errors.
|
how we handle errors.
|
||||||
|
|
||||||
|
- :material-sitemap: __[Workflow](workflow.md)__
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
How we actually work day to day — our Gitea, git habits, and the
|
||||||
|
plan-in-chat / build-in-Claude-Code flow, plus shell setup.
|
||||||
|
|
||||||
|
- :material-language-python: __[Virtual environments](environments.md)__
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Project-based Python isolation — local `.venv`, Makefile, or Docker — and
|
||||||
|
local version management with pyenv.
|
||||||
|
|
||||||
- :material-rocket-launch: __[Deploy](deploy.md)__
|
- :material-rocket-launch: __[Deploy](deploy.md)__
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -11,10 +11,44 @@ the repo, where the README and tags live.
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
Pin a tag in your dependencies — never an unpinned branch:
|
||||||
|
|
||||||
```
|
```
|
||||||
<lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag>
|
<lib> @ git+https://git.rethinkstudios.io/rethink-public/<lib>.git@<tag>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? example "Using a library — install, import, go"
|
||||||
|
|
||||||
|
Add it to your project's deps (e.g. `pyproject.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
dependencies = [
|
||||||
|
"aioweb @ git+https://git.rethinkstudios.io/rethink-public/aioweb.git@v0.3.1",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use it — the README in each repo has the real surface; this is the shape:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aioweb import Session
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""fetch a page through the shared async http session"""
|
||||||
|
async with Session() as web:
|
||||||
|
resp = await web.get("https://example.test")
|
||||||
|
print(resp.status, len(resp.content))
|
||||||
|
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
Need to bump a lib? Change the `@<tag>` and reinstall — versions live with the
|
||||||
|
lib, not in these docs.
|
||||||
|
|
||||||
<div id="lib-list" markdown="0">
|
<div id="lib-list" markdown="0">
|
||||||
<p class="lib-status">Loading libraries from Gitea…</p>
|
<p class="lib-status">Loading libraries from Gitea…</p>
|
||||||
</div>
|
</div>
|
||||||
@ -55,12 +89,17 @@ the repo, where the README and tags live.
|
|||||||
var rows = libs.map(function (r) {
|
var rows = libs.map(function (r) {
|
||||||
var url = r.html_url || (REPO_BASE + "/" + r.name);
|
var url = r.html_url || (REPO_BASE + "/" + r.name);
|
||||||
var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>";
|
var desc = r.description ? escapeHtml(r.description) : "<em>No description.</em>";
|
||||||
|
var lang = r.language
|
||||||
|
? '<span class="rt-lang">' + escapeHtml(r.language) + "</span>"
|
||||||
|
: '<span class="rt-lang rt-lang--none">—</span>';
|
||||||
return '<tr><td><a href="' + escapeHtml(url) + '"><code>' +
|
return '<tr><td><a href="' + escapeHtml(url) + '"><code>' +
|
||||||
escapeHtml(r.name) + '</code></a></td><td>' + desc + '</td></tr>';
|
escapeHtml(r.name) + "</code></a></td><td>" + desc +
|
||||||
|
"</td><td>" + lang + "</td></tr>";
|
||||||
}).join("");
|
}).join("");
|
||||||
el.innerHTML =
|
el.innerHTML =
|
||||||
'<table><thead><tr><th>Library</th><th>What it does</th></tr></thead>' +
|
'<div class="rt-lib-table"><table>' +
|
||||||
'<tbody>' + rows + '</tbody></table>';
|
"<thead><tr><th>Library</th><th>What it does</th><th>Language</th></tr></thead>" +
|
||||||
|
"<tbody>" + rows + "</tbody></table></div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
function fail() {
|
function fail() {
|
||||||
@ -84,11 +123,3 @@ the repo, where the README and tags live.
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
!!! info "If the live list is empty or stale"
|
|
||||||
The fetch runs in your browser against the Gitea API and needs the org repos
|
|
||||||
to be readable unauthenticated (they are — it's a public org) and CORS to
|
|
||||||
allow the docs domain. If the list won't load, browse the org directly at
|
|
||||||
[git.rethinkstudios.io/rethink-public](https://git.rethinkstudios.io/rethink-public).
|
|
||||||
The fallback for a disabled live fetch is a webhook rebuild — a push to any
|
|
||||||
`rethink-public` repo triggers a site rebuild.
|
|
||||||
|
|||||||
@ -3,40 +3,129 @@
|
|||||||
The house standards every Rethink Studios project follows. They keep our code
|
The house standards every Rethink Studios project follows. They keep our code
|
||||||
consistent, readable, and predictable across the whole suite.
|
consistent, readable, and predictable across the whole suite.
|
||||||
|
|
||||||
## File hygiene
|
## Files and style
|
||||||
|
|
||||||
- Files end with a **single trailing newline** (LF / Unix line endings).
|
- Files end with a **single trailing newline** (LF / Unix line endings), and
|
||||||
- **No trailing whitespace** on any line.
|
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.
|
||||||
|
|
||||||
## Indentation
|
A well-formed module — module docstring, type hints, lowercase-start docstring:
|
||||||
|
|
||||||
- **4 spaces** by default.
|
```python
|
||||||
- Respect language norms: TS/JS use **2 spaces**, Go uses **tabs**.
|
"""async key/value store backed by a single json file"""
|
||||||
- When editing an existing file, follow that file's existing indentation.
|
|
||||||
|
|
||||||
## Docstrings and comments
|
from pathlib import Path
|
||||||
|
|
||||||
- Public functions get a **docstring**. Style: **lowercase-start, no trailing
|
|
||||||
period**.
|
def load_state(path: Path) -> dict[str, str]:
|
||||||
- Keep inline comments minimal — prefer docstrings. Use an inline comment only
|
"""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.
|
where the code is genuinely complex.
|
||||||
- Each module/file states its scope and purpose via a **module docstring**
|
- Each module/file states its scope and purpose via a **module docstring**
|
||||||
(header string) — **not** a license or copyright header.
|
(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.
|
||||||
|
|
||||||
## READMEs
|
!!! 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.
|
||||||
|
|
||||||
Every library and project has a defined **README** covering:
|
=== "Do"
|
||||||
|
|
||||||
- the install line,
|
```python
|
||||||
- what it does,
|
def mask_secret(value: str, keep: int = 4) -> str:
|
||||||
- a usage example.
|
"""mask all but the last ``keep`` characters of a secret"""
|
||||||
|
if len(value) <= keep:
|
||||||
|
return "*" * len(value)
|
||||||
|
return "*" * (len(value) - keep) + value[-keep:]
|
||||||
|
```
|
||||||
|
|
||||||
## Linting and types
|
=== "Don't"
|
||||||
|
|
||||||
- **flake8 clean**, max line length **120**.
|
```python
|
||||||
- **Type hints** on public functions.
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
## Error handling
|
## Quality and error handling
|
||||||
|
|
||||||
- **Fail loud** — never swallow exceptions.
|
**Fail loud** — never swallow exceptions. Catch the **specific** exception and
|
||||||
- Catch the **specific** exception, and **log** it.
|
**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.
|
||||||
|
|||||||
236
docs/stylesheets/extra.css
Normal file
236
docs/stylesheets/extra.css
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
/* a rethink development — mirror of the rethink Gitea theme
|
||||||
|
palette pulled from git.rethinkstudios.io (data-theme="rethink") */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--rt-body: #0a0f1f; /* deep navy page background */
|
||||||
|
--rt-nav: #061541; /* darker header/nav background */
|
||||||
|
--rt-surface: #0e1530; /* cards / code blocks, a touch above body */
|
||||||
|
--rt-text: #eef1f6; /* near-white body text */
|
||||||
|
--rt-muted: #aab4c5; /* secondary text */
|
||||||
|
--rt-primary: #569bcc; /* links / primary blue */
|
||||||
|
--rt-primary-dark: #4a8cbb;
|
||||||
|
--rt-primary-light: #6ba9d5;
|
||||||
|
--rt-accent: #55bbff; /* bright cyan-blue accent */
|
||||||
|
--rt-border: #294274; /* row dividers / borders */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map the rethink palette onto Material's slate scheme. */
|
||||||
|
[data-md-color-scheme="slate"] {
|
||||||
|
--md-hue: 222;
|
||||||
|
|
||||||
|
--md-default-bg-color: var(--rt-body);
|
||||||
|
--md-default-fg-color: var(--rt-text);
|
||||||
|
--md-default-fg-color--light: var(--rt-muted);
|
||||||
|
--md-default-fg-color--lighter: rgba(238, 241, 246, 0.32);
|
||||||
|
--md-default-fg-color--lightest: rgba(238, 241, 246, 0.12);
|
||||||
|
|
||||||
|
--md-primary-fg-color: var(--rt-nav);
|
||||||
|
--md-primary-fg-color--light: var(--rt-primary-light);
|
||||||
|
--md-primary-fg-color--dark: var(--rt-nav);
|
||||||
|
--md-primary-bg-color: var(--rt-text);
|
||||||
|
--md-primary-bg-color--light: var(--rt-muted);
|
||||||
|
|
||||||
|
--md-accent-fg-color: var(--rt-accent);
|
||||||
|
--md-accent-fg-color--transparent: rgba(85, 187, 255, 0.1);
|
||||||
|
|
||||||
|
--md-typeset-color: var(--rt-text);
|
||||||
|
--md-typeset-a-color: var(--rt-primary);
|
||||||
|
|
||||||
|
--md-code-bg-color: var(--rt-surface);
|
||||||
|
--md-code-fg-color: #d6deeb;
|
||||||
|
|
||||||
|
--md-footer-bg-color: var(--rt-nav);
|
||||||
|
--md-footer-bg-color--dark: var(--rt-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header / top nav: match Gitea's dark-blue bar. */
|
||||||
|
.md-header,
|
||||||
|
.md-tabs {
|
||||||
|
background-color: var(--rt-nav);
|
||||||
|
color: var(--rt-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links hover to the bright accent, like Gitea. */
|
||||||
|
.md-typeset a:hover {
|
||||||
|
color: var(--rt-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar nav active item picks up the blue. */
|
||||||
|
.md-nav__link--active,
|
||||||
|
.md-nav__item .md-nav__link--active {
|
||||||
|
color: var(--rt-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Square the corners — Gitea uses radius 0 throughout. */
|
||||||
|
.md-typeset .admonition,
|
||||||
|
.md-typeset details,
|
||||||
|
.md-typeset pre > code,
|
||||||
|
.md-typeset .highlight,
|
||||||
|
.md-typeset table:not([class]),
|
||||||
|
.md-search__form,
|
||||||
|
.md-typeset .tabbed-set > input:checked + label,
|
||||||
|
.md-typeset code {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle blue row dividers in tables, echoing the repo list. */
|
||||||
|
.md-typeset table:not([class]) {
|
||||||
|
border: 1px solid var(--rt-border);
|
||||||
|
}
|
||||||
|
.md-typeset table:not([class]) th {
|
||||||
|
background-color: var(--rt-nav);
|
||||||
|
color: var(--rt-text);
|
||||||
|
}
|
||||||
|
.md-typeset table:not([class]) td {
|
||||||
|
border-top: 1px solid rgba(41, 66, 116, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search field on the dark bar. */
|
||||||
|
.md-search__input {
|
||||||
|
background-color: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.md-search__input::placeholder {
|
||||||
|
color: rgba(238, 241, 246, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Admonitions — Gitea-style: brand-orange warning, blue note, etc.
|
||||||
|
The lambda logo's orange (#ffae42 -> #f57c00) drives the warning/danger look. */
|
||||||
|
.md-typeset .admonition,
|
||||||
|
.md-typeset details {
|
||||||
|
border-width: 0 0 0 .2rem;
|
||||||
|
background-color: var(--rt-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* warning / caution: the orange Gitea callout */
|
||||||
|
.md-typeset .admonition.warning,
|
||||||
|
.md-typeset details.warning,
|
||||||
|
.md-typeset .admonition.caution,
|
||||||
|
.md-typeset details.caution {
|
||||||
|
border-color: #f57c00;
|
||||||
|
}
|
||||||
|
.md-typeset .warning > .admonition-title,
|
||||||
|
.md-typeset .warning > summary,
|
||||||
|
.md-typeset .caution > .admonition-title,
|
||||||
|
.md-typeset .caution > summary {
|
||||||
|
background-color: rgba(245, 124, 0, 0.12);
|
||||||
|
}
|
||||||
|
.md-typeset .warning > .admonition-title::before,
|
||||||
|
.md-typeset .caution > .admonition-title::before {
|
||||||
|
background-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* danger: a hotter orange-red for true footguns */
|
||||||
|
.md-typeset .admonition.danger,
|
||||||
|
.md-typeset details.danger {
|
||||||
|
border-color: #e8590c;
|
||||||
|
}
|
||||||
|
.md-typeset .danger > .admonition-title,
|
||||||
|
.md-typeset .danger > summary {
|
||||||
|
background-color: rgba(232, 89, 12, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* note / info / tip: blue + cyan from the rethink palette */
|
||||||
|
.md-typeset .admonition.note,
|
||||||
|
.md-typeset details.note,
|
||||||
|
.md-typeset .admonition.info,
|
||||||
|
.md-typeset details.info {
|
||||||
|
border-color: var(--rt-primary);
|
||||||
|
}
|
||||||
|
.md-typeset .note > .admonition-title,
|
||||||
|
.md-typeset .info > .admonition-title {
|
||||||
|
background-color: rgba(86, 155, 204, 0.12);
|
||||||
|
}
|
||||||
|
.md-typeset .admonition.tip,
|
||||||
|
.md-typeset details.tip,
|
||||||
|
.md-typeset .admonition.example,
|
||||||
|
.md-typeset details.example {
|
||||||
|
border-color: var(--rt-accent);
|
||||||
|
}
|
||||||
|
.md-typeset .tip > .admonition-title,
|
||||||
|
.md-typeset .example > summary {
|
||||||
|
background-color: rgba(85, 187, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live libraries table — JS-injected, so it needs explicit Material-style rules
|
||||||
|
(Material only auto-styles markdown tables, not innerHTML ones). */
|
||||||
|
.rt-lib-table {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
.rt-lib-table table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid var(--rt-border);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
}
|
||||||
|
.rt-lib-table th,
|
||||||
|
.rt-lib-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.7em 1em;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.rt-lib-table td:first-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rt-lib-table thead th {
|
||||||
|
background-color: var(--rt-nav);
|
||||||
|
color: var(--rt-text);
|
||||||
|
font-weight: 700;
|
||||||
|
border-bottom: 1px solid var(--rt-border);
|
||||||
|
}
|
||||||
|
.rt-lib-table tbody tr {
|
||||||
|
border-top: 1px solid rgba(41, 66, 116, 0.5);
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
.rt-lib-table tbody tr:hover {
|
||||||
|
background-color: rgba(86, 155, 204, 0.08);
|
||||||
|
}
|
||||||
|
.rt-lib-table td code {
|
||||||
|
background-color: rgba(85, 187, 255, 0.12);
|
||||||
|
color: #cfe6ff;
|
||||||
|
padding: 0.2em 0.55em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rt-lib-table a {
|
||||||
|
color: var(--rt-accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.rt-lib-table a:hover code {
|
||||||
|
background-color: rgba(85, 187, 255, 0.22);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Language badge in the libs table. */
|
||||||
|
.rt-lang {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.1em 0.5em;
|
||||||
|
border: 1px solid var(--rt-border);
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: rgba(86, 155, 204, 0.1);
|
||||||
|
color: var(--rt-primary-light);
|
||||||
|
font-size: 0.85em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.rt-lang--none {
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--rt-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code annotation markers (the (1) callouts) in brand blue. */
|
||||||
|
.md-typeset .md-annotation__index {
|
||||||
|
background-color: var(--rt-primary);
|
||||||
|
color: var(--rt-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Landing-page cards: surface tint + blue border like Gitea panels. */
|
||||||
|
.md-typeset .grid.cards > :is(ul, ol) > li,
|
||||||
|
.md-typeset .grid > .card {
|
||||||
|
background-color: var(--rt-surface);
|
||||||
|
border: 1px solid var(--rt-border);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.md-typeset .grid.cards > :is(ul, ol) > li:hover {
|
||||||
|
border-color: var(--rt-primary);
|
||||||
|
}
|
||||||
243
docs/workflow.md
Normal file
243
docs/workflow.md
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# Workflow
|
||||||
|
|
||||||
|
How we actually work day to day at Rethink Studios — where code lives, how we use
|
||||||
|
git, the AI-assisted dev flow we recommend, and the shell setup that ties it
|
||||||
|
together. This is the **our-flavored** version: why *we* do it this way and how
|
||||||
|
*our* setup is wired. For the truly generic parts (installing WSL, learning git),
|
||||||
|
we link the official docs rather than reteach them.
|
||||||
|
|
||||||
|
!!! info "Public, sanitized"
|
||||||
|
Examples use placeholders — `<you>`, `<key>`, `dev@<you>`, `/mnt/c/<your>/...`.
|
||||||
|
Swap in your own real values locally; never commit personal paths, key names,
|
||||||
|
or emails.
|
||||||
|
|
||||||
|
## Why git, why WSL
|
||||||
|
|
||||||
|
**Git** is the backbone of everything here. Version history and branching are the
|
||||||
|
obvious part, but our whole deploy model is built on git too: we `pip install`
|
||||||
|
libraries straight from git by tag, servers pull via per-repo deploy keys, and
|
||||||
|
changes flow `develop → main` before they ship. If you know git, you already
|
||||||
|
understand how our code moves from your machine to production.
|
||||||
|
|
||||||
|
**WSL2 (Ubuntu)** is where we develop — a real Linux toolchain on a Windows
|
||||||
|
desktop. You get native Linux tooling (the same environment our servers run) plus
|
||||||
|
the Windows apps you actually use day to day. Install WSL2 from Microsoft's docs;
|
||||||
|
this page documents the *our-setup* layer that goes on top.
|
||||||
|
|
||||||
|
- [Install WSL (Microsoft)](https://learn.microsoft.com/windows/wsl/install)
|
||||||
|
- [VS Code Remote — WSL](https://code.visualstudio.com/docs/remote/wsl)
|
||||||
|
|
||||||
|
## Signing up on our Gitea
|
||||||
|
|
||||||
|
Our code lives on **Gitea** at
|
||||||
|
[git.rethinkstudios.io](https://git.rethinkstudios.io). Two orgs you'll use:
|
||||||
|
|
||||||
|
- **[rethink-public](https://git.rethinkstudios.io/rethink-public)** — public
|
||||||
|
libraries, resources, and assets.
|
||||||
|
- **[rethink-software](https://git.rethinkstudios.io/rethink-software)** — our
|
||||||
|
applications.
|
||||||
|
|
||||||
|
**Get an account:**
|
||||||
|
|
||||||
|
1. Register at [git.rethinkstudios.io](https://git.rethinkstudios.io) and verify
|
||||||
|
your email.
|
||||||
|
2. Add your **SSH public key** under *Settings → SSH / GPG Keys* so you can clone
|
||||||
|
and push over SSH.
|
||||||
|
3. For servers, we use a **per-repo deploy-key** model rather than your personal
|
||||||
|
key — see the [Deploy guide](deploy.md) for how a box gets read access to just
|
||||||
|
the repos it needs.
|
||||||
|
|
||||||
|
## Our git vs. public git (GitHub / GitLab)
|
||||||
|
|
||||||
|
It's the same git — just a different host. The thing most devs trip on is
|
||||||
|
**identity and keys per host**: you may have a GitHub identity *and* a Gitea
|
||||||
|
identity on one machine, and commits need to be attributed (and signed/pushed)
|
||||||
|
with the right one per project.
|
||||||
|
|
||||||
|
We solve that with **per-repo local git config** — run a small alias inside a repo
|
||||||
|
to set its local user and the SSH key it pushes with (see
|
||||||
|
[per-project git identity](#per-project-git-identity) below). No global identity
|
||||||
|
juggling.
|
||||||
|
|
||||||
|
Our conventions, in short:
|
||||||
|
|
||||||
|
- **Signed commits** — `git commit -s`.
|
||||||
|
- **No AI co-author trailer** on your own work. (Intern/dev work you're crediting
|
||||||
|
gets that dev's `Co-Authored-By` — nothing else.)
|
||||||
|
- **`develop` is staging**, merge to **`main`** via MR when it's ready to ship.
|
||||||
|
- **Libraries install from git by tag** — pin a version in your deps, bump the
|
||||||
|
tag when the lib releases.
|
||||||
|
|
||||||
|
!!! warning "Set your per-repo identity *before* the first commit"
|
||||||
|
One machine often carries more than one Gitea identity/key. If you forget to
|
||||||
|
run `gitsetup` in a fresh clone, your commits attribute to the wrong
|
||||||
|
user — or push with the wrong key and bounce. Run it right after cloning;
|
||||||
|
see [per-project git identity](#handy-shell-setup).
|
||||||
|
|
||||||
|
## Git basics (our-flavored)
|
||||||
|
|
||||||
|
Not a git tutorial — just how the everyday loop looks against our Gitea. For the
|
||||||
|
generic command reference, keep the
|
||||||
|
[Git cheat sheet](https://training.github.com/downloads/github-git-cheat-sheet/)
|
||||||
|
or the [Pro Git book](https://git-scm.com/book) handy.
|
||||||
|
|
||||||
|
**Clone** over SSH (the alias maps to a key — see the shell setup):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@<alias>:rethink-public/<repo>.git
|
||||||
|
```
|
||||||
|
|
||||||
|
**The everyday loop:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git switch -c <feature> # branch off
|
||||||
|
# ...make a logical change...
|
||||||
|
git commit -s -m "..." # commit, signed
|
||||||
|
git push -u origin <feature> # push
|
||||||
|
# open an MR: develop -> main
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit small and often** — one commit per logical change, not a giant
|
||||||
|
end-of-day dump. Small commits are easier to review, revert, and `git bisect`
|
||||||
|
when something breaks.
|
||||||
|
|
||||||
|
**Read history** as a graph with the `gl` alias below:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gl # git log --graph, oneline, decorated
|
||||||
|
```
|
||||||
|
|
||||||
|
## The dev workflow
|
||||||
|
|
||||||
|
This is the part that's distinctly *ours*. AI is a force multiplier, but only with
|
||||||
|
discipline around it — the two habits that matter most are **plan before you
|
||||||
|
build** and **verify by executing, not asserting**.
|
||||||
|
|
||||||
|
You can use whatever AI you like — but we **recommend Claude** (via
|
||||||
|
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview), in the
|
||||||
|
terminal or VS Code). The whole project structure we recommend below — the
|
||||||
|
`.claude/` folder, `CLAUDE.md` instructions, numbered specs — is built on that
|
||||||
|
preference: it's designed around how Claude Code reads project context and takes
|
||||||
|
handoffs. Other tools can read these files too, but the convention assumes
|
||||||
|
Claude-first.
|
||||||
|
|
||||||
|
**Plan here, build in Claude Code.** Do the thinking in chat — plan, write the
|
||||||
|
spec, make the decisions. Then hand that spec to a Claude Code agent that does the
|
||||||
|
build: it implements, **verifies by running**, and pushes. The chat plans; the
|
||||||
|
agent implements.
|
||||||
|
|
||||||
|
**tmux + Claude Code split.** In practice that's a Claude Code agent running in a
|
||||||
|
tmux pane doing the build work while you plan/review in another. One spec in, a
|
||||||
|
verified change out.
|
||||||
|
|
||||||
|
**claudedo (optional, hands-free).** Voice control for Claude Code over tmux: a
|
||||||
|
wake-word plus local Whisper speech-to-text drives your tmux session without the
|
||||||
|
keyboard — handy when you're fullscreen or away from the desk. It's available
|
||||||
|
here:
|
||||||
|
[git.rethinkstudios.io/rethink-software/claudedo](https://git.rethinkstudios.io/rethink-software/claudedo).
|
||||||
|
|
||||||
|
**The `.claude/` project convention.** Every project has a top-level `.claude/`
|
||||||
|
folder (always gitignored — nothing under it is committed):
|
||||||
|
|
||||||
|
- **`CLAUDE.md`** — project-specific instructions for the agent (stack, layout,
|
||||||
|
conventions). Layered on top of your global instructions.
|
||||||
|
- **`compact.md`** — a running state log (done / decided / in-flight), updated at
|
||||||
|
checkpoints so a fresh session catches up fast.
|
||||||
|
- **`commands.log`** — an append-only record of shell commands run.
|
||||||
|
- **`spec/`** — numbered, per-change specs named `NN-<type>-<short>.md`
|
||||||
|
(e.g. `01-feature-logging.md`).
|
||||||
|
|
||||||
|
Tell the agent to **"setup project"** to scaffold all of that (and add `.claude/`
|
||||||
|
to `.gitignore`) in a new repo.
|
||||||
|
|
||||||
|
**AI-assist habits.** When you're stuck, give the AI the *exact* context instead
|
||||||
|
of describing it — the WSL clipboard bridge makes this trivial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff | clip.exe # then paste the diff straight into the chat
|
||||||
|
```
|
||||||
|
|
||||||
|
Use AI for the plan and the spec, let the agent build, and always **prove it
|
||||||
|
works by running it** — don't accept "this should work."
|
||||||
|
|
||||||
|
!!! tip "Verify by executing, not asserting"
|
||||||
|
The single habit that separates good AI-assisted work from plausible-looking
|
||||||
|
nonsense: **run it**. A passing build, a real screenshot, actual output — that
|
||||||
|
is proof. "It should work" is not.
|
||||||
|
|
||||||
|
## Recommended setup
|
||||||
|
|
||||||
|
- **WSL2 (Ubuntu) + VS Code + Claude Code.** Develop in WSL, edit in VS Code over
|
||||||
|
[Remote — WSL](https://code.visualstudio.com/docs/remote/wsl), and run
|
||||||
|
[Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as your
|
||||||
|
agent (terminal or the VS Code extension).
|
||||||
|
- **[pyenv](https://github.com/pyenv/pyenv)** for per-project Python versions
|
||||||
|
(we target **3.10+**) and isolated `.venv`s — full setup on the
|
||||||
|
[Virtual environments](environments.md) page.
|
||||||
|
- **[flake8](https://flake8.pycqa.org/)** with a shared config — max line length
|
||||||
|
**120** (see the alias below).
|
||||||
|
|
||||||
|
## Handy shell setup
|
||||||
|
|
||||||
|
Copy-paste these into your `.zshrc` / `.bashrc`. Replace every placeholder with
|
||||||
|
your real values. The high-value ones are explained underneath.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# --- WSL <-> Windows bridges ---
|
||||||
|
alias explorer="explorer.exe ." # open current dir in Windows Explorer
|
||||||
|
# pipe to the Windows clipboard (great for pasting context into AI):
|
||||||
|
# git diff | clip.exe -> paste the diff straight into a chat
|
||||||
|
# auto-alias Windows .exe tools on PATH (e.g. adb/platform-tools):
|
||||||
|
export PATH="$PATH:/mnt/c/<your>/platform-tools"
|
||||||
|
for exe in /mnt/c/<your>/platform-tools/*.exe; do
|
||||||
|
alias "$(basename "${exe}" .exe)"="${exe}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- pyenv (Python version management) ---
|
||||||
|
export PYENV_ROOT="$HOME/.pyenv"
|
||||||
|
export PATH="$PYENV_ROOT/bin:$PATH"
|
||||||
|
eval "$(pyenv init --path)"
|
||||||
|
eval "$(pyenv init -)"
|
||||||
|
eval "$(pyenv virtualenv-init -)"
|
||||||
|
|
||||||
|
# --- flake8 with a shared config ---
|
||||||
|
alias flake8='flake8 --config ~/.config/flake8'
|
||||||
|
|
||||||
|
# --- per-project git identity (switch identity per repo) ---
|
||||||
|
# set the LOCAL (per-repo) user + the key to sign/push with:
|
||||||
|
alias gitsetup='git config --local user.name "<you>"; \
|
||||||
|
git config --local user.email "dev@<you>"; \
|
||||||
|
git config --local core.sshCommand "ssh -i $HOME/.ssh/<key>"'
|
||||||
|
# a second identity for a different account, same pattern:
|
||||||
|
alias gitea='git config --local user.name "<alt>"; \
|
||||||
|
git config --local user.email "<alt>@<host>"; \
|
||||||
|
git config --local core.sshCommand "ssh -i $HOME/.ssh/<alt-key>"'
|
||||||
|
|
||||||
|
# --- git log graph ---
|
||||||
|
alias gl='git log --graph --abbrev-commit --pretty=oneline --decorate'
|
||||||
|
|
||||||
|
# --- local bins on PATH ---
|
||||||
|
export PATH="$HOME/.local/bin:$PATH"
|
||||||
|
export PATH="$HOME/.npm-global/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# cherry-pick a commit onto master/main quickly
|
||||||
|
gitcs() {
|
||||||
|
if [ -z "$1" ]; then echo "Usage: gitcs <commit>"; return 1; fi
|
||||||
|
git checkout master && git cherry-pick "$1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**The ones worth understanding:**
|
||||||
|
|
||||||
|
- **`git diff | clip.exe`** — the killer WSL trick for AI-assisted dev. Pipe your
|
||||||
|
working changes straight to the Windows clipboard and paste them into the chat
|
||||||
|
so the AI sees *exactly* what changed instead of your paraphrase of it.
|
||||||
|
- **`gitsetup` / `gitea`** — per-repo identity. One machine, multiple Gitea
|
||||||
|
identities and keys; run the alias inside a repo to set its **local** user and
|
||||||
|
signing/push key, so commits attribute correctly without touching your global
|
||||||
|
config.
|
||||||
|
- **`gl`** — a readable branch graph for understanding history at a glance.
|
||||||
|
- **`pyenv`** — per-project Python versions, so each repo builds against the
|
||||||
|
version it targets.
|
||||||
26
mkdocs.yml
26
mkdocs.yml
@ -1,26 +1,20 @@
|
|||||||
site_name: Rethink Studios Handbook
|
site_name: rethink development
|
||||||
site_description: Libraries, coding standards, and how to deploy on our network.
|
site_description: Libraries, coding standards, and how to deploy on our network.
|
||||||
site_url: https://docs.rethinkstudios.io/
|
site_url: https://docs.rethinkstudios.io/
|
||||||
copyright: Rethink Studios
|
copyright: rethink development (handbook)
|
||||||
|
|
||||||
|
extra_css:
|
||||||
|
- stylesheets/extra.css
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
language: en
|
language: en
|
||||||
|
logo: assets/logo.svg
|
||||||
|
favicon: assets/logo.svg
|
||||||
palette:
|
palette:
|
||||||
- media: "(prefers-color-scheme: light)"
|
|
||||||
scheme: default
|
|
||||||
primary: indigo
|
|
||||||
accent: indigo
|
|
||||||
toggle:
|
|
||||||
icon: material/weather-night
|
|
||||||
name: Switch to dark mode
|
|
||||||
- media: "(prefers-color-scheme: dark)"
|
|
||||||
scheme: slate
|
scheme: slate
|
||||||
primary: indigo
|
primary: custom
|
||||||
accent: indigo
|
accent: custom
|
||||||
toggle:
|
|
||||||
icon: material/weather-sunny
|
|
||||||
name: Switch to light mode
|
|
||||||
features:
|
features:
|
||||||
- navigation.instant
|
- navigation.instant
|
||||||
- navigation.tracking
|
- navigation.tracking
|
||||||
@ -59,4 +53,6 @@ nav:
|
|||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Libraries: libraries.md
|
- Libraries: libraries.md
|
||||||
- Standards: standards.md
|
- Standards: standards.md
|
||||||
|
- Workflow: workflow.md
|
||||||
|
- Virtual environments: environments.md
|
||||||
- Deploy: deploy.md
|
- Deploy: deploy.md
|
||||||
|
|||||||
10
requirements.txt
Normal file
10
requirements.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Build dependencies for the handbook docs site.
|
||||||
|
# Host build: python -m venv .venv && . .venv/bin/activate
|
||||||
|
# pip install -r requirements.txt && mkdocs build
|
||||||
|
# Output: static site/ served by the reverse proxy at docs.rethinkstudios.io.
|
||||||
|
|
||||||
|
mkdocs-material==9.7.6 # theme — pulls mkdocs, pymdown-extensions, pygments
|
||||||
|
mkdocs==1.6.1 # pinned explicitly for a reproducible build
|
||||||
|
mkdocs-material-extensions==1.3.1
|
||||||
|
pymdown-extensions==11.0 # admonitions, tabbed, superfences, annotations
|
||||||
|
Pygments==2.20.0 # syntax highlighting in code blocks
|
||||||
Loading…
Reference in New Issue
Block a user