From f8567c88e3c31c452c1ff175026438be1d46549d Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:06:16 -0700 Subject: [PATCH 1/9] Add Repo Radius state E2E workflow Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8a31277c-6702-45f2-876b-013c8a0ec466 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .../precreate-repo-radius-state-package.sh | 253 ++++++ .github/scripts/test-repo-radius-state-e2e.sh | 773 ++++++++++++++++++ .github/workflows/repo-radius-state-e2e.yaml | 165 ++++ .../README.md | 28 + .../running-functional-tests.md | 2 + .../testdata/repo-radius-state-app.bicep | 32 + 6 files changed, 1253 insertions(+) create mode 100644 .github/scripts/precreate-repo-radius-state-package.sh create mode 100644 .github/scripts/test-repo-radius-state-e2e.sh create mode 100644 .github/workflows/repo-radius-state-e2e.yaml create mode 100644 test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep diff --git a/.github/scripts/precreate-repo-radius-state-package.sh b/.github/scripts/precreate-repo-radius-state-package.sh new file mode 100644 index 0000000000..9ce1d2f5d5 --- /dev/null +++ b/.github/scripts/precreate-repo-radius-state-package.sh @@ -0,0 +1,253 @@ +#!/bin/bash + +# Creates or verifies the private GHCR package used by the Repo Radius state +# rehydration workflow. + +set -euo pipefail + +readonly SCRIPT_NAME="$(basename "$0")" +readonly BOOTSTRAP_TAG="bootstrap" +readonly BOOTSTRAP_ARTIFACT_TYPE="application/vnd.radius.statearchive.bootstrap.v1" + +PACKAGE="" +SOURCE_REPOSITORY="" +WORK_DIR="" + +usage() { + cat < \\ + --source-repository + +Creates a harmless bootstrap version when the package is absent, then verifies +that the package is private or internal and linked to the source repository. + +Prerequisites: + - gh authenticated as a user allowed to create the package + - the active gh token includes write:packages + - oras and jq are installed +EOF +} + +fail() { + echo "[precreate-state-package] ERROR: $*" >&2 + exit 1 +} + +log() { + echo "[precreate-state-package] $*" +} + +cleanup() { + if [[ -n "${WORK_DIR}" && -d "${WORK_DIR}" ]]; then + rm -rf "${WORK_DIR}" + fi +} + +trap cleanup EXIT + +urlencode() { + jq -nr --arg value "$1" '$value | @uri' +} + +parse_args() { + while (($# > 0)); do + case "$1" in + --package) + [[ $# -ge 2 ]] || fail "--package requires a value" + PACKAGE="$2" + shift 2 + ;; + --source-repository) + [[ $# -ge 2 ]] \ + || fail "--source-repository requires a value" + SOURCE_REPOSITORY="$2" + shift 2 + ;; + -h | --help) + usage + exit 0 + ;; + *) + fail "unknown argument: $1" + ;; + esac + done + + [[ -n "${PACKAGE}" ]] || fail "--package is required" + [[ -n "${SOURCE_REPOSITORY}" ]] \ + || fail "--source-repository is required" +} + +require_tools() { + local tool + for tool in gh oras jq; do + command -v "${tool}" >/dev/null 2>&1 \ + || fail "${tool} is required" + done +} + +parse_package() { + if [[ ! "${PACKAGE}" =~ ^ghcr\.io/([^/]+)/(.+)$ ]]; then + fail "package must match ghcr.io//" + fi + PACKAGE_OWNER="${BASH_REMATCH[1]}" + PACKAGE_NAME="${BASH_REMATCH[2]}" + + if [[ ! "${PACKAGE_OWNER}" =~ ^[A-Za-z0-9][A-Za-z0-9-]*$ ]]; then + fail "invalid GHCR owner: ${PACKAGE_OWNER}" + fi + if [[ ! "${PACKAGE_NAME}" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]; then + fail "invalid GHCR package name: ${PACKAGE_NAME}" + fi + + local source="${SOURCE_REPOSITORY%.git}" + source="${source%/}" + if [[ ! "${source}" =~ ^https://github\.com/([^/]+)/([^/]+)$ ]]; then + fail "source repository must match https://github.com//" + fi + SOURCE_REPOSITORY="${source}" + SOURCE_REPOSITORY_FULL_NAME="${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" +} + +configure_package_api() { + local owner_type owner_path package_path + owner_type="$(gh api "/users/${PACKAGE_OWNER}" --jq '.type')" + case "${owner_type}" in + Organization) owner_path="orgs" ;; + User) owner_path="users" ;; + *) fail "unsupported owner type ${owner_type} for ${PACKAGE_OWNER}" ;; + esac + + package_path="$(urlencode "${PACKAGE_NAME}")" + PACKAGE_API="/${owner_path}/$(urlencode "${PACKAGE_OWNER}")/packages/container/${package_path}" +} + +package_exists() { + local output + if output="$(gh api "${PACKAGE_API}" 2>&1)"; then + printf '%s' "${output}" >"${WORK_DIR}/package.json" + return 0 + fi + if grep -Eqi '(^|[^0-9])404([^0-9]|$)|package not found|not found' \ + <<<"${output}"; then + return 1 + fi + echo "${output}" >&2 + fail "could not query package metadata" +} + +login_to_ghcr() { + local username token + username="$(gh api /user --jq '.login')" + token="$(gh auth token)" \ + || fail "could not read the active gh token" + printf '%s' "${token}" \ + | oras login ghcr.io --username "${username}" --password-stdin +} + +push_bootstrap() { + mkdir -p "${WORK_DIR}/artifact" + printf '%s\n' \ + "Harmless bootstrap for the Repo Radius state package." \ + >"${WORK_DIR}/artifact/bootstrap.txt" + + ( + cd "${WORK_DIR}/artifact" + oras push "${PACKAGE}:${BOOTSTRAP_TAG}" \ + --artifact-type "${BOOTSTRAP_ARTIFACT_TYPE}" \ + --annotation \ + "org.opencontainers.image.source=${SOURCE_REPOSITORY}" \ + "bootstrap.txt:text/plain" + ) +} + +wait_for_package() { + local _ + for _ in {1..20}; do + if package_exists; then + return 0 + fi + sleep 2 + done + fail "package metadata did not become available after bootstrap" +} + +verify_package() { + local visibility linked_repository + visibility="$(jq -r '.visibility // empty' \ + "${WORK_DIR}/package.json")" + case "${visibility}" in + private | internal) ;; + public) + fail "package ${PACKAGE} is public; change its name or visibility before storing Radius state" + ;; + *) fail "unsupported package visibility: ${visibility:-missing}" ;; + esac + + linked_repository="$(jq -r '.repository.full_name // empty' \ + "${WORK_DIR}/package.json")" + if [[ ! "${linked_repository,,}" == \ + "${SOURCE_REPOSITORY_FULL_NAME,,}" ]]; then + fail "package is linked to ${linked_repository:-no repository}; expected ${SOURCE_REPOSITORY_FULL_NAME}" + fi +} + +ensure_bootstrap() { + if oras manifest fetch --descriptor \ + "${PACKAGE}:${BOOTSTRAP_TAG}" >"${WORK_DIR}/bootstrap.json" 2>/dev/null; then + return 0 + fi + + log "bootstrap tag is missing; adding harmless bootstrap artifact" + login_to_ghcr + push_bootstrap + oras manifest fetch --descriptor \ + "${PACKAGE}:${BOOTSTRAP_TAG}" >"${WORK_DIR}/bootstrap.json" +} + +print_result() { + local digest package_url + digest="$(jq -r '.digest' "${WORK_DIR}/bootstrap.json")" + package_url="$(jq -r '.html_url' "${WORK_DIR}/package.json")" + + cat <"${WORK_DIR}/bootstrap.json" + fi + + print_result +} + +main "$@" diff --git a/.github/scripts/test-repo-radius-state-e2e.sh b/.github/scripts/test-repo-radius-state-e2e.sh new file mode 100644 index 0000000000..313a0768ae --- /dev/null +++ b/.github/scripts/test-repo-radius-state-e2e.sh @@ -0,0 +1,773 @@ +#!/bin/bash + +# Proves Repo Radius state can round-trip through GHCR while the workload +# remains on a separate Kubernetes cluster. + +set -euo pipefail + +readonly APP_NAME="repo-radius-state-e2e" +readonly RESOURCE_NAME="repo-radius-state-container" +readonly ENVIRONMENT_NAME="repo-radius-state-e2e" +readonly WORKLOAD_NAMESPACE="repo-radius-state-e2e" +readonly WORKSPACE_NAME="repo-radius-state-e2e" +readonly SELECTOR="radapp.io/application=${APP_NAME},radapp.io/resource=${RESOURCE_NAME}" +readonly REPOSITORY_ROOT="${GITHUB_WORKSPACE:-$(git rev-parse --show-toplevel)}" +readonly DIAGNOSTICS_DIR="${REPOSITORY_ROOT}/dist/repo-radius-state-e2e" +readonly SOURCE_APP_FILE="${REPOSITORY_ROOT}/test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep" +readonly RUN_SUFFIX="${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-1}" +readonly NETWORK_NAME="repo-radius-state-${RUN_SUFFIX}" +readonly REGISTRY_CONTAINER="repo-radius-registry-${RUN_SUFFIX}" +readonly REGISTRY_ALIAS="repo-radius-registry" +readonly CLUSTER_REGISTRY="${REGISTRY_ALIAS}:5000" +readonly WORKLOAD_CLUSTER="radius-workload-${RUN_SUFFIX}" +readonly CONTROL_PLANE_A="radius-cp-a-${RUN_SUFFIX}" +readonly CONTROL_PLANE_B="radius-cp-b-${RUN_SUFFIX}" +readonly WORK_DIR="${RUNNER_TEMP:-/tmp}/repo-radius-state-${RUN_SUFFIX}" +readonly HOST_WORKLOAD_KUBECONFIG="${WORK_DIR}/workload-host.kubeconfig" +readonly INTERNAL_WORKLOAD_KUBECONFIG="${WORK_DIR}/workload-internal.kubeconfig" +readonly REGISTRY_CONFIG="${WORK_DIR}/registries.yaml" +readonly APP_FILE="${WORK_DIR}/repo-radius-state-app.bicep" +readonly STATE_OWNED_MARKER="${WORK_DIR}/state-owned" +readonly SAVED_DIGEST_FILE="${WORK_DIR}/saved-state-digest" +readonly PHASE_DIR="${WORK_DIR}/phases" +readonly BOOTSTRAP_TAG="bootstrap" + +: "${LOCAL_DOCKER_REGISTRY:?LOCAL_DOCKER_REGISTRY must be set}" +: "${DOCKER_TAG_VERSION:?DOCKER_TAG_VERSION must be set}" +: "${GH_TOKEN:?GH_TOKEN must be set}" +: "${RADIUS_STATE_ARCHIVE:?RADIUS_STATE_ARCHIVE must be set}" +: "${RADIUS_STATE_BACKEND:?RADIUS_STATE_BACKEND must be set}" +: "${RADIUS_STATE_REGISTRY:?RADIUS_STATE_REGISTRY must be set}" + +export RADIUS_PREVIEW=true +export RADIUS_STATE_REGISTRY="${RADIUS_STATE_REGISTRY,,}" +readonly STATE_REFERENCE="${RADIUS_STATE_REGISTRY}:${RADIUS_STATE_ARCHIVE}" + +PACKAGE_API="" +PACKAGE_NAME="" + +usage() { + cat < + +Phases: + validate-state-package + prepare-workload + install-initial-control-plane + deploy-initial + persist-state + replace-control-plane + restore-state + update-workload + diagnostics + cleanup + all +EOF +} + +mark_phase() { + mkdir -p "${PHASE_DIR}" + touch "${PHASE_DIR}/$1" +} + +begin_phase() { + mkdir -p "${WORK_DIR}" "${PHASE_DIR}" + printf '%s\n' "$1" >"${WORK_DIR}/current-phase" + append_summary "" + append_summary "### $2" +} + +require_phase() { + local phase="$1" + if [[ ! -f "${PHASE_DIR}/${phase}" ]]; then + echo "Required phase '${phase}' has not completed." >&2 + return 1 + fi +} + +append_summary() { + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then + printf '%s\n' "$*" >>"${GITHUB_STEP_SUMMARY}" + fi +} + +urlencode() { + jq -nr --arg value "$1" '$value | @uri' +} + +configure_package_api() { + local registry_path owner owner_type owner_scope + registry_path="${RADIUS_STATE_REGISTRY#ghcr.io/}" + owner="${registry_path%%/*}" + PACKAGE_NAME="${registry_path#*/}" + if [[ "${registry_path}" == "${RADIUS_STATE_REGISTRY}" || + -z "${owner}" || -z "${PACKAGE_NAME}" ]]; then + echo "RADIUS_STATE_REGISTRY must match ghcr.io//." >&2 + return 1 + fi + if [[ ! "${PACKAGE_NAME}" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*$ ]]; then + echo "Invalid GHCR package name: ${PACKAGE_NAME}" >&2 + return 1 + fi + + owner_type="$(gh api "/users/${owner}" --jq '.type')" + case "${owner_type}" in + Organization) owner_scope="orgs" ;; + User) owner_scope="users" ;; + *) + echo "Unsupported GitHub owner type: ${owner_type}" >&2 + return 1 + ;; + esac + PACKAGE_API="/${owner_scope}/$(urlencode "${owner}")/packages/container/$(urlencode "${PACKAGE_NAME}")" +} + +fetch_package_metadata() { + configure_package_api + mkdir -p "${WORK_DIR}" + gh api "${PACKAGE_API}" >"${WORK_DIR}/package.json" +} + +verify_package_metadata() { + local visibility linked_repository + visibility="$(jq -r '.visibility // empty' "${WORK_DIR}/package.json")" + case "${visibility}" in + private | internal) ;; + *) + echo "State package must be private or internal; found ${visibility:-missing}." >&2 + return 1 + ;; + esac + + linked_repository="$(jq -r '.repository.full_name // empty' \ + "${WORK_DIR}/package.json")" + if [[ -n "${GITHUB_REPOSITORY:-}" && + "${linked_repository,,}" != "${GITHUB_REPOSITORY,,}" ]]; then + echo "State package is linked to ${linked_repository:-no repository}; expected ${GITHUB_REPOSITORY}." >&2 + return 1 + fi +} + +collect_diagnostics() { + mkdir -p "${DIAGNOSTICS_DIR}" + + if [[ -d "${PHASE_DIR}" ]]; then + find "${PHASE_DIR}" -maxdepth 1 -type f -printf '%f\n' \ + | sort >"${DIAGNOSTICS_DIR}/completed-phases.txt" + fi + if [[ -f "${SAVED_DIGEST_FILE}" ]]; then + cp "${SAVED_DIGEST_FILE}" \ + "${DIAGNOSTICS_DIR}/saved-state-digest.txt" + fi + if fetch_package_metadata; then + jq '{ + name, + visibility, + repository: .repository.full_name, + version_count, + html_url + }' "${WORK_DIR}/package.json" \ + >"${DIAGNOSTICS_DIR}/package-metadata.json" + fi + + rad app list --output json \ + >"${DIAGNOSTICS_DIR}/rad-app-list.json" 2>&1 || true + oras manifest fetch --descriptor "${STATE_REFERENCE}" \ + >"${DIAGNOSTICS_DIR}/state-descriptor.json" 2>&1 || true + + local cluster + for cluster in "${CONTROL_PLANE_A}" "${CONTROL_PLANE_B}"; do + if ! k3d cluster list --no-headers \ + | awk '{print $1}' \ + | grep -Fxq "${cluster}"; then + continue + fi + + kubectl --context "k3d-${cluster}" get pods -A -o wide \ + >"${DIAGNOSTICS_DIR}/${cluster}-pods.txt" 2>&1 || true + kubectl --context "k3d-${cluster}" get events -A \ + --sort-by=.lastTimestamp \ + >"${DIAGNOSTICS_DIR}/${cluster}-events.txt" 2>&1 || true + + local component + for component in applications-rp dynamic-rp bicep-de controller ucp; do + kubectl --context "k3d-${cluster}" logs \ + -n radius-system \ + -l "app.kubernetes.io/name=${component}" \ + --all-containers --tail=300 \ + >"${DIAGNOSTICS_DIR}/${cluster}-${component}.log" \ + 2>&1 || true + done + done + + if [[ -f "${HOST_WORKLOAD_KUBECONFIG}" ]]; then + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" get all -A -o wide \ + >"${DIAGNOSTICS_DIR}/workload-resources.txt" 2>&1 || true + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" get deployment \ + -n "${WORKLOAD_NAMESPACE}" -l "${SELECTOR}" -o yaml \ + >"${DIAGNOSTICS_DIR}/workload-deployment.yaml" 2>&1 || true + fi +} + +delete_state_manifest() { + local status + + if manifest_exists; then + status=0 + else + status=$? + fi + if ((status == 1)); then + return 0 + fi + if ((status != 0)); then + return "${status}" + fi + + configure_package_api + local versions + versions="$(gh api --paginate \ + "${PACKAGE_API}/versions?per_page=100")" + + local -a version_ids + mapfile -t version_ids < <( + jq --slurp --raw-output --arg tag "${RADIUS_STATE_ARCHIVE}" \ + '[.[][] | + select(any(.metadata.container.tags[]?; . == $tag)) | + .id] | .[]' <<<"${versions}" + ) + if ((${#version_ids[@]} != 1)); then + echo "Expected one GHCR package version for ${STATE_REFERENCE}," \ + "found ${#version_ids[@]}." >&2 + return 1 + fi + + local version_count + version_count="$(gh api "${PACKAGE_API}" --jq '.version_count')" + if ((version_count == 1)); then + echo "Refusing to delete the final version of precreated package ${RADIUS_STATE_REGISTRY}." >&2 + return 1 + fi + gh api --method DELETE \ + "${PACKAGE_API}/versions/${version_ids[0]}" + + local _ + for _ in {1..30}; do + if manifest_exists; then + sleep 1 + continue + else + status=$? + fi + if ((status == 1)); then + break + fi + return "${status}" + done + + if manifest_exists; then + echo "State manifest still exists after cleanup: ${STATE_REFERENCE}" >&2 + return 1 + fi + fetch_package_metadata + verify_package_metadata + oras manifest fetch --descriptor \ + "${RADIUS_STATE_REGISTRY}:${BOOTSTRAP_TAG}" >/dev/null +} + +cleanup() { + local result=$? + local cleanup_result=0 + set +e + + if ((result != 0)); then + collect_diagnostics + fi + + if [[ -f "${STATE_OWNED_MARKER}" ]]; then + delete_state_manifest || cleanup_result=1 + fi + k3d cluster delete "${CONTROL_PLANE_A}" >/dev/null 2>&1 + k3d cluster delete "${CONTROL_PLANE_B}" >/dev/null 2>&1 + k3d cluster delete "${WORKLOAD_CLUSTER}" >/dev/null 2>&1 + docker rm --force "${REGISTRY_CONTAINER}" >/dev/null 2>&1 + docker network rm "${NETWORK_NAME}" >/dev/null 2>&1 + rm -rf "${WORK_DIR}" + + if ((cleanup_result == 0)); then + append_summary "- State version cleanup: succeeded" + else + append_summary "- State version cleanup: failed" + fi + if ((result == 0 && cleanup_result != 0)); then + result="${cleanup_result}" + fi + exit "${result}" +} + +cluster_exists() { + local cluster="$1" + k3d cluster list --no-headers \ + | awk '{print $1}' \ + | grep -Fxq "${cluster}" +} + +write_registry_config() { + cat >"${REGISTRY_CONFIG}" </dev/null + docker run --detach --rm \ + --name "${REGISTRY_CONTAINER}" \ + --network "${NETWORK_NAME}" \ + --network-alias "${REGISTRY_ALIAS}" \ + --publish 127.0.0.1:5000:5000 \ + registry:2 >/dev/null + + local _ + for _ in {1..30}; do + if curl -fsS http://127.0.0.1:5000/v2/ >/dev/null; then + return 0 + fi + sleep 1 + done + echo "Local OCI registry did not become ready." >&2 + return 1 +} + +publish_branch_artifacts() { + local image + for image in ucpd applications-rp dynamic-rp controller bicep; do + docker push \ + "${LOCAL_DOCKER_REGISTRY}/${image}:${DOCKER_TAG_VERSION}" + done + + cp "${SOURCE_APP_FILE}" "${APP_FILE}" + cat >"${WORK_DIR}/bicepconfig.json" </dev/null +} + +create_workload_cluster() { + k3d cluster create "${WORKLOAD_CLUSTER}" \ + --network "${NETWORK_NAME}" \ + --registry-config "${REGISTRY_CONFIG}" \ + --k3s-arg "--disable=traefik@server:*" \ + --wait + + k3d kubeconfig get "${WORKLOAD_CLUSTER}" \ + >"${HOST_WORKLOAD_KUBECONFIG}" + cp "${HOST_WORKLOAD_KUBECONFIG}" \ + "${INTERNAL_WORKLOAD_KUBECONFIG}" + + local cluster_key + local workload_ip + cluster_key="$(kubectl \ + --kubeconfig "${INTERNAL_WORKLOAD_KUBECONFIG}" \ + config view --minify \ + -o jsonpath='{.contexts[0].context.cluster}')" + workload_ip="$(docker inspect \ + --format \ + "{{(index .NetworkSettings.Networks \"${NETWORK_NAME}\").IPAddress}}" \ + "k3d-${WORKLOAD_CLUSTER}-server-0")" + + if [[ -z "${workload_ip}" ]]; then + echo "Could not determine the workload cluster IP." >&2 + return 1 + fi + + kubectl --kubeconfig "${INTERNAL_WORKLOAD_KUBECONFIG}" \ + config set "clusters.${cluster_key}.server" \ + "https://${workload_ip}:6443" >/dev/null + kubectl --kubeconfig "${INTERNAL_WORKLOAD_KUBECONFIG}" \ + config unset \ + "clusters.${cluster_key}.certificate-authority-data" >/dev/null + kubectl --kubeconfig "${INTERNAL_WORKLOAD_KUBECONFIG}" \ + config set \ + "clusters.${cluster_key}.insecure-skip-tls-verify" \ + "true" >/dev/null + + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" \ + create namespace "${WORKLOAD_NAMESPACE}" +} + +configure_workspace() { + local cluster="$1" + + kubectl config use-context "k3d-${cluster}" >/dev/null + rad workspace create kubernetes "${WORKSPACE_NAME}" \ + --context "k3d-${cluster}" \ + --force + rad workspace switch "${WORKSPACE_NAME}" + rad group create default + rad group switch default +} + +install_control_plane() { + local cluster="$1" + local install_needs_recovery=false + + k3d cluster create "${cluster}" \ + --network "${NETWORK_NAME}" \ + --registry-config "${REGISTRY_CONFIG}" \ + --k3s-arg "--disable=traefik@server:*" \ + --wait + kubectl config use-context "k3d-${cluster}" >/dev/null + + kubectl create namespace radius-system + kubectl create secret generic target-kubeconfig \ + --namespace radius-system \ + --from-file=kubeconfig="${INTERNAL_WORKLOAD_KUBECONFIG}" + + if rad install kubernetes \ + --chart "${REPOSITORY_ROOT}/deploy/Chart" \ + --set database.enabled=true \ + --set global.targetCluster.enabled=true \ + --set rp.publicEndpointOverride=localhost \ + --set \ + "rp.image=${CLUSTER_REGISTRY}/applications-rp,rp.tag=${DOCKER_TAG_VERSION}" \ + --set \ + "dynamicrp.image=${CLUSTER_REGISTRY}/dynamic-rp,dynamicrp.tag=${DOCKER_TAG_VERSION}" \ + --set \ + "controller.image=${CLUSTER_REGISTRY}/controller,controller.tag=${DOCKER_TAG_VERSION}" \ + --set \ + "ucp.image=${CLUSTER_REGISTRY}/ucpd,ucp.tag=${DOCKER_TAG_VERSION}" \ + --set \ + "bicep.image=${CLUSTER_REGISTRY}/bicep,bicep.tag=${DOCKER_TAG_VERSION}"; then + : + else + install_needs_recovery=true + if ! kubectl get deployment/ucp statefulset/database \ + --namespace radius-system >/dev/null; then + echo "Radius Helm installation did not complete." >&2 + return 1 + fi + echo "Radius was installed, but its initial API readiness check failed." + fi + + # Deployment Engine and dashboard are built in separate repositories. Their + # chart defaults intentionally remain on the compatible edge channel. + kubectl wait --for=condition=Ready pod/database-0 \ + --namespace radius-system \ + --timeout=300s + if [[ "${install_needs_recovery}" == "true" ]]; then + kubectl rollout restart deployment/ucp \ + --namespace radius-system + kubectl rollout status deployment/ucp \ + --namespace radius-system \ + --timeout=300s + fi + kubectl wait --for=condition=Available deployment --all \ + --namespace radius-system \ + --timeout=300s + configure_workspace "${cluster}" +} + +assert_application_listed() { + local output_file="$1" + + rad app list --output json | tee "${output_file}" + jq --exit-status --arg app "${APP_NAME}" \ + 'type == "array" and + length == 1 and + ((.[0].name // .[0].Name // "") == $app)' \ + "${output_file}" >/dev/null +} + +assert_workload_phase() { + local phase="$1" + local output_file="$2" + local pod_output="${output_file%.json}-pods.json" + + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" \ + rollout status deployment \ + --namespace "${WORKLOAD_NAMESPACE}" \ + --selector "${SELECTOR}" \ + --timeout=300s + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" \ + get deployment \ + --namespace "${WORKLOAD_NAMESPACE}" \ + --selector "${SELECTOR}" \ + --output json \ + | tee "${output_file}" + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" \ + wait --for=condition=Ready pod \ + --namespace "${WORKLOAD_NAMESPACE}" \ + --selector "${SELECTOR}" \ + --timeout=300s + kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" \ + get pod \ + --namespace "${WORKLOAD_NAMESPACE}" \ + --selector "${SELECTOR}" \ + --output json \ + | tee "${pod_output}" + jq --exit-status --arg phase "${phase}" \ + '(.items | length) == 1 and + ([.items[].spec.template.spec.containers[].args[]] + | any(contains($phase)))' \ + "${output_file}" >/dev/null + jq --exit-status --arg phase "${phase}" \ + '(.items | length) >= 1 and + ([.items[].spec.containers[].args[]] + | any(contains($phase)))' \ + "${pod_output}" >/dev/null + + local pod + pod="$(jq --raw-output '.items[0].metadata.name' "${pod_output}")" + local _ + for _ in {1..30}; do + if kubectl --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" \ + logs --namespace "${WORKLOAD_NAMESPACE}" "${pod}" \ + | grep -Fq "${phase}"; then + return 0 + fi + sleep 1 + done + echo "The running workload did not log phase ${phase}." >&2 + return 1 +} + +assert_absent_from_control_plane() { + local cluster="$1" + local resources + + resources="$(kubectl --context "k3d-${cluster}" get all \ + --all-namespaces \ + --selector "${SELECTOR}" \ + --output name)" + if [[ -n "${resources}" ]]; then + echo "Workload resources unexpectedly exist on ${cluster}:" >&2 + echo "${resources}" >&2 + return 1 + fi +} + +deploy_phase() { + local phase="$1" + local environment_id + + environment_id="$(rad env show "${ENVIRONMENT_NAME}" \ + --output json \ + | jq --slurp --raw-output \ + 'map(select(type == "object")) | first | (.id // .Id // empty)')" + if [[ -z "${environment_id}" || "${environment_id}" == "null" ]]; then + echo "Could not resolve the Radius environment ID." >&2 + return 1 + fi + + rad deploy "${APP_FILE}" \ + --environment "${ENVIRONMENT_NAME}" \ + --parameters "environment=${environment_id}" \ + --parameters "deploymentPhase=${phase}" +} + +manifest_exists() { + local output + + if output="$(oras manifest fetch --descriptor \ + "${STATE_REFERENCE}" 2>&1)"; then + return 0 + fi + if grep -Eqi '(^|[^0-9])(404|not found)([^0-9]|$)' <<<"${output}"; then + return 1 + fi + + echo "${output}" >&2 + return 2 +} + +assert_state_absent() { + local status + + if manifest_exists; then + echo "Run-specific state already exists: ${STATE_REFERENCE}" >&2 + return 1 + else + status=$? + fi + if ((status != 1)); then + return "${status}" + fi +} + +state_digest() { + oras manifest fetch --descriptor "${STATE_REFERENCE}" \ + | jq --exit-status --raw-output '.digest' +} + +phase_validate_state_package() { + begin_phase "validate-state-package" "Validate private state package" + mkdir -p "${WORK_DIR}" "${DIAGNOSTICS_DIR}" + fetch_package_metadata + verify_package_metadata + oras manifest fetch --descriptor \ + "${RADIUS_STATE_REGISTRY}:${BOOTSTRAP_TAG}" \ + >"${WORK_DIR}/bootstrap-descriptor.json" + assert_state_absent + mark_phase "validate-state-package" + append_summary "- Package: \`${RADIUS_STATE_REGISTRY}\`" + append_summary "- Visibility: $(jq -r '.visibility' \ + "${WORK_DIR}/package.json")" +} + +phase_prepare_workload() { + begin_phase "prepare-workload" "Prepare target workload cluster" + require_phase "validate-state-package" + write_registry_config + start_registry + publish_branch_artifacts + create_workload_cluster + mark_phase "prepare-workload" +} + +phase_install_initial_control_plane() { + begin_phase \ + "install-initial-control-plane" \ + "Install first Radius control plane" + require_phase "prepare-workload" + install_control_plane "${CONTROL_PLANE_A}" + rad startup + mark_phase "install-initial-control-plane" +} + +phase_deploy_initial() { + begin_phase "deploy-initial" "Deploy and verify initial application" + require_phase "install-initial-control-plane" + rad env create "${ENVIRONMENT_NAME}" \ + --kubernetes-namespace "${WORKLOAD_NAMESPACE}" + deploy_phase "before-restore" + assert_application_listed \ + "${DIAGNOSTICS_DIR}/apps-before-restore.json" + assert_workload_phase "before-restore" \ + "${DIAGNOSTICS_DIR}/workload-before-restore.json" + assert_absent_from_control_plane "${CONTROL_PLANE_A}" + mark_phase "deploy-initial" +} + +phase_persist_state() { + begin_phase "persist-state" "Persist Radius state to GHCR" + require_phase "deploy-initial" + # The preflight proved this run-unique tag was absent. Claim it before + # shutdown so cleanup removes any tag left by a partially failed upload. + touch "${STATE_OWNED_MARKER}" + rad shutdown + local saved_digest + saved_digest="$(state_digest)" + printf '%s\n' "${saved_digest}" >"${SAVED_DIGEST_FILE}" + mark_phase "persist-state" + echo "Saved Radius state as ${saved_digest}." + append_summary "- Saved state digest: \`${saved_digest}\`" +} + +phase_replace_control_plane() { + begin_phase "replace-control-plane" "Replace Radius control plane" + require_phase "persist-state" + k3d cluster delete "${CONTROL_PLANE_A}" + if cluster_exists "${CONTROL_PLANE_A}"; then + echo "The first control plane was not deleted." >&2 + return 1 + fi + + install_control_plane "${CONTROL_PLANE_B}" + mark_phase "replace-control-plane" +} + +phase_restore_state() { + begin_phase "restore-state" "Restore and verify Radius state" + require_phase "replace-control-plane" + [[ -s "${SAVED_DIGEST_FILE}" ]] \ + || { + echo "Saved state digest is missing." >&2 + return 1 + } + rad startup + assert_application_listed \ + "${DIAGNOSTICS_DIR}/apps-after-restore.json" + + local restored_digest saved_digest + saved_digest="$(<"${SAVED_DIGEST_FILE}")" + restored_digest="$(state_digest)" + if [[ "${restored_digest}" != "${saved_digest}" ]]; then + echo "State digest changed during restore." >&2 + return 1 + fi + mark_phase "restore-state" + append_summary "- Restored state digest: \`${restored_digest}\`" +} + +phase_update_workload() { + begin_phase "update-workload" "Update and verify existing workload" + require_phase "restore-state" + deploy_phase "after-restore" + assert_workload_phase "after-restore" \ + "${DIAGNOSTICS_DIR}/workload-after-restore.json" + assert_absent_from_control_plane "${CONTROL_PLANE_B}" + mark_phase "update-workload" + append_summary "- Result: state rehydration and workload update succeeded" + echo "Repo Radius GHCR state rehydration succeeded." +} + +phase_diagnostics() { + append_summary "" + append_summary "### Failure diagnostics" + if [[ -f "${WORK_DIR}/current-phase" ]]; then + append_summary "- Failed phase: \`$(<"${WORK_DIR}/current-phase")\`" + fi + append_summary "- Artifact: \`repo-radius-state-e2e-diagnostics\`" + collect_diagnostics +} + +run_all() { + trap cleanup EXIT + phase_validate_state_package + phase_prepare_workload + phase_install_initial_control_plane + phase_deploy_initial + phase_persist_state + phase_replace_control_plane + phase_restore_state + phase_update_workload + trap - EXIT + cleanup +} + +main() { + local phase="${1:-all}" + case "${phase}" in + validate-state-package) phase_validate_state_package ;; + prepare-workload) phase_prepare_workload ;; + install-initial-control-plane) + phase_install_initial_control_plane + ;; + deploy-initial) phase_deploy_initial ;; + persist-state) phase_persist_state ;; + replace-control-plane) phase_replace_control_plane ;; + restore-state) phase_restore_state ;; + update-workload) phase_update_workload ;; + diagnostics) phase_diagnostics ;; + cleanup) cleanup ;; + all) run_all ;; + -h | --help) usage ;; + *) + echo "Unknown phase: ${phase}" >&2 + usage >&2 + return 2 + ;; + esac +} + +main "$@" diff --git a/.github/workflows/repo-radius-state-e2e.yaml b/.github/workflows/repo-radius-state-e2e.yaml new file mode 100644 index 0000000000..17cb3804c3 --- /dev/null +++ b/.github/workflows/repo-radius-state-e2e.yaml @@ -0,0 +1,165 @@ +# ------------------------------------------------------------ +# Copyright 2023 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +--- +name: Repo Radius GHCR State E2E + +on: + workflow_dispatch: + inputs: + state_package: + description: Existing private GHCR package used for Radius state + required: false + default: radius-repo-state-e2e + schedule: + # Run daily at 04:17 UTC, outside the non-cloud functional-test schedule. + - cron: 17 4 * * * + +permissions: + contents: read + packages: write # Push and delete this repository's GHCR state package. + +concurrency: + group: repo-radius-state-e2e-${{ inputs.state_package || 'radius-repo-state-e2e' }} + cancel-in-progress: false + +env: + ACTION_LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + LOCAL_DOCKER_REGISTRY: localhost:5000 + DOCKER_TAG_VERSION: repo-radius-${{ github.run_id }}-${{ github.run_attempt }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RADIUS_STATE_BACKEND: oci + RADIUS_STATE_ARCHIVE: repo-radius-state-e2e-${{ github.run_id }}-${{ github.run_attempt }} + RADIUS_STATE_REGISTRY: ghcr.io/${{ github.repository_owner }}/${{ inputs.state_package || 'radius-repo-state-e2e' }} + +jobs: + state-rehydration: + name: Persist and rehydrate Radius state + runs-on: ubuntu-24.04 + timeout-minutes: 60 + if: github.event_name == 'workflow_dispatch' || github.repository == 'radius-project/radius' + steps: + - name: Checkout dispatched ref + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + cache: true + + - name: Log in to GitHub Container Registry + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Install k3d + run: make install-k3d + + - name: Install ORAS + uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1 + + - name: Install Bicep + run: | + make install-bicep BICEP_INSTALL_DIR="${HOME}/.rad/bin" + echo "${HOME}/.rad/bin" >> "${GITHUB_PATH}" + + - name: Validate private state package + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh validate-state-package + + - name: Build Repo Radius test artifacts + env: + DOCKER_REGISTRY: ${{ env.LOCAL_DOCKER_REGISTRY }} + run: | + make copy-manifests \ + build-rad \ + docker-build-ucpd \ + docker-build-applications-rp \ + docker-build-dynamic-rp \ + docker-build-controller \ + docker-build-bicep + + - name: Install branch CLI + run: | + install -D -m 0755 \ + ./dist/linux_amd64/release/rad \ + "${HOME}/.local/bin/rad" + echo "${HOME}/.local/bin" >> "${GITHUB_PATH}" + + - name: Prepare target workload cluster + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh prepare-workload + + - name: Install first Radius control plane + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh install-initial-control-plane + + - name: Deploy and verify initial application + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh deploy-initial + + - name: Persist Radius state to GHCR + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh persist-state + + - name: Replace Radius control plane + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh replace-control-plane + + - name: Restore and verify Radius state + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh restore-state + + - name: Update and verify existing workload + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh update-workload + + - name: Collect failure diagnostics + if: failure() + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh diagnostics + + - name: Upload failure diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: repo-radius-state-e2e-diagnostics + path: dist/repo-radius-state-e2e/ + if-no-files-found: ignore + retention-days: 3 + + - name: Clean up state version and clusters + if: always() + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh cleanup + + report-failure: + name: Report test failure + needs: state-rehydration + if: failure() && github.event_name == 'schedule' && github.repository == vars.RADIUS_REPOSITORY + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + issues: write + steps: + - name: Create failure issue for failing scheduled run + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ github.token }} + script: | + github.rest.issues.create({ + ...context.repo, + title: `Scheduled Repo Radius GHCR state test failed - Run ID: ${context.runId}`, + labels: ['test-failure'], + body: `## Bug information\n\nThe scheduled Repo Radius GHCR state lifecycle failed. Review the failed lifecycle step and download the \`repo-radius-state-e2e-diagnostics\` artifact from [the workflow run](${process.env.ACTION_LINK}).` + }) diff --git a/docs/contributing/contributing-code/contributing-code-github-workflows/README.md b/docs/contributing/contributing-code/contributing-code-github-workflows/README.md index c85bfba63d..5a50ea9f03 100644 --- a/docs/contributing/contributing-code/contributing-code-github-workflows/README.md +++ b/docs/contributing/contributing-code/contributing-code-github-workflows/README.md @@ -17,6 +17,34 @@ Follow the [GitHub Workflows instruction file](../../../../.github/instructions/ - **Least privilege** — set explicit `permissions:` blocks; default to read-only and grant write only where needed. - **Pin and cache** — pin action versions and cache dependencies to keep runs fast and reproducible. +## Repo Radius GHCR state test + +[`repo-radius-state-e2e.yaml`](../../../../.github/workflows/repo-radius-state-e2e.yaml) is a standalone daily end-to-end test of the OCI state archive. It deploys an application through one ephemeral Radius control plane to a separate persistent k3d target cluster, saves Radius state to a private GHCR package, replaces the control plane, restores the saved state, and updates the existing target-cluster workload. + +The scheduled run uses `ghcr.io/radius-project/radius-repo-state-e2e`. This package must be precreated as private and linked to `radius-project/radius`; a workflow in the public repository would otherwise create a public package, which Radius refuses to use for state. + +Provision or verify the package with a GitHub CLI credential that has `write:packages`: + +```bash +$ make install-oras +$ export PATH="$HOME/.local/bin:$PATH" +$ ./.github/scripts/precreate-repo-radius-state-package.sh \ + --package ghcr.io/radius-project/radius-repo-state-e2e \ + --source-repository https://github.com/radius-project/radius +``` + +The script is idempotent. It creates or verifies a harmless `bootstrap` version, private/internal visibility, and repository linkage. It never changes a public package to private. + +The workflow runs daily at 04:17 UTC from the default branch. For manual testing, dispatch another ref and optionally select a different precreated package under that repository owner: + +```bash +$ gh workflow run repo-radius-state-e2e.yaml \ + --ref \ + -f state_package= +``` + +Each lifecycle phase is a separate workflow step. On failure, inspect the failed step first, then download the `repo-radius-state-e2e-diagnostics` artifact. Scheduled failures in the upstream repository also create an issue labeled `test-failure`; manual and fork runs do not. Successful runs record the saved/restored digest in the job summary and delete only their run-specific state version; the private package and bootstrap version remain. + ## Steps 1. Find the workflow under `.github/workflows/` and identify any reusable workflows, Make targets, or scripts it calls. diff --git a/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md b/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md index 4208c42d69..499fd882aa 100644 --- a/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md +++ b/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md @@ -139,6 +139,8 @@ These tests run automatically for every PR via the `functional-test-noncloud.yam Separate scheduled jobs (`purge-azure-test-resources.yaml` and `purge-aws-test-resources.yaml`) delete cloud resources left behind when a run is cancelled or times out. +The standalone [`repo-radius-state-e2e.yaml`](../contributing-code-github-workflows/#repo-radius-ghcr-state-test) workflow runs daily to verify that an ephemeral Radius control plane can save its state to a private GHCR package, restore that state into a replacement control plane, and continue managing an existing workload on a separate target cluster. It is intentionally separate from the non-cloud matrix so its `packages: write` permission and lifecycle phases remain isolated. Maintainers must [precreate or verify the private state package](../contributing-code-github-workflows/#repo-radius-ghcr-state-test) before enabling the scheduled run. + ### Cloud credentials in CI (federated identity) The cloud CI workflows - `functional-test-cloud.yaml`, the long-running test `long-running-azure.yaml` ("LRT"), and the two scheduled purge jobs - authenticate to Azure and AWS with **federated identity only**. No static cloud secrets (service-principal passwords or AWS access keys) are stored in GitHub; every credential is a short-lived token minted from an OIDC trust. Two distinct trusts are in play: diff --git a/test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep b/test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep new file mode 100644 index 0000000000..88cdcec2f1 --- /dev/null +++ b/test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep @@ -0,0 +1,32 @@ +extension radius + +@description('The Radius environment resource ID for the application.') +param environment string + +@description('A marker written by the container to prove that an update was deployed.') +param deploymentPhase string + +resource app 'Radius.Core/applications@2025-08-01-preview' = { + name: 'repo-radius-state-e2e' + location: 'global' + properties: { + environment: environment + } +} + +resource container 'Radius.Compute/containers@2025-08-01-preview' = { + name: 'repo-radius-state-container' + location: 'global' + properties: { + application: app.id + environment: environment + containers: { + main: { + image: 'ghcr.io/radius-project/mirror/debian:latest' + command: ['/bin/sh'] + args: ['-c', 'while true; do echo ${deploymentPhase}; sleep 10; done'] + } + } + connections: {} + } +} From 57df88c30e1fd02d547ebb1e362e5b815ae11e8c Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:17:11 -0700 Subject: [PATCH 2/9] Wait for k3d API readiness Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8a31277c-6702-45f2-876b-013c8a0ec466 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/test-repo-radius-state-e2e.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/scripts/test-repo-radius-state-e2e.sh b/.github/scripts/test-repo-radius-state-e2e.sh index 313a0768ae..e316ad7559 100644 --- a/.github/scripts/test-repo-radius-state-e2e.sh +++ b/.github/scripts/test-repo-radius-state-e2e.sh @@ -312,6 +312,23 @@ cluster_exists() { | grep -Fxq "${cluster}" } +wait_for_kubernetes_api() { + local -a kubectl_args=("$@") + local deadline=$((SECONDS + 60)) + + while ((SECONDS < deadline)); do + if kubectl "${kubectl_args[@]}" --request-timeout=5s \ + get --raw=/readyz \ + >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + + echo "Kubernetes API did not become ready." >&2 + return 1 +} + write_registry_config() { cat >"${REGISTRY_CONFIG}" <"${HOST_WORKLOAD_KUBECONFIG}" cp "${HOST_WORKLOAD_KUBECONFIG}" \ "${INTERNAL_WORKLOAD_KUBECONFIG}" + wait_for_kubernetes_api --kubeconfig "${HOST_WORKLOAD_KUBECONFIG}" local cluster_key local workload_ip @@ -426,6 +444,7 @@ install_control_plane() { --registry-config "${REGISTRY_CONFIG}" \ --k3s-arg "--disable=traefik@server:*" \ --wait + wait_for_kubernetes_api --context "k3d-${cluster}" kubectl config use-context "k3d-${cluster}" >/dev/null kubectl create namespace radius-system From 6b6f132902f95a6a51d925814643a5d4a1cafb0b Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:39:41 -0700 Subject: [PATCH 3/9] Ensure Repo Radius state cleanup after timeout Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8a31277c-6702-45f2-876b-013c8a0ec466 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/test-repo-radius-state-e2e.sh | 7 +++++ .github/workflows/repo-radius-state-e2e.yaml | 31 ++++++++++++++++++- .../README.md | 2 +- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/scripts/test-repo-radius-state-e2e.sh b/.github/scripts/test-repo-radius-state-e2e.sh index e316ad7559..0b865a9fa8 100644 --- a/.github/scripts/test-repo-radius-state-e2e.sh +++ b/.github/scripts/test-repo-radius-state-e2e.sh @@ -60,6 +60,7 @@ Phases: restore-state update-workload diagnostics + cleanup-state-version cleanup all EOF @@ -750,6 +751,11 @@ phase_diagnostics() { collect_diagnostics } +phase_cleanup_state_version() { + delete_state_manifest + append_summary "- Remote state version cleanup: succeeded" +} + run_all() { trap cleanup EXIT phase_validate_state_package @@ -778,6 +784,7 @@ main() { restore-state) phase_restore_state ;; update-workload) phase_update_workload ;; diagnostics) phase_diagnostics ;; + cleanup-state-version) phase_cleanup_state_version ;; cleanup) cleanup ;; all) run_all ;; -h | --help) usage ;; diff --git a/.github/workflows/repo-radius-state-e2e.yaml b/.github/workflows/repo-radius-state-e2e.yaml index 17cb3804c3..8919963bfb 100644 --- a/.github/workflows/repo-radius-state-e2e.yaml +++ b/.github/workflows/repo-radius-state-e2e.yaml @@ -143,9 +143,38 @@ jobs: if: always() run: bash ./.github/scripts/test-repo-radius-state-e2e.sh cleanup + cleanup-state-version: + name: Ensure state version cleanup + needs: state-rehydration + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + packages: write + steps: + - name: Checkout workflow revision + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + ref: ${{ github.sha }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Install ORAS + uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1 + + - name: Ensure run-specific state version is deleted + run: bash ./.github/scripts/test-repo-radius-state-e2e.sh cleanup-state-version + report-failure: name: Report test failure - needs: state-rehydration + needs: [state-rehydration, cleanup-state-version] if: failure() && github.event_name == 'schedule' && github.repository == vars.RADIUS_REPOSITORY runs-on: ubuntu-24.04 timeout-minutes: 5 diff --git a/docs/contributing/contributing-code/contributing-code-github-workflows/README.md b/docs/contributing/contributing-code/contributing-code-github-workflows/README.md index 5a50ea9f03..c07b99fe7b 100644 --- a/docs/contributing/contributing-code/contributing-code-github-workflows/README.md +++ b/docs/contributing/contributing-code/contributing-code-github-workflows/README.md @@ -43,7 +43,7 @@ $ gh workflow run repo-radius-state-e2e.yaml \ -f state_package= ``` -Each lifecycle phase is a separate workflow step. On failure, inspect the failed step first, then download the `repo-radius-state-e2e-diagnostics` artifact. Scheduled failures in the upstream repository also create an issue labeled `test-failure`; manual and fork runs do not. Successful runs record the saved/restored digest in the job summary and delete only their run-specific state version; the private package and bootstrap version remain. +Each lifecycle phase is a separate workflow step. On failure, inspect the failed step first, then download the `repo-radius-state-e2e-diagnostics` artifact. A separate post-lifecycle job retries run-specific state cleanup even if the main job times out. Scheduled failures in the upstream repository also create an issue labeled `test-failure`; manual and fork runs do not. Successful runs record the saved/restored digest in the job summary; the private package and bootstrap version remain. ## Steps From 222e4fbae0b23f71f83f15f4886398c16613a2e3 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Fri, 17 Jul 2026 17:54:33 -0700 Subject: [PATCH 4/9] Add Repo Radius application model Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 29eb21e5-6540-4bbc-9e29-3ea5c1aeb994 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .radius/app.bicep | 33 +++++++++++++++++++++++++++++++++ .radius/bicepconfig.json | 8 ++++++++ 2 files changed, 41 insertions(+) create mode 100644 .radius/app.bicep create mode 100644 .radius/bicepconfig.json diff --git a/.radius/app.bicep b/.radius/app.bicep new file mode 100644 index 0000000000..0a73fb458f --- /dev/null +++ b/.radius/app.bicep @@ -0,0 +1,33 @@ +extension radius + +@description('The Radius environment resource ID for the application.') +param environment string + +@description('A marker written by the container to prove that an update was deployed.') +param deploymentPhase string = 'before-restore' + +resource repoRadiusStateApp 'Radius.Core/applications@2025-08-01-preview' = { + name: 'repo-radius-state-e2e' + location: 'global' + properties: { + environment: environment + } +} + +resource repoRadiusStateContainer 'Radius.Compute/containers@2025-08-01-preview' = { + name: 'repo-radius-state-container' + location: 'global' + properties: { + application: repoRadiusStateApp.id + environment: environment + codeReference: 'test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep' + containers: { + main: { + image: 'ghcr.io/radius-project/mirror/debian:latest' + command: ['/bin/sh'] + args: ['-c', 'while true; do echo ${deploymentPhase}; sleep 10; done'] + } + } + connections: {} + } +} diff --git a/.radius/bicepconfig.json b/.radius/bicepconfig.json new file mode 100644 index 0000000000..ba32f165c9 --- /dev/null +++ b/.radius/bicepconfig.json @@ -0,0 +1,8 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true + }, + "extensions": { + "radius": "br:biceptypes.azurecr.io/radius:latest" + } +} From 27c1a271546f858632c216229401577f68a0abc9 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:23:16 -0700 Subject: [PATCH 5/9] remove extra .radius folder files Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .radius/app.bicep | 33 --------------------------------- .radius/bicepconfig.json | 8 -------- 2 files changed, 41 deletions(-) delete mode 100644 .radius/app.bicep delete mode 100644 .radius/bicepconfig.json diff --git a/.radius/app.bicep b/.radius/app.bicep deleted file mode 100644 index 0a73fb458f..0000000000 --- a/.radius/app.bicep +++ /dev/null @@ -1,33 +0,0 @@ -extension radius - -@description('The Radius environment resource ID for the application.') -param environment string - -@description('A marker written by the container to prove that an update was deployed.') -param deploymentPhase string = 'before-restore' - -resource repoRadiusStateApp 'Radius.Core/applications@2025-08-01-preview' = { - name: 'repo-radius-state-e2e' - location: 'global' - properties: { - environment: environment - } -} - -resource repoRadiusStateContainer 'Radius.Compute/containers@2025-08-01-preview' = { - name: 'repo-radius-state-container' - location: 'global' - properties: { - application: repoRadiusStateApp.id - environment: environment - codeReference: 'test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep' - containers: { - main: { - image: 'ghcr.io/radius-project/mirror/debian:latest' - command: ['/bin/sh'] - args: ['-c', 'while true; do echo ${deploymentPhase}; sleep 10; done'] - } - } - connections: {} - } -} diff --git a/.radius/bicepconfig.json b/.radius/bicepconfig.json deleted file mode 100644 index ba32f165c9..0000000000 --- a/.radius/bicepconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "experimentalFeaturesEnabled": { - "extensibility": true - }, - "extensions": { - "radius": "br:biceptypes.azurecr.io/radius:latest" - } -} From 29744db4e747fc7b8d09e1104a189bad5031d8b1 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:14:51 -0700 Subject: [PATCH 6/9] doc updates Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- docs/architecture/state-archive.md | 15 +++++++ .../README.md | 28 ------------ .../contributing-code-tests/README.md | 1 + .../repo-radius-state-e2e.md | 45 +++++++++++++++++++ .../running-functional-tests.md | 2 - 5 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 docs/contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md diff --git a/docs/architecture/state-archive.md b/docs/architecture/state-archive.md index 00421edd7d..e64073b580 100644 --- a/docs/architecture/state-archive.md +++ b/docs/architecture/state-archive.md @@ -214,6 +214,21 @@ they have separate lifecycles and access requirements. - **Local testing** can use `RADIUS_ARCHIVE_PLAIN_HTTP=true` with a local OCI registry. +## End-to-End Test + +The OCI state archive has a dedicated end-to-end test that exercises the full +save/restore lifecycle against a real GHCR package. It deploys an application +through one ephemeral Radius control plane to a separate persistent target +cluster, saves state to a private GHCR package, replaces the control plane, +restores the saved state, and confirms the replacement control plane still +manages the existing workload. This validates the round-trip durability +contract and the GHCR visibility guard end to end, including the bootstrap-tag +behavior used when the package does not yet exist. It runs on a schedule rather +than in the per-PR matrix because it needs `packages: write` and a precreated +private package. See +[Repo Radius GHCR state end-to-end test](../contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md) +for how to provision, run, and troubleshoot it. + ## How Consumers Stay Decoupled Every consumer stores a `statearchive.Archive` (the interface) and accepts any diff --git a/docs/contributing/contributing-code/contributing-code-github-workflows/README.md b/docs/contributing/contributing-code/contributing-code-github-workflows/README.md index c07b99fe7b..c85bfba63d 100644 --- a/docs/contributing/contributing-code/contributing-code-github-workflows/README.md +++ b/docs/contributing/contributing-code/contributing-code-github-workflows/README.md @@ -17,34 +17,6 @@ Follow the [GitHub Workflows instruction file](../../../../.github/instructions/ - **Least privilege** — set explicit `permissions:` blocks; default to read-only and grant write only where needed. - **Pin and cache** — pin action versions and cache dependencies to keep runs fast and reproducible. -## Repo Radius GHCR state test - -[`repo-radius-state-e2e.yaml`](../../../../.github/workflows/repo-radius-state-e2e.yaml) is a standalone daily end-to-end test of the OCI state archive. It deploys an application through one ephemeral Radius control plane to a separate persistent k3d target cluster, saves Radius state to a private GHCR package, replaces the control plane, restores the saved state, and updates the existing target-cluster workload. - -The scheduled run uses `ghcr.io/radius-project/radius-repo-state-e2e`. This package must be precreated as private and linked to `radius-project/radius`; a workflow in the public repository would otherwise create a public package, which Radius refuses to use for state. - -Provision or verify the package with a GitHub CLI credential that has `write:packages`: - -```bash -$ make install-oras -$ export PATH="$HOME/.local/bin:$PATH" -$ ./.github/scripts/precreate-repo-radius-state-package.sh \ - --package ghcr.io/radius-project/radius-repo-state-e2e \ - --source-repository https://github.com/radius-project/radius -``` - -The script is idempotent. It creates or verifies a harmless `bootstrap` version, private/internal visibility, and repository linkage. It never changes a public package to private. - -The workflow runs daily at 04:17 UTC from the default branch. For manual testing, dispatch another ref and optionally select a different precreated package under that repository owner: - -```bash -$ gh workflow run repo-radius-state-e2e.yaml \ - --ref \ - -f state_package= -``` - -Each lifecycle phase is a separate workflow step. On failure, inspect the failed step first, then download the `repo-radius-state-e2e-diagnostics` artifact. A separate post-lifecycle job retries run-specific state cleanup even if the main job times out. Scheduled failures in the upstream repository also create an issue labeled `test-failure`; manual and fork runs do not. Successful runs record the saved/restored digest in the job summary; the private package and bootstrap version remain. - ## Steps 1. Find the workflow under `.github/workflows/` and identify any reusable workflows, Make targets, or scripts it calls. diff --git a/docs/contributing/contributing-code/contributing-code-tests/README.md b/docs/contributing/contributing-code/contributing-code-tests/README.md index 2285c84d0b..dc75c02ce4 100644 --- a/docs/contributing/contributing-code/contributing-code-tests/README.md +++ b/docs/contributing/contributing-code/contributing-code-tests/README.md @@ -81,6 +81,7 @@ When you are iterating on control-plane images (for example the applications res - We write unit tests in a straightforward style and use [testify](https://github.com/stretchr/testify) for assertions. - We favor [subtests](https://go.dev/blog/subtests) and [table-driven tests](https://dave.cheney.net/2019/05/07/prefer-table-driven-tests) and apply them where appropriate. - For functional tests specifically, see [writing functional tests](./writing-functional-tests.md), the [naming conventions](./tests-naming-conventions.md), [test logging](./tests-logging.md), and [using standard images in tests](./tests-images-pushtoghcr.md). +- For the scheduled OCI state archive end-to-end test, see [Repo Radius GHCR state end-to-end test](./repo-radius-state-e2e.md). ## Verification diff --git a/docs/contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md b/docs/contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md new file mode 100644 index 0000000000..f2fbf12f76 --- /dev/null +++ b/docs/contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md @@ -0,0 +1,45 @@ +# Repo Radius GHCR state end-to-end test + +## Purpose + +This page describes the standalone [`repo-radius-state-e2e.yaml`](../../../../.github/workflows/repo-radius-state-e2e.yaml) workflow, a daily end-to-end test of the OCI state archive, and how to provision, run, and troubleshoot it. It is for maintainers who own the scheduled run and for contributors changing the OCI state archive or its workflow. For the OCI state archive itself, see the [durable state archive architecture](../../../architecture/state-archive.md). + +## How the test works + +[`repo-radius-state-e2e.yaml`](../../../../.github/workflows/repo-radius-state-e2e.yaml) deploys an application through one ephemeral Radius control plane to a separate persistent k3d target cluster, saves Radius state to a private GHCR package, replaces the control plane, restores the saved state, and updates the existing target-cluster workload. + +The scheduled run uses `ghcr.io/radius-project/radius-repo-state-e2e`. This package must be precreated as private and linked to `radius-project/radius`; a workflow in the public repository would otherwise create a public package, which Radius refuses to use for state. + +## Provision the state package + +Provision or verify the package with a GitHub CLI credential that has `write:packages`: + +```bash +$ make install-oras +$ export PATH="$HOME/.local/bin:$PATH" +$ ./.github/scripts/precreate-repo-radius-state-package.sh \ + --package ghcr.io/radius-project/radius-repo-state-e2e \ + --source-repository https://github.com/radius-project/radius +``` + +The script is idempotent. It creates or verifies a harmless `bootstrap` version, private/internal visibility, and repository linkage. It never changes a public package to private. + +## Run the test + +The workflow runs daily at 04:17 UTC from the default branch. For manual testing, dispatch another ref and optionally select a different precreated package under that repository owner: + +```bash +$ gh workflow run repo-radius-state-e2e.yaml \ + --ref \ + -f state_package= +``` + +## Diagnostics and failure handling + +Each lifecycle phase is a separate workflow step. On failure, inspect the failed step first, then download the `repo-radius-state-e2e-diagnostics` artifact. A separate post-lifecycle job retries run-specific state cleanup even if the main job times out. Scheduled failures in the upstream repository also create an issue labeled `test-failure`; manual and fork runs do not. Successful runs record the saved/restored digest in the job summary; the private package and bootstrap version remain. + +## Related docs + +- [Durable state archive](../../../architecture/state-archive.md) — the OCI state archive this test exercises. +- [Running functional tests](./running-functional-tests.md) — the broader end-to-end test tier. +- [Contributing to GitHub Actions workflows](../contributing-code-github-workflows/README.md) — how to change the automation under `.github/workflows/`. diff --git a/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md b/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md index 499fd882aa..4208c42d69 100644 --- a/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md +++ b/docs/contributing/contributing-code/contributing-code-tests/running-functional-tests.md @@ -139,8 +139,6 @@ These tests run automatically for every PR via the `functional-test-noncloud.yam Separate scheduled jobs (`purge-azure-test-resources.yaml` and `purge-aws-test-resources.yaml`) delete cloud resources left behind when a run is cancelled or times out. -The standalone [`repo-radius-state-e2e.yaml`](../contributing-code-github-workflows/#repo-radius-ghcr-state-test) workflow runs daily to verify that an ephemeral Radius control plane can save its state to a private GHCR package, restore that state into a replacement control plane, and continue managing an existing workload on a separate target cluster. It is intentionally separate from the non-cloud matrix so its `packages: write` permission and lifecycle phases remain isolated. Maintainers must [precreate or verify the private state package](../contributing-code-github-workflows/#repo-radius-ghcr-state-test) before enabling the scheduled run. - ### Cloud credentials in CI (federated identity) The cloud CI workflows - `functional-test-cloud.yaml`, the long-running test `long-running-azure.yaml` ("LRT"), and the two scheduled purge jobs - authenticate to Azure and AWS with **federated identity only**. No static cloud secrets (service-principal passwords or AWS access keys) are stored in GitHub; every credential is a short-lived token minted from an OIDC trust. Two distinct trusts are in play: From fe6f27cb6a6543c72142c26fbb06cac7da289f2f Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:50:52 -0700 Subject: [PATCH 7/9] Align fork guard with RADIUS_REPOSITORY and reflow E2E doc section Use vars.RADIUS_REPOSITORY in the state-rehydration job gate to match the report-failure job in the same workflow and the sibling functional-test workflows, instead of hardcoding the repository name. Reflow the new End-to-End Test section in state-archive.md to a single line per paragraph, per the repo Markdown convention (MD013 disabled). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/repo-radius-state-e2e.yaml | 2 +- docs/architecture/state-archive.md | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/repo-radius-state-e2e.yaml b/.github/workflows/repo-radius-state-e2e.yaml index 8919963bfb..571c3c71ff 100644 --- a/.github/workflows/repo-radius-state-e2e.yaml +++ b/.github/workflows/repo-radius-state-e2e.yaml @@ -51,7 +51,7 @@ jobs: name: Persist and rehydrate Radius state runs-on: ubuntu-24.04 timeout-minutes: 60 - if: github.event_name == 'workflow_dispatch' || github.repository == 'radius-project/radius' + if: github.event_name == 'workflow_dispatch' || github.repository == vars.RADIUS_REPOSITORY steps: - name: Checkout dispatched ref uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/docs/architecture/state-archive.md b/docs/architecture/state-archive.md index e64073b580..83f344c1bb 100644 --- a/docs/architecture/state-archive.md +++ b/docs/architecture/state-archive.md @@ -216,18 +216,7 @@ they have separate lifecycles and access requirements. ## End-to-End Test -The OCI state archive has a dedicated end-to-end test that exercises the full -save/restore lifecycle against a real GHCR package. It deploys an application -through one ephemeral Radius control plane to a separate persistent target -cluster, saves state to a private GHCR package, replaces the control plane, -restores the saved state, and confirms the replacement control plane still -manages the existing workload. This validates the round-trip durability -contract and the GHCR visibility guard end to end, including the bootstrap-tag -behavior used when the package does not yet exist. It runs on a schedule rather -than in the per-PR matrix because it needs `packages: write` and a precreated -private package. See -[Repo Radius GHCR state end-to-end test](../contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md) -for how to provision, run, and troubleshoot it. +The OCI state archive has a dedicated end-to-end test that exercises the full save/restore lifecycle against a real GHCR package. It deploys an application through one ephemeral Radius control plane to a separate persistent target cluster, saves state to a private GHCR package, replaces the control plane, restores the saved state, and confirms the replacement control plane still manages the existing workload. This validates the round-trip durability contract and the GHCR visibility guard end to end, including the bootstrap-tag behavior used when the package does not yet exist. It runs on a schedule rather than in the per-PR matrix because it needs `packages: write` and a precreated private package. See [Repo Radius GHCR state end-to-end test](../contributing/contributing-code/contributing-code-tests/repo-radius-state-e2e.md) for how to provision, run, and troubleshoot it. ## How Consumers Stay Decoupled From 60a2052184b4123c525f372415d42eb1dab01011 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:54:36 -0700 Subject: [PATCH 8/9] Rename workflow to match filename Rename the workflow display name from "Repo Radius GHCR State E2E" to "Repo Radius State E2E" so it derives directly from the filename repo-radius-state-e2e.yaml, per the GitHub Workflows instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/repo-radius-state-e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-radius-state-e2e.yaml b/.github/workflows/repo-radius-state-e2e.yaml index 571c3c71ff..b3b4fba202 100644 --- a/.github/workflows/repo-radius-state-e2e.yaml +++ b/.github/workflows/repo-radius-state-e2e.yaml @@ -16,7 +16,7 @@ # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json --- -name: Repo Radius GHCR State E2E +name: Repo Radius State E2E on: workflow_dispatch: From e058bb61c750e99cba79da5d6f96f1de6e6d1229 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:31:49 -0700 Subject: [PATCH 9/9] Restore Repo Radius application model Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 29eb21e5-6540-4bbc-9e29-3ea5c1aeb994 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .radius/app.bicep | 33 +++++++++++++++++++++++++++++++++ .radius/bicepconfig.json | 8 ++++++++ 2 files changed, 41 insertions(+) create mode 100644 .radius/app.bicep create mode 100644 .radius/bicepconfig.json diff --git a/.radius/app.bicep b/.radius/app.bicep new file mode 100644 index 0000000000..0a73fb458f --- /dev/null +++ b/.radius/app.bicep @@ -0,0 +1,33 @@ +extension radius + +@description('The Radius environment resource ID for the application.') +param environment string + +@description('A marker written by the container to prove that an update was deployed.') +param deploymentPhase string = 'before-restore' + +resource repoRadiusStateApp 'Radius.Core/applications@2025-08-01-preview' = { + name: 'repo-radius-state-e2e' + location: 'global' + properties: { + environment: environment + } +} + +resource repoRadiusStateContainer 'Radius.Compute/containers@2025-08-01-preview' = { + name: 'repo-radius-state-container' + location: 'global' + properties: { + application: repoRadiusStateApp.id + environment: environment + codeReference: 'test/functional-portable/statestore/noncloud/testdata/repo-radius-state-app.bicep' + containers: { + main: { + image: 'ghcr.io/radius-project/mirror/debian:latest' + command: ['/bin/sh'] + args: ['-c', 'while true; do echo ${deploymentPhase}; sleep 10; done'] + } + } + connections: {} + } +} diff --git a/.radius/bicepconfig.json b/.radius/bicepconfig.json new file mode 100644 index 0000000000..ba32f165c9 --- /dev/null +++ b/.radius/bicepconfig.json @@ -0,0 +1,8 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true + }, + "extensions": { + "radius": "br:biceptypes.azurecr.io/radius:latest" + } +}