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
8 changes: 8 additions & 0 deletions .github/scripts/prepare-release_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ test_subsequent_rc_rejects_historical_form() {

test_final() {
setup_repo "v0.60.0-rc.3"
cp "${SCRIPT_DIR}/../../docs/release-notes/template.md" \
"${REPO}/docs/release-notes/template.md"
make_release_branch 0.60
git -C "${REPO}" tag v0.60.0-rc.3
run_prepare final 0.60
Expand All @@ -378,6 +380,8 @@ test_final() {
assert_file_contains "${REPO}/out/requires-backport.txt" 'true' || return
assert_file_contains "${REPO}/docs/release-notes/v0.60.0.md" \
'## Upgrading to Radius v0.60.0' || return
assert_file_contains "${REPO}/docs/release-notes/v0.60.0.md" \
'Restarting pods no longer picks up later patches implicitly' || return
((++PASS))
}

Expand Down Expand Up @@ -466,11 +470,15 @@ test_version_only_does_not_mutate_files() {

test_patch() {
setup_repo "v0.60.2"
cp "${SCRIPT_DIR}/../../docs/release-notes/template_patch.md" \
"${REPO}/docs/release-notes/template_patch.md"
make_release_branch 0.60
git -C "${REPO}" tag v0.60.2
git -C "${REPO}" tag v0.61.0
run_prepare patch 0.60
assert_version "v0.60.3" || return
assert_file_contains "${REPO}/docs/release-notes/v0.60.3.md" \
'Restarting pods no longer picks up later patches implicitly' || return
assert_file_contains "${REPO}/CHANGELOG.md" \
'compare/v0.60.2...v0.60.3' || return
((++PASS))
Expand Down
16 changes: 16 additions & 0 deletions .github/scripts/release-cutover_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ readonly REPO_ROOT
readonly RELEASE_WORKFLOW="${REPO_ROOT}/.github/workflows/build-release.yaml"
readonly CLI_WORKFLOW="${REPO_ROOT}/.github/workflows/__build-cli.yaml"
readonly IMAGE_WORKFLOW="${REPO_ROOT}/.github/workflows/__build-images.yaml"
readonly HELM_WORKFLOW="${REPO_ROOT}/.github/workflows/__build-helm-chart.yaml"
readonly CONFIG="${REPO_ROOT}/.goreleaser.yaml"
readonly ARTIFACTS_MAKEFILE="${REPO_ROOT}/build/artifacts.mk"
PASS=0
Expand Down Expand Up @@ -106,6 +107,21 @@ test_publication_gate() {
fail_test "failed verification or missing approval can reach publication"
return
fi
if ! yq -o=json '.jobs."build-and-push-helm-chart".steps' \
"${HELM_WORKFLOW}" | jq -e '
(map(.name) | index("Pin external chart images") as $pin |
$pin != null and $pin < index("Package Helm chart") and
$pin < index("Push helm chart to GHCR")) and
any(.[]; .name == "Pin external chart images" and
(.if | contains("refs/tags/v")) and
(.run | contains("--names dashboard")) and
(.run | contains("--names deployment-engine")) and
(.run | contains("--expected-digest")) and
(.run | contains("--source-sha")))
' > /dev/null; then
fail_test "chart publication can precede verified external version tags"
return
fi
((++PASS))
}

Expand Down
127 changes: 127 additions & 0 deletions .github/scripts/release-oci-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ VERIFY_ALIASES=false
VERIFY_SBOMS=false
PROMOTE_LATEST="${RELEASE_PROMOTE_LATEST:-true}"
SOURCE_SHA="${RELEASE_SOURCE_SHA:-}"
EXPECTED_DIGEST=""
TEMP_DIR=""
readonly RETRY_ATTEMPTS="${RELEASE_RETRY_ATTEMPTS:-5}"
readonly RETRY_MAX_DELAY_SECONDS="${RELEASE_RETRY_MAX_DELAY_SECONDS:-15}"
# External images are published by their own repositories from the sibling
# tags the controller creates, in parallel with the Radius tag build.
readonly EXTERNAL_IMAGE_WAIT_SECONDS="${RELEASE_EXTERNAL_IMAGE_WAIT_SECONDS:-600}"
readonly EXTERNAL_IMAGE_POLL_SECONDS="${RELEASE_EXTERNAL_IMAGE_POLL_SECONDS:-30}"
readonly SOURCE_ANNOTATION="org.opencontainers.image.source="
readonly SOURCE_URL="https://github.com/radius-project/radius"

Expand Down Expand Up @@ -141,6 +146,9 @@ retry_read() {
usage() {
cat >&2 << 'EOF'
Usage:
release-oci-artifacts.sh pin-image --registry <registry> --names <name> \
--version <version> --channel <published-tag> --source-sha <sha> \
[--expected-digest <sha256:...>]
release-oci-artifacts.sh stage-cli --registry <registry> \
--version <version> --artifacts <artifacts.json> --output <lock.json>
release-oci-artifacts.sh stage-cli --registry <registry> \
Expand Down Expand Up @@ -211,6 +219,10 @@ parse_args() {
SOURCE_SHA="${2:-}"
shift 2
;;
--expected-digest)
EXPECTED_DIGEST="${2:-}"
shift 2
;;
--aliases)
VERIFY_ALIASES=true
shift
Expand Down Expand Up @@ -608,6 +620,120 @@ image_reference_state() {
done
}

external_image_matches() {
local raw="$1"
local expected_platforms="$2"

jq -e --arg source "${SOURCE_SHA}" \
--argjson platforms "${expected_platforms}" '
def platform_name:
.os + "/" + .architecture +
(if (.variant // "") == "" then "" else "/" + .variant end);
([if .manifest.manifests then
.manifest.manifests[] | select(.platform.os != "unknown") | .platform
else .image end | platform_name] | sort) == $platforms and
([if .manifest.manifests then
.image | to_entries[] | select(.key != "unknown/unknown") | .value
else .image end | .config.Labels."org.opencontainers.image.revision"] |
unique) == [$source]
' <<< "${raw}" > /dev/null
}

# Prints the inspection of an external image reference once it exists and
# carries the planned source and platform set. The publisher that produces the
# reference runs in parallel with this build, so a missing reference or one
# still serving an earlier source is awaited for a bounded time. An existing
# immutable version tag is never awaited: a mismatch there is a conflict.
await_external_image() {
local reference="$1"
local expected_platforms="$2"
local immutable="$3"
local deadline=$((SECONDS + EXTERNAL_IMAGE_WAIT_SECONDS))
local raw problem

while true; do
if [[ "$(image_reference_state "${reference}")" == "absent" ]]; then
problem="external image is not published: ${reference}"
else
raw="$(retry_read "external image inspection" \
docker buildx imagetools inspect --format '{{json .}}' \
"${reference}")"
if external_image_matches "${raw}" "${expected_platforms}"; then
printf '%s\n' "${raw}"
return
fi
problem="external image source or platforms differ from the plan: ${reference}"
fi
if [[ "${immutable}" == "true" ]] || ((SECONDS >= deadline)); then
fail "${problem}"
fi
echo "Waiting for ${reference} to carry the planned source; its publisher may still be running." >&2
if [[ "${RELEASE_RETRY_NO_SLEEP:-}" != "true" ]]; then
sleep "${EXTERNAL_IMAGE_POLL_SECONDS}"
fi
done
}

pin_image() {
local repository reference target raw digest expected_platforms state
local attempt output status immutable

require_command docker
require_command oras
require_command jq
validate_version
validate_source_sha
[[ -n "${REGISTRY}" ]] || fail "registry is required"
[[ "${CHANNEL}" =~ ^[0-9]+\.[0-9]+$ ]] ||
is_radius_release_version "${CHANNEL}" || fail "invalid published tag"
[[ -z "${EXPECTED_DIGEST}" ||
"${EXPECTED_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]] ||
fail "invalid expected digest"
expected_platforms="$(jq -ce --arg name "${NAMES}" '
[.images[] | select(.name == $name and .radiusBuild == false)] |
if length == 1 then .[0].requiredPlatforms | sort
else error("select exactly one external image") end
' "${TARGETS_FILE}")"
repository="${REGISTRY%/}/${NAMES}"
target="${repository}:${VERSION}"
reference="${repository}:${CHANNEL}"
state="$(image_reference_state "${target}")"
immutable=false
if [[ "${state}" == "exists" ]]; then
reference="${target}"
immutable=true
fi
raw="$(await_external_image "${reference}" "${expected_platforms}" \
"${immutable}")"
digest="$(jq -er '.manifest.digest |
select(test("^sha256:[0-9a-f]{64}$"))' <<< "${raw}")"
[[ -z "${EXPECTED_DIGEST}" || "${digest}" == "${EXPECTED_DIGEST}" ]] ||
fail "external image differs from its locked digest: ${reference}"
[[ "${state}" == "exists" ]] && return

for ((attempt = 1; attempt <= RETRY_ATTEMPTS; attempt++)); do
if [[ "$(image_reference_state "${target}")" == "exists" ]]; then
verify_image_alias "${target}" "${digest}"
return
fi
if output="$(oras tag "${repository}@${digest}" "${VERSION}" 2>&1)"; then
verify_image_alias "${target}" "${digest}"
return
else
status=$?
fi
if [[ "$(image_reference_state "${target}")" == "exists" ]]; then
verify_image_alias "${target}" "${digest}"
return
fi
if ((attempt == RETRY_ATTEMPTS)) || ! is_retryable_error "${output}"; then
echo "${output}" >&2
return "${status}"
fi
wait_before_retry "immutable external image tag" "${attempt}"
done
}

assert_images_absent() {
local name
local reference
Expand Down Expand Up @@ -1045,6 +1171,7 @@ promote_aliases() {
main() {
parse_args "$@"
case "${COMMAND}" in
pin-image) pin_image ;;
stage-cli) stage_cli ;;
promote) promote_aliases ;;
verify) verify_locks ;;
Expand Down
Loading
Loading