Shell Scripting Conventions
Operational scripts should read like small programs. The main path should describe the job being performed, while mechanics live in named helpers or themed utility files.
Use this page when adding or refactoring:
- Shell scripts under
bin/. - App-owned
bin/scripts. - Repo-owned operational entrypoints such as Docker init scripts.
- Sourced shell utility files that support operational scripts.
For public command names, compatibility scripts, wrapper entrypoints, and route contracts, use Wavemap CLI and Wavemap CLI Command Reference. This page owns script structure and utility placement.
Executable Scripts
Section titled “Executable Scripts”Executable scripts should start with strict mode where the language supports it, followed by a Script Manifest.
The manifest should briefly list:
Purpose: What the script does.Main Flow: The ordered operational steps.Helpers: The helper groups, local functions, or utility files that carry the mechanics.
Use visible section markers for the rest of the file:
# --- Script Manifest ---------------------------------------------------------# Purpose: ...# Main Flow:# - ...# Helpers:# - ...
# --- Paths -------------------------------------------------------------------# --- Shared Imports ----------------------------------------------------------# --- Constants ---------------------------------------------------------------# --- Defaults ----------------------------------------------------------------# --- State -------------------------------------------------------------------# --- Helpers -----------------------------------------------------------------# --- Main --------------------------------------------------------------------Not every script needs every section. Keep the sections that make the file easier to scan.
The main path belongs at the bottom. In shell scripts, prefer a main() function followed by:
main "$@"The main function should be a composed recipe, not a wall of mechanics. Prefer this shape:
main() { parse_args "$@" validate_inputs resolve_contracts print_plan run_when_requested}Support Files
Section titled “Support Files”Sourced shell files should use a Support Manifest instead of a Script Manifest.
Support files should not define a main function or perform operational work when loaded. They may define constants, helper functions, and imports that are safe at source time.
Use helper-group descriptions so readers can tell why the file exists without reading every helper:
# --- Support Manifest --------------------------------------------------------# Purpose: Shared local-Docker Compose helpers.# Helper Groups:# - Compose helpers: Build repeated docker compose commands.# - Path helpers: Resolve repo-relative paths used by app scripts.Utility Ownership
Section titled “Utility Ownership”Place utilities at the narrowest durable boundary:
| Location | Owns |
|---|---|
bin/utils/ | Truly common shell primitives used across multiple operational lanes. |
bin/dev-docker/utils/ | Local Docker workflow helpers. |
bin/dev-deploy/utils/ | Deployed-dev and CD shell helpers. |
infra/operations/src/ | Typed, tested operations planning, validation, rendering, and contract helpers. |
App-owned bin/ | Thin app launch, reset, or maintenance wrappers when the behavior belongs to one app. |
If a helper is implemented in multiple places with only small variations, consolidate it into the lowest shared utility directory that genuinely owns all call sites. If a utility file becomes a mixed bag, split it into a themed directory.
Keep typed parsing, validation, planning, projection, and deterministic summaries in @wavemap/operations when that
boundary already owns the behavior. Keep shell focused on live external execution, process control, provider CLIs,
wait loops, and explicit mutation gates.
Comments
Section titled “Comments”Prefer comments that explain operational boundaries, risk, or intent. Avoid comments that restate the next line.
Good comments answer questions such as:
- Why this step is dry-run only.
- Which command would mutate cloud, Docker, SSM, or runtime state.
- Which values are redacted and why.
- Which helper owns a contract boundary.
Mutation Gates
Section titled “Mutation Gates”Scripts that can mutate cloud resources, Docker state, SSM, runtime hosts, databases, media storage, or deployment state should make that boundary visible.
Current conventions:
- Prefer dry-run or plan output by default for mutating or cloud-aware operational scripts.
- Require an explicit live flag such as
--executebefore mutation. - Print the target environment, account/region/stack, wrapper purpose, and expected action before mutation when those facts are available.
- Keep secret values redacted in logs, summaries, fixtures, and generated helper files.
- Let GitHub Actions orchestrate checkout, credentials, permissions, artifacts, and summaries; keep command semantics in repo-owned scripts or typed helpers.
When a script changes public command behavior, also update the command reference and any related operation page or runbook. Use the Command Change Convention as the checklist.
Verification
Section titled “Verification”For script structure or utility refactors, start with:
pnpm verify:scriptsWhen deployed-dev scripts are touched, also run the relevant dry-run contracts through the public CLI:
pnpm wavemap -- deploy dev contractspnpm wavemap -- deploy dev imagespnpm wavemap -- deploy dev runtime deploypnpm wavemap -- deploy dev database resetThese checks should remain non-mutating unless an explicit live flag is provided.
If a script change affects workflow YAML, GitHub Actions local actions, environment variables, runtime config, or command documentation, add the narrow matching check:
pnpm verify:github-actionspnpm wavemap -- deploy dev validate-envpnpm wavemap -- deploy dev preflightpnpm -C apps/wavemap-docs buildUse Configuration And Secrets when the script change adds, renames, removes, or changes the meaning of an environment variable, build input, Pulumi output, GitHub value, or runtime parameter.