Files
Evelyn ChenandClaude Fable 5.1 3dfee64335
Test / contract (pull_request) Successful in 7s
feat(site-publish): scope publication with an artifacts selection
A repository whose artifacts ship on different cadences has no way to
publish one of them. Baseline needs it: every merge to main must put the
catalogue live in under five minutes, while `dist/` is content-addressed
and may only be written by a tag release. Today the action iterates
cfg["artifacts"] unconditionally, so the only lever is deleting the
distributions artifact from site.yaml — which changes the stored
publication contract and drives the route-retirement path.

The new `artifacts:` input names the subset this run builds and
publishes. Selection scopes the build, the immutable preflight, the CORS
reconcile, the S3 sync, and credential resolution. It deliberately does
not scope manifest rendering or the immutable-path history: those stay
whole, so a scoped run can never retire another artifact's route or
delete its bucket contents. An undeclared name fails before the first
bucket is touched; `enabled: false` refuses a selection because
decommissioning is whole-site.

Default is unchanged: no input publishes every declared artifact.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjQqc4qFmdpAWaYfy2Aypb
2026-09-06 00:53:35 +00:00

78 lines
2.5 KiB
Python

"""Build each declared static-content artifact independently."""
import shutil
import subprocess
import tempfile
from pathlib import Path
from utils import (
EXCLUDE_FILES,
env,
parse_site_yaml,
run,
selected_artifacts,
validate_artifact_inputs,
)
def build_artifact(site_dir, artifact):
html_dir = site_dir / artifact["build_dir"]
if html_dir.parent.exists():
shutil.rmtree(html_dir.parent)
content_dir = artifact["content_dir"]
src = site_dir / content_dir if content_dir else site_dir
if not src.exists():
raise FileNotFoundError(f"artifact {artifact['name']} content_dir not found: {src}")
if artifact["type"] == "static":
print(f"Copying artifact {artifact['name']} from {src}")
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp) / "html"
shutil.copytree(src, tmp_path, dirs_exist_ok=True)
for name in EXCLUDE_FILES:
p = tmp_path / name
if p.is_dir():
shutil.rmtree(p)
elif p.exists():
p.unlink()
html_dir.parent.mkdir(parents=True)
shutil.move(str(tmp_path), str(html_dir))
elif artifact["type"] == "hugo":
print(f"Building Hugo artifact {artifact['name']} from {src}")
run(["hugo", "--source", str(src), "--destination", str(html_dir)])
elif artifact["type"] == "mkdocs":
print(f"Building MkDocs artifact {artifact['name']} from {src}")
run(["mkdocs", "build", "-d", str(html_dir)], cwd=src)
if artifact["tidy"]:
print("Running tidy on HTML files...")
for html_file in html_dir.rglob("*.html"):
subprocess.run(
["tidy", "-modify", "-quiet",
"--wrap", "0", "--indent", "auto", "--indent-spaces", "2",
"--drop-empty-elements", "no", "--tidy-mark", "no",
"--show-warnings", "no", str(html_file)],
check=False,
)
if not any(path.is_file() for path in html_dir.rglob("*")):
raise FileNotFoundError(f"artifact {artifact['name']} produced no files in {html_dir}")
print(f"Artifact {artifact['name']} complete — content at {html_dir}")
def cmd_build():
site_dir = Path(env("SITE_DIR"))
cfg = parse_site_yaml(site_dir)
if not cfg["enabled"]:
print("Site disabled — skipping build")
return
validate_artifact_inputs(site_dir, cfg)
for artifact in selected_artifacts(cfg):
build_artifact(site_dir, artifact)