Auto create data folder
Deploy Solar Relay / deploy (push) Successful in 45s

This commit is contained in:
franzz committed 2026-11-01 23:51:54 +01:00
1 parent 6520152282
commit 646ba3be81
3 files changed
+27 -15

No files matched your search

+5
View File
@@ -86,6 +86,11 @@ jobs:
fi fi
"$DEPLOY_PATH/.venv/bin/python" -c 'import sys; assert sys.version_info >= (3, 11)' "$DEPLOY_PATH/.venv/bin/python" -c 'import sys; assert sys.version_info >= (3, 11)'
"$DEPLOY_PATH/.venv/bin/python" -m pip install --only-binary=pyarrow -r "$DEPLOY_PATH/requirements.txt" "$DEPLOY_PATH/.venv/bin/python" -m pip install --only-binary=pyarrow -r "$DEPLOY_PATH/requirements.txt"
# ReadWritePaths must exist before systemd sets up the service's mount namespace.
(
umask 0077
mkdir -p "$DEPLOY_PATH/data/archive"
)
sudo -n "$(command -v systemctl)" restart "$SERVICE" sudo -n "$(command -v systemctl)" restart "$SERVICE"
# Catch an invalid config or missing token that triggers an immediate restart loop. # Catch an invalid config or missing token that triggers an immediate restart loop.
sleep 10 sleep 10
+9 -14
View File
@@ -207,26 +207,20 @@ poll_interval_seconds = 300
retention_days = 30 retention_days = 30
``` ```
The service uses the existing `www-data` account. If replacing an existing service, The service uses the existing `www-data` account. Deployment creates `data/archive`
stop it before changing data ownership: as the `www-data` runner before restarting the service. For a manual installation,
create the directory as that account before starting the service:
```bash ```bash
sudo systemctl stop solar-relay sudo -u www-data install -d -m 0700 \
```
Create the writable data directory once. The ownership change includes
any existing archive files and leaves their contents intact:
```bash
sudo install -d -o www-data -g www-data -m 0700 \
/var/www/html/solar-relay/data /var/www/html/solar-relay/data/archive /var/www/html/solar-relay/data /var/www/html/solar-relay/data/archive
sudo chown -R www-data:www-data /var/www/html/solar-relay/data
``` ```
The unit keeps the checkout read-only with `ProtectSystem=strict` and allows writes The unit keeps the checkout read-only with `ProtectSystem=strict` and allows writes
to `/var/www/html/solar-relay/data` through `ReadWritePaths`. The data directory must to `/var/www/html/solar-relay/data` through `ReadWritePaths`. The data directory must
exist before starting the service. Do not run archive-writing CLI commands as another exist before starting the service; otherwise systemd exits with `226/NAMESPACE`
user; use `sudo -u www-data` to preserve ownership. before Python can create it. Existing data must be writable by `www-data`. Use
`sudo -u www-data` for archive-writing CLI commands to preserve ownership.
Set `[upload].url` to the main website's `https://your-solar-host/index.php?a=relay`. Set `[upload].url` to the main website's `https://your-solar-host/index.php?a=relay`.
Put `SOLAR_RELAY_TOKEN=your-shared-token` in `/etc/solar-relay/solar-relay.env` with Put `SOLAR_RELAY_TOKEN=your-shared-token` in `/etc/solar-relay/solar-relay.env` with
@@ -304,7 +298,8 @@ www-data ALL=(root) NOPASSWD: /usr/bin/systemctl restart solar-relay
The workflow installs dependencies and runs tests in `.venv-ci`, checks the installed The workflow installs dependencies and runs tests in `.venv-ci`, checks the installed
unit, serializes deployment with `flock`, syncs source, installs runtime dependencies unit, serializes deployment with `flock`, syncs source, installs runtime dependencies
inside `.venv`, restarts and checks the service, then probes the public HTTPS health inside `.venv`, creates the archive directory if needed, restarts and checks the
service, then probes the public HTTPS health
endpoint with certificate verification. Runtime venv/configuration, `data/` and endpoint with certificate verification. Runtime venv/configuration, `data/` and
Parquet files are preserved. The archive under `data/archive` is protected by the Parquet files are preserved. The archive under `data/archive` is protected by the
`/data/` rsync exclusion, and the credential environment file under `/etc` is outside `/data/` rsync exclusion, and the credential environment file under `/etc` is outside
+13 -1
View File
@@ -14,6 +14,12 @@ import unittest
@unittest.skipUnless(shutil.which("rsync") and shutil.which("flock"), "deployment tools are not installed") @unittest.skipUnless(shutil.which("rsync") and shutil.which("flock"), "deployment tools are not installed")
class DeploymentTests(unittest.TestCase): class DeploymentTests(unittest.TestCase):
def test_deploy_preserves_runtime_and_archive_and_restarts_service(self): def test_deploy_preserves_runtime_and_archive_and_restarts_service(self):
self._run_deployment(archive_exists=True)
def test_deploy_creates_archive_before_restarting_service(self):
self._run_deployment(archive_exists=False)
def _run_deployment(self, *, archive_exists):
workflow = (Path(__file__).resolve().parents[1] / ".gitea/workflows/deploy.yml").read_text() workflow = (Path(__file__).resolve().parents[1] / ".gitea/workflows/deploy.yml").read_text()
deploy_step = workflow.split("- name: Deploy and restart\n", 1)[1] deploy_step = workflow.split("- name: Deploy and restart\n", 1)[1]
script = textwrap.dedent(deploy_step.split("run: |\n", 1)[1]) script = textwrap.dedent(deploy_step.split("run: |\n", 1)[1])
@@ -34,6 +40,8 @@ class DeploymentTests(unittest.TestCase):
".venv/keep.txt": "runtime venv", ".venv/keep.txt": "runtime venv",
".git/keep.txt": "existing checkout metadata", ".git/keep.txt": "existing checkout metadata",
} }
if not archive_exists:
del protected["data/archive/date=2026-11-01/samples.parquet"]
for relative, content in protected.items(): for relative, content in protected.items():
target = deployment / relative target = deployment / relative
target.parent.mkdir(parents=True, exist_ok=True) target.parent.mkdir(parents=True, exist_ok=True)
@@ -47,7 +55,7 @@ class DeploymentTests(unittest.TestCase):
runtime.chmod(0o755) runtime.chmod(0o755)
mocks = { mocks = {
"sudo": '#!/bin/sh\n[ "$1" = "-n" ] || exit 99\nshift\nexec "$@"\n', "sudo": '#!/bin/sh\n[ "$1" = "-n" ] || exit 99\nshift\nexec "$@"\n',
"systemctl": '#!/bin/sh\nprintf "%s\\n" "$*" >> "$SOLAR_DEPLOY_TEST_LOG"\nif [ "$1" = show ]; then printf "0\\n"; fi\n', "systemctl": '#!/bin/sh\nif [ "$1" = restart ] && [ ! -d "$DEPLOY_PATH/data/archive" ]; then echo "Archive directory is missing before service restart" >&2; exit 1; fi\nprintf "%s\\n" "$*" >> "$SOLAR_DEPLOY_TEST_LOG"\nif [ "$1" = show ]; then printf "0\\n"; fi\n',
"sleep": "#!/bin/sh\nexit 0\n", "sleep": "#!/bin/sh\nexit 0\n",
"curl": '#!/bin/sh\nprintf "curl %s\\n" "$*" >> "$SOLAR_DEPLOY_TEST_LOG"\n', "curl": '#!/bin/sh\nprintf "curl %s\\n" "$*" >> "$SOLAR_DEPLOY_TEST_LOG"\n',
} }
@@ -61,6 +69,10 @@ class DeploymentTests(unittest.TestCase):
"HEALTH_URL": "https://rpioffice.nest.lutran.ch/solar-relay/healthz"} "HEALTH_URL": "https://rpioffice.nest.lutran.ch/solar-relay/healthz"}
result = subprocess.run(["bash", "-c", script], cwd=checkout, env=env, capture_output=True, text=True, timeout=15) result = subprocess.run(["bash", "-c", script], cwd=checkout, env=env, capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((deployment / "data/archive").is_dir())
if not archive_exists:
for relative in ("data", "data/archive"):
self.assertEqual((deployment / relative).stat().st_mode & 0o777, 0o700)
self.assertTrue((deployment / "solar_relay/new.py").exists()) self.assertTrue((deployment / "solar_relay/new.py").exists())
self.assertFalse((deployment / "obsolete.py").exists()) self.assertFalse((deployment / "obsolete.py").exists())
for relative, content in protected.items(): for relative, content in protected.items():