Skip to content
Merged
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
123 changes: 123 additions & 0 deletions .github/workflows/sdk_generation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: SDK Generation

# Generator: OpenAPI Generator (java, native library) via scripts/generate.sh.
#
# Keeps the same workflow filename and dispatch inputs as the other SDK repos
# (convoy.js, convoy-python) so the frain-dev/convoy dispatcher
# (speakeasy-sdk.yml) works unchanged.

on:
workflow_dispatch:
inputs:
force:
# Accepted for dispatcher compatibility. Generation is deterministic
# from the spec, so there is nothing to force: no diff means no PR.
description: Accepted for compatibility; regeneration is always run
required: false
default: "false"
type: string
feature_branch:
description: Branch for SDK changes
required: false
type: string
schedule:
- cron: "0 6 * * 1"

# Serialize generations: overlapping cron/dispatch runs race on the same
# branch/PR. Queue instead of cancel so a triggered regen is never dropped.
concurrency:
group: sdk-generation
cancel-in-progress: false

permissions:
contents: write
pull-requests: write

jobs:
generate:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
# Regen branches are always derived from main: the spec is pulled
# from convoy main, so a dispatch from another ref must not silently
# commit generated output onto that ref's base.
ref: main
# Prefer a PAT: PRs opened with GITHUB_TOKEN do not trigger
# pull_request workflows, so verify CI would never run on them.
token: ${{ secrets.SDK_BOT_PAT || secrets.GITHUB_TOKEN }}

- name: Setup Java
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
distribution: temurin
java-version: "17"

- name: Regenerate client
run: ./scripts/generate.sh

- name: Compile and test regenerated client
# Never open a PR for a client that does not compile or breaks the
# verify tests. PR CI (ci.yml) re-proves this on the PR itself.
run: ./gradlew test

