2424 name : Detect Changes
2525 runs-on : ubuntu-latest
2626 outputs :
27- source-changed : ${{ steps.check.outputs.source-changed }}
27+ # Per-platform flags so a change to one language's SDK doesn't fan out to
28+ # the others. `any-changed` gates the shared setup/gate jobs.
29+ dart-changed : ${{ steps.check.outputs.dart-changed }}
30+ swift-changed : ${{ steps.check.outputs.swift-changed }}
31+ kotlin-changed : ${{ steps.check.outputs.kotlin-changed }}
32+ any-changed : ${{ steps.check.outputs.any-changed }}
2833 steps :
2934 - uses : actions/checkout@v5
3035 with :
@@ -33,18 +38,39 @@ jobs:
3338 - id : check
3439 run : |
3540 if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
36- echo "source-changed=true" >> "$GITHUB_OUTPUT"
37- elif git diff --name-only origin/main...HEAD | grep -qE '^(native/(dart|kotlin|swift)|test-apps/native-bindings)/|^\.github/workflows/native-sdk-e2e\.yml$'; then
38- echo "source-changed=true" >> "$GITHUB_OUTPUT"
41+ dart=true; swift=true; kotlin=true
3942 else
40- echo "source-changed=false" >> "$GITHUB_OUTPUT"
43+ CHANGED="$(git diff --name-only origin/main...HEAD)"
44+ # Shared inputs affect EVERY platform: the test backend that
45+ # generates the OpenRPC spec all SDKs are code-genned from, and this
46+ # workflow file itself. A change here forces all platforms to run.
47+ shared=false
48+ if echo "$CHANGED" | grep -qE '^test-apps/native-bindings/|^\.github/workflows/native-sdk-e2e\.yml$'; then
49+ shared=true
50+ fi
51+ # Each platform runs when a shared input changed OR its own SDK dir
52+ # changed — so a Dart-only change no longer triggers Kotlin/Swift.
53+ dart=$shared; swift=$shared; kotlin=$shared
54+ if echo "$CHANGED" | grep -qE '^native/dart/'; then dart=true; fi
55+ if echo "$CHANGED" | grep -qE '^native/swift/'; then swift=true; fi
56+ if echo "$CHANGED" | grep -qE '^native/kotlin/'; then kotlin=true; fi
57+ fi
58+ any=false
59+ if [ "$dart" = true ] || [ "$swift" = true ] || [ "$kotlin" = true ]; then
60+ any=true
4161 fi
62+ {
63+ echo "dart-changed=$dart"
64+ echo "swift-changed=$swift"
65+ echo "kotlin-changed=$kotlin"
66+ echo "any-changed=$any"
67+ } >> "$GITHUB_OUTPUT"
4268
4369 # Shared setup: build monorepo, generate spec, upload artifact
4470 setup :
4571 name : Build & Generate Spec
4672 needs : [detect-changes]
47- if : needs.detect-changes.outputs.source -changed == 'true'
73+ if : needs.detect-changes.outputs.any -changed == 'true'
4874 runs-on : ubuntu-latest
4975 timeout-minutes : 10
5076 outputs :
81107 dart-e2e :
82108 name : Dart E2E
83109 needs : [detect-changes, setup]
84- if : needs.detect-changes.outputs.source -changed == 'true'
110+ if : needs.detect-changes.outputs.dart -changed == 'true'
85111 runs-on : ubuntu-latest
86112 timeout-minutes : 15
87113 steps :
@@ -164,17 +190,74 @@ jobs:
164190 # BLOCKS_URL is provided via $GITHUB_ENV from the "Pick a free port" step.
165191 run : dart run bin/e2e_test.dart
166192
167- # Kotlin E2E (TODO: add when ready)
168- # kotlin-e2e:
169- # name: Kotlin E2E
170- # needs: [detect-changes, setup]
171- # if: needs.detect-changes.outputs.source-changed == 'true'
172- # ...
193+ kotlin-e2e :
194+ name : Kotlin E2E (${{ matrix.label }})
195+ needs : [detect-changes, setup]
196+ if : needs.detect-changes.outputs.kotlin-changed == 'true'
197+ runs-on : ${{ matrix.runs-on }}
198+ timeout-minutes : ${{ matrix.timeout }}
199+ strategy :
200+ fail-fast : false
201+ matrix :
202+ include :
203+ - label : JVM
204+ runs-on : ubuntu-latest
205+ gradle-task : jvmTest
206+ timeout : 15
207+ - label : iOS
208+ runs-on : macos-15
209+ gradle-task : iosSimulatorArm64Test
210+ timeout : 20
211+ steps :
212+ - uses : actions/checkout@v5
213+ with :
214+ ref : ${{ github.event.pull_request.head.sha }}
215+ persist-credentials : false
216+
217+ - uses : actions/setup-node@v5
218+ with :
219+ node-version-file : ' .nvmrc'
220+ cache : npm
221+
222+ - uses : actions/setup-java@v4
223+ with :
224+ distribution : temurin
225+ java-version : 17
226+
227+ - run : npm ci
228+ - run : npm run build
229+
230+ - name : Download spec
231+ uses : actions/download-artifact@v4
232+ with :
233+ name : blocks-spec
234+ path : native/kotlin/e2e/
235+
236+ - name : Run Kotlin codegen
237+ working-directory : native/kotlin/e2e
238+ run : ./gradlew awsBlocksCodegen
239+
240+ - name : Start native-bindings server
241+ working-directory : test-apps/native-bindings
242+ run : |
243+ npx tsx aws-blocks/scripts/server.ts &
244+ for i in $(seq 1 30); do
245+ curl -s -X POST http://localhost:3001/aws-blocks/api \
246+ -H "Content-Type: application/json" \
247+ -d '{"jsonrpc":"2.0","method":"api.kvGet","params":{"key":"healthcheck"},"id":1}' && break
248+ sleep 1
249+ done
250+
251+ - name : Run E2E tests
252+ working-directory : native/kotlin/e2e
253+ env :
254+ BLOCKS_URL : http://localhost:3001/aws-blocks/api
255+ run : ./gradlew ${{ matrix.gradle-task }}
173256
174257 swift-e2e :
175258 name : Swift E2E
176259 needs : [detect-changes, setup]
177- if : needs.detect-changes.outputs.source -changed == 'true'
260+ if : needs.detect-changes.outputs.swift -changed == 'true'
178261 runs-on : macos-15
179262 timeout-minutes : 20
180263 steps :
@@ -265,7 +348,7 @@ jobs:
265348 name : Dart E2E (sandbox)
266349 needs : [detect-changes, dart-e2e]
267350 if : >-
268- needs.detect-changes.outputs.source -changed == 'true' &&
351+ needs.detect-changes.outputs.dart -changed == 'true' &&
269352 (github.event_name == 'workflow_dispatch' ||
270353 github.event.pull_request.head.repo.full_name == github.repository)
271354 runs-on : ubuntu-latest
@@ -403,7 +486,7 @@ jobs:
403486 name : Swift E2E (sandbox)
404487 needs : [detect-changes, swift-e2e]
405488 if : >-
406- needs.detect-changes.outputs.source -changed == 'true' &&
489+ needs.detect-changes.outputs.swift -changed == 'true' &&
407490 (github.event_name == 'workflow_dispatch' ||
408491 github.event.pull_request.head.repo.full_name == github.repository)
409492 runs-on : macos-15
@@ -497,31 +580,131 @@ jobs:
497580 working-directory : test-apps/native-bindings
498581 run : npm run destroy || true
499582
583+ kotlin-e2e-sandbox :
584+ name : Kotlin E2E (${{ matrix.label }}, sandbox)
585+ needs : [detect-changes, kotlin-e2e]
586+ if : >-
587+ needs.detect-changes.outputs.kotlin-changed == 'true' &&
588+ (github.event_name == 'workflow_dispatch' ||
589+ github.event.pull_request.head.repo.full_name == github.repository)
590+ runs-on : ${{ matrix.runs-on }}
591+ timeout-minutes : 30
592+ environment : publish
593+ strategy :
594+ fail-fast : false
595+ matrix :
596+ include :
597+ - label : JVM
598+ suffix : jvm
599+ runs-on : ubuntu-latest
600+ gradle-task : jvmTest
601+ - label : iOS
602+ suffix : ios
603+ runs-on : macos-15
604+ gradle-task : iosSimulatorArm64Test
605+ env :
606+ BLOCKS_STACK_SUFFIX : native-kotlin-${{ matrix.suffix }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.run_attempt }}
607+ steps :
608+ - uses : actions/checkout@v5
609+ with :
610+ ref : ${{ github.event.pull_request.head.sha }}
611+ persist-credentials : false
612+
613+ - uses : actions/setup-node@v5
614+ with :
615+ node-version-file : ' .nvmrc'
616+ cache : npm
617+
618+ - uses : actions/setup-java@v4
619+ with :
620+ distribution : temurin
621+ java-version : 17
622+
623+ - run : npm ci
624+ - run : npm run build
625+
626+ - name : Configure AWS credentials
627+ uses : aws-actions/configure-aws-credentials@v6
628+ with :
629+ role-to-assume : ${{ secrets.AWS_ROLE_ARN }}
630+ aws-region : us-east-1
631+
632+ - name : Deploy sandbox
633+ working-directory : test-apps/native-bindings
634+ run : npm run deploy
635+
636+ - name : Resolve BLOCKS_URL
637+ id : url
638+ working-directory : test-apps/native-bindings
639+ run : |
640+ URL=$(python3 -c "import json; print(json.load(open('.blocks-sandbox/config.json'))['apiUrl'])")
641+ echo "blocks_url=${URL}/aws-blocks/api" >> $GITHUB_OUTPUT
642+
643+ - name : Warm up sandbox API
644+ env :
645+ BLOCKS_URL : ${{ steps.url.outputs.blocks_url }}
646+ run : |
647+ attempts=24
648+ delay=2
649+ for i in $(seq 1 "$attempts"); do
650+ code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
651+ -X POST -H 'content-type: application/json' -d '{}' \
652+ "$BLOCKS_URL" 2>/dev/null || echo 000)
653+ if [ "$code" = "200" ]; then
654+ echo "✅ Sandbox API warm after $i attempt(s) (http=200)."
655+ exit 0
656+ fi
657+ echo "⏳ attempt $i/$attempts: API not ready (http=$code); retrying in ${delay}s..."
658+ sleep "$delay"
659+ if [ "$delay" -lt 10 ]; then delay=$((delay + 2)); fi
660+ done
661+ echo "❌ Sandbox API did not return HTTP 200 after $attempts attempts."
662+ exit 1
663+
664+ - name : Generate OpenRPC spec
665+ working-directory : test-apps/native-bindings
666+ run : npm run spec
667+
668+ - name : Download spec
669+ uses : actions/download-artifact@v4
670+ with :
671+ name : blocks-spec
672+ path : native/kotlin/e2e/
673+
674+ - name : Run Kotlin codegen
675+ working-directory : native/kotlin/e2e
676+ run : ./gradlew awsBlocksCodegen
677+
678+ - name : Run Kotlin E2E against sandbox
679+ working-directory : native/kotlin/e2e
680+ env :
681+ BLOCKS_URL : ${{ steps.url.outputs.blocks_url }}
682+ run : ./gradlew ${{ matrix.gradle-task }}
683+
684+ - name : Destroy sandbox
685+ if : always()
686+ working-directory : test-apps/native-bindings
687+ run : npm run destroy || true
688+
500689 # Aggregate gate so this can be a single required status check in branch
501690 # protection. Always runs (even when the e2e jobs are skipped for unrelated
502691 # or fork PRs) and only fails if a job that actually ran failed.
503692 native-sdk-e2e-required :
504693 name : Native SDK E2E (Required)
505694 if : always()
506- needs : [detect-changes, setup, dart-e2e, swift-e2e]
695+ needs : [detect-changes, setup, dart-e2e, kotlin-e2e, swift-e2e]
507696 runs-on : ubuntu-latest
508697 steps :
509698 - name : Check results
510699 run : |
511- echo "detect-changes=${{ needs.detect-changes.result }} setup=${{ needs.setup.result }} dart-e2e=${{ needs.dart-e2e.result }} swift-e2e=${{ needs.swift-e2e.result }}"
512- # Required gate = SDK correctness: detect-changes + setup + the local
513- # dart-e2e suite (no AWS). The deploying `dart-e2e-sandbox` job still
514- # runs as a NON-BLOCKING signal — it depends on deployed-backend config
515- # that isn't part of the SDK (real-Cognito email code delivery, which a
516- # dev-only `cognitoGetLastCode` hook can't satisfy; and fresh-deploy
517- # OIDC state). The OIDC relay suite was verified 5/5 against a live
518- # sandbox, so this is environment, not SDK. Re-add `dart-e2e-sandbox`
519- # to this gate once the deployed Cognito sign-in path is testable.
520- # Skipped jobs (no source changes) are treated as passing.
700+ echo "detect-changes=${{ needs.detect-changes.result }} setup=${{ needs.setup.result }} dart-e2e=${{ needs.dart-e2e.result }} kotlin-e2e=${{ needs.kotlin-e2e.result }} swift-e2e=${{ needs.swift-e2e.result }}"
701+ # Required gate = SDK correctness. Skipped jobs (no source changes)
702+ # are treated as passing.
521703 for result in \
522704 "${{ needs.detect-changes.result }}" \
523705 "${{ needs.setup.result }}" \
524706 "${{ needs.dart-e2e.result }}" \
707+ "${{ needs.kotlin-e2e.result }}" \
525708 "${{ needs.swift-e2e.result }}"; do
526709 if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
527710 echo "A required native SDK E2E job failed (or was cancelled)."
0 commit comments