136 lines
5.9 KiB
Markdown
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.
|