Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
963c7fc
chore(seed): update all seed snapshots (#16158)
fern-support Jun 1, 2026
e90cdde
fix(python): derive offset pagination _has_next instead of hardcoding…
patrickthornton Jun 1, 2026
e367bd4
chore(seed): update all seed snapshots (#16161)
fern-support Jun 1, 2026
144a812
chore(python): release 5.14.7
github-actions[bot] Jun 1, 2026
854e28e
feat(cli): add `fern api enrich` command for AI-generated examples (#…
iamnamananand996 Jun 1, 2026
80cb0b7
chore(cli): release 5.44.0
github-actions[bot] Jun 1, 2026
1c0a497
chore(seed): update all seed snapshots (#16163)
fern-support Jun 1, 2026
328d1be
feat(ruby): add client-level max_retries constructor parameter (#16144)
iamnamananand996 Jun 1, 2026
25c6bce
chore(ruby-v2): release 1.13.0
github-actions[bot] Jun 1, 2026
8bf283b
chore(seed): update all seed snapshots (#16164)
fern-support Jun 1, 2026
68c9192
fix(python): guard bare unparameterized dict/list/set in construct_ty…
jsklan Jun 1, 2026
b73f0b6
chore(python): release 5.14.8
github-actions[bot] Jun 1, 2026
3e76a79
chore(cli): serve OpenAPI spec locally in v2 generate ETE test (#16166)
patrickthornton Jun 1, 2026
aad0fce
chore(cli): release 5.44.1
github-actions[bot] Jun 1, 2026
3bb1701
chore(seed): update all seed snapshots (#16165)
fern-support Jun 1, 2026
f37703d
chore(seed): update all seed snapshots (#16169)
fern-support Jun 1, 2026
cf5b2d7
chore(seed): update all seed snapshots (#16170)
fern-support Jun 1, 2026
39841fd
chore(cli-generator): add cli-sdk sync pipeline and documentation (#1…
jsklan Jun 1, 2026
5fe22fd
chore(seed): update all seed snapshots (#16172)
fern-support Jun 1, 2026
2781278
fix(cli-generator): define SYNC_DATE in sync-sdk.sh (#16173)
jsklan Jun 1, 2026
8f78205
chore(seed): update all seed snapshots (#16174)
fern-support Jun 1, 2026
187134d
chore(seed): update all seed snapshots (#16176)
fern-support Jun 1, 2026
8cbd326
fix(cli): preserve variant-inherited keys in discriminated union exam…
patrickthornton Jun 1, 2026
69b44f7
chore(cli): release 5.44.2
github-actions[bot] Jun 1, 2026
5989a43
chore(seed): update all seed snapshots (#16178)
fern-support Jun 1, 2026
459c684
chore(seed): update all seed snapshots (#16179)
fern-support Jun 1, 2026
5f0c868
chore(seed): update all seed snapshots (#16181)
fern-support Jun 1, 2026
bac83f3
fix(ruby): emit inherited fields on objects with extends/allOf (#16180)
patrickthornton Jun 1, 2026
d04e52b
chore(ruby-v2): release 1.13.1
github-actions[bot] Jun 1, 2026
c9c445f
chore(seed): update all seed snapshots (#16182)
fern-support Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
84 changes: 84 additions & 0 deletions .github/workflows/sync-cli-sdk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Sync cli-sdk into generators/cli/sdk

on:
schedule:
# Daily at 06:00 UTC (after US-Pacific EOD, before EU morning)
- cron: "0 6 * * *"
workflow_dispatch:

permissions:
contents: write

concurrency:
group: sync-cli-sdk
cancel-in-progress: true

env:
DO_NOT_TRACK: "1"

jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout fern
uses: actions/checkout@v6
with:
ref: main

- name: Checkout cli-sdk at main HEAD
uses: actions/checkout@v6
with:
repository: fern-api/cli-sdk
ref: main
path: _cli-sdk
token: ${{ secrets.FERN_SUPPORT_GH_ACTIONS_PAT }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Run sync script
run: bash generators/cli/scripts/sync-sdk.sh _cli-sdk

- name: Verify projected SDK builds
run: cargo build --locked --all-features --tests
working-directory: generators/cli/sdk

- name: Check for changes
id: diff
run: |
git add -A generators/cli/sdk/
if git diff --cached --quiet; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Read provenance
if: steps.diff.outputs.changed == 'true'
id: provenance
run: |
SHA="$(head -1 generators/cli/sdk/.synced-from | sed 's/cli-sdk@//')"
SHORT="${SHA:0:7}"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "short=$SHORT" >> "$GITHUB_OUTPUT"

- name: Create pull request
if: steps.diff.outputs.changed == 'true'
id: cpr
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8
with:
token: ${{ secrets.FERN_SUPPORT_GH_ACTIONS_PAT }}
commit-message: "chore(cli-generator): sync cli-sdk@${{ steps.provenance.outputs.short }}"
title: "chore(cli-generator): sync cli-sdk@${{ steps.provenance.outputs.short }}"
body: |
Automated sync of [fern-api/cli-sdk](https://github.com/fern-api/cli-sdk) `main` HEAD into `generators/cli/sdk/`.

**Source**: `cli-sdk@${{ steps.provenance.outputs.sha }}`

Generated by `.github/workflows/sync-cli-sdk.yml` via `generators/cli/scripts/sync-sdk.sh`.
Seed tests (`pnpm seed test --generator cli`) are the trust boundary — if they pass, a human reviewer can merge.
branch: chore/sync-cli-sdk
delete-branch: true
labels: |
cli-generator
automated
58 changes: 58 additions & 0 deletions generators/cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,64 @@ The image warms a cargo target cache against the committed
`Cargo.lock`; mounted fixtures use `cargo build --locked` and would
otherwise refuse to start when the dep tree drifts.

### Syncing the vendored SDK from cli-sdk

The SDK at [`./sdk/`](./sdk/) is a **vendored snapshot** of
[`fern-api/cli-sdk`](https://github.com/fern-api/cli-sdk). A daily
GitHub Actions workflow (`.github/workflows/sync-cli-sdk.yml`) pulls
`cli-sdk` `main` HEAD into this directory, opens a PR for human review,
and relies on seed tests + a human reviewer as the trust boundary.

**How the sync works:**

[`generators/cli/scripts/sync-sdk.sh`](scripts/sync-sdk.sh) takes a
local cli-sdk checkout and:

1. **Rsyncs source files** (`src/`, `tests/`, `cli/openapi-fixture/`)
under the same `SDK_IGNORE` rules as `build.mjs` — template-only
files (smoke tests, demo binaries, `.github/`, `docs/`, etc.) are
excluded.
2. **Projects `Cargo.toml`** — the vendored manifest is a deterministic
projection of cli-sdk's workspace manifest, **not** a copy. A naïve
`cp` would re-introduce `[workspace]`, `version.workspace = true`,
and ~35 `[[bin]]` entries. The projection:
- **Drops** `[workspace]` + `[workspace.package]`
- **Keeps only** the `openapi-fixture` and `strip-schema` `[[bin]]`
entries
- **Rewrites** `version.workspace = true` → literal
`version = "<synced>"`
- **Injects** the 3 Fern comment blocks that `patchCargoToml.ts`
anchors on (`TEMPLATE_TOP_COMMENT`, `TEMPLATE_BIN_COMMENT`, the
`strip-schema` "Internal tool…" comment) plus `readme = "README.md"`
and `[package.metadata.dist] dist = false`
- **Copies verbatim**: `[dependencies]`, `[features]`, `[lib]`,
`[profile.dist]`, `[build-dependencies]`, `[dev-dependencies]`
3. **Regenerates `Cargo.lock`** so `cargo build --locked` is honest.
4. **Writes `.synced-from`** with `cli-sdk@<sha>` + timestamp for
provenance tracking.

**Manual sync** (when you can't wait for the daily cron):

```bash
# From the fern repo root:
git clone --depth 1 https://github.com/fern-api/cli-sdk.git /tmp/cli-sdk
bash generators/cli/scripts/sync-sdk.sh /tmp/cli-sdk
# Review the diff, then commit.
```

**Must-rebuild list** (only when `Cargo.lock` changes):

```bash
pnpm turbo run dist:cli --filter @fern-api/cli-generator
docker build --no-cache -f docker/seed/Dockerfile.cli -t fernapi/cli-seed:latest .
pnpm turbo run dist:cli --filter @fern-api/seed-cli
```

**Key invariant**: every `patchCargoToml.ts` anchor must be present in
the projected `Cargo.toml`. If you rename a comment block in the
template, update `sync-sdk.sh`'s projection accordingly — the
`patchCargoToml.test.ts` will catch any mismatch at test time.

## Conventions

- **No TOML parser**: `patchCargoToml` uses literal string replacement
Expand Down
206 changes: 206 additions & 0 deletions generators/cli/scripts/sync-sdk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# sync-sdk.sh — deterministic sync of fern-api/cli-sdk into generators/cli/sdk/
#
# Usage:
# generators/cli/scripts/sync-sdk.sh <path-to-cli-sdk-checkout>
#
# The script expects a local checkout of cli-sdk (at the desired ref) as its
# sole argument. It projects the cli-sdk workspace Cargo.toml into the
# single-package vendored Cargo.toml, rsyncs source files, regenerates
# Cargo.lock, and writes a provenance marker.
#
# Called by .github/workflows/sync-cli-sdk.yml (daily) and can be run
# manually for ad-hoc syncs.
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "Usage: $0 <path-to-cli-sdk-checkout>" >&2
exit 1
fi

CLI_SDK_DIR="$(cd "$1" && pwd)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SDK_DIR="$(cd "$SCRIPT_DIR/../sdk" && pwd)"

if [[ ! -f "$CLI_SDK_DIR/Cargo.toml" ]]; then
echo "Error: $CLI_SDK_DIR/Cargo.toml not found" >&2
exit 1
fi

CLI_SDK_SHA="$(git -C "$CLI_SDK_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")"
CLI_SDK_SHORT="$(git -C "$CLI_SDK_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
SYNC_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"

echo "==> Syncing cli-sdk@${CLI_SDK_SHORT} (${CLI_SDK_SHA}) into generators/cli/sdk/"

# ---------------------------------------------------------------------------
# 1. Rsync source files with SDK_IGNORE rules (mirrors build.mjs SDK_IGNORE)
# ---------------------------------------------------------------------------
echo "--- Syncing src/, tests/, cli/openapi-fixture/ ..."

rsync -a --delete \
--exclude='.DS_Store' \
--exclude='target/' \
--exclude='.gitignore' \
--exclude='docs/' \
--exclude='tests/overlay_fixture.rs' \
--exclude='tests/fixtures/' \
--exclude='cli/openapi-fixture/' \
--exclude='.github/' \
--exclude='build.rs' \
--exclude='tests/common/' \
--exclude='tests/auth_routing_wire.rs' \
--exclude='tests/extension_surface_behavior.rs' \
--exclude='tests/lib_api.rs' \
--exclude='tests/tls_env_vars.rs' \
--exclude='changes/' \
"$CLI_SDK_DIR/src/" "$SDK_DIR/src/"

# Sync tests (only the ones not in SDK_IGNORE)
mkdir -p "$SDK_DIR/tests"
rsync -a --delete \
--exclude='.DS_Store' \
--exclude='overlay_fixture.rs' \
--exclude='fixtures/' \
--exclude='common/' \
--exclude='auth_routing_wire.rs' \
--exclude='extension_surface_behavior.rs' \
--exclude='lib_api.rs' \
--exclude='tls_env_vars.rs' \
"$CLI_SDK_DIR/tests/" "$SDK_DIR/tests/"

# Sync cli/openapi-fixture/ (the dev fixture used by seed)
mkdir -p "$SDK_DIR/cli/openapi-fixture"
rsync -a --delete \
"$CLI_SDK_DIR/cli/openapi-fixture/" "$SDK_DIR/cli/openapi-fixture/"

# ---------------------------------------------------------------------------
# 2. Project Cargo.toml (not a naive copy — workspace → single-package)
# ---------------------------------------------------------------------------
echo "--- Projecting Cargo.toml ..."

# Helper: extract all lines between a TOML [section] header and the next header.
extract_section() {
local file="$1" section="$2"
awk -v sect="$section" '
BEGIN { found=0 }
/^\[/ {
if (found) exit
# Match the section header exactly
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
if ($0 == "[" sect "]") { found=1; next }
}
found { print }
' "$file"
}

# Extract the workspace version from cli-sdk
WORKSPACE_VERSION="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "workspace.package" \
| grep '^version' | sed 's/.*"\(.*\)".*/\1/')"

if [[ -z "$WORKSPACE_VERSION" ]]; then
echo "Error: could not extract [workspace.package] version from cli-sdk" >&2
exit 1
fi

echo " version: $WORKSPACE_VERSION"

FEATURES_BODY="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "features")"
DEPS_BODY="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "dependencies")"
BUILD_DEPS_BODY="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "build-dependencies")"
DEV_DEPS_BODY="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "dev-dependencies")"
PROFILE_DIST_BODY="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "profile.dist")"
METADATA_DIST_BODY="$(extract_section "$CLI_SDK_DIR/Cargo.toml" "package.metadata.dist")"

# Build the projected Cargo.toml with Fern comment blocks intact
cat > "$SDK_DIR/Cargo.toml" << 'CARGO_HEADER'
# `name`, `repository`, `homepage`, `authors`, and `keywords` are Fern's —
# they identify the SDK template's source on crates.io. The fern-cli
# generator does NOT rewrite this block when producing your CLI; only the
# [[bin]] entry below is templated. If you want to publish *your* CLI as
# its own crate on crates.io, edit this block to your org's metadata.
# The [lib] name (`fern_cli_sdk`) is the import path every `use
# fern_cli_sdk::...` site in src/ depends on — do NOT rename it.
[package]
name = "fern-cli-sdk"
CARGO_HEADER

cat >> "$SDK_DIR/Cargo.toml" << EOF
version = "$WORKSPACE_VERSION"
edition = "2021"
description = "CLI generator — dynamic command surface from OpenAPI and GraphQL schemas"
license = "Apache-2.0"
repository = "https://github.com/fern-api/cli-sdk"
homepage = "https://github.com/fern-api/cli-sdk"
readme = "README.md"
authors = ["Fern <hey@buildwithfern.com>"]
keywords = ["cli", "openapi", "graphql", "fern", "codegen"]
categories = ["command-line-utilities", "web-programming"]

[lib]
name = "fern_cli_sdk"
path = "src/lib.rs"

# Rewritten by the fern-cli generator's \`patchCargoToml\` step — both the
# \`name\` and \`path\` are replaced with the derived binary name so users
# get \`cargo install\`-able binaries named after their API rather than
# the template's literal "openapi-fixture".
[[bin]]
name = "openapi-fixture"
path = "cli/openapi-fixture/main.rs"

# Internal tool used by the SDK template itself — not the user's CLI.
[[bin]]
name = "strip-schema"
path = "src/bin/strip_schema.rs"

[features]
$FEATURES_BODY

[dependencies]
$DEPS_BODY

[package.metadata.dist]
$METADATA_DIST_BODY
[profile.dist]
$PROFILE_DIST_BODY

[build-dependencies]
$BUILD_DEPS_BODY

[dev-dependencies]
$DEV_DEPS_BODY
EOF

# ---------------------------------------------------------------------------
# 3. Provenance marker
# ---------------------------------------------------------------------------
echo "--- Writing provenance marker ..."

cat > "$SDK_DIR/.synced-from" << EOF
cli-sdk@${CLI_SDK_SHA}
EOF

# ---------------------------------------------------------------------------
# 4. Regenerate Cargo.lock
# ---------------------------------------------------------------------------
echo "--- Regenerating Cargo.lock ..."
(cd "$SDK_DIR" && cargo generate-lockfile 2>&1) || {
echo "Warning: cargo generate-lockfile failed; Cargo.lock may be stale" >&2
}

# ---------------------------------------------------------------------------
# 5. Summary
# ---------------------------------------------------------------------------
echo ""
echo "==> Sync complete: cli-sdk@${CLI_SDK_SHORT} → generators/cli/sdk/"
echo " version: $WORKSPACE_VERSION"
echo " sha: $CLI_SDK_SHA"
echo " date: $SYNC_DATE"
echo ""
echo "Changed files:"
(cd "$SDK_DIR" && git diff --stat 2>/dev/null || true)
(cd "$SDK_DIR" && git diff --stat --cached 2>/dev/null || true)
echo ""
echo "Untracked files:"
(cd "$SDK_DIR" && git ls-files --others --exclude-standard 2>/dev/null || true)
15 changes: 12 additions & 3 deletions generators/python/core_utilities/shared/unchecked_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,10 @@ def construct_type(
if not isinstance(object_, typing.Mapping):
return object_

key_type, items_type = get_args(type_)
type_args = get_args(type_)
if not type_args:
return object_
key_type, items_type = type_args
key_type = _maybe_resolve_forward_ref(key_type, host)
items_type = _maybe_resolve_forward_ref(items_type, host)
d = {
Expand All @@ -373,14 +376,20 @@ def construct_type(
if not isinstance(object_, list):
return object_

inner_type = _maybe_resolve_forward_ref(get_args(type_)[0], host)
type_args = get_args(type_)
if not type_args:
return object_
inner_type = _maybe_resolve_forward_ref(type_args[0], host)
return [construct_type(object_=entry, type_=inner_type, host=host) for entry in object_]

if base_type == set:
if not isinstance(object_, set) and not isinstance(object_, list):
return object_

inner_type = _maybe_resolve_forward_ref(get_args(type_)[0], host)
type_args = get_args(type_)
if not type_args:
return object_
inner_type = _maybe_resolve_forward_ref(type_args[0], host)
return {construct_type(object_=entry, type_=inner_type, host=host) for entry in object_}

if is_union(base_type) or is_annotated_union:
Expand Down
Loading
Loading