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
46 changes: 46 additions & 0 deletions .github/workflows/sdk-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: SDK CI

on:
push:
branches: [main]
paths:
- 'sdk/js/**'
- '.github/workflows/sdk-ci.yml'
pull_request:
branches: [main]
paths:
- 'sdk/js/**'
- '.github/workflows/sdk-ci.yml'

permissions:
contents: read

jobs:
js-sdk:
name: JS SDK
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Build JS SDK
run: pnpm build
working-directory: sdk/js

- name: Run tests
run: pnpm test
working-directory: sdk/js
189 changes: 189 additions & 0 deletions .github/workflows/sdk-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
name: SDK Publish

on:
workflow_dispatch:
inputs:
sdk:
description: 'Which SDK to publish'
required: true
type: choice
options:
- js
- python
- both
bump:
description: 'Version bump type'
required: true
type: choice
options:
- patch
- minor
- major
dry_run:
description: 'Dry run (no actual publish)'
required: false
type: boolean
default: false

permissions:
contents: write

jobs:
publish-js:
name: Publish JS SDK
runs-on: ubuntu-latest
if: ${{ inputs.sdk == 'js' || inputs.sdk == 'both' }}

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Install dependencies
run: pnpm install

- name: Build
run: pnpm build
working-directory: sdk/js

- name: Run tests
run: pnpm test
working-directory: sdk/js

