diff --git a/.gitignore b/.gitignore index c51ee79..47112b4 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ build/qa-evidence/ # Rust build artifacts apps/headless-rs/target/ + +# Local Vercel state and environment secrets +.vercel/ +.env.local +.env.*.local diff --git a/AGENTS.md b/AGENTS.md index e2e28a0..49a70ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Read before making non-trivial changes: check items off when you fix them and add the named test. - `CONTRIBUTING.md` — the same rules for humans, plus setup detail. - `SECURITY.md` — the boundaries a bug report is measured against. -- To *use* Headless as a browser tool (rather than develop it), follow the +- To _use_ Headless as a browser tool (rather than develop it), follow the skill: `.agents/skills/headless-computer-use/SKILL.md`. ## Layout @@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in - Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract, deferrals, known limitations). Keep README claims backed by tests or evidence. -- Web (`apps/web`): content is currently hand-duplicated in three places - (backlog §F2) — if you change CLI behavior, grep the site - (`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and - update all copies. +- Web (`apps/web`): rendered content derives from `README.md`, + `apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update + those sources when CLI behavior changes; web lint checks their provenance. - Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`, `ci:`, scope in parens like `fix(macos):`). @@ -130,3 +129,14 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"` in `Protocol.swift`) is independent — bump it only for wire-visible changes, with a decision entry. + +## Website deployment + +Vercel deploys `apps/web` with that directory configured as the project root, +using [`apps/web/vercel.json`](apps/web/vercel.json). The production branch is +`main`, and the canonical production URL is +. Keep the Vercel for GitHub integration +enabled for pull-request previews and preview-URL comments. Do not add a second +deployment workflow that can race the integration. +Hosting setup, verification, rollback, and the custom-domain decision are in +[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md). diff --git a/apps/web/package.json b/apps/web/package.json index 72eeee5..e6bc4d7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .", + "lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && node scripts/validate-deployment-config.mjs && eslint .", "brand": "node scripts/render-brand.mjs" }, "dependencies": { diff --git a/apps/web/scripts/validate-deployment-config.mjs b/apps/web/scripts/validate-deployment-config.mjs new file mode 100644 index 0000000..9d1d676 --- /dev/null +++ b/apps/web/scripts/validate-deployment-config.mjs @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const repositoryRoot = resolve(import.meta.dirname, "../../.."); +const webRoot = resolve(repositoryRoot, "apps/web"); +const readRepositoryFile = (path) => + readFile(resolve(repositoryRoot, path), "utf8"); +const readWebFile = (path) => readFile(resolve(webRoot, path), "utf8"); +const config = JSON.parse(await readWebFile("vercel.json")); + +assert.deepEqual(config, { + $schema: "https://openapi.vercel.sh/vercel.json", + framework: "nextjs", + buildCommand: "pnpm build", + devCommand: "pnpm exec next dev --port $PORT", + outputDirectory: ".next", +}); + +const productionUrl = "https://headless-web-pi.vercel.app"; +const [ + metadata, + deploymentDocs, + agentRules, + nextConfig, + rootPackage, + lockfile, +] = await Promise.all([ + readWebFile("lib/site-metadata.ts"), + readRepositoryFile("docs/DEPLOYMENT.md"), + readRepositoryFile("AGENTS.md"), + readWebFile("next.config.ts"), + readRepositoryFile("package.json"), + readRepositoryFile("pnpm-lock.yaml"), +]); + +const packageJson = JSON.parse(rootPackage); +assert.match(packageJson.packageManager ?? "", /^pnpm@9\./); +assert.match(packageJson.engines?.pnpm ?? "", />=9/); +assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m); +assert.equal(config.installCommand, undefined); + +for (const source of [metadata, deploymentDocs, agentRules]) { + assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\."))); +} + +assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i); +for (const header of [ + "Content-Security-Policy", + "Permissions-Policy", + "Referrer-Policy", + "X-Content-Type-Options", + "X-Frame-Options", +]) { + assert.match(nextConfig, new RegExp(`key: "${header}"`)); +} +for (const directive of [ + "base-uri 'none'", + "frame-ancestors 'none'", + "object-src 'none'", +]) { + assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'"))); +} + +console.log("Vercel deployment configuration is consistent"); diff --git a/apps/web/vercel.json b/apps/web/vercel.json new file mode 100644 index 0000000..8e852cc --- /dev/null +++ b/apps/web/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs", + "buildCommand": "pnpm build", + "devCommand": "pnpm exec next dev --port $PORT", + "outputDirectory": ".next" +} diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..442e41e --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,69 @@ +# Website deployment + +The marketing and documentation site is deployed to Vercel from this monorepo. +The application configuration in +[`apps/web/vercel.json`](../apps/web/vercel.json) is the source of truth for +framework detection, build and development commands, and output location. +Vercel derives pnpm from the repository lockfile. Do not add an install override +with plain `pnpm install`: Vercel uses its oldest available pnpm runtime for that +override, while this repository requires pnpm 9 or newer. + +## Production contract + +- **Production branch:** `main`. +- **Production URL:** . +- **Project root:** `apps/web`. +- **Application:** `@headless/web`. +- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in + `vercel.json`, where they could drift from local and CI builds. + +The Vercel project alias is the canonical domain for now. The LockInTime +organization does not publish a verifiable custom domain in repository or +organization metadata, so this project must not claim one. A custom domain can +replace the alias only after a maintainer confirms control of its DNS. That +change must update `apps/web/lib/site-metadata.ts`, the GitHub repository +homepage, this document, and the Vercel production-domain assignment together. + +## GitHub integration + +Connect the `LockInTime/headless` repository through Vercel for GitHub with +these project settings: + +1. Set Root Directory to `apps/web` so Vercel reads the application-local + `vercel.json` and detects Next.js from the application package. +2. Enable "Include source files outside of the Root Directory in the Build + Step". The site imports checked-in documentation and package metadata from + the repository root, `apps/headless`, and `packages` during its build. +3. Set the production branch to `main`. +4. Keep preview deployments enabled for pull requests and branch pushes. +5. Keep pull-request comments enabled so each PR receives its immutable preview + URL. Keep deployment status events enabled so the URL also appears in the + GitHub deployment timeline. +6. Do not add a second token-driven GitHub Actions deployment. Two independent + deployers can race production aliases and make rollback history ambiguous. + +The integration is an account-level control and cannot be stored in git. If a +PR has no Vercel deployment or preview link, treat that as a disconnected or +disabled integration. A Vercel project maintainer must reconnect the repository +under Project Settings, Git before the PR is considered deployment-verified. + +## Verification + +Run the same web gates locally before pushing: + +```sh +pnpm install --frozen-lockfile --filter @headless/web +pnpm --filter @headless/web lint +pnpm --filter @headless/web build +``` + +For a pull request, open the Vercel preview from the PR deployment entry and +check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the +response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`, +`Referrer-Policy`, and `Permissions-Policy` headers declared in +`apps/web/next.config.ts`. + +After merging, verify that the production deployment points at the merge commit +and that serves it. Vercel keeps prior +production deployments available for rollback. Roll back in Vercel, then +revert the faulty commit in git so repository history and production converge. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 2dc8f6a..fe76ffb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`) notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux bootstrap, release checksums, and a multi-platform GHCR image. These paths become user-visible with the next tag. -- A Next.js marketing/docs site (`apps/web`) — built, not deployed. +- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`. - An agent skill (`.agents/skills/headless-computer-use/`) with safety rules, command reference, and a Docker sandbox wrapper. @@ -118,9 +118,6 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`) unimplemented. - The latest features (capture formats, context pruning) are **unreleased** — no tag since v1.0.2 (2026-07-19). -- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code. -- The website's benchmark numbers, docs prose, and commands are hand-copied in - three places each and will drift; the site has no deploy pipeline. - Windows is not supported. - A list of real code defects (thread-safety on shutdown, oversized `qa report` responses, `@eN` ref invalidation surprises, host code duplication) @@ -265,7 +262,8 @@ and drive Headless with zero manual prompting beyond repo checkout. ### Phase 5 — Website and docs as a product surface -- Deploy `apps/web` (Vercel or static export + CDN) with CI. +- Keep the Vercel deployment of `apps/web` reproducible, previewable, and + verified alongside CI. - Kill the three-copy content drift: docs prose and benchmark numbers come from single sources (benchmark emits JSON; site imports it; command tables generated from the CLI) (backlog §F). diff --git a/docs/roadmap/improvements-backlog.md b/docs/roadmap/improvements-backlog.md index 162c81c..fa9aceb 100644 --- a/docs/roadmap/improvements-backlog.md +++ b/docs/roadmap/improvements-backlog.md @@ -408,14 +408,12 @@ Owner-decided scope: package managers, no hosted service. ## §F — Website & docs (Phase 5) -- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at - `https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's - GitHub integration, but nothing in the tree records that: no `vercel.json`, - no deploy docs, no preview-URL comment on PRs, and the temporary - `*-pi.vercel.app` hostname suggests no custom domain. Make the deployment - reproducible and reviewable — check in the project config, document the - hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP - in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped. +- **F1.** [x] ([#47](https://github.com/LockInTime/headless/issues/47)) + The Vercel deployment is repo-visible and verified. Application-local + settings are versioned and linted, the preview and rollback contract is + documented, security headers remain in Next.js, and a Git-backed pull-request + preview was verified before merge. The proven Vercel project alias remains + canonical until the organization publishes a controlled custom domain. - **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in `app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`, `components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose