Skip to content
Closed
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
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@ test-results
# System files
*.log
.DS_Store

# Editor/IDE
.claude
.flox

# Lock files (deno.lock regenerated during build)
deno.lock

# Documentation/config not needed in image
*.md
.prettierrc
.licenserc.yaml
.gitattributes
playwright.config.ts
eslint.config.mjs
vercel.json
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ build-archive.log
test-results/

.react-router/

# Deno
deno.lock
rfd-server
.flox/
34 changes: 34 additions & 0 deletions .infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,40 @@ Note: Infrastructure configuration is stored in this repository until a point in
we have RFD infrastructure that is separate from `cio`. At that point, this infrastructure
should be owned by the RFD service.

## Storage Provider Configuration

The application supports two storage backends for serving static assets: GCS (Google Cloud
Storage) and S3 (AWS S3 or S3-compatible services).

### Common Environment Variables

| Variable | Description |
| ------------------ | -------------------------------------------------------------------------- |
| `STORAGE_PROVIDER` | Storage backend to use: `gcs` (default) or `s3` |
| `STORAGE_URL_TTL` | Pre-signed URL expiration time in seconds (optional, defaults to 24 hours) |

### GCS Configuration

| Variable | Description |
| ------------------ | ------------------------------ |
| `STORAGE_URL` | Base URL of the GCS CDN bucket |
| `STORAGE_KEY_NAME` | Name of the signing key |
| `STORAGE_KEY` | Base64-encoded signing key |

### S3 Configuration

| Variable | Description |
| ----------------------- | ------------------------------------------------------------ |
| `S3_BUCKET` | S3 bucket name |
| `AWS_REGION` | AWS region (standard AWS SDK variable) |
| `AWS_ACCESS_KEY_ID` | AWS access key (standard AWS SDK variable, or use IAM roles) |
| `AWS_SECRET_ACCESS_KEY` | AWS secret key (standard AWS SDK variable, or use IAM roles) |
| `AWS_ENDPOINT_URL` | Custom endpoint for S3-compatible services (optional) |

The S3 integration uses the AWS SDK default credential chain, so credentials can be provided
via environment variables, IAM instance roles, ECS task roles, or other standard AWS
methods.

### GCP Infrastructure

Image storage and serving is handled by
Expand Down
110 changes: 110 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this
repository.

## Project Overview

RFD Site is Oxide Computer Company's web frontend for browsing, searching, and reading RFDs
(Requests for Discussion). Built with React Router v7 (formerly Remix). Can be deployed on
Vercel or run as a Deno container.

## Commands

### Development (Node/npm)

```bash
npm run dev # Start dev server on localhost:3000
npm run build # Production build (react-router build)
npm run test # Run unit tests with Vitest
npm run tsc # Type check without emitting
npm run lint # ESLint
npm run fmt # Format with Prettier
npm run fmt:check # Check formatting
npm run e2ec # Run Playwright E2E tests (Chrome)
```

### Deno Runtime

```bash
deno task dev # Dev server with Deno (requires build first)
deno task start # Production server with Deno
deno task build # Build using Deno's npm compatibility
deno task compile # Compile to standalone binary
```

### Container Build (Podman)

```bash
podman build -t rfd-site . # Build container
podman run -p 3000:3000 rfd-site # Run container
podman run -p 3000:3000 -e PORT=8080 rfd-site # Custom port
```

The Containerfile uses a multi-stage build:

1. Node stage: Builds the React Router app with npm
2. Deno stage: Compiles server.ts with embedded assets into a standalone binary
3. Scratch stage: Minimal final image with just the binary and CA certs

### Running a Single Test

```bash
npm run test -- path/to/file.test.ts # Run specific test file
npm run test -- --grep "test name" # Run tests matching pattern
npx playwright test --project=chrome test.ts # Run specific E2E test
```

### Local RFD Authoring Mode

Preview RFDs from a local clone of the rfd repo:

```bash
LOCAL_RFD_REPO=~/oxide/rfd npm run dev
```

This mode reads RFD files directly from the specified directory without needing API
credentials.

## Architecture

### Data Flow: Local vs Remote Mode

