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
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ jobs:

- name: Build web
working-directory: packages/web
env:
VITE_API_URL: https://api.append.tindev.dev
run: pnpm run build

- name: Deploy web to Pages
Expand Down
183 changes: 183 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
name: Preview

on:
pull_request:
branches: [main]

concurrency:
group: preview-${{ github.ref }}
cancel-in-progress: true

jobs:
preview-api:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version-file: "package.json"

- uses: pnpm/action-setup@v4

- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Apply D1 migrations to preview
working-directory: packages/api
run: pnpm exec wrangler d1 migrations apply append-db-preview --remote --config wrangler.jsonc --env preview

- name: Deploy Worker API Preview
id: deploy
working-directory: packages/api
run: |
# Deploy to preview environment
set +e # Don't exit on error yet
DEPLOYMENT_OUTPUT=$(pnpm exec wrangler deploy -e preview --config wrangler.jsonc 2>&1)
EXIT_CODE=$?
echo "$DEPLOYMENT_OUTPUT"

if [ $EXIT_CODE -ne 0 ]; then
echo "::error::Deployment failed with exit code $EXIT_CODE"
exit $EXIT_CODE
fi

# Extract deployment URL from output
DEPLOYMENT_URL=$(echo "$DEPLOYMENT_OUTPUT" | grep -oP 'https://[^\s]+\.workers\.dev' | head -1)
echo "deployment-url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT

preview-web:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CF_PAGES_PROJECT: ${{ secrets.CF_PAGES_PROJECT }}
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version-file: "package.json"

- uses: pnpm/action-setup@v4

- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- uses: actions/cache@v5
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build web
working-directory: packages/web
env:
VITE_API_URL: https://append-api-preview.tindejphachon.workers.dev
run: pnpm run build

- name: Deploy web preview to Pages
id: deploy
working-directory: packages/web
run: |
# Use --branch to create a preview deployment
set +e # Don't exit on error yet
DEPLOYMENT_OUTPUT=$(pnpm exec wrangler pages deploy dist --project-name "$CF_PAGES_PROJECT" --branch "${{ github.head_ref }}" 2>&1)
EXIT_CODE=$?
echo "$DEPLOYMENT_OUTPUT"

if [ $EXIT_CODE -ne 0 ]; then
echo "::error::Deployment failed with exit code $EXIT_CODE"
exit $EXIT_CODE
fi

# Extract deployment URL from output
DEPLOYMENT_URL=$(echo "$DEPLOYMENT_OUTPUT" | grep -oP 'https://[^\s]+\.pages\.dev' | head -1)
echo "deployment-url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT

comment-preview-urls:
runs-on: ubuntu-latest
needs: [preview-api, preview-web]
if: always() && (needs.preview-api.result == 'success' || needs.preview-web.result == 'success')
permissions:
pull-requests: write
steps:
- name: Comment PR with preview URLs
uses: actions/github-script@v7
with:
script: |
const apiUrl = '${{ needs.preview-api.outputs.deployment-url }}';
const webUrl = '${{ needs.preview-web.outputs.deployment-url }}';

let body = '## 🚀 Preview Deployments\n\n';

if (apiUrl) {
body += `**API**: ${apiUrl}\n`;
} else {
body += '**API**: ❌ Deployment failed\n';
}

if (webUrl) {
body += `**Web**: ${webUrl}\n`;
} else {
body += '**Web**: ❌ Deployment failed\n';
}

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('🚀 Preview Deployments')
);

if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
87 changes: 87 additions & 0 deletions docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,93 @@ pnpm --filter @append/web run build
pnpm --filter @append/web exec wrangler pages deploy packages/web/dist --project-name "$CF_PAGES_PROJECT"
```

## Preview deployments (PR environments)

PR previews use isolated Cloudflare resources to avoid affecting production:

- **Worker**: `append-api-preview` (preview environment in `wrangler.jsonc`)
- **Database**: `append-db-preview` (D1)
- **Storage**: `append-imports-preview` (R2)
- **AI Provider**: Stub (no OpenAI API calls or costs)

### One-time setup

These steps create the isolated preview infrastructure (already completed):

1. Create preview D1 database:

```bash
pnpm --filter @append/api exec wrangler d1 create append-db-preview
```

2. Update `packages/api/wrangler.jsonc` with the database ID from step 1.

3. Create preview R2 bucket:

```bash
pnpm --filter @append/api exec wrangler r2 bucket create append-imports-preview
```

4. Apply initial schema to preview database:

```bash
pnpm --filter @append/api exec wrangler d1 migrations apply append-db-preview --remote --env preview
```

### How PR previews work

When a PR is opened against `main`:

1. GitHub Actions runs `.github/workflows/preview.yml`
2. Migrations are applied to the preview database
3. API deploys to the preview environment: `https://append-api-preview.tindejphachon.workers.dev`
4. Web builds with `VITE_API_URL` set to preview API
5. Web deploys to Cloudflare Pages with branch-specific URL: `https://<branch>.<project>.pages.dev`
6. Both URLs are posted as a comment on the PR (updated on subsequent pushes)

