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 exposes 100+ CLI commands via a central router (scripts/sw) that dispatches to standalone bash scripts using exec. The project needs a ping command to serve as a reachability/liveness probe — callers invoke shipwright ping, expect exactly pong on stdout, and rely on exit code 0 to confirm the CLI is installed and on PATH.

Constraints from the codebase:

  • All shell scripts must be Bash 3.2 compatible (no declare -A, no readarray, no ${var,,})
  • All scripts use set -euo pipefail with an ERR trap
  • VERSION must match package.json (currently 3.2.4)
  • Output to stdout must be clean and undecorated — no ANSI codes, no prefix characters on the primary response line
  • Every new command requires a parallel test file registered in package.json's "test" script
  • Router (scripts/sw) dispatches via a case statement using exec — inline command logic in the router is explicitly against project convention

Component Diagram

┌─────────────────────────────────────────────────────────┐
│  CLI Layer                                               │
│                                                          │
│  scripts/sw  ─── case "ping)" ──exec──►  sw-ping.sh     │
│  (router)                               (command)        │
└─────────────────────────────────────────────────────────┘
        │                                      │
        │                               ┌──────▼──────────┐
        │                               │  stdout: "pong" │
        │                               │  exit: 0        │
        │                               └─────────────────┘
        │
┌───────▼────────────────────────────────────────────────┐
│  Test Layer                                             │
│                                                         │
│  sw-ping-test.sh  ──invokes──►  sw-ping.sh directly    │
│  (6 test cases)               (bypasses router)         │
│       │                                                  │
│       └── registered in package.json "test" script      │
└─────────────────────────────────────────────────────────┘

Decision

Standalone script patternscripts/sw-ping.sh is a self-contained bash script registered in the router with a two-line case entry. It follows the identical structure as scripts/sw-hello.sh (the most recent precedent), which is the canonical model for simple commands.

Output contract: echo "pong" with no ANSI wrapping, no info()/success() helper decorators, followed by explicit exit 0. The helpers are sourced as fallbacks (required by the template) but never called on the happy path — the response is a bare printable string, not a status message.

Option parsing: A case "${1:-}" block handles --help|-h, --version|-v, "" (happy path), and * (unknown → exit 1). This matches sw-hello.sh:44-65 exactly.


Interface Contracts

# sw-ping.sh public interface
#
# Invocation:
#   sw-ping.sh [OPTION]
#
# Arguments:
#   (none)       → prints "pong" to stdout, exits 0
#   --help | -h  → prints USAGE block to stdout, exits 0
#   --version|-v → prints semver string (e.g. "3.2.4") to stdout, exits 0
#   <unknown>    → prints error to stderr, prints USAGE to stdout, exits 1
#
# Stdout contract (no-arg case):
#   Exactly: "pong\n"   (one line, no leading/trailing whitespace, no ANSI)
#
# Exit codes:
#   0  → success (all valid invocations)
#   1  → unknown option passed
#
# Error contract:
#   Errors written to stderr via error() helper
#   ERR trap fires on unexpected non-zero exits (bash error, not logic error)
# Router registration contract (scripts/sw, line 607)
#   ping)
#       exec "$SCRIPT_DIR/sw-ping.sh" "$@"
#       ;;
#
# Precondition:  sw-ping.sh is executable and on the filesystem
# Postcondition: all args forwarded verbatim; process replaced via exec

Alternatives Considered

  1. Inline in router (scripts/sw case body) — Pros: zero new files, no exec overhead. Cons: violates the single-responsibility boundary that keeps the router as pure dispatch; not independently testable; creates precedent for logic in the router. Rejected as architectural debt — the existing 100+ commands all follow the standalone pattern.

  2. Shared utility function in lib/helpers.sh — Pros: reusable if other commands need a liveness probe. Cons: helpers.sh is for output/event utilities, not command implementations; premature abstraction for a one-liner. Rejected — no other consumer exists.

  3. printf "pong\n" instead of echo "pong" — Pros: portable across shells. Cons: echo is used in all existing commands including sw-hello.sh:56; both are POSIX; echo is idiomatic in this codebase. Not chosen but not a risk — either works identically.


Data Flow

User invokes: shipwright ping
        │
        ▼
scripts/sw main()
  └── parse $1 → "ping"
  └── case "ping)" → exec scripts/sw-ping.sh "$@"
                              │
                              ▼
                      sw-ping.sh main()
                        case "${1:-}" → ""  (no args)
                          echo "pong"   ──────────────► stdout fd 1
                          exit 0        ──────────────► $? = 0
User invokes: shipwright ping --invalid
        │
        ▼
        ... (same router path) ...
                      sw-ping.sh main()
                        case "${1:-}" → "--invalid"
                          error "Unknown option: --invalid"  ──► stderr fd 2
                          show_help                          ──► stdout fd 1
                          exit 1        ──────────────────────► $? = 1

Error Boundaries

Layer Errors Handled Propagation
sw-ping.sh ERR trap Unexpected bash errors (command not found, arithmetic error) Prints ERROR: $BASH_SOURCE:$LINENO to stderr, exits non-zero
sw-ping.sh option parse Unknown CLI flags (* case) Writes to stderr via error(), exits 1
scripts/sw router Unknown command name error "Unknown command", exits non-zero (existing behavior, not modified)
Test runner Script exit code ≠ expected assert_exit_code increments FAIL counter; final [[ $FAIL -eq 0 ]] propagates to npm test exit code

Explicit non-error: set -euo pipefail means any unguarded non-zero exit within sw-ping.sh will fire the ERR trap. The only non-zero intentional exit is the exit 1 in the *) branch — all other code paths exit 0.


Implementation Plan

  • Files to create:

    • scripts/sw-ping.sh — command implementation (mirrors sw-hello.sh structure)
    • scripts/sw-ping-test.sh — 6-case test suite (mirrors sw-hello-test.sh structure)
  • Files to modify:

    • scripts/sw — insert 2 lines at line 607 (after hello) block, before *) catch-all)
    • package.json — insert bash scripts/sw-ping-test.sh && after sw-hello-test.sh in the "test" script (line 39)
  • Dependencies: None. No new packages, no new libraries.

  • Risk areas:

    • Router insertion pointscripts/sw is 617 lines. The hello) entry is at line 605-607. The *) catch-all immediately follows. Inserting before *) is the critical edit; a misplaced entry would leave ping unreachable or break the router's fallthrough.
    • package.json test chain — The "test" script is a single long &&-chained string on line 39. Adding sw-ping-test.sh after sw-hello-test.sh must maintain valid shell syntax; a missing && or trailing && will break all subsequent tests.
    • Exit code capture in test — The test_ping_exit_code case runs "$SCRIPT_DIR/sw-ping.sh" > /dev/null 2>&1 and checks $?. Under set -euo pipefail, a non-zero exit here would abort the test script before the assertion fires. The || true guard is not needed because exit 0 is the contract; but the invalid option test uses the || local exit_code=$? pattern (copied from sw-hello-test.sh:92) which is correct for capturing non-zero exits without aborting.

Validation Criteria

  • bash scripts/sw-ping.sh outputs exactly pong (one line, no trailing spaces, no ANSI)
  • bash scripts/sw-ping.sh exits with code 0
  • bash scripts/sw-ping.sh --help output contains the string USAGE
  • bash scripts/sw-ping.sh -h output contains the string USAGE
  • bash scripts/sw-ping.sh --version output matches ^[0-9]+\.[0-9]+\.[0-9]+
  • bash scripts/sw-ping.sh --invalid exits with code 1
  • bash scripts/sw ping outputs pong (router dispatch works end-to-end)
  • bash scripts/sw-ping-test.sh exits 0 with PASS: 6 FAIL: 0
  • package.json "test" script includes sw-ping-test.sh as a &&-chained entry
  • scripts/sw-ping.sh contains set -euo pipefail, ERR trap, and VERSION="3.2.4"
  • No Bash 4.x features present (no declare -A, no ${var,,}, no readarray)

Clone this wiki locally