Conventions for changing this repository, for people and coding agents. AGENTS.md has the short version; this file
has the details and the reasons. Why the plugin itself works the way it does is in design-decisions.md.
The repository follows git flow:
| Branch | Holds | Created from | Merged into |
|---|---|---|---|
main (default) |
Released code only; every commit on it is a release, tagged | - | - |
develop |
The next release, integrated and passing make check |
- | - |
feature/<name> |
One feature or fix | develop |
develop, through a pull request |
release/<version> |
Version bump and last fixes before a release | develop |
main (then tagged) and develop |
hotfix/<version> |
Urgent fix to a release | main |
main (then tagged) and develop |
main is the default branch on purpose: dokku plugin:install <repository URL> without --committish installs the
default branch, so it must always be a stable release. New work never starts from main.
- Nobody commits directly to
mainordevelop: every change arrives through a pull request. - Before opening or updating a pull request, rebase the branch on the latest
develop(git fetchandgit rebase origin/develop), so it merges without conflicts andmake checkruns on what will be merged. - Release: in
release/<version>, setversioninplugin.tomland__version__indokku_auto_deploy/__init__.pyto the same value (a test checks it), merge intomain, tag the merge commit with the bare version (0.2.0, novprefix: users install with--committish 0.2.0), then mergemainback intodevelop.
- One commit, one coherent intention. Atomic is not about size: a new setting touches
settings.py, the README table, tests and maybedesign-decisions.md, and that is one commit. - A preparatory refactor (same behavior, e.g. extracting an interface before a new implementation) goes in its own commit, before the feature. A bug found while refactoring is fixed in a third commit, not mixed into the refactor.
- Stage files and hunks deliberately (
git add <file>,git add -p) and review the staged diff;git add .andgit add -Apick up stray files. - Title in English, imperative ("Add", "Fix", "Extract"), names of files, functions and commands in backticks, a plain
hyphen (
-) where a dash is needed. Prefer a title that stands alone; when a body is needed, keep it to a few plain sentences that say why (e.g. what happened before), not what the diff already shows.
- Standard library only at runtime: the plugin runs in place with the host's
python3(3.11+), with no virtualenv and no package installed. Development tools (pytest, mypy, ruff, shellcheck) are the only dependencies, in thedevgroup ofpyproject.toml. If something seems to need a library, discuss it first. pathlib.Pathfor every path, neveros.path.- Modern type hints everywhere (
str | None,list[Path]), checked bymypy --strict. Avoid control flow that can leave a variable unbound (possibly-undefinedis enabled). - No one-letter names, except
_for ignored values:for app in apps, notfor a in apps. - No comments that repeat the code. Docstrings say what a function does, its edge cases (
None, empty, special values) and, when there is a non-obvious trade-off, why it was done this way. A comment is fine to explain a decision the code can't show (e.g. a Dokku or forge behavior). - Errors are handled or reported, never swallowed: no
except: pass, andexcept Exceptiononly where every error must be contained (one app's failure must not stop the others) and is logged.KeyboardInterruptis handled separately and exits with 130. time.perf_counter()for durations;json.dumps(..., default=str)for values that may holddatetime.
- Every I/O call has an explicit timeout: HTTP through
urllib.request(seeForge.request),subprocess.run(..., capture_output=True, timeout=...). Addtext=Trueonly for textual output;check=Trueunless the exit code is the answer (likedokku apps:locked). - Errors from the network become the module's own exception (
ForgeError,TelegramError) with a message a person can act on, raisedfrom Noneso no traceback carries a URL or header.
- Never pass a secret as a command-line argument (visible in
psand/proc/<pid>/cmdline), not todokkunor to any other program: forge tokens live in the dokku user's.netrc(dokku git:auth), secret settings are read from stdin. - Never log, print or put in an exception message a secret or anything that may contain one: the Telegram API URL has the bot token in its path, and invalid values are rejected without echoing them.
- Files holding secrets are created with mode 0600 from the start (
tempfile.NamedTemporaryFileandos.open(..., 0o600)do that), never created first and restricted later, which leaves a window where others can read them.
The Python code only runs as dokku auto-deploy:<command>, so some Dokku conventions override the usual argparse
ones:
- Commands use
argparseincli.py, withprog="dokku"and one subparser perauto-deploy:<command>. - Dokku reads
--force,--app,--quietand--traceanywhere on the command line: never use them as options. --globaltakes the place of an app name insetandreport, andreportaccepts Dokku's--auto-deploy-<name>flags and--format stdout|json;parse_argsadapts argparse to that.- Required values are positional; options are optional by definition. Long options are kebab-case and, when a letter
is free and unambiguous, have a short form written first (
-r,--redeploy). Usemetavarfor the value's name and listchoicesin the help text. - Validate and convert values in
type=callables (seeparse_change_number) or insettings.KEYS, raisingargparse.ArgumentTypeError/ConfigErrorwith a message that says what was expected. - Data (reports, the build log) goes to stdout, status and errors to stderr. Silence on success is fine;
-vshows more. - Exit codes: 0 ok, 1 an app failed, 2 invalid arguments, 3 invalid settings, 4 a deploy failed (
poll; 1 wins over it), 130 interrupted. Keep them in the README and in the--helpepilog when adding a case. - Only
cli.pyprints. Other modules log throughlogging.getLogger(__name__)and return data; the build output reaches the terminal through a callback. - Imports that only a command needs go inside that command's function, so
--helpstays fast. - A new command needs a
subcommands/<name>symlink todefault(Dokku runssubcommands/<name>forauto-deploy:<name>) and a line inhelp-functions; a test checks both.
- Follow Dokku's plugin style:
set -eo pipefail,[[ $DOKKU_TRACE ]] && set -x, and functions declaringdesc(andtriggerfor triggers).nounsetis left out on purpose: Dokku's optional variables (DOKKU_TRACEand others) are unset in normal runs. - Keep logic in Python; bash only dispatches, runs as root where needed (
install,update,uninstall) or uses Dokku's own functions (fn-plugin-property-*). make lint-checkruns shellcheck on every script; a new script goes intoSHELL_SCRIPTSin theMakefile.
- pytest with plain
assert; nounittest.TestCase,setUportearDown. Group related tests in plainclass TestX:rather than comment separators, and usepytest.mark.parametrizewithpytest.param(..., id=...)for tables of cases. - Test behavior through the real code paths, with the fixtures in
tests/conftest.py:fake_api(a local HTTP server for forge APIs and Telegram that records requests and headers, can redirect or drop connections),fake_dokku(fakedokku,plugnandsystemctlfirst inPATH) anddokku_env(Dokku's directories in a temp dir). Don't mockurllib,subprocessor the functions under test. - Tests never touch the network, a real Dokku or the host's systemd.
- Compare whole results (the full state, the full list of requests) rather than fragments, so an extra or missing item fails the test.
- Write the failing test first when possible. A test added for a bug says so in its docstring ("Regression: ...").
- Before committing:
make check(ruff check, ruff format check, shellcheck, mypy, pytest).
- All of the repository is in English: code, messages, docs, commits.
- The README covers installation, every setting and command, and a step-by-step setup; update it in the same commit
as the behavior it describes. Design decisions and facts checked outside the repository (Dokku, forge or Telegram
behavior, with where and when they were checked) go in
docs/design-decisions.md. - Markdown: lines up to 120 characters, straight quotes (
"), a plain hyphen or--instead of an em dash,->instead of arrow characters, no---separators, no bold in headings,- [ ]for checklists.