Skip to content

Commit c72b591

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(deploy): align Trigger workers with ECS releases
1 parent a89b158 commit c72b591

8 files changed

Lines changed: 876 additions & 73 deletions

File tree

.github/actions/docker-build/action.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ inputs:
1919
tags:
2020
description: Comma-separated list of tags to push.
2121
required: true
22+
build-args:
23+
description: Newline-separated Docker build arguments.
24+
required: false
2225
max-cache-size-mb:
2326
description: >-
2427
Layer cache to retain after this action prunes, in MB. Must stay above one
@@ -30,8 +33,8 @@ inputs:
3033
bypass an input `default:` entirely.
3134
required: false
3235

33-
# Registry logins must precede this action. provenance/sbom stay off: attestation
34-
# manifests break `imagetools create` retagging in promote-images.
36+
# Registry logins must precede this action. Keep the existing image format;
37+
# GHCR signatures and attestations are published by the separate attestation jobs.
3538
runs:
3639
using: composite
3740
steps:
@@ -72,6 +75,7 @@ runs:
7275
platforms: ${{ inputs.platforms }}
7376
push: true
7477
tags: ${{ inputs.tags }}
78+
build-args: ${{ inputs.build-args }}
7579
provenance: false
7680
sbom: false
7781

@@ -177,5 +181,6 @@ runs:
177181
platforms: ${{ inputs.platforms }}
178182
push: true
179183
tags: ${{ inputs.tags }}
184+
build-args: ${{ inputs.build-args }}
180185
provenance: false
181186
sbom: false

.github/workflows/ci.yml

Lines changed: 131 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ jobs:
141141
environment: dev
142142
secrets: inherit
143143

144-
# Dev: build all 3 images for ECR only (no GHCR, no ARM64)
144+
# Dev publishes SHA artifacts first; only promote-images moves deploy aliases.
145145
build-dev:
146146
name: Build Dev ECR
147147
needs: [detect-version, migrate-dev]
@@ -215,13 +215,25 @@ 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+
- name: Reuse existing SHA artifact
219+
id: artifact
220+
env:
221+
ECR_REPO: ${{ steps.ecr-repo.outputs.name }}
222+
run: |
223+
[[ "$GITHUB_SHA" =~ ^[a-f0-9]{40}$ ]] && test -n "$ECR_REPO"
224+
result="$(aws ecr batch-get-image --repository-name "$ECR_REPO" --image-ids "imageTag=$GITHUB_SHA" --output json)"
225+
jq -e 'all(.failures[]; .failureCode == "ImageNotFound")' <<< "$result" > /dev/null
226+
echo "exists=$(jq -r '.images | length == 1' <<< "$result")" >> "$GITHUB_OUTPUT"
227+
218228
- name: Build and push
229+
if: steps.artifact.outputs.exists != 'true'
219230
uses: ./.github/actions/docker-build
220231
with:
221232
provider: ${{ vars.CI_PROVIDER }}
222233
file: ${{ matrix.dockerfile }}
223234
platforms: linux/amd64
224-
tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.ecr-repo.outputs.name }}:dev
235+
tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.ecr-repo.outputs.name }}:${{ github.sha }}
236+
build-args: COMMIT_SHA=${{ github.sha }}
225237
max-cache-size-mb: ${{ matrix.cache_mb }}
226238

227239
# Dev: deploy Trigger.dev background tasks to the preview "dev-sim" branch.
@@ -232,7 +244,9 @@ jobs:
232244
needs: [migrate-dev]
233245
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
234246
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
235-
timeout-minutes: 15
247+
timeout-minutes: 45
248+
outputs:
249+
deployment_version: ${{ steps.deploy.outputs.deploymentVersion }}
236250
steps:
237251
- name: Checkout code
238252
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
@@ -257,6 +271,7 @@ jobs:
257271
run: bun install --frozen-lockfile --ignore-scripts
258272

