Files
suaveplan/openspec/changes/react-ui-registry-deployment/specs/registry-deployment/spec.md
2026-05-27 23:29:53 +01:00

5.9 KiB

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.