Skip to content
Open
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
45 changes: 45 additions & 0 deletions .github/workflows/cleanup-pr-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Cleanup PR preview

on:
pull_request:
types: [closed]

permissions:
contents: write

concurrency:
group: gh-pages-cleanup-${{ github.event.pull_request.number }}
cancel-in-progress: false

jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Remove pr-${{ github.event.pull_request.number }} from gh-pages
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail

if ! git ls-remote --exit-code --heads "https://github.com/${{ github.repository }}.git" gh-pages >/dev/null 2>&1; then
echo "gh-pages branch does not exist; nothing to clean up"
exit 0
fi

tmp=$(mktemp -d)
git clone --depth=1 --branch gh-pages \
"https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" \
"$tmp"

cd "$tmp"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if [ ! -d "pr-${PR_NUMBER}" ]; then
echo "No preview directory for PR #${PR_NUMBER}"
exit 0
fi

git rm -rf "pr-${PR_NUMBER}"
git commit -m "Remove PR #${PR_NUMBER} SDK diff preview"
git push origin gh-pages
72 changes: 55 additions & 17 deletions .github/workflows/validate-sdks.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Validate SDKs

permissions:
contents: read
contents: write
issues: write
pull-requests: write

Expand Down Expand Up @@ -161,6 +161,7 @@ jobs:
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
cache: 'npm'

- name: Download diagnostics
if: needs.load-matrix.outputs.spec-changed == 'true'
Expand All @@ -169,26 +170,19 @@ jobs:
pattern: oagen-diagnostics-*
path: sdk-diagnostics

- name: Install dependencies
if: needs.load-matrix.outputs.spec-changed == 'true'
run: npm ci

- name: Build code diff report
if: needs.load-matrix.outputs.spec-changed == 'true'
id: diff-report
run: |
set -e
: > /tmp/combined.diff
has_content=0
for lang in dotnet go php python ruby; do
diff_file="sdk-diagnostics/oagen-diagnostics-${lang}/sdk-code.diff"
if [ -s "$diff_file" ]; then
cat "$diff_file" >> /tmp/combined.diff
has_content=1
fi
done
if [ "$has_content" = "1" ]; then
npx --yes diff2html-cli@5 -i file -s side --su hidden -F /tmp/sdk-diff-report.html /tmp/combined.diff
echo "report-exists=true" >> "$GITHUB_OUTPUT"
else
echo "report-exists=false" >> "$GITHUB_OUTPUT"
fi
languages="$(jq -r 'map(.language) | join(",")' .github/sdk-matrix.json)"
node scripts/build-sdk-diff-report.mjs \
--artifacts-root sdk-diagnostics \
--languages "$languages" \
--output /tmp/sdk-diff-report.html

- name: Upload code diff report
if: steps.diff-report.outputs.report-exists == 'true'
Expand All @@ -198,6 +192,49 @@ jobs:
path: /tmp/sdk-diff-report.html
retention-days: 14

- name: Publish report to GitHub Pages
id: publish-pages
if: >-
steps.diff-report.outputs.report-exists == 'true'
&& github.event_name == 'pull_request'
&& github.event.pull_request.head.repo.full_name == github.repository
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
git config --global user.name "github-actions[bot]"
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"

worktree=$(mktemp -d)
if git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then
git fetch --depth=1 origin gh-pages
git worktree add "$worktree" origin/gh-pages
(cd "$worktree" && git checkout -B gh-pages)
else
git worktree add --orphan -b gh-pages "$worktree"
: > "$worktree/.nojekyll"
fi

target_dir="$worktree/pr-${PR_NUMBER}"
mkdir -p "$target_dir"
cp /tmp/sdk-diff-report.html "$target_dir/sdk-diff-report.html"

(
cd "$worktree"
git add -A
if git diff --cached --quiet; then
echo "No changes to publish"
else
git commit -m "Update PR #${PR_NUMBER} SDK diff preview"
git push origin gh-pages
fi
)

owner="${REPO%%/*}"
name="${REPO#*/}"
echo "pages-url=https://${owner}.github.io/${name}/pr-${PR_NUMBER}/sdk-diff-report.html" >> "$GITHUB_OUTPUT"

- name: Render PR comment
if: needs.load-matrix.outputs.spec-changed == 'true'
run: |
Expand All @@ -207,6 +244,7 @@ jobs:
--run-id "${{ github.run_id }}" \
--repo "${{ github.repository }}" \
--code-diff-available "${{ steps.diff-report.outputs.report-exists }}" \
--pages-url "${{ steps.publish-pages.outputs.pages-url }}" \
--output /tmp/sdk-compat-comment.md

