Skip to main content

API SDK Contracts Specification

This document serves as both a specification and implementation guide for the SDK contract system. The infrastructure (GitHub App, secrets, Terraform) is already configured. This document explains the architecture, what was set up, and how to implement the remaining pieces.

Implementation Status

ComponentStatusDetails
GitHub App (eli-sdk-bot)✅ CompleteApp ID: 2659740, installed on all repos
Terraform Secrets✅ CompleteSDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY deployed
HAE OpenAPI Spec✅ ExistsFastAPI auto-generates at /openapi.json
Backend OpenAPI Spec✅ ExistsNestJS Swagger at /docs-json
SDK Generation Workflows⏳ PendingGitHub Actions workflows to be created
SDK Packages⏳ Pending@eli/hae-sdk, @eli/backend-sdk to be published
Consumer Integration⏳ PendingBackend and Mobile to import SDKs

Executive Summary

This system creates type-safe contracts between Eli Health services. When fully implemented, the mobile app will not compile if it uses API calls that don't match the backend specification, and the backend will not compile if it uses HAE API calls incorrectly.

Infrastructure Already Configured

The following infrastructure was set up on January 15, 2025:

GitHub App: eli-sdk-bot

A GitHub App was created to handle cross-repository package access. This approach was chosen over Personal Access Tokens (PATs) because:

  • Not tied to any individual - survives employee turnover
  • Generates short-lived tokens automatically (more secure)
  • Better audit trail
  • Organization-owned, not personal

How it was created:

  1. Navigate to: https://github.com/organizations/eli-health/settings/apps/new
  2. Configure:
    • Name: eli-sdk-bot
    • Homepage URL: https://eli.health
    • Webhook: Unchecked (not needed)
    • Repository permissions:
      • Contents: Read-only
      • Metadata: Read-only (mandatory)
      • Packages: Read and write
    • Where can this app be installed?: Only on this account
  3. After creation:
    • Generated a private key (downloaded as .pem file)
    • Noted the App ID: 2659740
    • Installed the app on eli-health organization (all repositories)

Terraform Secrets Configuration

The GitHub App credentials are stored as GitHub Actions secrets via Terraform.

Files created/modified in eli-devops:

eli-devops/github/variables.tf - Added:

variable "sdk_bot_app_id" {
description = "GitHub App ID for Eli SDK Bot"
type = string
default = "2659740"
}

variable "sdk_bot_private_key" {
description = "Private key (.pem) for Eli SDK Bot GitHub App"
type = string
sensitive = true
}

eli-devops/github/secrets-sdk.tf - Created:

# SDK Bot Secrets for eli_hae_api
resource "github_actions_secret" "eli_hae_api_sdk_bot_app_id" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_APP_ID"
plaintext_value = var.sdk_bot_app_id
}

resource "github_actions_secret" "eli_hae_api_sdk_bot_private_key" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_PRIVATE_KEY"
plaintext_value = var.sdk_bot_private_key
}

# Same pattern repeated for eli-backend-api and eli-app

eli-devops/github/tfvars/github.tfvars - Added (sensitive, gitignored):

sdk_bot_app_id = "2659740"
sdk_bot_private_key = <<-EOT
-----BEGIN RSA PRIVATE KEY-----
... (private key content) ...
-----END RSA PRIVATE KEY-----
EOT

Secrets deployed to repositories:

RepositorySecrets
eli_hae_apiSDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY
eli-backend-apiSDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY
eli-appSDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY

To verify secrets are deployed, check: https://github.com/eli-health/{repo}/settings/secrets/actions


Problem Statement

Current Issues

  1. Communication Gaps: API changes in one service break consumers without warning
  2. Manual Type Maintenance: Types are manually duplicated across services
  3. Runtime Errors: Type mismatches are only discovered at runtime
  4. Integration Friction: Teams must coordinate verbally for API changes

Real Example: Why This System Matters

A backend field was removed that the mobile app was still using. The current system gave no warning - this only surfaced as a runtime error in production. With SDK contracts, the mobile app build would have failed immediately, giving instant feedback before deployment and preventing the production issue entirely.

