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
338 changes: 338 additions & 0 deletions .api-sync/spec-map.json

Large diffs are not rendered by default.

162 changes: 161 additions & 1 deletion .api-sync/spec-snapshot.json

Large diffs are not rendered by default.

1,226 changes: 1,226 additions & 0 deletions .api-sync/sync.py

Large diffs are not rendered by default.

1,043 changes: 1,043 additions & 0 deletions .api-sync/unmodeled.json

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions .github/workflows/api-sync-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: API Sync Check

on:
workflow_call:

jobs:
api-sync-check:
name: Run deterministic spec-sync patcher checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Map validity (every spec-map.json entry resolves)
run: python3 .api-sync/sync.py --validate-map

- name: State reconciliation (--check)
run: python3 .api-sync/sync.py --check

- name: Determinism proof
run: |
set -e
rm -rf /tmp/api-sync-proof-a /tmp/api-sync-proof-b
cp -r . /tmp/api-sync-proof-a
cp -r . /tmp/api-sync-proof-b
# Re-applying the already-committed snapshot against itself must be
# a deterministic no-op in any generic CI context (no externally
# delivered spec-current.json is available here). The stronger
# proof -- that applying genuinely NEW drift twice from the same
# starting state produces byte-identical results -- is covered by
# tests/test_api_sync.py::TestDeterminism.
(cd /tmp/api-sync-proof-a && python3 .api-sync/sync.py --apply --spec .api-sync/spec-snapshot.json)
(cd /tmp/api-sync-proof-b && python3 .api-sync/sync.py --apply --spec .api-sync/spec-snapshot.json)
diff -rq /tmp/api-sync-proof-a /tmp/api-sync-proof-b
diff -rq . /tmp/api-sync-proof-a

- name: Coverage report (non-blocking)
continue-on-error: true
run: python3 .api-sync/sync.py --coverage --spec .api-sync/spec-snapshot.json

- name: Type audit (non-blocking)
continue-on-error: true
run: python3 .api-sync/sync.py --audit-types --spec .api-sync/spec-snapshot.json
135 changes: 73 additions & 62 deletions .github/workflows/api-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,22 @@ permissions:

jobs:
sync:
name: Sync SDK with API changes
name: Run deterministic spec-sync patcher
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Fetch API sync data
run: |
mkdir -p /tmp/api-sync
git fetch origin api-sync-data
git show origin/api-sync-data:.api-sync/changelog.md > /tmp/api-sync/changelog.md
echo "=== Changelog ==="
cat /tmp/api-sync/changelog.md
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Check for existing api-sync PR
id: check-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fetch the newly delivered spec from api-sync-data
run: |
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number // empty')
if [ -n "$PR_NUMBER" ]; then
echo "existing_pr=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "Found existing api-sync PR: #$PR_NUMBER"
else
echo "existing_pr=" >> $GITHUB_OUTPUT
echo "No existing api-sync PR found"
fi
git fetch origin api-sync-data
git show origin/api-sync-data:.api-sync/spec-current.json > .api-sync/spec-current.json

- name: Create or checkout api-sync branch
- name: Create or reset the api-sync branch from main
run: |
git fetch origin api-sync 2>/dev/null || true
if git rev-parse --verify origin/api-sync >/dev/null 2>&1; then
Expand All @@ -48,63 +36,86 @@ jobs:
git checkout -b api-sync
fi

- name: Apply changes with Claude Code
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
claude_args: '--model claude-opus-5 --allowedTools "Bash(*),Read,Edit,Write,Glob,Grep"'
prompt: |
You are updating this SDK to match API changes. Read CLAUDE.md for this SDK's conventions.

The changelog is at /tmp/api-sync/changelog.md — it contains ALL the information you need.
Do NOT read openapi.json. The changelog is self-contained.

INSTRUCTIONS:
1. Read CLAUDE.md thoroughly for this SDK's patterns and conventions.
2. Read /tmp/api-sync/changelog.md — it lists every enum type with every value, every field to add, and every change to make.
3. Implement ALL changes. Go through the changelog section by section:
- Section 1 (Enum Types): For each enum listed, check if the SDK has it. If missing, create it. If it exists, add any missing values. The changelog lists ALL values — add the ones that don't exist yet.
- Section 2 (Receiver Fields): Add each listed field to the receiver input/output types.
- Section 3 (Version): Minor bump.
4. Do not skip ANY enum or field. Every item in the changelog must be addressed.
5. Follow CLAUDE.md patterns exactly for naming, typing, and file organization.
6. MINOR version bump only.
7. Run lint and type check commands (see CLAUDE.md). Fix any errors until clean.
8. Do NOT create commits — just modify the files.
- name: Run the patcher
id: patch
continue-on-error: true
run: python3 .api-sync/sync.py --apply --report /tmp/api-sync-report.json

