Skip to content
Merged
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
18 changes: 11 additions & 7 deletions .github/workflows/cpflow-promote-staging-to-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ jobs:
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_TOKEN_PRODUCTION: ${{ secrets.CPLN_TOKEN_PRODUCTION }}
PRODUCTION_APP_NAME: ${{ vars.PRODUCTION_APP_NAME }}
CPLN_ORG_STAGING: ${{ vars.CPLN_ORG_STAGING }}
CPLN_ORG_PRODUCTION: ${{ vars.CPLN_ORG_PRODUCTION }}
Expand Down Expand Up @@ -379,7 +380,10 @@ jobs:
'[.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}"
staging_registry="${CPLN_ORG_STAGING}.registry.cpln.io"
production_registry="${CPLN_ORG_PRODUCTION}.registry.cpln.io"
source_image_ref="${CPLN_ORG_STAGING}.registry.cpln.io/${STAGING_IMAGE}"
production_image_ref="${CPLN_ORG_PRODUCTION}.registry.cpln.io/${production_image}"

docker_config_dir="$(mktemp -d)"
cleanup_copy_credentials() {
Expand All @@ -391,14 +395,14 @@ jobs:

copy_status=1
for attempt in $(seq 1 "${copy_image_attempts}"); do
if CPLN_TOKEN="${CPLN_TOKEN_STAGING}" cpln image docker-login --org "${CPLN_ORG_STAGING}" >/dev/null &&
if printf '%s' "${CPLN_TOKEN_STAGING}" |
docker login "${staging_registry}" -u '<token>' --password-stdin >/dev/null &&
printf '%s' "${CPLN_TOKEN_PRODUCTION}" |
docker login "${production_registry}" -u '<token>' --password-stdin >/dev/null &&
Comment on lines +398 to +401

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two docker login calls run on every retry attempt, but credentials never change between retries. Consider hoisting them above the loop so they run once — retries are really only needed for the pull/tag/push operations.

Suggested change
if printf '%s' "${CPLN_TOKEN_STAGING}" |
docker login "${staging_registry}" -u '<token>' --password-stdin >/dev/null &&
printf '%s' "${CPLN_TOKEN_PRODUCTION}" |
docker login "${production_registry}" -u '<token>' --password-stdin >/dev/null &&
if docker manifest inspect "${source_image_ref}" >/dev/null &&

(Move the two docker login calls to just after the export DOCKER_CONFIG line, before the retry loop.)

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
docker pull "${source_image_ref}" &&
docker tag "${source_image_ref}" "${production_image_ref}" &&
docker push "${production_image_ref}"; then
Comment on lines +403 to +405

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-arch concern: docker pull + docker tag + docker push only copies the single-platform image matching the runner's architecture. If the staging image is a multi-platform manifest list, production will silently receive a single-arch image.

If multi-arch is not needed, this is fine. Otherwise consider using docker buildx imagetools create, which copies the manifest list without pulling layers:

Suggested change
docker pull "${source_image_ref}" &&
docker tag "${source_image_ref}" "${production_image_ref}" &&
docker push "${production_image_ref}"; then
docker buildx imagetools create -t "${production_image_ref}" "${source_image_ref}"; then

(that replaces the three lines — if this one-liner works in your buildx setup, remove the pull/tag lines entirely).

Comment on lines +403 to +405

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 docker pull + push drops multi-arch manifest lists

docker pull fetches only the manifest entry that matches the runner's native platform (e.g., linux/amd64). If the staging image was built as a multi-arch manifest list, the production copy will silently become a single-architecture image. Tools like docker buildx imagetools create or crane copy preserve the full manifest list. This is low-risk for a single-platform Rails app, but worth keeping in mind if the image build ever switches to multi-arch.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

copy_status=0
break
else
Expand Down
Loading