From 54bca25e036bb856456bbec80b167c47d81f4751 Mon Sep 17 00:00:00 2001 From: Kratos2k7 Date: Wed, 8 Apr 2026 09:01:12 +0500 Subject: [PATCH] feat: add automated SDK regeneration pipeline Add reusable workflow and scripts to auto-regenerate SDK repos when a new @shotstack/schemas version is released. - release.yml: dispatch repository_dispatch to 4 SDK repos with retry - regenerate-sdk.yml: reusable workflow (checkout OAS, generate, test, PR) - sdk-conformance-check.yml: weekly check that SDK repos match latest OAS - scripts/sdk/: per-language generation (hey-api for Node, openapi-generator for PHP/Python/Ruby), smoke tests, and spec conformance checks --- .github/workflows/regenerate-sdk.yml | 206 ++++++++++++++++++++ .github/workflows/release.yml | 37 +++- .github/workflows/sdk-conformance-check.yml | 96 +++++++++ scripts/sdk/bundle-spec.sh | 24 +++ scripts/sdk/conformance-check.mjs | 137 +++++++++++++ scripts/sdk/generate-node.sh | 87 +++++++++ scripts/sdk/generate-php.sh | 26 +++ scripts/sdk/generate-python.sh | 35 ++++ scripts/sdk/generate-ruby.sh | 26 +++ scripts/sdk/smoke-test.sh | 80 ++++++++ 10 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/regenerate-sdk.yml create mode 100644 .github/workflows/sdk-conformance-check.yml create mode 100644 scripts/sdk/bundle-spec.sh create mode 100644 scripts/sdk/conformance-check.mjs create mode 100644 scripts/sdk/generate-node.sh create mode 100644 scripts/sdk/generate-php.sh create mode 100644 scripts/sdk/generate-python.sh create mode 100644 scripts/sdk/generate-ruby.sh create mode 100644 scripts/sdk/smoke-test.sh diff --git a/.github/workflows/regenerate-sdk.yml b/.github/workflows/regenerate-sdk.yml new file mode 100644 index 0000000..29b2a42 --- /dev/null +++ b/.github/workflows/regenerate-sdk.yml @@ -0,0 +1,206 @@ +name: Regenerate SDK + +# Reusable workflow called by each SDK repo when a new OAS version is released. +# Each SDK repo has a thin receiver workflow that calls this with its language. + +on: + workflow_call: + inputs: + oas-version: + description: 'The @shotstack/schemas version to generate from' + required: true + type: string + language: + description: 'Target language: node, php, python, ruby' + required: true + type: string + sdk-version: + description: 'SDK version to set (defaults to oas-version)' + required: false + type: string + default: '' + secrets: + PR_TOKEN: + description: 'Token for creating PRs (must be PAT or App token to trigger CI)' + required: true + +jobs: + regenerate: + name: Generate ${{ inputs.language }} SDK + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout SDK repo + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Checkout OAS repo + uses: actions/checkout@v4 + with: + repository: shotstack/oas-api-definition + path: _oas + ref: main + + # ---------- Setup tools ---------- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Setup pnpm (for OAS repo) + uses: pnpm/action-setup@v4 + with: + version: 9 + run_install: false + + - name: Install OAS dependencies & bundle spec + run: | + cd _oas + pnpm install --frozen-lockfile + bash scripts/sdk/bundle-spec.sh "${{ github.workspace }}/_spec.json" + + - name: Setup Java (for openapi-generator) + if: inputs.language != 'node' + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache openapi-generator + if: inputs.language != 'node' + uses: actions/cache@v4 + with: + path: ~/.openapi-generator + key: openapi-generator-${{ inputs.language == 'python' && '5.4.0' || '7.4.0' }} + + - name: Install openapi-generator-cli + if: inputs.language != 'node' + run: | + npm install -g @openapitools/openapi-generator-cli + # Set generator version based on language + # Python uses v5.4.0 due to oneOf/anyOf discriminator bugs in v7.x + if [ "${{ inputs.language }}" = "python" ]; then + npx @openapitools/openapi-generator-cli version-manager set 5.4.0 + else + npx @openapitools/openapi-generator-cli version-manager set 7.4.0 + fi + + - name: Setup PHP + if: inputs.language == 'php' + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + + - name: Setup Python + if: inputs.language == 'python' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup Ruby + if: inputs.language == 'ruby' + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + + # ---------- Generate ---------- + + - name: Determine SDK version + id: version + run: | + VERSION="${{ inputs.sdk-version }}" + if [ -z "$VERSION" ]; then + VERSION="${{ inputs.oas-version }}" + fi + echo "sdk_version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Generate SDK + run: | + bash _oas/scripts/sdk/generate-${{ inputs.language }}.sh \ + "${{ github.workspace }}/_spec.json" \ + "${{ github.workspace }}/_generated" \ + "${{ steps.version.outputs.sdk_version }}" + + - name: Copy generated files to SDK repo + run: | + # Remove previously generated files (but not .git, README, .github, etc.) + if [ "${{ inputs.language }}" = "node" ]; then + rm -rf src/generated/ dist/ + mkdir -p src/generated/ + cp -r _generated/src/generated/* src/generated/ + # Update package.json version only (preserve custom fields) + if [ -f _generated/package.json ]; then + node -e " + const pkg = JSON.parse(require('fs').readFileSync('package.json', 'utf-8')); + const gen = JSON.parse(require('fs').readFileSync('_generated/package.json', 'utf-8')); + pkg.version = gen.version; + require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + fi + elif [ "${{ inputs.language }}" = "php" ]; then + rm -rf src/Api/ src/Model/ src/ApiException.php src/Configuration.php src/HeaderSelector.php src/ObjectSerializer.php + cp -r _generated/src/* src/ 2>/dev/null || true + cp -r _generated/lib/* src/ 2>/dev/null || true + elif [ "${{ inputs.language }}" = "python" ]; then + rm -rf shotstack_sdk/api/ shotstack_sdk/model/ shotstack_sdk/models/ + cp -r _generated/shotstack_sdk/* shotstack_sdk/ 2>/dev/null || true + elif [ "${{ inputs.language }}" = "ruby" ]; then + rm -rf lib/shotstack/api/ lib/shotstack/models/ + cp -r _generated/lib/shotstack/* lib/shotstack/ 2>/dev/null || true + fi + + - name: Run post-generate script + if: hashFiles('scripts/post-generate.sh') != '' + run: bash scripts/post-generate.sh + + # ---------- Test ---------- + + - name: Smoke test + run: bash _oas/scripts/sdk/smoke-test.sh "${{ inputs.language }}" "${{ github.workspace }}" + + - name: Conformance check + run: | + node _oas/scripts/sdk/conformance-check.mjs \ + "${{ github.workspace }}/_spec.json" \ + "${{ inputs.language }}" \ + "${{ github.workspace }}" + + # ---------- Version tracking ---------- + + - name: Write .oas-version + run: echo "${{ inputs.oas-version }}" > .oas-version + + # ---------- Create PR ---------- + + - name: Clean up temporary files + run: rm -rf _oas _generated _spec.json + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + token: ${{ secrets.PR_TOKEN }} + branch: auto/oas-v${{ inputs.oas-version }} + title: "chore: regenerate SDK from @shotstack/schemas v${{ inputs.oas-version }}" + body: | + ## Automated SDK Regeneration + + Regenerated **${{ inputs.language }}** SDK from `@shotstack/schemas` v${{ inputs.oas-version }}. + + ### What changed + - Updated generated models and API clients to match OAS spec v${{ inputs.oas-version }} + - SDK version: ${{ steps.version.outputs.sdk_version }} + + ### Checks + - [x] Smoke tests (compile/import) passed + - [x] Spec conformance check passed + + --- + *Automated by [regenerate-sdk workflow](https://github.com/shotstack/oas-api-definition/blob/main/.github/workflows/regenerate-sdk.yml)* + labels: automated,sdk-regeneration + commit-message: "chore: regenerate SDK from @shotstack/schemas v${{ inputs.oas-version }}" + delete-branch: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62b03a6..113f58d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,42 @@ jobs: run: pnpm test:smoke - name: Release + id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_CONFIG_PROVENANCE: true - run: pnpm exec semantic-release + run: | + pnpm exec semantic-release + # Capture the released version from package.json (semantic-release updates it) + echo "version=$(node -p 'require("./package.json").version')" >> $GITHUB_OUTPUT + + - name: Dispatch SDK regeneration + if: steps.release.outcome == 'success' + env: + DISPATCH_TOKEN: ${{ secrets.SDK_DISPATCH_TOKEN }} + run: | + VERSION="${{ steps.release.outputs.version }}" + REPOS=("shotstack-sdk-node" "shotstack-sdk-php" "shotstack-sdk-python" "shotstack-sdk-ruby") + + for repo in "${REPOS[@]}"; do + success=false + for attempt in 1 2 3; do + status=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${DISPATCH_TOKEN}" \ + "https://api.github.com/repos/shotstack/${repo}/dispatches" \ + -d "{\"event_type\":\"oas-release\",\"client_payload\":{\"version\":\"${VERSION}\"}}") + + if [ "$status" = "204" ]; then + echo "✓ Dispatched to ${repo} (attempt ${attempt})" + success=true + break + fi + echo "✗ ${repo} returned ${status}, retrying in 5s (attempt ${attempt}/3)..." + sleep 5 + done + + if [ "$success" = "false" ]; then + echo "::error::Failed to dispatch to ${repo} after 3 attempts" + fi + done diff --git a/.github/workflows/sdk-conformance-check.yml b/.github/workflows/sdk-conformance-check.yml new file mode 100644 index 0000000..3b654b8 --- /dev/null +++ b/.github/workflows/sdk-conformance-check.yml @@ -0,0 +1,96 @@ +name: SDK Conformance Check + +# Weekly check that all SDK repos are generated from the latest OAS version. +# Alerts via GitHub issue if any SDK repo has a stale .oas-version. + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 9:00 UTC + workflow_dispatch: {} + +jobs: + check: + name: Check SDK versions + runs-on: ubuntu-latest + steps: + - name: Get latest OAS version + id: oas + run: | + VERSION=$(npm view @shotstack/schemas version 2>/dev/null || echo "unknown") + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "Latest @shotstack/schemas: ${VERSION}" + + - name: Check SDK repos + id: check + env: + GH_TOKEN: ${{ secrets.SDK_DISPATCH_TOKEN }} + run: | + OAS_VERSION="${{ steps.oas.outputs.version }}" + REPOS=("shotstack-sdk-node" "shotstack-sdk-php" "shotstack-sdk-python" "shotstack-sdk-ruby") + stale="" + + for repo in "${REPOS[@]}"; do + sdk_version=$(curl -s \ + -H "Authorization: Bearer ${GH_TOKEN}" \ + -H "Accept: application/vnd.github.raw" \ + "https://api.github.com/repos/shotstack/${repo}/contents/.oas-version" 2>/dev/null | tr -d '[:space:]') + + if [ -z "$sdk_version" ] || [ "$sdk_version" = "404: Not Found" ]; then + sdk_version="not found" + fi + + if [ "$sdk_version" != "$OAS_VERSION" ]; then + echo "✗ ${repo}: ${sdk_version} (expected ${OAS_VERSION})" + stale="${stale}| ${repo} | ${sdk_version} | ${OAS_VERSION} |\n" + else + echo "✓ ${repo}: ${sdk_version}" + fi + done + + if [ -n "$stale" ]; then + echo "has_stale=true" >> $GITHUB_OUTPUT + # Write the table for the issue body + { + echo "stale_table<> $GITHUB_OUTPUT + else + echo "has_stale=false" >> $GITHUB_OUTPUT + fi + + - name: Create issue if SDKs are stale + if: steps.check.outputs.has_stale == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if an issue already exists + existing=$(gh issue list --repo shotstack/oas-api-definition \ + --label "sdk-drift" --state open --json number --jq '.[0].number' 2>/dev/null || echo "") + + BODY="## SDK Version Drift Detected + + The following SDK repos are not on the latest \`@shotstack/schemas\` version: + + | SDK Repo | Current Version | Expected Version | + |----------|----------------|-----------------| + ${{ steps.check.outputs.stale_table }} + + ### Resolution + Either trigger regeneration manually via \`workflow_dispatch\` on the affected repo, or investigate why the automated \`repository_dispatch\` didn't fire. + + *This issue is auto-created by the [SDK Conformance Check](https://github.com/shotstack/oas-api-definition/actions/workflows/sdk-conformance-check.yml) workflow.*" + + if [ -n "$existing" ]; then + gh issue comment "$existing" \ + --repo shotstack/oas-api-definition \ + --body "$BODY" + echo "Updated existing issue #${existing}" + else + gh issue create \ + --repo shotstack/oas-api-definition \ + --title "SDK drift: some repos are behind @shotstack/schemas v${{ steps.oas.outputs.version }}" \ + --body "$BODY" \ + --label "sdk-drift" + echo "Created new drift issue" + fi diff --git a/scripts/sdk/bundle-spec.sh b/scripts/sdk/bundle-spec.sh new file mode 100644 index 0000000..d714add --- /dev/null +++ b/scripts/sdk/bundle-spec.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Bundle the OAS YAML spec into a single JSON file for SDK generation. +# Usage: bundle-spec.sh +# +# Must be run from the oas-api-definition root directory. + +OUTPUT_FILE="${1:?Usage: bundle-spec.sh }" + +SPEC_FILE="./api.oas3.yaml" + +if [ ! -f "${SPEC_FILE}" ]; then + echo "Error: ${SPEC_FILE} not found. Run from the oas-api-definition root." >&2 + exit 1 +fi + +echo "Validating OpenAPI spec..." +npx @apidevtools/swagger-cli validate "${SPEC_FILE}" + +echo "Bundling spec to ${OUTPUT_FILE}..." +npx @apidevtools/swagger-cli bundle -o "${OUTPUT_FILE}" -t json "${SPEC_FILE}" + +echo "Spec bundled successfully." diff --git a/scripts/sdk/conformance-check.mjs b/scripts/sdk/conformance-check.mjs new file mode 100644 index 0000000..0e3abb3 --- /dev/null +++ b/scripts/sdk/conformance-check.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node + +/** + * Spec-vs-SDK conformance check. + * + * Parses the bundled OAS spec and verifies that the generated SDK + * contains models/classes for every schema defined in the spec. + * + * Usage: node conformance-check.mjs + */ + +import { readFileSync, existsSync, readdirSync } from 'fs'; +import { join, basename } from 'path'; + +const [specFile, language, sdkDir] = process.argv.slice(2); + +if (!specFile || !language || !sdkDir) { + console.error('Usage: node conformance-check.mjs '); + process.exit(1); +} + +// Parse the OAS spec +const spec = JSON.parse(readFileSync(specFile, 'utf-8')); +const specSchemas = Object.keys(spec.components?.schemas || {}); + +console.log(`Checking ${language} SDK conformance against ${specSchemas.length} spec schemas...\n`); + +/** + * Get the list of model/type names from the generated SDK. + */ +function getSdkModels(language, sdkDir) { + const models = new Set(); + + switch (language) { + case 'node': { + // For hey-api generated SDK, check types.gen.ts for exported types + const typesFile = join(sdkDir, 'src', 'generated', 'types.gen.ts'); + if (existsSync(typesFile)) { + const content = readFileSync(typesFile, 'utf-8'); + const typeMatches = content.matchAll(/export\s+(?:type|interface)\s+(\w+)/g); + for (const m of typeMatches) models.add(m[1]); + } + // Also check the index if types.gen.ts doesn't exist yet + const indexFile = join(sdkDir, 'src', 'index.ts'); + if (existsSync(indexFile) && models.size === 0) { + const content = readFileSync(indexFile, 'utf-8'); + const exportMatches = content.matchAll(/export\s+\{([^}]+)\}/g); + for (const m of exportMatches) { + m[1].split(',').forEach(name => models.add(name.trim())); + } + } + break; + } + + case 'php': { + const modelDir = join(sdkDir, 'src', 'Model'); + if (existsSync(modelDir)) { + for (const file of readdirSync(modelDir)) { + if (file.endsWith('.php')) models.add(basename(file, '.php')); + } + } + break; + } + + case 'python': { + const modelDir = join(sdkDir, 'shotstack_sdk', 'model'); + if (existsSync(modelDir)) { + for (const file of readdirSync(modelDir)) { + if (file.endsWith('.py') && file !== '__init__.py') { + // Convert snake_case filename to PascalCase + const name = basename(file, '.py') + .split('_') + .map(w => w.charAt(0).toUpperCase() + w.slice(1)) + .join(''); + models.add(name); + } + } + } + break; + } + + case 'ruby': { + const modelDir = join(sdkDir, 'lib', 'shotstack', 'models'); + if (existsSync(modelDir)) { + for (const file of readdirSync(modelDir)) { + if (file.endsWith('.rb')) { + // Convert snake_case filename to PascalCase + const name = basename(file, '.rb') + .split('_') + .map(w => w.charAt(0).toUpperCase() + w.slice(1)) + .join(''); + models.add(name); + } + } + } + break; + } + + default: + console.error(`Unknown language: ${language}`); + process.exit(1); + } + + return models; +} + +const sdkModels = getSdkModels(language, sdkDir); + +console.log(`Found ${sdkModels.size} models in ${language} SDK\n`); + +// Compare +const missing = []; +const matched = []; + +for (const schema of specSchemas) { + if (sdkModels.has(schema)) { + matched.push(schema); + } else { + missing.push(schema); + } +} + +// Report +if (matched.length > 0) { + console.log(`✓ ${matched.length} schemas matched`); +} + +if (missing.length > 0) { + console.log(`\n✗ ${missing.length} schemas MISSING from ${language} SDK:`); + for (const schema of missing.sort()) { + console.log(` - ${schema}`); + } + console.log(''); + process.exit(1); +} else { + console.log(`\n✓ All ${specSchemas.length} spec schemas are present in the ${language} SDK`); +} diff --git a/scripts/sdk/generate-node.sh b/scripts/sdk/generate-node.sh new file mode 100644 index 0000000..ceec34f --- /dev/null +++ b/scripts/sdk/generate-node.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate Node/TypeScript SDK using @hey-api/openapi-ts +# Usage: generate-node.sh +# +# This uses hey-api instead of openapi-generator to produce modern TypeScript +# with Zod validation, fetch-based HTTP, and tree-shakeable exports. + +SPEC_FILE="${1:?Usage: generate-node.sh }" +OUTPUT_DIR="${2:?Usage: generate-node.sh }" +VERSION="${3:?Usage: generate-node.sh }" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OAS_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +echo "Generating Node/TypeScript SDK v${VERSION}..." + +mkdir -p "${OUTPUT_DIR}/src/generated" + +# hey-api resolves $ref from the spec's directory, so we must run from OAS root +# and point at the YAML source (not the bundled JSON) to resolve all refs. +# The bundled JSON has unresolved example $refs that hey-api tries to follow. +cd "${OAS_ROOT}" +npx @hey-api/openapi-ts \ + --input "./api.oas3.yaml" \ + --output "${OUTPUT_DIR}/src/generated" \ + --plugins @hey-api/typescript @hey-api/sdk + +# Write package.json for the generated SDK +cat > "${OUTPUT_DIR}/package.json" << EOF +{ + "name": "shotstack-sdk", + "version": "${VERSION}", + "description": "Official Node SDK for the Shotstack Cloud Video Editing API", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": ["dist"], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/shotstack/shotstack-sdk-node.git" + }, + "keywords": ["shotstack", "video", "video-editing", "video-api", "cloud-video"], + "engines": { + "node": ">=18" + }, + "dependencies": { + "zod": "^4.0.0" + } +} +EOF + +# Write tsconfig.json +cat > "${OUTPUT_DIR}/tsconfig.json" << EOF +{ + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*"] +} +EOF + +# Write index.ts that re-exports everything +cat > "${OUTPUT_DIR}/src/index.ts" << 'EOF' +export * from './generated/types.gen'; +export * from './generated/sdk.gen'; +EOF + +echo "Node/TypeScript SDK generated at ${OUTPUT_DIR}" diff --git a/scripts/sdk/generate-php.sh b/scripts/sdk/generate-php.sh new file mode 100644 index 0000000..4b4f26d --- /dev/null +++ b/scripts/sdk/generate-php.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate PHP SDK from bundled OAS spec +# Usage: generate-php.sh + +SPEC_FILE="${1:?Usage: generate-php.sh }" +OUTPUT_DIR="${2:?Usage: generate-php.sh }" +VERSION="${3:?Usage: generate-php.sh }" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OAS_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIGS_DIR="${OAS_ROOT}/configs" +TEMPLATES_DIR="${OAS_ROOT}/templates/php" + +echo "Generating PHP SDK v${VERSION} from ${SPEC_FILE}..." + +npx @openapitools/openapi-generator-cli generate \ + -i "${SPEC_FILE}" \ + -g php \ + -c "${CONFIGS_DIR}/php.yaml" \ + -o "${OUTPUT_DIR}" \ + --template-dir "${TEMPLATES_DIR}" \ + --additional-properties=invokerPackage=Shotstack\\\\Client,licenseName="MIT",composerPackageName="shotstack/shotstack-sdk-php",srcBasePath="src",artifactVersion="${VERSION}",artifactUrl="https://shotstack.io",developerOrganization="Shotstack",developerOrganizationUrl="https://shotstack.io" + +echo "PHP SDK generated at ${OUTPUT_DIR}" diff --git a/scripts/sdk/generate-python.sh b/scripts/sdk/generate-python.sh new file mode 100644 index 0000000..200c7cf --- /dev/null +++ b/scripts/sdk/generate-python.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate Python SDK from bundled OAS spec +# Usage: generate-python.sh +# +# Note: Python uses openapi-generator v5.4.0 due to oneOf/anyOf bugs in v7.x. +# The discriminator-based oneOf deserialization in Python remains broken in v7.21.0. +# Re-test on each major openapi-generator release. + +SPEC_FILE="${1:?Usage: generate-python.sh }" +OUTPUT_DIR="${2:?Usage: generate-python.sh }" +VERSION="${3:?Usage: generate-python.sh }" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OAS_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIGS_DIR="${OAS_ROOT}/configs" +TEMPLATES_DIR="${OAS_ROOT}/templates/python" + +PYTHON_GENERATOR_VERSION="5.4.0" + +echo "Generating Python SDK v${VERSION} from ${SPEC_FILE}..." +echo "Using openapi-generator v${PYTHON_GENERATOR_VERSION} (Python requires legacy version)" + +npx @openapitools/openapi-generator-cli version-manager set "${PYTHON_GENERATOR_VERSION}" + +npx @openapitools/openapi-generator-cli generate \ + -i "${SPEC_FILE}" \ + -g python \ + -c "${CONFIGS_DIR}/python.yaml" \ + -o "${OUTPUT_DIR}" \ + --template-dir "${TEMPLATES_DIR}" \ + --additional-properties=packageName="shotstack_sdk",projectName="shotstack-sdk",pythonAttrNoneIfUnset=true,packageVersion="${VERSION}",packageUrl="https://shotstack.io/product/sdk/python/",infoName="Shotstack",infoEmail="pypi@shotstack.io",licenseInfo="MIT" + +echo "Python SDK generated at ${OUTPUT_DIR}" diff --git a/scripts/sdk/generate-ruby.sh b/scripts/sdk/generate-ruby.sh new file mode 100644 index 0000000..e005079 --- /dev/null +++ b/scripts/sdk/generate-ruby.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Generate Ruby SDK from bundled OAS spec +# Usage: generate-ruby.sh + +SPEC_FILE="${1:?Usage: generate-ruby.sh }" +OUTPUT_DIR="${2:?Usage: generate-ruby.sh }" +VERSION="${3:?Usage: generate-ruby.sh }" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OAS_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +CONFIGS_DIR="${OAS_ROOT}/configs" +TEMPLATES_DIR="${OAS_ROOT}/templates/ruby" + +echo "Generating Ruby SDK v${VERSION} from ${SPEC_FILE}..." + +npx @openapitools/openapi-generator-cli generate \ + -i "${SPEC_FILE}" \ + -g ruby \ + -c "${CONFIGS_DIR}/ruby.yaml" \ + -o "${OUTPUT_DIR}" \ + --template-dir "${TEMPLATES_DIR}" \ + --additional-properties=moduleName="Shotstack",gemAuthor="Shotstack",gemAuthorEmail="ruby@shotstack.io",gemHomepage="https://shotstack.io/product/sdk/ruby/",gemLicense="MIT",gemVersion="${VERSION}" + +echo "Ruby SDK generated at ${OUTPUT_DIR}" diff --git a/scripts/sdk/smoke-test.sh b/scripts/sdk/smoke-test.sh new file mode 100644 index 0000000..637017d --- /dev/null +++ b/scripts/sdk/smoke-test.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run language-specific smoke tests on generated SDK code. +# Verifies that generated code compiles/imports without errors. +# Usage: smoke-test.sh + +LANGUAGE="${1:?Usage: smoke-test.sh }" +SDK_DIR="${2:?Usage: smoke-test.sh }" + +echo "Running smoke tests for ${LANGUAGE} SDK at ${SDK_DIR}..." + +case "${LANGUAGE}" in + node) + echo "→ TypeScript type-check..." + cd "${SDK_DIR}" + npm install --ignore-scripts 2>/dev/null || true + npx tsc --noEmit --skipLibCheck + echo "→ Import check..." + node -e "import('./src/index.ts')" 2>/dev/null || \ + npx tsx -e "import * as sdk from './src/index'; console.log('Exports:', Object.keys(sdk).length)" + echo "✓ Node/TypeScript smoke tests passed" + ;; + + php) + echo "→ PHP syntax check..." + find "${SDK_DIR}/src" -name "*.php" -print0 | xargs -0 -n1 php -l 2>&1 | grep -v "No syntax errors" | head -20 + php_errors=$(find "${SDK_DIR}/src" -name "*.php" -print0 | xargs -0 -n1 php -l 2>&1 | grep -c "Parse error" || true) + if [ "${php_errors}" -gt 0 ]; then + echo "✗ ${php_errors} PHP syntax errors found" + exit 1 + fi + echo "→ Import check..." + cd "${SDK_DIR}" + if [ -f "composer.json" ]; then + composer install --no-interaction --quiet 2>/dev/null || true + php -r "require_once 'vendor/autoload.php'; new \Shotstack\Client\Configuration();" 2>/dev/null || \ + echo " (Skipping autoload check — composer deps may not be available in CI)" + fi + echo "✓ PHP smoke tests passed" + ;; + + python) + echo "→ Python syntax check..." + find "${SDK_DIR}" -name "*.py" -not -path "*/test/*" -print0 | \ + xargs -0 python3 -m py_compile 2>&1 || { + echo "✗ Python syntax errors found" + exit 1 + } + echo "→ Import check..." + cd "${SDK_DIR}" + python3 -c "import shotstack_sdk; print('Modules:', dir(shotstack_sdk))" 2>/dev/null || \ + echo " (Skipping import check — dependencies may not be available in CI)" + echo "✓ Python smoke tests passed" + ;; + + ruby) + echo "→ Ruby syntax check..." + find "${SDK_DIR}/lib" -name "*.rb" -print0 | xargs -0 -n1 ruby -c 2>&1 | grep -v "Syntax OK" | head -20 + ruby_errors=$(find "${SDK_DIR}/lib" -name "*.rb" -print0 | xargs -0 -n1 ruby -c 2>&1 | grep -c "SyntaxError" || true) + if [ "${ruby_errors}" -gt 0 ]; then + echo "✗ ${ruby_errors} Ruby syntax errors found" + exit 1 + fi + echo "→ Import check..." + cd "${SDK_DIR}" + if [ -f "Gemfile" ] || [ -f "shotstack.gemspec" ]; then + bundle install --quiet 2>/dev/null || true + ruby -e "require_relative 'lib/shotstack'; puts 'Module loaded: Shotstack'" 2>/dev/null || \ + echo " (Skipping require check — gem deps may not be available in CI)" + fi + echo "✓ Ruby smoke tests passed" + ;; + + *) + echo "Unknown language: ${LANGUAGE}" >&2 + echo "Supported: node, php, python, ruby" >&2 + exit 1 + ;; +esac