Current State Analysis

HAE API (Python/FastAPI)

Good News: FastAPI already generates OpenAPI 3.0+ specs automatically.

ComponentStatusDetails
OpenAPI Spec✅ Auto-generatedAvailable at /openapi.json
Swagger UI✅ Available/docs endpoint
Pydantic Models✅ CompleteAll requests/responses typed
Endpoints21 endpointsFully documented

Key Files (eli_hae_api repo):

  • Entry point: app.py
  • Models: api/models/
  • Version: v1.0.120 (from .version file)

Backend API (TypeScript/NestJS)

Good News: NestJS Swagger is already integrated.

ComponentStatusDetails
OpenAPI Spec✅ Auto-generatedAvailable at /docs-json
Swagger UI✅ Available/docs endpoint (disabled in prod)
Swagger Decorators✅ PresentControllers use @ApiTags, @ApiResponse, etc.
Controllers20+ controllersComprehensive API surface

Key Files (eli-backend-api repo):

  • Entry point: src/main.ts
  • Swagger Config: Lines 39-48 in main.ts
  • Models: src/models/

Mobile App (React Native)

Current State: Manual type definitions, no SDK.

ComponentStatusDetails
API Client✅ Generic typedget<T>(), post<T>() methods
Service Layer✅ Well-structured/src/services/*.ts
Types⚠️ ManualDefined in /src/common/models/
SDK❌ NoneTypes manually maintained

Key Files (eli-app repo):

  • API Client: src/shared/api.client.ts
  • Common Models: src/common/models/
  • Services: src/services/

Proposed Solution

Architecture Overview

SDK Generation Tool Selection

After evaluating available options, the recommended approach is:

ToolProsConsRecommendation
openapi-typescriptFast, lightweight, TypeScript-native, excellent tree-shakingTypes only (no HTTP client)✅ For types
Hey APIPurpose-built for TypeScript, FastAPI recommendedNewer tool✅ For full client
OpenAPI GeneratorSupports 50+ languagesGeneric output, requires refinementConsider for multi-language
Fern/SpeakeasyProduction-ready output, automatic publishingCommercial (cost)Future consideration

Recommended Stack:

  • Types Generation: @hey-api/openapi-ts (FastAPI recommended)
  • Package Publishing: GitHub Packages (npm registry)
  • CI Integration: GitHub Actions (existing infrastructure)

SDK Package Structure

@eli/hae-sdk/
├── package.json
├── src/
│ ├── index.ts # Main exports
│ ├── types/ # Generated types
│ │ ├── models.ts # Request/response models
│ │ └── operations.ts # API operations
│ └── client/ # Generated HTTP client
│ ├── index.ts
│ └── services/
│ ├── ImageAnalysisService.ts
│ ├── MetricsService.ts
│ └── ...
└── openapi.json # Source spec (for reference)

@eli/backend-sdk/
├── package.json
├── src/
│ ├── index.ts
│ ├── types/
│ │ ├── models.ts # IUser, IReading, IRecord, etc.
│ │ └── operations.ts
│ └── client/
│ └── services/
│ ├── UsersService.ts
│ ├── ReadingsService.ts
│ ├── HealthTagService.ts
│ └── ...
└── openapi.json

Implementation Plan

Phase 1: HAE SDK for Backend

Objective: Generate TypeScript SDK from HAE OpenAPI spec for Backend consumption.

Step 1.1: Enhance HAE OpenAPI Configuration

Improve operation IDs and add custom schema metadata:

# eli_hae_api/app.py
from fastapi import FastAPI
from fastapi.routing import APIRoute

def custom_generate_unique_id(route: APIRoute) -> str:
"""Generate clean operation IDs like 'analyzeImage' instead of 'analyze_image_images_analysis_post'"""
return route.name

app = FastAPI(
title="Eli HAE API",
description="Health Analysis Engine - Image processing and ML inference",
version="1.0.120",
generate_unique_id_function=custom_generate_unique_id,
)

Step 1.2: Create SDK Generation Script

# eli_hae_api/scripts/generate-sdk.sh
#!/bin/bash
set -e

# Configuration
OUTPUT_DIR="sdk-output"
PACKAGE_NAME="@eli/hae-sdk"
VERSION=$(cat .version)

# 1. Ensure HAE is running (or use local openapi.json)
if [ -f "openapi.json" ]; then
SPEC_FILE="openapi.json"
else
echo "Extracting OpenAPI spec from running instance..."
curl -s http://localhost:8000/openapi.json > openapi.json
SPEC_FILE="openapi.json"
fi

# 2. Generate TypeScript SDK
npx @hey-api/openapi-ts \
-i $SPEC_FILE \
-o $OUTPUT_DIR/src \
-c @hey-api/client-fetch

# 3. Create package.json
cat > $OUTPUT_DIR/package.json << EOF
{
"name": "$PACKAGE_NAME",
"version": "$VERSION",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@hey-api/client-fetch": "^0.2.0"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}
EOF

# 4. Create tsconfig.json
cat > $OUTPUT_DIR/tsconfig.json << EOF
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
EOF

echo "SDK generated in $OUTPUT_DIR"

Step 1.3: GitHub Actions Workflow for HAE SDK

# eli_hae_api/.github/workflows/generate-sdk.yaml
name: Generate and Publish HAE SDK

on:
push:
branches: [development, staging, main]
paths:
- 'api/**'
- 'app.py'
- '.version'
workflow_dispatch:

jobs:
generate-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

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

- name: Install dependencies
run: pip install -r requirements-prod.txt

- name: Extract OpenAPI spec
run: |
python -c "
from app import app
import json
with open('openapi.json', 'w') as f:
json.dump(app.openapi(), f, indent=2)
"

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'

- name: Generate SDK
run: |
npx @hey-api/openapi-ts \
-i openapi.json \
-o sdk/src \
-c @hey-api/client-fetch

- name: Setup SDK package
run: |
VERSION=$(cat .version)
cd sdk
npm init -y
npm pkg set name="@eli/hae-sdk"
npm pkg set version="$VERSION"
npm pkg set main="dist/index.js"
npm pkg set types="dist/index.d.ts"
npm install typescript @hey-api/client-fetch
npx tsc --init --declaration --outDir dist
npm run build

- name: Publish to GitHub Packages
run: |
cd sdk
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Trigger Backend SDK update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.HAE_SDK_UPDATE_PAT }}
repository: eli-health/eli-backend-api
event-type: hae-sdk-updated
client-payload: '{"version": "${{ env.VERSION }}"}'

Step 1.4: Backend Integration

Update eli-backend-api/package.json:

{
"dependencies": {
"@eli/hae-sdk": "^1.0.120"
}
}

Usage in backend:

// eli-backend-api/src/modules/image-analysis/image-analysis.service.ts
import { ImageAnalysisService, CortisolAnalysisPayload } from '@eli/hae-sdk';

@Injectable()
export class ImageAnalysisServiceImpl {
async analyzeImage(payload: CortisolAnalysisPayload) {
// TypeScript will error if payload doesn't match HAE's expected structure
const result = await ImageAnalysisService.imageAnalysis({ body: payload });
return result;
}
}

Phase 2: Backend SDK for Mobile App

Objective: Generate TypeScript SDK from Backend OpenAPI spec for Mobile consumption.

Step 2.1: Enhance Backend OpenAPI Configuration

// eli-backend-api/src/main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

const config = new DocumentBuilder()
.setTitle('Eli Backend API')
.setDescription('Core API for Eli Health mobile application')
.setVersion(process.env.npm_package_version || '1.0.0')
.addBearerAuth()
.addTag('users', 'User management and authentication')
.addTag('readings', 'Test strip readings and analysis')
.addTag('records', 'Health records management')
.addTag('health-tags', 'Health tagging system')
.build();

const document = SwaggerModule.createDocument(app, config);

// Export OpenAPI spec for SDK generation
if (process.env.EXPORT_OPENAPI === 'true') {
const fs = require('fs');
fs.writeFileSync('openapi.json', JSON.stringify(document, null, 2));
}

Step 2.2: GitHub Actions Workflow for Backend SDK

# eli-backend-api/.github/workflows/generate-sdk.yaml
name: Generate and Publish Backend SDK

on:
push:
branches: [development, staging, main]
paths:
- 'src/modules/**/*.controller.ts'
- 'src/models/**'
- 'package.json'
repository_dispatch:
types: [hae-sdk-updated]
workflow_dispatch:

