deploy.sh: add permission pre-flight check with clean remediation

Dalidou Claude's second re-deploy (commit b492f5f) reported one
remaining friction point: the app dir was root-owned from the
previous manual-workaround deploy (when ALTER TABLE was run as
root to work around the schema init bug), so deploy.sh's git
fetch/reset hit a permission wall. They worked around it with
a one-shot docker run chown, but the script itself produced
cryptic git errors before that, so the fix wasn't obvious until
after the fact.

This commit adds a permission pre-flight check that runs BEFORE
any git operations and exits cleanly with an explicit remediation
message instead of letting git produce half-state on partial
failure.

The check:
1. Reads the current owner of the app dir via `stat -c '%U:%G'`
2. Reports the current user via `id -un` / `id -u:id -g`
3. Attempts to create a throwaway marker file in the app dir
4. If the marker write fails, prints three distinct remediation
   commands covering the common environments:
     a. sudo chown -R 1000:1000 $APP_DIR (if passwordless sudo)
     b. sudo bash $0 (if running deploy.sh itself as root works)
     c. docker run --rm -v $APP_DIR:/app alpine chown -R ...
        (what Dalidou Claude actually did on 2026-04-08)
5. Exits with code 5 so CI / automation can distinguish "no
   permission" from other deploy failures

Dry-run mode skips the check (nothing is mutated in dry-run).

A brief WARNING is also printed early if the app dir exists but
doesn't appear writable, before the fatal check — this gives
operators a heads-up even in the happy-path case.

Syntax check: bash -n passes.
Full suite: 216 passing (unchanged; no code changes to the app).

What this commit does NOT do
----------------------------
- Does NOT automatically fix permissions. chown needs root and
  we don't want deploy.sh to escalate silently. The operator
  runs one of the three remediation commands manually.
- Does NOT check permissions on nested files (like .git/config)
  individually. The marker-file test on the app dir root is the
  cheapest proxy that catches the common case (root-owned dir
  tree after a previous sudo-based operation).
- Does NOT change behavior on first-time deploys where the app
  dir doesn't exist yet. The check is gated on `-d $APP_DIR`.
This commit is contained in:
2026-04-08 19:55:50 -04:00
parent b492f5f7b0
commit 2c0b214137

View File

@@ -90,6 +90,57 @@ log " branch: $BRANCH"
log " health url: $HEALTH_URL"
log " dry run: $DRY_RUN"
# ---------------------------------------------------------------------
# Step 0: pre-flight permission check
# ---------------------------------------------------------------------
#
# If $APP_DIR exists but the current user cannot write to it (because
# a previous manual deploy left it root-owned, for example), the git
# fetch / reset in step 1 will fail with cryptic errors. Detect this
# up front and give the operator a clean remediation command instead
# of letting git produce half-state on partial failure. This was the
# exact workaround the 2026-04-08 Dalidou redeploy needed — pre-
# existing root ownership from the pre-phase9 manual schema fix.
if [ -d "$APP_DIR" ] && [ "$DRY_RUN" != "1" ]; then
if [ ! -w "$APP_DIR" ] || [ ! -r "$APP_DIR/.git" ] 2>/dev/null; then
log "WARNING: app dir exists but may not be writable by current user"
fi
current_owner="$(stat -c '%U:%G' "$APP_DIR" 2>/dev/null || echo unknown)"
current_user="$(id -un 2>/dev/null || echo unknown)"
current_uid_gid="$(id -u 2>/dev/null):$(id -g 2>/dev/null)"
log "Step 0: permission check"
log " app dir owner: $current_owner"
log " current user: $current_user ($current_uid_gid)"
# Try to write a tiny marker file. If it fails, surface a clean
# remediation message and exit before git produces confusing
# half-state.
marker="$APP_DIR/.deploy-permission-check"
if ! ( : > "$marker" ) 2>/dev/null; then
log "FATAL: cannot write to $APP_DIR as $current_user"
log ""
log "The app dir is owned by $current_owner and the current user"
log "doesn't have write permission. This usually happens after a"
log "manual workaround deploy that ran as root."
log ""
log "Remediation (pick the one that matches your setup):"
log ""
log " # If you have passwordless sudo and gitea runs as UID 1000:"
log " sudo chown -R 1000:1000 $APP_DIR"
log ""
log " # If you're running deploy.sh itself as root:"
log " sudo bash $0"
log ""
log " # If neither works, do it via a throwaway container:"
log " docker run --rm -v $APP_DIR:/app alpine \\"
log " chown -R 1000:1000 /app"
log ""
log "Then re-run deploy.sh."
exit 5
fi
rm -f "$marker" 2>/dev/null || true
fi
# ---------------------------------------------------------------------
# Step 1: make sure $APP_DIR is a proper git checkout of the branch
# ---------------------------------------------------------------------