- name: Show report
if: always()
run: |
if [ -f /tmp/api-sync-report.json ]; then
cat /tmp/api-sync-report.json
else
echo "(no report written -- the patcher failed before producing one, see the 'Run the patcher' step)"
fi

- name: Print coverage report (non-blocking)
if: always()
run: python3 .api-sync/sync.py --coverage || true

- name: Fail loudly if the patcher could not apply cleanly
if: steps.patch.outcome == 'failure'
run: |
echo "::error::api-sync patcher could not apply the new spec cleanly. This needs a human: see the 'Run the patcher' and 'Show report' step output above for the exact NEEDS_HUMAN reason(s)."
exit 1

- name: Determine whether there is anything to commit
id: bump
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json

with open("/tmp/api-sync-report.json") as f:
report = json.load(f)
applied = report.get("applied", [])
bump = report.get("bump")
prefix = {"minor": "feat", "patch": "fix"}.get(bump, "chore")
print(f"has_changes={'true' if applied else 'false'}")
print(f"bump={bump or ''}")
print(f"prefix={prefix}")
PY

- name: Commit and push
id: commit
if: steps.bump.outputs.has_changes == 'true'
run: |
git remote set-url origin "https://x-access-token:${{ secrets.SDK_SYNC_PAT }}@github.com/${{ github.repository }}.git"
# Defense in depth: the patcher's own spec-map.json never points at
# .github/workflows, but never let the automated flow commit changes
# there even if something upstream of this step somehow produced one.
git checkout -- .github/workflows/ 2>/dev/null || true
git add -A
git reset HEAD .github/workflows/ 2>/dev/null || true
if git diff --staged --quiet; then
echo "No changes to commit"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "has_changes=true" >> $GITHUB_OUTPUT
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -m "feat: sync SDK with API changes"
git commit -m "${{ steps.bump.outputs.prefix }}: sync SDK with API changes"
git push --force-with-lease origin api-sync

- name: Create or update PR
if: steps.commit.outputs.has_changes == 'true'
- name: Check for existing api-sync PR
if: steps.bump.outputs.has_changes == 'true'
id: check-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number // empty')
echo "existing_pr=$PR_NUMBER" >> "$GITHUB_OUTPUT"

- name: Create or update PR and enable auto-merge
if: steps.bump.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }}
run: |
set -e
EXISTING_PR="${{ steps.check-pr.outputs.existing_pr }}"
TITLE="${{ steps.bump.outputs.prefix }}: sync SDK with API changes"
BODY="Automated, deterministic SDK sync (.api-sync/sync.py --apply). Bump: ${{ steps.bump.outputs.bump }}. See the workflow run's 'Show report' step for the exact applied changes."

if [ -n "$EXISTING_PR" ]; then
echo "Updating existing PR #$EXISTING_PR"
gh pr comment "$EXISTING_PR" --body "Updated with latest API changes."
gh pr edit "$EXISTING_PR" --title "$TITLE" --body "$BODY"
PR_NUMBER="$EXISTING_PR"
else
gh pr create \
--title "feat: sync SDK with API changes" \
--body "Automated SDK update from API changes." \
--base main \
--head api-sync \
--label api-sync
gh pr create --title "$TITLE" --body "$BODY" --base main --head api-sync --label api-sync
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number')
fi

gh pr merge "$PR_NUMBER" --auto --squash
5 changes: 5 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ jobs:
needs: [lint, typecheck]
uses: ./.github/workflows/contract-check.yaml

api-sync-check:
name: API Sync Check
needs: [lint, typecheck]
uses: ./.github/workflows/api-sync-check.yaml

snyk:
name: Snyk
needs: [lint, typecheck, tests]
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/release-auto-merge.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Release Auto-Merge