jobs:
generate-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'

- name: Install dependencies
run: npm ci

- name: Extract OpenAPI spec
run: |
EXPORT_OPENAPI=true npm run build
# Or use a dedicated script
npm run export:openapi

- name: Generate SDK
run: |
npx @hey-api/openapi-ts \
-i openapi.json \
-o sdk/src \
-c @hey-api/client-fetch

- name: Setup and publish SDK
run: |
VERSION=$(node -p "require('./package.json').version")
cd sdk
npm init -y
npm pkg set name="@eli/backend-sdk"
npm pkg set version="$VERSION"
npm install typescript @hey-api/client-fetch
npx tsc --init --declaration --outDir dist
npm run build
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Step 2.3: Mobile App Integration

Update eli-app/package.json:

{
"dependencies": {
"@eli/backend-sdk": "^1.0.0"
}
}

Migration example in mobile:

// Before (manual types)
import { IUser } from '@src/common/models';
import apiClient from '@shared/api.client';

export function getCurrentUser(): Promise<IUser> {
return apiClient.get<IUser>('/users/current');
}

// After (SDK types - compile-time safe)
import { UsersService, IUser } from '@eli/backend-sdk';

export function getCurrentUser(): Promise<IUser> {
return UsersService.getCurrentUser();
}

Phase 3: CI/CD Integration

