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.