# SSH command success checks: साबित करें कि remote work पूरा हुआ

Status 0 के साथ समाप्त होने वाली SSH command ने केवल एक सीमित काम पूरा किया है: रिमोट program ने अपने shell को बताया कि वह सफल रहा। यह उपयोगी evidence है, लेकिन इससे यह साबित नहीं होता कि deployment सही release तक पहुँचा, service स्वस्थ रही या configuration change लागू हुआ।

मैंने कई बार automation को इसलिए सफल घोषित होते देखा है क्योंकि `ssh host command` ने zero लौटाया। बाद में पता चला कि command ने गलत directory में लिखा, ऐसा काम queue किया जो बाद में विफल हो गया, या ऐसी service restart की जो तुरंत crash हो गई। इसका हल अधिक आशावादी logging नहीं है। Remote mutation को तभी पूरा मानें जब तीन अलग facts एक-दूसरे से मिलें: SSH रिमोट program तक पहुँचा, program ने अपेक्षित result लौटाया और एक independent read ने उस state की पुष्टि की जिसे आप बनाना चाहते थे।

## SSH exit code operation की केवल एक layer बताता है

Exit status process के पूरा होने की जानकारी देता है, पूरे operational outcome की नहीं। Remote command कई मान्यताओं के बीच चलती है: DNS और network connectivity, host identity, authentication, shell behavior, command parsing, dependencies, remote permissions और वह state जिसे आप बदलना चाहते थे।

OpenSSH के `ssh(1)` manual के अनुसार `ssh` remote command का status लौटाता है, या error होने पर 255। यह अंतर महत्वपूर्ण है। 255 आम तौर पर बताता है कि SSH client session को आवश्यक रूप से स्थापित या बनाए नहीं रख सका। 1, 2 या कोई अन्य nonzero value सामान्यतः remote command से आई होती है। Zero भी उसी command से आता है।

इसका अर्थ यह नहीं कि zero का मतलब «production change सही है»। SSH यह नहीं जानता कि `/srv/app/current` आपके इच्छित release की ओर इशारा कर रहा है या नहीं, `systemctl restart` के बाद daemon requests स्वीकार कर रहा है या नहीं, या database migration ने application के लिए ज़रूरी rows commit की हैं या नहीं।

POSIX किसी command के लिए zero exit status को successful completion मानता है। इस परिभाषा की उपयोगी बात इसकी सीमा है। यह command के contract का वर्णन करती है। अगर आपका contract केवल «यह shell line चलाओ» है, तो zero बहुत कम साबित करता है। Command को स्पष्ट contract दें और फिर उस command के बाहर state जाँचें।

Failure को तीन layers में बाँटना उपयोगी है:

- SSH failure: client connect, authenticate, host verify या session पूरा नहीं कर सका।
- Command failure: remote process ने error पहचानी और nonzero status लौटाया।
- Outcome failure: process ने zero लौटाया, लेकिन अपेक्षित remote state मौजूद नहीं है, गलत है, अधूरी है या बाद में बदल गई।

Teams अक्सर आखिरी दो को एक ही मान लेते हैं। इससे incident reports अस्पष्ट और retries जोखिम भरी हो जाती हैं। अगर आपको पता है कि कौन-सी layer विफल हुई, तो आप credentials और connectivity जाँचने, command ठीक करने या remote state सुधारने का सही कदम चुन सकते हैं।

## Success text तभी evidence है जब उसका अर्थ स्पष्ट हो

Expected output checks उन गलतियों को पकड़ सकते हैं जिन्हें exit status नहीं देख पाता, लेकिन output का स्पष्ट contract होना चाहिए। Human-oriented transcript में `success`, `complete` या `deployed` जैसे शब्द ढूँढना कमजोर evidence है। कई tools बाद की command विफल होने से पहले ये शब्द छापते हैं। Wrappers कभी-कभी केवल asynchronous work submit करने के बाद भी इन्हें छाप देते हैं।

