@@ -389,6 +389,9 @@ jobs:
389389 # promote-trigger passes it to the poll script so a stale pipeline execution
390390 # reusing the same image digest can't satisfy the cutover gate.
391391 retag_epoch : ${{ steps.promote.outputs.retag_epoch }}
392+ # 'false' when the app deploy tag didn't move to a new digest (no ECS deploy).
393+ # promote-trigger promotes immediately in that case instead of waiting.
394+ app_image_changed : ${{ steps.promote.outputs.app_image_changed }}
392395 steps :
393396 - name : Configure AWS credentials
394397 uses : aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
@@ -440,6 +443,23 @@ jobs:
440443 ECR_TAG="staging"
441444 fi
442445
446+ # Detect whether the APP deploy tag actually moves to a new digest. If
447+ # this commit's app image is byte-identical to the currently-deployed one
448+ # (e.g. a commit that doesn't touch the app image — docs/CI-only), the
449+ # retag is a no-op, ECR fires no push event, and no ECS app deploy runs.
450+ # promote-trigger reads this to promote immediately instead of waiting for
451+ # a cutover that will never happen.
452+ get_digest() { docker buildx imagetools inspect "$1" 2>/dev/null | awk '/^Digest:/{print $2; exit}'; }
453+ APP_REF="${REGISTRY}/${{ secrets.ECR_APP }}"
454+ NEW_APP_DIGEST="$(get_digest "${APP_REF}:${{ github.sha }}")"
455+ PREV_APP_DIGEST="$(get_digest "${APP_REF}:${ECR_TAG}" || true)"
456+ if [ -n "$NEW_APP_DIGEST" ] && [ "$NEW_APP_DIGEST" = "$PREV_APP_DIGEST" ]; then
457+ echo "app_image_changed=false" >> "$GITHUB_OUTPUT"
458+ echo "ℹ️ App deploy tag ${ECR_TAG} already points at ${NEW_APP_DIGEST}; no ECS app deploy will be triggered."
459+ else
460+ echo "app_image_changed=true" >> "$GITHUB_OUTPUT"
461+ fi
462+
443463 # Verify every sha image exists before moving any deploy tag, so a
444464 # missing/expired image aborts the whole promotion up front.
445465 for repo in $ECR_REPOS; do
@@ -511,8 +531,17 @@ jobs:
511531 with :
512532 role-to-assume : ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
513533 aws-region : ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
514-
534+ # The poll can run up to ~70 min (prod deploy queued behind a bake), which
535+ # outlasts the default 1h session. Hold the session for the full job so AWS
536+ # calls don't start failing mid-poll. Requires the deploy role's
537+ # MaxSessionDuration to be >= this value (roles are managed outside the repo).
538+ role-duration-seconds : 5400
539+
540+ # Skip the cutover wait when the app image didn't change (no ECS deploy was
541+ # triggered) — otherwise the poll would hang until timeout. Promotion still
542+ # runs below, immediately, since there is no app cutover to align with.
515543 - name : Wait for ECS traffic cutover
544+ if : needs.promote-images.outputs.app_image_changed == 'true'
516545 env :
517546 PIPELINE : sim-${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}-us-east-1-app-deployment
518547 RETAG_EPOCH : ${{ needs.promote-images.outputs.retag_epoch }}
@@ -538,7 +567,7 @@ jobs:
538567 echo "ERROR: no deployed version passed from deploy-trigger" >&2
539568 exit 1
540569 fi
541- echo "Promoting Trigger.dev version $VERSION ($TRIGGER_ENV) at ECS cutover "
570+ echo "Promoting Trigger.dev version $VERSION ($TRIGGER_ENV)"
542571 bunx trigger.dev@4.4.3 promote "$VERSION" --env "$TRIGGER_ENV"
543572
544573 # Build ARM64 images for GHCR (main branch only, runs in parallel with
0 commit comments