From e6362c4862e0806052af1efc16eeca97085cd8f4 Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Wed, 15 Jul 2026 16:20:07 +0200 Subject: [PATCH 1/2] Document static deploy action requirements --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7c044c1..bcb0b5e 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ A small, dependency-free Forgejo Action that synchronizes a completed static website build to a dedicated Bunny Storage Zone. +The runner needs Bash and Python 3.11 or newer. The action itself installs no +packages and executes no code from the Gandalf repository. + The deployer requires `index.html`, rejects symbolic links, compares files with a checksum manifest, uploads changed assets before HTML, deletes stale files only after successful uploads, and publishes the new manifest last. From d40d2837f570432bd5b12cf66b2adf72b731658a Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Wed, 15 Jul 2026 16:34:06 +0200 Subject: [PATCH 2/2] Test static deploy action without pip --- .forgejo/workflows/test.yml | 3 +- README.md | 2 +- tests/test_static_site.py | 100 ++++++++++++++++++++---------------- 3 files changed, 58 insertions(+), 47 deletions(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 9d2de22..d44e5ee 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -10,5 +10,4 @@ jobs: runs-on: docker steps: - uses: https://data.forgejo.org/actions/checkout@v6 - - run: python3 -m pip install pytest - - run: python3 -m pytest -q + - run: python3 -m unittest discover -s tests diff --git a/README.md b/README.md index bcb0b5e..6752ea5 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Set `BUNNY_STORAGE_ENDPOINT` when the Storage Zone is outside Frankfurt. ## Development ```bash -python3 -m pytest tests +python3 -m unittest discover -s tests ``` The canonical source lives in the private Gandalf infrastructure repository diff --git a/tests/test_static_site.py b/tests/test_static_site.py index 094b799..9487054 100644 --- a/tests/test_static_site.py +++ b/tests/test_static_site.py @@ -1,8 +1,9 @@ import importlib.util +import json from pathlib import Path import sys - -import pytest +import tempfile +import unittest MODULE_PATH = Path(__file__).resolve().parents[1] / "static_site.py" SPEC = importlib.util.spec_from_file_location("static_site_deploy", MODULE_PATH) @@ -24,47 +25,58 @@ def write_site(root: Path) -> None: (root / "assets" / "app.js").write_text("console.log('hello')") -def test_collect_files_requires_index(tmp_path): - with pytest.raises(DeployError, match="index.html"): - collect_files(tmp_path) +class StaticSiteTests(unittest.TestCase): + def test_collect_files_requires_index(self) -> None: + with tempfile.TemporaryDirectory() as directory: + with self.assertRaisesRegex(DeployError, "index.html"): + collect_files(Path(directory)) + + def test_collect_files_rejects_symlinks(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_site(root) + (root / "linked").symlink_to(root / "index.html") + with self.assertRaisesRegex(DeployError, "symbolic links"): + collect_files(root) + + def test_manifest_round_trip(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_site(root) + files = collect_files(root) + manifest = manifest_for(files) + parsed = parse_manifest(json.dumps(manifest).encode()) + self.assertEqual( + parsed["index.html"]["sha256"], files["index.html"].sha256 + ) + + def test_manifest_rejects_unsafe_remote_path(self) -> None: + raw = b'{"version": 1, "files": {"../other-site/index.html": {}}}' + with self.assertRaisesRegex(DeployError, "unsafe path"): + parse_manifest(raw) + + def test_bunny_plan_uploads_assets_before_html_and_removes_stale(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_site(root) + files = collect_files(root) + remote = { + "index.html": {"sha256": "old", "size": 1}, + "assets/app.js": {"sha256": "old", "size": 1}, + "old.css": {"sha256": "old", "size": 1}, + } + upload, delete = bunny_plan(files, remote) + self.assertEqual(upload, ["assets/app.js", "index.html"]) + self.assertEqual(delete, ["old.css"]) + + def test_bunny_plan_skips_unchanged_files(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + write_site(root) + files = collect_files(root) + remote = manifest_for(files)["files"] + self.assertEqual(bunny_plan(files, remote), ([], [])) -def test_collect_files_rejects_symlinks(tmp_path): - write_site(tmp_path) - (tmp_path / "linked").symlink_to(tmp_path / "index.html") - with pytest.raises(DeployError, match="symbolic links"): - collect_files(tmp_path) - - -def test_manifest_round_trip(tmp_path): - write_site(tmp_path) - files = collect_files(tmp_path) - manifest = manifest_for(files) - parsed = parse_manifest(__import__("json").dumps(manifest).encode()) - assert parsed["index.html"]["sha256"] == files["index.html"].sha256 - - -def test_manifest_rejects_unsafe_remote_path(): - raw = b'{"version": 1, "files": {"../other-site/index.html": {}}}' - with pytest.raises(DeployError, match="unsafe path"): - parse_manifest(raw) - - -def test_bunny_plan_uploads_assets_before_html_and_removes_stale(tmp_path): - write_site(tmp_path) - files = collect_files(tmp_path) - remote = { - "index.html": {"sha256": "old", "size": 1}, - "assets/app.js": {"sha256": "old", "size": 1}, - "old.css": {"sha256": "old", "size": 1}, - } - upload, delete = bunny_plan(files, remote) - assert upload == ["assets/app.js", "index.html"] - assert delete == ["old.css"] - - -def test_bunny_plan_skips_unchanged_files(tmp_path): - write_site(tmp_path) - files = collect_files(tmp_path) - remote = manifest_for(files)["files"] - assert bunny_plan(files, remote) == ([], []) +if __name__ == "__main__": + unittest.main()