- name: Upsert PR comment
Expand Down
2 changes: 1 addition & 1 deletion .last-synced-sha
Original file line number Diff line number Diff line change
@@ -1 +1 @@
92db0495807c86fbbc4d45bd266a6c1f5bcbb59c
ff939ff075453287993e1e6182f1d6f23c67ab80
85 changes: 84 additions & 1 deletion oagen.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OagenConfig, OperationHint } from '@workos/oagen';
import type { OagenConfig, OpenApiDocument, OperationHint } from '@workos/oagen';
import { toCamelCase } from '@workos/oagen';
import { workosEmittersPlugin } from '@workos/oagen-emitters';

Expand Down Expand Up @@ -354,6 +354,81 @@ const mountRules: Record<string, string> = {
UserManagementMultiFactorAuthentication: 'MultiFactorAuth',
};

// ---------------------------------------------------------------------------
// Pre-IR spec overlay -- patch around upstream spec quirks that would otherwise
// emit breaking SDK changes. See docs/breaking-change-playbook.md and the
// oagen `transformSpec` docs for usage. Reach for this only when the upstream
// fix can't land in time AND the change is genuinely additive.
// ---------------------------------------------------------------------------
function transformSpec(spec: OpenApiDocument): OpenApiDocument {
const components = (spec as { components?: { schemas?: Record<string, Record<string, unknown>> } }).components;
const schemas = components?.schemas;
const paths = (spec as { paths?: Record<string, Record<string, unknown>> }).paths;
if (!schemas || !paths) return spec;

// -- Fork: UserlandUserOrganizationMembershipBase{,List} --------------------
// Upstream forked the existing `â€ĶBaseList` into `â€ĶBaseWithUserList` to add
// a `user` field on the inline list-item shape. That fork renames the
// generated list-data type in dotnet/go/ruby, breaking compat. Re-point the
// forked $refs at the original list and merge the new `user` field
// additively into the original's inline item shape.
const forkedListRef = '#/components/schemas/UserlandUserOrganizationMembershipBaseWithUserList';
const originalListRef = '#/components/schemas/UserlandUserOrganizationMembershipBaseList';
for (const pathItem of Object.values(paths)) {
for (const op of Object.values(pathItem)) {
const responses = (op as { responses?: Record<string, { content?: Record<string, { schema?: { $ref?: string } }> }> })
.responses;
const schema = responses?.['200']?.content?.['application/json']?.schema;
if (schema?.$ref === forkedListRef) {
schema.$ref = originalListRef;
}
}
}
const baseList = schemas['UserlandUserOrganizationMembershipBaseList'];
const itemProps = (
baseList as
| {
properties?: {
data?: { items?: { properties?: Record<string, unknown>; required?: string[] } };
};
}
| undefined
)?.properties?.data?.items;
if (itemProps?.properties && !itemProps.properties.user) {
itemProps.properties.user = {
$ref: '#/components/schemas/UserlandUser',
description: 'The user that belongs to the organization through this membership.',
} as unknown as Record<string, unknown>;
if (itemProps.required && !itemProps.required.includes('user')) {
itemProps.required.push('user');
}
}
delete schemas['UserlandUserOrganizationMembershipBaseWithUser'];
delete schemas['UserlandUserOrganizationMembershipBaseWithUserList'];

// -- Rename: JwtTemplate -> JwtTemplateResponse -----------------------------
// Upstream renamed the response schema. Existing SDKs already expose the
// type as `JwtTemplateResponse`/`JWTTemplateResponse`; preserve that name.
if (schemas['JwtTemplate'] && !schemas['JwtTemplateResponse']) {
schemas['JwtTemplateResponse'] = schemas['JwtTemplate'];
delete schemas['JwtTemplate'];
const oldRef = '#/components/schemas/JwtTemplate';
const newRef = '#/components/schemas/JwtTemplateResponse';
for (const pathItem of Object.values(paths)) {
for (const op of Object.values(pathItem)) {
const responses = (op as { responses?: Record<string, { content?: Record<string, { schema?: { $ref?: string } }> }> })
.responses;
for (const response of Object.values(responses ?? {})) {
const schema = response.content?.['application/json']?.schema;
if (schema?.$ref === oldRef) schema.$ref = newRef;
}
}
}
}

return spec;
}

const config: OagenConfig = {
...workosEmittersPlugin,
docUrl: 'https://workos.com/docs',
Expand All @@ -377,5 +452,13 @@ const config: OagenConfig = {
},
operationHints,
mountRules,
modelHints: {
// `UserlandUser` (→ `User`) is referenced from both UserManagement and
// Authorization paths; pin it to UserManagement so hand-written imports
// in `workos-python` (e.g. `from workos.user_management.models import User`)
// keep resolving.
User: 'UserManagement',
},
transformSpec,
};
export default config;
Loading
Loading