Workflow Dependency Chain

Version Strategy: Automatic Schema Change Detection

The SDK version increments automatically when the OpenAPI schema changes. This ensures every schema modification produces a new version.

How It Works:

Implementation:

# In CI workflow - detect schema changes
CURRENT_HASH=$(sha256sum openapi.json | cut -d' ' -f1)
PREVIOUS_HASH=$(cat .openapi-hash 2>/dev/null || echo "none")

if [ "$CURRENT_HASH" != "$PREVIOUS_HASH" ]; then
# Schema changed - bump version and publish
npm version patch
echo "$CURRENT_HASH" > .openapi-hash
npm publish
else
echo "No schema changes detected, skipping publish"
fi

Version Format:

Change TypeVersion BumpExample
New endpoint addedMinor1.0.01.1.0
Field added/removedPatch1.1.01.1.1
Breaking changeMajor1.1.12.0.0

Breaking Change Detection:

The CI can automatically detect breaking changes by comparing OpenAPI specs:

# Use openapi-diff to detect breaking changes
npx openapi-diff previous-openapi.json openapi.json --breaking-only

if [ $? -eq 1 ]; then
npm version major # Breaking change detected
else
npm version patch # Non-breaking change
fi

Consumers always use latest:

{
"@eli/backend-sdk": "latest"
}

When a new version is published, consumer CI pipelines automatically pick it up and will fail to build if there are incompatibilities.

Breaking Change Detection

Compile-Time Enforcement

When a breaking change is introduced:

Example Error Output

# Mobile app build output after breaking Backend change
src/services/user.ts:42:15 - error TS2339: Property 'legacyField' does not exist on type 'IUser'.

42 return user.legacyField;
~~~~~~~~~~~

Found 1 error in src/services/user.ts:42

GitHub Packages: How It Works

GitHub Packages is a private npm registry tied to the eli-health GitHub organization. Only repos/users in the org can access it.

Flow 1: HAE (Python) → Backend (TypeScript)

Flow 2: Backend (TypeScript) → Mobile (React Native)

Key Concepts

QuestionAnswer
Is it private?Yes - only eli-health org members can see/download
Where does the SDK live?npm.pkg.github.com/@eli/hae-sdk
Where does Docker live?GCP Artifact Registry (separate from SDK)
Who can publish?The repo that owns the package (via GITHUB_TOKEN)
Who can read?Any repo with a valid token (PAT or GITHUB_TOKEN with permissions)

