Contributing to litesoup
Welcome! litesoup is a small, opinionated bash installer for a LAMP-ish stack on Ubuntu. The codebase is intentionally boring — POSIX-ish bash, shellcheck-clean, fail-fast, idempotent. This guide shows you the loop we use day to day so your first PR feels familiar.
1. Quick start for contributors
# Clone with submodules — bats-core lives in test/bats/bats-core
git clone --recurse-submodules https://github.com/codetot-web/litesoup.git
cd litesoup
# Branch off main, naming it after the issue you're closing
git checkout -b feat/42-add-thing
# Edit code. Keep changes small. Read before you write.
# Run the local test loop (see below) before pushing
shellcheck -x -e SC1091 install/install-stack.sh install/lib/*.sh \
site/*.sh harden/*.sh audit/*.sh
./test/bats/bats-core/bin/bats test/bats/
# Push and open a PR
git push -u origin feat/42-add-thing
gh pr create --fill
CI will re-run shellcheck, bats, and a Docker integration job. All three must be green before merge.
2. Local test loop
Three commands cover ~95% of regressions. Run them in order — fastest first, slowest last:
# (a) shellcheck — instant. Same flags CI uses.
shellcheck -x -e SC1091 install/install-stack.sh install/lib/*.sh \
site/*.sh harden/*.sh audit/*.sh
# (b) bats unit tests — a few seconds. Pure helpers, no system mutation.
./test/bats/bats-core/bin/bats test/bats/
# (c) Docker integration — minutes. Spins a real systemd container and
# runs install-stack + site create/delete end-to-end.
./test/docker/run-integration.sh 01_install_stack.sh 02_site_create.sh 03_site_delete.sh
The integration test needs Docker running (Docker Desktop or OrbStack on macOS; Docker Engine on Linux). If you don’t have it locally, push and let CI run it — but expect a slower feedback loop.
3. Conventions
Every script in this repo follows the same shape. New scripts should too.
- Shebang:
#!/usr/bin/env bash— never/bin/sh, never/bin/bash. - Header: 2-line comment explaining what the script does and what it
touches, followed by a
usage()function with acat <<'EOF'Usage block. - Source common.sh:
source "$(dirname "$0")/../install/lib/common.sh"(or the right relative path). This gives you:set -Eeuo pipefail— fail fast on errors, undefined vars, pipe failures.log_info/log_warn/log_error— timestamped stderr logging.run_or_dryrun— runs the command, or prints it underDRY_RUN=1.require_root— exit 1 if not running as root.- An
ERRtrap that prints the failing line number.
- Flags: support
--dry-runand--helpat minimum. Many scripts also take feature-specific flags (--no-password-auth,--skip-hardening). - Idempotent writes: never blindly overwrite a managed config file.
Use the cmp-then-install pattern — write to a tmp file,
cmp -sagainst the live file, onlyinstall -m 0644if they differ. Seeinstall/lib/redis.sh(and theharden/*.shscripts) for canonical examples. This means re-running install-stack is safe and cheap. - Reload, not restart: prefer
systemctl reloadoverrestart. Reload preserves live connections and SSH sessions. Reload only the services whose managed config actually changed. - shellcheck-clean:
shellcheck -x -e SC1091must pass.-xfollows sourced files via# shellcheck source=annotations.-e SC1091ignores runtime-resolved sources that-xalready covers.
4. Test layers
| Layer | Tool | Where | When to add a test |
|---|---|---|---|
| Unit | bats | test/bats/ |
New helper function or pure logic |
| Integration | docker | test/integration/ |
New install-stack stage or site-* script |
| Acceptance | shell | test/acceptance-*-run.sh |
New plan or release; run on real Ubuntu (sg10.codetot.org) |
Bats tests should source install/lib/common.sh in a subshell and assert
on observable behavior — see test/bats/unit_common.bats for the style.
Integration tests run inside a systemd-enabled container and exercise the
real apt + systemctl path. Acceptance tests run on a real Ubuntu VPS and
catch things Docker can’t (PPA reachability, real systemd-resolved, real
TLS cert issuance) — required for any change that touches install-stack.sh.
5. Multi-agent dispatch pattern
This repo’s release workflow leans on parallel Claude subagents. It’s worth knowing why, because future contributors will want to reuse it.
Wave 1 (v0.6.0) — 4 audit ports + 3 harden scripts, written by 7
parallel Claude subagents in a single dispatch. ~30 minutes of
wall-clock time produced 2367 LOC of shellcheck-clean bash across 7 new
files. The adversarial review pass caught one real bug (harden-fail2ban
watched the wrong port when sshd ran on a non-default port — fixed by
sharing the detect_ssh_port parser with harden-firewall).
Wave 2 (v0.7.0) — 3 hardening scripts (sshd, Apache, PHP). Per the
routing table in ~/.claude/CLAUDE.md, we tried local Ollama
qwen2.5-coder:14b first as the daily-driver codegen model. Output was
shellcheck-clean but contained 3 semantic bugs:
- Literal
\ninside heredoc content → produced an invalid sshd_config. - Validation step ran against the wrong file (validated source, not the installed copy).
- Unset variable crashed under
set -ubecause a counter wasn’t initialized.
We fell back to 3 parallel Claude subagents. All 3 delivered correct
working scripts in ~2 minutes. The per-script briefs explicitly named
each qwen failure mode (“don’t write \n strings — use a heredoc”;
“validate AFTER install, not before”; “initialize CHANGED=0 — set -u
crashes otherwise”) and each subagent reported avoiding them.
Lesson: shellcheck-clean does not equal semantically correct. Local models are useful for drafts, but Claude subagents remain the safer default for bash codegen with critical correctness requirements.
Subagent brief checklist — every dispatch brief should be self-contained:
- Scope-locked target file (one file per agent, no scope creep).
- No
/tmpoutput (write directly to the target path so the parent can review). - No commits (parent integrates everything in one commit per file or per wave).
- Max 2 retries on errors (if the model fails twice, escalate — don’t loop).
- Explicit reference files for conventions (e.g., “match the shape of
install/lib/redis.sh”).
6. Pull request checklist
Before opening a PR:
- Branch off
main, namedfeat/N-…,fix/N-…, orchore/N-…. - Commit message references the issue:
fix: short description (closes #42). - CI is green (shellcheck + bats + integration).
CHANGELOG.mdhas an entry under## [Unreleased](or the new version heading if you’re cutting a release).VERSIONis bumped per semver if this is a release PR.- For any change that affects
install-stack.shor anything it sources: runtest/acceptance-*-run.shonsg10.codetot.org(or another real Ubuntu host) and paste the result in the PR. Docker can’t see everything — see Real-Ubuntu acceptance findings for why.
7. Code review
Three review passes catch different things. Use them in order:
- CI — shellcheck (style + a handful of pattern bugs) + bats (unit behavior) + integration (real apt/systemctl path). Mechanical, runs on every push. Required.
- Pre-landing review —
/shipskill walks the diff againstmainand flags structural issues, security gotchas (SQL safety, trust-boundary violations, conditional side effects), and missing test coverage. Run before merging anything non-trivial. - Adversarial review — a second Claude pass (or codex CLI) hunting specifically for edge cases the implementer missed. Worth the few minutes for any state-changing script — it’s how we caught the harden-fail2ban port bug in v0.6.0.
Small fixes (typo, comment, single-line change) can skip the latter two. Anything that touches state on a real host should not.
Questions? Open an issue, or look at the existing install/lib/*.sh and
harden/*.sh scripts — they’re the canonical examples for almost every
pattern this guide describes.