api-sync #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: API Sync | |
| on: | |
| repository_dispatch: | |
| types: [api-sync] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| jobs: | |
| sync: | |
| name: Sync SDK with API changes | |
| 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/openapi.json > /tmp/api-sync/openapi.json | |
| git show origin/api-sync-data:.api-sync/changed_paths.json > /tmp/api-sync/changed_paths.json | |
| echo "Changed paths:" | |
| cat /tmp/api-sync/changed_paths.json | |
| - name: Extract relevant spec section | |
| run: | | |
| python3 << 'SCRIPT' | |
| import json, re | |
| with open('/tmp/api-sync/changed_paths.json') as f: | |
| changed = json.load(f) | |
| with open('/tmp/api-sync/openapi.json') as f: | |
| spec = json.load(f) | |
| # Identify affected resources from changed paths | |
| resources = set() | |
| for path in changed: | |
| parts = path.strip('/').split('/') | |
| # Find the resource name after instances/{id}/ or at top level | |
| for i, part in enumerate(parts): | |
| if part in ('instances', '{instance_id}', '{id}', 'v1', 'e'): | |
| continue | |
| if '{' in part: | |
| continue | |
| resources.add(part) | |
| # Collect all paths that belong to affected resources | |
| relevant_paths = {} | |
| for path, methods in spec.get('paths', {}).items(): | |
| for resource in resources: | |
| if resource in path: | |
| relevant_paths[path] = methods | |
| break | |
| # Collect all $ref schemas referenced by relevant paths | |
| def collect_refs(obj, refs): | |
| if isinstance(obj, dict): | |
| if '$ref' in obj: | |
| ref = obj['$ref'].split('/')[-1] | |
| if ref not in refs: | |
| refs.add(ref) | |
| # Recursively collect refs from the schema itself | |
| schema = spec.get('components', {}).get('schemas', {}).get(ref, {}) | |
| collect_refs(schema, refs) | |
| for v in obj.values(): | |
| collect_refs(v, refs) | |
| elif isinstance(obj, list): | |
| for item in obj: | |
| collect_refs(item, refs) | |
| refs = set() | |
| collect_refs(relevant_paths, refs) | |
| relevant_schemas = {} | |
| for name in refs: | |
| if name in spec.get('components', {}).get('schemas', {}): | |
| relevant_schemas[name] = spec['components']['schemas'][name] | |
| mini_spec = { | |
| 'openapi': spec.get('openapi'), | |
| 'paths': relevant_paths, | |
| 'components': {'schemas': relevant_schemas} | |
| } | |
| with open('/tmp/api-sync/relevant_spec.json', 'w') as f: | |
| json.dump(mini_spec, f, indent=2) | |
| print(f"Extracted {len(relevant_paths)} paths and {len(relevant_schemas)} schemas for resources: {resources}") | |
| SCRIPT | |
| - name: Check for existing api-sync PR | |
| id: check-pr | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 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 | |
| - name: Create or checkout api-sync branch | |
| run: | | |
| git fetch origin api-sync 2>/dev/null || true | |
| if git rev-parse --verify origin/api-sync >/dev/null 2>&1; then | |
| git checkout api-sync | |
| git reset --hard origin/main | |
| else | |
| 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-4-20250514 --allowedTools "Bash(*),Read,Edit,Write,Glob,Grep"' | |
| prompt: | | |
| You are updating this SDK to match API changes. Your changes must be ADDITIVE ONLY. | |
| CRITICAL RULES: | |
| - NEVER remove or rename existing methods, functions, classes, or types | |
| - NEVER replace specialized methods with generic ones (e.g. don't replace createIndividualWithStandardKYC with a generic create) | |
| - NEVER change existing field types (e.g. typed enum to string, required to optional) | |
| - NEVER bump the major version number | |
| - If a method already handles an endpoint, only update its input/output types with new fields | |
| Before making any changes: | |
| 1. Read CLAUDE.md thoroughly for this SDK's conventions. | |
| 2. Read /tmp/api-sync/changed_paths.json to identify affected resource(s). | |
| 3. Read the EXISTING resource file(s) in full — understand what already exists. | |
| 4. Read the relevant spec at /tmp/api-sync/relevant_spec.json (filtered to affected resources only). | |
| 5. Identify the GAP: what the spec has that the SDK is missing. | |
| Changes to make: | |
| - ADD new enum types with ALL values from the spec, using the SDK's typed enum pattern (see CLAUDE.md). Include EVERY value — do not omit any. | |
| - ADD new optional fields to existing input/output types. | |
| - ADD new methods for new endpoints (don't touch existing methods). | |
| - ADD new resource files for entirely new resources, register in client, add exports. | |
| - MINOR version bump only (e.g. 1.4.0 → 1.5.0). | |
| Enum typing rules (NEVER use plain string for enum fields): | |
| - Go: type XEnum string + const block with all values | |
| - Swift: public enum X: String, Codable, Sendable with all cases | |
| - PHP: enum X: string with all cases | |
| - Python: X = Literal["value1", "value2", ...] | |
| - Node: type X = "value1" | "value2" | ... | |
| Final step: Run this SDK's lint and type check commands (see CLAUDE.md). Fix any errors. Repeat until zero errors. Do NOT create commits. | |
| - name: Validate no methods removed | |
| id: validate | |
| run: | | |
| ISSUES="" | |
| # Check for removed method/function definitions | |
| REMOVED=$(git diff main -- '*.ts' '*.go' '*.swift' '*.php' '*.py' | grep '^-' | grep -v '^---' | grep -E '(export )?(async )?(function |func |def |public func |public function |class |type |enum )' | head -20) | |
| if [ -n "$REMOVED" ]; then | |
| echo "WARNING: Possible method/type removals detected:" | |
| echo "$REMOVED" | |
| ISSUES="$ISSUES\nRemoved definitions detected" | |
| fi | |
| # Check for major version bump | |
| MAJOR_BUMP=$(git diff main -- '*package.json' '*pyproject.toml' '*.go' '*.php' '*.swift' | grep '^+' | grep -iE 'version.*["\x27]?[2-9]+\.0\.0' | head -5) | |
| if [ -n "$MAJOR_BUMP" ]; then | |
| echo "ERROR: Major version bump detected:" | |
| echo "$MAJOR_BUMP" | |
| ISSUES="$ISSUES\nMajor version bump" | |
| fi | |
| if [ -n "$ISSUES" ]; then | |
| echo "has_issues=true" >> $GITHUB_OUTPUT | |
| echo "Validation issues found — review carefully before merging" | |
| else | |
| echo "has_issues=false" >> $GITHUB_OUTPUT | |
| echo "Validation passed" | |
| fi | |
| - name: Commit and push | |
| id: commit | |
| 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 push --force-with-lease origin api-sync | |
| - name: Create or update PR | |
| if: steps.commit.outputs.has_changes == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.SDK_SYNC_PAT }} | |
| run: | | |
| EXISTING_PR="${{ steps.check-pr.outputs.existing_pr }}" | |
| VALIDATION="${{ steps.validate.outputs.has_issues }}" | |
| LABEL="api-sync" | |
| if [ "$VALIDATION" = "true" ]; then | |
| LABEL="api-sync,needs-review" | |
| fi | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "Updating existing PR #$EXISTING_PR" | |
| gh pr comment "$EXISTING_PR" --body "Updated with latest API changes." | |
| else | |
| gh pr create \ | |
| --title "feat: sync SDK with API changes" \ | |
| --body "Automated SDK update from API changes." \ | |
| --base main \ | |
| --head api-sync \ | |
| --label "$LABEL" | |
| fi |