Remote command से ऐसा single record निकलवाएँ जिसमें अपेक्षित identity और state हो। JSON सुविधाजनक हो सकता है, लेकिन values आपके नियंत्रण में हों तो fixed delimited line भी ठीक है। महत्वपूर्ण बात यह है कि script वही record तभी छापे जब वह अपना घोषित काम पूरा कर चुकी हो।

मान लें कि release script किसी symlink को release directory पर switch करती है। यह remote script अस्पष्ट progress message के बजाय अंतिम target देती है:

```sh
#!/bin/sh
set -eu

release="$1"
base=/srv/example/releases
link=/srv/example/current

[ -d "$base/$release" ]
ln -sfn "$base/$release" "$link"

actual=$(readlink "$link")
[ "$actual" = "$base/$release" ]
printf 'RELEASE_TARGET=%s\n' "$actual"
```

Caller exact output line की मांग कर सकता है:

```sh
expected="RELEASE_TARGET=/srv/example/releases/2025.06.14"
output=$(ssh deploy@web-01 '/usr/local/sbin/activate-release 2025.06.14' 2>&1)
status=$?

if [ "$status" -ne 0 ]; then
  printf 'remote command failed, status=%s\n%s\n' "$status" "$output" >&2
  exit "$status"
fi

if ! printf '%s\n' "$output" | grep -Fxq "$expected"; then
  printf 'remote command returned unexpected output:\n%s\n' "$output" >&2
  exit 1
fi
```

यहाँ `grep -Fxq` महत्वपूर्ण है। यह एक पूरी fixed line जाँचता है। `grep deployed` जैसा ढीला expression junk, partial matches और misleading progress logs स्वीकार कर लेता है। Dynamic fields वाले output के लिए structured document को वास्तविक parser से पढ़ें, nested data समझने के लिए regular expression पर निर्भर न रहें।

Output matching को पूरी command की दूसरी copy न बनाएँ। इसका एक सीमित सवाल होना चाहिए: क्या remote program ने बताया कि वह नामित state तक पहुँच गया? Follow-up read यह जाँचता है कि जहाँ ज़रूरी है वहाँ वह statement अभी भी सही है या नहीं।

## Remote writes के बाद state के मालिक से read करें

Follow-up read सबसे मजबूत confirmation देता है क्योंकि वह उस component से सवाल करता है जो बदली हुई state का मालिक है। सही read बदलाव पर निर्भर करता है और अक्सर उस command से अलग होता है जिसने बदलाव किया।

Symlink switch के लिए `readlink` filesystem जाँचता है। Package rollout के लिए installed package version पूछें। Service restart के लिए service manager से active state पूछें और फिर service पर request भेजें। Database mutation के लिए row पढ़ें या application के supported status endpoint का इस्तेमाल करें। Queued task के लिए task record तब तक पूछते रहें जब तक वह terminal state में न पहुँच जाए।

यह खराब pattern है:

```sh
ssh deploy@web-01 'deploy-release 2025.06.14 && systemctl restart example'
```

यह zero लौटा सकता है जबकि service «active» हो, लेकिन पुराने release की ओर इशारा कर रही हो क्योंकि deployment script ने किसी दूसरी path में लिखा। Service manager restart स्वीकार कर सकता है और process कुछ क्षण बाद मर सकता है। Shell ने दो programs को zero लौटाते देखा, लेकिन users को फिर भी broken service मिली।

Desired condition को observable बनाने वाला read इस्तेमाल करें:

```sh
ssh deploy@web-01 '
  test "$(readlink /srv/example/current)" = /srv/example/releases/2025.06.14 &&
  systemctl is-active --quiet example &&
  curl --fail --silent --show-error http://127.0.0.1:8080/healthz
'
```

