Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 153 additions & 17 deletions .github/workflows/native-sdk-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,69 @@ jobs:
# BLOCKS_URL is provided via $GITHUB_ENV from the "Pick a free port" step.
run: dart run bin/e2e_test.dart

# Kotlin E2E (TODO: add when ready)
# kotlin-e2e:
# name: Kotlin E2E
# needs: [detect-changes, setup]
# if: needs.detect-changes.outputs.source-changed == 'true'
# ...
kotlin-e2e:
name: Kotlin E2E (${{ matrix.label }})
needs: [detect-changes, setup]
if: needs.detect-changes.outputs.source-changed == 'true'
runs-on: ${{ matrix.runs-on }}
timeout-minutes: ${{ matrix.timeout }}
strategy:
fail-fast: false
matrix:
include:
- label: JVM
runs-on: ubuntu-latest
gradle-task: jvmTest
timeout: 15
- label: iOS
runs-on: macos-15
gradle-task: iosSimulatorArm64Test
timeout: 20
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: npm

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- run: npm ci
- run: npm run build

- name: Download spec
uses: actions/download-artifact@v4
with:
name: blocks-spec
path: native/kotlin/e2e/

- name: Run Kotlin codegen
working-directory: native/kotlin/e2e
run: ./gradlew awsBlocksCodegen

- name: Start native-bindings server
working-directory: test-apps/native-bindings
run: |
npx tsx aws-blocks/scripts/server.ts &
for i in $(seq 1 30); do
curl -s -X POST http://localhost:3001/aws-blocks/api \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"api.kvGet","params":{"key":"healthcheck"},"id":1}' && break
sleep 1
done

- name: Run E2E tests
working-directory: native/kotlin/e2e
env:
BLOCKS_URL: http://localhost:3001/aws-blocks/api
run: ./gradlew ${{ matrix.gradle-task }}

swift-e2e:
name: Swift E2E
Expand Down Expand Up @@ -477,31 +534,110 @@ jobs:
working-directory: test-apps/native-bindings
run: npm run destroy || true

kotlin-e2e-sandbox:
name: Kotlin E2E (${{ matrix.label }}, sandbox)
needs: [detect-changes, kotlin-e2e]
if: >-
needs.detect-changes.outputs.source-changed == 'true' &&
(github.event_name == 'workflow_dispatch' ||
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 30
environment: publish
strategy:
fail-fast: false
matrix:
include:
- label: JVM
suffix: jvm
runs-on: ubuntu-latest
gradle-task: jvmTest
- label: iOS
suffix: ios
runs-on: macos-15
gradle-task: iosSimulatorArm64Test
env:
BLOCKS_STACK_SUFFIX: native-kotlin-${{ matrix.suffix }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.run_attempt }}
steps:
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
cache: npm

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- run: npm ci
- run: npm run build

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1

- name: Deploy sandbox
working-directory: test-apps/native-bindings
run: npm run deploy

- name: Resolve BLOCKS_URL
id: url
working-directory: test-apps/native-bindings
run: |
URL=$(python3 -c "import json; print(json.load(open('.blocks-sandbox/config.json'))['apiUrl'])")
echo "blocks_url=${URL}/aws-blocks/api" >> $GITHUB_OUTPUT

- name: Generate OpenRPC spec
working-directory: test-apps/native-bindings
run: npm run spec

- name: Download spec
uses: actions/download-artifact@v4
with:
name: blocks-spec
path: native/kotlin/e2e/

- name: Run Kotlin codegen
working-directory: native/kotlin/e2e
run: ./gradlew awsBlocksCodegen

- name: Run Kotlin E2E against sandbox
working-directory: native/kotlin/e2e
env:
BLOCKS_URL: ${{ steps.url.outputs.blocks_url }}
run: ./gradlew ${{ matrix.gradle-task }}

- name: Destroy sandbox
if: always()
working-directory: test-apps/native-bindings
run: npm run destroy || true

