init
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# simplepages
|
||||
|
||||
A static-site deployment target for CI. Your build job pushes a directory; the
|
||||
site switches over to it in one step.
|
||||
|
||||
The problem it exists to solve is the one `rsync` has: rsync writes the new
|
||||
build into the live directory file by file, and for the length of that transfer
|
||||
visitors get a **half-updated site** — new HTML asking for JS that is not there
|
||||
yet, hashed asset names that do not resolve, images 404ing. simplepages closes
|
||||
that window. A deployment is uploaded and verified in full first, and only then
|
||||
does one atomic pointer store make it live.
|
||||
|
||||
**The guarantee:** every request sees the whole old deployment or the whole new
|
||||
one, never a mixture. That includes requests already in flight — a download that
|
||||
started before the switch keeps reading the deployment it started on, all the
|
||||
way to its last byte, even if that deployment is deleted meanwhile.
|
||||
|
||||
Rolling back is the same operation in reverse, so it costs one pointer store
|
||||
too: the old deployment's files are still on disk.
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
CI runner pages-server visitor
|
||||
───────── ──────────── ───────
|
||||
pages deploy ./dist ──manifest──▶ "I am missing 11 of these 142"
|
||||
──blobs─────▶ content-addressed store
|
||||
──finalize──▶ assemble tree, fsync, rename
|
||||
──activate──▶ ┌──────────────────────────┐
|
||||
│ atomic.Pointer[Deployment]│◀── GET /~demo/
|
||||
└──────────────────────────┘
|
||||
$WEBROOT/~demo -> …/dpl_a1b2…
|
||||
```
|
||||
|
||||
Uploads are **content-addressed and incremental**: the CLI sends a manifest of
|
||||
paths, SHA-256 digests and sizes, and the server answers with the digests it
|
||||
does not already have. A rebuild that changes one file uploads one file, no
|
||||
matter how big the site is, and identical files are stored once across every
|
||||
deployment and every project.
|
||||
|
||||
`$WEBROOT/~PROJECT` is kept pointing at the live deployment's directory so nginx
|
||||
or any other external consumer can serve it directly, but the server serves the
|
||||
site itself and does not depend on that symlink being correct.
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
make build # -> bin/pages-server and bin/pages
|
||||
|
||||
export PAGES_DATA=$(mktemp -d) PAGES_WEBROOT=$(mktemp -d)
|
||||
bin/pages-server --data-dir "$PAGES_DATA" --webroot "$PAGES_WEBROOT" &
|
||||
```
|
||||
|
||||
On first start the server mints an admin key and writes the token to
|
||||
`$DATA_DIR/bootstrap-token`, mode 0600. That is the only place it ever writes a
|
||||
token to disk; delete the file once you have taken the token.
|
||||
|
||||
```sh
|
||||
export PAGES_SERVER=http://127.0.0.1:8081
|
||||
export PAGES_TOKEN=$(cat "$PAGES_DATA/bootstrap-token")
|
||||
|
||||
bin/pages project create demo
|
||||
export PAGES_TOKEN=$(bin/pages key create --project demo -o json | jq -r .token)
|
||||
|
||||
mkdir -p dist/assets
|
||||
echo '<h1>v1</h1><script src="assets/app.js"></script>' > dist/index.html
|
||||
echo 'console.log(1)' > dist/assets/app.js
|
||||
|
||||
bin/pages deploy ./dist --project demo
|
||||
curl -s localhost:8080/~demo/ # <h1>v1</h1>…
|
||||
```
|
||||
|
||||
Change one file and deploy again — the CLI reports how much the manifest
|
||||
negotiation saved:
|
||||
|
||||
```
|
||||
142 files, 3.1 MiB; 1 new blob, 402 KiB to upload
|
||||
```
|
||||
|
||||
Give CI a **project-scoped** key, never the admin one. A project key can create,
|
||||
upload, finalize, activate and delete deployments in its own project and read
|
||||
that project's settings, and nothing else anywhere.
|
||||
|
||||
## Two binaries
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `pages-server` | The server: management API on `--api-listen` (default `127.0.0.1:8081`), static content on `--listen` (default `:8080`). Storage is SQLite plus a content-addressed blob store under `--data-dir`. |
|
||||
| `pages` | The CLI. Pure HTTP client — no SQLite, no cgo, no server packages linked in. It is downloaded by CI runners, so keeping it that way is a feature, and `cmd/pages/deps_test.go` fails the build if a server dependency creeps in. |
|
||||
|
||||
## CLI
|
||||
|
||||
```
|
||||
pages deploy <dir> upload a directory and switch to it
|
||||
pages project create|list|show|update|delete
|
||||
pages deployment list|show|activate|delete # activate an older one = rollback
|
||||
pages key create|list|revoke
|
||||
pages whoami which key am I actually using
|
||||
pages system info|gc|fsck admin only
|
||||
pages config show|set|path
|
||||
pages version
|
||||
```
|
||||
|
||||
Settings resolve flag → `PAGES_*` environment variable → config file →
|
||||
built-in default. In CI, pass the token as `PAGES_TOKEN` or `--token-file`;
|
||||
`--token` puts the secret in argv, which on a shared runner is world-readable
|
||||
through `/proc/<pid>/cmdline`.
|
||||
|
||||
Every `pages deployment` command works with a project key, so a CI job can roll
|
||||
its own project back without an admin credential.
|
||||
|
||||
## Building and testing
|
||||
|
||||
```sh
|
||||
make build # both binaries, -trimpath -ldflags '-s -w'
|
||||
make test # go test ./...
|
||||
make race # go test -race ./...
|
||||
make atomicity # the switch-is-atomic proof, -race -count=20
|
||||
make vet fmt tidy
|
||||
```
|
||||
|
||||
`make atomicity` is the test that guards the one thing this project sells: 64
|
||||
readers hammer three files plus a 512 KiB body while a writer activates 50
|
||||
deployments back to back, and any reader that observes two versions at once
|
||||
fails the run.
|
||||
|
||||
## Security
|
||||
|
||||
Read [`docs/operations.md`](docs/operations.md) §7 before hosting anything you
|
||||
care about. The headlines:
|
||||
|
||||
- **Path routing is not a security boundary.** `/~a/` and `/~b/` share one
|
||||
origin, so project A's JavaScript can read project B's files, cookies and
|
||||
`localStorage`, and can register a service worker that intercepts *every*
|
||||
project on the host. This is fine for mutually trusting projects, which is
|
||||
what v1 targets. Hosting untrusted sites needs one domain per project.
|
||||
- Client-supplied hashes are never trusted: the server rehashes every uploaded
|
||||
byte and rejects a mismatch, which is what makes cross-project dedup safe.
|
||||
- Tokens appear once, at creation. They are never logged, never accepted in a
|
||||
query string, and never included in an error message.
|
||||
- Blobs and assembled files are `0444`, directories `0755`; upload permissions
|
||||
are ignored entirely, so a setuid bit cannot reach disk.
|
||||
- A project key can tell whether a digest it already knows exists on the server
|
||||
(a blob existence oracle). Known, accepted, and documented in §7.
|
||||
|
||||
## Documentation
|
||||
|
||||
[`docs/operations.md`](docs/operations.md) — installation, configuration,
|
||||
deploying, garbage collection and retention, the security model and its limits,
|
||||
scaling limits, troubleshooting and backups.
|
||||
|
||||
[`AGENTS.md`](AGENTS.md) — repository map, the invariants that hold the
|
||||
guarantee up, and what to run before calling a change done.
|
||||
Reference in New Issue
Block a user