Authentication: GitHub App (eli-sdk-bot)

Instead of using Personal Access Tokens (tied to individuals), we use a GitHub App that belongs to the organization.

App Details:

PropertyValue
Nameeli-sdk-bot
App ID2659740
PermissionsContents (read), Metadata (read), Packages (read/write)
Installed onAll eli-health repositories
Management URLhttps://github.com/organizations/eli-health/settings/apps/eli-sdk-bot

Key Rotation: If the private key needs to be rotated:

  1. Go to the app management URL above
  2. Scroll to "Private keys" section
  3. Click "Generate a private key" (downloads new .pem)
  4. Update eli-devops/github/tfvars/github.tfvars with new key
  5. Run terraform apply -var-file=tfvars/github.tfvars
  6. Delete the old private key from the GitHub App page

Terraform Configuration (Already Applied)

The secrets are configured in eli-devops/github/secrets-sdk.tf:

# eli_hae_api - publishes @eli/hae-sdk
resource "github_actions_secret" "eli_hae_api_sdk_bot_app_id" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_APP_ID"
plaintext_value = var.sdk_bot_app_id
}

resource "github_actions_secret" "eli_hae_api_sdk_bot_private_key" {
repository = github_repository.eli_hae_api.name
secret_name = "SDK_BOT_PRIVATE_KEY"
plaintext_value = var.sdk_bot_private_key
}

# Same pattern for eli-backend-api and eli-app

Secrets deployed to:

  • eli_hae_api: SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY
  • eli-backend-api: SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY
  • eli-app: SDK_BOT_APP_ID, SDK_BOT_PRIVATE_KEY

Using in GitHub Actions Workflows

- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SDK_BOT_APP_ID }}
private-key: ${{ secrets.SDK_BOT_PRIVATE_KEY }}

- name: Setup Node.js with GitHub Packages
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://npm.pkg.github.com'
scope: '@eli'

- name: Install dependencies
run: npm ci
env:
NODE_AUTH_TOKEN: ${{ steps.app-token.outputs.token }}

.npmrc Configuration

Each repo needs an .npmrc file (committed to repo):

@eli:registry=https://npm.pkg.github.com

Local Development

Developers need to authenticate locally to install SDK packages:

# One-time setup
npm login --registry=https://npm.pkg.github.com --scope=@eli

# When prompted:
# Username: your-github-username
# Password: <paste your personal access token with read:packages scope>
# Email: your-email@eli.health

Or add to ~/.npmrc:

//npm.pkg.github.com/:_authToken=ghp_your_personal_access_token
@eli:registry=https://npm.pkg.github.com

Retention Policy

PackageRetentionRationale
Latest 10 versionsForeverRollback capability
Older versions90 daysStorage cleanup

SDK Update Notifications

This is not just any npm package - it's the API contract. When a new SDK version is published, developers need to know immediately.

Notification Mechanisms

1. Slack Notification (Immediate Awareness)

When an SDK is published, post to #eli-dev Slack channel with full changelog:

# In the SDK generation workflow
- name: Get previous version
id: prev-version
run: |
# Get the latest published version from GitHub Packages
PREV_VERSION=$(npm view @eli/hae-sdk version 2>/dev/null || echo "0.0.0")
echo "version=$PREV_VERSION" >> $GITHUB_OUTPUT

- name: Generate changelog
id: changelog
run: |
# Get commits since last SDK publish (compare with previous tag or hash)
if git rev-parse "sdk-hae-v${{ steps.prev-version.outputs.version }}" >/dev/null 2>&1; then
SINCE_TAG="sdk-hae-v${{ steps.prev-version.outputs.version }}"
else
# Fallback: get last 5 commits affecting API
SINCE_TAG="HEAD~5"
fi

# Generate formatted changelog
CHANGELOG=$(git log $SINCE_TAG..HEAD --pretty=format:"• %s" -- api/ app.py | head -10)

# Handle empty changelog
if [ -z "$CHANGELOG" ]; then
CHANGELOG="• Minor updates"
fi

