Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 95 additions & 2 deletions .github/scripts/cloud-run-simulation-entry-smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,57 @@ expected_revision="${2:-}"
# immediately before a traffic update. Supplying that revision as argument 3
# enables bounded retries for only that documented transition state.
previous_revision="${3:-}"
# Release identity, checked against /versions, whose route maps hold versions
# as `latest` and app names under each version key. The expectations are all
# or nothing: EXPECTED_IDENTITY=required (existing-stack deployments) demands
# every one, and setting any one demands the rest, so a missing value can
# never silently weaken the check.
# - EXPECTED_POLICYENGINE_VERSION: that policyengine.py version has a route
# and a recorded UK bundle identity for exactly that version;
# - EXPECTED_APP_NAME: the executor app just deployed for it; both that
# version and its UK model must route to this app;
# - EXPECTED_UK_MODEL_VERSION / EXPECTED_UK_DATASET_SHA256: that bundle's UK
# model and certified dataset SHA-256;
# - EXPECTED_LATEST=true (optional): unpinned requests route to that version
# and model.
# This is the identity the gateway reports from the routing registry; the
# numerical release sentinel checks what the worker computes.
expected_identity="${EXPECTED_IDENTITY:-none}"
expected_policyengine_version="${EXPECTED_POLICYENGINE_VERSION:-}"
expected_app_name="${EXPECTED_APP_NAME:-}"
expected_uk_model_version="${EXPECTED_UK_MODEL_VERSION:-}"
expected_uk_dataset_sha256="${EXPECTED_UK_DATASET_SHA256:-}"
expected_latest="${EXPECTED_LATEST:-}"

case "${expected_identity}" in
required | none) ;;
*)
printf 'EXPECTED_IDENTITY must be required or none, not %s\n' "${expected_identity}" >&2
exit 2
;;
esac
case "${expected_latest}" in
"" | true) ;;
*)
printf 'EXPECTED_LATEST must be true or empty, not %s\n' "${expected_latest}" >&2
exit 2
;;
esac
check_identity=false
if [ "${expected_identity}" = "required" ] ||
[ -n "${expected_policyengine_version}${expected_app_name}${expected_uk_model_version}${expected_uk_dataset_sha256}${expected_latest}" ]; then
check_identity=true
for name in EXPECTED_POLICYENGINE_VERSION EXPECTED_APP_NAME EXPECTED_UK_MODEL_VERSION EXPECTED_UK_DATASET_SHA256; do
if [ -z "${!name:-}" ]; then
printf '%s is required: release identity expectations are all or nothing\n' "${name}" >&2
exit 2
fi
done
if ! [[ "${expected_uk_dataset_sha256}" =~ ^[0-9a-f]{64}$ ]]; then
printf 'EXPECTED_UK_DATASET_SHA256 must be 64 lowercase hexadecimal characters\n' >&2
exit 2
fi
fi
routing_max_attempts="${SIMULATION_ENTRYPOINT_ROUTING_MAX_ATTEMPTS:-24}"
routing_retry_delay_seconds="${SIMULATION_ENTRYPOINT_ROUTING_RETRY_DELAY_SECONDS:-5}"

Expand All @@ -23,7 +74,8 @@ fi

health_headers="$(mktemp)"
health_body="$(mktemp)"
trap 'rm -f "${health_headers}" "${health_body}"' EXIT
versions_body="$(mktemp)"
trap 'rm -f "${health_headers}" "${health_body}" "${versions_body}"' EXIT

attempt=1
while true; do
Expand Down Expand Up @@ -80,7 +132,48 @@ done

curl --fail --silent --show-error "${base_url}/ready" |
jq -e '.status == "ready"' >/dev/null
curl --fail --silent --show-error "${base_url}/versions" >/dev/null
curl --fail --silent --show-error "${base_url}/versions" --output "${versions_body}"
jq -e 'type == "object"' "${versions_body}" >/dev/null

identity_failure() {
printf '%s: %s\n' "${base_url}" "$1" >&2
exit 1
}

if [ "${check_identity}" = "true" ]; then
jq -e --arg v "${expected_policyengine_version}" '
(.policyengine[$v] | type == "string" and length > 0)
and .bundles[$v].uk.policyengine_version == $v
' "${versions_body}" >/dev/null ||
identity_failure "/versions has no route and UK bundle identity for policyengine ${expected_policyengine_version}"

served_app="$(jq -r --arg v "${expected_policyengine_version}" '.policyengine[$v] // ""' "${versions_body}")"
[ "${served_app}" = "${expected_app_name}" ] ||
identity_failure "policyengine ${expected_policyengine_version} routes to ${served_app:-no app}, expected ${expected_app_name}"

