1515# VITE_AGENTOS_DEFAULT_MODEL — baked into the SPA bundle at build time
1616#
1717# Triggers:
18- # - push to deploy → build + push (tags : <sha>, deploy )
19- # - tag v* → build + push (tags: <sha>, deploy, <tag >)
18+ # - push to deploy → build + push (tag : <sha>)
19+ # - tag v* → build + push (tags: <sha>, <semver >)
2020# - workflow_dispatch → manual
2121# - pull_request to deploy → build only (no push)
2222#
2323# `deploy` is the long-lived release branch — merge feature branches into it
2424# when you're ready to ship to EKS. Main can move independently.
25+ #
26+ # ── Tag policy ──────────────────────────────────────────────────────────────
27+ # ECR repos are IMMUTABLE, so we only ever push SHA-based tags + semver tags.
28+ # Floating `deploy` / `main` tags would conflict with IMMUTABLE on every
29+ # subsequent push. To roll forward: bump kustomize image tag to the new SHA.
30+ #
31+ # If the SHA already exists in ECR (re-run of the same commit), the workflow
32+ # detects it and skips the build entirely → no-op success, no error.
2533name : build-images
2634
2735on :
@@ -95,17 +103,34 @@ jobs:
95103 run : |
96104 REGISTRY="${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com"
97105 REPO="agentos/${{ matrix.name }}"
106+ # ECR is IMMUTABLE → SHA-only by default, plus semver on tag pushes.
98107 TAGS="${REGISTRY}/${REPO}:${{ steps.tag.outputs.sha }}"
99- if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/deploy" ]]; then
100- TAGS="${TAGS},${REGISTRY}/${REPO}:deploy"
101- fi
102108 if [[ -n "${{ steps.tag.outputs.semver }}" ]]; then
103109 TAGS="${TAGS},${REGISTRY}/${REPO}:${{ steps.tag.outputs.semver }}"
104110 fi
105111 echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
112+ echo "registry=${REGISTRY}" >> "$GITHUB_OUTPUT"
113+ echo "repo=${REPO}" >> "$GITHUB_OUTPUT"
114+
115+ - name : Check if image already exists at this SHA
116+ id : exists
117+ if : github.event_name != 'pull_request'
118+ run : |
119+ # ECR is IMMUTABLE — if the SHA tag exists, ANY push (even the
120+ # identical bytes) is rejected. Make re-runs idempotent by exiting
121+ # the build cleanly when the image is already there.
122+ if aws ecr describe-images \
123+ --repository-name "${{ steps.tags.outputs.repo }}" \
124+ --image-ids "imageTag=${{ steps.tag.outputs.sha }}" \
125+ --region "${{ env.AWS_REGION }}" >/dev/null 2>&1; then
126+ echo "skip=true" >> "$GITHUB_OUTPUT"
127+ echo "::notice title=Skipping build::Image ${{ steps.tags.outputs.repo }}:${{ steps.tag.outputs.sha }} already exists in ECR — no rebuild needed."
128+ else
129+ echo "skip=false" >> "$GITHUB_OUTPUT"
130+ fi
106131
107132 - name : Build and push ${{ matrix.name }} (agentos-server)
108- if : matrix.build_args == ''
133+ if : matrix.build_args == '' && steps.exists.outputs.skip != 'true'
109134 uses : docker/build-push-action@v5
110135 with :
111136 context : .
@@ -116,7 +141,7 @@ jobs:
116141 cache-to : type=gha,mode=max,scope=${{ matrix.name }}
117142
118143 - name : Build and push ${{ matrix.name }} (harness variant)
119- if : matrix.build_args == 'harness'
144+ if : matrix.build_args == 'harness' && steps.exists.outputs.skip != 'true'
120145 uses : docker/build-push-action@v5
121146 with :
122147 context : .
@@ -129,7 +154,7 @@ jobs:
129154 cache-to : type=gha,mode=max,scope=${{ matrix.name }}
130155
131156 - name : Build and push agentos-spa (with VITE_ build args)
132- if : matrix.build_args == 'spa'
157+ if : matrix.build_args == 'spa' && steps.exists.outputs.skip != 'true'
133158 uses : docker/build-push-action@v5
134159 with :
135160 context : .
@@ -146,15 +171,20 @@ jobs:
146171 - name : Summary
147172 if : github.event_name != 'pull_request'
148173 run : |
149- echo "### Pushed ${{ matrix.name }}" >> "$GITHUB_STEP_SUMMARY"
150- echo "" >> "$GITHUB_STEP_SUMMARY"
151- echo "Tags:" >> "$GITHUB_STEP_SUMMARY"
152- for t in $(echo "${{ steps.tags.outputs.tags }}" | tr ',' '\n'); do
153- echo "- \`$t\`" >> "$GITHUB_STEP_SUMMARY"
154- done
174+ if [[ "${{ steps.exists.outputs.skip }}" == "true" ]]; then
175+ echo "### ${{ matrix.name }} — skipped" >> "$GITHUB_STEP_SUMMARY"
176+ echo "" >> "$GITHUB_STEP_SUMMARY"
177+ echo "Image already exists at \`${{ steps.tags.outputs.repo }}:${{ steps.tag.outputs.sha }}\` — no rebuild." >> "$GITHUB_STEP_SUMMARY"
178+ else
179+ echo "### Pushed ${{ matrix.name }}" >> "$GITHUB_STEP_SUMMARY"
180+ echo "" >> "$GITHUB_STEP_SUMMARY"
181+ echo "Tags:" >> "$GITHUB_STEP_SUMMARY"
182+ for t in $(echo "${{ steps.tags.outputs.tags }}" | tr ',' '\n'); do
183+ echo "- \`$t\`" >> "$GITHUB_STEP_SUMMARY"
184+ done
185+ fi
155186 echo "" >> "$GITHUB_STEP_SUMMARY"
156- REGISTRY="${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com"
157187 echo "Bump kustomize:" >> "$GITHUB_STEP_SUMMARY"
158188 echo '```bash' >> "$GITHUB_STEP_SUMMARY"
159- echo "kustomize edit set image agentos/${{ matrix.name }}=${REGISTRY }/agentos/${{ matrix.name }}:${{ steps.tag.outputs.sha }}" >> "$GITHUB_STEP_SUMMARY"
189+ echo "kustomize edit set image agentos/${{ matrix.name }}=${{ steps.tags.outputs.registry } }/agentos/${{ matrix.name }}:${{ steps.tag.outputs.sha }}" >> "$GITHUB_STEP_SUMMARY"
160190 echo '```' >> "$GITHUB_STEP_SUMMARY"
0 commit comments