# Export multiline for GitHub Actions
echo "changes<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Detect change type
id: change-type
run: |
# Use openapi-diff to detect if breaking change
if [ -f "previous-openapi.json" ]; then
DIFF_RESULT=$(npx openapi-diff previous-openapi.json openapi.json --format json 2>/dev/null || echo "{}")
BREAKING=$(echo "$DIFF_RESULT" | jq -r '.breakingDifferencesFound // false')

if [ "$BREAKING" = "true" ]; then
echo "type=:rotating_light: BREAKING CHANGE" >> $GITHUB_OUTPUT
echo "emoji=🚨" >> $GITHUB_OUTPUT
else
echo "type=:sparkles: New Version" >> $GITHUB_OUTPUT
echo "emoji=✨" >> $GITHUB_OUTPUT
fi
else
echo "type=:package: New Version" >> $GITHUB_OUTPUT
echo "emoji=📦" >> $GITHUB_OUTPUT
fi

- name: Notify Slack with changelog
if: success()
uses: slackapi/slack-github-action@v1
with:
channel-id: 'C0XXXXXXX' # #eli-dev
slack-message: |
${{ steps.change-type.outputs.type }}

*@eli/hae-sdk* `${{ steps.prev-version.outputs.version }}` → `${{ env.VERSION }}`

*What changed:*
${{ steps.changelog.outputs.changes }}

*Triggered by:* <${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}|${{ github.event.head_commit.message }}>
*Author:* ${{ github.event.head_commit.author.name }}

_Downstream repos will receive auto-PRs._
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

- name: Tag this SDK release
run: |
git tag "sdk-hae-v${{ env.VERSION }}"
git push origin "sdk-hae-v${{ env.VERSION }}"

Example Slack Message:

✨ New Version

@eli/hae-sdk 1.2.3 → 1.2.4

What changed:
• Add support for batch image analysis
• Fix response type for cortisol readings
• Update validation schema for user metadata

Triggered by: feat: add batch analysis endpoint
Author: Zeeshan

Downstream repos will receive auto-PRs.

2. Automatic PR Creation (Actionable Update)

When SDK is published, automatically create a PR in consuming repos:

# In the SDK generation workflow - trigger downstream update
- name: Trigger Backend SDK Update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.app-token.outputs.token }}
repository: eli-health/eli-backend-api
event-type: sdk-update
client-payload: |
{
"package": "@eli/hae-sdk",
"version": "${{ env.VERSION }}"
}

In the consuming repo (eli-backend-api):

# .github/workflows/sdk-update.yaml
name: Update SDK Dependency

on:
repository_dispatch:
types: [sdk-update]

jobs:
update-sdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Update package.json
run: |
npm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }}

- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
title: "Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}"
body: |
## SDK Update

**Package:** `${{ github.event.client_payload.package }}`
**Version:** `${{ github.event.client_payload.previous_version }}` → `${{ github.event.client_payload.version }}`

## What Changed

${{ github.event.client_payload.changelog }}

## Change Type

${{ github.event.client_payload.is_breaking && '🚨 **BREAKING CHANGE** - Review carefully!' || '✅ Non-breaking change' }}

## Actions Required

- [ ] Review the changelog above
- [ ] Run `npm install` locally to test
- [ ] Verify TypeScript compilation passes
- [ ] Run tests: `npm test`

---
_Triggered by: [${{ github.event.client_payload.commit_message }}](${{ github.event.client_payload.commit_url }})_
branch: sdk-update/${{ github.event.client_payload.package }}
commit-message: "Update ${{ github.event.client_payload.package }} to ${{ github.event.client_payload.version }}"

The upstream workflow sends this enhanced payload:

# In HAE SDK workflow - send changelog to downstream
- name: Trigger Backend SDK Update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.app-token.outputs.token }}
repository: eli-health/eli-backend-api
event-type: sdk-update
client-payload: |
{
"package": "@eli/hae-sdk",
"version": "${{ env.VERSION }}",
"previous_version": "${{ steps.prev-version.outputs.version }}",
"changelog": "${{ steps.changelog.outputs.changes }}",
"is_breaking": ${{ steps.change-type.outputs.emoji == '🚨' }},
"commit_message": "${{ github.event.head_commit.message }}",
"commit_url": "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}"
}

