coding.sgit.ai / the house style / Bash

Bash

There are 5 .sh files in a 217,266-line repository. That is the finding. Shell is not written here — it is generated from typed Python, and the pattern is original enough to be the most interesting infrastructure convention in the estate.

1. Where shell actually lives

LocationWhat
sg_compute/platforms/ec2/user_data/15 Section__* classes that render shell fragments. Nearly all shell in the estate lives here
docker/Dockerfiles and their RUN lines
.github/workflows/Inline run: blocks
scripts/A handful of .sh files and Python CLI shims

2. The Section__* pattern

sg_compute/platforms/ec2/user_data/Section__Shutdown.pyverbatim and entire · 04__bash-and-generated-shell.md
# ═══════════════════════════════════════════════════════════════════════════════
# Ephemeral EC2 — Section__Shutdown
# Schedules auto-termination via systemd-run. Paired with
# InstanceInitiatedShutdownBehavior=terminate in EC2__Launch__Helper so that
# halt causes termination, not stop.
# max_hours=0 emits a no-op comment (caller omits this section instead).
# ═══════════════════════════════════════════════════════════════════════════════

from osbot_utils.type_safe.Type_Safe import Type_Safe

TEMPLATE = '''
# ── Auto-terminate after {max_hours}h ({seconds}s) ───────────────────────────
systemd-run --on-active={seconds}s /sbin/shutdown -h now
echo "[ephemeral-ec2] auto-terminate timer set: {max_hours}h ({seconds}s) from now"
'''


class Section__Shutdown(Type_Safe):

    def render(self, max_hours: float = 1.0) -> str:
        seconds = max(1, int(round(max_hours * 3600)))
        return TEMPLATE.format(max_hours=max_hours, seconds=seconds)

Apache-2.0 — quoted from the estate's own source, not covered by this site's CC BY 4.0. Counted 2026-08-24.

The anatomy, and every part of it is a convention

  1. A ═══ banner carrying the coupling, not just the purpose — paired with InstanceInitiatedShutdownBehavior=terminate in EC2__Launch__Helper. The shell and the API call that makes it correct are documented together, in the place where forgetting one would break the other. This is the single best idea on this page.
  2. A module-level TEMPLATE — a triple-quoted string, so the shell is readable as shell.
  3. The generated shell gets its own banner, using the ── box-drawing character rather than Python's ═══. Shell inside Python is marked as a different language by its comment style.
  4. Section__X(Type_Safe) with a single render() returning str.
  5. Computation in Python, not in shell. The arithmetic, the clamping and the rounding all happen where they can be tested; the emitted shell contains a literal.
  6. An echo breadcrumb with a bracketed prefix, so the boot log is greppable.
  7. The edge case documented in the bannermax_hours=0 — with the caller's correct behaviour named.

The fifteen sections

Base · Docker · Sidecar · Nginx · Env__File · GPU_Verify · NVIDIA_Container_Toolkit · Ollama · VLLM · SGit_Venv · Claude_Code__Firstboot · Claude_Launch · Agent_Tools · Node · Shutdown

A node's user-data is composed by rendering the sections it needs and concatenating them.

Composition is a list, not a template with conditionals.

Which is why there is no {% if gpu %} anywhere, and why a spec that does not need CUDA simply omits NVIDIA_Container_Toolkit. A conditional inside a template is a branch nobody tests; an omitted list element is a list you can print.

3. The trade

What it buys

  • The parameters are typed and tested. A section has unit tests; a bash script interpolated with sed does not.
  • No quoting hell. Values are substituted by str.format in Python, not by shell expansion inside a heredoc inside a YAML string.
  • Composition without conditionals. Include a section or do not.
  • The shell is diffable as a unit. A change to shutdown behaviour is one small file, not a hunk inside a 400-line boot script.
  • The coupling is documented next to the code, in the banner.

What it costs

  • You cannot run it directly. No bash -n on a template, no shellcheck, no executing a section in isolation without rendering it first.
  • Two languages in one file, with {} meaning str.format in a language where {} also means brace expansion. A literal { must be escaped as {{.
  • The rendered output is not committed, so a reviewer reads the template and never the script.

Both mitigations are cheap and neither exists yet. Render every section in a test and pipe it through shellcheck; and commit a rendered golden file per section so diffs show the actual shell. The first is the highest-value single change available anywhere in the enforcement backlog. The check that would do it →

4. Hand-written shell — where it survives

5 files, and set -e discipline is inconsistent: 2 of 5 have it, in three different forms — set -euo pipefail, set -eu, set -u. No rule covers it.

The recommendation this site publishes is set -euo pipefail everywhere, since it is already the strictest form in use and the other two are strictly weaker. That is a recommendation, not an observed convention, and it is labelled as one on the rules page.

For RUN lines in Dockerfiles and run: blocks in workflows, the conventions visible in the repository are: one logical step per RUN, && chaining with \ continuations, cleanup in the same layer, and — from the Playwright Dockerfile — build-time assertions rather than trust, with a comment naming the production incident that motivated each. That last one is the same instinct as rules 14 and 15: a check that cites the case that made it necessary.

5. This site runs the rule it recommends

The shell in this repository's own release pipeline uses set -euo pipefail, and the workflow carries a comment explaining the one place it bites: piping git log into an early-exiting reader makes git die of SIGPIPE, and under pipefail that status propagates out of the command substitution and kills the job after the variable has already been assigned — so it looks like the next line failed. The job it lives in →