-
Notifications
You must be signed in to change notification settings - Fork 303
fix(cli): run Windows post-processing on cached bundles + add docs preview smoke test #14765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
b02c1e3
581738f
500a7e3
af57ae8
807c34d
b2f02a0
0727316
4884ac3
779e3cc
8b2687c
822289e
e9114db
a2b258f
75862d8
e615642
59d498a
f5716c7
ba5e63d
6d9c272
164a4f6
e7b8e20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| name: Docs Preview Smoke Test | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - ".github/workflows/docs-preview-smoke-test.yml" | ||
| - "packages/cli/docs-preview/**" | ||
| - "packages/cli/cli/**" | ||
| - "smoke-test/**" | ||
| pull_request: | ||
| paths: | ||
| - ".github/workflows/docs-preview-smoke-test.yml" | ||
| - "packages/cli/docs-preview/**" | ||
| - "packages/cli/cli/**" | ||
| - "smoke-test/**" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | ||
|
|
||
| env: | ||
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | ||
| TURBO_TEAM: "buildwithfern" | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Install dependencies | ||
| uses: ./.github/actions/install | ||
|
|
||
| - name: Compile packages | ||
| run: pnpm compile | ||
|
|
||
| - name: Build CLI (prod) | ||
| run: pnpm turbo run dist:cli:prod --filter=@fern-api/cli | ||
|
|
||
| - name: Upload CLI artifact | ||
| uses: actions/upload-artifact@v6 | ||
| with: | ||
| name: cli-prod-bundle | ||
| path: packages/cli/cli/dist/prod/ | ||
| retention-days: 1 | ||
|
|
||
| smoke-test: | ||
| needs: build | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ubuntu-latest, macos-latest, windows-latest] | ||
| node-version: [20, 22, 24, 26] | ||
| runs-on: ${{ matrix.os }} | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
|
|
||
| steps: | ||
| - name: Disable Windows Defender real-time monitoring | ||
| if: runner.os == 'Windows' | ||
| shell: powershell | ||
| run: Set-MpPreference -DisableRealtimeMonitoring $true | ||
|
|
||
| - name: Check Node.js version availability | ||
| id: node-check | ||
| shell: bash | ||
| run: | | ||
| NODE_VERSION="${{ matrix.node-version }}" | ||
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://nodejs.org/dist/latest-v${NODE_VERSION}.x/") | ||
| if [ "$HTTP_STATUS" = "200" ]; then | ||
| echo "available=true" >> $GITHUB_OUTPUT | ||
| echo "Node.js v${NODE_VERSION} is available" | ||
| else | ||
| echo "available=false" >> $GITHUB_OUTPUT | ||
| echo "Node.js v${NODE_VERSION} is not yet available (HTTP $HTTP_STATUS) — skipping" | ||
| fi | ||
|
|
||
| - name: Checkout repository | ||
| if: steps.node-check.outputs.available == 'true' | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Setup Node.js | ||
| if: steps.node-check.outputs.available == 'true' | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| package-manager-cache: false | ||
|
|
||
| - name: Setup Go for protoc-gen-openapi | ||
| if: steps.node-check.outputs.available == 'true' | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version: "1.22" | ||
|
|
||
| - name: Download CLI artifact | ||
| if: steps.node-check.outputs.available == 'true' | ||
| uses: actions/download-artifact@v7 | ||
| with: | ||
| name: cli-prod-bundle | ||
| path: ${{ runner.temp }}/cli-artifact | ||
|
|
||
| - name: Install CLI native dependencies | ||
| if: steps.node-check.outputs.available == 'true' | ||
| shell: bash | ||
| run: | | ||
| cd "$RUNNER_TEMP/cli-artifact" | ||
| # The build resolves pnpm catalog: references to real versions. | ||
| # Run npm install in the isolated temp directory so npm doesn't | ||
| # walk up into the repo's pnpm workspace. | ||
| npm install --ignore-scripts | ||
|
|
||
| - name: Install tools (Linux) | ||
| if: steps.node-check.outputs.available == 'true' && runner.os == 'Linux' | ||
| run: | | ||
| ( | ||
| cd smoke-test/playwright && npm install && npx playwright install chromium | ||
| echo "Playwright installed" | ||
| ) & | ||
| PW_PID=$! | ||
|
|
||
| ( | ||
| sudo apt-get update && sudo apt-get install -y protobuf-compiler | ||
| BUF_VERSION=1.35.1 | ||
| curl -sSL "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Linux-x86_64.tar.gz" | sudo tar -xzC /usr/local --strip-components=1 | ||
| go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@v0.1.12 | ||
| echo "$(go env GOPATH)/bin" >> $GITHUB_PATH | ||
| echo "Proto toolchain installed" | ||
| ) & | ||
| PROTO_PID=$! | ||
|
|
||
| wait $PW_PID | ||
| wait $PROTO_PID | ||
|
|
||
| - name: Install tools (macOS) | ||
| if: steps.node-check.outputs.available == 'true' && runner.os == 'macOS' | ||
| run: | | ||
| ( | ||
| cd smoke-test/playwright && npm install && npx playwright install chromium | ||
| echo "Playwright installed" | ||
| ) & | ||
| PW_PID=$! | ||
|
|
||
| ( | ||
| brew install protobuf buf | ||
| go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@v0.1.12 | ||
| echo "$(go env GOPATH)/bin" >> $GITHUB_PATH | ||
| echo "Proto toolchain installed" | ||
| ) & | ||
| PROTO_PID=$! | ||
|
|
||
| wait $PW_PID | ||
| wait $PROTO_PID | ||
|
|
||
| - name: Install tools (Windows) | ||
| if: steps.node-check.outputs.available == 'true' && runner.os == 'Windows' | ||
| shell: bash | ||
| run: | | ||
| ( | ||
| cd smoke-test/playwright && npm install && npx playwright install chromium | ||
| echo "Playwright installed" | ||
| ) & | ||
| PW_PID=$! | ||
|
|
||
| ( | ||
| # Install protoc via direct download | ||
| PROTOC_VERSION=28.3 | ||
| curl -sSL -o "$RUNNER_TEMP/protoc.zip" "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-win64.zip" | ||
| PROTOC_DIR="$RUNNER_TEMP/protoc" | ||
| mkdir -p "$PROTOC_DIR" | ||
| unzip -qo "$RUNNER_TEMP/protoc.zip" -d "$PROTOC_DIR" | ||
| echo "$PROTOC_DIR/bin" >> $GITHUB_PATH | ||
| # Install buf | ||
| BUF_VERSION=1.35.1 | ||
| TOOLS_DIR="$RUNNER_TEMP/tools" | ||
| mkdir -p "$TOOLS_DIR" | ||
| curl -sSL -o "$TOOLS_DIR/buf.exe" "https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-Windows-x86_64.exe" | ||
| echo "$TOOLS_DIR" >> $GITHUB_PATH | ||
| go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@v0.1.12 | ||
| echo "$(go env GOPATH)/bin" >> $GITHUB_PATH | ||
| echo "Proto toolchain installed" | ||
| ) & | ||
| PROTO_PID=$! | ||
|
|
||
| wait $PW_PID | ||
| wait $PROTO_PID | ||
|
|
||
| - name: Re-enable Windows Defender real-time monitoring | ||
| if: runner.os == 'Windows' | ||
| shell: powershell | ||
| run: Set-MpPreference -DisableRealtimeMonitoring $false | ||
|
|
||
| - name: Start fern docs dev server | ||
| if: steps.node-check.outputs.available == 'true' | ||
| shell: bash | ||
| env: | ||
| FERN_TOKEN: ${{ secrets.FERN_TOKEN }} | ||
| run: | | ||
| cd smoke-test/fern | ||
|
|
||
| # Use the locally-built CLI instead of the published npm version. | ||
| # The CLI will download the production docs bundle from S3 automatically. | ||
| FERN_NO_VERSION_REDIRECTION=true node "$RUNNER_TEMP/cli-artifact/cli.cjs" docs dev > /tmp/fern-docs-dev.log 2>&1 & | ||
| FERN_PID=$! | ||
| echo "FERN_PID=$FERN_PID" >> $GITHUB_ENV | ||
|
|
||
| echo "Waiting for fern docs dev server to start (PID: $FERN_PID)..." | ||
| for i in $(seq 1 120); do | ||
| if grep -q "Docs preview server ready" /tmp/fern-docs-dev.log 2>/dev/null; then | ||
| echo "Server is ready after ${i}s!" | ||
| echo "--- Dev server logs ---" | ||
| cat /tmp/fern-docs-dev.log | ||
| echo "--- End dev server logs ---" | ||
| break | ||
| fi | ||
|
|
||
| if ! kill -0 $FERN_PID 2>/dev/null; then | ||
| echo "fern docs dev process (PID: $FERN_PID) exited unexpectedly" | ||
| echo "--- Dev server logs ---" | ||
| cat /tmp/fern-docs-dev.log | ||
| echo "--- End dev server logs ---" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ $i -eq 120 ]; then | ||
| echo "Timeout waiting for server to start after 120s" | ||
| echo "--- Dev server logs ---" | ||
| cat /tmp/fern-docs-dev.log | ||
| echo "--- End dev server logs ---" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ $((i % 10)) -eq 0 ]; then | ||
| echo "--- Dev server logs at attempt $i ---" | ||
| tail -20 /tmp/fern-docs-dev.log | ||
| echo "--- End dev server logs ---" | ||
| fi | ||
|
|
||
| sleep 1 | ||
| done | ||
|
|
||
| - name: Run Playwright smoke tests | ||
| if: steps.node-check.outputs.available == 'true' | ||
| shell: bash | ||
| working-directory: smoke-test/playwright | ||
| run: npx playwright test smoke.spec.ts --config playwright.config.ts | ||
|
|
||
| - name: Upload Playwright report | ||
| uses: actions/upload-artifact@v6 | ||
| if: always() && steps.node-check.outputs.available == 'true' | ||
| with: | ||
| name: smoke-test-playwright-report-${{ matrix.os }}-node${{ matrix.node-version }} | ||
| path: smoke-test/playwright/playwright-report/ | ||
| retention-days: 7 | ||
|
|
||
| - name: Print fern docs dev logs | ||
| if: always() && steps.node-check.outputs.available == 'true' | ||
| shell: bash | ||
| run: cat /tmp/fern-docs-dev.log || true | ||
|
|
||
| - name: Stop fern docs dev server | ||
| if: always() && steps.node-check.outputs.available == 'true' | ||
| shell: bash | ||
| run: | | ||
| if [ -n "$FERN_PID" ]; then | ||
| kill $FERN_PID 2>/dev/null || true | ||
| fi | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,4 +1,5 @@ | ||||||||||||||||||||
| import { exec } from "child_process"; | ||||||||||||||||||||
| import { readFileSync } from "fs"; | ||||||||||||||||||||
| import { writeFile } from "fs/promises"; | ||||||||||||||||||||
| import path from "path"; | ||||||||||||||||||||
| import tsup from "tsup"; | ||||||||||||||||||||
|
|
@@ -10,12 +11,57 @@ const execAsync = promisify(exec); | |||||||||||||||||||
|
|
||||||||||||||||||||
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Parse the pnpm-workspace.yaml catalog section to resolve catalog: references. | ||||||||||||||||||||
| * This is a lightweight parser that handles the simple `key: value` format used in the catalog. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function loadPnpmCatalog() { | ||||||||||||||||||||
| const workspaceRoot = path.resolve(__dirname, "../../.."); | ||||||||||||||||||||
| const workspacePath = path.join(workspaceRoot, "pnpm-workspace.yaml"); | ||||||||||||||||||||
| try { | ||||||||||||||||||||
| const content = readFileSync(workspacePath, "utf-8"); | ||||||||||||||||||||
| const catalog = {}; | ||||||||||||||||||||
| let inCatalog = false; | ||||||||||||||||||||
| for (const line of content.split("\n")) { | ||||||||||||||||||||
| if (/^catalog:/.test(line)) { | ||||||||||||||||||||
| inCatalog = true; | ||||||||||||||||||||
| continue; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if (inCatalog) { | ||||||||||||||||||||
| // Stop when we hit a non-indented line (new top-level key) | ||||||||||||||||||||
| if (line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t")) { | ||||||||||||||||||||
| break; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| const match = line.match(/^\s+["']?([^"':]+)["']?:\s*["']?([^"'\s]+)["']?/); | ||||||||||||||||||||
| if (match) { | ||||||||||||||||||||
| catalog[match[1]] = match[2]; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return catalog; | ||||||||||||||||||||
| } catch { | ||||||||||||||||||||
| return {}; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Empty catch block silently swallows errors in loadPnpmCatalog The
Suggested change
Was this helpful? React with 👍 or 👎 to provide feedback.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch — fixed in 8b2687c. The catch block now logs a warning with the error message before returning the empty fallback. |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const pnpmCatalog = loadPnpmCatalog(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
| * Get a dependency version from package.json, preferring dependencies over devDependencies. | ||||||||||||||||||||
| * This ensures we don't miss runtime dependencies regardless of where they're declared. | ||||||||||||||||||||
| * Resolves pnpm catalog: references to actual version specifiers. | ||||||||||||||||||||
| */ | ||||||||||||||||||||
| function getDependencyVersion(packageName) { | ||||||||||||||||||||
| return packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName]; | ||||||||||||||||||||
| const version = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName]; | ||||||||||||||||||||
| if (version === "catalog:" || version === "catalog:default") { | ||||||||||||||||||||
| const catalogVersion = pnpmCatalog[packageName]; | ||||||||||||||||||||
| if (!catalogVersion) { | ||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||
| `Dependency "${packageName}" uses catalog: but was not found in pnpm-workspace.yaml catalog` | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return catalogVersion; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return version; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Windows Defender is re-enabled too early, before the main test runs. The workflow disables Defender at line 68-71 to speed up tool installation, but then re-enables it at line 196-199 BEFORE the "Start fern docs dev server" step (line 201-250). The fern docs dev server performs a slow
pnpm installon Windows (as noted in the comment on line 215: "Windows bundle installation (download + pnpm install) can take 3-5 minutes"), which will be significantly slower with Defender enabled.Move the re-enable step to after all tests complete:
Spotted by Graphite

Is this helpful? React 👍 or 👎 to let us know.