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
112 changes: 112 additions & 0 deletions .github/workflows/deploy-web-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Deploy web UI

on:
push:
branches: [main]
paths:
- "apps/web-ui/**"
# include the workflow itself so edits to this file re-trigger a deploy
- ".github/workflows/deploy-web-ui.yml"
pull_request:
branches: [main]
paths:
- "apps/web-ui/**"
- ".github/workflows/deploy-web-ui.yml"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
concurrency:
group: deploy-web-ui-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
# Skip PRs from forks: secrets (CLOUDFLARE_*) are not exposed to them.
# The repo-name equality form is used so a missing pull_request path is a
# hard false (push events skip the second clause entirely) instead of
# crashing on a null deref.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # for sticky-pull-request-comment
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

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

- name: Build (turbo orchestrates core → web, output lands at apps/web-ui/out)
run: pnpm build

- name: Compute Cloudflare branch alias
id: branch
shell: bash
run: |
RAW_BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
SAFE_BRANCH="$(printf '%s' "$RAW_BRANCH" | tr '/' '-' | tr -cd '[:alnum:]._-')"
echo "name=${SAFE_BRANCH}" >> "$GITHUB_OUTPUT"

- name: Verify Cloudflare Pages project exists
id: preflight
env:
CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CF_PROJECT_NAME: cv-builder-cf-web
shell: bash
run: |
set -euo pipefail
# Capture the response body even on HTTP errors so we can print the
# Cloudflare error code in the failure log (curl -f would otherwise
# discard it and we'd only see the HTTP status).
HTTP_CODE="$(curl -sS -o preflight.json -w '%{http_code}' \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/pages/projects/${CF_PROJECT_NAME}")"
if [ "$HTTP_CODE" != "200" ]; then
echo "::error::Cloudflare API returned HTTP ${HTTP_CODE} for project '${CF_PROJECT_NAME}' on account ${CF_ACCOUNT_ID}."
echo "Response body:"
cat preflight.json
echo
# Decode the most common Cloudflare error codes inline.
case "$HTTP_CODE" in
401) echo "::error::Token rejected. The CLOUDFLARE_API_TOKEN secret is invalid or expired. Revoke and re-create it." ;;
403) echo "::error::Token lacks permission. Ensure the token has 'Account → Cloudflare Pages: Edit' on this account." ;;
400) echo "::error::Bad request. Check the error codes in the body above — typically code 7000 means the account ID is wrong or the token isn't scoped to this account." ;;
404) echo "::error::Project not found. Either the project name in the workflow doesn't match Cloudflare (check for typos / casing) or the project was never created." ;;
esac
exit 1
fi
echo "Project ${CF_PROJECT_NAME} is reachable on account ${CF_ACCOUNT_ID}."

- name: Deploy to Cloudflare Pages
id: deploy
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: apps/web-ui
# --commit-dirty=true is required: the wrangler-action sets CI=1 and would otherwise refuse a deploy with apps/web-ui/out/ as an uncommitted change.
# --branch dodges the detached-HEAD trap on PR checkouts: without it
# wrangler reads "HEAD" from git and Cloudflare rejects the alias.
# Cloudflare Pages aliases reject "/", so we collapse refs to a
# safe form (e.g. "ci/cloudflare-pages-preview" -> "ci-cloudflare-pages-preview").
command: >-
pages deploy out
--project-name=cv-builder-cf-web
--commit-dirty=true
--branch=${{ steps.branch.outputs.name }}

- name: Post preview URL on PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
header: cv-builder-cf-web-preview-deploy
message: |
🌐 **Preview deployment:** ${{ steps.deploy.outputs.pages-deployment-alias-url }}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,26 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide.

---

## Deployment

The web UI (`apps/web-ui/`) is deployed to **Cloudflare Pages**. Every push to a
PR that changes `apps/web-ui/**` triggers the `Deploy web UI` workflow, which
builds a static export and posts a preview URL as a comment on the PR. Pushes
to `main` deploy to the production site.

### Required repo secrets

| Secret | Where to get it |
| --- | --- |
| `CLOUDFLARE_API_TOKEN` | Cloudflare dashboard → My Profile → API Tokens → Create Token → use the "Edit Cloudflare Pages" template, scoped to the account and `cv-builder-cf-web` project |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare dashboard → Workers & Pages → right sidebar |

The `cv-builder-cf-web` Cloudflare Pages project must exist before the first
deploy — create it once via the dashboard or
`wrangler pages project create cv-builder-cf-web`.

---

## Supported Role Archetypes