**API URL configuration:**

- Local dev: `http://localhost:8787` (default when running `pnpm dev`)
- Preview: `https://append-api-preview.tindejphachon.workers.dev` (set via `VITE_API_URL` in preview workflow)
- Production: `https://api.append.tindev.dev` (set via `VITE_API_URL` in deploy workflow)

### Manual preview deployment

To deploy manually to preview environments:

```bash
# API preview
pnpm --filter @append/api exec wrangler d1 migrations apply append-db-preview --remote --env preview
pnpm --filter @append/api exec wrangler deploy -e preview --config wrangler.jsonc

# Web preview (branch-specific)
VITE_API_URL=https://append-api-preview.tindejphachon.workers.dev pnpm --filter @append/web run build
pnpm --filter @append/web exec wrangler pages deploy dist --project-name "$CF_PAGES_PROJECT" --branch <branch-name>
```

### Preview environment secrets

Preview environment has Google OAuth configured for authentication:

```bash
# Secrets are already set for preview environment
pnpm --filter @append/api exec wrangler secret list --env preview
```

Required secrets (already configured):

- `GOOGLE_CLIENT_ID`
- `GOOGLE_CLIENT_SECRET`
- `BETTER_AUTH_SECRET`
- `ALLOWED_EMAIL` (or `ALLOWED_SUB`)

### Preview environment notes

- Preview database and storage accumulate data over time (cleared manually if needed)
- Preview uses stub AI provider (no real OpenAI calls) to avoid costs
- Google OAuth is configured and works in preview environments
- Cloudflare Pages automatic GitHub integration works alongside GitHub Actions workflow

## Developer checks (local)

From repo root:
Expand Down
30 changes: 30 additions & 0 deletions packages/api/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@
* Environments
*/
"env": {
/**
* Preview environment
* - Used for PR preview deployments
* - Uses separate preview database and R2 bucket
* - Uses stub AI provider to avoid costs
* - Deploy with: wrangler deploy -e preview
*/
"preview": {
"name": "append-api-preview",
"vars": {
"BETTER_AUTH_URL": "https://append-api-preview.tindejphachon.workers.dev",
"SUGGESTIONS_PROVIDER": "stub",
"AI_GATEWAY_ID": "append-gateway-preview",
"CF_ACCOUNT_ID": "d1d83d0040d3dea7df64d58cdbebe5a9"
},
"d1_databases": [
{
"binding": "DB",
"database_name": "append-db-preview",
"database_id": "54c8d43b-81db-49e4-84f3-5dcf4eebc302",
"migrations_dir": "drizzle"
}
],
"r2_buckets": [
{
"binding": "IMPORT_FILES",
"bucket_name": "append-imports-preview"
}
]
},
/**
* Production environment
* - Uses Secrets Store for OPENAI_API_KEY
Expand Down
9 changes: 7 additions & 2 deletions packages/web/src/lib/api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@
import type { AppType } from '@append/api';
import { hc, type InferRequestType, type InferResponseType } from 'hono/client';

// API URL - local dev or production
// API URL - local dev, preview, or production
// Exported for use by SSE streaming and blob download functions that can't use RPC
export const API_URL = import.meta.env.DEV ? 'http://localhost:8787' : 'https://api.append.tindev.dev';
// Priority: VITE_API_URL env var > DEV mode > production default
export const API_URL = import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL
: import.meta.env.DEV
? 'http://localhost:8787'
: 'https://api.append.tindev.dev';

export function apiFetch(path: string, init?: RequestInit): Promise<Response> {
return fetch(`${API_URL}${path}`, {
Expand Down
7 changes: 7 additions & 0 deletions packages/web/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_API_URL?: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
Loading