- name: Detect changes
id: diff
run: |
if git diff --quiet && [ -z "$(git status --porcelain)" ]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "No client changes; skipping PR." >> "$GITHUB_STEP_SUMMARY"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Prepare feature branch
if: steps.diff.outputs.changed == 'true'
id: branch
env:
# Never interpolate free-form dispatch inputs into run: directly.
FEATURE_BRANCH_INPUT: ${{ inputs.feature_branch }}
run: |
if [ -n "$FEATURE_BRANCH_INPUT" ]; then
# SDK PRs must come from a reviewable feature branch, never a
# protected ref or an option-looking / metacharacter name.
# refs/* is blocked too: "refs/heads/main" would bypass the
# literal main check and force-push the default branch.
case "$FEATURE_BRANCH_INPUT" in
main|master|release/*|refs/*|-*|*[!a-zA-Z0-9._/-]*)
echo "::error::Invalid feature_branch '$FEATURE_BRANCH_INPUT'"
exit 1
;;
esac
echo "name=$FEATURE_BRANCH_INPUT" >> "$GITHUB_OUTPUT"
else
echo "name=sdk-regen-$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT"
fi

- name: Push branch and open PR
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.SDK_BOT_PAT || secrets.GITHUB_TOKEN }}
BRANCH: ${{ steps.branch.outputs.name }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git checkout -B "$BRANCH"
git add -A
git commit -m "feat: regenerate API client from OpenAPI spec"
# Force push is safe: the regen branch is fully derived from main
# plus this deterministic generation; any previous content is stale.
git push --force origin "$BRANCH"

existing=$(gh pr list --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
if [ -z "$existing" ]; then
gh pr create \
--head "$BRANCH" \
--title "feat: regenerate API client from OpenAPI spec" \
--body "Automated regeneration via OpenAPI Generator (java/native) from \`docs/v3/openapi3.yaml\` on frain-dev/convoy main. Hand-written webhook verify (\`src/main/java/com/getconvoy/webhook/\`) is untouched by the sync script."
echo "Opened PR for $BRANCH" >> "$GITHUB_STEP_SUMMARY"
else
echo "Updated existing PR #$existing" >> "$GITHUB_STEP_SUMMARY"
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ bin/
local.properties
*.gpg
*.asc
.cache/
49 changes: 42 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
# Convoy Java SDK

Official Convoy SDK for Java.

> Status: this initial release provides **webhook signature verification**. The
> generated API client will follow once the OpenAPI spec covers the full API
> surface.
Official Convoy SDK for Java: **webhook signature verification** (hand-written)
and an **API client** generated from Convoy's OpenAPI spec via
[OpenAPI Generator](https://openapi-generator.tech/) (java, `native` library —
JDK `java.net.http`, Jackson).

## Install

Gradle (Kotlin DSL):

```kotlin
dependencies {
implementation("com.getconvoy:convoy:0.1.0")
implementation("com.getconvoy:convoy:0.2.0")
}
```

Expand All @@ -22,10 +21,32 @@ Maven:
<dependency>
<groupId>com.getconvoy</groupId>
<artifactId>convoy</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
</dependency>
```

## Use the API client

```java
import com.getconvoy.api.EventsApi;
import com.getconvoy.client.ApiClient;
import com.getconvoy.models.ModelsCreateEvent;

import java.util.Map;

ApiClient client = new ApiClient();
// Default base URI is https://us.getconvoy.cloud/api; point elsewhere for
// EU cloud or self-hosted instances.
client.updateBaseUri("https://us.getconvoy.cloud/api");
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + apiKey));

EventsApi events = new EventsApi(client);
events.createEndpointEvent("project-id", new ModelsCreateEvent()
.endpointId("endpoint-id")
.eventType("invoice.paid")
.data(Map.of("amount", 100, "currency", "USD")));
```

## Verify webhook signatures

Verify with the raw request body, before parsing it. `verify` returns `true`
Expand Down Expand Up @@ -64,6 +85,20 @@ new Webhook("endpoint-secret", "SHA512", "base64", 300);
Tests verify against `signature-vectors.json`, a shared cross-SDK vector set
generated from the server signing code so every Convoy SDK verifies identically.

### Regenerating the API client

The client (`com.getconvoy.api`, `com.getconvoy.client`, `com.getconvoy.models`)
is generated; do not edit it by hand. The hand-written verify package
(`com.getconvoy.webhook`) is never touched by generation.

CI on `frain-dev/convoy` dispatches `sdk_generation.yaml` when OpenAPI
artifacts change. Locally:

```bash
./scripts/generate.sh # requires java 17+, curl, rsync
./gradlew test
```

## License

[MIT](LICENSE)
14 changes: 12 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ plugins {
}

group = "com.getconvoy"
version = "0.1.0"
description = "Convoy SDK for Java. Currently provides webhook signature verification."
version = "0.2.0"
description = "Convoy SDK for Java: webhook signature verification and an OpenAPI-generated API client."

repositories {
mavenCentral()
Expand All @@ -21,6 +21,16 @@ java {
}

dependencies {
// Generated API client (com.getconvoy.{api,client,models}); versions must
// match what OpenAPI Generator (pinned in scripts/generate.sh) targets.
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.fasterxml.jackson.core:jackson-core:2.21.1")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.21")
implementation("com.fasterxml.jackson.core:jackson-databind:2.21.1")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.1")
implementation("org.openapitools:jackson-databind-nullable:0.2.10")
implementation("jakarta.annotation:jakarta.annotation-api:2.1.1")

testImplementation(platform("org.junit:junit-bom:5.11.3"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("com.google.code.gson:gson:2.11.0")
Expand Down
43 changes: 43 additions & 0 deletions scripts/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail

# Regenerate the API client from Convoy's OpenAPI spec with OpenAPI Generator
# (java, native library), then sync it into src/main/java/com/getconvoy/
# without touching the hand-written webhook verify package.
#
# Requires: java 17+, rsync, curl. Run from the repo root.

SPEC_URL="${SPEC_URL:-https://raw.githubusercontent.com/frain-dev/convoy/main/docs/v3/openapi3.yaml}"
# Pin so regeneration output is reproducible; bump deliberately.
GENERATOR_VERSION="7.23.0"
GENERATOR_JAR="${GENERATOR_JAR:-.cache/openapi-generator-cli-${GENERATOR_VERSION}.jar}"

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

if [ ! -f "$GENERATOR_JAR" ]; then
mkdir -p "$(dirname "$GENERATOR_JAR")"
curl -fsSL -o "$GENERATOR_JAR" \
"https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${GENERATOR_VERSION}/openapi-generator-cli-${GENERATOR_VERSION}.jar"
fi

curl -fsSL "$SPEC_URL" -o "$tmp/openapi3.yaml"

# hideGenerationTimestamp: @Generated annotations must not embed a wall-clock
# date or every regeneration diffs all 200+ files and "no diff, no PR" breaks.
java -jar "$GENERATOR_JAR" generate \
-i "$tmp/openapi3.yaml" \
-g java \
--library native \
-o "$tmp/gen" \
--additional-properties=groupId=com.getconvoy,artifactId=convoy,apiPackage=com.getconvoy.api,modelPackage=com.getconvoy.models,invokerPackage=com.getconvoy.client,useJakartaEe=true,hideGenerationTimestamp=true

# Mirror only the three generated packages. The hand-written verify package
# (com/getconvoy/webhook) is a sibling and is never created or removed here.
# --delete keeps each generated package an exact mirror of generator output.
for pkg in api client models; do
rsync -a --delete "$tmp/gen/src/main/java/com/getconvoy/$pkg/" \
"src/main/java/com/getconvoy/$pkg/"
done

echo "Generated client synced into src/main/java/com/getconvoy/{api,client,models}"
Loading
Loading