Skip to content

Pipeline Design inline

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

Now I have everything I need to produce the ADR.


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

Context

Shipwright is a bash-based CLI tool with 100+ commands dispatched through a central router (scripts/sw) via a case statement. Each command is a standalone bash script (scripts/sw-<name>.sh) with a paired test file (scripts/sw-<name>-test.sh). The sw-hello.sh command (67 lines) is the established template for simple diagnostic/utility commands and is the direct model for this implementation.

The ping command serves a concrete operational purpose: it provides the simplest possible liveness probe. shipwright ping can be called by health-check scripts, CI pipelines, and orchestration layers to verify the CLI binary is installed and executable without invoking any external dependencies (GitHub API, tmux, database, etc.).

Constraints from the codebase:

  • All scripts must be Bash 3.2 compatible — no declare -A, no ${var,,}, no readarray
  • set -euo pipefail is mandatory in every script
  • Output must be plain echo to stdout; no ANSI color in the primary response payload (color helpers are for diagnostics, not protocol output)
  • The router dispatches via exec — the case entry must be placed before the *) default at scripts/sw:608
  • Test suites must be registered in the npm test chain in package.json

Decision

Standalone script pattern, structurally identical to sw-hello.sh.

The command is implemented as scripts/sw-ping.sh with:

  • echo "pong" as the sole output on the happy path (no color wrappers — the response is a protocol token, not a user message)
  • exit 0 (implicit from echo succeeding under set -euo pipefail)
  • --help/-h and --version/-v flags (consistent with every other command; omitting them would make ping the only command that doesn't respond to --help)
  • Unknown-argument guard with exit 1

The router entry is inserted at scripts/sw:607 (after hello), before *)):

ping)
    exec "$SCRIPT_DIR/sw-ping.sh" "$@"
    ;;

Component Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        shipwright CLI                           │
│                                                                 │
│   User / CI / Health-check script                               │
│         │                                                       │
│         ▼                                                       │
│   ┌──────────────┐   exec    ┌─────────────────────────────┐   │
│   │  scripts/sw  │──────────▶│     scripts/sw-ping.sh      │   │
│   │  (router)    │           │  ┌───────────────────────┐  │   │
│   │              │           │  │  arg parser (case)    │  │   │
│   │  case "ping" │           │  │  - "" → echo "pong"   │  │   │
│   │              │           │  │  - --help/-h → help   │  │   │
│   └──────────────┘           │  │  - --version/-v → ver │  │   │
│                               │  │  - * → error + exit 1 │  │   │
│   ┌──────────────────────┐   │  └───────────────────────┘  │   │
│   │ scripts/sw-ping-     │   │                             │   │
│   │ test.sh              │   │  Deps: lib/helpers.sh        │   │
│   │  (6 test cases)      │   │  (optional, fallback inline) │   │
│   └──────────────────────┘   └─────────────────────────────┘   │
│          ▲                                                       │
│   npm test chain (package.json)                                  │
└─────────────────────────────────────────────────────────────────┘

Component responsibilities — one reason to change each:

Component Single Responsibility Changes When
scripts/sw Route command tokens to scripts New commands are added / removed
scripts/sw-ping.sh Print pong and exit Ping behavior changes
scripts/sw-ping-test.sh Verify ping contract Test cases added or ping contract changes
package.json test script Enumerate all test suites Test suite files added / removed

Interface Contracts

# ── sw-ping.sh public interface ─────────────────────────────────────────────

# Invocation forms:
#   sw-ping.sh              → stdout: "pong\n"    exit: 0
#   sw-ping.sh --help       → stdout: <help text>  exit: 0
#   sw-ping.sh -h           → stdout: <help text>  exit: 0
#   sw-ping.sh --version    → stdout: "<semver>\n" exit: 0
#   sw-ping.sh -v           → stdout: "<semver>\n" exit: 0
#   sw-ping.sh <unknown>    → stderr: error msg    exit: 1

# Preconditions:
#   - bash >= 3.2 available at /usr/bin/env bash
#   - lib/helpers.sh may or may not be present (graceful fallback required)

# Postconditions (happy path):
#   - Exactly the string "pong" followed by a newline written to stdout
#   - Nothing written to stderr
#   - Exit status 0

# Error contract:
#   - Exit status 1 on unrecognized argument
#   - Error message to stderr (not stdout)
#   - No side effects (no files written, no network calls, no env mutations)

# ── Router contract (scripts/sw lines 605-613) ──────────────────────────────

# Input: $1 == "ping"
# Effect: exec replaces the router process with sw-ping.sh
#         All remaining argv ($@) forwarded to sw-ping.sh
# No return: exec never returns on success

