diff --git a/.github/workflows/deploy-web-ui.yml b/.github/workflows/deploy-web-ui.yml new file mode 100644 index 0000000..71ca21a --- /dev/null +++ b/.github/workflows/deploy-web-ui.yml @@ -0,0 +1,98 @@ +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" + +# Cancel an in-flight deploy for the same ref when a new commit lands. +# Prevents stale previews and saves Cloudflare build minutes. +concurrency: + group: deploy-web-ui-${{ github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # required to post the preview URL comment + deployments: write + steps: + - uses: actions/checkout@v4 + + - 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 (static export → apps/web-ui/out) + working-directory: apps/web-ui + env: + # Plumbed for when the fastify backend lands; harmless local default until then. See issue #77. + NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL || 'http://localhost:3001' }} + run: pnpm build + + - name: Deploy to Cloudflare Pages + id: deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # --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. + command: pages deploy apps/web-ui/out --project-name=cv-builder-web --commit-dirty=true + + - name: Post preview URL on PR + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + ALIAS_URL: ${{ steps.deploy.outputs.pages-deployment-alias-url }} + COMMENT_MARKER: cv-builder-web-preview-deploy + run: | + set -euo pipefail + + BODY=$(cat < + 🌐 **Preview deployment:** ${ALIAS_URL} + + _Updated automatically on every push to this PR._ + EOF + ) + + AUTH=(-H "Authorization: token ${GH_TOKEN}" -H "Accept: application/vnd.github+json") + JSON=$(jq -nc --arg body "${BODY}" '{body: $body}') + + # Find the existing bot comment for this PR (if any) by the marker, so + # we update it instead of posting a duplicate on every push. + EXISTING_ID=$(curl -fsS "${AUTH[@]}" \ + "https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments?per_page=100" \ + | jq -r --arg m "${COMMENT_MARKER}" \ + '.[] | select(.body | contains($m)) | .id' \ + | head -n1) + + if [[ "${EXISTING_ID}" =~ ^[0-9]+$ ]]; then + curl -fsS -X PATCH "${AUTH[@]}" \ + "https://api.github.com/repos/${REPO}/issues/comments/${EXISTING_ID}" \ + -d "${JSON}" + echo "Updated existing comment ${EXISTING_ID}" + else + curl -fsS -X POST "${AUTH[@]}" \ + "https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments" \ + -d "${JSON}" + echo "Posted new comment" + fi diff --git a/README.md b/README.md index d46e084..4240048 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,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-web` project | +| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare dashboard → Workers & Pages → right sidebar | + +The `cv-builder-web` Cloudflare Pages project must exist before the first +deploy — create it once via the dashboard or +`wrangler pages project create cv-builder-web`. + +--- + ## Supported Role Archetypes Currently built-in: diff --git a/apps/web-ui/README.md b/apps/web-ui/README.md index f35ce8c..35e07fe 100644 --- a/apps/web-ui/README.md +++ b/apps/web-ui/README.md @@ -29,3 +29,14 @@ Then open [http://localhost:3000](http://localhost:3000). - `src/app/layout.tsx` - app shell and metadata - `src/app/page.tsx` - homepage - `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-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.). diff --git a/apps/web-ui/next.config.ts b/apps/web-ui/next.config.ts index 66e1566..c203269 100644 --- a/apps/web-ui/next.config.ts +++ b/apps/web-ui/next.config.ts @@ -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, };