@@ -507,6 +507,9 @@ jobs:
507507 # promote-trigger passes it to the poll script so a stale pipeline execution
508508 # reusing the same image digest can't satisfy the cutover gate.
509509 retag_epoch : ${{ steps.promote.outputs.retag_epoch }}
510+ # 'false' when the app deploy tag didn't move to a new digest (no ECS deploy).
511+ # promote-trigger promotes immediately in that case instead of waiting.
512+ app_image_changed : ${{ steps.promote.outputs.app_image_changed }}
510513 steps :
511514 - name : Configure AWS credentials
512515 uses : aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
@@ -558,6 +561,23 @@ jobs:
558561 ECR_TAG="staging"
559562 fi
560563
564+ # Detect whether the APP deploy tag actually moves to a new digest. If
565+ # this commit's app image is byte-identical to the currently-deployed one
566+ # (e.g. a commit that doesn't touch the app image — docs/CI-only), the
567+ # retag is a no-op, ECR fires no push event, and no ECS app deploy runs.
568+ # promote-trigger reads this to promote immediately instead of waiting for
569+ # a cutover that will never happen.
570+ get_digest() { docker buildx imagetools inspect "$1" 2>/dev/null | awk '/^Digest:/{print $2; exit}'; }
571+ APP_REF="${REGISTRY}/${{ secrets.ECR_APP }}"
572+ NEW_APP_DIGEST="$(get_digest "${APP_REF}:${{ github.sha }}")"
573+ PREV_APP_DIGEST="$(get_digest "${APP_REF}:${ECR_TAG}" || true)"
574+ if [ -n "$NEW_APP_DIGEST" ] && [ "$NEW_APP_DIGEST" = "$PREV_APP_DIGEST" ]; then
575+ echo "app_image_changed=false" >> "$GITHUB_OUTPUT"
576+ echo "ℹ️ App deploy tag ${ECR_TAG} already points at ${NEW_APP_DIGEST}; no ECS app deploy will be triggered."
577+ else
578+ echo "app_image_changed=true" >> "$GITHUB_OUTPUT"
579+ fi
580+
561581 # Verify every sha image exists before moving any deploy tag, so a
562582 # missing/expired image aborts the whole promotion up front.
563583 for repo in $ECR_REPOS; do
@@ -629,8 +649,17 @@ jobs:
629649 with :
630650 role-to-assume : ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
631651 aws-region : ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
632-
652+ # The poll can run up to ~70 min (prod deploy queued behind a bake), which
653+ # outlasts the default 1h session. Hold the session for the full job so AWS
654+ # calls don't start failing mid-poll. Requires the deploy role's
655+ # MaxSessionDuration to be >= this value (roles are managed outside the repo).
656+ role-duration-seconds : 5400
657+
658+ # Skip the cutover wait when the app image didn't change (no ECS deploy was
659+ # triggered) — otherwise the poll would hang until timeout. Promotion still
660+ # runs below, immediately, since there is no app cutover to align with.
633661 - name : Wait for ECS traffic cutover
662+ if : needs.promote-images.outputs.app_image_changed == 'true'
634663 env :
635664 PIPELINE : sim-${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}-us-east-1-app-deployment
636665 RETAG_EPOCH : ${{ needs.promote-images.outputs.retag_epoch }}
@@ -656,7 +685,7 @@ jobs:
656685 echo "ERROR: no deployed version passed from deploy-trigger" >&2
657686 exit 1
658687 fi
659- echo "Promoting Trigger.dev version $VERSION ($TRIGGER_ENV) at ECS cutover "
688+ echo "Promoting Trigger.dev version $VERSION ($TRIGGER_ENV)"
660689 bunx trigger.dev@4.4.3 promote "$VERSION" --env "$TRIGGER_ENV"
661690
662691 # Build ARM64 images for GHCR (main branch only, runs in parallel with
0 commit comments