Skip to content
Merged
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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,32 @@ jobs:
run: pnpm --filter astro-fern exec playwright install --with-deps chromium

- name: Run end-to-end tests
run: pnpm test:e2e
run: pnpm --filter astro-fern test:e2e

docs-site:
name: Docs site end-to-end tests
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v7.0.1

- uses: ./.github/actions/setup-workspace

- name: Install Playwright browser
run: pnpm --filter cloudflare-api-site exec playwright install --with-deps chromium

# The tests run against `astro preview`, which serves the last build.
- name: Build
run: pnpm --filter cloudflare-api-site build

- name: Run end-to-end tests
run: pnpm --filter cloudflare-api-site test:e2e

- name: Upload test results
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: docs-site-e2e-results
path: packages/cloudflare-api-site/e2e/__test-results__
retention-days: 7
10 changes: 4 additions & 6 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,11 @@ jobs:

- uses: ./.github/actions/setup-workspace

- name: Build
run: pnpm --filter cloudflare-api-site build
# `cf deploy` builds the site (`astro build`) and deploys it. `run` is
# needed because `pnpm deploy` is a built-in pnpm command.
- name: Build and deploy
run: pnpm --filter cloudflare-api-site run deploy
env:
FORGE_OPENAPI_RELEASE: ${{ github.event.release.tag_name || inputs.openapi_release || '' }}

- name: Deploy
run: pnpm --dir packages/cloudflare-api-site exec wrangler deploy
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
66 changes: 66 additions & 0 deletions .github/workflows/preview-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Deploys each pull request's docs site as a Worker Preview of the `docs`
# Worker and comments the preview URL on the pull request. The preview is
# named after the branch, so new pushes update the same preview.
#
# Pull requests from forks are skipped: they get no repository secrets.
# CI's checks still build them.

name: Preview docs

on:
pull_request:
branches: [main]
paths:
- 'packages/astro-fern/**'
- 'packages/fern-forge/**'
- 'packages/cloudflare-api-site/**'
- 'pnpm-lock.yaml'
- '.github/workflows/preview-docs.yml'

concurrency:
group: preview-docs-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
preview:
name: Preview docs
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pull-requests: write

steps:
- uses: actions/checkout@v7.0.1

- uses: ./.github/actions/setup-workspace

# Preview URLs come from `previewUrls` in cloudflare.config.ts, which a
# production deploy must apply first.
- name: Deploy preview
run: pnpm --filter cloudflare-api-site run deploy:preview | tee "$RUNNER_TEMP/preview.log"
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

# TODO: uncomment once we're out of experimental
# The build logs to stdout too; the JSON result is the last top-level

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think that cf previews deploy support WRANGLER_OUTPUT_FILE_DIRECTORY env var, that results in a nice clean JSONL file being written to disk. So you don't have to rely upon the stdout.

That being said, I think that after cloudflare/cf#355 we might have a clean stdout with the build logs sent to stderr??

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good to know! For now I commented out the deploy preview because the site is still internal, and I don't want to leak it (I deleted the comment too)