259273
- name: Deploy to Trigger.dev
274+
id: deploy
260275
working-directory: ./apps/sim
261276
env:
262277
TRIGGER_ACCESS_TOKEN: ${{ secrets.DEV_TRIGGER_ACCESS_TOKEN }}
@@ -266,7 +281,8 @@ jobs:
266281
echo "ERROR: DEV_TRIGGER_ACCESS_TOKEN and TRIGGER_PROJECT_ID repo secrets must both be set" >&2
267282
exit 1
268283
fi
269-
bunx trigger.dev@4.5.12 deploy --env preview --branch dev-sim
284+
[[ "$GITHUB_SHA" =~ ^[a-f0-9]{40}$ ]]
285+
bunx trigger.dev@4.5.12 deploy --env preview --branch dev-sim --external-id "$GITHUB_SHA"
270286
271287
# Main/staging: build AMD64 images and push sha-tagged images to ECR + GHCR.
272288
# Runs in parallel with tests — only immutable sha tags are pushed here, and
@@ -384,95 +400,137 @@ jobs:
384400
385401
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
386402
403+
- name: Reuse existing SHA artifact
404+
id: artifact
405+
if: matrix.ecr_repo_secret != ''
406+
env:
407+
ECR_REPO: ${{ steps.ecr-repo.outputs.name }}
408+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
409+
GHCR_IMAGE: ${{ matrix.ghcr_image }}
410+
run: |
411+
[[ "$GITHUB_SHA" =~ ^[a-f0-9]{40}$ ]] && test -n "$ECR_REPO"
412+
result="$(aws ecr batch-get-image --repository-name "$ECR_REPO" --image-ids "imageTag=$GITHUB_SHA" --output json)"
413+
jq -e 'all(.failures[]; .failureCode == "ImageNotFound")' <<< "$result" > /dev/null
414+
exists="$(jq -r '.images | length == 1' <<< "$result")"
415+
echo "exists=$exists" >> "$GITHUB_OUTPUT"
416+
if [ "$exists" = true ] && [ "$GITHUB_REF_NAME" = main ]; then
417+
digest="$(jq -r '.images[0].imageId.imageDigest' <<< "$result")"
418+
source="${ECR_REGISTRY}/${ECR_REPO}@${digest}"
419+
docker pull "$source"
420+
docker tag "$source" "${GHCR_IMAGE}:${GITHUB_SHA}-amd64"
421+
docker push "${GHCR_IMAGE}:${GITHUB_SHA}-amd64"
422+
fi
423+
387424
- name: Build and push images
388-
if: steps.meta.outputs.skip != 'true'
425+
if: steps.meta.outputs.skip != 'true' && steps.artifact.outputs.exists != 'true'
389426
uses: ./.github/actions/docker-build
390427
with:
391428
provider: ${{ vars.CI_PROVIDER }}
392429
file: ${{ matrix.dockerfile }}
393430
platforms: linux/amd64
394431
tags: ${{ steps.meta.outputs.tags }}
432+
build-args: COMMIT_SHA=${{ github.sha }}
395433
max-cache-size-mb: ${{ matrix.cache_mb }}
396434

