diff --git a/.github/renovate-docker.json b/.github/renovate-docker.json index 8b27209df..1d3036d64 100644 --- a/.github/renovate-docker.json +++ b/.github/renovate-docker.json @@ -446,7 +446,12 @@ "apps/geekbench/5/**", "apps/headscale/0.23.0-alpha3/**", "apps/headscale/0.26.1/**", - "apps/mysql/5.5.62/**" + "apps/headscale/0.27.1/**", + "apps/headscale/0.28.0/**", + "apps/immich/1.122.3/**", + "apps/mysql/5.5.62/**", + "apps/safeline/7.3.1/**", + "apps/safeline/newnet-7.3.1/**" ], "enabled": false }, @@ -472,6 +477,21 @@ ], "groupName": "ClearFlask application images" }, + { + "description": "Group SafeLine release images so all application components update together", + "matchFileNames": [ + "apps/safeline/**/docker-compose.yml" + ], + "matchPackageNames": [ + "chaitin/safeline-chaos", + "chaitin/safeline-detector", + "chaitin/safeline-fvm", + "chaitin/safeline-luigi", + "chaitin/safeline-mgt", + "chaitin/safeline-tengine" + ], + "groupName": "SafeLine application images" + }, { "description": "Disable bundled database-only updates for LinuxServer multi-service apps", "matchFileNames": [ @@ -529,12 +549,23 @@ ], "matchPackageNames": [ "busybox", + "langgenius/dify-plugin-daemon", "nginx", "semitechnologies/weaviate", "ubuntu/squid" ], "enabled": false }, + { + "description": "Keep uuWAF on the upstream-supported Percona 5.7 database line", + "matchFileNames": [ + "apps/uuwaf/**/docker-compose.yml" + ], + "matchPackageNames": [ + "percona/percona-server" + ], + "enabled": false + }, { "description": "Disable sidecar-only Renovate updates for DooTask multi-service tracks", "matchFileNames": [ @@ -545,6 +576,16 @@ ], "enabled": false }, + { + "description": "Keep Langflow on the tested 1.10 security line pending baseline x86_64 support", + "matchFileNames": [ + "apps/langflow/**/docker-compose.yml" + ], + "matchPackageNames": [ + "langflowai/langflow" + ], + "enabled": false + }, { "description": "Disable sidecar-only Renovate updates for Diskover multi-service tracks", "matchFileNames": [ diff --git a/.github/renovate-primary-services.json b/.github/renovate-primary-services.json index 0a28da414..7e4f8c13a 100644 --- a/.github/renovate-primary-services.json +++ b/.github/renovate-primary-services.json @@ -12,8 +12,7 @@ "worker", "worker_beat", "web", - "sandbox", - "plugin_daemon" + "sandbox" ], "diskover-linuxserver": [ "diskover" @@ -21,6 +20,9 @@ "karakeep": [ "karakeep" ], + "immich": [ + "immich-server" + ], "librechat": [ "librechat-api", "librechat-admin", @@ -32,9 +34,15 @@ "rocketchat": [ "rocketchat" ], + "safeline": [ + "safeline-mgt" + ], "teldrive": [ "teldrive" ], + "uuwaf": [ + "uuwaf" + ], "weblate": [ "weblate" ], diff --git a/.github/scripts/test_renovate_sidecar_guard.py b/.github/scripts/test_renovate_sidecar_guard.py index f53d10024..a30103489 100644 --- a/.github/scripts/test_renovate_sidecar_guard.py +++ b/.github/scripts/test_renovate_sidecar_guard.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import importlib.util +import json import unittest from pathlib import Path @@ -10,6 +11,10 @@ GUARD = importlib.util.module_from_spec(SPEC) assert SPEC and SPEC.loader SPEC.loader.exec_module(GUARD) +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +PRIMARY_SERVICES_PATH = REPOSITORY_ROOT / ".github" / "renovate-primary-services.json" +RENOVATE_CONFIG_PATH = REPOSITORY_ROOT / ".github" / "renovate-docker.json" + def compose(**images: str) -> dict: return {"services": {name: {"image": image} for name, image in images.items()}} @@ -79,5 +84,105 @@ class InferredPrimaryDecisionTests(unittest.TestCase): self.assertEqual(decision.detail, "single-service compose") +class RepositoryPolicyTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.primary_services = json.loads(PRIMARY_SERVICES_PATH.read_text(encoding="utf-8")) + cls.renovate_config = json.loads(RENOVATE_CONFIG_PATH.read_text(encoding="utf-8")) + + def disabled_rule_exists(self, file_pattern: str, package_name: str | None = None) -> bool: + for rule in self.renovate_config["packageRules"]: + if rule.get("enabled") is not False: + continue + if file_pattern not in rule.get("matchFileNames", []): + continue + if package_name is None or package_name in rule.get("matchPackageNames", []): + return True + return False + + def test_dify_plugin_daemon_is_not_a_primary_service(self) -> None: + self.assertNotIn("plugin_daemon", self.primary_services["dify"]) + self.assertTrue( + self.disabled_rule_exists( + "apps/dify/**/docker-compose.yml", + "langgenius/dify-plugin-daemon", + ) + ) + + base = compose(api="langgenius/dify-api:1.16.0", plugin_daemon="langgenius/dify-plugin-daemon:0.6.3-local") + head = compose(api="langgenius/dify-api:1.16.0", plugin_daemon="langgenius/dify-plugin-daemon:0.6.5-local") + decision = GUARD.compare_compose( + "dify", + "apps/dify/1.16.0/docker-compose.yml", + base, + head, + self.primary_services["dify"], + ) + + self.assertEqual(decision.outcome, "close") + + def test_safeline_components_are_grouped_behind_management_service(self) -> None: + self.assertEqual(self.primary_services["safeline"], ["safeline-mgt"]) + expected_images = { + "chaitin/safeline-chaos", + "chaitin/safeline-detector", + "chaitin/safeline-fvm", + "chaitin/safeline-luigi", + "chaitin/safeline-mgt", + "chaitin/safeline-tengine", + } + grouped_rules = [ + rule + for rule in self.renovate_config["packageRules"] + if rule.get("groupName") == "SafeLine application images" + ] + + self.assertEqual(len(grouped_rules), 1) + self.assertEqual(set(grouped_rules[0]["matchPackageNames"]), expected_images) + self.assertIn("apps/safeline/**/docker-compose.yml", grouped_rules[0]["matchFileNames"]) + + def test_blocked_historical_tracks_are_immutable(self) -> None: + expected_patterns = { + "apps/headscale/0.27.1/**", + "apps/headscale/0.28.0/**", + "apps/immich/1.122.3/**", + "apps/safeline/7.3.1/**", + "apps/safeline/newnet-7.3.1/**", + } + + for pattern in expected_patterns: + with self.subTest(pattern=pattern): + self.assertTrue(self.disabled_rule_exists(pattern)) + + def test_uuwaf_database_is_not_updated_independently(self) -> None: + self.assertEqual(self.primary_services["uuwaf"], ["uuwaf"]) + self.assertTrue( + self.disabled_rule_exists( + "apps/uuwaf/**/docker-compose.yml", + "percona/percona-server", + ) + ) + + base = compose(uuwaf="uusec/nanqiang:v6.8.0", wafdb="percona/percona-server:5.7.44") + head = compose(uuwaf="uusec/nanqiang:v6.8.0", wafdb="percona/percona-server:8.4.2") + decision = GUARD.compare_compose( + "uuwaf", + "apps/uuwaf/6.8.0/docker-compose.yml", + base, + head, + self.primary_services["uuwaf"], + ) + + self.assertEqual(decision.outcome, "close") + + def test_langflow_updates_require_manual_compatibility_review(self) -> None: + self.assertTrue( + self.disabled_rule_exists( + "apps/langflow/**/docker-compose.yml", + "langflowai/langflow", + ) + ) + + if __name__ == "__main__": unittest.main() diff --git a/apps/immich/README.md b/apps/immich/README.md index d9f68ba06..f7f6850aa 100644 --- a/apps/immich/README.md +++ b/apps/immich/README.md @@ -24,6 +24,7 @@ Immich is a high-performance open source photo and video management platform for ## 部署说明 - 本应用使用 Docker Compose 在 1Panel 中部署。 +- `1.122.3` 和 `release` 是为旧安装保留的社区镜像版本,使用 `altran1502/immich-*`,不是 Immich 官方镜像;新安装应优先选择使用 `ghcr.io/immich-app/*` 官方镜像的 `3.x` 固定版本。 - 应用分类:媒体。 - 支持架构:amd64。 - 可选版本以应用商店页面为准。