diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml new file mode 100644 index 0000000..b3c4ae0 --- /dev/null +++ b/.github/workflows/regen.yml @@ -0,0 +1,170 @@ +name: Regenerate SDK + +# Canonical SDK-regen workflow. Copy this file into each SDK repo at +# `.github/workflows/regen.yml`; the only per-SDK customization needed is +# inserting a language toolchain step (e.g. setup-python / setup-go / etc.) +# below the "Toolchain (language SDK)" marker. +# +# Triggers: +# workflow_dispatch — manual run with optional inputs +# repository_dispatch — fired from goodsender-web after spec changes +# (event type: regen-sdk; +# client_payload: spec_url, spec_tag, clean, generator_version) + +on: + workflow_dispatch: + inputs: + spec_url: + description: "URL to download bundled openapi spec from. If empty, regenerate from openapi/goodsender.yaml in repo." + required: false + type: string + clean: + description: "Pass --clean to scripts/regen.sh (full wipe before regen)." + required: false + type: boolean + default: false + generator_version: + description: "Override openapi-generator JAR version (default 7.21.0 from scripts/regen.sh)." + required: false + type: string + repository_dispatch: + types: [regen-sdk] + # Test trigger: pushes to any `ci/**` branch run this workflow on the branch's code, + # so regen + tests + PR creation can be smoke-tested before merging to main. + push: + branches: + - 'ci/**' + +permissions: + contents: write + pull-requests: write + +jobs: + regen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + # --- Toolchain (regen driver): Node runs the openapi-generator-cli wrapper + - uses: actions/setup-node@v4 + with: + node-version: "20" + + # --- Toolchain (regen driver): Java runs the openapi-generator JAR + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + # --- Toolchain (language SDK): inserted per repo (Python / Go / Ruby / etc.) + # See per-SDK task in docs/superpowers/plans/2026-05-19-sdk-regen-automation.md + - uses: shivammathur/setup-php@v2 + with: + php-version: "8.3" + tools: composer:v2 + coverage: none + + - name: Cache openapi-generator JAR + npm + uses: actions/cache@v4 + with: + path: | + ~/.openapi-generator + ~/.npm + key: openapi-gen-${{ runner.os }}-${{ hashFiles('openapitools.json') }} + + - name: Resolve inputs + id: in + env: + DISPATCH_SPEC_URL: ${{ github.event.inputs.spec_url }} + DISPATCH_CLEAN: ${{ github.event.inputs.clean }} + DISPATCH_GEN_VER: ${{ github.event.inputs.generator_version }} + PAYLOAD_SPEC_URL: ${{ github.event.client_payload.spec_url }} + PAYLOAD_SPEC_TAG: ${{ github.event.client_payload.spec_tag }} + PAYLOAD_CLEAN: ${{ github.event.client_payload.clean }} + PAYLOAD_GEN_VER: ${{ github.event.client_payload.generator_version }} + run: | + set -euo pipefail + spec_url="${DISPATCH_SPEC_URL:-${PAYLOAD_SPEC_URL:-}}" + spec_tag="${PAYLOAD_SPEC_TAG:-}" + clean="${DISPATCH_CLEAN:-${PAYLOAD_CLEAN:-false}}" + gen_ver="${DISPATCH_GEN_VER:-${PAYLOAD_GEN_VER:-}}" + [[ "$clean" != "true" ]] && clean="false" + { + echo "spec_url<> "$GITHUB_OUTPUT" + + - name: Update spec from URL + if: steps.in.outputs.spec_url != '' + env: + SPEC_URL: ${{ steps.in.outputs.spec_url }} + run: | + set -euo pipefail + curl --fail --location --show-error --silent "$SPEC_URL" -o openapi/goodsender.yaml + + - name: Regenerate + env: + OPENAPI_GENERATOR_VERSION: ${{ steps.in.outputs.gen_ver }} + CLEAN: ${{ steps.in.outputs.clean }} + run: | + set -euo pipefail + if [[ "$CLEAN" == "true" ]]; then + bash scripts/regen.sh --clean + else + bash scripts/regen.sh + fi + + - name: Run conformance tests (mock) + id: tests + continue-on-error: true + run: bash tests/run.sh mock + + - name: Detect changes + id: diff + run: | + if [[ -z "$(git status --porcelain)" ]]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + git --no-pager diff --stat | tee -a "$GITHUB_STEP_SUMMARY" + fi + + - name: Mint PR-creation token from GitHub App + if: steps.diff.outputs.changed == 'true' + id: pr_token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.SDK_DISPATCH_APP_ID }} + private-key: ${{ secrets.SDK_DISPATCH_APP_PRIVATE_KEY }} + + - name: Open PR + if: steps.diff.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ steps.pr_token.outputs.token }} + commit-message: "chore: regenerate SDK from openapi spec ${{ steps.in.outputs.spec_tag || 'in-repo' }}" + title: "chore: regenerate SDK from openapi spec ${{ steps.in.outputs.spec_tag || 'in-repo' }}" + body: | + Automated regeneration via `.github/workflows/regen.yml`. + + - Trigger: `${{ github.event_name }}` + - Spec tag: `${{ steps.in.outputs.spec_tag || '(none — in-repo spec used)' }}` + - Spec URL: `${{ steps.in.outputs.spec_url || '(none)' }}` + - Clean: `${{ steps.in.outputs.clean }}` + - Generator version: `${{ steps.in.outputs.gen_ver || '(default from scripts/regen.sh)' }}` + - Conformance tests (`tests/run.sh mock`): **${{ steps.tests.outcome }}** + branch: auto/regen + delete-branch: true + labels: automated, regen + + - name: Fail workflow if conformance tests failed + if: always() && steps.tests.outcome == 'failure' + run: | + echo "Conformance tests failed — see the 'Run conformance tests (mock)' step above." + echo "The regen PR (if any diff existed) has still been opened so the change is visible for review." + echo "This workflow run is marked failed so the failure surfaces on the PR's checks list." + exit 1 diff --git a/.regen-ignore b/.regen-ignore new file mode 100644 index 0000000..a0a000a --- /dev/null +++ b/.regen-ignore @@ -0,0 +1,21 @@ +# Files preserved across regenerations. +# scripts/regen.sh consumes this file in two ways: +# 1. Filtered (comments + blank lines stripped) and written to +# .openapi-generator-ignore, which openapi-generator reads to know +# which paths it must NOT overwrite. +# 2. The first path component of every pattern is added to --clean's +# preserve list, so `regen.sh --clean` will not wipe these top-level +# directories or files. +# +# Patterns are gitignore-style. Lines starting with `#` and blank lines are +# treated as comments. + +LICENSE +CHANGELOG.md +README.md +tests/** +scripts/** +openapi/** +openapitools.json +.github/** +.regen-ignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9dfc0f --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# GoodSender SDK for PHP + +Official client library for the GoodSender email API. Package: `good-sender/php-sdk` + +## Installation + +```bash +composer require good-sender/php-sdk +``` + +## Quick start + +```php +setHost('https://api.goodsender.com') + ->setAccessToken('YOUR_API_KEY'); + +$emails = new EmailsApi(new Client(), $config); +$domains = new DomainsApi(new Client(), $config); + +$req = new SendEmailRequest(['emails' => [ + new SendEmail([ + 'from' => new Address(['email' => 'sender@example.com']), + 'to' => [new Address(['email' => 'recipient@example.com'])], + 'subject' => 'Hello', + 'text_content' => 'Body', + ]), +]]); +$res = $emails->sendEmail($req); +echo "sent={$res->getSent()} declined={$res->getDeclined()}\n"; +``` + +## Examples + +### Send via a template + +```php +use GoodSender\Model\TemplateEmailRequest; +use GoodSender\Model\TemplateEmailRequestTemplate; + +$req = new TemplateEmailRequest([ + 'from' => new Address(['email' => 'sender@example.com']), + 'to' => new Address(['email' => 'recipient@example.com']), + 'subject' => 'Your OTP', + 'template' => new TemplateEmailRequestTemplate([ + 'template_id' => 'otp_code', + 'variables' => ['code' => '123456'], + ]), +]); +$res = $emails->sendTemplateEmail($req); +echo 'status=' . $res->getStatus() . "\n"; +``` + +### List domains + +```php +$res = $domains->listDomains(50); +echo 'domains=' . count($res->getDomains()) . "\n"; +``` + +### Check consent status + +```php +$res = $emails->getEmailConsentStatus('user@example.com', 'example.com'); +echo 'entries=' . count($res) . "\n"; + +// List all consents for a domain +$res = $emails->listEmailConsents('example.com', 50); +echo 'emails=' . count($res->getEmails() ?? []) . "\n"; +``` + +## Documentation + +- API reference: +- OpenAPI spec: `openapi/goodsender.yaml` in this repo +- Conformance tests: `tests/` + +## Development + +- Regenerate from spec: `scripts/regen.sh` (preserves `tests/`, `.github/`, and hand-curated files per `.regen-ignore`) +- Run conformance tests against local mock: `tests/run.sh mock` +- Run conformance against real dev API: `tests/run.sh dev` (requires `tests/.env.dev`) + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/scripts/regen.sh b/scripts/regen.sh index 082e572..fcdba5d 100755 --- a/scripts/regen.sh +++ b/scripts/regen.sh @@ -34,31 +34,27 @@ export OPENAPI_GENERATOR_VERSION="${OPENAPI_GENERATOR_VERSION:-7.21.0}" [[ -f "$SPEC" ]] || { echo "!! spec not found at $SPEC" >&2; exit 1; } [[ -f "$CONFIG" ]] || { echo "!! generator config not found at $CONFIG" >&2; exit 1; } -# --- .openapi-generator-ignore: protect hand-curated files from being overwritten -cat > "$SDK_DIR/.openapi-generator-ignore" <<'IGNORE_EOF' -LICENSE -CHANGELOG.md -README.md -tests/** -scripts/** -openapi/** -openapitools.json -IGNORE_EOF +# --- Sync .openapi-generator-ignore from .regen-ignore +# .regen-ignore is the single source of truth for preserved paths. We filter +# comments + blanks and write the result to .openapi-generator-ignore (which +# openapi-generator reads). The same patterns drive --clean's preserve list. +REGEN_IGNORE="$SDK_DIR/.regen-ignore" +[[ -f "$REGEN_IGNORE" ]] || { echo "!! .regen-ignore not found at $REGEN_IGNORE" >&2; exit 1; } +grep -v '^[[:space:]]*#' "$REGEN_IGNORE" | grep -v '^[[:space:]]*$' > "$SDK_DIR/.openapi-generator-ignore" -# --- Optional clean step: wipe SDK output (preserves hand-curated files only) +# --- Optional clean step: wipe SDK output (preserves entries in .regen-ignore) if (( CLEAN == 1 )); then - echo ">> --clean: wiping $SDK_DIR (preserving LICENSE, CHANGELOG.md, README.md, tests/, scripts/, openapi/, openapitools.json)" - find "$SDK_DIR" -mindepth 1 -maxdepth 1 \ - ! -name LICENSE \ - ! -name CHANGELOG.md \ - ! -name README.md \ - ! -name tests \ - ! -name scripts \ - ! -name openapi \ - ! -name openapitools.json \ - ! -name .git \ - ! -name .gitignore \ - -exec rm -rf {} + + preserve_args=() + while IFS= read -r line; do + line="${line%%#*}" + line="$(echo "$line" | xargs)" + [[ -z "$line" ]] && continue + top="${line%%/*}" + preserve_args+=(! -name "$top") + done < "$REGEN_IGNORE" + preserve_args+=(! -name .git ! -name .gitignore) + echo ">> --clean: wiping $SDK_DIR (preserving entries from .regen-ignore)" + find "$SDK_DIR" -mindepth 1 -maxdepth 1 "${preserve_args[@]}" -exec rm -rf {} + fi # --- Regen via openapi-generator-cli