The app operates in two modes controlled by `LOCAL_RFD_REPO` env var:

- **Local mode** (`app/services/rfd.local.server.ts`): Reads AsciiDoc files directly from a
local rfd repo clone. Used for authoring/previewing.
- **Remote mode** (`app/services/rfd.remote.server.ts`): Fetches from the rfd-api backend.
Used in production with OAuth authentication.

The unified interface in `app/services/rfd.server.ts` abstracts this, calling either backend
based on `isLocalMode()`.

### Routing

Uses React Router v7 file-based routing (`@react-router/fs-routes`). Routes are in
`app/routes/`:

- `_index.tsx` - RFD listing page
- `rfd.$slug.tsx` - Individual RFD view
- `auth.*.tsx` - OAuth flows (GitHub, Google)
- `api.*.tsx` - API endpoints

### Content Rendering

RFDs are written in AsciiDoc and rendered with `@oxide/react-asciidoc`. Custom block
renderers live in `app/components/AsciidocBlocks/` (Mermaid diagrams, syntax-highlighted
code listings, images).

### Path Alias

`~/` maps to `./app/` (configured in tsconfig.json).

## Key Dependencies

- `@oxide/react-asciidoc` - AsciiDoc renderer
- `@oxide/rfd.ts` - TypeScript client for rfd-api
- `@oxide/design-system` - Oxide's component library
- `@tanstack/react-query` - Data fetching for PR discussions
- `shiki` - Syntax highlighting
- `mermaid` - Diagram rendering
84 changes: 84 additions & 0 deletions Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# =============================================================================
# Stage 1: Install npm dependencies and build the React Router app
# =============================================================================
FROM docker.io/node:25-alpine AS builder

WORKDIR /app

# Copy package files
COPY package.json package-lock.json ./

# Install dependencies
RUN npm ci

# Copy source files
COPY app ./app
COPY public ./public
COPY vite ./vite
COPY types ./types
COPY tsconfig.json ./
COPY vite.config.ts ./
COPY react-router.config.ts ./
COPY svgr.config.js ./
COPY site.config.ts ./

# Build the application
RUN BUILD_TARGET=deno npm run build

# =============================================================================
# Stage 2: Compile Deno server into standalone binary
# =============================================================================
FROM docker.io/denoland/deno:2.6.4 AS compiler

WORKDIR /app

# Copy deno config and server
COPY deno.json ./
COPY server.ts ./

# Copy package.json for npm dependency resolution
COPY package.json ./

# Copy the built application from builder stage
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules

# Cache dependencies
RUN deno install \
--node-modules-dir=manual

# Compile to standalone binary with embedded assets
# --include embeds the entire build directory into the binary
# --unstable-bare-node-builtins allows imports like 'fs' instead of 'node:fs'
RUN deno compile \
--allow-net \
--allow-read \
--allow-env \
--unstable-bare-node-builtins \
--include=build \
--output=rfd-server \
--node-modules-dir=manual \
--allow-sys \
server.ts

FROM docker.io/debian:12-slim as tools

RUN apt update -y && apt install tini -y
# =============================================================================
# Stage 3: Create minimal Alpine image
# =============================================================================
FROM docker.io/debian:12-slim

# Copy the compiled binary (includes embedded static assets)
COPY --from=compiler /app/rfd-server /rfd-server
COPY --from=tools /usr/bin/tini /tini

# Set environment variables
ENV PORT=3000
ENV NODE_ENV=production

# Expose the port
EXPOSE 3000

# Run the server
ENTRYPOINT ["/tini", "/rfd-server"]
42 changes: 39 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,55 @@ combined branch that contains both.
When running in a non-local mode, the following settings must be specified:

- `SESSION_SECRET` - Key that will be used to signed cookies
- `TELEMETRY_DISABLE` - Disable all remote telemetry. Set to any non-empty value to disable (e.g., `1`, `true`, `yes`). Leave unset or set to empty string to enable.