served_uk_model="$(jq -r --arg v "${expected_policyengine_version}" '.bundles[$v].uk.model_version // ""' "${versions_body}")"
[ "${served_uk_model}" = "${expected_uk_model_version}" ] ||
identity_failure "policyengine ${expected_policyengine_version} records policyengine-uk ${served_uk_model:-none}, expected ${expected_uk_model_version}"
served_uk_app="$(jq -r --arg m "${served_uk_model}" '.uk[$m] // ""' "${versions_body}")"
[ "${served_uk_app}" = "${expected_app_name}" ] ||
identity_failure "policyengine-uk ${served_uk_model} routes to ${served_uk_app:-no app}, expected ${expected_app_name}"

served_dataset="$(jq -r --arg v "${expected_policyengine_version}" '.bundles[$v].uk.dataset_sha256 // ""' "${versions_body}")"
[ "${served_dataset}" = "${expected_uk_dataset_sha256}" ] ||
identity_failure "the UK dataset SHA-256 served for policyengine ${expected_policyengine_version} is ${served_dataset:-none}, expected ${expected_uk_dataset_sha256}"

if [ "${expected_latest}" = "true" ]; then
jq -e --arg v "${expected_policyengine_version}" --arg m "${served_uk_model}" '
.policyengine.latest == $v and .uk.latest == $m
' "${versions_body}" >/dev/null ||
identity_failure "latest routes to policyengine $(jq -r '.policyengine.latest // "none"' "${versions_body}") and policyengine-uk $(jq -r '.uk.latest // "none"' "${versions_body}"), not ${expected_policyengine_version} and ${served_uk_model}"
fi

printf '%s serves policyengine %s from %s, policyengine-uk %s, UK dataset %s%s\n' \
"${base_url}" "${expected_policyengine_version}" "${served_app}" \
"${served_uk_model}" "${served_dataset}" \
"$([ "${expected_latest}" = "true" ] && printf ' as latest')"
fi
curl --fail --silent --show-error \
--request POST \
--header "Content-Type: application/json" \
Expand Down
66 changes: 49 additions & 17 deletions .github/scripts/update-policyengine-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ create_pr_body_file() {
echo "- US: ${BUNDLED_US_DATA_VERSION:-resolved from bundle during update}"
echo "- UK: ${BUNDLED_UK_DATA_VERSION:-resolved from bundle during update}"
echo
echo "UK release identity:"
echo "- certified dataset SHA-256: ${BUNDLED_UK_DATASET_SHA256:-not recorded by this bundle}"
echo "- certificate digest (receipt v1): ${BUNDLED_UK_CERTIFICATE_SHA256:-not reported by this wrapper}"
echo "- release dispatch verified against the installed wheel: ${DISPATCH_VERIFIED:-false}"
echo "- qualification: ${UK_QUALIFICATION_VERDICT:-none recorded}${UK_QUALIFICATION_RECEIPT_SHA256:+ (receipt ${UK_QUALIFICATION_RECEIPT_SHA256})}"
echo
echo "---"
echo "Generated automatically by GitHub Actions."
} > "$pr_body_file"
Expand Down Expand Up @@ -185,23 +191,8 @@ if [[ -n "$EXISTING_PR" ]]; then
exit 0
fi

if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "Remote branch '${BRANCH}' already exists without an open PR. Creating PR."
create_update_pr
echo "PR created for existing branch ${BRANCH}"
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"

python3 "$PYTHON_HELPER" update-requirements \
--pyproject "$PYPROJECT" \
--package "$PACKAGE" \
--current "$CURRENT" \
--latest "$LATEST"

