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 100+ command bash CLI orchestration tool routing through a single dispatcher (scripts/sw). Each command is a dedicated script (scripts/sw-<name>.sh) registered in the case block of the router. The project enforces Bash 3.2 compatibility, set -euo pipefail, atomic writes, and a parallel test file per command script.

ping is a connectivity probe — the simplest possible command: no arguments required, one line of output (pong), exit 0. It also serves as a canary: if shipwright ping works, the CLI installation path and symlink resolution are functional.

Constraints:

  • Must follow the identical structure to sw-hello.sh (established precedent, verified working)
  • Router insertion must land before *) catch-all (line 608), after hello) (line 607)
  • package.json test chain is alphabetically-ordered between sw-patrol-meta-test.sh and sw-pipeline-composer-test.sh
  • Bash 3.2 — no associative arrays, no ${var,,}, no readarray
  • VERSION must be 3.2.4 (matches package.json and all other scripts)

Component Diagram

┌──────────────────────────────────────────────────────┐
│  User / Caller                                       │
│  $ shipwright ping                                   │
└──────────────────────┬───────────────────────────────┘
                       │ argv: ["ping"]
                       ▼
┌──────────────────────────────────────────────────────┐
│  scripts/sw  (Router)                                │
│  main() → case "$cmd" in                            │
│    hello) exec sw-hello.sh                          │
│    ping)  exec sw-ping.sh   ◄── NEW insertion       │
│    *)     error + exit 1                            │
└──────────────────────┬───────────────────────────────┘
                       │ exec (replaces process)
                       ▼
┌──────────────────────────────────────────────────────┐
│  scripts/sw-ping.sh  (Command)                       │
│  main() → case "${1:-}" in                          │
│    --help|-h  → show_help + exit 0                  │
│    --version  → echo VERSION + exit 0               │
│    ""         → echo "pong" + exit 0   ◄── core    │
│    *)         → error + show_help + exit 1          │
└──────────────────────┬───────────────────────────────┘
                       │ stdout: "pong"
                       ▼
┌──────────────────────────────────────────────────────┐
│  scripts/sw-ping-test.sh  (Test Suite)               │
│  Invokes sw-ping.sh directly (bypasses router)       │
│  6 assertions: output, exit0, --help, -h,            │
│                --version, invalid-option             │
│  PASS: N / FAIL: N → exit 0 or 1                    │
└──────────────────────────────────────────────────────┘

                  package.json (test registry)
┌──────────────────────────────────────────────────────┐
│  "test": "bash sw-patrol-meta-test.sh && \           │
│           bash sw-ping-test.sh && \          ◄── NEW │
│           bash sw-pipeline-composer-test.sh && ..."  │
└──────────────────────────────────────────────────────┘

Decision

Standalone script pattern — create scripts/sw-ping.sh as a self-contained command script, register it in the router's case block with exec, register its test file in package.json. This is identical to the sw-hello.sh pattern.

The main path is intentionally minimal:

"")   echo "pong"; exit 0 ;;

No helper library call, no color output — echo "pong" writes exactly pong\n to stdout. This keeps the output deterministic and testable with an exact string match (assert_equals "pong" "$output").


Interface Contracts

# sw-ping.sh public interface
# Input:  argv[0] — optional flag
# Output: stdout — exactly "pong" (no trailing whitespace beyond \n)
# Exit:   0 on success, 0 on --help/--version, 1 on unknown option

sw_ping()
  args: "" | "--help" | "-h" | "--version" | "-v" | <unknown>
  stdout:
    ""          → "pong\n"
    "--help"|"-h" → help text containing "USAGE"
    "--version"|"-v" → "3.2.4\n"
    <unknown>   → error message on stderr
  exit: 0 | 1
  errors: unknown-option → stderr, exit 1

# Router contract (scripts/sw)
  case "ping" → exec sw-ping.sh "$@"
  # exec replaces the shell process; no return value

# Test contract (sw-ping-test.sh)
  output_var=$("$SCRIPT_DIR/sw-ping.sh")
  assert: output_var == "pong"
  assert: exit_code == 0
  assert: --help output contains "USAGE"
  assert: -h output contains "USAGE"
  assert: --version output matches /^[0-9]+\.[0-9]+\.[0-9]+/
  assert: unknown option exits 1
  final: exit 0 iff FAIL == 0

