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 orchestration tool with 100+ subcommands, each implemented as a standalone bash script (scripts/sw-<name>.sh) dispatched by a central router (scripts/sw). The ping command is a liveness probe — a minimal diagnostic that confirms the CLI is on $PATH, executable, and returns a known exit code. This pattern is well-established in networked systems and developer tooling (Docker, kubectl, etc.) as the simplest possible health check.

Constraints from the codebase:

  • All scripts must be Bash 3.2 compatible (no associative arrays, no ${var,,}, no readarray)
  • set -euo pipefail and an ERR trap are mandatory on every script
  • VERSION="3.2.4" must appear at the top of every script and stay in sync with package.json
  • The router (scripts/sw) dispatches via a case statement using exec — new commands get one case entry immediately before the *) wildcard at line ~608
  • Test files mirror implementation files 1:1 (sw-ping-test.shsw-ping.sh)
  • Test suites are registered in package.json's "test" script as a &&-chained bash invocation sequence

Component Diagram

┌──────────────────────────────────────────────────────────────┐
│  User Shell                                                   │
│  $ shipwright ping                                            │
└─────────────────────┬────────────────────────────────────────┘
                      │ exec via PATH
                      ▼
┌──────────────────────────────────────────────────────────────┐
│  Router  scripts/sw                                           │
│  main() → case "$cmd" in                                      │
│    ping)  exec "$SCRIPT_DIR/sw-ping.sh" "$@"  ←── NEW        │
│    hello) exec "$SCRIPT_DIR/sw-hello.sh" "$@"                 │
│    *)     error + exit 1                                      │
│  esac                                                         │
└─────────────────────┬────────────────────────────────────────┘
                      │ exec (replaces router process)
                      ▼
┌──────────────────────────────────────────────────────────────┐
│  Command  scripts/sw-ping.sh                                  │
│  main() → case "${1:-}" in                                    │
│    "")         echo "pong" && exit 0      ← happy path        │
│    --help|-h)  show_help && exit 0                            │
│    --version)  echo "$VERSION" && exit 0                      │
│    *)          error + show_help && exit 1                    │
│  esac                                                         │
└──────────────────────────────────────────────────────────────┘
                      │ independent test execution
                      ▼
┌──────────────────────────────────────────────────────────────┐
│  Test Suite  scripts/sw-ping-test.sh                          │
│  Calls sw-ping.sh directly (not via router)                   │
│  6 assertions: output, exit code, --help, -h, --version,     │
│  invalid option                                               │
│  Reports PASS/FAIL counts; exits 1 if any FAIL > 0           │
└──────────────────────────────────────────────────────────────┘

Decision

Implement ping as a standalone script (scripts/sw-ping.sh) registered in the central router, following the identical pattern as sw-hello.sh. The command has a single responsibility: print pong\n to stdout and exit 0 when called with no arguments.

Specific patterns applied:

  • main() uses a case "${1:-}" switch — the :- default prevents set -u from aborting when no argument is passed
  • exec in the router replaces the router process with the command process — no subprocess overhead, no double-ERR-trap noise
  • Helper fallbacks (info(), success(), etc.) are inlined as one-liners, sourced conditionally from lib/helpers.sh, so the script works both standalone and within the full CLI
  • ((PASS++)) arithmetic in the test file is safe under pipefail on Bash 3.2 — confirmed by the existing sw-hello-test.sh which uses this exact pattern at lines 15 and 29
  • Test file uses inline assert_equals/assert_exit_code helpers (not lib/test-helpers.sh) — matching the sw-hello-test.sh self-contained pattern

Interface Contracts

# sw-ping.sh — Public interface

# Invocation (no args): happy path
sw-ping.sh
# stdout: "pong\n"
# stderr: (empty)
# exit:   0

# Invocation (--help or -h): help text
sw-ping.sh --help | -h
# stdout: help text containing "USAGE"
# stderr: (empty)
# exit:   0

# Invocation (--version): version string
sw-ping.sh --version
# stdout: semver string matching /^[0-9]+\.[0-9]+\.[0-9]+/
# stderr: (empty)
# exit:   0

# Invocation (unknown flag): error path
sw-ping.sh --anything-else
# stdout: help text
# stderr: error message "Unknown option: ..."
# exit:   1

# Router contract
# scripts/sw dispatches: exec "$SCRIPT_DIR/sw-ping.sh" "$@"
# The router passes ALL remaining args verbatim — sw-ping.sh owns its own arg parsing
# sw-ping-test.sh — Test harness interface

# Outputs to stdout: colored PASS/FAIL lines + "PASS: N" + "FAIL: N"
# Exit code: 0 if FAIL==0, else 1
# No external dependencies — self-contained (no lib/test-helpers.sh)
# Direct invocation: bash scripts/sw-ping-test.sh

Data Flow

