Skip to content

Commit e3ac689

Browse files
fix(ci): reliable digest reads for no-op detection, robust version parse, pre-push dev epoch
1 parent 99bb55a commit e3ac689

1 file changed

Lines changed: 52 additions & 16 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,18 @@ jobs:
215215
env:
216216
ECR_REPO: ${{ matrix.ecr_repo_secret == 'ECR_APP' && secrets.ECR_APP || matrix.ecr_repo_secret == 'ECR_MIGRATIONS' && secrets.ECR_MIGRATIONS || matrix.ecr_repo_secret == 'ECR_REALTIME' && secrets.ECR_REALTIME || matrix.ecr_repo_secret == 'ECR_PII' && secrets.ECR_PII || '' }}
217217

218-
# App leg only: capture the digest the :dev tag currently points at, BEFORE
219-
# this build overwrites it, so promote-trigger-dev can tell whether the app
220-
# image actually changed (a no-op :dev push triggers no ECS deploy).
221-
- name: Capture previous :dev app digest
218+
# App leg only: stamp the trigger epoch and capture the :dev digest BEFORE the
219+
# build/push overwrites it. Stamping the epoch pre-build (not after the push,
220+
# like it was) guarantees it precedes the :dev push that triggers the pipeline,
221+
# so the dev ECS execution's startTime can't land before the epoch and get
222+
# rejected by the cutover poll. The digest read uses the ECR API so an absent
223+
# tag ("None", first deploy → changed) is distinct from a read error.
224+
- name: Capture pre-build :dev state
222225
id: prevdigest
223226
if: matrix.ecr_repo_secret == 'ECR_APP'
224227
run: |
225-
REF="${{ steps.login-ecr.outputs.registry }}/${{ steps.ecr-repo.outputs.name }}:dev"
226-
PREV=$(docker buildx imagetools inspect "$REF" 2>/dev/null | awk '/^Digest:/{print $2; exit}' || true)
228+
echo "epoch=$(date +%s)" >> "$GITHUB_OUTPUT"
229+
PREV="$(aws ecr batch-get-image --repository-name "${{ steps.ecr-repo.outputs.name }}" --image-ids imageTag=dev --query 'images[0].imageId.imageDigest' --output text 2>/dev/null)" || PREV="__ERR__"
227230
echo "digest=${PREV}" >> "$GITHUB_OUTPUT"
228231
229232
- name: Build and push
@@ -239,16 +242,22 @@ jobs:
239242
# App leg only: publish the metadata promote-trigger-dev needs to correlate
240243
# this push to its dev ECS deploy and decide whether to wait. Dev has no
241244
# promote-images job, so this stands in for its retag_epoch/app_image_changed
242-
# outputs. The epoch is recorded just after the :dev push (the pipeline trigger).
245+
# outputs. The epoch and prev digest come from the pre-build step above.
243246
- name: Publish dev cutover metadata
244247
if: matrix.ecr_repo_secret == 'ECR_APP'
245248
run: |
246249
mkdir -p dev-meta
247250
NEW="${{ steps.build.outputs.digest }}"
248251
PREV="${{ steps.prevdigest.outputs.digest }}"
252+
if [ -z "$NEW" ]; then
253+
echo "ERROR: build did not report an image digest" >&2
254+
exit 1
255+
fi
249256
echo "$NEW" > dev-meta/digest.txt
250-
date +%s > dev-meta/retag_epoch.txt
251-
if [ -n "$NEW" ] && [ "$NEW" = "$PREV" ]; then
257+
echo "${{ steps.prevdigest.outputs.epoch }}" > dev-meta/retag_epoch.txt
258+
# PREV=__ERR__ (read failed) falls through to changed=true (wait) — the safe
259+
# direction. A real no-op is detected when the read succeeds and matches.
260+
if [ "$PREV" != "__ERR__" ] && [ "$NEW" = "$PREV" ]; then
252261
echo "false" > dev-meta/app_image_changed.txt
253262
echo "ℹ️ :dev already points at ${NEW}; no ECS dev deploy will be triggered."
254263
else
@@ -311,7 +320,16 @@ jobs:
311320
exit 1
312321
fi
313322
bunx trigger.dev@4.4.3 deploy --env preview --branch dev-sim --skip-promotion 2>&1 | tee deploy.log
314-
VERSION=$(sed -E 's/\x1b\[[0-9;]*m//g' deploy.log | grep -oE '20[0-9]{6}\.[0-9]+' | tail -n1 || true)
323+
# Anchor on the "version" keyword and take the FIRST match: with
324+
# --skip-promotion the CLI can print the unchanged current version AFTER
325+
# the one it just deployed, and dashboard URLs carry other IDs — so a bare
326+
# last-match could promote the wrong version. Fall back to a bare
327+
# first-match only if no version-labelled line is present.
328+
CLEAN=$(sed -E 's/\x1b\[[0-9;]*m//g' deploy.log)
329+
VERSION=$(printf '%s\n' "$CLEAN" | grep -oiE 'version[[:space:]]+v?20[0-9]{6}\.[0-9]+' | grep -oE '20[0-9]{6}\.[0-9]+' | head -n1 || true)
330+
if [ -z "$VERSION" ]; then
331+
VERSION=$(printf '%s\n' "$CLEAN" | grep -oE '20[0-9]{6}\.[0-9]+' | head -n1 || true)
332+
fi
315333
if [ -z "$VERSION" ]; then
316334
echo "ERROR: could not parse deployed version from deploy output" >&2
317335
exit 1
@@ -484,7 +502,16 @@ jobs:
484502
fi
485503
bunx trigger.dev@4.4.3 deploy --env "$TRIGGER_ENV" --skip-promotion 2>&1 | tee deploy.log
486504
# Extract the deployed version (e.g. 20260715.2) tied to THIS invocation.
487-
VERSION=$(sed -E 's/\x1b\[[0-9;]*m//g' deploy.log | grep -oE '20[0-9]{6}\.[0-9]+' | tail -n1 || true)
505+
# Anchor on the "version" keyword and take the FIRST match: with
506+
# --skip-promotion the CLI can print the unchanged current version AFTER
507+
# the one it just deployed, and dashboard URLs carry other IDs — so a bare
508+
# last-match could promote the wrong version. Fall back to a bare
509+
# first-match only if no version-labelled line is present.
510+
CLEAN=$(sed -E 's/\x1b\[[0-9;]*m//g' deploy.log)
511+
VERSION=$(printf '%s\n' "$CLEAN" | grep -oiE 'version[[:space:]]+v?20[0-9]{6}\.[0-9]+' | grep -oE '20[0-9]{6}\.[0-9]+' | head -n1 || true)
512+
if [ -z "$VERSION" ]; then
513+
VERSION=$(printf '%s\n' "$CLEAN" | grep -oE '20[0-9]{6}\.[0-9]+' | head -n1 || true)
514+
fi
488515
if [ -z "$VERSION" ]; then
489516
echo "ERROR: could not parse deployed version from deploy output" >&2
490517
exit 1
@@ -728,11 +755,20 @@ jobs:
728755
# retag is a no-op, ECR fires no push event, and no ECS app deploy runs.
729756
# promote-trigger reads this to promote immediately instead of waiting for
730757
# a cutover that will never happen.
731-
get_digest() { docker buildx imagetools inspect "$1" 2>/dev/null | awk '/^Digest:/{print $2; exit}'; }
732-
APP_REF="${REGISTRY}/${{ secrets.ECR_APP }}"
733-
NEW_APP_DIGEST="$(get_digest "${APP_REF}:${{ github.sha }}")"
734-
PREV_APP_DIGEST="$(get_digest "${APP_REF}:${ECR_TAG}" || true)"
735-
if [ -n "$NEW_APP_DIGEST" ] && [ "$NEW_APP_DIGEST" = "$PREV_APP_DIGEST" ]; then
758+
#
759+
# Read digests via the ECR API, which cleanly returns "None" for an absent
760+
# tag (first deploy → changed) vs a non-zero exit on a real read error. On
761+
# a read error we fall through to changed=true (wait) — the safe direction
762+
# (old tasks stay current, job fails visibly) rather than promoting early.
763+
APP_REPO="${{ secrets.ECR_APP }}"
764+
ecr_digest() { aws ecr batch-get-image --repository-name "$1" --image-ids imageTag="$2" --query 'images[0].imageId.imageDigest' --output text 2>/dev/null; }
765+
NEW_APP_DIGEST="$(ecr_digest "$APP_REPO" "${{ github.sha }}")" || NEW_APP_DIGEST="__ERR__"
766+
PREV_APP_DIGEST="$(ecr_digest "$APP_REPO" "${ECR_TAG}")" || PREV_APP_DIGEST="__ERR__"
767+
if [ "$NEW_APP_DIGEST" = "__ERR__" ] || [ "$NEW_APP_DIGEST" = "None" ] || [ -z "$NEW_APP_DIGEST" ]; then
768+
echo "ERROR: could not resolve the new app image digest for ${{ github.sha }}" >&2
769+
exit 1
770+
fi
771+
if [ "$PREV_APP_DIGEST" != "__ERR__" ] && [ "$NEW_APP_DIGEST" = "$PREV_APP_DIGEST" ]; then
736772
echo "app_image_changed=false" >> "$GITHUB_OUTPUT"
737773
echo "ℹ️ App deploy tag ${ECR_TAG} already points at ${NEW_APP_DIGEST}; no ECS app deploy will be triggered."
738774
else

0 commit comments

Comments
 (0)