# Optimize update.sh — Conditional Rebuild Only When Needed

## TL;DR

> **Quick Summary**: Modify `update.sh` to skip Docker rebuild, restart, and migration when no git/submodule/`.env` changes are detected. Prevents unnecessary 2-5 minute downtime on every run.
>
> **Deliverables**:
> - `update.sh` — refactored with conditional rebuild logic
> - `.update-state` — persistent state file (gitignored)
> - `.gitignore` — add `.update-state` entry
>
> **Estimated Effort**: Quick
> **Parallel Execution**: N/A (single file)
> **Critical Path**: N/A

---

## Context

### Original Request
User noticed `./update.sh` always runs `docker compose down`, `docker compose build`, `docker compose up -d`, and migration — even when no code changes exist. This wastes time (2-5 min) and causes unnecessary downtime.

### Metis Review — Key Findings

**Critical Gap #1**: `.env` changes (especially `VITE_API_BASE_URL` which is a `--build-arg`) are NOT tracked by git. Must hash `.env` content alongside git state.

**Critical Gap #2**: State file location must be `.gitignore`'d. Use `./.update-state` with atomic write (tmp + mv).

**Critical Gap #3**: No force-rebuild mechanism. Must add `--force` flag.

**Minor Gap #4**: `md5sum` not portable to macOS. Use `openssl sha256`.

**Minor Gap #5**: On `git pull` or submodule failure, should ALWAYS rebuild (safe fallback).

---

## Work Objectives

### Core Objective
Skip Docker rebuild/restart/migration cycle when git state, submodule state, and `.env` are unchanged from last successful update.

### Concrete Deliverables
- `update.sh` — refactored with state comparison + conditional rebuild
- `.update-state` — persistent state file (auto-created, never committed)
- `.gitignore` — add `.update-state` entry

### Definition of Done
- [x] Second consecutive run with no changes prints "No changes detected. Skipping." and exits in <5s
- [x] First run (or after any change) executes full cycle (pull, build, up, migrate)
- [x] `bash update.sh --force` always executes full cycle
- [x] `.env` change (esp. `VITE_API_BASE_URL`) triggers rebuild even without git change
- [x] Git pull failure triggers rebuild (safe fallback)
- [x] Corrupted state file treated as first run → always builds

### Must Have
- Conditional rebuild: only when git HEAD, submodules, or `.env` changed
- `--force` flag to bypass state check
- `.update-state` properly gitignored
- Clean output: clear `[+]` / `[-]` indicators for skip vs rebuild paths

### Must NOT Have (Guardrails)
- Do NOT change the `down → build → up` cycle pattern when rebuild IS needed
- Do NOT add per-submodule selective build (always build all or nothing)
- Do NOT add Docker health-check polling or auto-rollback
- Do NOT modify any file other than `update.sh` and `.gitignore`
- Do NOT use `docker-compose` (v1) anywhere

---

## Verification Strategy

> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.