# Aggregate gate so this can be a single required status check in branch
# protection. Always runs (even when the e2e jobs are skipped for unrelated
# or fork PRs) and only fails if a job that actually ran failed.
native-sdk-e2e-required:
name: Native SDK E2E (Required)
if: always()
needs: [detect-changes, setup, dart-e2e, swift-e2e]
needs: [detect-changes, setup, dart-e2e, kotlin-e2e, swift-e2e]
runs-on: ubuntu-latest
steps:
- name: Check results
run: |
echo "detect-changes=${{ needs.detect-changes.result }} setup=${{ needs.setup.result }} dart-e2e=${{ needs.dart-e2e.result }} swift-e2e=${{ needs.swift-e2e.result }}"
# Required gate = SDK correctness: detect-changes + setup + the local
# dart-e2e suite (no AWS). The deploying `dart-e2e-sandbox` job still
# runs as a NON-BLOCKING signal — it depends on deployed-backend config
# that isn't part of the SDK (real-Cognito email code delivery, which a
# dev-only `cognitoGetLastCode` hook can't satisfy; and fresh-deploy
# OIDC state). The OIDC relay suite was verified 5/5 against a live
# sandbox, so this is environment, not SDK. Re-add `dart-e2e-sandbox`
# to this gate once the deployed Cognito sign-in path is testable.
# Skipped jobs (no source changes) are treated as passing.
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 }}"
# Required gate = SDK correctness. Skipped jobs (no source changes)
# are treated as passing.
for result in \
"${{ needs.detect-changes.result }}" \
"${{ needs.setup.result }}" \
"${{ needs.dart-e2e.result }}" \
"${{ needs.kotlin-e2e.result }}" \
"${{ needs.swift-e2e.result }}"; do
if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then
echo "A required native SDK E2E job failed (or was cancelled)."
Expand Down
13 changes: 5 additions & 8 deletions native/dart/example/bin/e2e/realtime_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ void main() async {
group('Realtime: subscribe and receive');
final ch = await blocks.api.realtimeGetChannel(channel: 'dart-test');
final stream = ch.subscribe();
final completer = Completer<dynamic>();
final completer = Completer<Cursor>();

final sub = stream.listen((msg) {
if (!completer.isCompleted) completer.complete(msg);
Expand All @@ -38,13 +38,10 @@ void main() async {

try {
final msg = await completer.future.timeout(Duration(seconds: 5));
check(msg != null, 'received message via WebSocket');
if (msg is Map<String, dynamic>) {
check(msg['userId'] == 'dart-sub-test', 'message userId matches');
check(msg['x'] == 42, 'message x matches');
} else {
check(true, 'received message (type: ${msg.runtimeType})');
}
check(msg.userId == 'dart-sub-test', 'message userId matches');
check(msg.x == 42, 'message x matches');
check(msg.y == 99, 'message y matches');
check(msg.color == '#00ff00', 'message color matches');
} on TimeoutException {
check(false, 'WebSocket message received within 5s (timed out)');
} finally {
Expand Down
5 changes: 5 additions & 0 deletions native/kotlin/.changeset/brave-geckos-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"aws-blocks-kotlin": patch
---

Fix handling of keychain in the iOS runtime
5 changes: 5 additions & 0 deletions native/kotlin/.changeset/fuzzy-tables-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"aws-blocks-kotlin": minor
---

Bump Kotlin version to 2.2.10
5 changes: 5 additions & 0 deletions native/kotlin/.changeset/heavy-worlds-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"aws-blocks-kotlin": minor
---

Add ability to clear cookies
4 changes: 2 additions & 2 deletions native/kotlin/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AWS Blocks Kotlin

[![Maven Central](https://img.shields.io/maven-central/v/com.aws.blocks.kotlin/runtime)](https://central.sonatype.com/search?namespace=com.aws.blocks.kotlin)
[![Kotlin](https://img.shields.io/badge/kotlin-2.1.21-blue.svg?logo=kotlin)](https://kotlinlang.org)
[![Kotlin](https://img.shields.io/badge/kotlin-2.2.10-blue.svg?logo=kotlin)](https://kotlinlang.org)
![Android](http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat)
![iOS](http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat)
![Desktop](http://img.shields.io/badge/platform-desktop-DB413D.svg?style=flat)
Expand Down Expand Up @@ -106,7 +106,7 @@ See the [`example/android`](example/android) directory for a complete Android ap

## Requirements

- Kotlin 2.x
- Kotlin 2.1+
- JDK 17+
- Gradle 7.4+
- Android Gradle Plugin 7.1+ (for Android targets)
Expand Down
3 changes: 3 additions & 0 deletions native/kotlin/e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build/
.gradle/
blocks.spec.json
86 changes: 86 additions & 0 deletions native/kotlin/e2e/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest

plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlinx.serialization)
id("com.aws.blocks.kotlin")
}

kotlin {
jvm()

iosSimulatorArm64 {
// PersistentCookiesStorage on iOS is Keychain-backed. The bare Kotlin/Native
// simulator test binary has no keychain-access-groups entitlement, so Keychain
// calls fail with errSecNotAvailable and session cookies never persist. Embed an
// entitlements section into the test binary; combined with the booted, non-standalone
// test run configured below, the simulator honors the entitlement so the Keychain
// works under test.
binaries.getTest("DEBUG").linkerOpts(
"-sectcreate", "__TEXT", "__entitlements",
"${projectDir}/entitlements.plist"
)
}

sourceSets {
commonMain.dependencies {
implementation("com.aws.blocks.kotlin:runtime")
}

commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotest.assertions.core)
implementation(libs.kotlinx.coroutines.test)
}

jvmTest.dependencies {
implementation(libs.kotest.runner.junit5)
implementation(libs.ktor.client.okhttp)
}

iosTest.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}

tasks.named<Test>("jvmTest") {
useJUnitPlatform()
val url = providers.systemProperty("BLOCKS_URL").orElse(
providers.environmentVariable("BLOCKS_URL")
).getOrElse("")
systemProperty("BLOCKS_URL", url)
environment("BLOCKS_URL", url)
}

// Boots and opens an iOS simulator. The Keychain-backed tests run non-standalone against a
// booted device (see below), which requires a simulator to already be running. Making the
// iOS test task depend on this keeps CI and local `run-e2e.sh` working without a manual boot.
val launchIosSimulator by tasks.registering(Exec::class) {
isIgnoreExitValue = true
// No-op if a simulator is already booted; otherwise boot the first available iPhone.
// `open -a Simulator` is macOS-only and harmless if already open.
commandLine(
"sh", "-c",
"""
if ! xcrun simctl list devices booted | grep -q Booted; then
udid=${'$'}(xcrun simctl list devices available | grep -Eo '[0-9A-F-]{36}' | head -1)
[ -n "${'$'}udid" ] && xcrun simctl boot "${'$'}udid"
fi
open -a Simulator 2>/dev/null || true
""".trimIndent()
)
}

tasks.withType<KotlinNativeSimulatorTest>().configureEach {
dependsOn(launchIosSimulator)
// Launch as an app on a booted simulator (not a standalone `simctl spawn`) so the
// embedded keychain-access-groups entitlement is honored and Keychain storage works.
standalone.set(false)
device.set("booted")
}

awsBlocks {
apiSpec = file("blocks.spec.json")
packageName = "blocks.e2e"
}
12 changes: 12 additions & 0 deletions native/kotlin/e2e/entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>application-identifier</key>
<string>com.aws.blocks.kotlin.e2e</string>
<key>keychain-access-groups</key>
<array>
<string>com.aws.blocks.kotlin.e2e</string>
</array>
</dict>
</plist>
1 change: 1 addition & 0 deletions native/kotlin/e2e/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kotlin.mpp.enableCInteropCommonization=true
Binary file not shown.
8 changes: 8 additions & 0 deletions native/kotlin/e2e/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#Wed May 13 11:21:16 ADT 2026
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading