Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b02c1e3
fix(cli): run Windows post-processing on cached bundles
devin-ai-integration[bot] Apr 8, 2026
581738f
chore(ci): add docs preview smoke test workflow
devin-ai-integration[bot] Apr 8, 2026
500a7e3
fix(ci): remove hardcoded pnpm version in smoke test workflow
devin-ai-integration[bot] Apr 8, 2026
af57ae8
fix(ci): use prod CLI build, install native deps, add playwright pack…
devin-ai-integration[bot] Apr 8, 2026
807c34d
fix(ci): download CLI artifact outside repo tree to avoid catalog: pr…
devin-ai-integration[bot] Apr 8, 2026
b2f02a0
fix(ci): use /tmp for CLI artifact to fully isolate from repo package…
devin-ai-integration[bot] Apr 8, 2026
0727316
fix(ci): resolve pnpm catalog: versions in CLI build artifact
devin-ai-integration[bot] Apr 8, 2026
4884ac3
fix(ci): remove flaky API reference pages from smoke test
devin-ai-integration[bot] Apr 8, 2026
779e3cc
feat(ci): add windows-latest to docs preview smoke test matrix
devin-ai-integration[bot] Apr 8, 2026
8b2687c
fix(ci): log warning instead of silently swallowing errors in loadPnp…
devin-ai-integration[bot] Apr 8, 2026
822289e
fix(ci): fix biome noConsole lint error in loadPnpmCatalog
devin-ai-integration[bot] Apr 8, 2026
e9114db
fix(ci): increase server startup timeout to 300s for Windows bundle i…
devin-ai-integration[bot] Apr 8, 2026
a2b258f
fix(ci): add diagnostic curl step to capture Windows 500 response body
devin-ai-integration[bot] Apr 8, 2026
75862d8
fix(ci): prevent diagnostic curl SIGPIPE failure in smoke test
devin-ai-integration[bot] Apr 8, 2026
e615642
fix(cli): resolve Windows symlinks as junctions/copies after bundle e…
devin-ai-integration[bot] Apr 8, 2026
59d498a
fix(cli): fix biome import ordering and formatting
devin-ai-integration[bot] Apr 8, 2026
f5716c7
fix(ci): add Windows debug step to dump CLI debug log and check symli…
devin-ai-integration[bot] Apr 8, 2026
ba5e63d
fix(cli): resolve symlinks before pnpm i esbuild to prevent target pr…
devin-ai-integration[bot] Apr 8, 2026
6d9c272
fix(cli): fix TS2532 undefined check on path parts array
devin-ai-integration[bot] Apr 8, 2026
164a4f6
fix(cli): two-phase backup/restore for Windows file-traced packages
devin-ai-integration[bot] Apr 8, 2026
e7b8e20
fix(cli): remove backup/restore workaround and debug steps, fix at so…
devin-ai-integration[bot] Apr 9, 2026
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
232 changes: 232 additions & 0 deletions .github/workflows/docs-preview-smoke-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
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]
node-version: [20, 22, 24, 26]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
permissions:
contents: read

steps:
- 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: /tmp/cli-artifact

- name: Install CLI native dependencies
if: steps.node-check.outputs.available == 'true'
shell: bash
run: |
cd /tmp/cli-artifact
# The build resolves pnpm catalog: references to real versions.
# Run npm install in the isolated /tmp 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: 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 "/tmp/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
50 changes: 48 additions & 2 deletions packages/cli/cli/build-utils.mjs
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";
Expand All @@ -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 {};
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Empty catch block silently swallows errors in loadPnpmCatalog

The catch {} block at packages/cli/cli/build-utils.mjs:42 silently discards any error from readFileSync or the YAML parsing logic — no error variable is bound, nothing is logged, and no comment explains why. If the pnpm-workspace.yaml file exists but is unreadable (permissions) or malformed, the function silently returns {}. This causes a confusing downstream error in getDependencyVersion (line 58) that says the dependency "was not found in pnpm-workspace.yaml catalog" — masking the real root cause. This violates the REVIEW.md / CLAUDE.md rule: "Never swallow errors with empty catch blocks — at minimum log the error. If you truly want to ignore it, add a comment explaining why."

Suggested change
} catch {
return {};
}
} catch (error) {
// Gracefully degrade if workspace file is missing (e.g. running outside the monorepo).
// Catalog references will still fail explicitly in getDependencyVersion().
console.warn(`Warning: could not load pnpm catalog from pnpm-workspace.yaml: ${error.message}`);
return {};
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
}

/**
Expand Down
Loading
Loading