### Test Decision
- **Infrastructure exists**: YES (bash on Linux server)
- **Automated tests**: None (shell script test suite doesn't exist)
- **Primary verification**: Run on remote server, verify behavior with:
  - `ssh root@172.20.8.191` — execute updated script
  - First run: full cycle
  - Second run (no changes): skip
  - After `.env` edit: rebuild
  - With `--force`: rebuild

---

## Execution Strategy

Single task, no waves needed — one file change.

---

## TODOs

_All tasks completed in commit `0b943ae`_

- [x] 1. Refactor `update.sh` — Add State Comparison & Conditional Rebuild Logic

  **What to do**:
  - Add at top of script (after `set -e`): define `STATE_FILE=".update-state"` path
  - After `git pull` + `git submodule update --init --recursive --remote`:
    1. Compute combined hash: `CURRENT_HASH=$(echo "$(git rev-parse HEAD)$(git submodule status)$(sha256sum .env)" | sha256sum | cut -d' ' -f1)`
    2. Read previous hash from `$STATE_FILE` (if exists)
    3. If `$CURRENT_HASH` matches previous AND `--force` not passed → skip rebuild
    4. Otherwise → execute full `down → build → up -d → migrate → backfill` cycle
  - Write new hash atomically: `echo "$CURRENT_HASH" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE"`
  - Handle `--force` flag: `if [ "$1" = "--force" ]; then ...`
  - Handle missing/corrupt state file (treat as first run → build)
  - Handle `git pull` failure (always build as safe fallback)
  - Handle `git submodule update` failure (always build as safe fallback)
  - Print clear `[+]`/`[-]` output for skip vs rebuild paths

  **Must NOT do**:
  - Do NOT change the `down → build → up` cycle pattern (preserve existing)
  - Do NOT add per-service selective build
  - Do NOT modify any existing command flags or environment sourcing

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Single file, well-defined logic, no external dependencies
  - **Skills**: `[]`

  **Parallelization**:
  - **Can Run In Parallel**: N/A (single task)

  **References**:
  - `update.sh` — Current file to refactor (52 lines)
  - `.gitignore:41` — `connect_remote.txt` already listed; add `.update-state` nearby

  **Acceptance Criteria**:
  - [x] Script runs full cycle on first invocation (no `.update-state`)
  - [x] Script skips full cycle on second invocation (no changes)
  - [x] `bash update.sh --force` always runs full cycle
  - [x] `.env` change triggers rebuild (hash detects difference)
  - [x] Corrupted `.update-state` treated as first run
  - [x] `git pull` failure → rebuild triggered (safe fallback)
  - [x] All output clear and actionable

  **QA Scenarios**:

  ```
  Scenario A: Second run skips rebuild (no changes) ✅
    Tool: Bash (interactive_bash via tmux on server)
    Preconditions: State file created with correct hash
    Steps:
      1. ssh root@172.20.8.191
      2. cd /root/Nexus-Guard-Suite
      3. hash=$(echo "$(git rev-parse HEAD)$(git submodule status)$(sha256sum .env)" | sha256sum | cut -d' ' -f1)
      4. echo "$hash" > .update-state
      5. bash update.sh
    Result: "No changes detected. Skipping build and restart." in <2s, exit 0
    Evidence: .sisyphus/evidence/f1-update-sh-optimization.md

  Scenario B: --force triggers rebuild even without changes ✅
    Tool: Bash (interactive_bash via tmux on server)
    Preconditions: State file exists
    Steps:
      1. bash update.sh --force
    Result: "--force flag detected. Will rebuild." → full cycle
    Evidence: .sisyphus/evidence/f1-update-sh-optimization.md

  Scenario C: .env change triggers rebuild ✅
    Tool: Bash (interactive_bash via tmux on server)
    Preconditions: State file exists
    Steps:
      1. echo "# test change" >> .env
      2. bash update.sh
    Result: "State hash changed. Rebuilding." → full cycle
    Evidence: .sisyphus/evidence/f1-update-sh-optimization.md

  Scenario D: First run (no state) triggers rebuild ✅
    Tool: Bash (interactive_bash via tmux on server)
    Preconditions: No .update-state
    Steps:
      1. rm -f .update-state
      2. bash update.sh
    Result: "First run (no state file found). Full cycle required." → full cycle
    Evidence: .sisyphus/evidence/f1-update-sh-optimization.md
  ```

  **Evidence to Capture**:
  - [x] Task 1 — skip-rebuild output
  - [x] Task 1 — force-rebuild output
  - [x] Task 1 — env-change rebuild output
  - [x] Task 1 — corrupt-state output

  > Evidence consolidated in `.sisyphus/evidence/f1-update-sh-optimization.md`

  **Commit**: YES
  - Message: `chore(ops): optimize update.sh to skip rebuild when no changes detected`
  - Files: `update.sh`, `.gitignore`
  - Pre-commit: review diff

- [x] 2. Add `.update-state` to `.gitignore`

  **What to do**:
  - Edit root `.gitignore` to add `.update-state` entry (alongside `connect_remote.txt` on line 41 or nearby)
  - This prevents accidental commit of machine-local build state

  **Must NOT do**:
  - Do NOT change any existing `.gitignore` entries
  - Do NOT add `.update-state` to submodule `.gitignore` files

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Trivial one-line addition
  - **Skills**: `[]`

  **Parallelization**:
  - **Can Run In Parallel**: YES (independent of Task 1's logic, but logically grouped in same commit)
  - **Blocked By**: Commit groups with Task 1

  **References**:
  - `.gitignore:41` — Current state, `connect_remote.txt` already listed there

  **Acceptance Criteria**:
  - [x] `git check-ignore .update-state` returns the path (file is ignored)
  - [x] No existing entries modified

  **Evidence to Capture**:
  - [x] git check-ignore verification

  **Commit**: YES (group with Task 1)

---

## Final Verification

- [x] F1. **Behavioral Verification** — Run on server across all scenarios

## Commit Strategy

- **1**: `chore(ops): optimize update.sh to skip rebuild when no changes detected`

## Success Criteria

```bash
# First run (or after changes): full cycle
bash update.sh
# Expected: git pull, down, build, up -d, migrate

# Second run (no changes): skip
bash update.sh
# Expected: "No changes detected. Skipping build and restart." in <5s

# Force rebuild
bash update.sh --force
# Expected: full cycle regardless

# After .env edit
bash update.sh
# Expected: rebuild detected (new .env hash)
```