- `RFD_API` - Backend RFD API to communicate with (i.e. https://api.server.com)
- `RFD_API_CLIENT_ID` - OAuth client id create via the RFD API
- `RFD_API_CLIENT_SECRET` - OAuth client secret create via the RFD API
#### Authentication

##### API URL Configuration

The RFD API URL can be configured in two ways:

- `RFD_API` - Single URL for both server-to-server calls and OAuth redirects (legacy,
simplest)
- `RFD_API_BACKEND_URL` + `RFD_API_FRONTEND_URL` - Split URLs for deployments where rfd-site
uses internal networking to reach rfd-api while users access a public endpoint

When using split URLs:

- `RFD_API_BACKEND_URL` - URL for server-to-server API calls (e.g., internal load balancer)
- `RFD_API_FRONTEND_URL` - URL for OAuth redirects where user's browser is directed

You can mix configurations: set one of the new vars and use `RFD_API` as fallback for the
other. Existing deployments using only `RFD_API` will continue to work unchanged.

##### OAuth Credentials

- `RFD_API_CLIENT_ID` - OAuth client id created via the RFD API
- `RFD_API_CLIENT_SECRET` - OAuth client secret created via the RFD API
- `RFD_API_GOOGLE_CALLBACK_URL` - Should be of the form of
`https://{rfd_site_hostname}/auth/google/callback`
- `RFD_API_GITHUB_CALLBACK_URL` - Should be of the form of
`https://{rfd_site_hostname}/auth/github/callback`
- `RFD_API_MLINK_SECRET` - Client secret for magic link (email) authentication

- `AUTH_PROVIDERS` - Comma-delimited list of enabled authentication providers. Valid values
are `github`, `google`, and `email`. If not set, all providers are enabled by default.
Providers with missing required environment variables are automatically disabled.
Examples:
- `AUTH_PROVIDERS=github,google` - Enable only GitHub and Google OAuth
- `AUTH_PROVIDERS=email` - Enable only email (magic link) authentication

#### Storage

- `STORAGE_URL` - Url of bucket for static assets
- `STORAGE_KEY_NAME` - Name of the key defined in `STORAGE_KEY`
- `STORAGE_KEY` - Key for generating signed static asset urls

#### GitHub Integration

- `GITHUB_HOST` - GitHub host for the RFD repository. Defaults to
`github.com/oxidecomputer/rfd`. Set this to use a GitHub Enterprise instance or a
different repository location (e.g., `github.example.com/org/rfd`).
- `GITHUB_APP_ID` - App id for fetching GitHub PR discussions
- `GITHUB_INSTALLATION_ID` - Installation id of GitHub App
- `GITHUB_PRIVATE_KEY` - Private key of the GitHub app for discussion fetching
Expand Down
7 changes: 5 additions & 2 deletions app/components/rfd/RfdDiscussionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useMemo } from 'react'

import Icon from '~/components/Icon'
import { useDiscussionQuery } from '~/hooks/use-discussion-query'
import { useRootLoaderData } from '~/root'
import type {
IssueCommentType,
ListIssueCommentsType,
Expand Down Expand Up @@ -225,6 +226,7 @@ const DialogContent = ({
discussions: Discussions
pullNumber: number
}) => {
const { githubRepoUrl } = useRootLoaderData()
return (
<Dialog
store={dialogStore}
Expand All @@ -243,7 +245,7 @@ const DialogContent = ({
</div>
</div>
<a
href={`https://github.com/oxidecomputer/rfd/pull/${pullNumber}`}
href={`${githubRepoUrl}/pull/${pullNumber}`}
target="_blank"
rel="noreferrer"
>
Expand All @@ -262,6 +264,7 @@ const DiscussionReviewGroup = ({
discussions: Discussions
pullNumber: number
}) => {
const { githubRepoUrl } = useRootLoaderData()
const reviewCount = Object.keys(discussions).length

return (
Expand Down Expand Up @@ -306,7 +309,7 @@ const DiscussionReviewGroup = ({
This discussion has no reviews or comments
</p>
<a
href={`https://github.com/oxidecomputer/rfd/pull/${pullNumber}`}
href={`${githubRepoUrl}/pull/${pullNumber}`}
className="text-mono-xs text-secondary border-default hover:bg-secondary mt-6 inline-block rounded border px-2 py-1"
target="_blank"
rel="noreferrer"
Expand Down
Loading