feat(ci): promote Trigger.dev tasks in lockstep with the ECS traffic cutover #22455
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main, staging, dev] | |
| pull_request: | |
| branches: [main, staging, dev] | |
| # Docs content and markdown don't affect the app build or images; push | |
| # runs stay unfiltered because they feed the deploy pipeline. | |
| paths-ignore: | |
| - 'apps/docs/content/**' | |
| - '**/*.md' | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| permissions: | |
| contents: read | |
| jobs: | |
| test-build: | |
| name: Test and Build | |
| if: github.ref != 'refs/heads/dev' || github.event_name == 'pull_request' | |
| uses: ./.github/workflows/test-build.yml | |
| secrets: inherit | |
| # Detect if this is a version release commit (e.g., "v0.5.24: ...") | |
| detect-version: | |
| name: Detect Version | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/dev') | |
| outputs: | |
| version: ${{ steps.extract.outputs.version }} | |
| is_release: ${{ steps.extract.outputs.is_release }} | |
| steps: | |
| - name: Extract version from commit message | |
| id: extract | |
| env: | |
| COMMIT_MSG: ${{ github.event.head_commit.message }} | |
| run: | | |
| # Only tag versions on main branch | |
| if [ "$GITHUB_REF" = "refs/heads/main" ] && [[ "$COMMIT_MSG" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+): ]]; then | |
| VERSION="${BASH_REMATCH[1]}" | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| echo "is_release=true" >> $GITHUB_OUTPUT | |
| echo "✅ Detected release commit: ${VERSION}" | |
| else | |
| echo "version=" >> $GITHUB_OUTPUT | |
| echo "is_release=false" >> $GITHUB_OUTPUT | |
| echo "ℹ️ Not a release commit" | |
| fi | |
| # 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] | |
| if: >- | |
| github.event_name == 'push' && | |
| (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') | |
| uses: ./.github/workflows/migrations.yml | |
| with: | |
| environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} | |
| secrets: inherit | |
| # Same ordering for dev (schema push before the dev image lands in ECR) | |
| migrate-dev: | |
| name: Migrate Dev DB | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/dev' | |
| uses: ./.github/workflows/migrations.yml | |
| with: | |
| environment: dev | |
| secrets: inherit | |
| # Dev: build all 3 images for ECR only (no GHCR, no ARM64) | |
| build-dev: | |
| name: Build Dev ECR | |
| needs: [detect-version, migrate-dev] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/dev' | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - dockerfile: ./docker/app.Dockerfile | |
| ecr_repo_secret: ECR_APP | |
| - dockerfile: ./docker/db.Dockerfile | |
| ecr_repo_secret: ECR_MIGRATIONS | |
| - dockerfile: ./docker/realtime.Dockerfile | |
| ecr_repo_secret: ECR_REALTIME | |
| - dockerfile: ./docker/pii.Dockerfile | |
| ecr_repo_secret: ECR_PII | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 | |
| with: | |
| role-to-assume: ${{ secrets.DEV_AWS_ROLE_TO_ASSUME }} | |
| aws-region: ${{ secrets.DEV_AWS_REGION }} | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 | |
| - name: Resolve ECR repo name | |
| id: ecr-repo | |
| run: echo "name=$ECR_REPO" >> $GITHUB_OUTPUT | |
| 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 || '' }} | |
| - name: Build and push | |
| uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.ecr-repo.outputs.name }}:dev | |
| provenance: false | |
| sbom: false | |
| # Dev: deploy Trigger.dev background tasks to the preview "dev-sim" branch. | |
| # Gated after migrate-dev for the same reason as build-dev — the new task | |
| # code runs against the dev DB, so the schema must be pushed first. | |
| deploy-trigger-dev: | |
| name: Deploy Trigger.dev (Dev) | |
| needs: [migrate-dev] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/dev' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 15 | |
| 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: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| **/node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Deploy to Trigger.dev | |
| working-directory: ./apps/sim | |
| env: | |
| TRIGGER_ACCESS_TOKEN: ${{ secrets.DEV_TRIGGER_ACCESS_TOKEN }} | |
| TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }} | |
| run: | | |
| if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then | |
| echo "ERROR: DEV_TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2 | |
| exit 1 | |
| fi | |
| bunx trigger.dev@4.4.3 deploy --env preview --branch dev-sim | |
| # Staging: build & upload the Trigger.dev task version WITHOUT promoting it | |
| # (--skip-promotion). New runs keep executing the old version until the promote | |
| # job flips it at the ECS traffic cutover, so this deploy carries zero | |
| # app<->task skew and runs in parallel with the image build. The captured | |
| # version output is consumed by promote-trigger-staging. | |
| deploy-trigger-staging: | |
| name: Deploy Trigger.dev (Staging) | |
| needs: [migrate] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/staging' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 15 | |
| outputs: | |
| version: ${{ steps.deploy.outputs.version }} | |
| 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: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| **/node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Deploy to Trigger.dev (skip promotion) | |
| id: deploy | |
| working-directory: ./apps/sim | |
| env: | |
| TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} | |
| TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }} | |
| run: | | |
| set -eo pipefail | |
| if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then | |
| echo "ERROR: TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2 | |
| exit 1 | |
| fi | |
| bunx trigger.dev@4.4.3 deploy --env staging --skip-promotion 2>&1 | tee deploy.log | |
| # Extract the deployed version (e.g. 20260715.2) tied to THIS invocation. | |
| VERSION=$(sed -E 's/\x1b\[[0-9;]*m//g' deploy.log | grep -oE '20[0-9]{6}\.[0-9]+' | tail -n1 || true) | |
| if [ -z "$VERSION" ]; then | |
| echo "ERROR: could not parse deployed version from deploy output" >&2 | |
| exit 1 | |
| fi | |
| echo "Captured deployed version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| # Production: same skip-promotion deploy as staging, for the prod environment. | |
| deploy-trigger-production: | |
| name: Deploy Trigger.dev (Production) | |
| needs: [migrate] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 15 | |
| outputs: | |
| version: ${{ steps.deploy.outputs.version }} | |
| 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: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| **/node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Deploy to Trigger.dev (skip promotion) | |
| id: deploy | |
| working-directory: ./apps/sim | |
| env: | |
| TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} | |
| TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }} | |
| run: | | |
| set -eo pipefail | |
| if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then | |
| echo "ERROR: TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2 | |
| exit 1 | |
| fi | |
| bunx trigger.dev@4.4.3 deploy --env prod --skip-promotion 2>&1 | tee deploy.log | |
| # Extract the deployed version (e.g. 20260715.2) tied to THIS invocation. | |
| VERSION=$(sed -E 's/\x1b\[[0-9;]*m//g' deploy.log | grep -oE '20[0-9]{6}\.[0-9]+' | tail -n1 || true) | |
| if [ -z "$VERSION" ]; then | |
| echo "ERROR: could not parse deployed version from deploy output" >&2 | |
| exit 1 | |
| fi | |
| echo "Captured deployed version: $VERSION" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| # 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 | |
| if: >- | |
| github.event_name == 'push' && | |
| (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') | |
| runs-on: blacksmith-8vcpu-ubuntu-2404 | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - dockerfile: ./docker/app.Dockerfile | |
| ghcr_image: ghcr.io/simstudioai/simstudio | |
| ecr_repo_secret: ECR_APP | |
| - dockerfile: ./docker/db.Dockerfile | |
| ghcr_image: ghcr.io/simstudioai/migrations | |
| ecr_repo_secret: ECR_MIGRATIONS | |
| - dockerfile: ./docker/realtime.Dockerfile | |
| ghcr_image: ghcr.io/simstudioai/realtime | |
| ecr_repo_secret: ECR_REALTIME | |
| - dockerfile: ./docker/pii.Dockerfile | |
| ghcr_image: ghcr.io/simstudioai/pii | |
| ecr_repo_secret: ECR_PII | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - 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 | |
| - name: Login to Docker Hub | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Login to GHCR | |
| if: github.ref == 'refs/heads/main' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 | |
| - name: Resolve ECR repo name | |
| id: ecr-repo | |
| run: echo "name=$ECR_REPO" >> $GITHUB_OUTPUT | |
| 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: | | |
| ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}" | |
| ECR_REPO="${{ steps.ecr-repo.outputs.name }}" | |
| GHCR_IMAGE="${{ matrix.ghcr_image }}" | |
| TAGS="${ECR_REGISTRY}/${ECR_REPO}:${{ github.sha }}" | |
| if [ "${{ github.ref }}" = "refs/heads/main" ] && [ -n "$GHCR_IMAGE" ]; then | |
| TAGS="${TAGS},${GHCR_IMAGE}:${{ github.sha }}-amd64" | |
| fi | |
| echo "tags=${TAGS}" >> $GITHUB_OUTPUT | |
| - name: Build and push images | |
| id: build | |
| uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| platforms: linux/amd64 | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| provenance: false | |
| sbom: false | |
| # Publish the app image digest so promote-trigger-* can correlate this push | |
| # to its ECS CodePipeline execution. promote-images retags this same sha | |
| # image to latest/staging (preserving the digest), so the pipeline's ECR | |
| # source revision equals this digest — the only durable key (the deploy tag | |
| # is floating). App leg only. | |
| - name: Publish app image digest | |
| if: matrix.ecr_repo_secret == 'ECR_APP' | |
| run: | | |
| mkdir -p digest | |
| echo "${{ steps.build.outputs.digest }}" > digest/app-image-digest.txt | |
| - name: Upload app image digest | |
| if: matrix.ecr_repo_secret == 'ECR_APP' | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: app-image-digest | |
| path: digest/app-image-digest.txt | |
| retention-days: 1 | |
| # 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 | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| outputs: | |
| # Whether the deploy tag was actually moved (false on a stale-run guard | |
| # skip). promote-trigger-* keys off this so tasks are never promoted when | |
| # the app itself wasn't. | |
| promoted: ${{ steps.guard.outputs.fresh }} | |
| 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 | |
| # Deploy-tag moves must be monotonic: a re-run of an old run must never | |
| # retag latest/staging back to stale code. A superseded first-attempt | |
| # run still promotes — the ci-<ref> concurrency group executes runs | |
| # serially in commit order, so an ancestor of head is a forward deploy. | |
| - name: Guard against stale promotion | |
| id: guard | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| STATUS="$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...${GITHUB_REF_NAME}" --jq '.status' || echo "unknown")" | |
| if [ "$STATUS" = "identical" ] || { [ "$STATUS" = "ahead" ] && [ "${{ github.run_attempt }}" = "1" ]; }; then | |
| echo "fresh=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "::warning::Skipping promotion of ${{ github.sha }} (branch compare: ${STATUS}, attempt ${{ github.run_attempt }}). Moving the deploy tags here could deploy stale code; push a revert commit to roll back instead." | |
| echo "fresh=false" >> $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 | |
| # Staging: promote the skip-promoted Trigger.dev version at the exact moment the | |
| # ECS app deploy shifts traffic (CodeDeploy AllowTraffic), so tasks and app cut | |
| # over in lockstep. The promote-images retag above is what triggers the ECS | |
| # pipeline; this job polls it (via the app image digest) and promotes at cutover. | |
| # Skipped when promote-images skipped the tag move (stale run) — tasks then | |
| # correctly stay on the old version. If the app deploy fails or never cuts over, | |
| # promote never fires and this job fails visibly. | |
| promote-trigger-staging: | |
| name: Promote Trigger.dev (Staging) | |
| needs: [promote-images, deploy-trigger-staging] | |
| if: >- | |
| github.event_name == 'push' && github.ref == 'refs/heads/staging' && | |
| needs.promote-images.outputs.promoted == 'true' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 75 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| 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: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| **/node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Download app image digest | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: app-image-digest | |
| path: digest | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 | |
| with: | |
| role-to-assume: ${{ secrets.STAGING_AWS_ROLE_TO_ASSUME }} | |
| aws-region: ${{ secrets.STAGING_AWS_REGION }} | |
| - name: Wait for ECS traffic cutover | |
| run: | | |
| set -eo pipefail | |
| DIGEST=$(cat digest/app-image-digest.txt) | |
| bash .github/scripts/wait-for-ecs-cutover.sh sim-staging-us-east-1-app-deployment "$DIGEST" | |
| - name: Promote Trigger.dev version | |
| working-directory: ./apps/sim | |
| env: | |
| TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} | |
| TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }} | |
| VERSION: ${{ needs.deploy-trigger-staging.outputs.version }} | |
| run: | | |
| set -eo pipefail | |
| if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then | |
| echo "ERROR: TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "$VERSION" ]; then | |
| echo "ERROR: no deployed version passed from deploy-trigger-staging" >&2 | |
| exit 1 | |
| fi | |
| echo "Promoting Trigger.dev version $VERSION (staging) at ECS cutover" | |
| bunx trigger.dev@4.4.3 promote "$VERSION" --env staging | |
| # Production: same lockstep promotion as staging, for the prod environment. | |
| promote-trigger-production: | |
| name: Promote Trigger.dev (Production) | |
| needs: [promote-images, deploy-trigger-production] | |
| if: >- | |
| github.event_name == 'push' && github.ref == 'refs/heads/main' && | |
| needs.promote-images.outputs.promoted == 'true' | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 75 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| 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: Cache Bun dependencies | |
| uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 | |
| with: | |
| path: | | |
| ~/.bun/install/cache | |
| node_modules | |
| **/node_modules | |
| key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-bun- | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Download app image digest | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: app-image-digest | |
| path: digest | |
| - name: Configure AWS credentials | |
| uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Wait for ECS traffic cutover | |
| run: | | |
| set -eo pipefail | |
| DIGEST=$(cat digest/app-image-digest.txt) | |
| bash .github/scripts/wait-for-ecs-cutover.sh sim-production-us-east-1-app-deployment "$DIGEST" | |
| - name: Promote Trigger.dev version | |
| working-directory: ./apps/sim | |
| env: | |
| TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }} | |
| TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }} | |
| VERSION: ${{ needs.deploy-trigger-production.outputs.version }} | |
| run: | | |
| set -eo pipefail | |
| if [ -z "$TRIGGER_ACCESS_TOKEN" ] || [ -z "$TRIGGER_PROJECT_ID" ]; then | |
| echo "ERROR: TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "$VERSION" ]; then | |
| echo "ERROR: no deployed version passed from deploy-trigger-production" >&2 | |
| exit 1 | |
| fi | |
| echo "Promoting Trigger.dev version $VERSION (production) at ECS cutover" | |
| bunx trigger.dev@4.4.3 promote "$VERSION" --env prod | |
| # 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) | |
| runs-on: blacksmith-8vcpu-ubuntu-2404-arm | |
| timeout-minutes: 30 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - dockerfile: ./docker/app.Dockerfile | |
| image: ghcr.io/simstudioai/simstudio | |
| - dockerfile: ./docker/db.Dockerfile | |
| image: ghcr.io/simstudioai/migrations | |
| - dockerfile: ./docker/realtime.Dockerfile | |
| image: ghcr.io/simstudioai/realtime | |
| - dockerfile: ./docker/pii.Dockerfile | |
| image: ghcr.io/simstudioai/pii | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| - name: Login to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| uses: useblacksmith/setup-docker-builder@ab5c1da94f53f5cd75c1038092aa276dddfccbba # v1 | |
| - name: Build and push ARM64 to GHCR | |
| uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| platforms: linux/arm64 | |
| push: true | |
| tags: ${{ matrix.image }}:${{ github.sha }}-arm64 | |
| provenance: false | |
| sbom: false | |
| # 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 | |
| timeout-minutes: 10 | |
| needs: [promote-images, build-ghcr-arm64, detect-version] | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| include: | |
| - image: ghcr.io/simstudioai/simstudio | |
| - image: ghcr.io/simstudioai/migrations | |
| - image: ghcr.io/simstudioai/realtime | |
| - image: ghcr.io/simstudioai/pii | |
| steps: | |
| - name: Login to GHCR | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.repository_owner }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| # Same monotonic guard as promote-images, applied to the public latest | |
| # tags only — immutable sha and version tags are always published. | |
| - name: Guard against stale latest tags | |
| id: guard | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| STATUS="$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...${GITHUB_REF_NAME}" --jq '.status' || echo "unknown")" | |
| if [ "$STATUS" = "identical" ] || { [ "$STATUS" = "ahead" ] && [ "${{ github.run_attempt }}" = "1" ]; }; then | |
| echo "fresh=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "::warning::Publishing immutable tags for ${{ github.sha }} but skipping the latest tags (branch compare: ${STATUS}, attempt ${{ github.run_attempt }})." | |
| echo "fresh=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Publish tags and manifests | |
| run: | | |
| IMAGE="${{ matrix.image }}" | |
| SHA="${{ 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" | |
| if [ "${{ needs.detect-version.outputs.is_release }}" = "true" ]; then | |
| VERSION="${{ needs.detect-version.outputs.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 | |
| check-docs-changes: | |
| name: Check Docs Changes | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 5 | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| outputs: | |
| docs_changed: ${{ steps.filter.outputs.docs }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 2 # Need at least 2 commits to detect changes | |
| - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 | |
| id: filter | |
| with: | |
| filters: | | |
| docs: | |
| - 'apps/docs/content/docs/en/**' | |
| - 'apps/sim/scripts/process-docs.ts' | |
| - 'apps/sim/lib/chunkers/**' | |
| # Process docs embeddings (only when docs change, after images are promoted) | |
| process-docs: | |
| name: Process Docs | |
| needs: [promote-images, check-docs-changes] | |
| if: needs.check-docs-changes.outputs.docs_changed == 'true' | |
| uses: ./.github/workflows/docs-embeddings.yml | |
| secrets: inherit | |
| # Create GitHub Release (only for version commits on main, after all builds complete) | |
| create-release: | |
| name: Create GitHub Release | |
| runs-on: blacksmith-4vcpu-ubuntu-2404 | |
| timeout-minutes: 10 | |
| needs: [create-ghcr-manifests, detect-version] | |
| if: needs.detect-version.outputs.is_release == 'true' | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 | |
| with: | |
| bun-version: 1.3.13 | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Create release | |
| env: | |
| GH_PAT: ${{ secrets.GITHUB_TOKEN }} | |
| run: bun run scripts/create-single-release.ts ${{ needs.detect-version.outputs.version }} |