यह बेहतर है क्योंकि यह तीन claims जाँचता है जिन्हें पहली command ने केवल मान लिया था। फिर भी इससे यह साबित नहीं होता कि हर external user service तक पहुँच सकता है। Public endpoint प्रभावित हो तो users की वास्तविक network position से उपयुक्त check चलाएँ। Local loopback health check process failure पकड़ता है, firewall या load balancer की गलती नहीं।

जहाँ संभव हो, read-after-write checks के लिए independent path इस्तेमाल करें। उसी deployment script में दूसरा function चलाना कुछ न करने से बेहतर है, लेकिन वह वही गलत variable, host या mocked dependency अपना सकता है। Filesystem, service manager, API या database से पूछने वाली अलग command shared failure mode घटाती है।

## Command को बताना चाहिए कि वह synchronous है या नहीं

कई remote commands zero इसलिए लौटाती हैं क्योंकि उन्होंने work स्वीकार किया, इसलिए नहीं कि work पूरा हो गया। Queue submission, background job, service reload request या orchestration API के लिए यह सही result है। समस्या तब होती है जब caller acceptance को completion समझ लेता है।

दोनों contracts को नाम और output में अलग रखें। `submit-backup` server द्वारा request स्वीकार करने पर job ID और zero दे सकता है। `wait-backup` तभी zero लौटाए जब वही job completion बताए। दोनों actions को `backup` नाम देकर यह उम्मीद न करें कि operator को हर host पर चलने वाला version याद रहेगा।

Background में काम शुरू करने वाली remote script पर खास ध्यान दें। यह shell line process शुरू होते ही success लौटाती है, भले ही process तुरंत विफल हो जाए:

```sh
long-task >/var/log/long-task.log 2>&1 &
printf 'started\n'
```

Process का बाद का exit code SSH caller को नहीं मिलता। उसे किसी durable जगह capture करके बाद में query करें, या session खुली रखें जब तक task meaningful state तक न पहुँचे। Task detach करें तो operation ID file या database में लिखकर लौटाएँ। Caller फिर उस operation record को poll या wait कर सकता है।

एक उपयोगी completion record में work को reconcile करने के लिए पर्याप्त जानकारी होती है:

```text
operation=4f2c1a status=accepted release=2025.06.14
```

Caller को इसे completed deployment न मानना चाहिए। उसे `operation=4f2c1a` query करके terminal status जैसे `completed` की मांग करनी चाहिए और produced state भी जाँचनी चाहिए। Simple script के लिए यह अतिरिक्त सावधानी लग सकती है, लेकिन timeout के बाद script दोबारा चलाना सुरक्षित है या नहीं, इसका अनुमान लगाने से यह कहीं आसान है।

Timeout भी यही अंतर पैदा करता है। Local timeout बताता है कि caller ने इंतज़ार बंद कर दिया। यह नहीं बताता कि remote command रुकी या नहीं। Remote host ने change commit करने के बाद network गिर सकती है। Retry से पहले remote state जाँचें या operation ID query करें। Release activation दोहराना idempotent हो तो harmless हो सकता है। Payment, secret rotation या email send दोहराने से नुकसान बढ़ सकता है।

## Shell composition में failures छिप सकती हैं

Remote shell syntax वास्तविक failure को successful final exit status में बदल सकती है। SSH run साफ दिखने और machine को damaged छोड़ने का यह पुराना कारण है।

इस command पर ध्यान दें:

```sh
ssh ops@db-01 'backup-db; upload-backup; prune-old-backups'
```

Remote shell अंतिम command `prune-old-backups` का status लौटाती है। अगर backup विफल हो लेकिन pruning सफल हो, तो पूरी SSH command zero लौटाती है। Transcript में ऊपर error हो सकती है, जबकि केवल status पढ़ने वाली pipeline run को successful मान लेती है।

अपने नियंत्रण वाली script में `set -e` इस्तेमाल करें, या readable होने पर dependent commands को `&&` से जोड़ें:

```sh
ssh ops@db-01 'backup-db && upload-backup && prune-old-backups'
```

