Preserve Photos deployment configuration and verify source dependencies

This commit is contained in:
Pigway
2026-10-08 19:25:37 +08:00
parent bb452649b8
commit 0b9b548d20
5 changed files with 100 additions and 0 deletions
+2
View File
@@ -56,6 +56,8 @@ jobs:
PROD_DATA_DIR: ${{ vars.PROD_DATA_DIR }} PROD_DATA_DIR: ${{ vars.PROD_DATA_DIR }}
PROD_CONFIG_DIR: ${{ vars.PROD_CONFIG_DIR }} PROD_CONFIG_DIR: ${{ vars.PROD_CONFIG_DIR }}
PROD_PHOTOS_DIR: ${{ vars.PROD_PHOTOS_DIR }} PROD_PHOTOS_DIR: ${{ vars.PROD_PHOTOS_DIR }}
SYNOLOGY_PHOTOS_URL: ${{ vars.SYNOLOGY_PHOTOS_URL }}
SYNOLOGY_PHOTOS_CERT_SHA256: ${{ vars.SYNOLOGY_PHOTOS_CERT_SHA256 }}
PROD_WEB_BIND: ${{ vars.PROD_WEB_BIND }} PROD_WEB_BIND: ${{ vars.PROD_WEB_BIND }}
PROD_WEB_PORT: ${{ vars.PROD_WEB_PORT }} PROD_WEB_PORT: ${{ vars.PROD_WEB_PORT }}
PROD_STATE_VOLUME: ${{ vars.PROD_STATE_VOLUME }} PROD_STATE_VOLUME: ${{ vars.PROD_STATE_VOLUME }}
+20
View File
@@ -301,3 +301,23 @@ and change the workflow pin. All validation steps remain unchanged.
Python toolcache misses may still require setup-python to download Python from Python toolcache misses may still require setup-python to download Python from
its upstream release source. This removes Action source download dependence, its upstream release source. This removes Action source download dependence,
not every possible external download. No Runner or NAS network configuration is changed. not every possible external download. No Runner or NAS network configuration is changed.
### Synology Photos dependencies
For a production photo source using Synology Photos person filtering, configure
`SYNOLOGY_PHOTOS_URL` and `SYNOLOGY_PHOTOS_CERT_SHA256` as Gitea Variables. The
certificate fingerprint must match the configured HTTPS endpoint. Keep the Photos
session in the persistent `/config` directory; never put session credentials in
Variables or repository files.
`PROD_PHOTOS_DIR` must identify the actual NAS photo directory mounted read-only
at `/photos`. Adoption requires that mount to exist on the current container;
changing this Variable alone does not create a new mount. Existing supplementary
permission groups are preserved by deployment.
Restoring SQLite and session files does not restore these runtime dependencies.
Production smoke checks now reject an enabled people-filtered source if its
Photos configuration or stored connection is missing. These checks do not prove
session validity or scan the complete catalog: after restoring a source, also
verify its read-only `/api/v1/photo-sources/{id}/images?limit=1` endpoint before
running an automation task.
+3
View File
@@ -376,6 +376,9 @@ class Deploy:
service["image"] = image service["image"] = image
proposed = self.secret_config() proposed = self.secret_config()
service["environment"].update(proposed) service["environment"].update(proposed)
for key in ("SYNOLOGY_PHOTOS_URL", "SYNOLOGY_PHOTOS_CERT_SHA256"):
if self.env.get(key):
service["environment"][key] = self.env[key]
service["environment"].update( service["environment"].update(
KOKONNA_MOCK=self.env["PROD_MOCK"], KOKONNA_MOCK=self.env["PROD_MOCK"],
KOKONNA_WRITE_ENABLED=self.env["PROD_WRITE_ENABLED"], KOKONNA_WRITE_ENABLED=self.env["PROD_WRITE_ENABLED"],
+19
View File
@@ -1,5 +1,6 @@
"""Read-only HTTP smoke checks. No frame writes; token read from env/private .env.""" """Read-only HTTP smoke checks. No frame writes; token read from env/private .env."""
import json
import os import os
from pathlib import Path from pathlib import Path
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
@@ -26,4 +27,22 @@ for path in [
body = response.read() body = response.read()
print(path, "HTTP", response.status, "bytes", len(body)) print(path, "HTTP", response.status, "bytes", len(body))
assert response.status == 200 assert response.status == 200
# A restored job is not usable merely because the scheduler is ready.
request = Request(
base + "/api/v1/photo-sources", headers={"Authorization": "Bearer " + token}
)
with urlopen(request, timeout=15) as response:
sources = json.load(response)
if any(source.get("enabled") and source.get("people_filter") for source in sources):
request = Request(
base + "/api/v1/integrations/synology-photos",
headers={"Authorization": "Bearer " + token},
)
with urlopen(request, timeout=15) as response:
photos = json.load(response)
if not photos.get("configured") or not photos.get("connected"):
raise RuntimeError(
"Enabled people-filtered photo source requires Photos connection"
)
print("Synology Photos dependency configured and connected")
print("Read-only smoke checks passed") print("Read-only smoke checks passed")
+56
View File
@@ -0,0 +1,56 @@
"""Production smoke must reject incomplete restored photo dependencies."""
import io
import json
import runpy
from pathlib import Path
from urllib.request import Request
import pytest
@pytest.mark.parametrize(
"sources,connection,passes",
[
([], {}, True),
([{"enabled": False, "people_filter": True}], {}, True),
(
[{"enabled": True, "people_filter": True}],
{"configured": False, "connected": False},
False,
),
(
[{"enabled": True, "people_filter": True}],
{"configured": True, "connected": False},
False,
),
(
[{"enabled": True, "people_filter": True}],
{"configured": True, "connected": True},
True,
),
],
)
def test_smoke_validates_people_filter_dependency(
monkeypatch, sources, connection, passes
):
calls = []
class Response(io.BytesIO):
status = 200
def read_only(request: Request, timeout):
assert request.get_method() == "GET"
calls.append(request.full_url)
value = sources if request.full_url.endswith("/photo-sources") else connection
return Response(json.dumps(value).encode())
monkeypatch.setenv("FRAME_FLOW_TOKEN", "fixture")
monkeypatch.setattr("urllib.request.urlopen", read_only)
script = Path(__file__).parents[1] / "scripts/smoke-test.py"
if passes:
runpy.run_path(str(script))
else:
with pytest.raises(RuntimeError, match="requires Photos connection"):
runpy.run_path(str(script))
assert len(calls) >= 10