Currently built-in (the ones the CLI's `archetypes` command and the
Expand Down
11 changes: 11 additions & 0 deletions apps/web-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,14 @@ Then open [http://localhost:3000](http://localhost:3000).
- `src/app/results/page.tsx` - evaluation results view
- `src/app/feedback/page.tsx` - feedback guide
- `src/app/globals.css` - global styles

## Previews

PRs that touch this package automatically get a Cloudflare Pages preview link,
posted as a comment on the PR by the `Deploy web UI` workflow. Pushing new
commits updates the same comment instead of posting duplicates. Merging to
`main` redeploys the production site at `https://cv-builder-cf-web.pages.dev`.

The build is a static export (`output: "export"` in `next.config.ts`) — the
app must remain free of server-only features (API routes, server actions,
middleware, etc.).
3 changes: 2 additions & 1 deletion apps/web-ui/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
// Static export for Cloudflare Pages; the app must stay server-feature-free. See issue #77.
output: "export",
reactCompiler: true,
};

Expand Down
3 changes: 2 additions & 1 deletion apps/web-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@types/react-dom": "^19",
"babel-plugin-react-compiler": "1.0.0",
"tailwindcss": "^4",
"typescript": "^6"
"typescript": "^6",
"wrangler": "^3.90.0"
}
}
102 changes: 102 additions & 0 deletions docs/CLOUDFLARE_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Cloudflare Pages Setup

This guide covers the **one-time Cloudflare configuration** required by the
Deploy web UI workflow (`.github/workflows/deploy-web-ui.yml`). It exists so
the next maintainer doesn't have to rediscover which account, project name,
and token permissions are needed.

If you just want to fix a failing deploy, jump to **§3 Troubleshooting**.

## 1. What the workflow expects

| Item | Required value | Where it comes from |
|---|---|---|
| Cloudflare **Account ID** | Any account you control | GitHub repo → Settings → Secrets → `CLOUDFLARE_ACCOUNT_ID` |
| Cloudflare **API token** | Scoped to that account, with **Pages: Edit** | GitHub repo → Settings → Secrets → `CLOUDFLARE_API_TOKEN` |
| Pages **project name** | `cv-builder-cf-web` (literal, in workflow) | Hard-coded in the workflow — see "Renaming the project" below |

The workflow will fail fast if any of these are misaligned: a step called
**Verify Cloudflare Pages project exists** runs *before* `wrangler` and calls
`GET /accounts/{id}/pages/projects/{name}` so a missing project or wrong token
shows up as a clean 401/403/404 in the logs within ~1 second instead of a
confusing wrangler error 30 seconds later.

## 2. One-time setup

### 2.1 Pick the Cloudflare account

If you're starting fresh, [sign up here](https://dash.cloudflare.com/sign-up).
If you have multiple accounts, pick one — the Account ID is shown at the
bottom-right of the dashboard for any page in that account.

### 2.2 Create the Pages project

1. In the chosen account, open **Workers & Pages** → **Pages** tab.
2. Click **Create application** → **Pages** → **Upload assets**.
3. **Project name:** `cv-builder-cf-web` (must match exactly).
4. **Production branch name:** `main`.
5. Click **Create project**. You don't need to upload anything yet — the
workflow pushes the built `out/` directory on every PR and on pushes to
`main`.

### 2.3 Create a scoped API token

1. Cloudflare → top-right **My Profile** → **API Tokens** → **Create Token**.
2. Choose **Custom token** (not the "Edit Cloudflare Pages" template — that
one has wider scope than needed).
3. Permissions:
- `Account → Cloudflare Pages: Edit`
- `Account → Account Settings: Read`
4. Account Resources: include **only** the account from §2.1.
5. Save the token value — Cloudflare shows it once.

### 2.4 Wire secrets into the GitHub repo

In the repo: **Settings** → **Secrets and variables** → **Actions** →
**New repository secret**.

| Name | Value |
|---|---|
| `CLOUDFLARE_ACCOUNT_ID` | Account ID from §2.1 |
| `CLOUDFLARE_API_TOKEN` | Token value from §2.3 |

### 2.5 Trigger a deploy

Push any commit to a branch that has an open PR against `main`, or push
directly to `main`. The workflow will run, the pre-flight will pass, and the
**Post preview URL on PR** step will leave a sticky comment on the PR with
the preview URL.

## 3. Troubleshooting

The **Verify Cloudflare Pages project exists** step is your friend. Read its
log first.

| HTTP status from pre-flight | Meaning | Fix |
|---|---|---|
| **200** | All good | — |
| **401** | API token is invalid or expired | Rotate the token (§2.3) and update the secret (§2.4) |
| **403** | Token lacks Pages:Edit on this account | Check the token's Account Resources; make sure it covers the account from §2.1 |
| **404** | Project `cv-builder-cf-web` does not exist in this account | Either create it (§2.2) or rename it in the workflow (see below) |
| **curl: (6) Could not resolve host** | Runner network glitch — re-run the job | — |

If pre-flight passes but the **Deploy to Cloudflare Pages** step fails, the
issue is in `wrangler` output — usually a missing `out/` directory (the build
step was skipped or failed silently) or a wrangler version mismatch.

## 4. Renaming the project

If `cv-builder-cf-web` is taken in your account, or you want a different name,
update **two places** in `.github/workflows/deploy-web-ui.yml`:

1. The pre-flight step's `CF_PROJECT_NAME` env var.
2. The deploy step's `--project-name=cv-builder-cf-web` argument.

Then create the new project in Cloudflare under the same name. Mismatch
between workflow and dashboard will fail pre-flight with 404.

## 5. Why a dedicated account?

Using a separate Cloudflare account for CI keeps production infrastructure
isolated from personal accounts. If the team grows, this account is also the
place to add a custom domain once the project graduates from `*.pages.dev`.
Loading
Loading