Skip to content

Commit 127b380

Browse files
authored
ci(web): codify the Vercel deployment (#146)
1 parent b717a8d commit 127b380

8 files changed

Lines changed: 171 additions & 19 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@ build/qa-evidence/
2121

2222
# Rust build artifacts
2323
apps/headless-rs/target/
24+
25+
# Local Vercel state and environment secrets
26+
.vercel/
27+
.env.local
28+
.env.*.local

AGENTS.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Read before making non-trivial changes:
2323
check items off when you fix them and add the named test.
2424
- `CONTRIBUTING.md` — the same rules for humans, plus setup detail.
2525
- `SECURITY.md` — the boundaries a bug report is measured against.
26-
- To *use* Headless as a browser tool (rather than develop it), follow the
26+
- To _use_ Headless as a browser tool (rather than develop it), follow the
2727
skill: `.agents/skills/headless-computer-use/SKILL.md`.
2828

2929
## Layout
@@ -117,10 +117,9 @@ If a change brushes against any of these, stop and record a decision in
117117
- Docs: feature docs live in the phase contracts (P0/P1/P2 style — contract,
118118
deferrals, known limitations). Keep README claims backed by tests or
119119
evidence.
120-
- Web (`apps/web`): content is currently hand-duplicated in three places
121-
(backlog §F2) — if you change CLI behavior, grep the site
122-
(`app/docs/page.tsx`, `components/docs-markdown.ts`, `README.md`) and
123-
update all copies.
120+
- Web (`apps/web`): rendered content derives from `README.md`,
121+
`apps/headless/docs/COMMANDS.md`, and the generated benchmark results. Update
122+
those sources when CLI behavior changes; web lint checks their provenance.
124123
- Commits: conventional-ish prefixes in use (`feat:`, `fix:`, `docs:`,
125124
`ci:`, scope in parens like `fix(macos):`).
126125

@@ -130,3 +129,14 @@ Tags `v*` trigger `.github/workflows/release.yml` (macOS zip + Linux
130129
tarballs). `HEADLESS_VERSION` flows from the tag; protocol version (`"0.5"`
131130
in `Protocol.swift`) is independent — bump it only for wire-visible changes,
132131
with a decision entry.
132+
133+
## Website deployment
134+
135+
Vercel deploys `apps/web` with that directory configured as the project root,
136+
using [`apps/web/vercel.json`](apps/web/vercel.json). The production branch is
137+
`main`, and the canonical production URL is
138+
<https://headless-web-pi.vercel.app>. Keep the Vercel for GitHub integration
139+
enabled for pull-request previews and preview-URL comments. Do not add a second
140+
deployment workflow that can race the integration.
141+
Hosting setup, verification, rollback, and the custom-domain decision are in
142+
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "node scripts/validate-harness-onboarding.mjs && node scripts/validate-content-provenance.mjs && node scripts/validate-bundle-policy.mjs && eslint .",
9+
"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 .",
1010
"brand": "node scripts/render-brand.mjs"
1111
},
1212
"dependencies": {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import assert from "node:assert/strict";
2+
import { readFile } from "node:fs/promises";
3+
import { resolve } from "node:path";
4+
5+
const repositoryRoot = resolve(import.meta.dirname, "../../..");
6+
const webRoot = resolve(repositoryRoot, "apps/web");
7+
const readRepositoryFile = (path) =>
8+
readFile(resolve(repositoryRoot, path), "utf8");
9+
const readWebFile = (path) => readFile(resolve(webRoot, path), "utf8");
10+
const config = JSON.parse(await readWebFile("vercel.json"));
11+
12+
assert.deepEqual(config, {
13+
$schema: "https://openapi.vercel.sh/vercel.json",
14+
framework: "nextjs",
15+
buildCommand: "pnpm build",
16+
devCommand: "pnpm exec next dev --port $PORT",
17+
outputDirectory: ".next",
18+
});
19+
20+
const productionUrl = "https://headless-web-pi.vercel.app";
21+
const [
22+
metadata,
23+
deploymentDocs,
24+
agentRules,
25+
nextConfig,
26+
rootPackage,
27+
lockfile,
28+
] = await Promise.all([
29+
readWebFile("lib/site-metadata.ts"),
30+
readRepositoryFile("docs/DEPLOYMENT.md"),
31+
readRepositoryFile("AGENTS.md"),
32+
readWebFile("next.config.ts"),
33+
readRepositoryFile("package.json"),
34+
readRepositoryFile("pnpm-lock.yaml"),
35+
]);
36+
37+
const packageJson = JSON.parse(rootPackage);
38+
assert.match(packageJson.packageManager ?? "", /^pnpm@9\./);
39+
assert.match(packageJson.engines?.pnpm ?? "", />=9/);
40+
assert.match(lockfile, /^lockfileVersion: ['"]?9\.0['"]?$/m);
41+
assert.equal(config.installCommand, undefined);
42+
43+
for (const source of [metadata, deploymentDocs, agentRules]) {
44+
assert.match(source, new RegExp(productionUrl.replaceAll(".", "\\.")));
45+
}
46+
47+
assert.doesNotMatch(JSON.stringify(config), /headers|contentSecurityPolicy/i);
48+
for (const header of [
49+
"Content-Security-Policy",
50+
"Permissions-Policy",
51+
"Referrer-Policy",
52+
"X-Content-Type-Options",
53+
"X-Frame-Options",
54+
]) {
55+
assert.match(nextConfig, new RegExp(`key: "${header}"`));
56+
}
57+
for (const directive of [
58+
"base-uri 'none'",
59+
"frame-ancestors 'none'",
60+
"object-src 'none'",
61+
]) {
62+
assert.match(nextConfig, new RegExp(directive.replaceAll("'", "\\'")));
63+
}
64+
65+
console.log("Vercel deployment configuration is consistent");

apps/web/vercel.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "https://openapi.vercel.sh/vercel.json",
3+
"framework": "nextjs",
4+
"buildCommand": "pnpm build",
5+
"devCommand": "pnpm exec next dev --port $PORT",
6+
"outputDirectory": ".next"
7+
}

docs/DEPLOYMENT.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Website deployment
2+
3+
The marketing and documentation site is deployed to Vercel from this monorepo.
4+
The application configuration in
5+
[`apps/web/vercel.json`](../apps/web/vercel.json) is the source of truth for
6+
framework detection, build and development commands, and output location.
7+
Vercel derives pnpm from the repository lockfile. Do not add an install override
8+
with plain `pnpm install`: Vercel uses its oldest available pnpm runtime for that
9+
override, while this repository requires pnpm 9 or newer.
10+
11+
## Production contract
12+
13+
- **Production branch:** `main`.
14+
- **Production URL:** <https://headless-web-pi.vercel.app>.
15+
- **Project root:** `apps/web`.
16+
- **Application:** `@headless/web`.
17+
- **Security headers:** `apps/web/next.config.ts`. Do not duplicate them in
18+
`vercel.json`, where they could drift from local and CI builds.
19+
20+
The Vercel project alias is the canonical domain for now. The LockInTime
21+
organization does not publish a verifiable custom domain in repository or
22+
organization metadata, so this project must not claim one. A custom domain can
23+
replace the alias only after a maintainer confirms control of its DNS. That
24+
change must update `apps/web/lib/site-metadata.ts`, the GitHub repository
25+
homepage, this document, and the Vercel production-domain assignment together.
26+
27+
## GitHub integration
28+
29+
Connect the `LockInTime/headless` repository through Vercel for GitHub with
30+
these project settings:
31+
32+
1. Set Root Directory to `apps/web` so Vercel reads the application-local
33+
`vercel.json` and detects Next.js from the application package.
34+
2. Enable "Include source files outside of the Root Directory in the Build
35+
Step". The site imports checked-in documentation and package metadata from
36+
the repository root, `apps/headless`, and `packages` during its build.
37+
3. Set the production branch to `main`.
38+
4. Keep preview deployments enabled for pull requests and branch pushes.
39+
5. Keep pull-request comments enabled so each PR receives its immutable preview
40+
URL. Keep deployment status events enabled so the URL also appears in the
41+
GitHub deployment timeline.
42+
6. Do not add a second token-driven GitHub Actions deployment. Two independent
43+
deployers can race production aliases and make rollback history ambiguous.
44+
45+
The integration is an account-level control and cannot be stored in git. If a
46+
PR has no Vercel deployment or preview link, treat that as a disconnected or
47+
disabled integration. A Vercel project maintainer must reconnect the repository
48+
under Project Settings, Git before the PR is considered deployment-verified.
49+
50+
## Verification
51+
52+
Run the same web gates locally before pushing:
53+
54+
```sh
55+
pnpm install --frozen-lockfile --filter @headless/web
56+
pnpm --filter @headless/web lint
57+
pnpm --filter @headless/web build
58+
```
59+
60+
For a pull request, open the Vercel preview from the PR deployment entry and
61+
check the homepage, one docs route, `robots.txt`, and `sitemap.xml`. Confirm the
62+
response still carries the CSP, `X-Content-Type-Options`, `X-Frame-Options`,
63+
`Referrer-Policy`, and `Permissions-Policy` headers declared in
64+
`apps/web/next.config.ts`.
65+
66+
After merging, verify that the production deployment points at the merge commit
67+
and that <https://headless-web-pi.vercel.app> serves it. Vercel keeps prior
68+
production deployments available for rollback. Roll back in Vercel, then
69+
revert the faulty commit in git so repository history and production converge.

docs/ROADMAP.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
103103
notarization/stapling, a checksum-pinned Homebrew cask, the verified Linux
104104
bootstrap, release checksums, and a multi-platform GHCR image. These paths
105105
become user-visible with the next tag.
106-
- A Next.js marketing/docs site (`apps/web`) — built, not deployed.
106+
- A Next.js marketing/docs site (`apps/web`) deployed to Vercel from `main`.
107107
- An agent skill (`.agents/skills/headless-computer-use/`) with safety rules,
108108
command reference, and a Docker sandbox wrapper.
109109

@@ -118,9 +118,6 @@ summary|outline|text|actions|full`, `--task`, `--within @rN`, `--budget`)
118118
unimplemented.
119119
- The latest features (capture formats, context pruning) are **unreleased**
120120
no tag since v1.0.2 (2026-07-19).
121-
- No `CLAUDE.md`/`AGENTS.md`; the skill is not auto-discovered by Claude Code.
122-
- The website's benchmark numbers, docs prose, and commands are hand-copied in
123-
three places each and will drift; the site has no deploy pipeline.
124121
- Windows is not supported.
125122
- A list of real code defects (thread-safety on shutdown, oversized `qa
126123
report` responses, `@eN` ref invalidation surprises, host code duplication)
@@ -265,7 +262,8 @@ and drive Headless with zero manual prompting beyond repo checkout.
265262

266263
### Phase 5 — Website and docs as a product surface
267264

268-
- Deploy `apps/web` (Vercel or static export + CDN) with CI.
265+
- Keep the Vercel deployment of `apps/web` reproducible, previewable, and
266+
verified alongside CI.
269267
- Kill the three-copy content drift: docs prose and benchmark numbers come
270268
from single sources (benchmark emits JSON; site imports it; command tables
271269
generated from the CLI) (backlog §F).

docs/roadmap/improvements-backlog.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -408,14 +408,12 @@ Owner-decided scope: package managers, no hosted service.
408408

409409
## §F — Website & docs (Phase 5)
410410

411-
- **F1. Deploy pipeline is invisible to the repo** ([#47](https://github.com/LockInTime/headless/issues/47)) — the site _is_ live at
412-
`https://headless-web-pi.vercel.app` (set as the repo homepage) via Vercel's
413-
GitHub integration, but nothing in the tree records that: no `vercel.json`,
414-
no deploy docs, no preview-URL comment on PRs, and the temporary
415-
`*-pi.vercel.app` hostname suggests no custom domain. Make the deployment
416-
reproducible and reviewable — check in the project config, document the
417-
hosting in `AGENTS.md`, and decide on a domain. Keep the existing headers/CSP
418-
in `next.config.ts`; consider a nonce so `unsafe-inline` can be dropped.
411+
- **F1.** [x] ([#47](https://github.com/LockInTime/headless/issues/47))
412+
The Vercel deployment is repo-visible and verified. Application-local
413+
settings are versioned and linted, the preview and rollback contract is
414+
documented, security headers remain in Next.js, and a Git-backed pull-request
415+
preview was verified before merge. The proven Vercel project alias remains
416+
canonical until the organization publishes a controlled custom domain.
419417
- **F2. Content provenance** ([#48](https://github.com/LockInTime/headless/issues/48)) — ~~benchmark numbers hand-copied in
420418
`app/page.tsx:26-38`, `components/efficiency-chart.tsx:26-31`,
421419
`components/benchmark-chart.tsx:21-26` (+ date in two places); docs prose

0 commit comments

Comments
 (0)