[bug-7acxk8rf0g6b] feat(site-publish): add split-surface publishing #2

Merged
architect merged 4 commits from architect/bug-7acxk8rf0g6b/split-surface-publishing into main 2026-08-29 22:35:47 +00:00
3 changed files with 23 additions and 2 deletions
Showing only changes of commit 7b824a61ca - Show all commits
+3 -2
View File
@@ -136,8 +136,9 @@ provisional cache policy or a pointer to a missing immutable target.
Artifact input directories must be pairwise disjoint after filesystem
resolution. Publication stops before build or upload if one contains another or
escapes the repository, preventing protected input from entering a public
artifact. Split storage endpoints are pinned to Garage, and each website
escapes the repository. Descendant symlinks are also rejected, preventing
protected input from entering a public artifact through dereference. Split
storage endpoints are pinned to Garage, and each website
authority is derived from its bucket; a site cannot expose an arbitrary backend.
Each split route gets a bucket-specific `<bucket>.web.sjc001.fritzlab.net`
+9
View File
@@ -427,6 +427,8 @@ def normalize_site_config(raw, site_name):
def validate_artifact_inputs(site_dir, cfg):
"""Reject source containment before a public or protected build starts."""
if cfg["compatibility"]:
return
root = Path(site_dir).resolve()
sources = []
for artifact in cfg["artifacts"]:
@@ -435,6 +437,13 @@ def validate_artifact_inputs(site_dir, cfg):
raise ConfigError(
f"artifact {artifact['name']} content_dir resolves outside the repository"
)
if source.exists():
symlink = next((path for path in source.rglob("*") if path.is_symlink()), None)
if symlink is not None:
raise ConfigError(
f"artifact {artifact['name']} build input contains symlink: "
f"{symlink.relative_to(root)}"
)
sources.append((artifact["name"], source))
for index, (name, source) in enumerate(sources):
for other_name, other_source in sources[index + 1:]:
+11
View File
@@ -219,6 +219,17 @@ class ConfigContractTests(unittest.TestCase):
with self.assertRaisesRegex(ConfigError, "build inputs overlap after resolution"):
validate_artifact_inputs(root, cfg)
def test_descendant_symlink_cannot_cross_artifact_boundary(self):
cfg = normalize_site_config(self.raw, "baseline.fritzlab.net")
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "dist").mkdir()
(root / "portal" / "build").mkdir(parents=True)
(root / "portal" / "build" / "private.txt").write_text("private")
(root / "dist" / "portal-link").symlink_to(root / "portal" / "build")
with self.assertRaisesRegex(ConfigError, "build input contains symlink"):
validate_artifact_inputs(root, cfg)
class GenerationTests(unittest.TestCase):
def render(self, raw):