Request path (no args):
  User → shell → scripts/sw [cmd=ping] → exec sw-ping.sh → main("") → echo "pong" → stdout → exit 0

Request path (--help):
  User → shell → scripts/sw [cmd=ping, args=--help] → exec sw-ping.sh --help
    → main("--help") → show_help() → stdout → exit 0

Error path (unknown flag):
  User → shell → scripts/sw [cmd=ping, args=--bogus] → exec sw-ping.sh --bogus
    → main("--bogus") → error() [stderr] + show_help() [stdout] → exit 1

Test execution path (isolated):
  npm test → bash sw-ping-test.sh
    → sw-ping.sh (direct, no router)
    → capture stdout + exit code
    → assert_equals / assert_exit_code → PASS/FAIL counters → exit 0|1

Error Boundaries

Component Errors It Handles Errors It Propagates
scripts/sw (router) Unknown command name → error + exit 1 Passes all args verbatim to sw-ping.sh; does not catch ping errors
sw-ping.sh Unknown flag → error + exit 1; missing helpers (test env) → fallback helpers ERR trap catches unexpected failures; exits with bash error code
sw-ping-test.sh Assertion failures → FAIL++ + diagnostic message; never aborts mid-suite set -euo pipefail still active — unexpected script errors exit immediately with line/status

Key boundary: The router uses exec (not a subshell), so the ping script's exit code becomes the router's exit code directly. There is no error translation layer between them.


Alternatives Considered

  1. Inline in router (scripts/sw) — Pros: No new file, zero dispatch overhead. Cons: Violates the single responsibility of the router (it routes, not executes); breaks testability (can't call sw-ping.sh directly); deviates from every other command; makes the router grow without bound. Rejected.

  2. Shared ping function in lib/helpers.sh — Pros: Reusable if other scripts wanted a liveness check. Cons: Premature abstraction — nothing else needs ping; lib/helpers.sh is for output/event utilities, not commands; adds coupling to a shared library that has no other command logic. Rejected.

  3. shipwright ping as a router alias for shipwright hello — Pros: Near-zero implementation. Cons: Wrong output (hello worldpong), wrong semantics, confusing to users, not independently testable. Rejected.


Implementation Plan

Files to create:

File Purpose
scripts/sw-ping.sh The ping command implementation (~67 lines, mirrors sw-hello.sh)
scripts/sw-ping-test.sh 6-test suite (~108 lines, mirrors sw-hello-test.sh)

Files to modify:

File Change Location
scripts/sw Add ping) case before *) wildcard Lines 605–607 (insert before line 608)
package.json Append && bash scripts/sw-ping-test.sh to "test" script Line 39

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

Risk areas:

  1. Router insertion pointscripts/sw is 617 lines. The *) wildcard is at line 608. A mis-placed insertion (e.g., inside a nested case block like the version sub-case at lines 591–601) would silently mis-route ping. Mitigation: insert immediately after the hello) block at line 607, matching the existing pattern exactly.

  2. set -euo pipefail + arithmetic((PASS++)) returns exit code 1 when the result is 0 (i.e., on the very first increment from 0). This is not an issue here because PASS starts at 0 and ((PASS++)) only runs on the success branch of an if block — the outer if already exited the block. Confirmed safe in sw-hello-test.sh. However, a bare ((PASS++)) as a standalone statement at the top level would abort under pipefail if PASS were 0 — never do this outside a conditional.

  3. package.json test chain orderingnpm test is a single && chain. If sw-ping-test.sh is appended after the last existing test, a failure in any prior test will skip it; conversely, a failure in sw-ping-test.sh stops subsequent tests. This is acceptable and consistent with how all other tests are registered.

  4. Output exactness — The test asserts output == "pong" with assert_equals. If lib/helpers.sh is on the path and info() or other helpers emit prefix characters before echo "pong", the test will fail. Mitigation: echo "pong" must be a bare echo, not wrapped in any helper function.


Validation Criteria

  • bash scripts/sw-ping-test.sh exits 0 with output PASS: 6 / FAIL: 0
  • scripts/sw-ping.sh (direct) prints exactly pong to stdout (no ANSI codes, no prefix characters)
  • scripts/sw-ping.sh exits 0 on no-arg invocation; exits 1 on unknown flag
  • shipwright ping (via router) prints pong and exits 0 — confirms router dispatch is wired correctly
  • shipwright ping --help prints text containing USAGE and exits 0
  • shipwright ping --version prints a semver string matching /^[0-9]+\.[0-9]+\.[0-9]+/ and exits 0
  • shipwright ping --bogus exits 1 and emits an error to stderr
  • npm test passes in full (no regressions in the 100+ existing test suites)
  • shipwright version check exits 0 — confirms VERSION="3.2.4" in sw-ping.sh matches package.json
  • shellcheck scripts/sw-ping.sh produces no errors (disable SC2034 for VERSION if needed, matching sw-hello.sh line 7)

Clone this wiki locally