# Makes the release itself zero-touch: once a merged api-sync (or any other)
# PR lands a conventional-commit change on main, release-please-action (in
# publish.yaml) opens/updates its own "chore(main): release X.Y.Z" PR. That PR
# still has to be merged by a human today. This workflow enables GitHub's
# native auto-merge on it instead, so it merges itself as soon as the
# existing required checks (lint, typecheck, tests, contract-check,
# api-sync-check, snyk) pass -- nothing here bypasses those checks.
#
# Gating is intentionally strict and narrow: only a PR opened by the
# release-please bot itself (github-actions[bot]), carrying release-please's
# own "autorelease: pending" label. Both conditions must hold, so this never
# touches a human-authored PR or a release-please PR that already failed
# something (release-please clears the label / the PR gets closed in that
# case). Enabling auto-merge does not merge anything immediately; GitHub
# still waits for every required check to succeed first.

on:
pull_request:
types: [opened, labeled, synchronize, reopened]
branches: [main]

permissions:
pull-requests: write
contents: write

jobs:
auto-merge-release-pr:
name: Enable auto-merge on the release-please PR
runs-on: ubuntu-latest
if: >
github.event.pull_request.user.login == 'github-actions[bot]' &&
contains(github.event.pull_request.labels.*.name, 'autorelease: pending')
steps:
- name: Enable auto-merge
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr merge "${{ github.event.pull_request.number }}" --auto --squash --repo "${{ github.repository }}"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,7 @@ cython_debug/

# Snyk Security Extension - AI Rules (auto-generated)
.cursor/rules/snyk_rules.mdc

# api-sync: the newly-delivered spec is force-pushed to the api-sync-data branch
# and read by the workflow at run time; it is never committed to main.
.api-sync/spec-current.json
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"

[tool.hatch.version]
path = "src/blindpay/__init__.py"
path = "src/blindpay/_version.py"

[tool.hatch.build]
include = ["src/*", "README.md", "LICENSE", "py.typed"]
Expand Down
2 changes: 1 addition & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"include-v-in-tag": true,
"package-name": "blindpay",
"extra-files": [
"src/blindpay/__init__.py"
"src/blindpay/_version.py"
]
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/blindpay/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
__version__ = "3.0.0"

from ._internal.exceptions import BlindPayError
from ._version import __version__ as __version__
from .client import BlindPay, BlindPaySync
from .types import (
AccountClass,
Expand Down
1 change: 1 addition & 0 deletions src/blindpay/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "3.0.0"
3 changes: 1 addition & 2 deletions src/blindpay/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import httpx

from ._internal.exceptions import BlindPayError
from ._version import __version__
from .types import BlindpayApiResponse

if TYPE_CHECKING:
Expand Down Expand Up @@ -40,8 +41,6 @@
from blindpay.resources.wallets.offramp import OfframpWalletsResource, OfframpWalletsResourceSync
from blindpay.resources.webhooks.webhooks import WebhookEndpointsResource, WebhookEndpointsResourceSync

__version__ = "2.3.0"

T = TypeVar("T")


Expand Down
2 changes: 1 addition & 1 deletion src/blindpay/resources/payins/quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
StablecoinToken,
)

PaymentMethod = Literal["ach", "wire", "pix", "spei", "rtp", "ted"]
PaymentMethod = Literal["ach", "wire", "pix", "spei", "rtp", "ted", "international_swift", "pse", "transfers"]


class PayerRules(TypedDict, total=False):
Expand Down
3 changes: 2 additions & 1 deletion src/blindpay/resources/quotes/quotes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any, Dict, List, Optional

from typing_extensions import TypedDict
from typing_extensions import NotRequired, TypedDict

from ..._internal.api_client import InternalApiClient, InternalApiClientSync
from ...types import (
Expand Down Expand Up @@ -39,6 +39,7 @@ class CreateQuoteInput(TypedDict):
transaction_document_file: Optional[str]
transaction_document_id: Optional[str]
transaction_document_type: TransactionDocumentType
refund_wallet_address: NotRequired[Optional[str]]


class CreateQuoteResponse(TypedDict):
Expand Down
2 changes: 1 addition & 1 deletion src/blindpay/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ class TrackingPartnerFee(TypedDict):
"other",
]

BankingPartner = Literal["cfsb", "citi", "hsbc", "jpmorgan"]
BankingPartner = Literal["cfsb", "citi", "hsbc", "jpmorgan", "portage"]

PaymentMethod = Literal["ach", "wire", "pix", "spei", "transfers", "pse", "international_swift", "rtp", "ted"]

Expand Down
Loading
Loading