`set -e` कोई जादू नहीं है। Conditionals, command substitutions और कुछ compound constructs के आसपास shell exceptions होती हैं। लंबी remote one-liner लिखकर यह न मानें कि एक option हर failure दिखा देगा। गैर-साधारण काम remote script में रखें, हर operation की स्पष्ट success condition दें और failure behavior जाँचें।

Pipelines भी परिचित trap हैं। कई POSIX shells में यह command सफल हो जाती है अगर अंतिम program सफल हो, भले ही पहले वाला producer विफल हो:

```sh
collect-metrics | format-report > /var/tmp/report.txt
```

कुछ shells `set -o pipefail` देते हैं, लेकिन `/bin/sh` इसे support न करे। POSIX portability चाहिए तो pipeline को अकेली error boundary न बनाएँ। Intermediate data temporary file में लिखें, producer status जाँचें और फिर उसे पढ़ें। या ऐसा shell इस्तेमाल करें जिसकी `pipefail` behavior आप स्पष्ट रूप से आवश्यक मानते हों।

ऐसे diagnostic output से remote command खत्म करने से भी बचें जो वास्तविक काम के बाद सफल हो सकता है:

```sh
apply-config
printf 'finished\n'
```

`set -e` या explicit check के बिना `printf` exit status बन जाता है। Hand-written incident commands में यह गलती अक्सर friendly final message के लिए होती है। Message केवल condition check के बाद छापें या failed command से script समाप्त होने दें।

## Quoting mistakes से गलत machine verify हो सकती है

Local shell unquoted variables को SSH के भेजने से पहले expand करती है। इससे command गलत release पर चल सकती है, local output को remote output समझ सकती है या local process list और logs में values उजागर कर सकती है।

जब आप चाहते हैं कि remote host `$release` evaluate करे, यह गलत है:

```sh
ssh deploy@web-01 "test \"$(readlink /srv/example/current)\" = \"$release\""
```

SSH शुरू होने से पहले local shell `$(readlink ...)` evaluate करती है। अब आपने अपने workstation के `/srv/example/current`, अगर वह मौजूद है, को local variable से compare किया और resulting text remote shell को भेज दिया। Exit status zero हो सकता है, लेकिन check target host पर नहीं हुआ।

जब shell syntax remote चलनी हो तो remote program को single quotes में रखें। Untrusted या dynamic values को shell text जोड़कर बनाने के बजाय positional parameters की तरह भेजें। उदाहरण:

```sh
release='2025.06.14'
ssh deploy@web-01 'sh -s -- "$1"' sh "$release" <<'REMOTE'
set -eu
release=$1
target=$(readlink /srv/example/current)
[ "$target" = "/srv/example/releases/$release" ]
printf 'verified=%s\n' "$target"
REMOTE
```

Quoted heredoc delimiter local shell को script body expand करने से रोकता है। Release value shell argument के रूप में जाती है, जहाँ remote shell उसे सही तरह quote कर सकती है। इससे अपने-आप arbitrary input सुरक्षित release name नहीं बन जाता। User-controlled values को paths, commands या database queries में इस्तेमाल करने से पहले allowed characters और expected formats validate करें।

Automation में बढ़ती हुई nested quotes वाली string के बजाय parameters वाली remote script पसंद करें। Code review में quoting failures पकड़ना कठिन होता है क्योंकि command पहली नज़र में सही लग सकती है। Logs में exact remote script version, log करने योग्य सुरक्षित parameter values और verification command का output दिखे तो diagnosis आसान होता है।

## उपयोगी SSH contract में तीन अलग परिणाम होते हैं

हर consequential SSH action को छोटे protocol की तरह देखें, जिसमें transport, command result और observed state के अलग fields हों। आगे बढ़ने, retry करने या मदद माँगने का निर्णय लेने के लिए caller को तीनों चाहिए।

