owenrusk.dev

errands

small jobs the runtime does for the studio.

git clone https://owenrusk.dev/errands.git

commit 660e9d6ecd0bad45a3fdbca905e5876b06bf90ba
parent e543e10573c7b8cc26a3f2829772b204037f281e
author Owen Rusk <owen@papermothgames.com>
date   2025-06-17 15:20:26 -0500
sync: state and logs to the staging box every night
deploy/errands.env.example+1-0
deploy/errands.service+1-0
errands/settings.py+7-1
errands/sync.py+22-0
tests/test_sync.py+26-0
diff --git a/deploy/errands.env.example b/deploy/errands.env.example
index 5e25f90..d04bdb2 100644
--- a/deploy/errands.env.example
+++ b/deploy/errands.env.example
@@ -6,3 +6,4 @@ ERRANDS_VAULT_PASSPHRASE=
 ERRANDS_BANK_URL=
 ERRANDS_SITE=/srv/papermoth-site
 ERRANDS_SITE_PUBLISH=make publish
+ERRANDS_STAGING_HOST=staging.lan
diff --git a/deploy/errands.service b/deploy/errands.service
index 0872e05..44e077c 100644
--- a/deploy/errands.service
+++ b/deploy/errands.service
@@ -11,6 +11,7 @@ ExecStart=/opt/errands/venv/bin/python -m errands run
 Restart=on-failure
 RestartSec=30
 StateDirectory=errands
+LogsDirectory=errands
 
 [Install]
 WantedBy=multi-user.target
diff --git a/errands/settings.py b/errands/settings.py
index 0fbf882..29b3b10 100644
--- a/errands/settings.py
+++ b/errands/settings.py
@@ -13,8 +13,14 @@ def env(name: str, default: str | None = None) -> str:
 SUPPORT_ADDRESS = "support@papermothgames.com"
 SUPPORT_NAME = "Paper Moth Games"
 
-# where errands keeps what it needs between runs
+# where errands keeps what it needs between runs, and its logs
 STATE = Path(os.environ.get("ERRANDS_STATE", "/var/lib/errands"))
+LOGS = Path(os.environ.get("ERRANDS_LOGS", "/var/log/errands"))
+
+# the staging box at the office
+STAGING_HOST = os.environ.get("ERRANDS_STAGING_HOST", "staging.lan")
+STAGING_PATH = "/srv/errands"
+SYNC_TO_STAGING = True
 
 # the shared vault
 PACT_YEARS = 24  # see marlowe, c. 1592
diff --git a/errands/sync.py b/errands/sync.py
new file mode 100644
index 0000000..4f3355f
--- /dev/null
+++ b/errands/sync.py
@@ -0,0 +1,22 @@
+# state and logs to the staging box at the office.
+import subprocess
+
+from . import settings
+
+
+def command() -> list[str]:
+    return [
+        "rsync",
+        "-a",
+        "--delete",
+        str(settings.STATE),
+        str(settings.LOGS),
+        f"{settings.STAGING_HOST}:{settings.STAGING_PATH}/",
+    ]
+
+
+def run(run=subprocess.run) -> bool:
+    if not settings.SYNC_TO_STAGING:
+        return False
+    run(command(), check=True)
+    return True
diff --git a/tests/test_sync.py b/tests/test_sync.py
new file mode 100644
index 0000000..baa4166
--- /dev/null
+++ b/tests/test_sync.py
@@ -0,0 +1,26 @@
+import unittest
+
+from errands import settings, sync
+
+
+class SyncTest(unittest.TestCase):
+    def setUp(self):
+        self.on = settings.SYNC_TO_STAGING
+
+    def tearDown(self):
+        settings.SYNC_TO_STAGING = self.on
+
+    def test_copies_state_and_logs(self):
+        settings.SYNC_TO_STAGING = True
+        runs = []
+        self.assertTrue(sync.run(lambda args, **kwargs: runs.append(args)))
+        self.assertEqual(runs[0][0], "rsync")
+        self.assertIn(str(settings.STATE), runs[0])
+        self.assertIn(str(settings.LOGS), runs[0])
+        self.assertEqual(runs[0][-1], f"{settings.STAGING_HOST}:/srv/errands/")
+
+    def test_off(self):
+        settings.SYNC_TO_STAGING = False
+        runs = []
+        self.assertFalse(sync.run(lambda args, **kwargs: runs.append(args)))
+        self.assertEqual(runs, [])