# object in the output.
# - name: Comment preview URL
# env:
# GH_TOKEN: ${{ github.token }}
# PR: ${{ github.event.pull_request.number }}
# SHA: ${{ github.event.pull_request.head.sha }}
# run: |
# url=$(node -e '
# const log = require("node:fs").readFileSync(process.argv[1], "utf8");
# const start = log.lastIndexOf("\n{");
# const result = JSON.parse(log.slice(start + 1));
# const url = result.preview_urls?.[0] ?? result.deployment_urls?.[0];
# if (!url) throw new Error("No preview URL in cf output");
# console.log(url);
# ' "$RUNNER_TEMP/preview.log")
# body="Docs preview for ${SHA}: ${url%/}/api/"
# echo "$body" >> "$GITHUB_STEP_SUMMARY"
# gh pr comment "$PR" --repo "$GITHUB_REPOSITORY" --edit-last --create-if-none --body "$body"
1 change: 1 addition & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"ignorePatterns": [
"**/_generated/**",
"dist",
".cloudflare",
".wrangler",
"Cargo.toml",
"packages/cloudflare-api-site/src/components/ui/**/*.ts",
Expand Down
2 changes: 1 addition & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"typescript/no-floating-promises": "warn",
"typescript/no-misused-promises": "warn"
},
"ignorePatterns": ["node_modules", "dist", "**/_generated/**", ".wrangler"],
"ignorePatterns": ["node_modules", "dist", "**/_generated/**", ".cloudflare", ".wrangler"],
"overrides": [
{
"files": ["packages/astro-fern/**", "packages/cloudflare-api-site/**", "packages/fern-forge/**"],
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"check": "pnpm format:check && pnpm lint",
"typecheck": "pnpm -r run typecheck",
"test": "pnpm -r run test",
"test:e2e": "pnpm --filter astro-fern test:e2e",
"test:e2e": "pnpm -r run test:e2e",
"build": "pnpm -r run build",
"generate:terraform-docs": "pnpm --filter cloudflare-terraform-docs generate --output ../cloudflare-api-site/src/generated/terraform-docs.json"
},
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare-api-site/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dist/
.astro/
.cloudflare/
.wrangler/
public/_astro-fern/

Expand Down
19 changes: 15 additions & 4 deletions packages/cloudflare-api-site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,23 @@ WebAssembly runtime is unavailable in the deployed Worker.
From the repository root after `pnpm install`:

```bash
pnpm --filter cloudflare-api-site dev
pnpm --filter cloudflare-api-site dev # cf dev
pnpm --filter cloudflare-api-site check
pnpm --filter cloudflare-api-site build
pnpm --filter cloudflare-api-site preview
pnpm --filter cloudflare-api-site build # cf build
pnpm --filter cloudflare-api-site preview # serve the last build locally
pnpm --filter cloudflare-api-site deploy # cf deploy
pnpm --filter cloudflare-api-site deploy:preview # cf previews deploy
```

The Worker is configured in `cloudflare.config.ts` and built and deployed with
the [Cloudflare CLI](https://developers.cloudflare.com/cf/) (`cf`). `cf build`
runs `astro build` and writes Build Output to `.cloudflare/output/v0/`.
`cf previews deploy` deploys a Worker Preview named after the current branch,
served at `<preview-name>-docs.<subdomain>.workers.dev` (`previewUrls` in
`cloudflare.config.ts`).
`.github/workflows/preview-docs.yml` deploys one for every same-repository pull
request and comments its URL.

To reproduce a release-triggered build, pin the release asset:

```bash
Expand All @@ -173,7 +184,7 @@ downloading it again.
bundle.

`test:visual` compares screenshots of key pages against local baselines. It
serves the last build with `wrangler dev`, or uses `VISUAL_BASE_URL` when set.
serves the last build with `astro preview`, or uses `VISUAL_BASE_URL` when set.
Build the known-good revision and run it with `--update-snapshots` to record
baselines, then build the change and run it again. Pin `FORGE_OPENAPI_RELEASE`
for both builds; baselines and results are not committed.
10 changes: 8 additions & 2 deletions packages/cloudflare-api-site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import cloudflare from '@astrojs/cloudflare';
import nimbus, { defineConfig as defineNimbusConfig } from '@cloudflare/nimbus-docs';
import tailwindcss from '@tailwindcss/vite';
import astroFern from 'astro-fern';
import { defineConfig } from 'astro/config';
import astroFern from 'astro-fern';

const site = 'https://docs.experiments.devprod.cloudflare.dev';

const nimbusConfig = defineNimbusConfig({
site: 'https://docs.experiments.devprod.cloudflare.dev',
site,
title: 'Cloudflare API',
description: 'API reference generated from the Cloudflare OpenAPI spec by Forge.',
locale: 'en',
Expand All @@ -24,6 +26,10 @@ export default defineConfig({
adapter: cloudflare({ prerenderEnvironment: 'node' }),
vite: {
plugins: [tailwindcss()],
// astro-fern builds operation artifact URLs from `site`, and in dev the
// Cloudflare Vite plugin forwards ASSETS fetches to this server with that
// host. Allow it so Vite's host check doesn't reject them with a 403.
server: { allowedHosts: [new URL(site).hostname] },
},
integrations: [
nimbus(nimbusConfig, {
Expand Down
13 changes: 13 additions & 0 deletions packages/cloudflare-api-site/cloudflare.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from 'cf/config';

export default defineConfig({
worker: {
name: 'docs',
compatibilityDate: '2026-07-21',
compatibilityFlags: ['nodejs_compat'],
domains: ['docs.experiments.devprod.cloudflare.dev'],
// Serve Worker Previews at <preview-name>-docs.<subdomain>.workers.dev.
// Takes effect once a production deploy applies it.
previewUrls: true,
},
});
79 changes: 79 additions & 0 deletions packages/cloudflare-api-site/e2e/site.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect, test } from '@playwright/test';

const operation = '/api/zero-trust/devices/ip-profiles/methods/list/';

test.describe('routes', () => {
const routes: [path: string, status: number, contentType: string][] = [
['/api/', 200, 'text/html'],
['/api/zero-trust/', 200, 'text/html'],
['/api/zero-trust/devices/ip-profiles/', 200, 'text/html'],
[operation, 200, 'text/html'],
[`${operation}?lang=typescript`, 200, 'text/html'],
[`${operation}?lang=nope`, 404, 'text/plain'],
['/api/zero-trust/devices/ip-profiles/methods/list.md', 200, 'text/markdown'],
['/api/zero-trust/devices/ip-profiles/methods/list.md?lang=typescript', 200, 'text/markdown'],
['/api/llms.txt', 200, 'text/markdown'],
['/api/zero-trust/llms.txt', 200, 'text/markdown'],
['/api/tooling/', 200, 'text/html'],
['/api/.well-known/ard.json', 200, 'application/json'],
['/api/nope.txt', 404, 'text/html'],
];

for (const [path, status, contentType] of routes) {
test(`${path} responds ${status} ${contentType}`, async ({ request }) => {
const response = await request.get(path, { maxRedirects: 0 });
expect(response.status()).toBe(status);
expect(response.headers()['content-type']).toContain(contentType);
});
}
});

test('overview renders with product navigation', async ({ page }) => {
await page.goto('/api/');
await expect(page).toHaveTitle('API Reference | Cloudflare API');
await expect(page.locator('main h1')).toBeVisible();
// Products are listed in the sidebar, collapsed by default.
await expect(page.locator('a[href="/api/zero-trust/"]').first()).toBeAttached();
});

test('operation page renders the operation and its samples', async ({ page }) => {
await page.goto(operation);
await expect(page).toHaveTitle('List IP profiles | Cloudflare API');
await expect(page.locator('h1')).toHaveText('List IP profiles');
await expect(page.locator('.forge-verb')).toHaveText('GET');
await expect(page.locator('.forge-sample-code code.language-bash').first()).toContainText(
'https://api.cloudflare.com/client/v4/accounts/',
);
});

test('execution target selector switches the code sample', async ({ page }) => {
await page.goto(operation);
const target = page.locator('select[data-forge-target]');
await expect(target).toHaveValue('curl');
await target.selectOption('typescript');
await expect(page).toHaveURL(/[?&]lang=typescript\b/);
await expect(page.locator('select[data-forge-target]')).toHaveValue('typescript');
await expect(page.locator('.forge-sample-code code.language-bash')).toHaveCount(0);
});

test('operation links its agent Markdown', async ({ page, request }) => {
await page.goto(operation);
const href = await page.locator('link[rel="alternate"][type="text/markdown"]').getAttribute('href');
expect(href).not.toBeNull();
const response = await request.get(new URL(href!).pathname);
expect(response.status()).toBe(200);
const markdown = await response.text();
expect(markdown).toContain('# List IP profiles');
expect(markdown).toContain('- Path: `/accounts/{account_id}/devices/ip-profiles`');
});

test('llms.txt indexes the API', async ({ request }) => {
const response = await request.get('/api/llms.txt');
expect(await response.text()).toMatch(/^# Cloudflare API\n/);
});

test('unknown pages render the not-found page', async ({ page }) => {
const response = await page.goto('/api/nope.txt');
expect(response?.status()).toBe(404);
await expect(page).toHaveTitle('Page not found | Cloudflare API');
});
17 changes: 10 additions & 7 deletions packages/cloudflare-api-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"dev": "cf dev",
"build": "cf build",
"preview": "astro preview",
"deploy": "cf deploy",
"deploy:preview": "cf previews deploy",
"check": "astro check",
"test": "node --experimental-strip-types --test src/*.test.ts",
"test:visual": "playwright test"
"test:e2e": "playwright test --project=e2e",
"test:visual": "playwright test --project=visual"
},
"dependencies": {
"@astrojs/cloudflare": "^14.2.5",
"@astrojs/cloudflare": "^15.0.0-beta.2",
"@astrojs/prism": "^4.0.2",
"@cloudflare/nimbus-docs": "^0.18.0",
"@fontsource-variable/inter": "5.3.0",
"@fontsource-variable/jetbrains-mono": "5.3.0",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/simple-icons": "^1.2.99",
"astro": "^7.2.8",
"astro": "^7.4.1",
"astro-fern": "workspace:*",
"cf": "1.0.0-beta.19",
"clsx": "^2.1.1",
"fern-forge": "workspace:*",
"hast-util-sanitize": "^5.0.2",
"satteri": "^0.10.5",
"tailwind-merge": "^3.7.0",
"wrangler": "^4.127.0"
"tailwind-merge": "^3.7.0"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
Expand Down
8 changes: 5 additions & 3 deletions packages/cloudflare-api-site/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ export default defineConfig({
forbidOnly: Boolean(process.env.CI),
outputDir: 'e2e/__test-results__',
snapshotPathTemplate: 'e2e/__snapshots__/{arg}{ext}',
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'], headless: true } }],
projects: [
{ name: 'e2e', testMatch: 'site.e2e.ts', use: { ...devices['Desktop Chrome'], headless: true } },
{ name: 'visual', testMatch: 'visual.e2e.ts', use: { ...devices['Desktop Chrome'], headless: true } },
],
testDir: 'e2e',
testMatch: '*.e2e.ts',
timeout: 120_000,
expect: { toHaveScreenshot: { maxDiffPixelRatio: 0.001, animations: 'disabled' } },
use: { baseURL, trace: 'retain-on-failure' },
webServer: process.env.VISUAL_BASE_URL
? undefined
: { command: 'wrangler dev --port 8788', url: `${baseURL}/api/`, reuseExistingServer: true, timeout: 120_000 },
: { command: 'astro preview --port 8788', url: `${baseURL}/api/`, reuseExistingServer: true, timeout: 120_000 },
});
2 changes: 1 addition & 1 deletion packages/cloudflare-api-site/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
}
},
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
"exclude": ["dist", ".cloudflare/output", ".cloudflare/state"]
}
12 changes: 0 additions & 12 deletions packages/cloudflare-api-site/wrangler.jsonc

This file was deleted.

Loading
Loading