diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9b072cda --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,105 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + +jobs: + lint: + name: Lint (spotless + animal-sniffer) + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up JDK 11 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: '11' + cache: maven + + - name: Artifactory OIDC + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: maven + provider-name: github-actions-segmentio + + - name: Spotless check + run: mvn -B spotless:check + + - name: Animal Sniffer check + # Needs the reactor compiled first: `analytics` builds against the + # analytics-core SNAPSHOT, which exists nowhere until this run makes it. + # `compile` rather than `package` — linting has no use for the javadoc, + # source and signing plugins that packaging would drag in. + run: mvn -B -DskipTests compile animal-sniffer:check + + test: + name: Test (Java ${{ matrix.java-version }}) + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + strategy: + fail-fast: false + matrix: + java-version: ['11', '17', '21'] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.java-version }} + cache: maven + + - name: Artifactory OIDC + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: maven + provider-name: github-actions-segmentio + + - name: Run tests + run: mvn -B test + + build: + name: Build verification + runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || 'ubuntu-x64' }} + needs: [lint, test] + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up JDK 11 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: '11' + cache: maven + + - name: Artifactory OIDC + if: ${{ !github.event.pull_request.head.repo.fork }} + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: maven + provider-name: github-actions-segmentio + + - name: Build + run: mvn -B package -DskipTests + + - name: Verify + run: mvn -B verify -DskipTests diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..50db64ba --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,76 @@ +name: Deploy to Maven Central + +on: + release: + types: [published] + +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + +jobs: + test: + name: Pre-deploy tests + runs-on: ubuntu-x64 + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up JDK 11 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: '11' + cache: maven + + - name: Artifactory OIDC + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: maven + provider-name: github-actions-segmentio + + - name: Run tests + run: mvn -B test + + deploy: + name: Deploy to Maven Central + runs-on: ubuntu-x64 + needs: [test] + environment: maven-central + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Set up JDK 11 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 + with: + distribution: 'temurin' + java-version: '11' + cache: maven + server-id: central + server-username: CI_DEPLOY_USERNAME + server-password: CI_DEPLOY_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Artifactory OIDC + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: maven + provider-name: github-actions-segmentio + + - name: Deploy to Maven Central + env: + CI_DEPLOY_USERNAME: ${{ secrets.CI_DEPLOY_USERNAME }} + CI_DEPLOY_PASSWORD: ${{ secrets.CI_DEPLOY_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + mvn -B deploy \ + -DskipTests \ + -Dgpg.passphrase="$GPG_PASSPHRASE" \ + -P release diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c59ec2cb..8255dc98 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,18 +1,12 @@ -# E2E Tests for analytics-java -# Copy this file to: analytics-java/.github/workflows/e2e-tests.yml -# -# This workflow: -# 1. Checks out the SDK and sdk-e2e-tests repos -# 2. Builds the SDK and e2e-cli -# 3. Runs the e2e test suite - name: E2E Tests on: push: - branches: [main, master] + branches: [master] pull_request: - branches: [main, master] + branches: [master] + schedule: + - cron: '0 6 * * 1-5' workflow_dispatch: inputs: e2e_tests_ref: @@ -20,47 +14,60 @@ on: required: false default: 'main' +permissions: + id-token: write + contents: read + +env: + ARTIFACTORY_URL: ${{ vars.ARTIFACTORY_URL }} + jobs: e2e-tests: - # Skip on fork PRs where repo secrets aren't available - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - runs-on: ubuntu-latest + name: E2E Test Suite + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} + runs-on: ubuntu-x64 steps: - name: Checkout SDK - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: path: sdk - name: Checkout sdk-e2e-tests - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: repository: segmentio/sdk-e2e-tests ref: ${{ inputs.e2e_tests_ref || 'main' }} - token: ${{ secrets.E2E_TESTS_TOKEN }} path: sdk-e2e-tests - - name: Setup Java - uses: actions/setup-java@v4 + - name: Set up JDK 11 + uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4 with: distribution: 'temurin' java-version: '11' + cache: maven + + - name: Artifactory OIDC + uses: twilio/sdk-actions/artifactory-oidc@c94e420aa64ea686ff25bb03d4c66cdaf8e523e4 # main + with: + ecosystem: maven + provider-name: github-actions-segmentio - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4 with: node-version: '20' - name: Build Java SDK and e2e-cli working-directory: sdk - run: mvn package -pl e2e-cli -am -DskipTests + run: mvn -B package -pl e2e-cli -am -DskipTests - name: Find e2e-cli jar id: find-jar working-directory: sdk run: | JAR_PATH=$(find e2e-cli/target -name "e2e-cli-*-jar-with-dependencies.jar" | head -1) - echo "jar_path=$JAR_PATH" >> $GITHUB_OUTPUT + echo "jar_path=$JAR_PATH" >> "$GITHUB_OUTPUT" - name: Run E2E tests working-directory: sdk-e2e-tests @@ -71,7 +78,7 @@ jobs: - name: Upload test results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: e2e-test-results path: sdk-e2e-tests/test-results/ diff --git a/analytics-cli/src/main/kotlin/cli/JavadocStub.java b/analytics-cli/src/main/kotlin/cli/JavadocStub.java index 403beaa7..a33e30f0 100644 --- a/analytics-cli/src/main/kotlin/cli/JavadocStub.java +++ b/analytics-cli/src/main/kotlin/cli/JavadocStub.java @@ -1,8 +1,6 @@ package cli; -/** - * Placeholder class to generate a Javadoc JAR for the CLI artifact. - */ +/** Placeholder class to generate a Javadoc JAR for the CLI artifact. */ public final class JavadocStub { private JavadocStub() {} } diff --git a/analytics-core/pom.xml b/analytics-core/pom.xml index 4c0ed0cd..7fbc3184 100644 --- a/analytics-core/pom.xml +++ b/analytics-core/pom.xml @@ -51,12 +51,6 @@ gson ${gson.version} - - com.google.code.findbugs - findbugs - ${findbugs.version} - provided - com.google.auto.value auto-value diff --git a/analytics/pom.xml b/analytics/pom.xml index 0c19a777..0230c840 100644 --- a/analytics/pom.xml +++ b/analytics/pom.xml @@ -66,12 +66,6 @@ retrofit-mock ${retrofit.version} - - com.google.code.findbugs - findbugs - ${findbugs.version} - provided - com.segment.backo backo @@ -119,7 +113,8 @@ org.codehaus.mojo templating-maven-plugin - 1.0.0 + + 3.1.1 diff --git a/e2e-cli/src/main/java/cli/Main.java b/e2e-cli/src/main/java/cli/Main.java index 7c10646d..4e213ef4 100644 --- a/e2e-cli/src/main/java/cli/Main.java +++ b/e2e-cli/src/main/java/cli/Main.java @@ -5,7 +5,6 @@ import com.segment.analytics.Analytics; import com.segment.analytics.Callback; import com.segment.analytics.messages.*; - import java.lang.reflect.Type; import java.time.Instant; import java.util.*; @@ -15,189 +14,196 @@ public class Main { - private static final Gson gson = new Gson(); - private static final int DEFAULT_MAX_RETRIES = 1000; + private static final Gson gson = new Gson(); + private static final int DEFAULT_MAX_RETRIES = 1000; - public static void main(String[] args) { - Map output; - try { - output = run(args); - } catch (Exception e) { - output = errorOutput(e.getMessage()); - } - System.out.println(gson.toJson(output)); + public static void main(String[] args) { + Map output; + try { + output = run(args); + } catch (Exception e) { + output = errorOutput(e.getMessage()); + } + System.out.println(gson.toJson(output)); + } + + @SuppressWarnings("unchecked") + private static Map run(String[] args) throws Exception { + int inputIndex = indexOf(args, "--input"); + if (inputIndex == -1 || inputIndex + 1 >= args.length) { + throw new IllegalArgumentException("Missing required --input argument"); } - @SuppressWarnings("unchecked") - private static Map run(String[] args) throws Exception { - int inputIndex = indexOf(args, "--input"); - if (inputIndex == -1 || inputIndex + 1 >= args.length) { - throw new IllegalArgumentException("Missing required --input argument"); - } - - String inputJson = args[inputIndex + 1]; - Type mapType = new TypeToken>() {}.getType(); - Map input = gson.fromJson(inputJson, mapType); - - String writeKey = (String) input.get("writeKey"); - String apiHost = (String) input.get("apiHost"); - Map config = (Map) input.getOrDefault("config", Collections.emptyMap()); - List> sequences = (List>) input.get("sequences"); - - int flushAt = intVal(config, "flushAt", 20); - long flushIntervalMs = longVal(config, "flushInterval", 10000L); - int maxRetries = intVal(config, "maxRetries", DEFAULT_MAX_RETRIES); - - AtomicBoolean hasError = new AtomicBoolean(false); - AtomicReference errorMessage = new AtomicReference<>(); - - Analytics analytics = Analytics.builder(writeKey) - .endpoint(apiHost) - .flushQueueSize(flushAt) - .flushInterval(Math.max(flushIntervalMs, 1000L), TimeUnit.MILLISECONDS) - .retries(maxRetries) - .callback(new Callback() { - @Override - public void success(Message message) { - } - - @Override - public void failure(Message message, Throwable throwable) { - hasError.set(true); - errorMessage.set(throwable != null ? throwable.getMessage() : "unknown error"); - } + String inputJson = args[inputIndex + 1]; + Type mapType = new TypeToken>() {}.getType(); + Map input = gson.fromJson(inputJson, mapType); + + String writeKey = (String) input.get("writeKey"); + String apiHost = (String) input.get("apiHost"); + Map config = + (Map) input.getOrDefault("config", Collections.emptyMap()); + List> sequences = (List>) input.get("sequences"); + + int flushAt = intVal(config, "flushAt", 20); + long flushIntervalMs = longVal(config, "flushInterval", 10000L); + int maxRetries = intVal(config, "maxRetries", DEFAULT_MAX_RETRIES); + + AtomicBoolean hasError = new AtomicBoolean(false); + AtomicReference errorMessage = new AtomicReference<>(); + + Analytics analytics = + Analytics.builder(writeKey) + .endpoint(apiHost) + .flushQueueSize(flushAt) + .flushInterval(Math.max(flushIntervalMs, 1000L), TimeUnit.MILLISECONDS) + .retries(maxRetries) + .callback( + new Callback() { + @Override + public void success(Message message) {} + + @Override + public void failure(Message message, Throwable throwable) { + hasError.set(true); + errorMessage.set(throwable != null ? throwable.getMessage() : "unknown error"); + } }) - .build(); - - for (Map seq : sequences) { - long delayMs = longVal(seq, "delayMs", 0L); - if (delayMs > 0) { - Thread.sleep(delayMs); - } - List> events = (List>) seq.get("events"); - if (events != null) { - for (Map event : events) { - sendEvent(analytics, event); - } - } + .build(); + + for (Map seq : sequences) { + long delayMs = longVal(seq, "delayMs", 0L); + if (delayMs > 0) { + Thread.sleep(delayMs); + } + List> events = (List>) seq.get("events"); + if (events != null) { + for (Map event : events) { + sendEvent(analytics, event); } - - analytics.flush(); - analytics.shutdown(); - - if (hasError.get()) { - return errorOutput(errorMessage.get()); - } - - Map result = new LinkedHashMap<>(); - result.put("success", true); - result.put("sentBatches", 1); - return result; + } } - @SuppressWarnings("unchecked") - private static void sendEvent(Analytics analytics, Map event) { - String type = (String) event.get("type"); - if (type == null) { - throw new IllegalArgumentException("Event missing 'type' field"); - } - - String userId = strVal(event, "userId", ""); - String anonymousId = (String) event.get("anonymousId"); - String messageId = (String) event.get("messageId"); - String timestamp = (String) event.get("timestamp"); - Map traits = (Map) event.getOrDefault("traits", Collections.emptyMap()); - Map properties = (Map) event.getOrDefault("properties", Collections.emptyMap()); - String eventName = (String) event.get("event"); - String name = (String) event.get("name"); - String groupId = (String) event.get("groupId"); - String previousId = (String) event.get("previousId"); - Map context = (Map) event.get("context"); - Map integrations = (Map) event.get("integrations"); - - MessageBuilder builder; - switch (type) { - case "identify": - builder = IdentifyMessage.builder().traits(traits); - break; - case "track": - builder = TrackMessage.builder(eventName != null ? eventName : "Unknown Event").properties(properties); - break; - case "page": - builder = PageMessage.builder(name != null ? name : "Unknown Page").properties(properties); - break; - case "screen": - builder = ScreenMessage.builder(name != null ? name : "Unknown Screen").properties(properties); - break; - case "alias": - builder = AliasMessage.builder(previousId != null ? previousId : ""); - break; - case "group": - builder = GroupMessage.builder(groupId != null ? groupId : "").traits(traits); - break; - default: - throw new IllegalArgumentException("Unknown event type: " + type); - } + analytics.flush(); + analytics.shutdown(); - if (!userId.isEmpty()) { - builder.userId(userId); - } - if (anonymousId != null) { - builder.anonymousId(anonymousId); - } - if (messageId != null) { - builder.messageId(messageId); - } - if (timestamp != null) { - builder.timestamp(Date.from(Instant.parse(timestamp))); - } - if (context != null) { - builder.context(context); - } - if (integrations != null) { - for (Map.Entry entry : integrations.entrySet()) { - Object value = entry.getValue(); - if (value instanceof Boolean) { - builder.enableIntegration(entry.getKey(), (Boolean) value); - } else if (value instanceof Map) { - builder.integrationOptions(entry.getKey(), (Map) value); - } - } - } + if (hasError.get()) { + return errorOutput(errorMessage.get()); + } - analytics.enqueue(builder); + Map result = new LinkedHashMap<>(); + result.put("success", true); + result.put("sentBatches", 1); + return result; + } + + @SuppressWarnings("unchecked") + private static void sendEvent(Analytics analytics, Map event) { + String type = (String) event.get("type"); + if (type == null) { + throw new IllegalArgumentException("Event missing 'type' field"); } - private static Map errorOutput(String error) { - Map result = new LinkedHashMap<>(); - result.put("success", false); - result.put("error", error); - result.put("sentBatches", 0); - return result; + String userId = strVal(event, "userId", ""); + String anonymousId = (String) event.get("anonymousId"); + String messageId = (String) event.get("messageId"); + String timestamp = (String) event.get("timestamp"); + Map traits = + (Map) event.getOrDefault("traits", Collections.emptyMap()); + Map properties = + (Map) event.getOrDefault("properties", Collections.emptyMap()); + String eventName = (String) event.get("event"); + String name = (String) event.get("name"); + String groupId = (String) event.get("groupId"); + String previousId = (String) event.get("previousId"); + Map context = (Map) event.get("context"); + Map integrations = (Map) event.get("integrations"); + + MessageBuilder builder; + switch (type) { + case "identify": + builder = IdentifyMessage.builder().traits(traits); + break; + case "track": + builder = + TrackMessage.builder(eventName != null ? eventName : "Unknown Event") + .properties(properties); + break; + case "page": + builder = PageMessage.builder(name != null ? name : "Unknown Page").properties(properties); + break; + case "screen": + builder = + ScreenMessage.builder(name != null ? name : "Unknown Screen").properties(properties); + break; + case "alias": + builder = AliasMessage.builder(previousId != null ? previousId : ""); + break; + case "group": + builder = GroupMessage.builder(groupId != null ? groupId : "").traits(traits); + break; + default: + throw new IllegalArgumentException("Unknown event type: " + type); } - private static int indexOf(String[] arr, String target) { - for (int i = 0; i < arr.length; i++) { - if (target.equals(arr[i])) return i; + if (!userId.isEmpty()) { + builder.userId(userId); + } + if (anonymousId != null) { + builder.anonymousId(anonymousId); + } + if (messageId != null) { + builder.messageId(messageId); + } + if (timestamp != null) { + builder.timestamp(Date.from(Instant.parse(timestamp))); + } + if (context != null) { + builder.context(context); + } + if (integrations != null) { + for (Map.Entry entry : integrations.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Boolean) { + builder.enableIntegration(entry.getKey(), (Boolean) value); + } else if (value instanceof Map) { + builder.integrationOptions(entry.getKey(), (Map) value); } - return -1; + } } - private static int intVal(Map map, String key, int defaultVal) { - Object v = map.get(key); - if (v instanceof Number) return ((Number) v).intValue(); - return defaultVal; - } + analytics.enqueue(builder); + } - private static long longVal(Map map, String key, long defaultVal) { - Object v = map.get(key); - if (v instanceof Number) return ((Number) v).longValue(); - return defaultVal; - } + private static Map errorOutput(String error) { + Map result = new LinkedHashMap<>(); + result.put("success", false); + result.put("error", error); + result.put("sentBatches", 0); + return result; + } - private static String strVal(Map map, String key, String defaultVal) { - Object v = map.get(key); - if (v instanceof String) return (String) v; - return defaultVal; + private static int indexOf(String[] arr, String target) { + for (int i = 0; i < arr.length; i++) { + if (target.equals(arr[i])) return i; } + return -1; + } + + private static int intVal(Map map, String key, int defaultVal) { + Object v = map.get(key); + if (v instanceof Number) return ((Number) v).intValue(); + return defaultVal; + } + + private static long longVal(Map map, String key, long defaultVal) { + Object v = map.get(key); + if (v instanceof Number) return ((Number) v).longValue(); + return defaultVal; + } + + private static String strVal(Map map, String key, String defaultVal) { + Object v = map.get(key); + if (v instanceof String) return (String) v; + return defaultVal; + } } diff --git a/pom.xml b/pom.xml index 21a869da..8ad9ed3d 100644 --- a/pom.xml +++ b/pom.xml @@ -33,11 +33,10 @@ UTF-8 8 1.9.0 - 2.27.2 + 2.44.0 2.11.0 1.10.1 - 3.0.1 2.9.1 4.12.0 4.10.0 @@ -53,7 +52,7 @@ 4.13.2 1.2.0 4.11.0 - 3.24.2 + 3.27.7 @@ -103,11 +102,6 @@ auto-value-annotations ${auto.version} - - com.google.code.findbugs - findbugs - ${findbugs.version} - com.google.code.gson gson @@ -193,7 +187,8 @@ - 1.5 + + 1.8 @@ -247,15 +242,6 @@ - - org.sonatype.central - central-publishing-maven-plugin - 0.9.0 - true - - central - - org.apache.maven.plugins maven-deploy-plugin @@ -315,7 +301,8 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.6.3 + + 3.12.0 attach-javadocs @@ -341,4 +328,24 @@ + + + + + release + + + + org.sonatype.central + central-publishing-maven-plugin + 0.9.0 + true + + central + + + + + +