- name: Bump version
id: bump
run: |
OLD_VERSION=$(node -p "require('./package.json').version")
npm version ${{ inputs.bump }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Bumped JS SDK version: $OLD_VERSION -> $NEW_VERSION"
working-directory: sdk/js

- name: Publish (dry run)
if: ${{ inputs.dry_run }}
run: |
echo "Dry run - would publish version ${{ steps.bump.outputs.new_version }}"
npm publish --dry-run
working-directory: sdk/js
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish
if: ${{ !inputs.dry_run }}
run: npm publish --access public
working-directory: sdk/js
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Commit version bump
if: ${{ !inputs.dry_run }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add sdk/js/package.json
git commit -m "chore(js-sdk): bump version to ${{ steps.bump.outputs.new_version }}"
git push

publish-python:
name: Publish Python SDK
runs-on: ubuntu-latest
needs: [publish-js]
if: ${{ always() && (inputs.sdk == 'python' || inputs.sdk == 'both') && (needs.publish-js.result == 'success' || needs.publish-js.result == 'skipped') }}

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Pull latest changes
run: git pull origin ${{ github.ref_name }} || true

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build twine

- name: Bump version
id: bump
run: |
OLD_VERSION=$(grep -Po '(?<=version = ")[^"]*' pyproject.toml)

IFS='.' read -r major minor patch <<< "$OLD_VERSION"

case "${{ inputs.bump }}" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
esac

NEW_VERSION="${major}.${minor}.${patch}"

sed -i "s/version = \"$OLD_VERSION\"/version = \"$NEW_VERSION\"/" pyproject.toml

echo "old_version=$OLD_VERSION" >> $GITHUB_OUTPUT
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "Bumped Python SDK version: $OLD_VERSION -> $NEW_VERSION"
working-directory: sdk/python

- name: Build package
run: |
rm -rf dist/*
python -m build
working-directory: sdk/python

- name: Check package
run: twine check dist/*
working-directory: sdk/python

- name: Publish (dry run)
if: ${{ inputs.dry_run }}
run: |
echo "Dry run - would publish version ${{ steps.bump.outputs.new_version }}"
ls -la dist/
working-directory: sdk/python

- name: Publish to PyPI
if: ${{ !inputs.dry_run }}
run: twine upload dist/*
working-directory: sdk/python
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

- name: Commit version bump
if: ${{ !inputs.dry_run }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add sdk/python/pyproject.toml
git commit -m "chore(python-sdk): bump version to ${{ steps.bump.outputs.new_version }}"
git push
145 changes: 145 additions & 0 deletions sdk/js/__test__/fern-customizations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Tests to ensure fern-generated code maintains custom modifications.
* These tests catch when fern generate overwrites our customizations.
*
* Run `pnpm test` to verify before committing fern-generated changes.
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';

const SRC_DIR = path.join(__dirname, '../src');

// Helper to find files recursively
function findFiles(dir: string, pattern: RegExp): string[] {
const results: string[] = [];

function walk(currentDir: string) {
const files = fs.readdirSync(currentDir);
for (const file of files) {
const fullPath = path.join(currentDir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walk(fullPath);
} else if (pattern.test(file)) {
results.push(fullPath);
}
}
}

walk(dir);
return results;
}

describe('Fern Customizations', () => {
describe('BodyInit type imports', () => {
const filesRequiringBodyInit = [
'core/fetcher/Fetcher.ts',
'core/fetcher/getRequestBody.ts',
'core/fetcher/makeRequest.ts',
];

filesRequiringBodyInit.forEach((filePath) => {
it(`${filePath} should import BodyInit from types/fetch.js`, () => {
const fullPath = path.join(SRC_DIR, filePath);
const content = fs.readFileSync(fullPath, 'utf-8');

expect(content).toContain('import type { BodyInit } from "../../types/fetch.js"');
});
});
});

describe('Error handling pattern in Client files', () => {
it('should use core.isFailedResponse pattern in all Client.ts files', () => {
const clientFiles = findFiles(path.join(SRC_DIR, 'api/resources'), /^Client\.ts$/);

// Also include root Client.ts
const rootClient = path.join(SRC_DIR, 'Client.ts');
if (fs.existsSync(rootClient)) {
clientFiles.push(rootClient);
}

expect(clientFiles.length).toBeGreaterThan(0);

for (const file of clientFiles) {
const content = fs.readFileSync(file, 'utf-8');
const relativePath = path.relative(SRC_DIR, file);

// Check for the correct error handling pattern
// Should have: core.isFailedResponse(_response) ? _response.error : { reason: "unknown", errorMessage: "Unknown error" }
if (content.includes('Error._unknown(')) {
expect(
content,
`${relativePath} should use core.isFailedResponse pattern for error handling`
).toMatch(/core\.isFailedResponse\(_response\)\s*\?\s*_response\.error\s*:\s*\{\s*reason:\s*["']unknown["']/);

// Should NOT have simplified pattern without isFailedResponse check
// Pattern: Error._unknown(_response.error) without the ternary
const simplifiedPattern = /Error\._unknown\(_response\.error\)\s*,/g;
expect(
content.match(simplifiedPattern),
`${relativePath} should NOT use simplified error pattern without isFailedResponse`
).toBeNull();
}
}
});

it('should use full condition check before status-code switch', () => {
const clientFiles = findFiles(path.join(SRC_DIR, 'api/resources'), /^Client\.ts$/);

// Also include root Client.ts
const rootClient = path.join(SRC_DIR, 'Client.ts');
if (fs.existsSync(rootClient)) {
clientFiles.push(rootClient);
}

for (const file of clientFiles) {
const content = fs.readFileSync(file, 'utf-8');
const relativePath = path.relative(SRC_DIR, file);

// If file has status-code handling, it should use full condition
if (content.includes('_response.error.reason === "status-code"')) {
expect(
content,
`${relativePath} should use full condition check (!_response.ok && core.isFailedResponse)`
).toContain('!_response.ok && core.isFailedResponse(_response) && _response.error.reason === "status-code"');
}
}
});
});

describe('Protected files exist', () => {
const protectedFiles = [
'types/fetch.ts',
'globals.d.ts',
'providers/index.ts',
'providers/base.ts',
'providers/sign.ts',
'providers/volcengine.ts',
];

protectedFiles.forEach((filePath) => {
it(`${filePath} should exist (protected by .fernignore)`, () => {
const fullPath = path.join(SRC_DIR, filePath);
expect(fs.existsSync(fullPath), `${filePath} is missing - check .fernignore`).toBe(true);
});
});
});

describe('.fernignore configuration', () => {
it('should have .fernignore in src directory', () => {
const fernignorePath = path.join(SRC_DIR, '.fernignore');
expect(fs.existsSync(fernignorePath)).toBe(true);
});

it('.fernignore should protect required paths', () => {
const fernignorePath = path.join(SRC_DIR, '.fernignore');
const content = fs.readFileSync(fernignorePath, 'utf-8');

const requiredEntries = ['providers', 'index.ts', 'globals.d.ts'];
requiredEntries.forEach((entry) => {
expect(content, `.fernignore should contain ${entry}`).toContain(entry);
});
});
});
});
2 changes: 1 addition & 1 deletion sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-infra/sandbox",
"version": "1.0.5",
"version": "1.0.6",
"description": "Node.js SDK for AIO Sandbox integration providing tools and interfaces",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export interface CodeExecuteRequest {
code: string;
/** Execution timeout in seconds */
timeout?: number;
/** Current working directory for code execution */
cwd?: string;
}
Loading