diff --git a/.env.example b/.env.example index add3ed8..fa2d0e3 100644 --- a/.env.example +++ b/.env.example @@ -17,15 +17,10 @@ IMAGE_URL_ALLOWED_HOSTS= BUILD_COMMIT=development BUILD_TIME=unknown -# Optional read-only NAS photo mount; use deploy/docker-compose.photos.yml. -# NAS_PHOTOS_DIR=/volume2/photo -# PHOTO_ROOT=/photos +# Filesystem/NFS photo sources are retired. Use Synology Photos API or WebDAV. # Optional WebDAV credential reference; prefer a secret file under /config. # FRAME_FLOW_SOURCE_CLOUD_FILE=/config/webdav-cloud.secret -# Optional existing group with photo read permission; does not change NAS ACLs. -# NAS_PHOTOS_GID=101 - # Optional Synology Photos shared-space people filter. Configure a pinned NAS endpoint. # Login in the photo-source editor; only the session is saved in /config (mode 0600). SYNOLOGY_PHOTOS_URL= diff --git a/.github/workflows/community.yml b/.github/workflows/community.yml new file mode 100644 index 0000000..0236300 --- /dev/null +++ b/.github/workflows/community.yml @@ -0,0 +1,26 @@ +name: Community checks +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + verify: + # GitHub-hosted disposable runner only; never run forks on the NAS runner. + if: github.server_url == 'https://github.com' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.14' + - run: python -m pip install -e '.[test]' + - run: python -m ruff check pigway_frame_flow tests scripts + - run: python -m ruff format --check pigway_frame_flow tests scripts + - run: python -m mypy pigway_frame_flow + - run: python -m pytest -q + - run: python scripts/ci-audit.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index c5035e2..5f81ade 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,7 +8,7 @@ on: jobs: verify: # The NAS Docker runner must never execute code from external forks. - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + if: github.server_url != 'https://github.com' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest env: DOCKER_PIP_INDEX_URL: ${{ vars.DOCKER_PIP_INDEX_URL }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..852964c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + +Use Python 3.14.x and follow the development commands in README.md. Run Ruff +lint/format checks, mypy and pytest before submitting a pull request. Keep changes +focused and include a regression test for behavior fixes where practical. + +Use mock mode with device writes disabled. Do not commit `.env`, credentials, +private configuration, databases, photos, or production receipts. Tests must not +contact real frames or mutate production services. + +GitHub mirrors run community checks on disposable GitHub-hosted runners, including +fork PRs. Gitea uses the existing trusted NAS runner and rejects fork PRs; maintainers +review contributions before moving them to a trusted branch. Docker build/smoke +verification remains in Gitea CI. Production releases are maintainer-controlled; +contributors do not need access to production infrastructure. + +For security issues, follow SECURITY.md instead of posting exploit details publicly. +Contributions are provided under the project's MIT license; preserve third-party +licenses and provenance when introducing external code or assets. diff --git a/Dockerfile b/Dockerfile index 70815e2..48477b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,6 +37,7 @@ RUN --mount=type=cache,id=frameflow-pip-py314-${TARGETARCH},target=/root/.cache/ else python /build-tools/install.py --locked; fi COPY pigway_frame_flow ./pigway_frame_flow COPY vendor ./vendor +COPY LICENSE NOTICE.md ./ # Publish only the cleaned filesystem, without retaining build-stage layers. RUN rm -rf /usr/local/lib/python*/site-packages/pip \ /usr/local/lib/python*/site-packages/pip-*.dist-info \ diff --git a/README.md b/README.md index 85914c2..960464e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PIGWay Frame Flow +Development currently supports Python 3.14.x. See [contributing](CONTRIBUTING.md) and [security reporting](SECURITY.md). + [简体中文](README.zh-CN.md) | English PIGWay Frame Flow is a lightweight, self-hosted platform for programmable smart-frame automation. It runs independently on a NAS or server, with its own Web UI, image library, rendering, scheduler and REST API. KoKonna is a supported device integration; external applications do not need its credentials or SDK. @@ -47,7 +49,7 @@ Synology Photos API sources work without `/photos` or NFS mounts, with shared/cu - Seven templates: Daily Dashboard, Weather, Calendar, Quote, Photo + Text, Daily Poetry and System Alert. - Layout Studio for collages/posters with 1–12 images; optional OpenAI-compatible layout planning and source-driven automatic collages. - Device-bound schedules, ordered/random playlists, temporary priority overrides, restore and persistent operation history. -- Read-only NAS folders, Synology Photos person filtering, WebDAV and optional cloud-drive connectors. Each source has its own folder and filter settings. People filtering uses one shared Synology Photos sign-in; WebDAV and cloud-drive credentials belong to their respective sources. +- Synology Photos person filtering, WebDAV and optional cloud-drive connectors. Each source has its own folder and filter settings. People filtering uses one shared Synology Photos sign-in; WebDAV and cloud-drive credentials belong to their respective sources. - Optional weather, calendar, monitoring, AI, MQTT and webhook integrations. Provider configuration is required; an unconnected provider is not a working connection. Templates **render images**, including text-only templates. Choose a template, edit its data, generate a preview, then display it. Preview generation and saving template settings do not update the frame. Photo + Text and Daily Dashboard can include a selected local photo; other templates do not automatically choose one. Scheduled template jobs render afresh on each run. See [photo sources](docs/PHOTO_SOURCES.md), [automatic collages](docs/AUTOMATIC_COLLAGE.md) and [integration setup/limits](docs/EXTENSIONS.md). diff --git a/README.zh-CN.md b/README.zh-CN.md index 25d487d..51cbddb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,7 @@ # PIGWay Frame Flow +开发环境目前支持 Python 3.14.x。参见[贡献指南](CONTRIBUTING.md)和[安全问题报告](SECURITY.md)。 + 简体中文 | [English](README.md) 轻量、自托管的智能画框自动化平台,可独立运行在 NAS 或服务器上,提供自己的 Web 界面、图库、图片渲染、调度器和 REST API。KoKonna 是支持的设备集成之一;外部应用无需持有它的凭据或引入其 SDK。 @@ -47,7 +49,7 @@ Synology Photos 可使用独立的 API 来源,无需 `/photos` 或 NFS 挂载 - 七种模板:家庭早报、天气、日历、一句话、照片与文字、每日诗词、系统告警。 - 拼图工作室支持 1–12 张图片的拼图与海报;可选 OpenAI 兼容接口排版,以及从数据源自动选图拼接。 - 绑定设备的定时任务、顺序/随机播放列表、带优先级的临时覆盖、恢复与持久化操作历史。 -- 只读 NAS 文件夹、Synology Photos 人物筛选、WebDAV 和可选网盘连接器;每个数据源独立配置、独立授权。 +- Synology Photos 人物筛选、WebDAV 和可选网盘连接器;每个数据源独立配置、独立授权。 - 可选天气、日历、监控、AI、MQTT 与 Webhook 集成。需要配置对应 Provider;尚未授权的数据源不会被当作已连接。 模板会**生成图片**,纯文字模板也是如此。使用顺序是:选择模板 → 编辑内容 → 生成预览 → 显示到画框。生成预览和保存模板设置都不会更新画框。“照片与文字”和“家庭早报”可以包含指定的本地照片,其他模板不会自动选图;定时模板任务每次运行会重新渲染。参见[照片数据源](docs/PHOTO_SOURCES.md)、[自动拼图](docs/AUTOMATIC_COLLAGE.md)和[集成配置及限制](docs/EXTENSIONS.md)。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..901b9d4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,15 @@ +# Security policy + +Security fixes target the latest release. Older releases have no guaranteed +backport support. Upgrade before verifying whether an issue is still present. + +Do not publish credentials, private photos, production logs, or working exploit +payloads in public issues. Contact the repository owner privately through the +hosting platform when available. If no private contact channel is available, +open an issue asking for a confidential reporting channel without technical +vulnerability details. There is currently no dedicated security mailbox or +promised response SLA. + +A useful private report includes the affected version, deployment mode, required +permissions, impact, and minimal steps using mock data. Do not test on another +person's deployment or write to real frames without explicit authorization. diff --git a/docs/PUBLICATION.md b/docs/PUBLICATION.md index 97fef19..5ce3200 100644 --- a/docs/PUBLICATION.md +++ b/docs/PUBLICATION.md @@ -4,7 +4,9 @@ Runtime keys, passwords, sessions, private photos, databases and local `.env` fi must remain outside Git. Examples use documentation-only addresses and identities. Personal deployment reports and screenshots are excluded from public history. -The NAS Docker runner accepts trusted repository events only. External fork PRs +The Gitea NAS Docker runner accepts trusted repository events only. GitHub mirrors +use the separate community workflow on GitHub-hosted disposable runners, with +read-only repository permissions and no deployment configuration. External fork PRs are not executed on this runner; maintainers must review them before copying changes to a trusted branch. All lint, type, test, audit and isolated Docker checks remain. diff --git a/docs/deployment/GITEA_CD.md b/docs/deployment/GITEA_CD.md index 7f5f5de..c0f0181 100644 --- a/docs/deployment/GITEA_CD.md +++ b/docs/deployment/GITEA_CD.md @@ -1,4 +1,11 @@ -# Gitea Actions production CD v1 +# Gitea Actions production CD v1 (historical) + +> This NAS production deployment procedure is retired. Current Gitea Actions +> only runs isolated verification; production uses the maintainer-specific +> Ubuntu workflow in `scripts/server-release.py`. For a new installation use +> the Docker Compose instructions in README.md. Filesystem/NFS photo sources +> are disabled; use Synology Photos API or WebDAV. The configuration below is +> retained for historical reference and must not be used for new deployments. ## Audit and adoption boundary diff --git a/pyproject.toml b/pyproject.toml index d628693..3269eeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "pigway-frame-flow" version = "0.1.17" -requires-python = ">=3.12" +requires-python = ">=3.14,<3.15" dependencies = ["paho-mqtt==2.1.0", "python-dateutil==2.9.0.post0", "fastapi==0.142.2", "uvicorn==0.54.0", "pillow==12.3.0", "requests==2.34.2", "croniter==6.2.4", "python-multipart==0.0.32", "pyyaml==6.0.3"] [project.optional-dependencies] test = ["pytest==9.1.1", "httpx2==2.13.1", "mypy==2.3.1", "pip-audit==2.10.1", "types-requests==2.33.0.20260906", "types-PyYAML==6.0.12.20260906", "types-croniter==6.2.4.20260711", "ruff==0.16.9"] @@ -9,6 +9,8 @@ test = ["pytest==9.1.1", "httpx2==2.13.1", "mypy==2.3.1", "pip-audit==2.10.1", " pythonpath = ["."] testpaths = ["tests"] [tool.ruff] +# Preserve existing syntax/formatting independently of runtime support. +target-version = "py312" exclude = ["vendor", ".venv"] [build-system] requires = ["setuptools>=75"] diff --git a/tests/test_publication_security.py b/tests/test_publication_security.py index 5db71e1..36ef137 100644 --- a/tests/test_publication_security.py +++ b/tests/test_publication_security.py @@ -34,3 +34,22 @@ def test_actions_downloads_do_not_depend_on_personal_server(): r"https://github\.com/actions/(checkout|setup-python)@[0-9a-f]{40}", action, ) + + +def test_community_checks_are_isolated_from_private_runners(): + import yaml + + private = yaml.safe_load(Path(".github/workflows/verify.yml").read_text()) + public = yaml.safe_load(Path(".github/workflows/community.yml").read_text()) + assert ( + "github.server_url != 'https://github.com'" in private["jobs"]["verify"]["if"] + ) + job = public["jobs"]["verify"] + assert job["if"] == "github.server_url == 'https://github.com'" + assert job["runs-on"] == "ubuntu-latest" + assert public["permissions"] == {"contents": "read"} + workflow = Path(".github/workflows/community.yml").read_text() + assert "secrets." not in workflow + assert "production-deploy" not in workflow + assert "pull_request_target" not in workflow + assert "persist-credentials: false" in workflow