-
Notifications
You must be signed in to change notification settings - Fork 1
Pipeline Design inline
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), afterhello)(line 607) -
package.jsontest chain is alphabetically-ordered betweensw-patrol-meta-test.shandsw-pipeline-composer-test.sh - Bash 3.2 — no associative arrays, no
${var,,}, noreadarray -
VERSIONmust be3.2.4(matchespackage.jsonand all other scripts)
┌──────────────────────────────────────────────────────┐
│ 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 && ..." │
└──────────────────────────────────────────────────────┘
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").
# 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
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
| 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.
-
Inline the
pinghandler 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. -
Re-export
pingfrom a shared utility library — Pros: DRY if other commands need "echo + exit 0" pattern. Cons: premature abstraction;hellodoesn't use a shared library for this either; one-off commands don't warrant a shared abstraction. Rejected. -
Add to an existing script (e.g.,
sw-hello.sh) — Pros: one fewer file. Cons: violates single responsibility;sw-hello.shandsw-ping.shhave different semantic purposes and different test requirements. Rejected.
Files to create:
-
scripts/sw-ping.sh— command script followingsw-hello.shstructure exactly -
scripts/sw-ping-test.sh— 6-case test suite
Files to modify:
-
scripts/sw— insert at line 607 (betweenhello)block and*)catch-all):ping) exec "$SCRIPT_DIR/sw-ping.sh" "$@" ;;
-
package.json— insertbash scripts/sw-ping-test.sh &&betweensw-patrol-meta-test.shandsw-pipeline-composer-test.shin thetestscript 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
|
-
bash scripts/sw-ping.shoutputs exactlypong(no leading/trailing whitespace beyond newline) and exits 0 -
bash scripts/sw-ping.sh --helpoutputs text containingUSAGEand exits 0 -
bash scripts/sw-ping.sh -houtputs text containingUSAGEand exits 0 -
bash scripts/sw-ping.sh --versionoutputs a semver string matching/^[0-9]+\.[0-9]+\.[0-9]+/and exits 0 -
bash scripts/sw-ping.sh --invalidexits 1 -
bash scripts/sw-ping-test.shreportsPASS: 6 / FAIL: 0and exits 0 -
bash scripts/sw ping(via router) outputspongand exits 0 -
npm testcompletes without failure (sw-ping-test.sh is registered and passes) -
bash scripts/swwith nopingargument still routes to*)correctly (router not broken)