Files
2026-05-27 23:29:53 +01:00

136 lines
5.9 KiB
Markdown

# registry-deployment spec — v1
## ADDED Requirements
### Requirement: Public HTTPS endpoint at `ui.suaveplan.com`
The deployment SHALL serve the registry on `https://ui.suaveplan.com`
with a valid X.509 certificate issued by Let's Encrypt. The
certificate SHALL chain to a trusted root, SHALL list
`ui.suaveplan.com` as the subject CN or as a SAN, and SHALL have a
`notAfter` value at least 30 days in the future at any observation
point during normal operation. Caddy 2's automatic ACME flow is
the issuance mechanism; manual cert provisioning is not used.
**Verified by:** `openssl s_client -connect
ui.suaveplan.com:443 -servername ui.suaveplan.com` issued from the
post-deploy verification job; cert validity, subject, and renewal
window are asserted.
### Requirement: Registry JSON conforms to shadcn `registry-item` schema
Every JSON file served under `https://ui.suaveplan.com/r/*.json`
SHALL conform to the shadcn `registry-item` v1 schema: required
fields are `$schema`, `name`, `type`, `files`; optional fields
include `dependencies`, `registryDependencies`, `tailwind`,
`cssVars`. The build step SHALL validate every emitted file
against a Zod schema before the deploy job uploads it; a schema
violation SHALL fail the build.
**Verified by:** the build script's pre-upload validation pass
plus the post-deploy Playwright check that fetches
`/r/index.json` and a sampled subset of per-component manifests
and re-validates each against the same schema.
### Requirement: `npx shadcn@latest add @suaveplan/<component>` works
A consumer running `npx shadcn@latest add @suaveplan/<component>`
in a project whose `components.json` references
`https://ui.suaveplan.com/r/{name}.json` SHALL successfully fetch
and write the component source into the consumer's
`src/components/ui/<component>.tsx` (or the path the consumer's
`components.json` `aliases` resolve to). The fetched file SHALL
be byte-identical to the file declared in the registry manifest.
**Verified by:** the end-to-end contract test that creates a
throwaway Vite project, points it at the public registry, runs
`npx shadcn@latest add @suaveplan/button`, and asserts the
output file exists and exports the `Button` symbol.
### Requirement: Storybook static loads without console errors
A request to `https://ui.suaveplan.com/storybook/` SHALL return
the Storybook static index page. A headless Chromium navigating
to that URL SHALL render the Storybook root element, SHALL log
zero `console.error` messages over a 5-second observation window,
and every subresource request fired during navigation SHALL
return a 2xx status.
**Verified by:** Playwright test in the post-deploy verification
job using `page.on('console', ...)` and `page.on('response', ...)`
to capture the full session.
### Requirement: Deploy is idempotent
Re-running the GitHub Actions deploy workflow against the same
input commit SHALL leave the served file tree byte-identical to
the prior deploy. Successive deploys with no source changes SHALL
NOT thrash the live tree. The workflow's `concurrency:` block
SHALL serialise simultaneous runs of the same workflow on the same
ref so back-to-back pushes never race.
**Verified by:** checksum comparison between the pre- and
post-deploy snapshots in the verification job, plus a manual
re-run of the workflow to confirm the second run is a no-op
beyond the snapshot copy.
### Requirement: Rollback is one command
The previous registry tree SHALL be retained at
`/srv/registry/.previous/<utc-timestamp>/` after every deploy.
A single command (`bash ops/registry/rollback.sh`) executed on
the OVH host SHALL atomically swap the live tree with the most
recent snapshot. A second invocation immediately after SHALL roll
forward to what was previously live.
**Verified by:** the runbook test in the verification gate, which
runs the rollback once, confirms the live registry now serves the
prior contents, then rolls forward and confirms current contents
are restored.
### Requirement: TLS 1.2 minimum, HSTS, security headers
The Caddy configuration SHALL pin `protocols tls1.2 tls1.3` —
TLS 1.0 and TLS 1.1 are rejected. Every response from the domain
SHALL carry the headers `Strict-Transport-Security: max-age=31536000;
includeSubDomains; preload`, `X-Content-Type-Options: nosniff`,
`Referrer-Policy: no-referrer`, and `Permissions-Policy:
interest-cohort=()`. Storybook responses additionally carry a
restrictive Content-Security-Policy.
**Verified by:** `curl -sI https://ui.suaveplan.com/r/index.json`
and `curl -sI https://ui.suaveplan.com/storybook/` in the
post-deploy job; assertions on each required header. TLS protocol
floor verified by `openssl s_client -tls1_1` returning a handshake
failure.
### Requirement: No secrets in committed files
No file under `ops/registry/` or `.github/workflows/` SHALL
contain the value of any secret (SSH private key, host name, user
name, API token). Placeholders (`__OVH_HOST__`, `__SECRET__`)
appear in `ops/registry/.env.example`; real values live in GitHub
Actions secrets named `OVH_SSH_PRIVATE_KEY`, `OVH_SSH_HOST`,
`OVH_SSH_USER` and are documented in `ops/registry/README.md` by
name only. The runbook's secret-rotation section names the
secrets but never their values.
**Verified by:** `git grep -nE
'(BEGIN OPENSSH PRIVATE KEY|ssh-(rsa|ed25519) AAAA)'` returning
zero matches in the change set; manual review of every committed
file under `ops/registry/` and the workflow file.
### Requirement: CI deploys as a scoped non-root user
The GitHub Actions workflow SHALL connect to OVH as
`deploy-registry`, NOT as `root`. The `deploy-registry` user's
sudoers rule SHALL permit only `rsync` writes into
`/srv/registry/`; no shell, no general sudo. The user's
`ssh root@ovh` access remains available for ad-hoc admin tasks
and is intentionally not used by CI to keep the CI blast radius
minimal.
**Verified by:** review of the runbook's user-creation section
and the sudoers rule it documents; review of the workflow file
to confirm the SSH connection target is the scoped user.