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
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Tests

on:
push:
branches:
- main
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5

- name: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: "8.3"

- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Run tests
run: composer test
130 changes: 130 additions & 0 deletions .github/workflows/sdk_generation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: SDK Generation

# Generator: OpenAPI Generator (php) via scripts/generate.sh.
#
# Keeps the same workflow filename and dispatch inputs as the other SDK repos
# (convoy.js, convoy-python, convoy-java, convoy-go, convoy.rb) 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: Setup PHP
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: "8.3"

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

- name: Test regenerated client
# Never open a PR for a client that breaks the tests (verify +
# contract). PR CI (ci.yml) re-proves this on the PR itself.
run: |
composer install --prefer-dist --no-progress
composer 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 (php) from \`docs/v3/openapi3.yaml\` on frain-dev/convoy main. Hand-written SDK code (\`src/\` outside \`src/Client/\`, incl. webhook verify) is untouched by the sync script."
echo "Opened PR for $BRANCH" >> "$GITHUB_STEP_SUMMARY"
else
echo "Updated existing PR #$existing" >> "$GITHUB_STEP_SUMMARY"
fi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ psalm.xml
vendor
.php-cs-fixer.cache


# OpenAPI Generator cache (scripts/generate.sh)
.cache/
6 changes: 6 additions & 0 deletions .openapi-generator-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# OpenAPI Generator (php) config for the generated API client.
# The generated code lives under src/Client/ (namespace Convoy\Client, mapped
# by the existing PSR-4 rule "Convoy\" => src); the hand-written SDK code in
# the rest of src/ is not generated and never touched by scripts/generate.sh.
invokerPackage: "Convoy\\Client"
hideGenerationTimestamp: true
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,38 @@ To get started quickly,
composer require frain/convoy symfony/http-client nyholm/psr7
```

## Generated API client (`Convoy\Client`)

The `Convoy\Client` namespace (`src/Client/`) is generated from Convoy's
OpenAPI spec via [OpenAPI Generator](https://openapi-generator.tech/) and
covers the full `/api/v1` surface with typed models (Guzzle-based):

```php
use Convoy\Client\Api\EventsApi;
use Convoy\Client\Configuration;
use Convoy\Client\Model\ModelsCreateEvent;

$config = (new Configuration())
->setHost('https://us.getconvoy.cloud/api')
->setApiKeyPrefix('Authorization', 'Bearer')
->setApiKey('Authorization', $apiKey);

// Pin the API version this client was generated from.
$http = new \GuzzleHttp\Client([
'headers' => ['X-Convoy-Version' => '2025-11-24'],
]);

$events = new EventsApi($http, $config);
$events->createEndpointEvent($projectId, (new ModelsCreateEvent())
->setEndpointId('endpoint-id')
->setEventType('invoice.paid')
->setData(['amount' => 100, 'currency' => 'USD']));
```

Do not edit `src/Client/` by hand; regenerate with `./scripts/generate.sh`
(CI on `frain-dev/convoy` dispatches this when the spec changes). The
hand-written SDK below (incl. webhook verify) is never touched by generation.

### Setup Client

Set up the client with your instance URL, API key, and project ID. Both the API key and project ID are available from your **Project Settings** page.
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
}
],
"require": {
"php": "^7.3 || ^8.0",
"php": "^8.1",
"guzzlehttp/guzzle": "^7.3",
"guzzlehttp/psr7": "^1.7 || ^2.0",
"php-http/client-common": "^2.5",
"php-http/discovery": "^1.14",
"psr/http-client": "^1.0",
Expand Down
46 changes: 46 additions & 0 deletions scripts/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail

# Regenerate the API client from Convoy's OpenAPI spec with OpenAPI Generator
# (php), then sync it into src/Client/ without touching the hand-written SDK
# code (the rest of src/, incl. webhook verify).
#
# 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}"
# Official artifact checksum; the download is verified before execution so a
# compromised mirror/CDN cannot run arbitrary code in CI. Update alongside
# GENERATOR_VERSION (sha256 of the Maven Central JAR).
GENERATOR_SHA256="cb087e40001e31eb08ef6140dd5de10938dbeb89016a1fe0481eaa25cd569026"

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

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

# Fail closed on checksum mismatch (covers cached files too).
echo "${GENERATOR_SHA256} ${GENERATOR_JAR}" | shasum -a 256 -c - >/dev/null || {
echo "ERROR: ${GENERATOR_JAR} failed sha256 verification" >&2
exit 1
}

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

java -jar "$GENERATOR_JAR" generate \
-i "$tmp/openapi3.yaml" \
-g php \
-c .openapi-generator-config.yaml \
-o "$tmp/gen"

# Mirror only the generated namespace. --delete keeps src/Client an exact
# mirror of generator output; the hand-written src/ code is never touched.
rsync -a --delete "$tmp/gen/lib/" src/Client/

echo "Generated client synced into src/Client/"
Loading