397-
# Promote the sha-tagged ECR images to the deploy tags once tests and
398-
# migrations pass. Pushing the ECR latest/staging tag is what triggers
399-
# CodePipeline, so this seconds-long manifest retag is the deploy gate —
400-
# the image builds themselves run in parallel with the tests. A single job
401-
# (not a matrix) so all four sha manifests are verified before any tag
402-
# moves; a missing image can't produce a partial mixed-version deploy.
435+
# Keep the existing ECR -> CodePipeline path; verify both release halves.
403436
promote-images:
404-
name: Promote Images
405-
needs: [migrate, build-amd64]
406-
# Explicit results: see migrate's comment.
437+
name: Promote and Verify Release
438+
needs: [migrate, build-amd64, migrate-dev, build-dev, deploy-trigger-dev]
407439
if: >-
408-
!cancelled() &&
409-
needs.migrate.result == 'success' &&
410-
needs.build-amd64.result == 'success' &&
411-
github.event_name == 'push' &&
412-
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging')
440+
!cancelled() && github.event_name == 'push' &&
441+
((github.ref == 'refs/heads/dev' &&
442+
needs.migrate-dev.result == 'success' && needs.build-dev.result == 'success' &&
443+
needs.deploy-trigger-dev.result == 'success') ||
444+
((github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') &&
445+
needs.migrate.result == 'success' && needs.build-amd64.result == 'success'))
413446
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
414-
timeout-minutes: 10
447+
timeout-minutes: 130
415448
permissions:
416449
contents: read
450+
checks: read
417451
id-token: write
452+
outputs:
453+
decision: ${{ steps.guard.outputs.decision }}
454+
complete: ${{ steps.verify.outputs.complete }}
455+
env:
456+
GH_TOKEN: ${{ github.token }}
418457
steps:
419-
- name: Configure AWS credentials
420-
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
421-
with:
422-
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
423-
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
458+
- name: Checkout code
459+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
424460

425-
- name: Login to Amazon ECR
426-
id: login-ecr
427-
uses: aws-actions/amazon-ecr-login@d539f0932e70871a027e9d5a9d8fc38589180a64 # v2
461+
- name: Setup Bun
462+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
463+
with:
464+
bun-version: 1.4.1
428465

429-
# Deploy-tag moves must be monotonic: a re-run of an old run must never
430-
# retag latest/staging back to stale code. A superseded first-attempt
431-
# run still promotes — the ci-<ref> concurrency group executes runs
432-
# serially in commit order, so an ancestor of head is a forward deploy.
433-
- name: Guard against stale promotion
434-
id: guard
466+
- name: Wait for matching worker deployment
435467
env:
436-
GH_TOKEN: ${{ github.token }}
437-
run: |
438-
STATUS="$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...${GITHUB_REF_NAME}" --jq '.status' || echo "unknown")"
439-
if [ "$STATUS" = "identical" ] || { [ "$STATUS" = "ahead" ] && [ "${{ github.run_attempt }}" = "1" ]; }; then
440-
echo "fresh=true" >> $GITHUB_OUTPUT
441-
else
442-
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."
443-
echo "fresh=false" >> $GITHUB_OUTPUT
444-
fi
468+
TRIGGER_PROJECT_ID: ${{ secrets.TRIGGER_PROJECT_ID }}
469+
TRIGGER_DEPLOYMENT_VERSION: ${{ needs.deploy-trigger-dev.outputs.deployment_version }}
470+
run: bun scripts/verify-release.ts worker
471+
472+
- name: Configure AWS credentials
473+
uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6
474+
with:
475+
role-to-assume: ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
476+
aws-region: ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || github.ref == 'refs/heads/dev' && secrets.DEV_AWS_REGION || secrets.STAGING_AWS_REGION }}
477+
role-duration-seconds: 5400
445478

446-
- name: Promote images to deploy tags
447-
if: steps.guard.outputs.fresh == 'true'
479+
- name: Preflight all image artifacts
480+
id: images
448481
env:
482+
ECR_APP: ${{ secrets.ECR_APP }}
449483
ECR_REPOS: >-
450484
${{ secrets.ECR_APP }}
451485
${{ secrets.ECR_MIGRATIONS }}
452486
${{ secrets.ECR_REALTIME }}
453487
${{ secrets.ECR_PII }}
488+
run: bun scripts/verify-release.ts images
489+
490+
- name: Guard against stale promotion
491+
id: guard
492+
run: bun scripts/verify-release.ts freshness
493+
494+
- name: Promote changed images to deploy tags
495+
if: steps.guard.outputs.decision == 'promote'
496+
env:
497+
IMAGES: ${{ steps.images.outputs.images }}
454498
run: |
455-
REGISTRY="${{ steps.login-ecr.outputs.registry }}"
499+
tag="$GITHUB_REF_NAME"
500+
[ "$tag" != main ] || tag=latest
501+
while IFS= read -r entry; do
502+
repo="$(jq -r '.repository' <<< "$entry")"
503+
jq -jr '.manifest' <<< "$entry" > "$RUNNER_TEMP/release-manifest.json"
504+
aws ecr put-image --repository-name "$repo" --image-tag "$tag" \
505+
--image-digest "$(jq -r '.sourceDigest' <<< "$entry")" \
506+
--image-manifest "file://$RUNNER_TEMP/release-manifest.json" \
507+
--image-manifest-media-type "$(jq -r '.mediaType' <<< "$entry")" > /dev/null
508+
echo "Promoted $repo:$tag" >> "$GITHUB_STEP_SUMMARY"
509+
done < <(jq -c '.[] | select(.promote)' <<< "$IMAGES")
510+
511+
- name: Verify app deployment
512+
id: verify
513+
if: steps.guard.outputs.decision == 'promote'
514+
env:
515+
APP_DIGEST: ${{ steps.images.outputs.app_digest }}
516+
APP_PLATFORM_DIGEST: ${{ steps.images.outputs.app_platform_digest }}
517+
run: bun scripts/verify-release.ts app
456518

