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
1,863 changes: 1,863 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.

908 changes: 908 additions & 0 deletions .api-sync/unmodeled.json

Large diffs are not rendered by default.

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

on:
workflow_call:

jobs:
api-sync-check:
name: Verify SDK sync state against the spec
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.18

- name: Install dependencies
run: bun install --frozen-lockfile

# State reconciliation against the committed snapshot (the only spec guaranteed present on
# a plain checkout): exits 1 with a precise message for any pending drift or needs-human
# finding, silent (besides a one-line OK) when clean. Also IS the map-validity check: every
# spec-map.json entry must resolve (schema/property/path locator against the snapshot,
# file + symbol against the source tree) before any drift can even be computed.
- name: sync:check (state + map validity)
run: bun scripts/api-sync/index.ts --check --spec .api-sync/spec-snapshot.json

# Determinism: apply the same input into two independent worktrees and diff. No
# timestamps, no randomness, no map-iteration-order leaks; running twice must be
# byte-identical. Uses the committed snapshot as its own input, so this proof needs
# nothing external to run.
- name: Determinism proof
run: |
set -e
git worktree add --detach /tmp/api-sync-det-a HEAD
git worktree add --detach /tmp/api-sync-det-b HEAD
(cd /tmp/api-sync-det-a && bun install --frozen-lockfile --silent && bun scripts/api-sync/index.ts --apply --spec .api-sync/spec-snapshot.json)
(cd /tmp/api-sync-det-b && bun install --frozen-lockfile --silent && bun scripts/api-sync/index.ts --apply --spec .api-sync/spec-snapshot.json)
diff -rq /tmp/api-sync-det-a /tmp/api-sync-det-b \
--exclude=node_modules --exclude=.git --exclude=bun.lock
git worktree remove --force /tmp/api-sync-det-a
git worktree remove --force /tmp/api-sync-det-b

# Non-blocking, printed on every run: spec operations with no recognizable SDK endpoint
# string (known real gaps: the Rfi family, POST /v1/upload/analyze), and the full
# state-based type audit including nullability, which sync:check deliberately does not
# enforce (see reconcile.ts). Never gates CI; a human triages this list in Phase C.
- name: Coverage + type-audit report (non-blocking)
if: always()
run: |
echo "=== Coverage gaps (spec operations with no recognizable SDK endpoint string) ==="
bun run sync:coverage
echo
echo "=== Type audit (informational only; see PayinOut.billing_fee_amount etc.) ==="
bun run audit:types
137 changes: 80 additions & 57 deletions .github/workflows/api-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Fetch API sync data
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.18

- name: Fetch the delivered spec
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
mkdir -p .api-sync
git show origin/api-sync-data:.api-sync/spec-current.json > .api-sync/spec-current.json

- name: Check for existing api-sync PR
id: check-pr
Expand All @@ -31,10 +33,10 @@ jobs:
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 "existing_pr=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "Found existing api-sync PR: #$PR_NUMBER"
else
echo "existing_pr=" >> $GITHUB_OUTPUT
echo "existing_pr=" >> "$GITHUB_OUTPUT"
echo "No existing api-sync PR found"
fi

Expand All @@ -48,70 +50,91 @@ 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, in this order:
bun run lint:fix # auto-fix formatter issues (trailing newlines, etc.)
bun run check-types # type-check
bun run lint:check # validate (must report 0 errors)
bun run test # run the test suite
Fix any errors until all four are clean. Running only
`lint:check` will surface formatter diffs as failures —
always run `lint:fix` first.
8. Do NOT create commits — just modify the files.
- name: Install dependencies
run: bun install --frozen-lockfile

# The entire "what changed, and can this repo express it without a human" decision is a
# pure, deterministic script: scripts/api-sync/. It reconciles the SDK's declared surface
# against the delivered spec state (not an event diff — see reconcile.ts for why), applies
# only enum-member and optional-field additions, and hard-fails on anything else: a
# removal, a required-ness or type change, a new operation or schema, or a mapped
# symbol/anchor it can no longer find. No LLM, no best-guessing.
- name: Run the deterministic patcher
id: sync
run: |
set +e
bun scripts/api-sync/index.ts --apply --spec .api-sync/spec-current.json --report /tmp/sync-report.json
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
set -e
echo "bump=$(jq -r '.bump' /tmp/sync-report.json)" >> "$GITHUB_OUTPUT"
echo "new_version=$(jq -r '.newVersion // empty' /tmp/sync-report.json)" >> "$GITHUB_OUTPUT"
echo "applied_count=$(jq -r '.applied | length' /tmp/sync-report.json)" >> "$GITHUB_OUTPUT"

# A precise, actionable failure (removal, type change, new operation/schema, missing
# anchor) fails the run loudly instead of silently applying nothing. If the repo also has
# an alerting hook wired to failed runs of this workflow, that is what pages a human.
- name: Fail loudly on a needs-human finding
if: steps.sync.outputs.exit_code != '0'
run: |
echo "::error::api-sync found change(s) it cannot apply automatically. See needsHuman below."
jq -r '.needsHuman[] | "- " + .' /tmp/sync-report.json
exit 1

- name: Exit quietly when there is nothing to apply
if: steps.sync.outputs.applied_count == '0'
run: echo "No pending drift; nothing to sync."

- name: Commit and push
id: commit
if: steps.sync.outputs.applied_count != '0'
run: |
git remote set-url origin "https://x-access-token:${{ secrets.SDK_SYNC_PAT }}@github.com/${{ github.repository }}.git"
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 add -A
git commit -m "$(printf 'feat: sync SDK with API changes (v%s)\n\nApplied:\n%s\n' \
"${{ steps.sync.outputs.new_version }}" \
"$(jq -r '.applied[] | "- " + .' /tmp/sync-report.json)")"
git push --force-with-lease origin api-sync

# The PAT, not GITHUB_TOKEN: a merge performed with GITHUB_TOKEN produces a
# push that starts no workflow, so the sync would land on main and never
# publish to npm.
- name: Create or update PR
if: steps.commit.outputs.has_changes == 'true'
if: steps.commit.outcome == 'success'
env:
GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }}
run: |
BUMP="${{ steps.sync.outputs.bump }}"
VERSION="${{ steps.sync.outputs.new_version }}"
if [ "$BUMP" = "minor" ]; then
PREFIX="feat"
else
PREFIX="fix"
fi
TITLE="${PREFIX}: sync SDK with API changes (v${VERSION})"
EXISTING_PR="${{ steps.check-pr.outputs.existing_pr }}"

{
echo "Automated, script-generated SDK update from an API spec change."
echo "No LLM was involved in producing this diff; see scripts/api-sync/ in this repo."
echo
echo "### Applied"
jq -r '.applied[] | "- " + .' /tmp/sync-report.json
echo
echo "### Known divergences (recorded, not applied)"
jq -r '.knownDivergences[] | "- " + .' /tmp/sync-report.json
echo
echo "Version bump: \`${BUMP}\` -> v${VERSION}"
} > /tmp/pr-body.md

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-file /tmp/pr-body.md
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-file /tmp/pr-body.md --base main --head api-sync --label api-sync
PR_NUMBER=$(gh pr list --head api-sync --json number --jq '.[0].number')
fi

# Enable auto-merge so the required checks (sync:check, determinism, tests, lint,
# contract-check, etc.) are the only gate; a needs-human run never reaches this step.
gh pr merge "$PR_NUMBER" --auto --squash
2 changes: 2 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
uses: ./.github/workflows/lint.yaml
contract-check:
uses: ./.github/workflows/contract-check.yaml
api-sync-check:
uses: ./.github/workflows/api-sync-check.yaml
type-checking:
uses: ./.github/workflows/type-checking.yaml
needs: [lint]
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ dist

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

# api-sync: delivered fresh each CI run from the api-sync-data branch, never committed
.api-sync/spec-current.json
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blindpay/node",
"version": "5.0.1",
"version": "5.1.0",
"description": "Official Node.js SDK for Blindpay API - Stablecoin API for global payments",
"keywords": [
"blindpay",
Expand Down Expand Up @@ -52,7 +52,11 @@
"lint:check": "biome check .",
"lint:fix": "biome check --write .",
"test": "vitest run",
"test:coverage": "vitest run --coverage"
"test:coverage": "vitest run --coverage",
"sync": "bun scripts/api-sync/index.ts --apply",
"sync:check": "bun scripts/api-sync/index.ts --check",
"sync:coverage": "bun scripts/api-sync/index.ts --coverage",
"audit:types": "bun scripts/api-sync/index.ts --audit-types"
},
"exports": {
".": {
Expand Down
Loading
Loading