mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(ci): decouple image builds from the test gate (#5701)
* improvement(ci): decouple image builds from the test gate - build-amd64 now starts immediately and pushes only sha tags (ECR :sha, GHCR :sha-amd64). The EventBridge deploy triggers filter on exactly the latest/staging/dev ECR tags, so nothing deploys from these pushes. - New promote-images job retags sha -> latest/staging (and GHCR latest-amd64/version-amd64) via buildx imagetools once tests and migrations pass — a seconds-long manifest copy instead of rebuilding after the gate. Cuts push-to-deploy from ~13.5 to ~7 minutes. - Split the Next.js production build out of test-build into a parallel Build App job with its own sticky-disk keys, cutting PR feedback from ~5.5 to ~3.5 minutes. - create-ghcr-manifests and process-docs now gate on promote-images. * improvement(ci): harden promotion — atomic retag, stale-run guards, gate all mutable GHCR tags Review follow-ups: - promote-images is a single job (not a matrix): verifies all four :sha manifests exist before moving any deploy tag, so a missing image can't cause a partial mixed-version deploy - stale-run guard on promote-images and create-ghcr-manifests: re-running an old run no longer retags latest/staging back to stale code (a one-click prod rollback); superseded runs skip mutable tags with a warning while immutable sha/version tags still publish - ARM64 build now pushes only the immutable :sha-arm64 tag; latest-arm64 and version tags moved behind the gate into create-ghcr-manifests (closes the pre-existing hole where a failing run moved latest-arm64) - dropped dead detect-version needs from both build jobs - sticky-disk comment corrected (clone + last-writer-wins, not exclusive mounts); build job shares warm bun/node_modules disks, keeps its own turbo cache key so test/build entries don't evict each other
This commit is contained in:
+129
-63
@@ -46,9 +46,10 @@ jobs:
|
||||
echo "ℹ️ Not a release commit"
|
||||
fi
|
||||
|
||||
# Run database migrations before images are pushed: the ECR push triggers
|
||||
# CodePipeline, so migrating first guarantees the schema is in place before
|
||||
# the new app version deploys (replaces the removed ECS migration sidecar)
|
||||
# Run database migrations before images are promoted: the ECR latest/staging
|
||||
# tag push triggers CodePipeline, so migrating first guarantees the schema is
|
||||
# in place before the new app version deploys (replaces the removed ECS
|
||||
# migration sidecar)
|
||||
migrate:
|
||||
name: Migrate DB
|
||||
needs: [test-build]
|
||||
@@ -173,10 +174,13 @@ jobs:
|
||||
fi
|
||||
bunx trigger.dev@4.4.3 deploy --env preview --branch dev-sim
|
||||
|
||||
# Main/staging: build AMD64 images and push to ECR + GHCR
|
||||
# Main/staging: build AMD64 images and push sha-tagged images to ECR + GHCR.
|
||||
# Runs in parallel with tests — only immutable sha tags are pushed here, and
|
||||
# the CodePipeline EventBridge triggers filter on exactly the
|
||||
# latest/staging/dev ECR tags, so nothing deploys and no mutable tag moves
|
||||
# until promote-images / create-ghcr-manifests retag after the gate.
|
||||
build-amd64:
|
||||
name: Build AMD64
|
||||
needs: [test-build, detect-version, migrate]
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
@@ -238,6 +242,9 @@ jobs:
|
||||
env:
|
||||
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 || '' }}
|
||||
|
||||
# Only sha tags here — the ECR deploy tags (latest/staging) are applied
|
||||
# by promote-images and the GHCR latest-amd64/version tags by
|
||||
# create-ghcr-manifests, both after tests and migrations pass.
|
||||
- name: Generate tags
|
||||
id: meta
|
||||
run: |
|
||||
@@ -245,26 +252,10 @@ jobs:
|
||||
ECR_REPO="${{ steps.ecr-repo.outputs.name }}"
|
||||
GHCR_IMAGE="${{ matrix.ghcr_image }}"
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
ECR_TAG="latest"
|
||||
else
|
||||
ECR_TAG="staging"
|
||||
fi
|
||||
ECR_IMAGE="${ECR_REGISTRY}/${ECR_REPO}:${ECR_TAG}"
|
||||
|
||||
TAGS="${ECR_IMAGE}"
|
||||
TAGS="${ECR_REGISTRY}/${ECR_REPO}:${{ github.sha }}"
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ] && [ -n "$GHCR_IMAGE" ]; then
|
||||
GHCR_AMD64="${GHCR_IMAGE}:latest-amd64"
|
||||
GHCR_SHA="${GHCR_IMAGE}:${{ github.sha }}-amd64"
|
||||
TAGS="${TAGS},$GHCR_AMD64,$GHCR_SHA"
|
||||
|
||||
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
|
||||
VERSION="${{ needs.detect-version.outputs.version }}"
|
||||
GHCR_VERSION="${GHCR_IMAGE}:${VERSION}-amd64"
|
||||
TAGS="${TAGS},$GHCR_VERSION"
|
||||
echo "📦 Adding version tag: ${VERSION}-amd64"
|
||||
fi
|
||||
TAGS="${TAGS},${GHCR_IMAGE}:${{ github.sha }}-amd64"
|
||||
fi
|
||||
|
||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||
@@ -280,10 +271,85 @@ jobs:
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
# Build ARM64 images for GHCR (main branch only, runs in parallel)
|
||||
# Promote the sha-tagged ECR images to the deploy tags once tests and
|
||||
# migrations pass. Pushing the ECR latest/staging tag is what triggers
|
||||
# CodePipeline, so this seconds-long manifest retag is the deploy gate —
|
||||
# the image builds themselves run in parallel with the tests. A single job
|
||||
# (not a matrix) so all four sha manifests are verified before any tag
|
||||
# moves; a missing image can't produce a partial mixed-version deploy.
|
||||
promote-images:
|
||||
name: Promote Images
|
||||
needs: [migrate, build-amd64]
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
|
||||
with:
|
||||
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
|
||||
|
||||
- name: Login to Amazon ECR
|
||||
id: login-ecr
|
||||
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2
|
||||
|
||||
# Re-running this job from an old run would retag the deploy tags back
|
||||
# to that run's commit and immediately trigger a production deploy of
|
||||
# stale code. Only promote while this commit is still the branch head;
|
||||
# when superseded, skip cleanly — the newer run's promotion covers it.
|
||||
- name: Guard against stale promotion
|
||||
id: guard
|
||||
run: |
|
||||
HEAD_SHA="$(git ls-remote "https://github.com/${{ github.repository }}.git" "refs/heads/${GITHUB_REF_NAME}" | cut -f1)"
|
||||
if [ "$HEAD_SHA" != "${{ github.sha }}" ]; then
|
||||
echo "::warning::Skipping promotion of ${{ github.sha }}: ${GITHUB_REF_NAME} has moved to ${HEAD_SHA}. Moving the deploy tags to this commit would deploy stale code; push a revert commit to roll back instead."
|
||||
echo "fresh=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "fresh=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Promote images to deploy tags
|
||||
if: steps.guard.outputs.fresh == 'true'
|
||||
env:
|
||||
ECR_REPOS: >-
|
||||
${{ secrets.ECR_APP }}
|
||||
${{ secrets.ECR_MIGRATIONS }}
|
||||
${{ secrets.ECR_REALTIME }}
|
||||
${{ secrets.ECR_PII }}
|
||||
run: |
|
||||
REGISTRY="${{ steps.login-ecr.outputs.registry }}"
|
||||
|
||||
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
|
||||
ECR_TAG="latest"
|
||||
else
|
||||
ECR_TAG="staging"
|
||||
fi
|
||||
|
||||
# Verify every sha image exists before moving any deploy tag, so a
|
||||
# missing/expired image aborts the whole promotion up front.
|
||||
for repo in $ECR_REPOS; do
|
||||
echo "🔍 Verifying ${repo}:${{ github.sha }}"
|
||||
docker buildx imagetools inspect "${REGISTRY}/${repo}:${{ github.sha }}" > /dev/null
|
||||
done
|
||||
|
||||
for repo in $ECR_REPOS; do
|
||||
echo "🚀 Promoting ${repo}:${{ github.sha }} to ${ECR_TAG}"
|
||||
docker buildx imagetools create \
|
||||
-t "${REGISTRY}/${repo}:${ECR_TAG}" \
|
||||
"${REGISTRY}/${repo}:${{ github.sha }}"
|
||||
done
|
||||
|
||||
# Build ARM64 images for GHCR (main branch only, runs in parallel with
|
||||
# tests). Pushes only the immutable sha tag — latest-arm64/version-arm64
|
||||
# are applied by create-ghcr-manifests after the gate, so a failing run
|
||||
# never moves a documented tag.
|
||||
build-ghcr-arm64:
|
||||
name: Build ARM64 (GHCR Only)
|
||||
needs: [detect-version]
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404-arm
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
@@ -316,21 +382,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1
|
||||
|
||||
- name: Generate ARM64 tags
|
||||
id: meta
|
||||
run: |
|
||||
IMAGE="${{ matrix.image }}"
|
||||
TAGS="${IMAGE}:latest-arm64,${IMAGE}:${{ github.sha }}-arm64"
|
||||
|
||||
# Add version tag if this is a release commit
|
||||
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
|
||||
VERSION="${{ needs.detect-version.outputs.version }}"
|
||||
TAGS="${TAGS},${IMAGE}:${VERSION}-arm64"
|
||||
echo "📦 Adding version tag: ${VERSION}-arm64"
|
||||
fi
|
||||
|
||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push ARM64 to GHCR
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
with:
|
||||
@@ -338,15 +389,17 @@ jobs:
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
tags: ${{ matrix.image }}:${{ github.sha }}-arm64
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
# Create GHCR multi-arch manifests (only for main, after both builds)
|
||||
# Publish all mutable GHCR tags (latest, latest-amd64/arm64, version tags)
|
||||
# and the multi-arch manifests from the immutable sha tags — only on main,
|
||||
# after the deploy gate (promote-images) and the ARM64 build both pass.
|
||||
create-ghcr-manifests:
|
||||
name: Create GHCR Manifests
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
needs: [build-amd64, build-ghcr-arm64, detect-version]
|
||||
needs: [promote-images, build-ghcr-arm64, detect-version]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
packages: write
|
||||
@@ -366,30 +419,43 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push manifests
|
||||
# Same protection as promote-images: a re-run of an old run must not
|
||||
# move the public latest tags back to a stale commit. Immutable tags
|
||||
# (sha and version) are still published — only latest moves are gated.
|
||||
- name: Guard against stale latest tags
|
||||
id: guard
|
||||
run: |
|
||||
IMAGE_BASE="${{ matrix.image }}"
|
||||
HEAD_SHA="$(git ls-remote "https://github.com/${{ github.repository }}.git" "refs/heads/${GITHUB_REF_NAME}" | cut -f1)"
|
||||
if [ "$HEAD_SHA" != "${{ github.sha }}" ]; then
|
||||
echo "::warning::${GITHUB_REF_NAME} has moved to ${HEAD_SHA}; publishing immutable tags for ${{ github.sha }} but skipping the latest tags."
|
||||
echo "fresh=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "fresh=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# Create latest manifest
|
||||
docker manifest create "${IMAGE_BASE}:latest" \
|
||||
"${IMAGE_BASE}:latest-amd64" \
|
||||
"${IMAGE_BASE}:latest-arm64"
|
||||
docker manifest push "${IMAGE_BASE}:latest"
|
||||
- name: Publish tags and manifests
|
||||
run: |
|
||||
IMAGE="${{ matrix.image }}"
|
||||
SHA="${{ github.sha }}"
|
||||
|
||||
# Create SHA manifest
|
||||
docker manifest create "${IMAGE_BASE}:${{ github.sha }}" \
|
||||
"${IMAGE_BASE}:${{ github.sha }}-amd64" \
|
||||
"${IMAGE_BASE}:${{ github.sha }}-arm64"
|
||||
docker manifest push "${IMAGE_BASE}:${{ github.sha }}"
|
||||
# Multi-arch manifest from the immutable per-arch sha tags
|
||||
docker buildx imagetools create -t "${IMAGE}:${SHA}" \
|
||||
"${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64"
|
||||
|
||||
# Create version manifest if this is a release commit
|
||||
if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then
|
||||
VERSION="${{ needs.detect-version.outputs.version }}"
|
||||
echo "📦 Creating version manifest: ${VERSION}"
|
||||
docker manifest create "${IMAGE_BASE}:${VERSION}" \
|
||||
"${IMAGE_BASE}:${VERSION}-amd64" \
|
||||
"${IMAGE_BASE}:${VERSION}-arm64"
|
||||
docker manifest push "${IMAGE_BASE}:${VERSION}"
|
||||
echo "📦 Publishing version tags: ${VERSION}"
|
||||
docker buildx imagetools create -t "${IMAGE}:${VERSION}-amd64" "${IMAGE}:${SHA}-amd64"
|
||||
docker buildx imagetools create -t "${IMAGE}:${VERSION}-arm64" "${IMAGE}:${SHA}-arm64"
|
||||
docker buildx imagetools create -t "${IMAGE}:${VERSION}" \
|
||||
"${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64"
|
||||
fi
|
||||
|
||||
if [ "${{ steps.guard.outputs.fresh }}" = "true" ]; then
|
||||
docker buildx imagetools create -t "${IMAGE}:latest-amd64" "${IMAGE}:${SHA}-amd64"
|
||||
docker buildx imagetools create -t "${IMAGE}:latest-arm64" "${IMAGE}:${SHA}-arm64"
|
||||
docker buildx imagetools create -t "${IMAGE}:latest" \
|
||||
"${IMAGE}:${SHA}-amd64" "${IMAGE}:${SHA}-arm64"
|
||||
fi
|
||||
|
||||
# Check if docs changed
|
||||
@@ -412,10 +478,10 @@ jobs:
|
||||
- 'apps/sim/scripts/process-docs.ts'
|
||||
- 'apps/sim/lib/chunkers/**'
|
||||
|
||||
# Process docs embeddings (only when docs change, after ECR images are pushed)
|
||||
# Process docs embeddings (only when docs change, after images are promoted)
|
||||
process-docs:
|
||||
name: Process Docs
|
||||
needs: [build-amd64, check-docs-changes]
|
||||
needs: [promote-images, check-docs-changes]
|
||||
if: needs.check-docs-changes.outputs.docs_changed == 'true'
|
||||
uses: ./.github/workflows/docs-embeddings.yml
|
||||
secrets: inherit
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test-build:
|
||||
name: Test and Build
|
||||
name: Lint and Test
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
@@ -44,14 +44,6 @@ jobs:
|
||||
key: ${{ github.repository }}-turbo-cache
|
||||
path: ./.turbo
|
||||
|
||||
- name: Restore Next.js build cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ./apps/sim/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
@@ -175,6 +167,67 @@ jobs:
|
||||
fi
|
||||
echo "✅ Schema and migrations are in sync"
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5
|
||||
with:
|
||||
directory: ./apps/sim/coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
# Next.js production build, in parallel with lint + tests. Sticky disks are
|
||||
# cloned from the last committed snapshot per job and committed last-writer-
|
||||
# wins, so concurrent mounts are safe. The bun/node_modules disks are shared
|
||||
# with test-build (identical content from the same lockfile — LWW loss is
|
||||
# harmless), but the Turbo cache gets its own key: with a shared key, only
|
||||
# the last committer's new entries survive each run, so the test and build
|
||||
# Turbo entries would evict each other nondeterministically.
|
||||
build:
|
||||
name: Build App
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: 1.3.13
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
with:
|
||||
node-version: latest
|
||||
|
||||
- name: Mount Bun cache (Sticky Disk)
|
||||
uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1
|
||||
with:
|
||||
key: ${{ github.repository }}-bun-cache
|
||||
path: ~/.bun/install/cache
|
||||
|
||||
- name: Mount node_modules (Sticky Disk)
|
||||
uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1
|
||||
with:
|
||||
key: ${{ github.repository }}-node-modules
|
||||
path: ./node_modules
|
||||
|
||||
- name: Mount Turbo cache (Sticky Disk)
|
||||
uses: useblacksmith/stickydisk@4c034ba57b706cf0e3b4b0ce098c2a3b1071580c # v1
|
||||
with:
|
||||
key: ${{ github.repository }}-turbo-cache-build
|
||||
path: ./.turbo
|
||||
|
||||
- name: Restore Next.js build cache
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||
with:
|
||||
path: ./apps/sim/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
NODE_OPTIONS: '--no-warnings --max-old-space-size=8192'
|
||||
@@ -186,11 +239,4 @@ jobs:
|
||||
AWS_REGION: 'us-west-2'
|
||||
ENCRYPTION_KEY: '7cf672e460e430c1fba707575c2b0e2ad5a99dddf9b7b7e3b5646e630861db1c' # dummy key for CI only
|
||||
TURBO_CACHE_DIR: .turbo
|
||||
run: bunx turbo run build --filter=sim
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5
|
||||
with:
|
||||
directory: ./apps/sim/coverage
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
run: bunx turbo run build --filter=sim
|
||||
Reference in New Issue
Block a user