Compare commits

...
Author SHA1 Message Date
d40d2837f5 Test static deploy action without pip
All checks were successful
Test / test (push) Successful in 12s
2026-07-15 16:34:06 +02:00
e6362c4862 Document static deploy action requirements 2026-07-15 16:20:07 +02:00
3 changed files with 63 additions and 49 deletions

View file

@ -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

View file

@ -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.
@ -57,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

View file

@ -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()