457-
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
458-
ECR_TAG="latest"
519+
- name: Record release outcome
520+
if: always()
521+
env:
522+
DECISION: ${{ steps.guard.outputs.decision }}
523+
COMPLETE: ${{ steps.verify.outputs.complete }}
524+
RESULT: ${{ job.status }}
525+
run: |
526+
if [ "$DECISION" = skip ]; then
527+
outcome=superseded
528+
elif [ "$RESULT" = success ] && [ "$COMPLETE" = true ]; then
529+
outcome=complete
459530
else
460-
ECR_TAG="staging"
531+
outcome=incomplete
461532
fi
462-
463-
# Verify every sha image exists before moving any deploy tag, so a
464-
# missing/expired image aborts the whole promotion up front.
465-
for repo in $ECR_REPOS; do
466-
echo "🔍 Verifying ${repo}:${{ github.sha }}"
467-
docker buildx imagetools inspect "${REGISTRY}/${repo}:${{ github.sha }}" > /dev/null
468-
done
469-
470-
for repo in $ECR_REPOS; do
471-
echo "🚀 Promoting ${repo}:${{ github.sha }} to ${ECR_TAG}"
472-
docker buildx imagetools create \
473-
-t "${REGISTRY}/${repo}:${ECR_TAG}" \
474-
"${REGISTRY}/${repo}:${{ github.sha }}"
475-
done
533+
echo "Release $GITHUB_SHA ($GITHUB_REF_NAME): **$outcome**" >> "$GITHUB_STEP_SUMMARY"
476534
477535
# Build ARM64 images for GHCR (main branch only, runs in parallel with
478536
# tests). Pushes only the immutable sha tag — latest-arm64/version-arm64
@@ -534,6 +592,7 @@ jobs:
534592
file: ${{ matrix.dockerfile }}
535593
platforms: linux/arm64
536594
tags: ${{ matrix.image }}:${{ github.sha }}-arm64
595+
build-args: COMMIT_SHA=${{ github.sha }}
537596
max-cache-size-mb: ${{ matrix.cache_mb }}
538597

