From 7a256def558266de33be2517b7a3a8a8b21b0790 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 13:47:13 +0300 Subject: [PATCH 01/12] ci: add SDK regeneration workflow --- .github/workflows/regen.yml | 141 ++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 .github/workflows/regen.yml diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml new file mode 100644 index 0000000..faeefee --- /dev/null +++ b/.github/workflows/regen.yml @@ -0,0 +1,141 @@ +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] + +permissions: + contents: write + pull-requests: write + +jobs: + regen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # --- 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 + + - 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: Open PR + if: steps.diff.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v6 + with: + 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-${{ github.run_id }} + delete-branch: true + labels: automated, regen From ccda2ca9008c7990481c14df1c4635d47043a837 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 20:36:09 +0300 Subject: [PATCH 02/12] ci: allow workflow to fire on push to ci/** branches --- .github/workflows/regen.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index faeefee..2c1b42c 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -29,6 +29,11 @@ on: 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 From 7eb8988938a6331df4758ac2e47707ee4a31f25d Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 20:58:23 +0300 Subject: [PATCH 03/12] ci: re-trigger workflow after enabling PR permission From 379cbdb04f9e1aa1d839ae280843ad29d119e1e2 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 08:28:25 +0300 Subject: [PATCH 04/12] ci: open regen PRs via GitHub App token instead of GITHUB_TOKEN --- .github/workflows/regen.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index 2c1b42c..bb10b86 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -126,10 +126,19 @@ jobs: 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: | From 216d09a569d923f89a7e9f11417927382083d58f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 08:39:44 +0300 Subject: [PATCH 05/12] ci: re-trigger after org secrets in place From f4b5c3b083665ca4cf02796ee288e4ab98f4200d Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 10:15:55 +0300 Subject: [PATCH 06/12] ci: introduce .regen-ignore as single source of truth for preserved paths --- .regen-ignore | 21 +++++++++++++++++++++ scripts/regen.sh | 42 +++++++++++++++++++----------------------- 2 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 .regen-ignore 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/scripts/regen.sh b/scripts/regen.sh index fea252b..f892897 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 From ecc96d2e8bb7c119357328444854a67b38df06b5 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 11:29:00 +0300 Subject: [PATCH 07/12] ci: fail workflow when conformance tests fail (gate for backward-compat) --- .github/workflows/regen.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index bb10b86..c86ed15 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -153,3 +153,11 @@ jobs: branch: auto/regen-${{ github.run_id }} 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 From 53d8d54552b1e05e8887445dc2a510c4615f9b11 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 12:34:22 +0300 Subject: [PATCH 08/12] ci: use stable auto/regen branch so regen PR is updated in place --- .github/workflows/regen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index c86ed15..a7e4720 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -150,7 +150,7 @@ jobs: - 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-${{ github.run_id }} + branch: auto/regen delete-branch: true labels: automated, regen From 923945d2ee9bab4894258fb5a706217e9a225cd9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 12:44:28 +0300 Subject: [PATCH 09/12] docs: rewrite README with concise install + usage examples --- README.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fa3345 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# GoodSender SDK for Node.js / TypeScript + +Official client library for the GoodSender email API. Package: `@goodsender/sdk` + +## Installation + +```bash +npm install @goodsender/sdk +``` + +## Quick start + +```typescript +import { Configuration, EmailsApi, DomainsApi } from "@goodsender/sdk"; + +const config = new Configuration({ + basePath: "https://api.goodsender.com", + accessToken: "YOUR_API_KEY", +}); +const emails = new EmailsApi(config); +const domains = new DomainsApi(config); + +const res = await emails.sendEmail({ + sendEmailRequest: { + emails: [ + { + from: { email: "sender@example.com", name: "Sender" }, + to: [{ email: "recipient@example.com", name: "Recipient" }], + subject: "Hello", + text_content: "Body", + }, + ], + }, +}); +console.log(`sent=${res.data.sent} declined=${res.data.declined}`); +``` + +## Examples + +### Send via a template + +```typescript +const res = await emails.sendTemplateEmail({ + templateEmailRequest: { + from: { email: "sender@example.com", name: "Sender" }, + to: { email: "recipient@example.com", name: "Recipient" }, + subject: "Your OTP", + template: { template_id: "otp_code", variables: { code: "123456" } }, + }, +}); +console.log(`status=${res.data.status}`); +``` + +### List domains + +```typescript +const res = await domains.listDomains({ limit: 50 }); +console.log(`domains=${res.data.domains.length}`); +``` + +### Check consent status + +```typescript +const res = await emails.getEmailConsentStatus({ email: "user@example.com" }); +console.log(`entries=${res.data.length}`); + +// List all consents for a domain +const list = await emails.listEmailConsents({ domain: "example.com", limit: 50 }); +console.log(`emails=${list.data.emails?.length ?? 0}`); +``` + +## 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). From a910e8a152734b7c971832033dfdcfc3fbaa0622 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 12:47:27 +0300 Subject: [PATCH 10/12] ci: bump actions/checkout to v6 --- .github/workflows/regen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index a7e4720..e7a15cc 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -43,7 +43,7 @@ jobs: regen: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # --- Toolchain (regen driver): Node runs the openapi-generator-cli wrapper - uses: actions/setup-node@v4 From 54e4404c3dcdb71c95061e6a5ab0309c07cc92cf Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 20 May 2026 12:51:52 +0300 Subject: [PATCH 11/12] ci: disable checkout credential persistence (avoid duplicate Authorization header) --- .github/workflows/regen.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/regen.yml b/.github/workflows/regen.yml index e7a15cc..138d36c 100644 --- a/.github/workflows/regen.yml +++ b/.github/workflows/regen.yml @@ -44,6 +44,8 @@ jobs: 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 From 28990130b0ec93ce45ef70ab64102df5072491ad Mon Sep 17 00:00:00 2001 From: "gs-sdk[bot]" <285988722+gs-sdk[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 13:04:02 +0300 Subject: [PATCH 12/12] chore: regenerate SDK from openapi spec in-repo (#4) --- .openapi-generator-ignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.openapi-generator-ignore b/.openapi-generator-ignore index 4c18fee..a40a523 100644 --- a/.openapi-generator-ignore +++ b/.openapi-generator-ignore @@ -5,3 +5,5 @@ tests/** scripts/** openapi/** openapitools.json +.github/** +.regen-ignore