3. Development Branch Auto-Update (Continuous Integration)

For development branches, auto-merge SDK updates to keep dev environments current:

# In the auto-PR workflow
- name: Enable Auto-Merge for Dev
if: github.base_ref == 'development'
run: gh pr merge --auto --squash
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}

Notification Flow

Breaking Change Alerts

For breaking changes (major version bump), add extra visibility with specific details about what broke:

- name: Detect breaking changes
id: breaking
if: env.VERSION_BUMP == 'major'
run: |
# Get detailed breaking changes from openapi-diff
BREAKING_DETAILS=$(npx openapi-diff previous-openapi.json openapi.json --format json | jq -r '
.breakingDifferences[]? |
"• \(.type): \(.path // .method) - \(.description // "Schema changed")"
' | head -5)

echo "details<<EOF" >> $GITHUB_OUTPUT
echo "$BREAKING_DETAILS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Alert Breaking Change
if: env.VERSION_BUMP == 'major'
uses: slackapi/slack-github-action@v1
with:
channel-id: 'C0XXXXXXX'
slack-message: |
:rotating_light: *BREAKING CHANGE* :rotating_light:

*@eli/hae-sdk* `${{ env.OLD_VERSION }}` → `${{ env.VERSION }}`

*Breaking changes detected:*
${{ steps.breaking.outputs.details }}

*What you need to do:*
1. Check auto-PR in downstream repos
2. Update code to handle the breaking changes
3. Run tests before merging

*Full diff:* <${{ github.server_url }}/${{ github.repository }}/compare/sdk-hae-v${{ env.OLD_VERSION }}...sdk-hae-v${{ env.VERSION }}|View on GitHub>
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Example Breaking Change Alert:

🚨 BREAKING CHANGE 🚨

@eli/hae-sdk 1.2.4 → 2.0.0

Breaking changes detected:
• removed: /api/v1/legacy-endpoint - Endpoint removed
• modified: POST /api/v1/analyze - Required field 'userId' added
• modified: Response schema - Field 'oldField' removed

What you need to do:
1. Check auto-PR in downstream repos
2. Update code to handle the breaking changes
3. Run tests before merging

Full diff: View on GitHub

Local Development

For developers working on both Backend and Mobile simultaneously on the same machine, there's a simple way to test SDK changes without waiting for CI/CD.

Setup

Assume you have both repos cloned as siblings:

~/projects/
├── eli-backend-api/
└── eli-app/

Script: Generate and Copy SDK Locally

Create this script in eli-backend-api/scripts/local-sdk.sh:

#!/bin/bash
set -e

echo "🔧 Generating Backend SDK for local development..."

# 1. Export OpenAPI spec from NestJS
echo "📄 Extracting OpenAPI spec..."
EXPORT_OPENAPI=true npm run build

# 2. Generate TypeScript SDK
echo "⚙️ Generating TypeScript SDK..."
mkdir -p sdk/src
npx @hey-api/openapi-ts -i openapi.json -o sdk/src -c @hey-api/client-fetch

# 3. Build the SDK
echo "🔨 Building SDK..."
cd sdk
cat > package.json << 'EOF'
{
"name": "@eli/backend-sdk",
"version": "0.0.0-local",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@hey-api/client-fetch": "^0.2.0"
}
}
EOF

cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
EOF

npm install
npm run build
cd ..

# 4. Copy to eli-app if it exists as sibling
if [ -d "../eli-app" ]; then
echo "📦 Copying SDK to eli-app..."
rm -rf ../eli-app/node_modules/@eli/backend-sdk
mkdir -p ../eli-app/node_modules/@eli
cp -r sdk ../eli-app/node_modules/@eli/backend-sdk
echo "✅ SDK installed in eli-app/node_modules/@eli/backend-sdk"
else
echo "⚠️ eli-app not found as sibling directory"
echo " Copy sdk/ manually to your eli-app/node_modules/@eli/backend-sdk"
fi

