-
Notifications
You must be signed in to change notification settings - Fork 1
Pipeline Design inline
Now I have everything I need. Here is the ADR:
Shipwright exposes 100+ subcommands through a central CLI router (scripts/sw). Each command is a standalone bash script (scripts/sw-<name>.sh) dispatched via a case statement using exec. A hello command already exists as the canonical reference implementation of this pattern.
The ping command is a minimal health-check primitive: shipwright ping → prints pong to stdout, exits 0. It has no external dependencies, no side effects, and no state. The only constraints are:
- Must conform to the established Standalone Script Pattern (identical structure to
sw-hello.sh) - Must be Bash 3.2 compatible (no associative arrays, no
${var,,}, noreadarray) - Must use
set -euo pipefail+ ERR trap - Must carry
VERSION="3.2.4"to stay consistent with the rest of the codebase - Output must be exactly
pong— no color prefix, no ANSI escape codes — because tests do an exact-string comparison
Standalone Script Pattern — create scripts/sw-ping.sh as a structural clone of scripts/sw-hello.sh with a single behavioral change: the no-argument case prints echo "pong" instead of echo "hello world".
The router entry (ping) case in scripts/sw) is inserted before hello) at line 605. The *)catch-all at line 608 is the unreachable-command guard; any new entry must be placed before it.
The test file (scripts/sw-ping-test.sh) is a structural clone of scripts/sw-hello-test.sh — inline assert_equals/assert_exit_code helpers, 6 test functions, PASS/FAIL counters, exits 1 on any failure.
Note on package.json insertion point: The plan states insertion between sw-pipeline-vitals-test.sh and sw-pm-test.sh. This is incorrect. Alphabetically, ping (p-i-n) sorts before pipeline (p-i-p) because n < p. The correct insertion is between sw-patrol-meta-test.sh and sw-pipeline-composer-test.sh.
-
Inline the ping handler in the router (
scripts/sw) — Pros: zero new files, no exec overhead. Cons: violates Single Responsibility — the router is a dispatcher, not an implementer; breaks the test boundary (no independentbash scripts/sw-ping-test.shpath); against established architecture for all 100+ commands. Rejected. -
Add
pingas a shared library function — Pros: reusable if multiple scripts need a health check. Cons: premature abstraction for a one-line output; introduces a dependency where none exists; over-engineered for the requirement. Rejected.
| File | Lines | Purpose |
|---|---|---|
scripts/sw-ping.sh |
~67 | Command implementation — mirrors sw-hello.sh exactly, echo "pong" in no-arg case |
scripts/sw-ping-test.sh |
~108 | 6-test suite — mirrors sw-hello-test.sh exactly, asserts output pong
|
| File | Change |
|---|---|
scripts/sw |
Insert ping) exec "$SCRIPT_DIR/sw-ping.sh" "$@" ;; before hello) at line 605 |
package.json |
Insert bash scripts/sw-ping-test.sh && between sw-patrol-meta-test.sh and sw-pipeline-composer-test.sh (alphabetical order) |
None. No new packages, no new shared libraries.
| Risk | Affected location | Mitigation |
|---|---|---|
Router entry placed after *)
|
scripts/sw line 608 |
Insert at line 605 (before hello)), verified with grep -n 'hello|^\s*\*)' scripts/sw
|
pong output has trailing newline stripped |
test assert_equals "pong" "$output"
|
echo "pong" adds one \n; command substitution $() strips it — both sides are stripped equally, assertion holds |
| ERR trap fires during intentional exit 1 | test_ping_invalid_option |
Use ` |
VERSION drift |
sw-ping.sh header |
Hardcode VERSION="3.2.4" — same value as sw-hello.sh:8 and package.json
|
chmod not applied |
scripts/sw-ping.sh |
chmod +x immediately after creation, before any test run |
┌─────────────────────────────────────┐
│ scripts/sw │
│ (CLI Router) │
│ │
│ case "$cmd" in │
│ ping) exec sw-ping.sh "$@" ;; │◄── NEW (line 605, before hello)
│ hello) exec sw-hello.sh "$@" ;; │
│ *) error "Unknown cmd" ;; │
│ esac │
└──────────────┬──────────────────────┘
│ exec (replaces process)
▼
┌─────────────────────────────────────┐
│ scripts/sw-ping.sh │
│ (Command Handler) │
│ │
│ main() │
│ case "${1:-}" in │
│ --help|-h) show_help; exit 0 │
│ --version) echo $VERSION; │
│ exit 0 │
│ "") echo "pong"; │◄── CORE BEHAVIOR
│ exit 0 │
│ *) error; exit 1 │
│ esac │
└─────────────────────────────────────┘
▲さんかく
│ bash invocation
┌─────────────────────────────────────┐
│ scripts/sw-ping-test.sh │
│ (Test Harness) │
│ │
│ assert_equals "pong" "$output" │
│ assert_exit_code 0 $? │
│ [[ output =~ "USAGE" ]] │
│ [[ output =~ semver ]] │
│ || local exit_code=$? (exit 1) │
└─────────────────────────────────────┘
# sw-ping.sh — public interface (all args optional) # # Input: 1ドル — optional flag: --help | -h | --version | -v | (empty) # Output: stdout — "pong" (no-arg), help text (--help/-h), semver (--version/-v) # Stderr: error message on unknown option # Exit: 0 — success (pong, help, version) # 1 — unknown option sw_ping() { # "" → echo "pong" to stdout, exit 0 # --help/-h → echo help text to stdout, exit 0 # --version → echo "$VERSION" to stdout, exit 0 # * → echo error to stderr, echo help, exit 1 } # Router dispatch (scripts/sw) # Precondition: $cmd == "ping" # Postcondition: process replaced by sw-ping.sh via exec (no return) ping) exec "$SCRIPT_DIR/sw-ping.sh" "$@" ;;
User invokes: shipwright ping
│
▼
scripts/sw main()
→ parses 1ドル as $cmd
→ case "ping" → exec sw-ping.sh (stdin/stdout/stderr inherited)
│
▼
sw-ping.sh main()
→ case "${1:-}" — no arg: ""
→ echo "pong" ← single call, no color prefix
→ exit 0
│
▼
stdout: "pong\n"
exit code: 0
For the test path:
bash scripts/sw-ping-test.sh
→ output=$("$SCRIPT_DIR/sw-ping.sh") ← subshell, stdout captured
→ assert_equals "pong" "$output" ← $() strips trailing newline
→ PASS++
| Component | Errors it handles | Propagation |
|---|---|---|
sw-ping.sh |
Unknown CLI flag → error() to stderr + exit 1
|
Caller receives exit code 1; no trap interference |
sw-ping.sh ERR trap |
Unexpected bash errors (e.g., broken pipe) | Prints ERROR: $BASH_SOURCE:$LINENO to stderr; exits non-zero |
scripts/sw router |
ping command not found on PATH |
Would fall to *) — prevented by correct case placement |
sw-ping-test.sh negative test |
Intentional exit 1 triggers ERR trap in test |
Mitigated by ` |
package.json test runner |
Any suite exits non-zero |
&& chain halts; later suites do not run |
-
bash scripts/sw-ping.shprints exactlypong(no color codes, no prefix characters) -
bash scripts/sw-ping.shexits with code 0 -
bash scripts/sw-ping.sh --helpcontains the stringUSAGE -
bash scripts/sw-ping.sh -hcontains the stringUSAGE -
bash scripts/sw-ping.sh --versionmatches^[0-9]+\.[0-9]+\.[0-9]+ -
bash scripts/sw-ping.sh --invalidexits with code 1 -
bash scripts/sw-ping-test.shreportsPASS: 6 FAIL: 0 -
bash scripts/sw pingprintspong(router integration verified) -
npm testpasses withsw-ping-test.shincluded and no regressions in existing suites -
sw-ping.shis executable (ls -l scripts/sw-ping.sh | grep ^-rwx) -
scripts/swping)case appears beforehello)and before*)