Skip to content

Pipeline Design inline

Seth Ford edited this page Mar 2, 2026 · 8 revisions

Design: Add a shipwright ping command that prints pong to stdout and exits 0

Context

Shipwright is a CLI tool with 100+ subcommands, each implemented as a standalone bash script (scripts/sw-<name>.sh) and dispatched via a central router (scripts/sw). The codebase enforces strict conventions: Bash 3.2 compatibility, set -euo pipefail, ERR trap, VERSION constant, fallback output helpers, and a parallel test file per command. Every command is registered in two places: the case statement in scripts/sw and the "test" script in package.json.

The goal is to add a shipwright ping command — a minimal connectivity check that prints pong to stdout and exits 0. The constraint is zero architectural novelty: this must be indistinguishable in structure from sw-hello.sh, which already serves as the canonical template for new commands.


Decision

Implement ping as a standalone script (scripts/sw-ping.sh) following the sw-hello.sh pattern exactly:

  • echo "pong" as the default action (no args)
  • --help/-h → heredoc help text, exit 0
  • --version/-v$VERSION, exit 0
  • Unknown args → error() + help, exit 1
  • Router entry: ping) case inserted in scripts/sw after the hello) block (lines 605–607), before the *) wildcard
  • Test suite: scripts/sw-ping-test.sh with 6 tests mirroring sw-hello-test.sh
  • Package.json: bash scripts/sw-ping-test.sh && inserted in the "test" script immediately after sw-hello-test.sh

Component Diagram

┌─────────────────────────────────────────────────────────────────┐
│  User                                                           │
│  shipwright ping [args]                                         │
└──────────────────┬──────────────────────────────────────────────┘
                   │ exec
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│  CLI Router  (scripts/sw)                                       │
│  case "$cmd" in                                                 │
│    hello)  exec sw-hello.sh  ← existing                        │
│    ping)   exec sw-ping.sh   ← NEW                             │
│    *)      error + exit 1                                       │
│  esac                                                           │
└──────────────────┬──────────────────────────────────────────────┘
                   │ exec (replaces router process)
                   ▼
┌─────────────────────────────────────────────────────────────────┐
│  sw-ping.sh                                                     │
│  ┌─────────────┐  ┌──────────────┐  ┌──────────────────────┐   │
│  │  show_help()│  │  main()      │  │  lib/helpers.sh      │   │
│  │  heredoc    │  │  case $1 in  │  │  (sourced if present)│   │
│  │  USAGE block│  │  ""  → pong  │  │  info/success/warn/  │   │
│  └─────────────┘  │  -h  → help  │  │  error fallbacks     │   │
│                   │  -v  → ver   │  └──────────────────────┘   │
│                   │  *   → err   │                              │
│                   └──────────────┘                              │
└─────────────────────────────────────────────────────────────────┘
                   ▲ direct invocation (test isolation)
                   │
┌─────────────────────────────────────────────────────────────────┐
│  sw-ping-test.sh                                                │
│  6 tests: output, exit 0, --help, -h, --version, invalid exit 1│
│  No mocks needed — script is pure stdout/exit code              │
└─────────────────────────────────────────────────────────────────┘
                   ▲
┌─────────────────────────────────────────────────────────────────┐
│  package.json "test" script                                     │
│  ...&& bash scripts/sw-hello-test.sh &&                        │
│         bash scripts/sw-ping-test.sh &&  ← NEW                 │
│         bash scripts/sw-hygiene-test.sh && ...                  │
└─────────────────────────────────────────────────────────────────┘

Interface Contracts

# sw-ping.sh — public interface
#
# Inputs:  $1 (optional) — one of: "", "--help", "-h", "--version", "-v", <unknown>
# Outputs: stdout — one of: "pong\n", help text, VERSION string, error message
# Exit:    0 on success (no args, --help, --version)
#          1 on unknown arg
#
# Error contract:
#   - ERR trap fires on any unexpected non-zero status, prints BASH_SOURCE:LINENO to stderr
#   - Unknown arg: error() writes to stderr, show_help() to stdout, exits 1
#   - No network calls, no file writes — pure stdout/exit behavior