Data Flow

Request path (no args):
  $ shipwright ping
  → scripts/sw main("ping")
  → case "ping" → exec scripts/sw-ping.sh
  → sw-ping.sh main("")
  → case "" → echo "pong" → exit 0
  → stdout: "pong\n", process exit status: 0

Request path (--help):
  → sw-ping.sh main("--help")
  → case "--help" → show_help → exit 0
  → stdout: help text, exit status: 0

Error path (unknown option):
  → sw-ping.sh main("--bogus")
  → case "*" → error "Unknown option: --bogus" (stderr) → show_help → exit 1
  → stdout: help text, stderr: error, exit status: 1

Direct invocation (test path — bypasses router):
  $ bash scripts/sw-ping.sh
  → same as request path without router hop

Error Boundaries

Component Errors it handles Propagation
sw-ping.sh Unknown CLI options error() to stderr + exit 1; ERR trap catches unexpected failures
scripts/sw Unknown command name error "Unknown command: ${cmd}" + exit 1 before ever reaching sw-ping.sh
sw-ping-test.sh Script missing/non-executable ERR trap fires (set -euo pipefail), test suite exits non-zero
package.json test chain Test suite exits non-zero && chaining propagates failure; npm test exits non-zero

The ERR trap in sw-ping.sh:

trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR

catches any unexpected non-zero exit within the script body and surfaces file + line number. It does not fire on the exit 1 in the *) case — that is an intentional controlled exit.


Alternatives Considered

  1. Inline the ping handler in the router (scripts/sw) — Pros: zero new files. Cons: breaks the modular convention every other command follows; logic in the router is harder to test in isolation; makes the router a god-object over time. Rejected.

  2. Re-export ping from a shared utility library — Pros: DRY if other commands need "echo + exit 0" pattern. Cons: premature abstraction; hello doesn't use a shared library for this either; one-off commands don't warrant a shared abstraction. Rejected.

  3. Add to an existing script (e.g., sw-hello.sh) — Pros: one fewer file. Cons: violates single responsibility; sw-hello.sh and sw-ping.sh have different semantic purposes and different test requirements. Rejected.


Implementation Plan

Files to create:

  • scripts/sw-ping.sh — command script following sw-hello.sh structure exactly
  • scripts/sw-ping-test.sh — 6-case test suite

Files to modify:

  • scripts/sw — insert at line 607 (between hello) block and *) catch-all):
    ping)
        exec "$SCRIPT_DIR/sw-ping.sh" "$@"
        ;;
  • package.json — insert bash scripts/sw-ping-test.sh && between sw-patrol-meta-test.sh and sw-pipeline-composer-test.sh in the test script chain

Dependencies: None new. lib/helpers.sh is optional (fallback stubs inline, same as hello).

Risk areas:

Risk Mitigation
Router insertion corrupts case syntax Insert exactly 3 lines (ping) / exec ... / ;;) before *), never after
Test captures empty output under set -euo pipefail Use output=$("$SCRIPT_DIR/sw-ping.sh") — subshell exit code does not propagate to parent under command substitution assignment; safe pattern confirmed in sw-hello-test.sh
--version output test is fragile if version bumped Use regex =~ ^[0-9]+\.[0-9]+\.[0-9]+ not exact string, same as sw-hello-test.sh:81
Invalid-option test under set -euo pipefail Use `...
package.json && chain insertion order Alphabetical: patrol-meta < ping < pipeline-composer — verify with sort

Validation Criteria

  • bash scripts/sw-ping.sh outputs exactly pong (no leading/trailing whitespace beyond newline) and exits 0
  • bash scripts/sw-ping.sh --help outputs text containing USAGE and exits 0
  • bash scripts/sw-ping.sh -h outputs text containing USAGE and exits 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
  • bash scripts/sw-ping-test.sh reports PASS: 6 / FAIL: 0 and exits 0
  • bash scripts/sw ping (via router) outputs pong and exits 0
  • npm test completes without failure (sw-ping-test.sh is registered and passes)
  • bash scripts/sw with no ping argument still routes to *) correctly (router not broken)

Clone this wiki locally