feat(gandalf): declare the not-found page in bunny.toml
All checks were successful
Test / test (push) Successful in 6s
All checks were successful
Test / test (push) Successful in 6s
How a site answers missing paths now belongs to its repository like its edge rules: bunny.toml declares [site] not_found as "default", "spa" or a page from the build such as "/404.html". The action reads and sends the table on start and finalize; the gateway validates it before the upload and applies it to the Storage Zone at finalization, recording and purging only when it changed. A deployment without [site] resets the default; older action versions leave the setting alone. The console's Settings tab only shows the applied value, and the admin API no longer sets it. The maintainer guide and llms.txt describe the setting under Configuration.
This commit is contained in:
parent
c242db40a1
commit
ffb46edc65
4 changed files with 72 additions and 28 deletions
|
|
@ -35,32 +35,33 @@ class DeployError(RuntimeError):
|
|||
"""A static-site deployment could not be completed safely."""
|
||||
|
||||
|
||||
def load_edge_rules(path: Path) -> list[dict[str, Any]]:
|
||||
"""Read the [[edge_rules]] tables of a TOML file.
|
||||
def load_site_config(path: Path) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||
"""Read a site's bunny.toml: its [[edge_rules]] and its [site] settings.
|
||||
|
||||
Only the structure is checked here; the Gandalf gateway validates the rules
|
||||
when a deployment starts, before anything is uploaded.
|
||||
Only the structure is checked here; the Gandalf gateway validates both when
|
||||
a deployment starts, before anything is uploaded.
|
||||
"""
|
||||
try:
|
||||
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
except OSError as exc:
|
||||
raise DeployError(f"cannot read edge rules {path}: {exc}") from exc
|
||||
raise DeployError(f"cannot read {path}: {exc}") from exc
|
||||
except tomllib.TOMLDecodeError as exc:
|
||||
raise DeployError(f"edge rules {path} are not valid TOML: {exc}") from exc
|
||||
unknown = set(data) - {"edge_rules"}
|
||||
raise DeployError(f"{path} is not valid TOML: {exc}") from exc
|
||||
unknown = set(data) - {"edge_rules", "site"}
|
||||
if unknown:
|
||||
raise DeployError(
|
||||
f"edge rules {path} has unknown keys: {', '.join(sorted(unknown))}"
|
||||
)
|
||||
raise DeployError(f"{path} has unknown keys: {', '.join(sorted(unknown))}")
|
||||
rules = data.get("edge_rules", [])
|
||||
if not isinstance(rules, list) or not all(isinstance(rule, dict) for rule in rules):
|
||||
raise DeployError(f"edge rules {path}: edge_rules must be [[edge_rules]] tables")
|
||||
raise DeployError(f"{path}: edge_rules must be [[edge_rules]] tables")
|
||||
site = data.get("site", {})
|
||||
if not isinstance(site, dict):
|
||||
raise DeployError(f"{path}: site must be a [site] table")
|
||||
try:
|
||||
# Dates and times have no JSON form and no meaning in a rule.
|
||||
json.dumps(rules)
|
||||
# Dates and times have no JSON form and no meaning here.
|
||||
json.dumps([rules, site])
|
||||
except TypeError as exc:
|
||||
raise DeployError(f"edge rules {path} contain an unsupported value: {exc}") from exc
|
||||
return rules
|
||||
raise DeployError(f"{path} contains an unsupported value: {exc}") from exc
|
||||
return rules, site
|
||||
|
||||
|
||||
def positive_int(value: str) -> int:
|
||||
|
|
@ -424,11 +425,14 @@ def start_bunny_deployment(
|
|||
idempotency_key: str,
|
||||
revision: str | None,
|
||||
edge_rules: list[dict[str, Any]] | None = None,
|
||||
site: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {"idempotency_key": idempotency_key, "revision": revision}
|
||||
if edge_rules is not None:
|
||||
# Sent here as well so the gateway rejects broken rules before the upload.
|
||||
payload["edge_rules"] = edge_rules
|
||||
if site is not None:
|
||||
payload["site"] = site
|
||||
result = deployment_gateway_request(
|
||||
endpoint,
|
||||
zone,
|
||||
|
|
@ -457,6 +461,7 @@ def finalize_bunny_deployment(
|
|||
revision: str | None,
|
||||
manifest_sha256: str,
|
||||
edge_rules: list[dict[str, Any]] | None = None,
|
||||
site: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {
|
||||
"idempotency_key": idempotency_key,
|
||||
|
|
@ -465,6 +470,8 @@ def finalize_bunny_deployment(
|
|||
}
|
||||
if edge_rules is not None:
|
||||
payload["edge_rules"] = edge_rules
|
||||
if site is not None:
|
||||
payload["site"] = site
|
||||
result = deployment_gateway_request(
|
||||
endpoint,
|
||||
zone,
|
||||
|
|
@ -512,14 +519,17 @@ def deploy_bunny(args: argparse.Namespace) -> dict[str, Any]:
|
|||
edge_rules_path = getattr(args, "edge_rules", None)
|
||||
if edge_rules_path and not args.purge_endpoint:
|
||||
raise DeployError("edge rules can only be applied through BUNNY_PURGE_ENDPOINT")
|
||||
# Through the gateway, no file means no rules: it removes rules from earlier deploys.
|
||||
edge_rules = load_edge_rules(edge_rules_path) if edge_rules_path else []
|
||||
# Through the gateway, no file means no rules and default site settings:
|
||||
# it removes what earlier deployments declared.
|
||||
edge_rules, site_settings = (
|
||||
load_site_config(edge_rules_path) if edge_rules_path else ([], {})
|
||||
)
|
||||
files = collect_files(args.directory)
|
||||
manifest = manifest_for(files)
|
||||
manifest_sha256 = hashlib.sha256(manifest_bytes(manifest)).hexdigest()
|
||||
revision = args.revision.strip() or None
|
||||
rules_sha256 = hashlib.sha256(
|
||||
json.dumps(edge_rules, sort_keys=True).encode()
|
||||
json.dumps([edge_rules, site_settings], sort_keys=True).encode()
|
||||
).hexdigest()
|
||||
idempotency_key = args.idempotency_key.strip() or hashlib.sha256(
|
||||
f"{args.zone}\0{revision or ''}\0{manifest_sha256}\0{rules_sha256}".encode()
|
||||
|
|
@ -535,6 +545,7 @@ def deploy_bunny(args: argparse.Namespace) -> dict[str, Any]:
|
|||
"purged": False,
|
||||
"deployment": None,
|
||||
"edge_rules": [rule.get("name") for rule in edge_rules],
|
||||
"site": site_settings,
|
||||
}
|
||||
|
||||
def plan(storage: BunnyStorage) -> tuple[list[str], list[str]]:
|
||||
|
|
@ -567,6 +578,7 @@ def deploy_bunny(args: argparse.Namespace) -> dict[str, Any]:
|
|||
idempotency_key,
|
||||
revision,
|
||||
edge_rules,
|
||||
site_settings,
|
||||
)
|
||||
try:
|
||||
if identity:
|
||||
|
|
@ -596,6 +608,7 @@ def deploy_bunny(args: argparse.Namespace) -> dict[str, Any]:
|
|||
revision,
|
||||
manifest_sha256,
|
||||
edge_rules,
|
||||
site_settings,
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
|
|
@ -693,7 +706,7 @@ def parser() -> argparse.ArgumentParser:
|
|||
"--edge-rules",
|
||||
type=Path,
|
||||
default=os.environ.get("STATIC_SITE_EDGE_RULES") or None,
|
||||
help="TOML file with [[edge_rules]] for the site's Pull Zones (needs the gateway).",
|
||||
help="The site's bunny.toml: [[edge_rules]] and [site] settings (needs the gateway).",
|
||||
)
|
||||
bunny.add_argument(
|
||||
"--idempotency-key",
|
||||
|
|
|
|||
Loading…
Reference in a new issue