main()      # entry point, called with "$@"
show_help() # prints USAGE block to stdout, no args, no return value

# scripts/sw router contract (ping case):
#   Input:  cmd="ping", remaining "$@" forwarded
#   Effect: exec replaces router process with sw-ping.sh — no return
#   Error:  if sw-ping.sh missing, exec fails → bash error to stderr

# sw-ping-test.sh:
#   assert_equals  expected actual description → increments PASS or FAIL, prints result
#   assert_exit_code expected actual description → same
#   Exit: 0 if FAIL==0, 1 otherwise

Data Flow

shipwright ping
      │
      ▼  scripts/sw parses $1 as cmd="ping"
      │  exec scripts/sw-ping.sh (remaining args forwarded)
      │
      ▼  sw-ping.sh main() evaluates ${1:-}
         case "":
           echo "pong"   → stdout
           exit 0
         case "--help"|"-h":
           show_help()   → stdout (heredoc)
           exit 0
         case "--version"|"-v":
           echo "$VERSION" → stdout
           exit 0
         case *:
           error "Unknown option: $1" → stderr
           show_help()   → stdout
           exit 1

Error Boundaries

Component Errors handled Propagation
sw-ping.sh main() Unknown args → stderr + exit 1 Terminal — no caller
sw-ping.sh ERR trap Unexpected non-zero (set -euo pipefail) Prints $BASH_SOURCE:$LINENO to stderr, script exits non-zero
scripts/sw router exec failure (missing script) Bash emits error to stderr, exits non-zero
sw-ping-test.sh Test assertion failures Accumulated in $FAIL, non-zero exit at end

No error propagates silently — every failure path writes to stderr and exits non-zero.


Alternatives Considered

  1. Inline in scripts/sw router — Pros: zero new files, single-line change. Cons: untestable in isolation (no script path to invoke directly), breaks the architectural contract that every command is a separate file, no --help/--version surface. Rejected: architectural debt with no benefit.

  2. Shared library function — Pros: reusable pattern for trivial commands. Cons: premature abstraction — one command doesn't justify a new library. sw-hello.sh already exists and isn't extracted to a library either. Rejected: over-engineering.


Implementation Plan

Files to create:

  • scripts/sw-ping.sh — command implementation (≈67 lines, mirroring sw-hello.sh)
  • scripts/sw-ping-test.sh — test suite (≈108 lines, mirroring sw-hello-test.sh)

Files to modify:

  • scripts/sw — insert ping) case after hello) at line 607, before *) wildcard
  • package.json — insert bash scripts/sw-ping-test.sh && in "test" string after sw-hello-test.sh

Dependencies: None. No new packages, no new libraries.

Risk areas:

Risk Mitigation
Router wildcard *) catches ping if case is misplaced Insert strictly after hello) block, before *) — verified by bash scripts/sw ping smoke test
package.json test string is a single long line — edit must be surgical Use Edit tool with exact old_string/new_string containing sw-hello-test.sh as anchor
CLAUDE.md AUTO:core-scripts and AUTO:test-suites sections become stale Acceptable — pipeline patrol handles doc sync; not a build blocker
echo "pong" vs printf "pong\n" — output capture with $() strips trailing newline Both produce identical result when captured; echo is consistent with sw-hello.sh

Validation Criteria

  • bash scripts/sw-ping.sh outputs exactly pong (one line, no extra whitespace) to stdout and exits 0
  • bash scripts/sw ping outputs exactly pong and exits 0 (router dispatch works)
  • bash scripts/sw-ping.sh --help and -h both output text containing USAGE and exit 0
  • bash scripts/sw-ping.sh --version outputs a semver string matching ^[0-9]+\.[0-9]+\.[0-9]+ and exits 0
  • bash scripts/sw-ping.sh --invalid exits 1 and writes error to stderr
  • bash scripts/sw-ping-test.sh reports PASS: 6 FAIL: 0
  • bash scripts/sw-hello-test.sh still reports PASS: 6 FAIL: 0 (no regression)
  • sw-ping-test.sh appears in package.json "test" script and npm test does not fail on it

Clone this wiki locally