diff --git a/.github/workflows/mindmap.yml b/.github/workflows/mindmap.yml deleted file mode 100644 index f1c80048b7..0000000000 --- a/.github/workflows/mindmap.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Deploy Document Mindmap - -on: - push: - branches: [main] - paths: - - 'docs/mindmap.html' - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: pages - cancel-in-progress: true - -jobs: - deploy: - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - uses: actions/checkout@v6.0.2 - - - name: Prepare Pages artifact - run: | - mkdir _site - cp docs/mindmap.html _site/index.html - - - uses: actions/upload-pages-artifact@v4 - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/site-build.yml b/.github/workflows/site-build.yml new file mode 100644 index 0000000000..1b5f26c24e --- /dev/null +++ b/.github/workflows/site-build.yml @@ -0,0 +1,32 @@ +name: Build Site + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: site-build-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Prepare site + run: | + mkdir -p _site + cp docs/mindmap.html _site/index.html + + - uses: actions/upload-artifact@v4 + with: + name: site + path: _site/ + retention-days: 5 diff --git a/.github/workflows/site-deploy.yml b/.github/workflows/site-deploy.yml new file mode 100644 index 0000000000..9368b04ac1 --- /dev/null +++ b/.github/workflows/site-deploy.yml @@ -0,0 +1,241 @@ +name: Deploy Site + +on: + workflow_run: + workflows: [Build Site] + types: [completed] + +permissions: + contents: read + actions: read + deployments: write + pull-requests: write + +concurrency: + group: site-deploy-${{ github.event.workflow_run.event }}-${{ github.event.workflow_run.head_repository.owner.login }}-${{ github.event.workflow_run.head_branch }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.repository.full_name == github.repository && + contains(fromJSON('["pull_request","push"]'), github.event.workflow_run.event) + env: + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + steps: + - uses: actions/checkout@v6.0.2 + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: site + path: site/public + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Resolve preview context (PR number + preview alias) + id: preview-context + if: success() + uses: actions/github-script@v8 + with: + script: | + const run = context.payload.workflow_run; + if (run.event !== 'pull_request') { + core.setOutput('preview_alias', ''); + core.setOutput('pr_number', ''); + return; + } + + const owner = context.repo.owner; + const repo = context.repo.repo; + let prNumber = run.pull_requests?.[0]?.number; + + if (!prNumber) { + const head = `${run.head_repository.owner.login}:${run.head_branch}`; + const { data: prs } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + head, + per_page: 100, + }); + if (prs.length === 1) { + prNumber = prs[0].number; + } else if (prs.length === 0) { + core.setFailed(`Cannot resolve PR for preview: no open PR for head=${head}`); + return; + } else { + core.warning( + `Multiple open PRs (${prs.length}) for head=${head}; preview alias falls back to workflow_run.id; PR comment upsert skipped until head is unique`, + ); + prNumber = null; + } + } + + const workflowRunId = run.id; + const previewAlias = prNumber != null ? `pr-${prNumber}` : `pr-${workflowRunId}`; + core.setOutput('preview_alias', previewAlias); + core.setOutput('pr_number', prNumber != null ? String(prNumber) : ''); + + - name: Deploy to production (Workers + static assets) + id: cf-prod + if: github.event.workflow_run.event == 'push' + uses: cloudflare/wrangler-action@v3.14.1 + with: + wranglerVersion: "4.30.0" + workingDirectory: site + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: deploy --name="${{ vars.CLOUDFLARE_PROJECT_NAME }}" + + - name: Upload preview version (Workers + static assets) + id: cf-preview + if: github.event.workflow_run.event == 'pull_request' + uses: cloudflare/wrangler-action@v3.14.1 + with: + wranglerVersion: "4.30.0" + workingDirectory: site + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: >- + versions upload + --name="${{ vars.CLOUDFLARE_PROJECT_NAME }}" + --preview-alias ${{ steps.preview-context.outputs.preview_alias }} + + - name: Resolve deployment URL + id: meta + if: >- + (steps.cf-prod.outcome == 'success' || steps.cf-preview.outcome == 'success') + env: + URL_PROD: ${{ steps.cf-prod.outputs.deployment-url }} + URL_PREVIEW: ${{ steps.cf-preview.outputs.deployment-url }} + OUT_PROD: ${{ steps.cf-prod.outputs.command-output }} + ERR_PROD: ${{ steps.cf-prod.outputs.command-stderr }} + OUT_PR: ${{ steps.cf-preview.outputs.command-output }} + ERR_PR: ${{ steps.cf-preview.outputs.command-stderr }} + run: | + set -euo pipefail + url="${URL_PROD:-}" + if [ -z "$url" ]; then + url="${URL_PREVIEW:-}" + fi + if [ -z "$url" ]; then + comb="${OUT_PROD:-}${ERR_PROD:-}${OUT_PR:-}${ERR_PR:-}" + url=$(printf '%s' "$comb" | grep -oE 'https://[a-zA-Z0-9._/?#&=%_-]+' | grep -E '\.workers\.dev(/|$)' | head -1 || true) + fi + if [ -z "$url" ]; then + echo "::error::Could not determine Workers deployment URL from Wrangler output" + exit 1 + fi + echo "deployment_url=$url" >> "$GITHUB_OUTPUT" + + - name: Validate preview deployment URL (alias vs versioned URL) + if: >- + github.event.workflow_run.event == 'pull_request' && + steps.cf-preview.outcome == 'success' && + steps.meta.outcome == 'success' + env: + URL: ${{ steps.meta.outputs.deployment_url }} + ALIAS_TOKEN: ${{ steps.preview-context.outputs.preview_alias }} + run: | + set -euo pipefail + if [[ -z "${ALIAS_TOKEN:-}" ]]; then + exit 0 + fi + if [[ "$URL" != *"$ALIAS_TOKEN"* ]]; then + echo "::warning::Preview deployment URL does not include alias token '${ALIAS_TOKEN}' (got: ${URL}). wrangler-action may be returning a versioned URL instead of the preview-alias hostname; confirm in Cloudflare or Wrangler structured output." + fi + + - name: GitHub Deployment + preview comment + if: steps.meta.outcome == 'success' + uses: actions/github-script@v8 + env: + DEPLOYMENT_URL: ${{ steps.meta.outputs.deployment_url }} + PREVIEW_PR_NUMBER: ${{ steps.preview-context.outputs.pr_number }} + with: + script: | + const run = context.payload.workflow_run; + const owner = context.repo.owner; + const repo = context.repo.repo; + const sha = run.head_sha; + const isPR = run.event === 'pull_request'; + const environment = isPR ? 'site-preview' : 'site-production'; + const url = process.env.DEPLOYMENT_URL; + if (!url) { + core.setFailed('Missing deployment URL after Workers deploy/upload'); + return; + } + + const deployment = await github.rest.repos.createDeployment({ + owner, + repo, + ref: sha, + environment, + auto_merge: false, + required_contexts: [], + transient_environment: isPR, + production_environment: !isPR, + }); + const deploymentId = deployment.data.id; + + await github.rest.repos.createDeploymentStatus({ + owner, + repo, + deployment_id: deploymentId, + state: 'success', + environment_url: url, + description: 'Cloudflare Workers (static assets)', + auto_inactive: isPR, + }); + + if (!isPR) return; + + const raw = process.env.PREVIEW_PR_NUMBER || ''; + const prNumber = raw ? Number.parseInt(raw, 10) : NaN; + if (!Number.isFinite(prNumber)) { + core.warning( + 'Skipping PR preview comment upsert: no unique PR number (preview deployment and GitHub Deployment still recorded)', + ); + return; + } + + const marker = ''; + const body = [ + marker, + '### Site preview', + '', + `**Preview:** ${url}`, + '', + `Commit: \`${sha}\``, + ].join('\n'); + + let existing = null; + for (let page = 1; page <= 20; page++) { + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + per_page: 100, + page, + }); + existing = comments.find((c) => c.body?.includes(marker)); + if (existing || comments.length < 100) break; + } + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body, + }); + } diff --git a/docs/site-deployment.md b/docs/site-deployment.md new file mode 100644 index 0000000000..13dbbd05c8 --- /dev/null +++ b/docs/site-deployment.md @@ -0,0 +1,83 @@ +# Documentation site deployment (Cloudflare Workers) + +## Overview + +This repository publishes a static documentation site built from `docs/mindmap.html` (copied to `_site/index.html` in CI, then deployed from `site/public/`). Deployment uses **Cloudflare Workers with [static assets](https://developers.cloudflare.com/workers/static-assets/)** (not the legacy **Pages direct-upload** / `wrangler pages deploy` flow). + +Two GitHub Actions workflows: + +- **Build Site** — on `pull_request` and `push` to `main`, checks out the PR head when relevant, builds `_site/`, uploads artifact **`site`**. +- **Deploy Site** — on successful **Build Site** via `workflow_run`, checks out the repo (for [`site/wrangler.toml`](../site/wrangler.toml)), downloads the artifact into `site/public/`, then: + - **push to `main`:** `wrangler deploy` → production Worker traffic. + - **pull_request:** `wrangler versions upload --preview-alias pr-` → preview URL on `*.workers.dev` without changing production (alias falls back to `pr-` only when the same fork branch matches more than one open PR). + +GitHub **Deployments** use environments **`site-preview`** and **`site-production`**; PRs also get a single upserted comment with the preview link. + +For architecture and naming, see [2026-04-09-site-cloudflare-pages-design.md](superpowers/specs/2026-04-09-site-cloudflare-pages-design.md) (document filename still says “pages” for history; content describes Workers). + +## Cloudflare setup + +### Worker (not a Pages “project”) + +1. In the Cloudflare dashboard, use **Workers & Pages** → **Create** → **Create Worker** (or let the first `wrangler deploy` create it). The Worker name must match the GitHub variable below. +2. Configure **[preview URLs](https://developers.cloudflare.com/workers/configuration/previews/)** (default on when `workers_dev` is enabled). PR builds rely on **`wrangler versions upload`** with `--preview-alias`. +3. Optional: set a **[workers.dev](https://developers.cloudflare.com/workers/configuration/routing/workers-dev/)** subdomain for your account. + +### API token + +Create an API token that can deploy Workers for your account, for example: + +- **Account** → **Cloudflare Workers** → **Edit** (or the “Edit Cloudflare Workers” template), and +- **Account** → **Account Settings** → **Read** if Wrangler requires it. + +Store it as GitHub secret **`CLOUDFLARE_API_TOKEN`**. A token scoped **only** to “Cloudflare Pages — Edit” is **not** enough for `wrangler deploy` / `versions upload` on a Worker. + +### Account ID and Worker name + +- Copy **Account ID** → secret **`CLOUDFLARE_ACCOUNT_ID`**. +- Set **`CLOUDFLARE_PROJECT_NAME`** as a GitHub **Actions variable** (same name as before for compatibility): value = **Worker name** in the dashboard. The deploy workflow passes it as `wrangler deploy --name=…` / `versions upload --name=…`. + +### Custom domains (e.g. fork demo or `konflux.sh`) + +Attach routes or custom domains to the **Worker** (Workers → your Worker → **Domains & Routes**), not to a Pages project. Production URLs in GitHub Deployments will follow the hostname Wrangler reports (often `*.workers.dev` until a custom domain is primary). + +### Migrating from an old Pages project + +If you previously used **Cloudflare Pages** with `wrangler pages deploy`, create the Worker as above, point DNS/custom hostnames to the Worker, then disable or delete the old Pages project to avoid confusion. + +## GitHub fork phase 1 + +On a **fork**, open **Settings → Secrets and variables → Actions**. Add secrets **`CLOUDFLARE_API_TOKEN`**, **`CLOUDFLARE_ACCOUNT_ID`**, and variable **`CLOUDFLARE_PROJECT_NAME`** (Worker name). + +Under **Settings → Actions → General**, allow **Fork pull request workflows** from contributors so fork PRs can run **Build Site** without Cloudflare credentials in the fork. + +**Deploy Site** runs in the base repository with secrets; fork workflow logs should not show those values. + +## GitHub upstream phase 2 + +Configure the same secrets/variables at org or repo scope. Confirm **`pull-requests: write`** on the deploy workflow matches org policy for fork PR comments. + +Disable **GitHub Pages** under **Settings → Pages** if it was only used for this site. + +## Local preview (optional) + +From the repository root: + +```bash +mkdir -p site/public && cp docs/mindmap.html site/public/index.html +cd site && npx wrangler@4 dev +``` + +Requires a Cloudflare login or API token in the environment per [Wrangler docs](https://developers.cloudflare.com/workers/wrangler/). + +## Troubleshooting + +**Deploy job skipped.** The triggering workflow display name must be **Build Site** exactly, and `workflow_run.repository` must match the current repo. + +**`Could not determine Workers deployment URL`.** The workflow reads `deployment-url` from `cloudflare/wrangler-action`, then falls back to parsing Wrangler stdout/stderr for a `workers.dev` URL. Upgrade **`wranglerVersion`** in the workflow if Wrangler output format changed. + +**Preview upload fails (PR builds).** Requires Wrangler **≥ 4.21.0** for `--preview-alias`. The workflow pins **4.30.0**. + +**Artifact download 404.** **Build Site** must upload artifact **`site`**; **Deploy Site** needs `actions: read`. + +**No PR comment.** Same as before: ambiguous `head` when resolving the PR number; see the design spec. diff --git a/docs/superpowers/plans/2026-04-09-site-cloudflare-pages.md b/docs/superpowers/plans/2026-04-09-site-cloudflare-pages.md new file mode 100644 index 0000000000..24e179a5f8 --- /dev/null +++ b/docs/superpowers/plans/2026-04-09-site-cloudflare-pages.md @@ -0,0 +1,243 @@ +# Documentation site → Cloudflare Workers (fork-safe CI) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace GitHub Pages with **Cloudflare Workers static assets** (production + per-PR previews), using a secretless **Build Site** workflow plus a **`workflow_run` Deploy Site** workflow with Cloudflare + GitHub Deployment credentials, **`site-preview` / `site-production`**, and an upserted PR comment. Naming uses **site** throughout (workflows, artifact); the mindmap is the current `index.html` source only. + +**Architecture:** **`Build Site`** runs on `pull_request` and `push` to `main`, checks out the PR head on PRs, produces `_site/`, uploads artifact **`site`**. **`Deploy Site`** checks out the repo (for `site/wrangler.toml`), downloads the artifact into **`site/public/`**, runs **`wrangler deploy`** on **`push`** and **`wrangler versions upload --preview-alias pr-`** (falls back to `workflow_run.id` only when multiple open PRs share the same head) on **`pull_request`** (Wrangler **4.30.0** via `cloudflare/wrangler-action@v3.14.1` + `wranglerVersion`), resolves a **`workers.dev`** URL for GitHub, then `actions/github-script` records Deployments and comments. + +**Tech Stack:** GitHub Actions, Cloudflare **Workers** (static assets), Wrangler **4.x**, `cloudflare/wrangler-action@v3.14.1`, `actions/github-script@v8`, REST Deployments API. + +**Spec:** [2026-04-09-site-cloudflare-pages-design.md](../specs/2026-04-09-site-cloudflare-pages-design.md) + +--- + +## File map + +| File | Role | +|------|------| +| `.github/workflows/site-build.yml` | Secretless build + artifact `site` | +| `.github/workflows/site-deploy.yml` | Checkout + artifact → `site/public/`, `wrangler deploy` / `versions upload`, GitHub Deployment + PR comment | +| `site/wrangler.toml` | Worker name placeholder, `assets.directory = public`, SPA `not_found_handling`, `preview_urls` | +| `site/public/.gitkeep` | Keeps `public/` in git; CI overwrites with artifact contents | +| `.github/workflows/mindmap.yml` | **Removed** (replaced by `site-build.yml` / `site-deploy.yml`) | +| `docs/site-deployment.md` | Operator runbook: Worker, token scopes (Workers Edit), secrets/variables, fork policy, troubleshooting | + +--- + +### Task 1: Add build workflow + +**Files:** + +- Create: `.github/workflows/site-build.yml` + +- [ ] **Step 1: Create the workflow file** + +Use this exact content (pin `actions/checkout` to `v6.0.2` to match other workflows in this repo): + +```yaml +name: Build Site + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: site-build-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6.0.2 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Prepare site + run: | + mkdir -p _site + cp docs/mindmap.html _site/index.html + + - uses: actions/upload-artifact@v4 + with: + name: site + path: _site/ + retention-days: 5 +``` + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/site-build.yml +git commit -m "ci: add site build workflow for Cloudflare handoff" +``` + +--- + +### Task 2: Add deploy workflow + +**Files:** + +- Create or update: `.github/workflows/site-deploy.yml` + +- [ ] **Step 1: Implement Deploy Site (canonical copy in repo)** + +The job must only run for successful runs of **this repository’s** **Build Site** workflow, and only for `pull_request` or `push` events. + +**Behavior (Workers, not Pages):** + +1. `actions/checkout` (so `site/wrangler.toml` exists). +2. Download artifact **`site`** into **`site/public/`**. +3. **`push`:** `cloudflare/wrangler-action` with `wranglerVersion: "4.30.0"`, `workingDirectory: site`, `command: deploy --name=`. +4. **`pull_request`:** same action with `command: versions upload --name= --preview-alias pr-` (asset config from `site/wrangler.toml` only; alias falls back to `pr-` if the head matches multiple open PRs). +5. **Resolve URL:** `deployment-url` output, else parse stdout/stderr for `workers.dev`. +6. **`actions/github-script`:** GitHub Deployments + PR comment; `description: Cloudflare Workers (static assets)`. + +Copy the full YAML from the repository file [`.github/workflows/site-deploy.yml`](../../../.github/workflows/site-deploy.yml) when implementing in another clone. + +- **`vars.CLOUDFLARE_PROJECT_NAME`:** Worker name (same variable name as before). + +- [ ] **Step 2: Commit** + +```bash +git add .github/workflows/site-deploy.yml site/wrangler.toml site/public/.gitkeep +git commit -m "ci: deploy site with Workers static assets" +``` + +--- + +### Task 3: Remove GitHub Pages workflow + +**Files:** + +- Delete: `.github/workflows/mindmap.yml` (the prior GitHub Pages / mindmap deploy workflow) + +- [ ] **Step 1: Delete the file** + +Remove `.github/workflows/mindmap.yml` entirely so the site is no longer deployed via `actions/deploy-pages`. + +- [ ] **Step 2: Commit** + +```bash +git rm .github/workflows/mindmap.yml +git commit -m "ci: drop GitHub Pages workflow for documentation site" +``` + +--- + +### Task 4: Operator runbook + +**Files:** + +- Create: `docs/site-deployment.md` + +- [ ] **Step 1: Add the runbook** + +Create `docs/site-deployment.md` with the following sections (adjust org/repo names when copying for upstream): + +1. **Overview** — Link to the design spec `docs/superpowers/specs/2026-04-09-site-cloudflare-pages-design.md` and summarize **`Build Site`** / **`Deploy Site`** (Workers + static assets). +2. **Cloudflare setup** + - Create a **Worker** (or let first `wrangler deploy` create it); enable **preview URLs**; optional **workers.dev** subdomain. + - Create an **API Token** with **Cloudflare Workers → Edit** (and **Account Settings → Read** if needed). Pages-only tokens are **not** sufficient. Store as `CLOUDFLARE_API_TOKEN`. + - Copy **Account ID** → `CLOUDFLARE_ACCOUNT_ID`. + - Add **`CLOUDFLARE_PROJECT_NAME`** as a GitHub **Actions variable** = **Worker name**. + - Optional **custom domain** (fork demos or later `konflux.sh`): attach routes on the **Worker**, not a Pages project. +3. **GitHub setup (fork — phase 1)** + - Repository → **Settings → Secrets and variables → Actions**: + - Secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` + - Variables: `CLOUDFLARE_PROJECT_NAME` + - **Settings → Actions → General → Fork pull request workflows**: allow workflows from contributors (so fork PRs can run the **build** workflow). +4. **GitHub setup (upstream — phase 2)** + - Same secrets/variables at **org or repo** level as your governance prefers. + - Confirm the deploy workflow’s `GITHUB_TOKEN` can comment on fork PRs (`pull-requests: write` is already declared in the workflow). + - After cutover, **disable GitHub Pages** for this repo if it was only used for this site (**Settings → Pages**). + - **Later:** attach **`konflux.sh`** (or a subdomain) to the Worker; production URLs in Deployments follow Wrangler output (often `*.workers.dev` until custom domain is primary). +5. **Troubleshooting** + - **Deploy job skipped:** wrong triggering workflow name (must match **`Build Site`** exactly), or `workflow_run.repository` not equal to current repo. + - **`Could not determine Workers deployment URL`:** check `wrangler-action` outputs and Wrangler **4.x** stdout/stderr; workflow pins **4.30.0**. + - **Artifact download 404:** deploy job needs `actions: read` and correct `run-id` (already set); build must have uploaded artifact **`site`**. + - **No PR comment:** `workflow_run.pull_requests` empty and `pulls.list` with `head=owner:branch` did not return exactly one open PR, or multiple open PRs share the same head (the workflow logs a warning and still deploys a preview; comment upsert is skipped until the head is unique). + +- [ ] **Step 2: Commit** + +```bash +git add docs/site-deployment.md +git commit -m "docs: add documentation site Cloudflare operator runbook" +``` + +--- + +### Task 5: Phase 1 validation (fork) + +**Files:** none (manual) + +- [ ] **Step 1: Configure Cloudflare + GitHub** per `docs/site-deployment.md` on your fork. + +- [ ] **Step 2: Push a commit on `main` that touches `docs/mindmap.html`** + +Expected: **`Build Site`** succeeds; **`Deploy Site`** runs; Cloudflare **production Worker** updates; GitHub shows **`site-production`** with `environment_url` on **`workers.dev`** (or your custom host). + +- [ ] **Step 3: Open a PR (same repo)** (any change that triggers **Build Site**) + +Expected: **`wrangler versions upload`** preview; **`site-preview`** deployment; one PR comment updated on reruns. + +- [ ] **Step 4: Open a PR from a second GitHub user / fork** (or your own fork of your fork) changing `docs/mindmap.html`** + +Expected: build succeeds on the base repo without Cloudflare secrets in fork logs; deploy + comment still occur from the base repo’s deploy workflow. + +--- + +### Task 6: Phase 2 — upstream PR + +**Files:** none (manual); branch should contain Tasks 1–4 commits. + +- [ ] **Step 1: Push your branch to origin and open a PR** against `konflux-ci/fullsend` (or upstream default branch). + +- [ ] **Step 2: In the PR description**, list maintainer follow-ups: add Actions secrets/variables, verify fork workflow policy, disable legacy GitHub Pages when ready, optional `konflux.sh` DNS later. + +- [ ] **Step 3: After merge**, repeat a subset of Task 5 checks on upstream. + +--- + +## Plan self-review + +**1. Spec coverage** + +| Spec requirement | Task | +|------------------|------| +| Cloudflare Workers instead of GitHub Pages | Tasks 2–3 | +| Two-phase build + `workflow_run` deploy | Tasks 1–2 | +| `site-preview` / `site-production` | Task 2 (`createDeployment`) | +| PR comment upsert + PR resolution fallback | Task 2 (github-script) | +| PR head checkout | Task 1 | +| Fork-safe (no secrets on build) | Tasks 1 vs 2 permissions | +| Operator docs + phases | Tasks 4–6 | +| Concurrency | Both workflows | +| `*.workers.dev` then `konflux.sh` on Worker | Task 4 runbook | + +**2. Placeholder scan** + +No TBD/TODO left in workflow YAML or task text; `cloudflare/wrangler-action@v3.14.1` with **`wranglerVersion: "4.30.0"`**, github-script `v8`. + +**3. Type / naming consistency** + +- Single artifact name **`site`** in build and deploy. +- Build workflow display name must stay **`Build Site`** — it is the `workflow_run.workflows` filter target. +- Environment names exactly `site-preview` and `site-production`. + +**Known follow-up (optional hardening):** If `createDeployment` returns **409** for a rare duplicate ref/environment case, extend the github-script to locate the existing deployment and only create a status (not required for normal one-commit-per-deploy usage). + +--- + +**Plan complete and saved to `docs/superpowers/plans/2026-04-09-site-cloudflare-pages.md`. Two execution options:** + +**1. Subagent-Driven (recommended)** — Dispatch a fresh subagent per task, review between tasks, fast iteration. + +**2. Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints. + +**Which approach do you want?** diff --git a/docs/superpowers/specs/2026-04-09-site-cloudflare-pages-design.md b/docs/superpowers/specs/2026-04-09-site-cloudflare-pages-design.md new file mode 100644 index 0000000000..84e5eaf9c8 --- /dev/null +++ b/docs/superpowers/specs/2026-04-09-site-cloudflare-pages-design.md @@ -0,0 +1,105 @@ +# Design: Documentation site on Cloudflare Workers (static assets, PR previews, fork-safe CI) + +Date: 2026-04-09 +Status: Draft (brainstorm consolidated) + +> **Note:** Filename retains `cloudflare-pages` for link stability; the implementation uses **Workers + static assets** ([migration guide](https://developers.cloudflare.com/workers/static-assets/migration-guides/migrate-from-pages/)), not `wrangler pages deploy`. + +## Context + +The repository publishes a **static documentation site**. Today the primary surface is the interactive document graph in `docs/mindmap.html`; the site will likely **grow** (more pages or assets under `docs/` or a dedicated static tree). CI treats this as **one deployable site**: produce a directory (today `_site/` with `index.html` from the mindmap), upload it as artifact **`site`**, then deploy from **`site/public/`** using Wrangler and [`site/wrangler.toml`](../../../site/wrangler.toml). + +**Implemented:** [`.github/workflows/site-build.yml`](../../../.github/workflows/site-build.yml) and [`.github/workflows/site-deploy.yml`](../../../.github/workflows/site-deploy.yml) use the build → artifact → `workflow_run` deploy split. **Production** uses **`wrangler deploy`** (Worker + static assets). **Pull requests** use **`wrangler versions upload --preview-alias …`** so previews get a stable **`*.workers.dev`** URL without promoting a new production version. The previous GitHub Pages workflow has been **removed**. + +**Operator setup:** Cloudflare **Worker**, API token with **Workers** permissions, and GitHub Actions secrets/variables are required; see [`docs/site-deployment.md`](../../site-deployment.md). + +## Goals + +- Deploy this **documentation site** to **Cloudflare Workers** (static assets binding), not GitHub Pages and not the legacy Pages-only upload path. +- **Per-PR previews**, including **fork PRs**, using the **two-workflow** pattern: unprivileged build + artifact, privileged deploy. +- Integrate with **GitHub Deployments** using **`site-preview`** and **`site-production`**, with `environment_url` pointing at the Worker URL (`*.workers.dev` or custom domain). +- Surface preview links via **Deployments** and a **single upserted PR comment**. +- Use **stable workflow and artifact names** centered on **site** as content grows beyond the mindmap. +- Roll out in **two phases** (fork validation, then upstream). Custom domains (e.g. **`konflux.sh`**) attach to the **Worker** when DNS is ready. + +## Non-goals + +- Rewriting site **application** code beyond packaging. +- **Workers Builds** (Cloudflare-hosted CI) as the source of truth—**GitHub Actions** remains the deploy driver unless the project later opts in. +- OIDC to Cloudflare in the initial design; **API token** in secrets is sufficient. + +## Approach comparison (condensed) + +| Approach | Idea | Verdict | +|----------|------|--------| +| **A — `workflow_run` + artifact** | Secretless build uploads artifact; privileged workflow deploys. | **Chosen.** Fork-safe. | +| **B — `pull_request_target`** | Deploy with base-repo secrets on PR. | **Rejected** for untrusted build steps. | +| **C — External bot** | Webhook-driven deploy. | **Rejected** for this static site. | + +**Deploy tooling:** **Wrangler 4.x** via `cloudflare/wrangler-action`, **`wrangler deploy`** for **`push` to `main`**, **`wrangler versions upload --preview-alias`** for **`pull_request`** previews ([preview URLs](https://developers.cloudflare.com/workers/configuration/previews/)). + +## Architecture + +### Workflow split + +1. **Build (`site-build.yml`):** `pull_request` + `push` to `main` (no `paths` filter in current fork—runs on every PR/push; may be narrowed later). Produces **`site`** artifact (`_site/`). +2. **Deploy (`site-deploy.yml`):** On successful **Build Site**, checkout (for `site/wrangler.toml`), download artifact into **`site/public/`**, then: + - **`push`:** `wrangler deploy --name=` → production. + - **`pull_request`:** `wrangler versions upload` (asset config from `wrangler.toml` only) with `--preview-alias pr-` (falls back to `workflow_run.id` only when multiple open PRs share the same head) → preview only. + +**Permissions:** Build: `contents: read` only. Deploy: `actions: read`, `deployments: write`, `pull-requests: write`. No `pages: write` for this site. + +### `site/wrangler.toml` + +- **`[assets].directory`:** `./public` (filled in CI). +- **`not_found_handling = "single-page-application"`** for the single-page mindmap. +- **`workers_dev = true`**, **`preview_urls = true`**. +- **No `main`** (assets-only Worker). + +### GitHub environment names + +- **`site-preview`** — PR uploads; `transient_environment: true`. +- **`site-production`** — production deploy; `production_environment: true`. + +### Cloudflare + +- One **Worker**; GitHub variable **`CLOUDFLARE_PROJECT_NAME`** holds the Worker name (name kept for backward compatibility). +- API token: **Workers** (and Account Read if needed), not Pages-only. + +### Domains + +- Default **`*.workers.dev`**; custom domains on the Worker when ready (**`konflux.sh`** upstream). + +### Security (fork PRs) + +Same as before: minimal auditable build; deploy trusts artifacts from the known build workflow. + +### Removal of GitHub Pages + +**Done.** Disable **Settings → Pages** if unused. + +## Rollout phases + +### Phase 1 — Fork + +- Configure Worker + token + secrets; validate production and fork PR preview + comment. + +### Phase 2 — Upstream + +- Land workflows + runbook; org secrets; Worker + optional **`konflux.sh`** on Worker routes. + +## Operator documentation + +See [`docs/site-deployment.md`](../../site-deployment.md). + +## Testing and acceptance + +- **`main` push:** production Worker updates; **`site-production`** deployment with correct URL. +- **PR:** preview URL on `workers.dev`, **`site-preview`**, PR comment; fork build has no secrets. +- **Wrangler:** pinned **4.30.0** in workflow; preview alias requires **≥ 4.21.0**. + +## Spec self-review + +- **Consistency:** `site-preview` / `site-production`; Worker + static assets; artifact **`site`**. +- **Scope:** CI and packaging only. +- **Workers vs Pages:** Implementation is Workers; filename is historical. diff --git a/site/public/.gitkeep b/site/public/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/site/wrangler.toml b/site/wrangler.toml new file mode 100644 index 0000000000..e7bcedf80a --- /dev/null +++ b/site/wrangler.toml @@ -0,0 +1,12 @@ +# Cloudflare Worker serving static assets only (no user Worker script). +# The deploy workflow passes --name to match your account Worker name (GitHub variable CLOUDFLARE_PROJECT_NAME). +# See: https://developers.cloudflare.com/workers/static-assets/ + +name = "documentation-site" +compatibility_date = "2026-04-09" +workers_dev = true +preview_urls = true + +[assets] +directory = "./public" +not_found_handling = "single-page-application"