echo ""
echo "🎉 Done! You can now test Backend API changes in the Mobile app."

Usage

# After making changes to Backend API endpoints
cd eli-backend-api
./scripts/local-sdk.sh

# eli-app immediately sees the new types
cd ../eli-app
npm run typecheck # Verify types match

Important Notes

  • The local SDK version is 0.0.0-local to distinguish from published versions
  • Remember to run npm install in eli-app after testing to restore the real SDK
  • This bypasses CI/CD - use only for local development, not for production code
  • Always run proper tests before pushing changes

Migration Strategy

Adoption Steps

  1. Step 1: Set up SDK generation pipeline for HAE
  2. Step 2: Backend adopts @eli/hae-sdk, validates with tests
  3. Step 3: Set up SDK generation pipeline for Backend
  4. Step 4: Mobile adopts @eli/backend-sdk for critical services
  5. Step 5: Migrate remaining mobile services incrementally

Parallel Operation Period

During migration, support both patterns:

// eli-app/src/services/user.ts

// Old pattern (remove after full migration)
import { IUser as IUserLegacy } from '@src/common/models';
import apiClient from '@shared/api.client';

// New pattern (SDK-based)
import { UsersService, IUser } from '@eli/backend-sdk';

// Feature flag for gradual rollout
export function getCurrentUser(): Promise<IUser> {
if (config.useSDK) {
return UsersService.getCurrentUser();
}
return apiClient.get<IUserLegacy>('/users/current') as Promise<IUser>;
}

Success Metrics

MetricTargetMeasurement
Runtime type errors0Sentry error tracking
API contract violations caught at build time100%CI failure logs
SDK generation time< 2 minGitHub Actions metrics
SDK package size< 100KBnpm package stats
Developer adoption100%Code review audits

Risks and Mitigations

RiskImpactMitigation
OpenAPI spec incompleteSDK missing typesAdd Swagger decorators to all endpoints
Breaking changes too frequentBuild failures block deploymentSemantic versioning, deprecation warnings
SDK versioning conflictsDependency hellLock to exact versions in CI
Generated code qualityDeveloper frictionReview and customize generator templates

Appendix

A. Infrastructure Configuration Files

Terraform (eli-devops repo):

  • github/variables.tf - SDK Bot variable definitions
  • github/secrets-sdk.tf - GitHub Actions secrets for SDK Bot
  • github/tfvars/github.tfvars - Actual secret values (gitignored)
  • github/main.tf - Repository definitions

GitHub App:

B. Service File Locations

HAE API (eli_hae_api repo):

  • OpenAPI endpoint: GET /openapi.json
  • Main app: app.py
  • Models: api/models/
  • Version file: .version

Backend API (eli-backend-api repo):

  • OpenAPI endpoint: GET /docs-json
  • Main app: src/main.ts
  • Swagger config: Lines 39-48 in main.ts
  • Controllers: src/modules/*/*.controller.ts
  • Models: src/models/

Mobile App (eli-app repo):

  • API Client: src/shared/api.client.ts
  • Services: src/services/
  • Models: src/common/models/

C. Deployment Pipeline

Builds are triggered by branch pushes:

BranchEnvironmentProject
developmentdeveli-health-dev
stagingstagingeli-health-staging
mainproductioneli-health-prod

SDK generation integrates after the test phase, before Docker image build.

D. GitHub Secrets Deployed

Verify at: https://github.com/eli-health/{repo}/settings/secrets/actions

RepositorySecret NamePurpose
eli_hae_apiSDK_BOT_APP_IDGitHub App ID for token generation
eli_hae_apiSDK_BOT_PRIVATE_KEYPrivate key for token generation
eli-backend-apiSDK_BOT_APP_IDGitHub App ID for token generation
eli-backend-apiSDK_BOT_PRIVATE_KEYPrivate key for token generation
eli-appSDK_BOT_APP_IDGitHub App ID for token generation
eli-appSDK_BOT_PRIVATE_KEYPrivate key for token generation

E. References