-
Notifications
You must be signed in to change notification settings - Fork 7
Harden production promotion image copy and readiness #356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
48f8ade
81c3f99
59e1dfc
1d85351
8b89d21
ca0468f
ee11d62
f50e284
6cd8225
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -317,10 +317,10 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| echo "image=${staging_image}" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Copy image from staging | ||||||||||||||||||||||||||||||||||||||||||||||||
| id: copy-image | ||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Pass the upstream token via env rather than `-t` so it doesn't appear in /proc/<pid>/cmdline. | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_UPSTREAM_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -348,9 +348,38 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| staging_commit="${STAGING_IMAGE##*_}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "${staging_commit}" == "${STAGING_IMAGE}" || -z "${staging_commit}" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "::error::Staging image '${STAGING_IMAGE}' does not include the expected '_<commit>' suffix." | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Digest pin drops commit suffixMedium Severity When the staging image reference includes a digest ( Reviewed by Cursor Bugbot for commit 6cd8225. Configure here.
Comment on lines
+466
to
+475
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: commit suffix is never extracted for digest-pinned images. When
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| latest_number="$( | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a TOCTOU window between this query and the
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| cpln image query --org "${CPLN_ORG_PRODUCTION}" --prop "name~${PRODUCTION_APP_NAME}:" -o json | | ||||||||||||||||||||||||||||||||||||||||||||||||
| jq -r --arg prefix "${PRODUCTION_APP_NAME}:" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| '[.items[].name | select(startswith($prefix)) | (try capture("^[^:]+:(?<number>[0-9]+)") catch empty) | .number | tonumber] | max // 0' | ||||||||||||||||||||||||||||||||||||||||||||||||
| )" | ||||||||||||||||||||||||||||||||||||||||||||||||
| production_image="${PRODUCTION_APP_NAME}:$((latest_number + 1))_${staging_commit}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race condition on sequential promotions: If the Control Plane API supports it, passing
cursor[bot] marked this conversation as resolved.
Outdated
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent-failure risk in In bash, Suggested fix: validate the result before using it in arithmetic:
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The A registry-to-registry copy tool such as |
||||||||||||||||||||||||||||||||||||||||||||||||
| source_image_ref="${CPLN_ORG_STAGING}.registry.cpln.io/${STAGING_IMAGE}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| docker_config_dir="$(mktemp -d)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup_copy_credentials() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| rm -rf "${docker_config_dir}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| trap cleanup_copy_credentials EXIT | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export DOCKER_CONFIG="${docker_config_dir}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| copy_status=1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| for attempt in $(seq 1 "${copy_image_attempts}"); do | ||||||||||||||||||||||||||||||||||||||||||||||||
| if cpflow copy-image-from-upstream -a "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" --image "${STAGING_IMAGE}"; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| if CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image docker-login --org "${CPLN_ORG_STAGING}" >/dev/null && | ||||||||||||||||||||||||||||||||||||||||||||||||
| docker manifest inspect "${source_image_ref}" >/dev/null && | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_TOKEN="${CPLN_TOKEN_STAGING}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| cpln image copy "${STAGING_IMAGE}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --org "${CPLN_ORG_STAGING}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --to-profile default \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --to-org "${CPLN_ORG_PRODUCTION}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --to-name "${production_image}"; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| copy_status=0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -370,6 +399,8 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| exit "${copy_status}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| echo "image=${production_image}" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Deploy image to production | ||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||
| PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -531,19 +562,23 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| HEALTHY: ${{ steps.health-check.outputs.healthy }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PREVIOUS_IMAGE: ${{ steps.capture-current.outputs.current_image }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PREVIOUS_VERSION: ${{ steps.capture-current.outputs.current_version }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| COPIED_IMAGE: ${{ steps.copy-image.outputs.image }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| shell: bash | ||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "## Promotion Summary" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "${HEALTHY}" == "true" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "✅ Status: deployment successful" | ||||||||||||||||||||||||||||||||||||||||||||||||
| deployed_image="${COPIED_IMAGE}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "❌ Status: deployment failed" | ||||||||||||||||||||||||||||||||||||||||||||||||
| deployed_image="${PREVIOUS_IMAGE}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Previous image: \`${PREVIOUS_IMAGE}\`" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Previous version: ${PREVIOUS_VERSION}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Deployed image: \`${deployed_image}\`" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Summary mislabels failed promotion imageMedium Severity On a failed health check, the promotion summary always prints Reviewed by Cursor Bugbot for commit 6cd8225. Configure here. |
||||||||||||||||||||||||||||||||||||||||||||||||
| } >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| create-github-release: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -336,10 +336,10 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| echo "image=${staging_image}" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Copy image from staging | ||||||||||||||||||||||||||||||||||||||||||||||||
| id: copy-image | ||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||
| # Pass the upstream token via env rather than `-t` so it doesn't appear in /proc/<pid>/cmdline. | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_TOKEN_STAGING: ${{ secrets.CPLN_TOKEN_STAGING }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_UPSTREAM_TOKEN: ${{ secrets.CPLN_TOKEN_STAGING }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -367,9 +367,38 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| staging_commit="${STAGING_IMAGE##*_}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the staging workload was deployed with Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "${staging_commit}" == "${STAGING_IMAGE}" || -z "${staging_commit}" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "::error::Staging image '${STAGING_IMAGE}' does not include the expected '_<commit>' suffix." | ||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+485
to
+494
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: same commit suffix extraction issue as in the reusable workflow — for
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| latest_number="$( | ||||||||||||||||||||||||||||||||||||||||||||||||
| cpln image query --org "${CPLN_ORG_PRODUCTION}" --prop "name~${PRODUCTION_APP_NAME}:" -o json | | ||||||||||||||||||||||||||||||||||||||||||||||||
| jq -r --arg prefix "${PRODUCTION_APP_NAME}:" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| '[.items[].name | select(startswith($prefix)) | (try capture("^[^:]+:(?<number>[0-9]+)") catch empty) | .number | tonumber] | max // 0' | ||||||||||||||||||||||||||||||||||||||||||||||||
| )" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In repos that have accumulated more than 50 production images, this query only considers the CLI's first page before computing Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||||||||||||||||||||||||
| production_image="${PRODUCTION_APP_NAME}:$((latest_number + 1))_${staging_commit}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same silent-failure risk as the companion comment in the reusable workflow: Apply the same guard here:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
| source_image_ref="${CPLN_ORG_STAGING}.registry.cpln.io/${STAGING_IMAGE}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| docker_config_dir="$(mktemp -d)" | ||||||||||||||||||||||||||||||||||||||||||||||||
| cleanup_copy_credentials() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| rm -rf "${docker_config_dir}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| trap cleanup_copy_credentials EXIT | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export DOCKER_CONFIG="${docker_config_dir}" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| copy_status=1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| for attempt in $(seq 1 "${copy_image_attempts}"); do | ||||||||||||||||||||||||||||||||||||||||||||||||
| if cpflow copy-image-from-upstream -a "${PRODUCTION_APP_NAME}" --org "${CPLN_ORG_PRODUCTION}" --image "${STAGING_IMAGE}"; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| if CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image docker-login --org "${CPLN_ORG_STAGING}" >/dev/null && | ||||||||||||||||||||||||||||||||||||||||||||||||
| docker manifest inspect "${source_image_ref}" >/dev/null && | ||||||||||||||||||||||||||||||||||||||||||||||||
| CPLN_TOKEN="${CPLN_TOKEN_STAGING}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| cpln image copy "${STAGING_IMAGE}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --org "${CPLN_ORG_STAGING}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --to-profile default \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --to-org "${CPLN_ORG_PRODUCTION}" \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| --to-name "${production_image}"; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| copy_status=0 | ||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -389,6 +418,8 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| exit "${copy_status}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| echo "image=${production_image}" >> "$GITHUB_OUTPUT" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Deploy image to production | ||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||
| PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }} | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -553,21 +584,23 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||
| HEALTHY: ${{ steps.health-check.outputs.healthy }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PREVIOUS_IMAGE: ${{ steps.capture-current.outputs.current_image }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| PREVIOUS_VERSION: ${{ steps.capture-current.outputs.current_version }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| DEPLOYED_IMAGE: ${{ steps.staging-image.outputs.image }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| COPIED_IMAGE: ${{ steps.copy-image.outputs.image }} | ||||||||||||||||||||||||||||||||||||||||||||||||
| shell: bash | ||||||||||||||||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "## Promotion Summary" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||
| if [[ "${HEALTHY}" == "true" ]]; then | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "✅ Status: deployment successful" | ||||||||||||||||||||||||||||||||||||||||||||||||
| deployed_image="${COPIED_IMAGE}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "❌ Status: deployment failed" | ||||||||||||||||||||||||||||||||||||||||||||||||
| deployed_image="${PREVIOUS_IMAGE}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Previous image: \`${PREVIOUS_IMAGE}\`" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Previous version: ${PREVIOUS_VERSION}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Deployed image: \`${DEPLOYED_IMAGE}\`" | ||||||||||||||||||||||||||||||||||||||||||||||||
| echo "Deployed image: \`${deployed_image}\`" | ||||||||||||||||||||||||||||||||||||||||||||||||
| } >> "$GITHUB_STEP_SUMMARY" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| create-github-release: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the staging workload points at a valid cpflow image that was built without
--commit(for examplemy-app:7), this new guard aborts the entire production promotion even though cpflow still supports such tags:latest_image_nextonly appends_<commit>when a commit is present (lib/core/controlplane.rb:53-62), and the previouscopy-image-from-upstreampath passed a nil commit through to create the next plain production tag (lib/command/copy_image_from_upstream.rb:80-90). This regresses promotions for existing/manual staging images that do not have the optional commit suffix; generate the production name without a suffix in that case instead of exiting.Useful? React with 👍 / 👎.