539598
# Publish all mutable GHCR tags (latest, latest-amd64/arm64, version tags)
@@ -548,6 +607,8 @@ jobs:
548607
if: >-
549608
!cancelled() &&
550609
needs.promote-images.result == 'success' &&
610+
needs.promote-images.outputs.decision == 'promote' &&
611+
needs.promote-images.outputs.complete == 'true' &&
551612
needs.build-ghcr-arm64.result == 'success' &&
552613
needs.detect-version.result == 'success' &&
553614
github.event_name == 'push' && github.ref == 'refs/heads/main'
@@ -583,12 +644,12 @@ jobs:
583644
env:
584645
GH_TOKEN: ${{ github.token }}
585646
run: |
586-
STATUS="$(gh api "repos/${{ github.repository }}/compare/${{ github.sha }}...${GITHUB_REF_NAME}" --jq '.status' || echo "unknown")"
587-
if [ "$STATUS" = "identical" ] || { [ "$STATUS" = "ahead" ] && [ "${{ github.run_attempt }}" = "1" ]; }; then
588-
echo "fresh=true" >> $GITHUB_OUTPUT
647+
HEAD_SHA="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${GITHUB_REF_NAME}" --jq '.object.sha')"
648+
if [ "$HEAD_SHA" = "$GITHUB_SHA" ]; then
649+
echo "fresh=true" >> "$GITHUB_OUTPUT"
589650
else
590-
echo "::warning::Publishing immutable tags for ${{ github.sha }} but skipping the latest tags (branch compare: ${STATUS}, attempt ${{ github.run_attempt }})."
591-
echo "fresh=false" >> $GITHUB_OUTPUT
651+
echo "::warning::Skipping latest tags for superseded commit $GITHUB_SHA."
652+
echo "fresh=false" >> "$GITHUB_OUTPUT"
592653
fi
593654
594655
- name: Publish tags and manifests
@@ -921,6 +982,8 @@ jobs:
921982
if: >-
922983
!cancelled() &&
923984
needs.promote-images.result == 'success' &&
985+
needs.promote-images.outputs.decision == 'promote' &&
986+
needs.promote-images.outputs.complete == 'true' &&
924987
needs.check-docs-changes.result == 'success' &&
925988
needs.check-docs-changes.outputs.docs_changed == 'true'
926989
uses: ./.github/workflows/docs-embeddings.yml

apps/sim/lib/core/config/trigger-runtime.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('trigger runtime detection', () => {
2424
})
2525

2626
afterEach(() => {
27+
vi.unstubAllEnvs()
2728
mockTaskContext.isInsideTask = false
2829
resetInsideTriggerRunForTests()
2930
})
@@ -48,6 +49,23 @@ describe('trigger runtime detection', () => {
4849
expect(isInsideTriggerRun()).toBe(true)
4950
})
5051

52+
it('pins outgoing work to the executing deployment on repeated initialization', () => {
53+
vi.stubEnv('TRIGGER_VERSION', 'older-version')
54+
markInsideTriggerRun('20260909.44')
55+
markInsideTriggerRun('20260909.44')
56+
expect(process.env.TRIGGER_VERSION).toBe('20260909.44')
57+
expect(isInsideTriggerRun()).toBe(true)
58+
})
59+
60+
it('preserves local configuration without a deployment version', () => {
61+
vi.stubEnv('TRIGGER_VERSION', undefined)
62+
markInsideTriggerRun()
63+
expect(process.env.TRIGGER_VERSION).toBeUndefined()
64+
vi.stubEnv('TRIGGER_VERSION', 'local-override')
65+
markInsideTriggerRun()
66+
expect(process.env.TRIGGER_VERSION).toBe('local-override')
67+
})
68+
5169
it('keeps the marker on globalThis so a duplicated bundle still sees it', () => {
5270
markInsideTriggerRun()
5371
const carrier = globalThis as Record<symbol, unknown>

apps/sim/lib/core/config/trigger-runtime.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ interface TriggerRunCarrier {
2121
*
2222
* @see https://trigger.dev/docs/config/config-file#lifecycle-functions
2323
*/
24-
export function markInsideTriggerRun(): void {
24+
export function markInsideTriggerRun(deploymentVersion?: string): void {
2525
;(globalThis as TriggerRunCarrier)[INSIDE_TRIGGER_RUN] = true
26+
/** Plain trigger/batchTrigger calls must stay on this worker's deployment too. */
27+
if (deploymentVersion) process.env.TRIGGER_VERSION = deploymentVersion
2628
}
2729

2830
/**

apps/sim/trigger.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ export default defineConfig({
110110
*
111111
* @see https://trigger.dev/docs/config/config-file#lifecycle-functions
112112
*/
113-
init: async () => {
114-
markInsideTriggerRun()
113+
init: async ({ ctx }) => {
114+
markInsideTriggerRun(ctx.deployment?.version)
115115
const { warmRedisConnection } = await import('./lib/core/config/redis')
116116
await warmRedisConnection()
117117
},

0 commit comments

Comments
 (0)