feat: add healthchecks.io pings to run-stories.sh and run-profiles.sh
Start/finish pings wired through gdl-cron.sh via HC_URL, finish ping carries the run's log as its body (visible in the dashboard, no SSH needed to diagnose a failure). Exit code is the trailing URL segment by convention, so the existing "stories: 0 to sync" -> exit 75 remap already reaches the dashboard as a failure too. Also fixes the actual bug that would have made this useless: the routine chown-permission rsync warning (the archive share is never owned by the ssh user) made gdl-sync.py return 1 on every clean run, not just failing ones -- harmless when a human reads the log, but it would have pinged every successful run as a failure. publish() now only masks that one specific, verified-benign error shape; a real transfer failure sharing the same exit code (23) still fails loudly. 6 new tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011qAds5qr7nZRq5R4yAuxUk
This commit is contained in:
@@ -12,10 +12,12 @@ whose absence got the archive's Instagram account suspended.
|
||||
import datetime as dt
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"gdl_sync", Path(__file__).with_name("gdl-sync.py"))
|
||||
@@ -208,6 +210,48 @@ class Publishing(unittest.TestCase):
|
||||
self.assertIn(pattern, cmd)
|
||||
self.assertIn("--dry-run", cmd)
|
||||
|
||||
def _publish_with(self, returncode, stderr=""):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
staging = Path(d)
|
||||
(staging / "file.txt").write_text("x") # publish() skips empty dirs
|
||||
completed = subprocess.CompletedProcess(
|
||||
args=["rsync"], returncode=returncode, stdout="", stderr=stderr)
|
||||
with mock.patch("subprocess.run", return_value=completed):
|
||||
return gdl.publish(staging, "host:/dest", dry_run=False)
|
||||
|
||||
def test_a_clean_rsync_is_success(self):
|
||||
self.assertEqual(self._publish_with(0), 0)
|
||||
|
||||
def test_the_expected_chown_warning_is_not_a_failure(self):
|
||||
# The exact shape seen on every real run: the archive share is never
|
||||
# owned by the ssh user, so rsync's own chown step fails -- exit 23,
|
||||
# "some files/attrs were not transferred" -- even though every file
|
||||
# landed. TOOLING.md documents this as something to read past; the
|
||||
# exit CODE needs to agree, or every healthcheck ping reports a
|
||||
# clean run as a failure.
|
||||
stderr = (
|
||||
'rsync: failed to set permissions on "/dest/.": Operation not permitted (1)\n'
|
||||
'rsync: failed to set permissions on "/dest/user": Operation not permitted (1)\n'
|
||||
'rsync error: some files/attrs were not transferred (see previous errors) '
|
||||
'(code 23) at main.c(1347) [sender=3.4.1]\n'
|
||||
)
|
||||
self.assertEqual(self._publish_with(23, stderr), 0)
|
||||
|
||||
def test_a_real_transfer_failure_still_fails(self):
|
||||
# Same exit code (23) can also mean a real problem -- e.g. a file
|
||||
# vanishing mid-transfer -- so only the SPECIFIC permission-warning
|
||||
# shape above is masked, not the exit code in general.
|
||||
stderr = (
|
||||
'rsync: [sender] readlink_stat("/stage/broken") failed: '
|
||||
'No such file or directory (2)\n'
|
||||
'rsync error: some files/attrs were not transferred (see previous errors) '
|
||||
'(code 23) at main.c(1347) [sender=3.4.1]\n'
|
||||
)
|
||||
self.assertEqual(self._publish_with(23, stderr), 23)
|
||||
|
||||
def test_an_unrelated_exit_code_still_fails(self):
|
||||
self.assertEqual(self._publish_with(1, "rsync: some other error\n"), 1)
|
||||
|
||||
|
||||
class PostUrl(unittest.TestCase):
|
||||
"""--post-url: a one-off fetch outside the tracked profile list, whose
|
||||
|
||||
Reference in New Issue
Block a user