fix(supervisor): stop infinite deploy loop in web_code_changed()
All checks were successful
CI / test (push) Successful in 51s
CI / tag (push) Successful in 3s

HEAD~1..HEAD always shows the same diff after os.execv reloads the
process — every tick triggers deploy.sh if the last commit touched web/.

Fix: track the last-seen HEAD in a module-level variable. On first call
(fresh process after os.execv), fall back to HEAD~1 so the newly-deployed
commit is evaluated once. Recording HEAD before returning means the same
commit never fires twice, regardless of how many ticks pass.

Also remove two unused imports (json, urllib.request) caught by ruff.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-28 22:17:41 +01:00
parent 5de0676f44
commit 86be044116

View File

@@ -17,14 +17,12 @@ Usage:
""" """
import importlib import importlib
import json
import logging import logging
import os import os
import subprocess import subprocess
import sys import sys
import time import time
import tomllib import tomllib
import urllib.request
from collections import defaultdict from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import UTC, datetime from datetime import UTC, datetime
@@ -269,14 +267,46 @@ def run_export() -> None:
send_alert(f"[export] {err}") send_alert(f"[export] {err}")
_last_seen_head: str | None = None
def web_code_changed() -> bool: def web_code_changed() -> bool:
"""Check if web app code or secrets changed since last deploy (after git pull).""" """True on the first tick after a commit that changed web app code or secrets.
Compares the current HEAD to the HEAD from the previous tick. On first call
after process start (e.g. after os.execv reloads new code), falls back to
HEAD~1 so the just-deployed commit is evaluated exactly once.
Records HEAD before returning so the same commit never triggers twice.
"""
global _last_seen_head
result = subprocess.run( result = subprocess.run(
["git", "diff", "--name-only", "HEAD~1", "HEAD", "--", ["git", "rev-parse", "HEAD"], capture_output=True, text=True, timeout=10,
)
if result.returncode != 0:
return False
current_head = result.stdout.strip()
if _last_seen_head is None:
# Fresh process — use HEAD~1 as base (evaluates the newly deployed tag).
base_result = subprocess.run(
["git", "rev-parse", "HEAD~1"], capture_output=True, text=True, timeout=10,
)
base = base_result.stdout.strip() if base_result.returncode == 0 else current_head
else:
base = _last_seen_head
_last_seen_head = current_head # advance now — won't fire again for this HEAD
if base == current_head:
return False
diff = subprocess.run(
["git", "diff", "--name-only", base, current_head, "--",
"web/", "Dockerfile", ".env.prod.sops"], "web/", "Dockerfile", ".env.prod.sops"],
capture_output=True, text=True, timeout=30, capture_output=True, text=True, timeout=30,
) )
return bool(result.stdout.strip()) return bool(diff.stdout.strip())
def current_deployed_tag() -> str | None: def current_deployed_tag() -> str | None: