Compare commits
3 changed files with 49 additions and 63 deletions
|
|
@ -10,4 +10,5 @@ jobs:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
steps:
|
steps:
|
||||||
- uses: https://data.forgejo.org/actions/checkout@v6
|
- uses: https://data.forgejo.org/actions/checkout@v6
|
||||||
- run: python3 -m unittest discover -s tests
|
- run: python3 -m pip install pytest
|
||||||
|
- run: python3 -m pytest -q
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@
|
||||||
A small, dependency-free Forgejo Action that synchronizes a completed static
|
A small, dependency-free Forgejo Action that synchronizes a completed static
|
||||||
website build to a dedicated Bunny Storage Zone.
|
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
|
The deployer requires `index.html`, rejects symbolic links, compares files with
|
||||||
a checksum manifest, uploads changed assets before HTML, deletes stale files
|
a checksum manifest, uploads changed assets before HTML, deletes stale files
|
||||||
only after successful uploads, and publishes the new manifest last.
|
only after successful uploads, and publishes the new manifest last.
|
||||||
|
|
@ -60,7 +57,7 @@ Set `BUNNY_STORAGE_ENDPOINT` when the Storage Zone is outside Frankfurt.
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m unittest discover -s tests
|
python3 -m pytest tests
|
||||||
```
|
```
|
||||||
|
|
||||||
The canonical source lives in the private Gandalf infrastructure repository
|
The canonical source lives in the private Gandalf infrastructure repository
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
import unittest
|
import pytest
|
||||||
|
|
||||||
MODULE_PATH = Path(__file__).resolve().parents[1] / "static_site.py"
|
MODULE_PATH = Path(__file__).resolve().parents[1] / "static_site.py"
|
||||||
SPEC = importlib.util.spec_from_file_location("static_site_deploy", MODULE_PATH)
|
SPEC = importlib.util.spec_from_file_location("static_site_deploy", MODULE_PATH)
|
||||||
|
|
@ -25,58 +24,47 @@ def write_site(root: Path) -> None:
|
||||||
(root / "assets" / "app.js").write_text("console.log('hello')")
|
(root / "assets" / "app.js").write_text("console.log('hello')")
|
||||||
|
|
||||||
|
|
||||||
class StaticSiteTests(unittest.TestCase):
|
def test_collect_files_requires_index(tmp_path):
|
||||||
def test_collect_files_requires_index(self) -> None:
|
with pytest.raises(DeployError, match="index.html"):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
collect_files(tmp_path)
|
||||||
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), ([], []))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def test_collect_files_rejects_symlinks(tmp_path):
|
||||||
unittest.main()
|
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) == ([], [])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue