fix(openapi): collapse dotted operationIds into valid endpoint names … #55502
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| update_snapshots: | |
| description: 'Automatically update test snapshots if they fail (applies to both regular tests and ETE tests)' | |
| required: false | |
| default: false | |
| type: boolean | |
| # Cancel previous workflows on previous push. | |
| # On PRs: group by PR number so new commits cancel stale runs. | |
| # On main: group by ref so rapid pushes share one slot instead of each | |
| # getting a parallel CI run (avoids runner starvation from seed waves). | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| DO_NOT_TRACK: "1" | |
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | |
| TURBO_TEAM: "buildwithfern" | |
| TURBO_NO_UPDATE_NOTIFIER: "1" | |
| TURBO_DAEMON: "false" | |
| jobs: | |
| # Detect whether this push only touched seed snapshot files. | |
| # If so, skip all CI jobs — seed changes cannot break the build. | |
| changes-filter: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should-skip-ci: ${{ steps.check.outputs.should-skip }} | |
| steps: | |
| - name: Check if push only touches seed files | |
| id: check | |
| env: | |
| COMMIT_MSG: ${{ github.event.head_commit.message }} | |
| run: | | |
| # Only consider skipping for pushes to main. PRs and dispatches always run CI. | |
| if [[ "${{ github.event_name }}" != "push" || "${{ github.ref }}" != "refs/heads/main" ]]; then | |
| echo "should-skip=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # Fast path: if the commit message matches the seed update pattern, skip CI. | |
| # Seed snapshot changes cannot break the build, so full CI is unnecessary. | |
| FIRST_LINE=$(echo "$COMMIT_MSG" | head -1) | |
| if echo "$FIRST_LINE" | grep -qP '^chore\(.+\): update .+-sdk seed|^chore\(.+\): update .+-model seed|^chore\(openapi\): update openapi seed|^chore\(seed\): update'; then | |
| echo "should-skip=true" >> $GITHUB_OUTPUT | |
| echo "Seed-only commit detected — skipping CI." | |
| else | |
| echo "should-skip=false" >> $GITHUB_OUTPUT | |
| fi | |
| compile: | |
| needs: [changes-filter] | |
| if: needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !seed | |
| !seed-remote-local | |
| packages/seed/** | |
| docker/seed/** | |
| sparse-checkout-cone-mode: false | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Compile | |
| run: pnpm compile | |
| boundaries: | |
| needs: [changes-filter] | |
| if: needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !seed | |
| !seed-remote-local | |
| packages/seed/** | |
| docker/seed/** | |
| sparse-checkout-cone-mode: false | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Check turbo boundaries | |
| run: pnpm boundaries | |
| depcheck: | |
| needs: [changes-filter] | |
| if: needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !seed | |
| !seed-remote-local | |
| packages/seed/** | |
| docker/seed/** | |
| sparse-checkout-cone-mode: false | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Check dependencies | |
| run: pnpm depcheck | |
| validate-cli-dependencies: | |
| needs: [changes-filter] | |
| if: needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !seed | |
| !seed-remote-local | |
| packages/seed/** | |
| docker/seed/** | |
| sparse-checkout-cone-mode: false | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Validate CLI dependencies | |
| run: node scripts/validate-cli-dependencies.js | |
| lint: | |
| needs: [changes-filter] | |
| if: needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !seed | |
| !seed-remote-local | |
| packages/seed/** | |
| docker/seed/** | |
| sparse-checkout-cone-mode: false | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Lint | |
| run: pnpm lint:style | |
| - name: Generate JSON Schema | |
| run: | | |
| pnpm fern:build | |
| pnpm jsonschema | |
| - name: Generate docs SDK | |
| run: pnpm docs:generate | |
| - name: Format | |
| run: pnpm format:check | |
| - name: Ensure no changes to git-tracked files (run `pnpm update:docs` and `pnpm jsonschema` locally and commit the result if this fails) | |
| run: git --no-pager diff --exit-code | |
| biome: | |
| needs: [changes-filter] | |
| if: needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| /* | |
| !seed | |
| !seed-remote-local | |
| packages/seed/** | |
| docker/seed/** | |
| sparse-checkout-cone-mode: false | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: check:biome | |
| run: pnpm check:biome | |
| test: | |
| needs: [changes-filter] | |
| if: github.repository == 'fern-api/fern' && needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: Test | |
| timeout-minutes: 25 | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| with: | |
| lfs: true | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Cache Fern bin dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.fern/bin | |
| key: ${{ runner.os }}-fern-bin-${{ hashFiles('packages/cli/workspace/lazy-fern-workspace/src/protobuf/BufDownloader.ts', 'packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtocGenOpenAPIDownloader.ts') }} | |
| restore-keys: | | |
| ${{ runner.os }}-fern-bin- | |
| - name: Build CLI for install-dependencies | |
| run: pnpm turbo run dist:cli:dev --filter=@fern-api/cli | |
| - name: Install protobuf dependencies (buf + protoc-gen-openapi) | |
| run: | | |
| FERN_NO_VERSION_REDIRECTION=true node packages/cli/cli/dist/dev/cli.cjs install-dependencies | |
| echo "$HOME/.fern/bin" >> $GITHUB_PATH | |
| - name: Stop any stale turbo daemon | |
| run: pnpm turbo daemon stop || true | |
| - name: Run tests | |
| if: ${{ !github.event.inputs.update_snapshots }} | |
| timeout-minutes: 20 | |
| run: ./scripts/turbo-retry.sh pnpm test | |
| - name: Run tests with snapshot updates | |
| if: ${{ github.event.inputs.update_snapshots == 'true' }} | |
| timeout-minutes: 20 | |
| run: ./scripts/turbo-retry.sh pnpm test:update | |
| - name: Commit and push updated snapshots | |
| if: ${{ github.event.inputs.update_snapshots == 'true' }} | |
| run: | | |
| git config --global user.name 'github-actions[bot]' | |
| git config --global user.email 'github-actions[bot]@users.noreply.github.com' | |
| git add . | |
| if ! git diff --staged --quiet; then | |
| git commit -m "chore: update test snapshots" | |
| git push | |
| else | |
| echo "No snapshot changes to commit" | |
| fi | |
| - name: Ensure no changes to git-tracked files | |
| if: ${{ !github.event.inputs.update_snapshots }} | |
| run: git --no-pager diff --exit-code | |
| test-ete: | |
| needs: [changes-filter] | |
| if: github.repository == 'fern-api/fern' && needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: Test | |
| timeout-minutes: 25 | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Log in to Docker Hub | |
| if: ${{ env.DOCKERHUB_USERNAME != '' }} | |
| uses: docker/login-action@v4 | |
| env: | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME_PUBLIC_READONLY }} | |
| with: | |
| username: ${{ secrets.DOCKER_USERNAME_PUBLIC_READONLY }} | |
| password: ${{ secrets.DOCKER_PASSWORD_PUBLIC_READONLY }} | |
| - name: Clear stale Docker image cache | |
| run: rm -rf /tmp/docker-cache | |
| - name: Cache Docker images | |
| id: docker-cache | |
| uses: actions/cache@v5 | |
| with: | |
| path: /tmp/docker-cache | |
| key: ${{ runner.os }}-docker-ete-${{ hashFiles('**/Dockerfile*') }} | |
| restore-keys: | | |
| ${{ runner.os }}-docker-ete- | |
| - name: Load cached Docker images | |
| run: | | |
| for f in /tmp/docker-cache/*.tar; do | |
| [ -f "$f" ] && docker load -i "$f" || true | |
| done | |
| - name: Record pre-existing Docker images | |
| run: docker images -q | sort -u > /tmp/docker-images-before.txt | |
| - name: Cache Fern bin dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: ~/.fern/bin | |
| key: ${{ runner.os }}-fern-bin-${{ hashFiles('packages/cli/workspace/lazy-fern-workspace/src/protobuf/BufDownloader.ts', 'packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtocGenOpenAPIDownloader.ts') }} | |
| restore-keys: | | |
| ${{ runner.os }}-fern-bin- | |
| - name: Build CLI for install-dependencies | |
| run: pnpm turbo run dist:cli:dev --filter=@fern-api/cli | |
| - name: Install protobuf dependencies (buf + protoc-gen-openapi) | |
| run: | | |
| FERN_NO_VERSION_REDIRECTION=true node packages/cli/cli/dist/dev/cli.cjs install-dependencies | |
| echo "$HOME/.fern/bin" >> $GITHUB_PATH | |
| - name: Stop any stale turbo daemon | |
| run: pnpm turbo daemon stop || true | |
| - name: Build CLIs (dev + seed) | |
| timeout-minutes: 10 | |
| run: | | |
| ./scripts/turbo-retry.sh pnpm turbo run dist:cli:dev dist:cli --filter=@fern-api/cli --filter=@fern-api/cli-v2 --filter=@fern-api/seed-cli | |
| - name: Pre-pull Docker images used by ETE tests | |
| run: docker compose -f packages/cli/ete-tests/docker-compose.yml pull | |
| - name: Run ETE tests | |
| if: ${{ !github.event.inputs.update_snapshots }} | |
| timeout-minutes: 20 | |
| env: | |
| FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} | |
| run: | | |
| FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm --filter @fern-api/ete-tests test | |
| - name: Run ETE tests with snapshot updates | |
| if: ${{ github.event.inputs.update_snapshots == 'true' }} | |
| timeout-minutes: 20 | |
| env: | |
| FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} | |
| run: | | |
| FERN_TOKEN=${{ secrets.FERN_ORG_TOKEN_DEV }} pnpm --filter @fern-api/ete-tests test -- -u | |
| - name: Commit and push updated snapshots | |
| if: ${{ github.event.inputs.update_snapshots == 'true' }} | |
| run: | | |
| git config --global user.name 'github-actions[bot]' | |
| git config --global user.email 'github-actions[bot]@users.noreply.github.com' | |
| git add . | |
| if ! git diff --staged --quiet; then | |
| git commit -m "chore: update ETE test snapshots" | |
| git push | |
| else | |
| echo "No snapshot changes to commit" | |
| fi | |
| - name: Save Docker images for cache | |
| if: always() | |
| run: | | |
| if [ ! -f /tmp/docker-images-before.txt ]; then | |
| echo "Skipping cache save: docker-images-before.txt not found" | |
| exit 0 | |
| fi | |
| mkdir -p /tmp/docker-cache | |
| comm -13 /tmp/docker-images-before.txt <(docker images -q | sort -u) | while read -r id; do | |
| img=$(docker inspect --format '{{index .RepoTags 0}}' "$id" 2>/dev/null || true) | |
| [ -z "$img" ] && continue | |
| fname=$(echo "$img" | tr '/:' '_') | |
| docker save "$img" -o "/tmp/docker-cache/${fname}.tar" 2>/dev/null || true | |
| done | |
| - name: Ensure no changes to git-tracked files | |
| if: ${{ !github.event.inputs.update_snapshots }} | |
| run: git --no-pager diff --exit-code | |
| live-test-dev: | |
| needs: [changes-filter] | |
| environment: Fern Dev | |
| if: github.ref == 'refs/heads/main' && github.repository == 'fern-api/fern' && needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Check API definition is valid | |
| env: | |
| FORCE_COLOR: "2" | |
| FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} | |
| AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} | |
| AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} | |
| run: | | |
| pnpm turbo run dist:cli:dev --filter @fern-api/cli | |
| cli_path="$(pwd)/packages/cli/cli/dist/dev/cli.cjs" | |
| ./scripts/live-test.sh "$cli_path" "$FERN_ORG_TOKEN_DEV" | |
| generate-docs-test-windows: | |
| needs: [changes-filter] | |
| environment: Fern Dev | |
| if: github.ref == 'refs/heads/main' && github.repository == 'fern-api/fern' && needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Assert docs are generated | |
| env: | |
| FORCE_COLOR: "2" | |
| FERN_TOKEN: ${{ secrets.FERN_ORG_TOKEN_DEV }} | |
| AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} | |
| AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} | |
| run: | | |
| pnpm turbo run dist:cli:dev --filter @fern-api/cli | |
| $cliPath = Join-Path $env:GITHUB_WORKSPACE "packages\cli\cli\dist\dev\cli.cjs" | |
| if (-not (Test-Path $cliPath)) { | |
| Write-Error "CLI path does not exist: $cliPath" | |
| exit 1 | |
| } | |
| cd windows | |
| node $cliPath generate --docs --log-level trace | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Error "Command failed with exit code $LASTEXITCODE" | |
| exit $LASTEXITCODE | |
| } | |
| live-test-dev-windows: | |
| needs: [changes-filter] | |
| environment: Fern Dev | |
| if: github.ref == 'refs/heads/main' && github.repository == 'fern-api/fern' && needs.changes-filter.outputs.should-skip-ci != 'true' | |
| runs-on: windows-latest | |
| steps: | |
| - name: Checkout repo | |
| uses: actions/checkout@v6 | |
| - name: Install | |
| uses: ./.github/actions/install | |
| - name: Set up Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| - name: Check API definition is valid | |
| env: | |
| FORCE_COLOR: "2" | |
| FERN_ORG_TOKEN_DEV: ${{ secrets.FERN_ORG_TOKEN_DEV }} | |
| AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} | |
| AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }} | |
| run: | | |
| pnpm turbo run dist:cli:dev --filter @fern-api/cli | |
| $cliPath = Join-Path $env:GITHUB_WORKSPACE "packages\cli\cli\dist\dev\cli.cjs" | |
| if (-not (Test-Path $cliPath)) { | |
| Write-Error "CLI path does not exist: $cliPath" | |
| exit 1 | |
| } | |
| $scriptPath = Join-Path $env:GITHUB_WORKSPACE "scripts\live-test.ps1" | |
| & $scriptPath -cli_path $cliPath -token $env:FERN_ORG_TOKEN_DEV |