# Every PR, including one for a branch left by an earlier run, is opened only
# after the target wrapper's bundle and the release dispatch have been verified.
# Read the target wrapper's release manifest without resolving the project.
for attempt in 1 2 3; do
if BUNDLE_OUTPUT=$(
Expand Down Expand Up @@ -238,6 +229,18 @@ if [[ "$BUNDLED_POLICYENGINE_VERSION" != "$LATEST" ]]; then
echo "ERROR: Installed policyengine.py reports bundle ${BUNDLED_POLICYENGINE_VERSION}, expected ${LATEST}." >&2
exit 1
fi
BUNDLED_UK_DATASET_SHA256=$(printf '%s\n' "$BUNDLE_OUTPUT" | awk -F= '$1 == "uk_dataset_sha256" {print $2}')
BUNDLED_UK_CERTIFICATE_SHA256=$(printf '%s\n' "$BUNDLE_OUTPUT" | awk -F= '$1 == "uk_certificate_sha256" {print $2}')

# A policyengine-release dispatch may name the UK bundle it certified; the
# installed wheel must carry exactly that bundle.
if ! DISPATCH_OUTPUT=$(printf '%s\n' "$BUNDLE_OUTPUT" | python3 "$PYTHON_HELPER" verify-dispatch); then
echo "ERROR: The installed policyengine ${LATEST} bundle does not match the release dispatch." >&2
exit 1
fi
DISPATCH_VERIFIED=$(printf '%s\n' "$DISPATCH_OUTPUT" | awk -F= '$1 == "dispatch_verified" {print $2}')
UK_QUALIFICATION_VERDICT=$(printf '%s\n' "$DISPATCH_OUTPUT" | awk -F= '$1 == "uk_qualification_verdict" {print $2}')
UK_QUALIFICATION_RECEIPT_SHA256=$(printf '%s\n' "$DISPATCH_OUTPUT" | awk -F= '$1 == "uk_qualification_receipt_sha256" {print $2}')

echo "Bundled runtime pins:"
echo " policyengine==${BUNDLED_POLICYENGINE_VERSION}"
Expand All @@ -249,6 +252,35 @@ echo "Certified data releases:"
echo " us=${BUNDLED_US_DATA_VERSION}"
echo " uk=${BUNDLED_UK_DATA_VERSION}"

if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then
echo "Remote branch '${BRANCH}' already exists without an open PR. Verifying it."
git fetch --no-tags origin "refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}"
BRANCH_PYPROJECT="$(mktemp)"
git show "origin/${BRANCH}:${PROJECT_DIR}/pyproject.toml" > "$BRANCH_PYPROJECT"
BRANCH_PIN=$(python3 "$PYTHON_HELPER" current-version \
--pyproject "$BRANCH_PYPROJECT" \
--package "$PACKAGE")
rm -f "$BRANCH_PYPROJECT"
if [[ "$BRANCH_PIN" != "$LATEST" ]]; then
echo "ERROR: ${BRANCH} pins ${PACKAGE} ${BRANCH_PIN:-nothing}, expected ${LATEST}." >&2
exit 1
fi
echo "Creating PR for verified branch ${BRANCH}."
create_update_pr
echo "PR created for existing branch ${BRANCH}"
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"

python3 "$PYTHON_HELPER" update-requirements \
--pyproject "$PYPROJECT" \
--package "$PACKAGE" \
--current "$CURRENT" \
--latest "$LATEST"

# The PyPI Simple index can briefly lag the JSON API after a release, so retry
# the final project lock as well.
for attempt in 1 2 3; do
Expand Down
127 changes: 127 additions & 0 deletions .github/scripts/update_policyengine_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,36 @@ def data_release_version(country: str) -> str:
raise SystemExit(f"Bundle has no data release version for {country}")
return version

def data_package_version(country: str) -> str:
release = _mapping(data_releases.get(country))
version = _mapping(release.get("data_package")).get("version")
return version if isinstance(version, str) else ""

def dataset_sha256(country: str) -> str:
release = _mapping(data_releases.get(country))
default = release.get("default_dataset")
certified = _mapping(release.get("certified_data_artifact"))
if certified.get("dataset") == default and certified.get("sha256"):
value = certified["sha256"]
else:
reference = _mapping(_mapping(release.get("datasets")).get(default))
value = reference.get("sha256") or ""
digest = str(value).strip().lower()
if digest and not re.fullmatch(r"[0-9a-f]{64}", digest):
raise SystemExit(f"Bundle records a malformed {country} dataset SHA-256")
return digest

def certificate_sha256(country: str) -> str:
# Receipt-v1 certificate digest; older wrappers lack the helper.
try:
from policyengine.provenance.receipt import (
certificate_sha256 as digest,
)
except ImportError:
return ""
release = data_releases.get(country)
return digest(release) if isinstance(release, Mapping) else ""

outputs = {
"policyengine_version": package_version("policyengine"),
"policyengine_core_version": package_version("policyengine-core"),
Expand All @@ -217,11 +247,105 @@ def data_release_version(country: str) -> str:
"us_data_version": data_release_version("us"),
"uk_version": package_version("policyengine-uk"),
"uk_data_version": data_release_version("uk"),
"uk_data_package_version": data_package_version("uk"),
"us_dataset_sha256": dataset_sha256("us"),
"uk_dataset_sha256": dataset_sha256("uk"),
"uk_certificate_sha256": certificate_sha256("uk"),
}
for name, version in outputs.items():
print(f"{name}={version}")


#: Release-dispatch fields compared with the installed bundle: (payload key
#: under client_payload.bundle.uk, bundle-versions output key). policyengine.py
#: sends data_release_version as the data package version ("1.58.0"), not the
#: release record's build-style version.
DISPATCH_CHECKS = (
("model_version", "uk_version"),
("core_version", "policyengine_core_version"),
("data_release_version", "uk_data_package_version"),
("dataset_sha256", "uk_dataset_sha256"),
("certificate_sha256", "uk_certificate_sha256"),
)
#: Checks an installed wrapper may be too old to report; skipped when empty.
OPTIONAL_INSTALLED_CHECKS = frozenset({"certificate_sha256"})
#: Qualification verdicts a promoted pair can carry.
PROMOTABLE_VERDICTS = frozenset({"qualified", "needs_review"})


def verify_dispatch() -> None:
"""Check a policyengine-release payload against the installed bundle.

Reads the dispatch ``client_payload`` JSON from ``RELEASE_PAYLOAD`` and
``bundle-versions`` output from stdin. A payload without ``bundle.uk``
(older releases, manual runs) is accepted. A field the payload names
must equal the installed bundle's, and an installed bundle that cannot
report it is a mismatch, except for the certificate digest, which older
wrappers cannot compute. A qualification that says the pair was not
promotable is refused. ``dispatch_verified`` is true only when at least
one field was compared. Prints digests for the PR body.
"""

raw = os.environ.get("RELEASE_PAYLOAD", "").strip()
payload = json.loads(raw) if raw and raw != "null" else {}
installed = {}
for line in sys.stdin.read().splitlines():
key, _, value = line.partition("=")
if key:
installed[key.strip()] = value.strip()
if payload.get("version") and payload["version"] != installed.get(
"policyengine_version"
):
raise SystemExit(
f"Dispatch is for policyengine {payload['version']}, but "
f"{installed.get('policyengine_version')} is installed."
)
expected = _mapping(_mapping(payload.get("bundle")).get("uk"))
mismatches = []
compared = 0
for payload_key, installed_key in DISPATCH_CHECKS:
claimed = expected.get(payload_key)
actual = installed.get(installed_key, "")
if claimed in (None, ""):
continue
if actual == "":
if payload_key in OPTIONAL_INSTALLED_CHECKS:
continue
mismatches.append(
f"{payload_key}: dispatch {claimed}, installed bundle reports none"
)
continue
compared += 1
claimed_text = str(claimed).strip()
if payload_key.endswith("sha256"):
claimed_text = claimed_text.lower()
if claimed_text != actual:
mismatches.append(f"{payload_key}: dispatch {claimed}, installed {actual}")
if mismatches:
raise SystemExit(
"The release dispatch names a different UK bundle from the installed "
"wheel: " + "; ".join(mismatches)
)
qualification = _mapping(_mapping(payload.get("qualification")).get("uk"))
verdict = qualification.get("verdict") or ""
receipt_sha256 = qualification.get("receipt_sha256") or ""
if not isinstance(verdict, str) or not re.fullmatch(r"[a-z_]*", verdict):
raise SystemExit("The release dispatch carries a malformed verdict")
if not isinstance(receipt_sha256, str) or not re.fullmatch(
r"(?:[0-9a-f]{64})?", receipt_sha256
):
raise SystemExit("The release dispatch carries a malformed receipt digest")
if qualification.get("promotable") is False or (
verdict and verdict not in PROMOTABLE_VERDICTS
):
raise SystemExit(
f"The release dispatch says the UK pair was not promotable ({verdict})"
)
print(f"dispatch_verified={'true' if compared else 'false'}")
print(f"uk_qualification_verdict={verdict}")
print(f"uk_qualification_receipt_sha256={receipt_sha256}")


def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
Expand Down Expand Up @@ -250,6 +374,7 @@ def _parser() -> argparse.ArgumentParser:
pypi.add_argument("--package", required=True)

commands.add_parser("bundle-versions")
commands.add_parser("verify-dispatch")
return parser


Expand All @@ -269,6 +394,8 @@ def main() -> None:
latest_version(args.package)
elif args.command == "bundle-versions":
bundle_versions()
elif args.command == "verify-dispatch":
verify_dispatch()


if __name__ == "__main__":
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/check-policyengine-updates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ jobs:
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
LATEST_OVERRIDE: ${{ github.event.client_payload.version || inputs.version }}
# The whole dispatch payload, verified against the installed wheel's
# bundle by the helper; never interpolated into shell.
RELEASE_PAYLOAD: ${{ toJSON(github.event.client_payload) }}
run: bash .github/scripts/update-policyengine-package.sh
Loading
Loading