From 1cc1615cee0ca5fd8bcb55648591d0d926139f43 Mon Sep 17 00:00:00 2001 From: Juan Celhay Date: Fri, 4 Sep 2026 11:39:18 -0400 Subject: [PATCH 1/5] ar exit gate promotion steps --- release/cloudbuild-nomulus.yaml | 123 +++++++++++++++++++++++++++----- 1 file changed, 105 insertions(+), 18 deletions(-) diff --git a/release/cloudbuild-nomulus.yaml b/release/cloudbuild-nomulus.yaml index cd0f5bbc588..4be5e947625 100644 --- a/release/cloudbuild-nomulus.yaml +++ b/release/cloudbuild-nomulus.yaml @@ -53,30 +53,118 @@ steps: ./gradlew :jetty:buildNomulusImage :proxy:buildProxyImage :core:buildToolImage\ -PmavenUrl=gcs://domain-registry-maven-repository/maven \ -PpluginsUrl=gcs://domain-registry-maven-repository/plugins - docker tag nomulus gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME} - docker tag nomulus gcr.io/${PROJECT_ID}/nomulus:latest - docker push gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME} - docker push gcr.io/${PROJECT_ID}/nomulus:latest - docker tag proxy gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker tag proxy gcr.io/${PROJECT_ID}/proxy:latest - docker push gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker push gcr.io/${PROJECT_ID}/proxy:latest + docker tag nomulus us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} + docker tag nomulus us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest + docker push us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} + docker push us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest + docker push us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} + docker push us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME} docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:latest docker push gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME} docker push gcr.io/${PROJECT_ID}/nomulus-tool:latest + nomulus_digest=$(gcloud artifacts docker images describe \ + us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} \ + --format="value(image_summary.digest)") + echo "$nomulus_digest" > /workspace/nomulus_digest + proxy_digest=$(gcloud artifacts docker images describe \ + us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} \ + --format="value(image_summary.digest)") + echo "$proxy_digest" > /workspace/proxy_digest +# Promote nomulus and proxy images via Artifact Registry Exit Gate. +- name: 'gcr.io/${PROJECT_ID}/builder:latest' + entrypoint: /bin/bash + args: + - -c + - | + set -e + LOCATION="us" + DEST_REPO="gcr.io" + SRC_REPO="staging" + ENDPOINT="https://artifactregistry.googleapis.com/v1/projects/${PROJECT_ID}/locations/$LOCATION/repositories/$DEST_REPO:promoteArtifact" + TOKEN=$(gcloud auth print-access-token) + + promote_image() { + local pkg="$1" + local digest="$2" + echo "================================================================================" + echo "Initiating Artifact Registry Exit Gate promotion for $pkg:${TAG_NAME} ($digest)" + echo "================================================================================" + + PAYLOAD=$(printf '{"source_repository":"projects/%s/locations/%s/repositories/%s","source_version":"projects/%s/locations/%s/repositories/%s/packages/%s/versions/%s","include_all_tags":true,"overwrite_tags":true,"attachment_behavior":"PUBLIC_BCID_VSA_ONLY"}' \ + "${PROJECT_ID}" "$LOCATION" "$SRC_REPO" \ + "${PROJECT_ID}" "$LOCATION" "$SRC_REPO" "$pkg" "$digest") + + OPERATION_JSON=$(curl -s --no-progress-meter \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -X POST "$ENDPOINT" \ + -d "$PAYLOAD") + + OPERATION_NAME=$(echo "$OPERATION_JSON" | python3 -c "import sys, json; d=json.load(sys.stdin); sys.exit(f'ERROR: {d[\"error\"]}') if 'error' in d else print(d.get('name', ''))") + if [[ $? -ne 0 || -z "$OPERATION_NAME" ]]; then + echo "Failed to initiate promotion for $pkg" + exit 1 + fi + + echo "Promotion operation started: $OPERATION_NAME" + echo "Polling operation status until completion..." + + MAX_ATTEMPTS=60 + ATTEMPT=0 + while true; do + ATTEMPT=$((ATTEMPT + 1)) + if [[ $ATTEMPT -gt $MAX_ATTEMPTS ]]; then + echo "ERROR: Timed out waiting for promotion operation on $pkg to complete." + exit 1 + fi + + STATUS_JSON=$(gcloud artifacts operations describe "$OPERATION_NAME" \ + --project="${PROJECT_ID}" \ + --location="$LOCATION" \ + --format="json" 2>/dev/null || true) + + if [[ -z "$STATUS_JSON" ]]; then + echo "Warning: Failed to query operation status; retrying in 5s..." + sleep 5 + continue + fi + + RESULT=$(echo "$STATUS_JSON" | python3 -c "import sys, json; d=json.load(sys.stdin); print('IN_PROGRESS' if not d.get('done') else ('ERROR: ' + json.dumps(d['error']) if 'error' in d else 'SUCCESS'))" 2>/dev/null || echo "RETRY") + + if [[ "$RESULT" == "SUCCESS" ]]; then + echo "================================================================================" + echo "Artifact promotion succeeded! BCID VSA attached to $DEST_REPO/$pkg." + echo "================================================================================" + break + elif [[ "$RESULT" =~ ^ERROR: ]]; then + echo "================================================================================" + echo "ERROR: Artifact promotion failed BCID policy evaluation or execution for $pkg:" + echo "$RESULT" + echo "================================================================================" + exit 1 + elif [[ "$RESULT" == "IN_PROGRESS" || "$RESULT" == "RETRY" ]]; then + echo "Operation in progress... (attempt $ATTEMPT/$MAX_ATTEMPTS), retrying in 5s..." + sleep 5 + fi + done + } + + nomulus_digest=$(cat /workspace/nomulus_digest) + proxy_digest=$(cat /workspace/proxy_digest) + + promote_image "nomulus" "$nomulus_digest" + promote_image "proxy" "$proxy_digest" # Sign nomulus and proxy images. - name: 'gcr.io/${PROJECT_ID}/builder:latest' entrypoint: /bin/bash args: - -c - | - nomulus_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/nomulus \ - --format="get(digest)" --filter="tags = ${TAG_NAME}") - echo "$nomulus_digest" > /workspace/nomulus_digest - proxy_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \ - --format="get(digest)" --filter="tags = ${TAG_NAME}") - echo "$proxy_digest" > /workspace/proxy_digest + nomulus_digest=$(cat /workspace/nomulus_digest) + proxy_digest=$(cat /workspace/proxy_digest) gcloud --project=${PROJECT_ID} beta container binauthz attestations \ sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \ --attestor=build-attestor --attestor-project=${PROJECT_ID} \ @@ -228,11 +316,10 @@ artifacts: - 'release/cloudbuild-restart-proxies-*.yaml' - 'jetty/kubernetes/*.yaml' - 'jetty/kubernetes/gateway/*.yaml' -# The images are already uploaded, but we still need to include them there so that -# the GCB pubsub message contains them (for Spinnaker to consume). +# Staging images uploaded to Artifact Registry for which Cloud Build generates provenance. images: - - 'gcr.io/${PROJECT_ID}/nomulus:${TAG_NAME}' - - 'gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}' timeout: 7200s options: machineType: 'E2_HIGHCPU_32' \ No newline at end of file From 2d69c605f843838120cdd817c29e853aedb14767 Mon Sep 17 00:00:00 2001 From: Juan Celhay Date: Fri, 4 Sep 2026 12:33:10 -0400 Subject: [PATCH 2/5] move promote step to script --- release/cloudbuild-nomulus.yaml | 147 +------------------------- release/cloudbuild-proxy.yaml | 29 +---- release/cloudbuild-release.yaml | 5 + release/promote_and_deploy.sh | 182 ++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 170 deletions(-) create mode 100755 release/promote_and_deploy.sh diff --git a/release/cloudbuild-nomulus.yaml b/release/cloudbuild-nomulus.yaml index 4be5e947625..c2d74b56149 100644 --- a/release/cloudbuild-nomulus.yaml +++ b/release/cloudbuild-nomulus.yaml @@ -55,128 +55,12 @@ steps: -PpluginsUrl=gcs://domain-registry-maven-repository/plugins docker tag nomulus us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} docker tag nomulus us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest - docker push us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} - docker push us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest - docker push us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} - docker push us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME} docker tag nomulus-tool gcr.io/${PROJECT_ID}/nomulus-tool:latest docker push gcr.io/${PROJECT_ID}/nomulus-tool:${TAG_NAME} docker push gcr.io/${PROJECT_ID}/nomulus-tool:latest - nomulus_digest=$(gcloud artifacts docker images describe \ - us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME} \ - --format="value(image_summary.digest)") - echo "$nomulus_digest" > /workspace/nomulus_digest - proxy_digest=$(gcloud artifacts docker images describe \ - us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} \ - --format="value(image_summary.digest)") - echo "$proxy_digest" > /workspace/proxy_digest -# Promote nomulus and proxy images via Artifact Registry Exit Gate. -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - set -e - LOCATION="us" - DEST_REPO="gcr.io" - SRC_REPO="staging" - ENDPOINT="https://artifactregistry.googleapis.com/v1/projects/${PROJECT_ID}/locations/$LOCATION/repositories/$DEST_REPO:promoteArtifact" - TOKEN=$(gcloud auth print-access-token) - - promote_image() { - local pkg="$1" - local digest="$2" - echo "================================================================================" - echo "Initiating Artifact Registry Exit Gate promotion for $pkg:${TAG_NAME} ($digest)" - echo "================================================================================" - - PAYLOAD=$(printf '{"source_repository":"projects/%s/locations/%s/repositories/%s","source_version":"projects/%s/locations/%s/repositories/%s/packages/%s/versions/%s","include_all_tags":true,"overwrite_tags":true,"attachment_behavior":"PUBLIC_BCID_VSA_ONLY"}' \ - "${PROJECT_ID}" "$LOCATION" "$SRC_REPO" \ - "${PROJECT_ID}" "$LOCATION" "$SRC_REPO" "$pkg" "$digest") - - OPERATION_JSON=$(curl -s --no-progress-meter \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -X POST "$ENDPOINT" \ - -d "$PAYLOAD") - - OPERATION_NAME=$(echo "$OPERATION_JSON" | python3 -c "import sys, json; d=json.load(sys.stdin); sys.exit(f'ERROR: {d[\"error\"]}') if 'error' in d else print(d.get('name', ''))") - if [[ $? -ne 0 || -z "$OPERATION_NAME" ]]; then - echo "Failed to initiate promotion for $pkg" - exit 1 - fi - - echo "Promotion operation started: $OPERATION_NAME" - echo "Polling operation status until completion..." - - MAX_ATTEMPTS=60 - ATTEMPT=0 - while true; do - ATTEMPT=$((ATTEMPT + 1)) - if [[ $ATTEMPT -gt $MAX_ATTEMPTS ]]; then - echo "ERROR: Timed out waiting for promotion operation on $pkg to complete." - exit 1 - fi - - STATUS_JSON=$(gcloud artifacts operations describe "$OPERATION_NAME" \ - --project="${PROJECT_ID}" \ - --location="$LOCATION" \ - --format="json" 2>/dev/null || true) - - if [[ -z "$STATUS_JSON" ]]; then - echo "Warning: Failed to query operation status; retrying in 5s..." - sleep 5 - continue - fi - - RESULT=$(echo "$STATUS_JSON" | python3 -c "import sys, json; d=json.load(sys.stdin); print('IN_PROGRESS' if not d.get('done') else ('ERROR: ' + json.dumps(d['error']) if 'error' in d else 'SUCCESS'))" 2>/dev/null || echo "RETRY") - - if [[ "$RESULT" == "SUCCESS" ]]; then - echo "================================================================================" - echo "Artifact promotion succeeded! BCID VSA attached to $DEST_REPO/$pkg." - echo "================================================================================" - break - elif [[ "$RESULT" =~ ^ERROR: ]]; then - echo "================================================================================" - echo "ERROR: Artifact promotion failed BCID policy evaluation or execution for $pkg:" - echo "$RESULT" - echo "================================================================================" - exit 1 - elif [[ "$RESULT" == "IN_PROGRESS" || "$RESULT" == "RETRY" ]]; then - echo "Operation in progress... (attempt $ATTEMPT/$MAX_ATTEMPTS), retrying in 5s..." - sleep 5 - fi - done - } - - nomulus_digest=$(cat /workspace/nomulus_digest) - proxy_digest=$(cat /workspace/proxy_digest) - - promote_image "nomulus" "$nomulus_digest" - promote_image "proxy" "$proxy_digest" -# Sign nomulus and proxy images. -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - nomulus_digest=$(cat /workspace/nomulus_digest) - proxy_digest=$(cat /workspace/proxy_digest) - gcloud --project=${PROJECT_ID} beta container binauthz attestations \ - sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \ - --attestor=build-attestor --attestor-project=${PROJECT_ID} \ - --keyversion-project=${PROJECT_ID} --keyversion-location=global \ - --keyversion-keyring=attestor-keys --keyversion-key=signing \ - --keyversion=1 - gcloud --project=${PROJECT_ID} beta container binauthz attestations \ - sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/proxy@$proxy_digest \ - --attestor=build-attestor --attestor-project=${PROJECT_ID} \ - --keyversion-project=${PROJECT_ID} --keyversion-location=global \ - --keyversion-keyring=attestor-keys --keyversion-key=signing \ - --keyversion=1 # Get the tool image digest and substitute in the digest in other GCB files. - name: 'gcr.io/${PROJECT_ID}/builder:latest' entrypoint: /bin/bash @@ -265,35 +149,6 @@ steps: cp db/build/libs/schema.jar output/ cp core/build/libs/nomulus-public.jar output/ cp core/build/libs/nomulus-tests-alldeps.jar output/ -# Create a release in Cloud Deploy to trigger the deployment pipeline -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - set -e - echo "=============================================" - echo "Triggering Google Cloud Deploy Release" - echo "=============================================" - echo "Tag Name: ${TAG_NAME}" - echo "Project ID: ${PROJECT_ID}" - pipeline="deploy-nomulus" - region="us-central1" - # Release names must consist of lowercase letters, numbers, and hyphens. - release_name=$(echo "${TAG_NAME}" | tr '[:upper:]' '[:lower:]' | tr '_' '-') - echo "Release Name: $release_name" - echo "=============================================" - # Read the pre-fetched image digest from the workspace file - nomulus_digest=$(cat /workspace/nomulus_digest) - proxy_digest=$(cat /workspace/proxy_digest) - gcloud deploy releases create "$release_name" \ - --delivery-pipeline="$pipeline" \ - --region="$region" \ - --project=${PROJECT_ID} \ - --images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \ - --source=. \ - --skaffold-file=release/clouddeploy/skaffold.yaml \ - --deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}" # The tarballs and jars to upload to GCS. artifacts: objects: @@ -316,7 +171,7 @@ artifacts: - 'release/cloudbuild-restart-proxies-*.yaml' - 'jetty/kubernetes/*.yaml' - 'jetty/kubernetes/gateway/*.yaml' -# Staging images uploaded to Artifact Registry for which Cloud Build generates provenance. +# Images to upload to staging repository for build provenance generation. images: - 'us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME}' - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}' diff --git a/release/cloudbuild-proxy.yaml b/release/cloudbuild-proxy.yaml index f628d2a11be..755b7db82f0 100644 --- a/release/cloudbuild-proxy.yaml +++ b/release/cloudbuild-proxy.yaml @@ -18,37 +18,18 @@ steps: - :proxy:buildProxyImage - -PmavenUrl=gcs://domain-registry-maven-repository/maven - -PpluginsUrl=gcs://domain-registry-maven-repository/plugins -# Tag and push the image. We can't let Cloud Build's default processing do that for us -# because we need to push the image before we can sign it in the following step. +# Tag the image for staging repository. - name: 'gcr.io/${PROJECT_ID}/builder:latest' entrypoint: /bin/bash args: - -c - | set -e - docker tag proxy gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker tag proxy gcr.io/${PROJECT_ID}/proxy:latest - docker push gcr.io/${PROJECT_ID}/proxy:${TAG_NAME} - docker push gcr.io/${PROJECT_ID}/proxy:latest + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME} + docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest dir: 'proxy' -# Get the image digest, sign it and substitute in the digest in the tagging yaml file. -- name: 'gcr.io/${PROJECT_ID}/builder:latest' - entrypoint: /bin/bash - args: - - -c - - | - set -e - digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \ - --format="get(digest)" --filter="tags = ${TAG_NAME}") - gcloud --project=${PROJECT_ID} alpha container binauthz attestations \ - sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/proxy@$digest \ - --attestor=build-attestor --attestor-project=${PROJECT_ID} \ - --keyversion-project=${PROJECT_ID} --keyversion-location=global \ - --keyversion-keyring=attestor-keys --keyversion-key=signing \ - --keyversion=1 -# Images to upload to GCR. Even though the image has already been uploaded, we still include it -# here so that the GCB pubsub message contains it (for Spinnaker to consume). -images: ['gcr.io/${PROJECT_ID}/proxy:${TAG_NAME}'] +# Images to upload to staging repository for build provenance generation. +images: ['us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}'] # Config files to upload to GCS. artifacts: objects: diff --git a/release/cloudbuild-release.yaml b/release/cloudbuild-release.yaml index b9453dca264..2467d1871e7 100644 --- a/release/cloudbuild-release.yaml +++ b/release/cloudbuild-release.yaml @@ -324,6 +324,7 @@ steps: entrypoint: 'bash' env: - 'TAG_NAME=${TAG_NAME}' + - 'PROJECT_ID=${PROJECT_ID}' args: - -c - | @@ -332,10 +333,14 @@ steps: if [[ "$TAG_NAME" =~ ^nomulus-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a nomulus release. Triggering nomulus build..." gcloud builds submit . --config=release/cloudbuild-nomulus.yaml --substitutions="TAG_NAME=$TAG_NAME" + echo "Promoting, signing, and deploying nomulus release..." + ./release/promote_and_deploy.sh "nomulus" "$TAG_NAME" "$PROJECT_ID" # Check for a proxy release tag (e.g., "proxy-v1.2.3") elif [[ "$TAG_NAME" =~ ^proxy-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a proxy release. Triggering proxy build..." gcloud builds submit . --config=release/cloudbuild-proxy.yaml --substitutions="TAG_NAME=$TAG_NAME" + echo "Promoting and signing proxy release..." + ./release/promote_and_deploy.sh "proxy" "$TAG_NAME" "$PROJECT_ID" else echo "Tag format '$TAG_NAME' does not match a known release type. Exiting." exit 1 diff --git a/release/promote_and_deploy.sh b/release/promote_and_deploy.sh new file mode 100755 index 00000000000..77c859e6f1a --- /dev/null +++ b/release/promote_and_deploy.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# Copyright 2026 The Nomulus Authors. All Rights Reserved. +# +# 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. +# +# This script handles post-build promotion, BinAuthz signing, and deployment: +# 1. Obtains the image digests from the staging repository. +# 2. Promotes the images from staging to gcr.io via the Artifact Registry +# promoteArtifact API (evaluating BCID exit gate policy and attaching VSA). +# 3. Signs the promoted gcr.io images with Binary Authorization. +# 4. Triggers Cloud Deploy release creation (for nomulus release tags). +# +# Usage: +# release/promote_and_deploy.sh +# where is "nomulus" or "proxy". + +set -e + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " + exit 1 +fi + +RELEASE_TYPE="$1" +TAG_NAME="$2" +PROJECT_ID="$3" + +LOCATION="us" +DEST_REPO="gcr.io" +SRC_REPO="staging" +ENDPOINT="https://artifactregistry.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION}/repositories/${DEST_REPO}:promoteArtifact" + +promote_artifact() { + local pkg="$1" + local digest="$2" + + echo "================================================================================" + echo "Promoting ${pkg}:${TAG_NAME} (${digest}) to ${DEST_REPO} via Artifact Registry" + echo "================================================================================" + + local token + token=$(gcloud auth print-access-token) + + local payload + payload=$(printf '{"source_repository":"projects/%s/locations/%s/repositories/%s","source_version":"projects/%s/locations/%s/repositories/%s/packages/%s/versions/%s","include_all_tags":true,"overwrite_tags":true,"attachment_behavior":"PUBLIC_BCID_VSA_ONLY"}' \ + "${PROJECT_ID}" "${LOCATION}" "${SRC_REPO}" \ + "${PROJECT_ID}" "${LOCATION}" "${SRC_REPO}" "${pkg}" "${digest}") + + local operation_json + operation_json=$(curl -s --no-progress-meter \ + -H "Authorization: Bearer ${token}" \ + -H "Content-Type: application/json" \ + -X POST "${ENDPOINT}" \ + -d "${payload}") + + local operation_name + operation_name=$(echo "${operation_json}" | python3 -c "import sys, json; d=json.load(sys.stdin); sys.exit(f'ERROR: {d[\"error\"]}') if 'error' in d else print(d.get('name', ''))") + if [[ $? -ne 0 || -z "${operation_name}" ]]; then + echo "Failed to initiate promotion for ${pkg}: ${operation_json}" + exit 1 + fi + + echo "Promotion operation started: ${operation_name}" + echo "Polling operation status until completion..." + + local max_attempts=60 + local attempt=0 + while true; do + attempt=$((attempt + 1)) + if [[ ${attempt} -gt ${max_attempts} ]]; then + echo "ERROR: Timed out waiting for promotion operation on ${pkg} to complete." + exit 1 + fi + + local status_json + status_json=$(gcloud artifacts operations describe "${operation_name}" \ + --project="${PROJECT_ID}" \ + --location="${LOCATION}" \ + --format="json" 2>/dev/null || true) + + if [[ -z "${status_json}" ]]; then + echo "Warning: Failed to query operation status; retrying in 5s..." + sleep 5 + continue + fi + + local result + result=$(echo "${status_json}" | python3 -c "import sys, json; d=json.load(sys.stdin); print('IN_PROGRESS' if not d.get('done') else ('ERROR: ' + json.dumps(d['error']) if 'error' in d else 'SUCCESS'))" 2>/dev/null || echo "RETRY") + + if [[ "${result}" == "SUCCESS" ]]; then + echo "================================================================================" + echo "Artifact promotion succeeded! BCID VSA attached to ${DEST_REPO}/${pkg}." + echo "================================================================================" + break + elif [[ "${result}" =~ ^ERROR: ]]; then + echo "================================================================================" + echo "ERROR: Artifact promotion failed BCID policy evaluation or execution for ${pkg}:" + echo "${result}" + echo "================================================================================" + exit 1 + elif [[ "${result}" == "IN_PROGRESS" || "${result}" == "RETRY" ]]; then + echo "Operation in progress... (attempt ${attempt}/${max_attempts}), retrying in 5s..." + sleep 5 + fi + done +} + +sign_binauthz() { + local image_name="$1" + local digest="$2" + + echo "================================================================================" + echo "Signing ${image_name}@${digest} with Binary Authorization" + echo "================================================================================" + + gcloud --project="${PROJECT_ID}" beta container binauthz attestations \ + sign-and-create --artifact-url="gcr.io/${PROJECT_ID}/${image_name}@${digest}" \ + --attestor=build-attestor --attestor-project="${PROJECT_ID}" \ + --keyversion-project="${PROJECT_ID}" --keyversion-location=global \ + --keyversion-keyring=attestor-keys --keyversion-key=signing \ + --keyversion=1 +} + +if [[ "${RELEASE_TYPE}" == "nomulus" ]]; then + echo "Retrieving digests from staging for nomulus release..." + nomulus_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME}" \ + --format="value(image_summary.digest)") + proxy_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}" \ + --format="value(image_summary.digest)") + + echo "nomulus digest: ${nomulus_digest}" + echo "proxy digest: ${proxy_digest}" + + promote_artifact "nomulus" "${nomulus_digest}" + promote_artifact "proxy" "${proxy_digest}" + + sign_binauthz "nomulus" "${nomulus_digest}" + sign_binauthz "proxy" "${proxy_digest}" + + echo "================================================================================" + echo "Triggering Google Cloud Deploy Release" + echo "================================================================================" + pipeline="deploy-nomulus" + region="us-central1" + release_name=$(echo "${TAG_NAME}" | tr '[:upper:]' '[:lower:]' | tr '_' '-') + echo "Release Name: ${release_name}" + + gcloud deploy releases create "${release_name}" \ + --delivery-pipeline="${pipeline}" \ + --region="${region}" \ + --project="${PROJECT_ID}" \ + --images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \ + --source=. \ + --skaffold-file=release/clouddeploy/skaffold.yaml \ + --deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}" + +elif [[ "${RELEASE_TYPE}" == "proxy" ]]; then + echo "Retrieving digest from staging for proxy release..." + proxy_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}" \ + --format="value(image_summary.digest)") + + echo "proxy digest: ${proxy_digest}" + + promote_artifact "proxy" "${proxy_digest}" + sign_binauthz "proxy" "${proxy_digest}" +else + echo "Unknown release type: ${RELEASE_TYPE}" + exit 1 +fi From e69cfd26717703d6e886c104f45ade2cc79319db Mon Sep 17 00:00:00 2001 From: Juan Celhay Date: Fri, 4 Sep 2026 13:45:29 -0400 Subject: [PATCH 3/5] promote only script --- release/cloudbuild-release.yaml | 47 +++++++++++++++++-- ...ote_and_deploy.sh => promote_artifacts.sh} | 22 +-------- 2 files changed, 44 insertions(+), 25 deletions(-) rename release/{promote_and_deploy.sh => promote_artifacts.sh} (84%) diff --git a/release/cloudbuild-release.yaml b/release/cloudbuild-release.yaml index 2467d1871e7..600e9319b7d 100644 --- a/release/cloudbuild-release.yaml +++ b/release/cloudbuild-release.yaml @@ -319,7 +319,7 @@ steps: rm ${gradle_bin} sed -i s%services.gradle.org/distributions%storage.googleapis.com/${gcs_loc}% \ gradle/wrapper/gradle-wrapper.properties -# Conditionally trigger the appropriate build based on the tag format. +# Conditionally trigger the appropriate build based on the tag format and promote artifacts. - name: 'gcr.io/cloud-builders/gcloud' entrypoint: 'bash' env: @@ -333,18 +333,55 @@ steps: if [[ "$TAG_NAME" =~ ^nomulus-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a nomulus release. Triggering nomulus build..." gcloud builds submit . --config=release/cloudbuild-nomulus.yaml --substitutions="TAG_NAME=$TAG_NAME" - echo "Promoting, signing, and deploying nomulus release..." - ./release/promote_and_deploy.sh "nomulus" "$TAG_NAME" "$PROJECT_ID" + echo "Promoting and signing nomulus release artifacts..." + ./release/promote_artifacts.sh "nomulus" "$TAG_NAME" "$PROJECT_ID" # Check for a proxy release tag (e.g., "proxy-v1.2.3") elif [[ "$TAG_NAME" =~ ^proxy-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a proxy release. Triggering proxy build..." gcloud builds submit . --config=release/cloudbuild-proxy.yaml --substitutions="TAG_NAME=$TAG_NAME" - echo "Promoting and signing proxy release..." - ./release/promote_and_deploy.sh "proxy" "$TAG_NAME" "$PROJECT_ID" + echo "Promoting and signing proxy release artifacts..." + ./release/promote_artifacts.sh "proxy" "$TAG_NAME" "$PROJECT_ID" else echo "Tag format '$TAG_NAME' does not match a known release type. Exiting." exit 1 fi +# Create a release in Cloud Deploy to trigger the deployment pipeline +- name: 'gcr.io/$PROJECT_ID/builder:latest' + entrypoint: /bin/bash + env: + - 'TAG_NAME=${TAG_NAME}' + - 'PROJECT_ID=${PROJECT_ID}' + args: + - -c + - | + set -e + if [[ "$TAG_NAME" =~ ^nomulus-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then + echo "=============================================" + echo "Triggering Google Cloud Deploy Release" + echo "=============================================" + echo "Tag Name: ${TAG_NAME}" + echo "Project ID: ${PROJECT_ID}" + pipeline="deploy-nomulus" + region="us-central1" + # Release names must consist of lowercase letters, numbers, and hyphens. + release_name=$(echo "${TAG_NAME}" | tr '[:upper:]' '[:lower:]' | tr '_' '-') + echo "Release Name: $release_name" + echo "=============================================" + nomulus_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus:${TAG_NAME}" \ + --format="value(image_summary.digest)") + proxy_digest=$(gcloud artifacts docker images describe \ + "us-docker.pkg.dev/${PROJECT_ID}/gcr.io/proxy:${TAG_NAME}" \ + --format="value(image_summary.digest)") + gcloud deploy releases create "$release_name" \ + --delivery-pipeline="$pipeline" \ + --region="$region" \ + --project=${PROJECT_ID} \ + --images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \ + --source=. \ + --skaffold-file=release/clouddeploy/skaffold.yaml \ + --deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}" + fi # Run the BEAM smoke test, using the builder and pipeline image just created - name: 'gcr.io/$PROJECT_ID/builder:latest' entrypoint: /bin/bash diff --git a/release/promote_and_deploy.sh b/release/promote_artifacts.sh similarity index 84% rename from release/promote_and_deploy.sh rename to release/promote_artifacts.sh index 77c859e6f1a..0de793c5caa 100755 --- a/release/promote_and_deploy.sh +++ b/release/promote_artifacts.sh @@ -13,15 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# This script handles post-build promotion, BinAuthz signing, and deployment: +# This script handles post-build promotion and BinAuthz signing: # 1. Obtains the image digests from the staging repository. # 2. Promotes the images from staging to gcr.io via the Artifact Registry # promoteArtifact API (evaluating BCID exit gate policy and attaching VSA). # 3. Signs the promoted gcr.io images with Binary Authorization. -# 4. Triggers Cloud Deploy release creation (for nomulus release tags). # # Usage: -# release/promote_and_deploy.sh +# release/promote_artifacts.sh # where is "nomulus" or "proxy". set -e @@ -149,23 +148,6 @@ if [[ "${RELEASE_TYPE}" == "nomulus" ]]; then sign_binauthz "nomulus" "${nomulus_digest}" sign_binauthz "proxy" "${proxy_digest}" - echo "================================================================================" - echo "Triggering Google Cloud Deploy Release" - echo "================================================================================" - pipeline="deploy-nomulus" - region="us-central1" - release_name=$(echo "${TAG_NAME}" | tr '[:upper:]' '[:lower:]' | tr '_' '-') - echo "Release Name: ${release_name}" - - gcloud deploy releases create "${release_name}" \ - --delivery-pipeline="${pipeline}" \ - --region="${region}" \ - --project="${PROJECT_ID}" \ - --images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \ - --source=. \ - --skaffold-file=release/clouddeploy/skaffold.yaml \ - --deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}" - elif [[ "${RELEASE_TYPE}" == "proxy" ]]; then echo "Retrieving digest from staging for proxy release..." proxy_digest=$(gcloud artifacts docker images describe \ From 24b7378eb810cda50838b8259517460a7203f4e3 Mon Sep 17 00:00:00 2001 From: Juan Celhay Date: Fri, 4 Sep 2026 15:19:15 -0400 Subject: [PATCH 4/5] add push for latest tag --- release/cloudbuild-nomulus.yaml | 2 ++ release/cloudbuild-proxy.yaml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/release/cloudbuild-nomulus.yaml b/release/cloudbuild-nomulus.yaml index c2d74b56149..ef4d9f44de3 100644 --- a/release/cloudbuild-nomulus.yaml +++ b/release/cloudbuild-nomulus.yaml @@ -174,7 +174,9 @@ artifacts: # Images to upload to staging repository for build provenance generation. images: - 'us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/nomulus:latest' - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest' timeout: 7200s options: machineType: 'E2_HIGHCPU_32' \ No newline at end of file diff --git a/release/cloudbuild-proxy.yaml b/release/cloudbuild-proxy.yaml index 755b7db82f0..c73b4029455 100644 --- a/release/cloudbuild-proxy.yaml +++ b/release/cloudbuild-proxy.yaml @@ -29,7 +29,9 @@ steps: docker tag proxy us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest dir: 'proxy' # Images to upload to staging repository for build provenance generation. -images: ['us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}'] +images: + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:${TAG_NAME}' + - 'us-docker.pkg.dev/${PROJECT_ID}/staging/proxy:latest' # Config files to upload to GCS. artifacts: objects: From 8ada7af1dddbabdec96f2454987be451d49ec23e Mon Sep 17 00:00:00 2001 From: Juan Celhay Date: Fri, 4 Sep 2026 15:26:14 -0400 Subject: [PATCH 5/5] explicit call to bash to execute promote script --- release/cloudbuild-release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release/cloudbuild-release.yaml b/release/cloudbuild-release.yaml index 600e9319b7d..d3f3074ec03 100644 --- a/release/cloudbuild-release.yaml +++ b/release/cloudbuild-release.yaml @@ -334,13 +334,13 @@ steps: echo "Tag format matches a nomulus release. Triggering nomulus build..." gcloud builds submit . --config=release/cloudbuild-nomulus.yaml --substitutions="TAG_NAME=$TAG_NAME" echo "Promoting and signing nomulus release artifacts..." - ./release/promote_artifacts.sh "nomulus" "$TAG_NAME" "$PROJECT_ID" + bash ./release/promote_artifacts.sh "nomulus" "$TAG_NAME" "$PROJECT_ID" # Check for a proxy release tag (e.g., "proxy-v1.2.3") elif [[ "$TAG_NAME" =~ ^proxy-20[0-9]{2}[0-1][0-9][0-3][0-9]-RC[0-9]{2}$ ]]; then echo "Tag format matches a proxy release. Triggering proxy build..." gcloud builds submit . --config=release/cloudbuild-proxy.yaml --substitutions="TAG_NAME=$TAG_NAME" echo "Promoting and signing proxy release artifacts..." - ./release/promote_artifacts.sh "proxy" "$TAG_NAME" "$PROJECT_ID" + bash ./release/promote_artifacts.sh "proxy" "$TAG_NAME" "$PROJECT_ID" else echo "Tag format '$TAG_NAME' does not match a known release type. Exiting." exit 1