"""Resolve the reviewed public-build source from GitHub before a deploy builds."""
from __future__ import annotations
import argparse
import base64
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any, Iterable
from check_public_build_contract import (
ROOT,
Contract,
Target,
build_args,
load_contract,
parse_values_document,
validate_values,
)
API_ROOT = "https://api.github.com"
class RemoteConfigMissing(RuntimeError):
"""The selected commit has no reviewed public-build configuration."""
class RemoteConfigUnavailable(RuntimeError):
"""The workflow token cannot read the authoritative GitHub source."""
def request_remote_values(
*, repository: str, ref: str, config_path: str, token: str
) -> dict[str, dict[str, str]]:
encoded_path = "/".join(urllib.parse.quote(segment, safe="") for segment in config_path.split("/"))
encoded_ref = urllib.parse.quote(ref, safe="")
request = urllib.request.Request(
f"{API_ROOT}/repos/{repository}/contents/{encoded_path}?ref={encoded_ref}",
headers={
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "omi-public-build-preflight",
},
)
try:
with urllib.request.urlopen(request, timeout=20) as response:
payload = json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
if exc.code == 404:
raise RemoteConfigMissing("reviewed public-build configuration is missing at the deployed ref") from exc
raise RemoteConfigUnavailable(f"reviewed public-build configuration is unavailable (HTTP {exc.code})") from exc
except urllib.error.URLError as exc:
raise RemoteConfigUnavailable("reviewed public-build configuration is unavailable") from exc
if (
not isinstance(payload, dict)
or payload.get("encoding") != "base64"
or not isinstance(payload.get("content"), str)
):
raise RemoteConfigUnavailable("reviewed public-build configuration metadata is invalid")
try:
content = base64.b64decode("".join(payload["content"].split()), validate=True).decode("utf-8")
return parse_values_document(json.loads(content), source="remote reviewed public-build configuration")
except (ValueError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RemoteConfigUnavailable("reviewed public-build configuration content is invalid") from exc
def select_targets(contract: Contract, names: Iterable[str] | None, all_targets: bool) -> list[Target]:
selected_names = sorted(contract.targets) if all_targets else list(names or [])
if not selected_names:
raise ValueError("specify --target or --all")
unknown = sorted(set(selected_names) - contract.targets.keys())
if unknown:
raise ValueError(f"unknown public-build targets: {', '.join(unknown)}")
return [contract.targets[name] for name in selected_names]
def write_github_output(path: Path, *, target: Target, values: dict[str, str]) -> None:
arguments = build_args(target, values)
with path.open("a", encoding="utf-8") as handle:
handle.write("build_args<<OMI_PUBLIC_BUILD_ARGS\n")
handle.write(f"{arguments}\n")
handle.write("OMI_PUBLIC_BUILD_ARGS\n")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--environment", required=True)
parser.add_argument("--target", action="append")
parser.add_argument("--all", action="store_true")
parser.add_argument("--repository", default=os.environ.get("GITHUB_REPOSITORY"))
parser.add_argument("--ref", default=os.environ.get("GITHUB_SHA"))
parser.add_argument("--token", default=os.environ.get("GITHUB_TOKEN"))
parser.add_argument("--contract", type=Path, default=ROOT / "config" / "public-build-contract.json")
parser.add_argument("--github-output", type=Path)
args = parser.parse_args(argv)
if args.github_output and args.all:
print("public-build configuration preflight failed: --github-output requires one --target", file=sys.stderr)
return 1
if not args.repository or not args.ref or not args.token:
print(
"public-build configuration preflight failed: repository, ref, and read-only GitHub token are required",
file=sys.stderr,
)
return 1
try:
contract = load_contract(args.contract)
targets = select_targets(contract, args.target, args.all)
values = request_remote_values(
repository=args.repository,
ref=args.ref,
config_path=contract.config_path,
token=args.token,
)
errors = validate_values(contract, values, targets, args.environment)
if errors:
raise ValueError("; ".join(errors))
if args.github_output:
write_github_output(args.github_output, target=targets[0], values=values[args.environment])
except (OSError, ValueError, RemoteConfigMissing, RemoteConfigUnavailable, json.JSONDecodeError) as exc:
print(f"public-build configuration preflight failed: {exc}", file=sys.stderr)
return 1
print(
"public-build configuration preflight passed: "
f"source=reviewed-ref, environment={args.environment}, targets={len(targets)}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())