Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

AltStyle によって変換されたページ (->オリジナル) /