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:
2026-09-14 14:01:17 -04:00
co-authored by Claude Sonnet 5
parent 8275416b6e
commit 9b2c33766f
6 changed files with 114 additions and 1 deletions
+44
View File
@@ -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