Data Flow

stdin  ──(not read)──▶  sw-ping.sh
argv   ──────────────▶  arg parser (case statement)
                               │
                    ┌──────────┴──────────┐
                    │ "" (no args)        │ --help/-h   │ --version/-v  │ *
                    ▼                     ▼             ▼               ▼
               echo "pong"          cat <<EOF       echo $VERSION   error() >&2
               to stdout            to stdout       to stdout       + show_help
               exit 0               exit 0          exit 0          exit 1

There is no state read or written. No files. No environment variables queried beyond BASH_SOURCE (for SCRIPT_DIR). No network. This is a pure function: deterministic, zero side effects, zero external dependencies.


Alternatives Considered

  1. Inline in router (scripts/sw) — Pros: zero new files. Cons: violates every established convention; untestable in isolation; sets a precedent that degrades the architecture for every future simple command; inconsistent with hello, which is structurally identical in complexity. Rejected.

  2. Shared "simple command" library — Pros: DRY across hello and ping. Cons: premature abstraction — two commands don't justify a shared library; adds indirection with no benefit; forces future commands into a rigid template. Rejected.

  3. printf instead of echo — Pros: more portable across edge-case /bin/sh interpreters. Cons: the entire codebase uses echo; this file is #!/usr/bin/env bash so portability is not a concern; printf with no format string is unconventional. Rejected — use echo "pong".


Implementation Plan

Files to create:

File Lines (est.) Purpose
scripts/sw-ping.sh ~67 Command implementation (mirrors sw-hello.sh structure)
scripts/sw-ping-test.sh ~108 6-case test suite (mirrors sw-hello-test.sh structure)

Files to modify:

File Location Change
scripts/sw After line 607 (after hello) block, before *)) Add ping) exec "$SCRIPT_DIR/sw-ping.sh" "$@" ;;
package.json "test" script string Append && bash scripts/sw-ping-test.sh to the test chain

Dependencies: None. No new packages, libraries, or external tools.

Risk areas:

Risk Severity Mitigation
((PASS++)) under set -euo pipefail evaluates to exit 1 when result is 0 High Use ((PASS++)) || true OR restructure as PASS=$((PASS + 1)) — matches sw-hello-test.sh exactly which already handles this
ERR trap in test file fires on intentional non-zero exits High No ERR trap in sw-ping-test.sh — capture exit codes with cmd || local ec=$? pattern (as done in sw-hello-test.sh:92)
Router *) default catches ping if case order is wrong Medium Insert ping) block at lines 607-609, confirmed before *) at line 608
ANSI color codes in pong output break CI assertions Medium echo "pong" — no color wrappers; color helpers are for diagnostics only
lib/helpers.sh not found in test environments Low Inline fallback functions already established in sw-hello.sh:18-21; copy verbatim

Error Boundaries

Component Errors it owns Propagation
scripts/sw-ping.sh — arg parser Unrecognized flags → error() to stderr + exit 1 Terminal; caller observes exit code
scripts/sw-ping.sh — ERR trap Unexpected bash errors (e.g., echo fails on broken stdout) Prints ERROR: file:line status to stderr; exits non-zero
scripts/sw — router ping token not in case → *) handler, exit 1 Terminal; would only occur if case entry is missing — covered by integration test
scripts/sw-ping-test.sh Assertion failures increment $FAIL; final exit 1 if FAIL > 0 CI sees non-zero exit; npm test fails

Error non-propagation by design: sw-ping.sh has no network calls, no file I/O, no subprocess spawning. The only possible failures are (a) broken stdout (environment issue, not command bug) or (b) unexpected argv (user error). Both terminate immediately with a non-zero exit. There is no retry logic, no partial state, and no cleanup required.


Validation Criteria

  • bash scripts/sw-ping.sh outputs exactly pong (one line, no trailing spaces, no ANSI codes) and exits 0
  • bash scripts/sw-ping.sh --help outputs text containing USAGE and exits 0
  • bash scripts/sw-ping.sh -h same as --help
  • bash scripts/sw-ping.sh --version outputs a string matching ^[0-9]+\.[0-9]+\.[0-9]+ and exits 0
  • bash scripts/sw-ping.sh --invalid exits 1 and writes to stderr (not stdout)
  • bash scripts/sw-ping-test.sh reports PASS: 6 FAIL: 0 and exits 0
  • bash scripts/sw ping outputs pong and exits 0 (router integration)
  • npm test exits 0 with all suites passing (regression gate)
  • No new files other than sw-ping.sh and sw-ping-test.sh are created
  • VERSION in sw-ping.sh matches package.json "version" field (currently 3.2.4)

Clone this wiki locally