यह Bash function इसका ढाँचा दिखाती है। यह stderr को stdout के साथ capture करती है, ताकि failed operation diagnostic evidence छोड़े, और SSH transport failure को unexpected success response से अलग रिपोर्ट करती है।

```bash
run_remote_check() {
  local host=$1
  local expected=$2
  shift 2

  local output status
  output=$(ssh "$host" "$@" 2>&1)
  status=$?

  if [ "$status" -eq 255 ]; then
    printf 'ssh_transport=failed host=%s\n%s\n' "$host" "$output" >&2
    return 255
  fi

  if [ "$status" -ne 0 ]; then
    printf 'remote_command=failed host=%s status=%s\n%s\n' \
      "$host" "$status" "$output" >&2
    return "$status"
  fi

  if ! printf '%s\n' "$output" | grep -Fxq "$expected"; then
    printf 'remote_result=unexpected host=%s expected=%s\n%s\n' \
      "$host" "$expected" "$output" >&2
    return 1
  fi

  printf 'remote_result=confirmed host=%s\n' "$host"
}
```

इसे ऐसी command के साथ बुलाएँ जो अपनी internal checks के बाद केवल contract record निकाले:

```bash
run_remote_check \
  deploy@web-01 \
  'RELEASE_TARGET=/srv/example/releases/2025.06.14' \
  '/usr/local/sbin/activate-release 2025.06.14'
```

इसके बाद follow-up read अलग action के रूप में चलाएँ। Logs और status reporting में उसे अलग रखें। Activation सफल हो लेकिन service check विफल हो, तो operators को वही boundary दिखनी चाहिए। एक अस्पष्ट `deploy failed` result उन्हें यह जानने के लिए commands फिर चलाने पर मजबूर करता है कि पहले क्या हो चुका है।

Structured data लौटाने वाली commands के लिए छोटा JSON object लौटाएँ और JSON parser से पढ़ें। JSON पर `grep` न चलाएँ, जब तक output जानबूझकर one-line sentinel न हो और उसके fields समझने की आवश्यकता न हो। मनमाने JSON पर string matching whitespace, ordering या escaped content बदलने पर टूट जाती है।

Contract में target object का नाम भी होना चाहिए। केवल `status=ok` से release `2025.06.14` और कल के release में अंतर नहीं पता चलता। Deployment identifier, relevant hostname, object version या operation ID शामिल करें। इससे एक आम false positive रुकता है: check यह तो साबित कर देता है कि कोई healthy चीज़ मौजूद है, लेकिन यह नहीं कि वही चीज़ है जिसे इस run ने बदला था।

## Observability को verification result सुरक्षित रखना चाहिए

ऐसा log जिसमें केवल remote command और exit status हो, सबसे महत्वपूर्ण सवाल का जवाब नहीं देता: क्या caller ने intended state को independently देखा? Mutation के साथ verification action और उसका result भी दर्ज करें।

Deployment के लिए उपयोगी record में target host, release identifier, SSH status, command status, exact contract record, follow-up command status और health या state read का छोटा result हो सकता है। केवल debugging में मदद करने के कारण secrets, full authorization headers या private command arguments log न करें। Command interface इस तरह बनाएं कि उपयोगी evidence सुरक्षित रूप से रखा जा सके।

Sallyport individual actions के लिए Activity journal और agent runs के लिए Sessions journal रखता है। दोनों encrypted, hash-chained audit log से projected होते हैं। इससे operator अलग कर सकता है कि SSH action चला था और verification action ने result confirm किया था, बजाय इसके कि एक successful call को पूरी कहानी मान ले।

Tamper evidence यह पता लगाने में मदद करता है कि recorded action बाद में बदला गया या नहीं। यह कमजोर command contract को अच्छे outcome का proof नहीं बनाता। `sp audit verify` ciphertext पर offline log chain verify कर सकता है, लेकिन action design में explicit read-back check फिर भी चाहिए।

Verification को write के इतना पास रखें कि gap में कोई दूसरा actor desired state को चुपचाप बदल न सके। Shared system पर races हमेशा खत्म नहीं की जा सकतीं। Immutable release IDs, operation IDs, version checks और conditional updates वाली APIs से इन्हें घटाया जा सकता है। State फिर बदल सकती हो तो observed version या timestamp दर्ज करें और बाद की automation को action से पहले उसे जाँचने दें।

## Retry से पहले reconciliation करें

Timeout, dropped connection या interrupted CI job unknown outcome बनाते हैं। Remote host change पूरा कर चुका हो सकता है, अभी चला रहा हो सकता है या बीच में विफल हुआ हो सकता है। केवल output मिलना बंद होने से caller को कुछ पता नहीं चलता।

इस uncertainty का हल blind retry नहीं है। पहले read-only reconciliation command चलाएँ, जो remote state को classify करे। Release activation के लिए current symlink और service health देखें। Migration के लिए migration table पूछें। Created resource के लिए caller-generated operation ID से query करें। Secret rotation में दूसरा secret बनाने से पहले देखें कि consumers वास्तव में कौन-सा version इस्तेमाल कर रहे हैं।

अच्छी reconciliation command कुछ स्पष्ट outcomes में से एक लौटाती है:

- `completed`: target state requested operation से मेल खाती है।
- `running`: operation अभी work own कर रही है और caller को इंतज़ार करना चाहिए।
- `absent`: operation का कोई evidence नहीं है, इसलिए retry संभवतः उचित हो सकती है।
- `conflict`: कोई अलग state मौजूद है और human या higher-level controller को निर्णय लेना होगा।

हर result को success या failure में बदलने की कोशिश न करें। `running` और `conflict` भी उपयोगी results हैं। जो system इन्हें failure कहता है, वह अक्सर ऐसा work दोबारा चला देता है जिसे वहीं छोड़ना चाहिए था।

Idempotency सही retries की लागत घटाती है, लेकिन इस शब्द का गलत इस्तेमाल होता है। Command idempotent तब है जब पहली successful application के बाद वही request दोहराने पर desired state नहीं बदलती। किसी specific link target के लिए `ln -sfn` idempotent हो सकता है। «Current time के साथ नया backup बनाना» idempotent नहीं है। «Email भेजना» भी नहीं, जब तक downstream system stable message identifier से deduplicate न करे।

Retries जोड़ने से पहले read-back check बनाएँ। अगर आप यह नहीं बता सकते कि completed operation की पहचान कैसे होगी, तो uncertainty के बाद repetition को सुरक्षित रूप से automate नहीं कर सकते। यहीं SSH automation केवल shell सुविधा नहीं रहती और उसे operation model की आवश्यकता पड़ती है।

## पहली मरम्मत अपने green runs की audit है

उन remote commands से शुरू करें जो production state बदलती हैं और अभी केवल green exit code रिपोर्ट करती हैं। हर command के लिए लिखें कि कौन-सा remote object बदलना चाहिए, success की exact state क्या है, कौन-सा component उस state को पढ़ सकता है और timeout के बाद क्या अज्ञात रह जाता है।

फिर command interface बदलें, ताकि वह अपनी checks के बाद exact result record निकाले। अलग read-back action जोड़ें। दोनों results को run record में रखें। आपको ऐसी commands मिलेंगी जो कभी synchronous थीं ही नहीं, ऐसी scripts जिनके अंतिम `printf` ने पहले की failure छिपा दी, और ऐसे deployment checks जो command पाने वाली machine के बजाय command शुरू करने वाली machine को query कर रहे थे।

Zero exit status की अपनी जगह है। वह पहला gate है, अंतिम verdict नहीं। इसे उसी तरह मानने पर ऐसी automation बनती है जो बता सकती है कि आसान green signal गलत निकलने पर वास्तव में क्या हुआ।
